1 | 'use strict';
|
---|
2 |
|
---|
3 | const HTML = require('../common/html');
|
---|
4 |
|
---|
5 | //Aliases
|
---|
6 | const $ = HTML.TAG_NAMES;
|
---|
7 | const 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.
|
---|
13 | function isImpliedEndTagRequired(tn) {
|
---|
14 | switch (tn.length) {
|
---|
15 | case 1:
|
---|
16 | return tn === $.P;
|
---|
17 |
|
---|
18 | case 2:
|
---|
19 | return tn === $.RB || tn === $.RP || tn === $.RT || tn === $.DD || tn === $.DT || tn === $.LI;
|
---|
20 |
|
---|
21 | case 3:
|
---|
22 | return tn === $.RTC;
|
---|
23 |
|
---|
24 | case 6:
|
---|
25 | return tn === $.OPTION;
|
---|
26 |
|
---|
27 | case 8:
|
---|
28 | return tn === $.OPTGROUP;
|
---|
29 | }
|
---|
30 |
|
---|
31 | return false;
|
---|
32 | }
|
---|
33 |
|
---|
34 | function isImpliedEndTagRequiredThoroughly(tn) {
|
---|
35 | switch (tn.length) {
|
---|
36 | case 1:
|
---|
37 | return tn === $.P;
|
---|
38 |
|
---|
39 | case 2:
|
---|
40 | return (
|
---|
41 | tn === $.RB ||
|
---|
42 | tn === $.RP ||
|
---|
43 | tn === $.RT ||
|
---|
44 | tn === $.DD ||
|
---|
45 | tn === $.DT ||
|
---|
46 | tn === $.LI ||
|
---|
47 | tn === $.TD ||
|
---|
48 | tn === $.TH ||
|
---|
49 | tn === $.TR
|
---|
50 | );
|
---|
51 |
|
---|
52 | case 3:
|
---|
53 | return tn === $.RTC;
|
---|
54 |
|
---|
55 | case 5:
|
---|
56 | return tn === $.TBODY || tn === $.TFOOT || tn === $.THEAD;
|
---|
57 |
|
---|
58 | case 6:
|
---|
59 | return tn === $.OPTION;
|
---|
60 |
|
---|
61 | case 7:
|
---|
62 | return tn === $.CAPTION;
|
---|
63 |
|
---|
64 | case 8:
|
---|
65 | return tn === $.OPTGROUP || tn === $.COLGROUP;
|
---|
66 | }
|
---|
67 |
|
---|
68 | return false;
|
---|
69 | }
|
---|
70 |
|
---|
71 | function isScopingElement(tn, ns) {
|
---|
72 | switch (tn.length) {
|
---|
73 | case 2:
|
---|
74 | if (tn === $.TD || tn === $.TH) {
|
---|
75 | return ns === NS.HTML;
|
---|
76 | } else if (tn === $.MI || tn === $.MO || tn === $.MN || tn === $.MS) {
|
---|
77 | return ns === NS.MATHML;
|
---|
78 | }
|
---|
79 |
|
---|
80 | break;
|
---|
81 |
|
---|
82 | case 4:
|
---|
83 | if (tn === $.HTML) {
|
---|
84 | return ns === NS.HTML;
|
---|
85 | } else if (tn === $.DESC) {
|
---|
86 | return ns === NS.SVG;
|
---|
87 | }
|
---|
88 |
|
---|
89 | break;
|
---|
90 |
|
---|
91 | case 5:
|
---|
92 | if (tn === $.TABLE) {
|
---|
93 | return ns === NS.HTML;
|
---|
94 | } else if (tn === $.MTEXT) {
|
---|
95 | return ns === NS.MATHML;
|
---|
96 | } else if (tn === $.TITLE) {
|
---|
97 | return ns === NS.SVG;
|
---|
98 | }
|
---|
99 |
|
---|
100 | break;
|
---|
101 |
|
---|
102 | case 6:
|
---|
103 | return (tn === $.APPLET || tn === $.OBJECT) && ns === NS.HTML;
|
---|
104 |
|
---|
105 | case 7:
|
---|
106 | return (tn === $.CAPTION || tn === $.MARQUEE) && ns === NS.HTML;
|
---|
107 |
|
---|
108 | case 8:
|
---|
109 | return tn === $.TEMPLATE && ns === NS.HTML;
|
---|
110 |
|
---|
111 | case 13:
|
---|
112 | return tn === $.FOREIGN_OBJECT && ns === NS.SVG;
|
---|
113 |
|
---|
114 | case 14:
|
---|
115 | return tn === $.ANNOTATION_XML && ns === NS.MATHML;
|
---|
116 | }
|
---|
117 |
|
---|
118 | return false;
|
---|
119 | }
|
---|
120 |
|
---|
121 | //Stack of open elements
|
---|
122 | class OpenElementStack {
|
---|
123 | constructor(document, treeAdapter) {
|
---|
124 | this.stackTop = -1;
|
---|
125 | this.items = [];
|
---|
126 | this.current = document;
|
---|
127 | this.currentTagName = null;
|
---|
128 | this.currentTmplContent = null;
|
---|
129 | this.tmplCount = 0;
|
---|
130 | this.treeAdapter = treeAdapter;
|
---|
131 | }
|
---|
132 |
|
---|
133 | //Index of element
|
---|
134 | _indexOf(element) {
|
---|
135 | let idx = -1;
|
---|
136 |
|
---|
137 | for (let i = this.stackTop; i >= 0; i--) {
|
---|
138 | if (this.items[i] === element) {
|
---|
139 | idx = i;
|
---|
140 | break;
|
---|
141 | }
|
---|
142 | }
|
---|
143 | return idx;
|
---|
144 | }
|
---|
145 |
|
---|
146 | //Update current element
|
---|
147 | _isInTemplate() {
|
---|
148 | return this.currentTagName === $.TEMPLATE && this.treeAdapter.getNamespaceURI(this.current) === NS.HTML;
|
---|
149 | }
|
---|
150 |
|
---|
151 | _updateCurrentElement() {
|
---|
152 | this.current = this.items[this.stackTop];
|
---|
153 | this.currentTagName = this.current && this.treeAdapter.getTagName(this.current);
|
---|
154 |
|
---|
155 | this.currentTmplContent = this._isInTemplate() ? this.treeAdapter.getTemplateContent(this.current) : null;
|
---|
156 | }
|
---|
157 |
|
---|
158 | //Mutations
|
---|
159 | push(element) {
|
---|
160 | this.items[++this.stackTop] = element;
|
---|
161 | this._updateCurrentElement();
|
---|
162 |
|
---|
163 | if (this._isInTemplate()) {
|
---|
164 | this.tmplCount++;
|
---|
165 | }
|
---|
166 | }
|
---|
167 |
|
---|
168 | pop() {
|
---|
169 | this.stackTop--;
|
---|
170 |
|
---|
171 | if (this.tmplCount > 0 && this._isInTemplate()) {
|
---|
172 | this.tmplCount--;
|
---|
173 | }
|
---|
174 |
|
---|
175 | this._updateCurrentElement();
|
---|
176 | }
|
---|
177 |
|
---|
178 | replace(oldElement, newElement) {
|
---|
179 | const idx = this._indexOf(oldElement);
|
---|
180 |
|
---|
181 | this.items[idx] = newElement;
|
---|
182 |
|
---|
183 | if (idx === this.stackTop) {
|
---|
184 | this._updateCurrentElement();
|
---|
185 | }
|
---|
186 | }
|
---|
187 |
|
---|
188 | insertAfter(referenceElement, newElement) {
|
---|
189 | const insertionIdx = this._indexOf(referenceElement) + 1;
|
---|
190 |
|
---|
191 | this.items.splice(insertionIdx, 0, newElement);
|
---|
192 |
|
---|
193 | if (insertionIdx === ++this.stackTop) {
|
---|
194 | this._updateCurrentElement();
|
---|
195 | }
|
---|
196 | }
|
---|
197 |
|
---|
198 | popUntilTagNamePopped(tagName) {
|
---|
199 | while (this.stackTop > -1) {
|
---|
200 | const tn = this.currentTagName;
|
---|
201 | const ns = this.treeAdapter.getNamespaceURI(this.current);
|
---|
202 |
|
---|
203 | this.pop();
|
---|
204 |
|
---|
205 | if (tn === tagName && ns === NS.HTML) {
|
---|
206 | break;
|
---|
207 | }
|
---|
208 | }
|
---|
209 | }
|
---|
210 |
|
---|
211 | popUntilElementPopped(element) {
|
---|
212 | while (this.stackTop > -1) {
|
---|
213 | const poppedElement = this.current;
|
---|
214 |
|
---|
215 | this.pop();
|
---|
216 |
|
---|
217 | if (poppedElement === element) {
|
---|
218 | break;
|
---|
219 | }
|
---|
220 | }
|
---|
221 | }
|
---|
222 |
|
---|
223 | popUntilNumberedHeaderPopped() {
|
---|
224 | while (this.stackTop > -1) {
|
---|
225 | const tn = this.currentTagName;
|
---|
226 | const ns = this.treeAdapter.getNamespaceURI(this.current);
|
---|
227 |
|
---|
228 | this.pop();
|
---|
229 |
|
---|
230 | if (
|
---|
231 | tn === $.H1 ||
|
---|
232 | tn === $.H2 ||
|
---|
233 | tn === $.H3 ||
|
---|
234 | tn === $.H4 ||
|
---|
235 | tn === $.H5 ||
|
---|
236 | (tn === $.H6 && ns === NS.HTML)
|
---|
237 | ) {
|
---|
238 | break;
|
---|
239 | }
|
---|
240 | }
|
---|
241 | }
|
---|
242 |
|
---|
243 | popUntilTableCellPopped() {
|
---|
244 | while (this.stackTop > -1) {
|
---|
245 | const tn = this.currentTagName;
|
---|
246 | const ns = this.treeAdapter.getNamespaceURI(this.current);
|
---|
247 |
|
---|
248 | this.pop();
|
---|
249 |
|
---|
250 | if (tn === $.TD || (tn === $.TH && ns === NS.HTML)) {
|
---|
251 | break;
|
---|
252 | }
|
---|
253 | }
|
---|
254 | }
|
---|
255 |
|
---|
256 | popAllUpToHtmlElement() {
|
---|
257 | //NOTE: here we assume that root <html> element is always first in the open element stack, so
|
---|
258 | //we perform this fast stack clean up.
|
---|
259 | this.stackTop = 0;
|
---|
260 | this._updateCurrentElement();
|
---|
261 | }
|
---|
262 |
|
---|
263 | clearBackToTableContext() {
|
---|
264 | while (
|
---|
265 | (this.currentTagName !== $.TABLE && this.currentTagName !== $.TEMPLATE && this.currentTagName !== $.HTML) ||
|
---|
266 | this.treeAdapter.getNamespaceURI(this.current) !== NS.HTML
|
---|
267 | ) {
|
---|
268 | this.pop();
|
---|
269 | }
|
---|
270 | }
|
---|
271 |
|
---|
272 | clearBackToTableBodyContext() {
|
---|
273 | while (
|
---|
274 | (this.currentTagName !== $.TBODY &&
|
---|
275 | this.currentTagName !== $.TFOOT &&
|
---|
276 | this.currentTagName !== $.THEAD &&
|
---|
277 | this.currentTagName !== $.TEMPLATE &&
|
---|
278 | this.currentTagName !== $.HTML) ||
|
---|
279 | this.treeAdapter.getNamespaceURI(this.current) !== NS.HTML
|
---|
280 | ) {
|
---|
281 | this.pop();
|
---|
282 | }
|
---|
283 | }
|
---|
284 |
|
---|
285 | clearBackToTableRowContext() {
|
---|
286 | while (
|
---|
287 | (this.currentTagName !== $.TR && this.currentTagName !== $.TEMPLATE && this.currentTagName !== $.HTML) ||
|
---|
288 | this.treeAdapter.getNamespaceURI(this.current) !== NS.HTML
|
---|
289 | ) {
|
---|
290 | this.pop();
|
---|
291 | }
|
---|
292 | }
|
---|
293 |
|
---|
294 | remove(element) {
|
---|
295 | for (let i = this.stackTop; i >= 0; i--) {
|
---|
296 | if (this.items[i] === element) {
|
---|
297 | this.items.splice(i, 1);
|
---|
298 | this.stackTop--;
|
---|
299 | this._updateCurrentElement();
|
---|
300 | break;
|
---|
301 | }
|
---|
302 | }
|
---|
303 | }
|
---|
304 |
|
---|
305 | //Search
|
---|
306 | tryPeekProperlyNestedBodyElement() {
|
---|
307 | //Properly nested <body> element (should be second element in stack).
|
---|
308 | const element = this.items[1];
|
---|
309 |
|
---|
310 | return element && this.treeAdapter.getTagName(element) === $.BODY ? element : null;
|
---|
311 | }
|
---|
312 |
|
---|
313 | contains(element) {
|
---|
314 | return this._indexOf(element) > -1;
|
---|
315 | }
|
---|
316 |
|
---|
317 | getCommonAncestor(element) {
|
---|
318 | let elementIdx = this._indexOf(element);
|
---|
319 |
|
---|
320 | return --elementIdx >= 0 ? this.items[elementIdx] : null;
|
---|
321 | }
|
---|
322 |
|
---|
323 | isRootHtmlElementCurrent() {
|
---|
324 | return this.stackTop === 0 && this.currentTagName === $.HTML;
|
---|
325 | }
|
---|
326 |
|
---|
327 | //Element in scope
|
---|
328 | hasInScope(tagName) {
|
---|
329 | for (let i = this.stackTop; i >= 0; i--) {
|
---|
330 | const tn = this.treeAdapter.getTagName(this.items[i]);
|
---|
331 | const ns = this.treeAdapter.getNamespaceURI(this.items[i]);
|
---|
332 |
|
---|
333 | if (tn === tagName && ns === NS.HTML) {
|
---|
334 | return true;
|
---|
335 | }
|
---|
336 |
|
---|
337 | if (isScopingElement(tn, ns)) {
|
---|
338 | return false;
|
---|
339 | }
|
---|
340 | }
|
---|
341 |
|
---|
342 | return true;
|
---|
343 | }
|
---|
344 |
|
---|
345 | hasNumberedHeaderInScope() {
|
---|
346 | for (let i = this.stackTop; i >= 0; i--) {
|
---|
347 | const tn = this.treeAdapter.getTagName(this.items[i]);
|
---|
348 | const ns = this.treeAdapter.getNamespaceURI(this.items[i]);
|
---|
349 |
|
---|
350 | if (
|
---|
351 | (tn === $.H1 || tn === $.H2 || tn === $.H3 || tn === $.H4 || tn === $.H5 || tn === $.H6) &&
|
---|
352 | ns === NS.HTML
|
---|
353 | ) {
|
---|
354 | return true;
|
---|
355 | }
|
---|
356 |
|
---|
357 | if (isScopingElement(tn, ns)) {
|
---|
358 | return false;
|
---|
359 | }
|
---|
360 | }
|
---|
361 |
|
---|
362 | return true;
|
---|
363 | }
|
---|
364 |
|
---|
365 | hasInListItemScope(tagName) {
|
---|
366 | for (let i = this.stackTop; i >= 0; i--) {
|
---|
367 | const tn = this.treeAdapter.getTagName(this.items[i]);
|
---|
368 | const ns = this.treeAdapter.getNamespaceURI(this.items[i]);
|
---|
369 |
|
---|
370 | if (tn === tagName && ns === NS.HTML) {
|
---|
371 | return true;
|
---|
372 | }
|
---|
373 |
|
---|
374 | if (((tn === $.UL || tn === $.OL) && ns === NS.HTML) || isScopingElement(tn, ns)) {
|
---|
375 | return false;
|
---|
376 | }
|
---|
377 | }
|
---|
378 |
|
---|
379 | return true;
|
---|
380 | }
|
---|
381 |
|
---|
382 | hasInButtonScope(tagName) {
|
---|
383 | for (let i = this.stackTop; i >= 0; i--) {
|
---|
384 | const tn = this.treeAdapter.getTagName(this.items[i]);
|
---|
385 | const ns = this.treeAdapter.getNamespaceURI(this.items[i]);
|
---|
386 |
|
---|
387 | if (tn === tagName && ns === NS.HTML) {
|
---|
388 | return true;
|
---|
389 | }
|
---|
390 |
|
---|
391 | if ((tn === $.BUTTON && ns === NS.HTML) || isScopingElement(tn, ns)) {
|
---|
392 | return false;
|
---|
393 | }
|
---|
394 | }
|
---|
395 |
|
---|
396 | return true;
|
---|
397 | }
|
---|
398 |
|
---|
399 | hasInTableScope(tagName) {
|
---|
400 | for (let i = this.stackTop; i >= 0; i--) {
|
---|
401 | const tn = this.treeAdapter.getTagName(this.items[i]);
|
---|
402 | const ns = this.treeAdapter.getNamespaceURI(this.items[i]);
|
---|
403 |
|
---|
404 | if (ns !== NS.HTML) {
|
---|
405 | continue;
|
---|
406 | }
|
---|
407 |
|
---|
408 | if (tn === tagName) {
|
---|
409 | return true;
|
---|
410 | }
|
---|
411 |
|
---|
412 | if (tn === $.TABLE || tn === $.TEMPLATE || tn === $.HTML) {
|
---|
413 | return false;
|
---|
414 | }
|
---|
415 | }
|
---|
416 |
|
---|
417 | return true;
|
---|
418 | }
|
---|
419 |
|
---|
420 | hasTableBodyContextInTableScope() {
|
---|
421 | for (let i = this.stackTop; i >= 0; i--) {
|
---|
422 | const tn = this.treeAdapter.getTagName(this.items[i]);
|
---|
423 | const ns = this.treeAdapter.getNamespaceURI(this.items[i]);
|
---|
424 |
|
---|
425 | if (ns !== NS.HTML) {
|
---|
426 | continue;
|
---|
427 | }
|
---|
428 |
|
---|
429 | if (tn === $.TBODY || tn === $.THEAD || tn === $.TFOOT) {
|
---|
430 | return true;
|
---|
431 | }
|
---|
432 |
|
---|
433 | if (tn === $.TABLE || tn === $.HTML) {
|
---|
434 | return false;
|
---|
435 | }
|
---|
436 | }
|
---|
437 |
|
---|
438 | return true;
|
---|
439 | }
|
---|
440 |
|
---|
441 | hasInSelectScope(tagName) {
|
---|
442 | for (let i = this.stackTop; i >= 0; i--) {
|
---|
443 | const tn = this.treeAdapter.getTagName(this.items[i]);
|
---|
444 | const ns = this.treeAdapter.getNamespaceURI(this.items[i]);
|
---|
445 |
|
---|
446 | if (ns !== NS.HTML) {
|
---|
447 | continue;
|
---|
448 | }
|
---|
449 |
|
---|
450 | if (tn === tagName) {
|
---|
451 | return true;
|
---|
452 | }
|
---|
453 |
|
---|
454 | if (tn !== $.OPTION && tn !== $.OPTGROUP) {
|
---|
455 | return false;
|
---|
456 | }
|
---|
457 | }
|
---|
458 |
|
---|
459 | return true;
|
---|
460 | }
|
---|
461 |
|
---|
462 | //Implied end tags
|
---|
463 | generateImpliedEndTags() {
|
---|
464 | while (isImpliedEndTagRequired(this.currentTagName)) {
|
---|
465 | this.pop();
|
---|
466 | }
|
---|
467 | }
|
---|
468 |
|
---|
469 | generateImpliedEndTagsThoroughly() {
|
---|
470 | while (isImpliedEndTagRequiredThoroughly(this.currentTagName)) {
|
---|
471 | this.pop();
|
---|
472 | }
|
---|
473 | }
|
---|
474 |
|
---|
475 | generateImpliedEndTagsWithExclusion(exclusionTagName) {
|
---|
476 | while (isImpliedEndTagRequired(this.currentTagName) && this.currentTagName !== exclusionTagName) {
|
---|
477 | this.pop();
|
---|
478 | }
|
---|
479 | }
|
---|
480 | }
|
---|
481 |
|
---|
482 | module.exports = OpenElementStack;
|
---|