1'use strict';
2
3var HTML = require('../common/html');
4
5//Aliases
6var $ = HTML.TAG_NAMES,
7    NS = HTML.NAMESPACES;
8
9//Element utils
10
11//OPTIMIZATION: Integer comparisons are low-cost, so we can use very fast tag name length filters here.
12//It's faster than using dictionary.
13function isImpliedEndTagRequired(tn) {
14    switch (tn.length) {
15        case 1:
16            return tn === $.P;
17
18        case 2:
19            return tn === $.RP || tn === $.RT || tn === $.DD || tn === $.DT || tn === $.LI;
20
21        case 6:
22            return tn === $.OPTION;
23
24        case 8:
25            return tn === $.OPTGROUP;
26    }
27
28    return false;
29}
30
31function isScopingElement(tn, ns) {
32    switch (tn.length) {
33        case 2:
34            if (tn === $.TD || tn === $.TH)
35                return ns === NS.HTML;
36
37            else if (tn === $.MI || tn === $.MO || tn == $.MN || tn === $.MS)
38                return ns === NS.MATHML;
39
40            break;
41
42        case 4:
43            if (tn === $.HTML)
44                return ns === NS.HTML;
45
46            else if (tn === $.DESC)
47                return ns === NS.SVG;
48
49            break;
50
51        case 5:
52            if (tn === $.TABLE)
53                return ns === NS.HTML;
54
55            else if (tn === $.MTEXT)
56                return ns === NS.MATHML;
57
58            else if (tn === $.TITLE)
59                return ns === NS.SVG;
60
61            break;
62
63        case 6:
64            return (tn === $.APPLET || tn === $.OBJECT) && ns === NS.HTML;
65
66        case 7:
67            return (tn === $.CAPTION || tn === $.MARQUEE) && ns === NS.HTML;
68
69        case 8:
70            return tn === $.TEMPLATE && ns === NS.HTML;
71
72        case 13:
73            return tn === $.FOREIGN_OBJECT && ns === NS.SVG;
74
75        case 14:
76            return tn === $.ANNOTATION_XML && ns === NS.MATHML;
77    }
78
79    return false;
80}
81
82//Stack of open elements
83var OpenElementStack = module.exports = function (document, treeAdapter) {
84    this.stackTop = -1;
85    this.items = [];
86    this.current = document;
87    this.currentTagName = null;
88    this.currentTmplContent = null;
89    this.tmplCount = 0;
90    this.treeAdapter = treeAdapter;
91};
92
93//Index of element
94OpenElementStack.prototype._indexOf = function (element) {
95    var idx = -1;
96
97    for (var i = this.stackTop; i >= 0; i--) {
98        if (this.items[i] === element) {
99            idx = i;
100            break;
101        }
102    }
103    return idx;
104};
105
106//Update current element
107OpenElementStack.prototype._isInTemplate = function () {
108    if (this.currentTagName !== $.TEMPLATE)
109        return false;
110
111    return this.treeAdapter.getNamespaceURI(this.current) === NS.HTML;
112};
113
114OpenElementStack.prototype._updateCurrentElement = function () {
115    this.current = this.items[this.stackTop];
116    this.currentTagName = this.current && this.treeAdapter.getTagName(this.current);
117
118    this.currentTmplContent = this._isInTemplate() ? this.treeAdapter.getChildNodes(this.current)[0] : null;
119};
120
121//Mutations
122OpenElementStack.prototype.push = function (element) {
123    this.items[++this.stackTop] = element;
124    this._updateCurrentElement();
125
126    if (this._isInTemplate())
127        this.tmplCount++;
128
129};
130
131OpenElementStack.prototype.pop = function () {
132    this.stackTop--;
133
134    if (this.tmplCount > 0 && this._isInTemplate())
135        this.tmplCount--;
136
137    this._updateCurrentElement();
138};
139
140OpenElementStack.prototype.replace = function (oldElement, newElement) {
141    var idx = this._indexOf(oldElement);
142    this.items[idx] = newElement;
143
144    if (idx === this.stackTop)
145        this._updateCurrentElement();
146};
147
148OpenElementStack.prototype.insertAfter = function (referenceElement, newElement) {
149    var insertionIdx = this._indexOf(referenceElement) + 1;
150
151    this.items.splice(insertionIdx, 0, newElement);
152
153    if (insertionIdx == ++this.stackTop)
154        this._updateCurrentElement();
155};
156
157OpenElementStack.prototype.popUntilTagNamePopped = function (tagName) {
158    while (this.stackTop > -1) {
159        var tn = this.currentTagName;
160
161        this.pop();
162
163        if (tn === tagName)
164            break;
165    }
166};
167
168OpenElementStack.prototype.popUntilTemplatePopped = function () {
169    while (this.stackTop > -1) {
170        var tn = this.currentTagName,
171            ns = this.treeAdapter.getNamespaceURI(this.current);
172
173        this.pop();
174
175        if (tn === $.TEMPLATE && ns === NS.HTML)
176            break;
177    }
178};
179
180OpenElementStack.prototype.popUntilElementPopped = function (element) {
181    while (this.stackTop > -1) {
182        var poppedElement = this.current;
183
184        this.pop();
185
186        if (poppedElement === element)
187            break;
188    }
189};
190
191OpenElementStack.prototype.popUntilNumberedHeaderPopped = function () {
192    while (this.stackTop > -1) {
193        var tn = this.currentTagName;
194
195        this.pop();
196
197        if (tn === $.H1 || tn === $.H2 || tn === $.H3 || tn === $.H4 || tn === $.H5 || tn === $.H6)
198            break;
199    }
200};
201
202OpenElementStack.prototype.popAllUpToHtmlElement = function () {
203    //NOTE: here we assume that root <html> element is always first in the open element stack, so
204    //we perform this fast stack clean up.
205    this.stackTop = 0;
206    this._updateCurrentElement();
207};
208
209OpenElementStack.prototype.clearBackToTableContext = function () {
210    while (this.currentTagName !== $.TABLE && this.currentTagName !== $.TEMPLATE && this.currentTagName !== $.HTML)
211        this.pop();
212};
213
214OpenElementStack.prototype.clearBackToTableBodyContext = function () {
215    while (this.currentTagName !== $.TBODY && this.currentTagName !== $.TFOOT &&
216           this.currentTagName !== $.THEAD && this.currentTagName !== $.TEMPLATE &&
217           this.currentTagName !== $.HTML) {
218        this.pop();
219    }
220};
221
222OpenElementStack.prototype.clearBackToTableRowContext = function () {
223    while (this.currentTagName !== $.TR && this.currentTagName !== $.TEMPLATE && this.currentTagName !== $.HTML)
224        this.pop();
225};
226
227OpenElementStack.prototype.remove = function (element) {
228    for (var i = this.stackTop; i >= 0; i--) {
229        if (this.items[i] === element) {
230            this.items.splice(i, 1);
231            this.stackTop--;
232            this._updateCurrentElement();
233            break;
234        }
235    }
236};
237
238//Search
239OpenElementStack.prototype.tryPeekProperlyNestedBodyElement = function () {
240    //Properly nested <body> element (should be second element in stack).
241    var element = this.items[1];
242    return element && this.treeAdapter.getTagName(element) === $.BODY ? element : null;
243};
244
245OpenElementStack.prototype.contains = function (element) {
246    return this._indexOf(element) > -1;
247};
248
249OpenElementStack.prototype.getCommonAncestor = function (element) {
250    var elementIdx = this._indexOf(element);
251
252    return --elementIdx >= 0 ? this.items[elementIdx] : null;
253};
254
255OpenElementStack.prototype.isRootHtmlElementCurrent = function () {
256    return this.stackTop === 0 && this.currentTagName === $.HTML;
257};
258
259//Element in scope
260OpenElementStack.prototype.hasInScope = function (tagName) {
261    for (var i = this.stackTop; i >= 0; i--) {
262        var tn = this.treeAdapter.getTagName(this.items[i]);
263
264        if (tn === tagName)
265            return true;
266
267        var ns = this.treeAdapter.getNamespaceURI(this.items[i]);
268
269        if (isScopingElement(tn, ns))
270            return false;
271    }
272
273    return true;
274};
275
276OpenElementStack.prototype.hasNumberedHeaderInScope = function () {
277    for (var i = this.stackTop; i >= 0; i--) {
278        var tn = this.treeAdapter.getTagName(this.items[i]);
279
280        if (tn === $.H1 || tn === $.H2 || tn === $.H3 || tn === $.H4 || tn === $.H5 || tn === $.H6)
281            return true;
282
283        if (isScopingElement(tn, this.treeAdapter.getNamespaceURI(this.items[i])))
284            return false;
285    }
286
287    return true;
288};
289
290OpenElementStack.prototype.hasInListItemScope = function (tagName) {
291    for (var i = this.stackTop; i >= 0; i--) {
292        var tn = this.treeAdapter.getTagName(this.items[i]);
293
294        if (tn === tagName)
295            return true;
296
297        var ns = this.treeAdapter.getNamespaceURI(this.items[i]);
298
299        if (((tn === $.UL || tn === $.OL) && ns === NS.HTML) || isScopingElement(tn, ns))
300            return false;
301    }
302
303    return true;
304};
305
306OpenElementStack.prototype.hasInButtonScope = function (tagName) {
307    for (var i = this.stackTop; i >= 0; i--) {
308        var tn = this.treeAdapter.getTagName(this.items[i]);
309
310        if (tn === tagName)
311            return true;
312
313        var ns = this.treeAdapter.getNamespaceURI(this.items[i]);
314
315        if ((tn === $.BUTTON && ns === NS.HTML) || isScopingElement(tn, ns))
316            return false;
317    }
318
319    return true;
320};
321
322OpenElementStack.prototype.hasInTableScope = function (tagName) {
323    for (var i = this.stackTop; i >= 0; i--) {
324        var tn = this.treeAdapter.getTagName(this.items[i]);
325
326        if (tn === tagName)
327            return true;
328
329        var ns = this.treeAdapter.getNamespaceURI(this.items[i]);
330
331        if ((tn === $.TABLE || tn === $.TEMPLATE || tn === $.HTML) && ns === NS.HTML)
332            return false;
333    }
334
335    return true;
336};
337
338OpenElementStack.prototype.hasTableBodyContextInTableScope = function () {
339    for (var i = this.stackTop; i >= 0; i--) {
340        var tn = this.treeAdapter.getTagName(this.items[i]);
341
342        if (tn === $.TBODY || tn === $.THEAD || tn === $.TFOOT)
343            return true;
344
345        var ns = this.treeAdapter.getNamespaceURI(this.items[i]);
346
347        if ((tn === $.TABLE || tn === $.HTML) && ns === NS.HTML)
348            return false;
349    }
350
351    return true;
352};
353
354OpenElementStack.prototype.hasInSelectScope = function (tagName) {
355    for (var i = this.stackTop; i >= 0; i--) {
356        var tn = this.treeAdapter.getTagName(this.items[i]);
357
358        if (tn === tagName)
359            return true;
360
361        var ns = this.treeAdapter.getNamespaceURI(this.items[i]);
362
363        if (tn !== $.OPTION && tn !== $.OPTGROUP && ns === NS.HTML)
364            return false;
365    }
366
367    return true;
368};
369
370//Implied end tags
371OpenElementStack.prototype.generateImpliedEndTags = function () {
372    while (isImpliedEndTagRequired(this.currentTagName))
373        this.pop();
374};
375
376OpenElementStack.prototype.generateImpliedEndTagsWithExclusion = function (exclusionTagName) {
377    while (isImpliedEndTagRequired(this.currentTagName) && this.currentTagName !== exclusionTagName)
378        this.pop();
379};
380