• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1import * as assert from 'node:assert';
2import { TAG_ID as $, TAG_NAMES as TN, NS } from '../common/html.js';
3import { OpenElementStack } from './open-element-stack.js';
4import type { TreeAdapterTypeMap } from '../tree-adapters/interface';
5import { generateTestsForEachTreeAdapter } from 'parse5-test-utils/utils/common.js';
6
7function ignore(): void {
8    /* Ignore */
9}
10
11const stackHandler = {
12    onItemPop: ignore,
13    onItemPush: ignore,
14};
15
16generateTestsForEachTreeAdapter('open-element-stack', (treeAdapter) => {
17    function createElement(tagName: string, namespaceURI = NS.HTML): TreeAdapterTypeMap['element'] {
18        return treeAdapter.createElement(tagName, namespaceURI, []);
19    }
20
21    test('Push element', () => {
22        const document = treeAdapter.createDocument();
23        const element1 = createElement('#element1', NS.XLINK);
24        const element2 = createElement('#element2', NS.SVG);
25        const stack = new OpenElementStack(document, treeAdapter, stackHandler);
26
27        assert.strictEqual(stack.current, document);
28        assert.strictEqual(stack.stackTop, -1);
29
30        stack.push(element1, $.UNKNOWN);
31        assert.strictEqual(stack.current, element1);
32        assert.strictEqual(stack.stackTop, 0);
33
34        stack.push(element2, $.UNKNOWN);
35        assert.strictEqual(stack.current, element2);
36        assert.strictEqual(stack.stackTop, 1);
37    });
38
39    test('Pop element', () => {
40        const element = createElement('#element', NS.XLINK);
41        const stack = new OpenElementStack(treeAdapter.createDocument(), treeAdapter, stackHandler);
42
43        stack.push(element, $.UNKNOWN);
44        stack.push(createElement('#element2', NS.XML), $.UNKNOWN);
45        stack.pop();
46        assert.strictEqual(stack.current, element);
47        assert.strictEqual(stack.stackTop, 0);
48
49        stack.pop();
50        assert.ok(!stack.current);
51        assert.ok(!stack.currentTagId);
52        assert.strictEqual(stack.stackTop, -1);
53    });
54
55    test('Replace element', () => {
56        const element = createElement('#element', NS.MATHML);
57        const newElement = createElement('#newElement', NS.SVG);
58        const stack = new OpenElementStack(treeAdapter.createDocument(), treeAdapter, stackHandler);
59
60        stack.push(createElement('#element2', NS.XML), $.UNKNOWN);
61        stack.push(element, $.UNKNOWN);
62        stack.replace(element, newElement);
63        assert.strictEqual(stack.current, newElement);
64        assert.strictEqual(stack.stackTop, 1);
65    });
66
67    test('Insert element after element', () => {
68        const element1 = createElement('#element1', NS.XLINK);
69        const element2 = createElement('#element2', NS.SVG);
70        const element3 = createElement('#element3', NS.XML);
71        const stack = new OpenElementStack(treeAdapter.createDocument(), treeAdapter, stackHandler);
72
73        stack.push(element1, $.UNKNOWN);
74        stack.push(element2, $.UNKNOWN);
75        stack.insertAfter(element1, element3, $.UNKNOWN);
76        assert.strictEqual(stack.stackTop, 2);
77        assert.strictEqual(stack.items[1], element3);
78
79        stack.insertAfter(element2, element1, $.UNKNOWN);
80        assert.strictEqual(stack.stackTop, 3);
81        assert.strictEqual(stack.current, element1);
82    });
83
84    test('Pop elements until popped with given tagName', () => {
85        const element1 = createElement(TN.ASIDE);
86        const element2 = createElement(TN.MAIN);
87        const stack = new OpenElementStack(treeAdapter.createDocument(), treeAdapter, stackHandler);
88
89        stack.push(element2, $.MAIN);
90        stack.push(element2, $.MAIN);
91        stack.push(element2, $.MAIN);
92        stack.push(element2, $.MAIN);
93        stack.popUntilTagNamePopped($.ASIDE);
94        assert.ok(!stack.current);
95        assert.strictEqual(stack.stackTop, -1);
96
97        stack.push(element2, $.MAIN);
98        stack.push(element1, $.ASIDE);
99        stack.push(element2, $.MAIN);
100        stack.popUntilTagNamePopped($.ASIDE);
101        assert.strictEqual(stack.current, element2);
102        assert.strictEqual(stack.stackTop, 0);
103    });
104
105    test('Pop elements until given element popped', () => {
106        const element1 = createElement('#element1');
107        const element2 = createElement('#element2');
108        const stack = new OpenElementStack(treeAdapter.createDocument(), treeAdapter, stackHandler);
109
110        stack.push(element2, $.UNKNOWN);
111        stack.push(element2, $.UNKNOWN);
112        stack.push(element2, $.UNKNOWN);
113        stack.push(element2, $.UNKNOWN);
114        stack.popUntilElementPopped(element1);
115        assert.ok(!stack.current);
116        assert.strictEqual(stack.stackTop, -1);
117
118        stack.push(element2, $.UNKNOWN);
119        stack.push(element1, $.UNKNOWN);
120        stack.push(element2, $.UNKNOWN);
121        stack.popUntilElementPopped(element1);
122        assert.strictEqual(stack.current, element2);
123        assert.strictEqual(stack.stackTop, 0);
124    });
125
126    test('Pop elements until numbered header popped', () => {
127        const element1 = createElement(TN.H3);
128        const element2 = createElement(TN.DIV);
129        const stack = new OpenElementStack(treeAdapter.createDocument(), treeAdapter, stackHandler);
130
131        stack.push(element2, $.DIV);
132        stack.push(element2, $.DIV);
133        stack.push(element2, $.DIV);
134        stack.push(element2, $.DIV);
135        stack.popUntilNumberedHeaderPopped();
136        assert.ok(!stack.current);
137        assert.strictEqual(stack.stackTop, -1);
138
139        stack.push(element2, $.DIV);
140        stack.push(element1, $.H3);
141        stack.push(element2, $.DIV);
142        stack.popUntilNumberedHeaderPopped();
143        assert.strictEqual(stack.current, element2);
144        assert.strictEqual(stack.stackTop, 0);
145    });
146
147    test('Pop all up to <html> element', () => {
148        const htmlElement = createElement(TN.HTML);
149        const stack = new OpenElementStack(treeAdapter.createDocument(), treeAdapter, stackHandler);
150
151        stack.push(htmlElement, $.HTML);
152        stack.push('#element1', $.UNKNOWN);
153        stack.push('#element2', $.UNKNOWN);
154
155        stack.popAllUpToHtmlElement();
156        assert.strictEqual(stack.current, htmlElement);
157    });
158
159    test('Clear back to a table context', () => {
160        const htmlElement = createElement(TN.HTML);
161        const tableElement = createElement(TN.TABLE);
162        const divElement = createElement(TN.DIV);
163        const stack = new OpenElementStack(treeAdapter.createDocument(), treeAdapter, stackHandler);
164
165        stack.push(htmlElement, $.HTML);
166        stack.push(divElement, $.DIV);
167        stack.push(divElement, $.DIV);
168        stack.push(divElement, $.DIV);
169        stack.clearBackToTableContext();
170        assert.strictEqual(stack.current, htmlElement);
171        assert.strictEqual(stack.stackTop, 0);
172
173        stack.push(divElement, $.DIV);
174        stack.push(tableElement, $.TABLE);
175        stack.push(divElement, $.DIV);
176        stack.push(divElement, $.DIV);
177        stack.clearBackToTableContext();
178        assert.strictEqual(stack.current, tableElement);
179        assert.strictEqual(stack.stackTop, 2);
180    });
181
182    test('Clear back to a table body context', () => {
183        const htmlElement = createElement(TN.HTML);
184        const theadElement = createElement(TN.THEAD);
185        const divElement = createElement(TN.DIV);
186        const stack = new OpenElementStack(treeAdapter.createDocument(), treeAdapter, stackHandler);
187
188        stack.push(htmlElement, $.HTML);
189        stack.push(divElement, $.DIV);
190        stack.push(divElement, $.DIV);
191        stack.push(divElement, $.DIV);
192        stack.clearBackToTableBodyContext();
193        assert.strictEqual(stack.current, htmlElement);
194        assert.strictEqual(stack.stackTop, 0);
195
196        stack.push(divElement, $.DIV);
197        stack.push(theadElement, $.THEAD);
198        stack.push(divElement, $.DIV);
199        stack.push(divElement, $.DIV);
200        stack.clearBackToTableBodyContext();
201        assert.strictEqual(stack.current, theadElement);
202        assert.strictEqual(stack.stackTop, 2);
203    });
204
205    test('Clear back to a table row context', () => {
206        const htmlElement = createElement(TN.HTML);
207        const trElement = createElement(TN.TR);
208        const divElement = createElement(TN.DIV);
209        const stack = new OpenElementStack(treeAdapter.createDocument(), treeAdapter, stackHandler);
210
211        stack.push(htmlElement, $.HTML);
212        stack.push(divElement, $.DIV);
213        stack.push(divElement, $.DIV);
214        stack.push(divElement, $.DIV);
215        stack.clearBackToTableRowContext();
216        assert.strictEqual(stack.current, htmlElement);
217        assert.strictEqual(stack.stackTop, 0);
218
219        stack.push(divElement, $.DIV);
220        stack.push(trElement, $.TR);
221        stack.push(divElement, $.DIV);
222        stack.push(divElement, $.DIV);
223        stack.clearBackToTableRowContext();
224        assert.strictEqual(stack.current, trElement);
225        assert.strictEqual(stack.stackTop, 2);
226    });
227
228    test('Remove element', () => {
229        const element = createElement('#element');
230        const stack = new OpenElementStack(treeAdapter.createDocument(), treeAdapter, stackHandler);
231
232        stack.push(element, $.UNKNOWN);
233        stack.push(createElement('element1'), $.UNKNOWN);
234        stack.push(createElement('element2'), $.UNKNOWN);
235
236        stack.remove(element);
237
238        assert.strictEqual(stack.stackTop, 1);
239
240        for (let i = stack.stackTop; i >= 0; i--) {
241            assert.notStrictEqual(stack.items[i], element);
242        }
243    });
244
245    test('Try peek properly nested <body> element', () => {
246        const bodyElement = createElement(TN.BODY);
247        let stack = new OpenElementStack(treeAdapter.createDocument(), treeAdapter, stackHandler);
248
249        stack.push(createElement(TN.HTML), $.HTML);
250        stack.push(bodyElement, $.BODY);
251        stack.push(createElement(TN.DIV), $.DIV);
252        assert.strictEqual(stack.tryPeekProperlyNestedBodyElement(), bodyElement);
253
254        stack = new OpenElementStack(treeAdapter.createDocument(), treeAdapter, stackHandler);
255        stack.push(createElement(TN.HTML), $.HTML);
256        assert.ok(!stack.tryPeekProperlyNestedBodyElement());
257    });
258
259    test('Is root <html> element current', () => {
260        const stack = new OpenElementStack(treeAdapter.createDocument(), treeAdapter, stackHandler);
261
262        stack.push(createElement(TN.HTML), $.HTML);
263        assert.ok(stack.isRootHtmlElementCurrent());
264
265        stack.push(createElement(TN.DIV), $.DIV);
266        assert.ok(!stack.isRootHtmlElementCurrent());
267    });
268
269    test('Get common ancestor', () => {
270        const stack = new OpenElementStack(treeAdapter.createDocument(), treeAdapter, stackHandler);
271        const element = createElement('#element');
272        const ancestor = createElement('#ancestor');
273
274        stack.push(createElement('#someElement'), $.UNKNOWN);
275        assert.ok(!stack.getCommonAncestor(element));
276
277        stack.pop();
278        assert.ok(!stack.getCommonAncestor(element));
279
280        stack.push(element, $.UNKNOWN);
281        assert.ok(!stack.getCommonAncestor(element));
282
283        stack.push(createElement('#someElement'), $.UNKNOWN);
284        stack.push(ancestor, $.UNKNOWN);
285        stack.push(element, $.UNKNOWN);
286        assert.strictEqual(stack.getCommonAncestor(element), ancestor);
287    });
288
289    test('Contains element', () => {
290        const stack = new OpenElementStack(treeAdapter.createDocument(), treeAdapter, stackHandler);
291        const element = createElement('#element');
292
293        stack.push(createElement('#someElement'), $.UNKNOWN);
294        assert.ok(!stack.contains(element));
295
296        stack.push(element, $.UNKNOWN);
297        assert.ok(stack.contains(element));
298    });
299
300    test('Has element in scope', () => {
301        const stack = new OpenElementStack(treeAdapter.createDocument(), treeAdapter, stackHandler);
302
303        stack.push(createElement(TN.HTML), $.HTML);
304        stack.push(createElement(TN.DIV), $.DIV);
305        assert.ok(!stack.hasInScope($.P));
306
307        stack.push(createElement(TN.P), $.P);
308        stack.push(createElement(TN.UL), $.UL);
309        stack.push(createElement(TN.BUTTON), $.BUTTON);
310        stack.push(createElement(TN.OPTION), $.OPTION);
311        assert.ok(stack.hasInScope($.P));
312
313        stack.push(createElement(TN.TITLE, NS.SVG), $.TITLE);
314        assert.ok(!stack.hasInScope($.P));
315    });
316
317    test('Has numbered header in scope', () => {
318        const stack = new OpenElementStack(treeAdapter.createDocument(), treeAdapter, stackHandler);
319
320        assert.ok(stack.hasNumberedHeaderInScope());
321
322        stack.push(createElement(TN.HTML), $.HTML);
323        stack.push(createElement(TN.DIV), $.DIV);
324        assert.ok(!stack.hasNumberedHeaderInScope());
325
326        stack.push(createElement(TN.P), $.P);
327        stack.push(createElement(TN.UL), $.UL);
328        stack.push(createElement(TN.H3), $.H3);
329        stack.push(createElement(TN.OPTION), $.OPTION);
330        assert.ok(stack.hasNumberedHeaderInScope());
331
332        stack.push(createElement(TN.TITLE, NS.SVG), $.TITLE);
333        assert.ok(!stack.hasNumberedHeaderInScope());
334
335        stack.push(createElement(TN.H6), $.H6);
336        assert.ok(stack.hasNumberedHeaderInScope());
337    });
338
339    test('Has element in list item scope', () => {
340        const stack = new OpenElementStack(treeAdapter.createDocument(), treeAdapter, stackHandler);
341
342        assert.ok(stack.hasInListItemScope($.P));
343
344        stack.push(createElement(TN.HTML), $.HTML);
345        stack.push(createElement(TN.DIV), $.DIV);
346        assert.ok(!stack.hasInListItemScope($.P));
347
348        stack.push(createElement(TN.P), $.P);
349        stack.push(createElement(TN.BUTTON), $.BUTTON);
350        stack.push(createElement(TN.OPTION), $.OPTION);
351        assert.ok(stack.hasInListItemScope($.P));
352
353        stack.push(createElement(TN.UL), $.UL);
354        assert.ok(!stack.hasInListItemScope($.P));
355    });
356
357    test('Has element in button scope', () => {
358        const stack = new OpenElementStack(treeAdapter.createDocument(), treeAdapter, stackHandler);
359
360        assert.ok(stack.hasInButtonScope($.P));
361
362        stack.push(createElement(TN.HTML), $.HTML);
363        stack.push(createElement(TN.DIV), $.DIV);
364        assert.ok(!stack.hasInButtonScope($.P));
365
366        stack.push(createElement(TN.P), $.P);
367        stack.push(createElement(TN.UL), $.UL);
368        stack.push(createElement(TN.OPTION), $.OPTION);
369        assert.ok(stack.hasInButtonScope($.P));
370
371        stack.push(createElement(TN.BUTTON), $.BUTTON);
372        assert.ok(!stack.hasInButtonScope($.P));
373    });
374
375    test('Has element in table scope', () => {
376        const stack = new OpenElementStack(treeAdapter.createDocument(), treeAdapter, stackHandler);
377
378        stack.push(createElement(TN.HTML), $.HTML);
379        stack.push(createElement(TN.DIV), $.DIV);
380        assert.ok(!stack.hasInTableScope($.P));
381
382        stack.push(createElement(TN.P), $.P);
383        stack.push(createElement(TN.UL), $.UL);
384        stack.push(createElement(TN.TD), $.TD);
385        stack.push(createElement(TN.OPTION), $.OPTION);
386        assert.ok(stack.hasInTableScope($.P));
387
388        stack.push(createElement(TN.TABLE), $.TABLE);
389        assert.ok(!stack.hasInTableScope($.P));
390    });
391
392    test('Has table body context in table scope', () => {
393        const stack = new OpenElementStack(treeAdapter.createDocument(), treeAdapter, stackHandler);
394
395        stack.push(createElement(TN.HTML), $.HTML);
396        stack.push(createElement(TN.DIV), $.DIV);
397        assert.ok(!stack.hasTableBodyContextInTableScope());
398
399        stack.push(createElement(TN.TABLE), $.TABLE);
400        stack.push(createElement(TN.UL), $.UL);
401        stack.push(createElement(TN.TBODY), $.TBODY);
402        stack.push(createElement(TN.OPTION), $.OPTION);
403        assert.ok(stack.hasTableBodyContextInTableScope());
404
405        stack.push(createElement(TN.TABLE), $.TABLE);
406        assert.ok(!stack.hasTableBodyContextInTableScope());
407
408        stack.push(createElement(TN.TFOOT), $.TFOOT);
409        assert.ok(stack.hasTableBodyContextInTableScope());
410    });
411
412    test('Has element in select scope', () => {
413        const stack = new OpenElementStack(treeAdapter.createDocument(), treeAdapter, stackHandler);
414
415        assert.ok(stack.hasInSelectScope($.P));
416
417        stack.push(createElement(TN.HTML), $.HTML);
418        stack.push(createElement(TN.DIV), $.DIV);
419        assert.ok(!stack.hasInSelectScope($.P));
420
421        stack.push(createElement(TN.P), $.P);
422        stack.push(createElement(TN.OPTION), $.OPTION);
423        assert.ok(stack.hasInSelectScope($.P));
424
425        stack.push(createElement(TN.DIV), $.DIV);
426        assert.ok(!stack.hasInSelectScope($.P));
427    });
428
429    test('Generate implied end tags', () => {
430        const stack = new OpenElementStack(treeAdapter.createDocument(), treeAdapter, stackHandler);
431
432        stack.push(createElement(TN.HTML), $.HTML);
433        stack.push(createElement(TN.LI), $.LI);
434        stack.push(createElement(TN.DIV), $.DIV);
435        stack.push(createElement(TN.LI), $.LI);
436        stack.push(createElement(TN.OPTION), $.OPTION);
437        stack.push(createElement(TN.P), $.P);
438
439        stack.generateImpliedEndTags();
440
441        assert.strictEqual(stack.stackTop, 2);
442        assert.strictEqual(stack.currentTagId, $.DIV);
443    });
444
445    test('Generate implied end tags with exclusion', () => {
446        const stack = new OpenElementStack(treeAdapter.createDocument(), treeAdapter, stackHandler);
447
448        stack.push(createElement(TN.HTML), $.HTML);
449        stack.push(createElement(TN.LI), $.LI);
450        stack.push(createElement(TN.DIV), $.DIV);
451        stack.push(createElement(TN.LI), $.LI);
452        stack.push(createElement(TN.OPTION), $.OPTION);
453        stack.push(createElement(TN.P), $.P);
454
455        stack.generateImpliedEndTagsWithExclusion($.LI);
456
457        assert.strictEqual(stack.stackTop, 3);
458        assert.strictEqual(stack.currentTagId, $.LI);
459    });
460
461    test('Template count', () => {
462        const stack = new OpenElementStack(treeAdapter.createDocument(), treeAdapter, stackHandler);
463
464        stack.push(createElement(TN.HTML), $.HTML);
465        stack.push(createElement(TN.TEMPLATE, NS.MATHML), $.TEMPLATE);
466        assert.strictEqual(stack.tmplCount, 0);
467
468        stack.push(createElement(TN.TEMPLATE), $.TEMPLATE);
469        stack.push(createElement(TN.LI), $.LI);
470        assert.strictEqual(stack.tmplCount, 1);
471
472        stack.push(createElement(TN.OPTION), $.OPTION);
473        stack.push(createElement(TN.TEMPLATE), $.TEMPLATE);
474        assert.strictEqual(stack.tmplCount, 2);
475
476        stack.pop();
477        assert.strictEqual(stack.tmplCount, 1);
478
479        stack.pop();
480        stack.pop();
481        stack.pop();
482        assert.strictEqual(stack.tmplCount, 0);
483    });
484});
485