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