parse5 8.0.1 → 8.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -9,12 +9,32 @@ import * as unicode from '../common/unicode.js';
9
9
  import { TAG_ID as $, TAG_NAMES as TN, NS, ATTRS, SPECIAL_ELEMENTS, DOCUMENT_MODE, NUMBERED_HEADERS, getTagID, } from '../common/html.js';
10
10
  import { TokenType, getTokenAttr, } from '../common/token.js';
11
11
  //Misc constants
12
+ // Elements that can select an insertion mode when it is reset.
13
+ const INSERTION_MODE_TAGS = [
14
+ $.TR,
15
+ $.TBODY,
16
+ $.THEAD,
17
+ $.TFOOT,
18
+ $.CAPTION,
19
+ $.COLGROUP,
20
+ $.TABLE,
21
+ $.FRAMESET,
22
+ $.SELECT,
23
+ $.TEMPLATE,
24
+ $.TD,
25
+ $.TH,
26
+ $.HEAD,
27
+ $.BODY,
28
+ $.HTML,
29
+ ];
12
30
  const HIDDEN_INPUT_TYPE = 'hidden';
31
+ // For the integration-point check of elements whose attributes do not matter; never attached to a node.
32
+ const EMPTY_ATTRS = [];
13
33
  //Adoption agency loops iteration count
14
34
  const AA_OUTER_LOOP_ITER = 8;
15
35
  const AA_INNER_LOOP_ITER = 3;
16
36
  //Insertion modes
17
- var InsertionMode;
37
+ export var InsertionMode;
18
38
  (function (InsertionMode) {
19
39
  InsertionMode[InsertionMode["INITIAL"] = 0] = "INITIAL";
20
40
  InsertionMode[InsertionMode["BEFORE_HTML"] = 1] = "BEFORE_HTML";
@@ -77,8 +97,7 @@ export class Parser {
77
97
  /** Indicates that the current node is not an element in the HTML namespace */
78
98
  this.currentNotInHTML = false;
79
99
  /**
80
- * The template insertion mode stack is maintained from the left.
81
- * Ie. the topmost element will always have index 0.
100
+ * The topmost template insertion mode is the last element.
82
101
  *
83
102
  * @internal
84
103
  */
@@ -105,7 +124,7 @@ export class Parser {
105
124
  }
106
125
  this.document = document !== null && document !== void 0 ? document : this.treeAdapter.createDocument();
107
126
  this.tokenizer = new Tokenizer(this.options, this);
108
- this.activeFormattingElements = new FormattingElementList(this.treeAdapter);
127
+ this.activeFormattingElements = new FormattingElementList();
109
128
  this.fragmentContextID = fragmentContext ? getTagID(this.treeAdapter.getTagName(fragmentContext)) : $.UNKNOWN;
110
129
  this._setContextModes(fragmentContext !== null && fragmentContext !== void 0 ? fragmentContext : this.document, this.fragmentContextID);
111
130
  this.openElements = new OpenElementStack(this.document, this.treeAdapter, this);
@@ -130,7 +149,7 @@ export class Parser {
130
149
  const documentMock = opts.treeAdapter.createElement('documentmock', NS.HTML, []);
131
150
  const parser = new this(opts, documentMock, fragmentContext);
132
151
  if (parser.fragmentContextID === $.TEMPLATE) {
133
- parser.tmplInsertionModeStack.unshift(InsertionMode.IN_TEMPLATE);
152
+ parser.tmplInsertionModeStack.push(InsertionMode.IN_TEMPLATE);
134
153
  }
135
154
  parser._initTokenizerForFragmentParsing();
136
155
  parser._insertFakeRootElement();
@@ -172,11 +191,12 @@ export class Parser {
172
191
  }
173
192
  /** @internal */
174
193
  onItemPop(node, isTop) {
175
- var _a, _b;
194
+ var _a, _b, _c;
176
195
  if (this.options.sourceCodeLocationInfo) {
177
196
  this._setEndLocation(node, this.currentToken);
178
197
  }
179
198
  (_b = (_a = this.treeAdapter).onItemPop) === null || _b === void 0 ? void 0 : _b.call(_a, node, this.openElements.current);
199
+ (_c = this.annotationXmlIntegrationPoints) === null || _c === void 0 ? void 0 : _c.delete(node);
180
200
  if (isTop) {
181
201
  let current;
182
202
  let currentTagId;
@@ -219,7 +239,7 @@ export class Parser {
219
239
  _findFormInFragmentContext() {
220
240
  let node = this.fragmentContext;
221
241
  while (node) {
222
- if (this.treeAdapter.getTagName(node) === TN.FORM) {
242
+ if (this.treeAdapter.getTagName(node) === TN.FORM && this.treeAdapter.getNamespaceURI(node) === NS.HTML) {
223
243
  this.formElement = node;
224
244
  break;
225
245
  }
@@ -273,7 +293,7 @@ export class Parser {
273
293
  }
274
294
  }
275
295
  /** @protected */
276
- _attachElementToTree(element, location) {
296
+ _attachElementToTree(element, location, insertionLocation) {
277
297
  if (this.options.sourceCodeLocationInfo) {
278
298
  const loc = location && {
279
299
  ...location,
@@ -281,13 +301,34 @@ export class Parser {
281
301
  };
282
302
  this.treeAdapter.setNodeSourceCodeLocation(element, loc);
283
303
  }
284
- if (this._shouldFosterParentOnInsertion()) {
285
- this._fosterParentElement(element);
304
+ if (insertionLocation.beforeElement) {
305
+ this.treeAdapter.insertBefore(insertionLocation.parent, element, insertionLocation.beforeElement);
286
306
  }
287
307
  else {
288
- const parent = this.openElements.currentTmplContentOrNode;
289
- this.treeAdapter.appendChild(parent !== null && parent !== void 0 ? parent : this.document, element);
308
+ this.treeAdapter.appendChild(insertionLocation.parent, element);
309
+ }
310
+ }
311
+ /** @protected */
312
+ _getInsertionLocation() {
313
+ var _a;
314
+ if (this._shouldFosterParentOnInsertion()) {
315
+ return this._findFosterParentingLocation();
290
316
  }
317
+ return {
318
+ parent: (_a = this.openElements.currentTmplContentOrNode) !== null && _a !== void 0 ? _a : this.document,
319
+ beforeElement: null,
320
+ };
321
+ }
322
+ /** @protected */
323
+ _createElement(tagName, namespaceURI, attrs, intendedParent) {
324
+ var _a, _b;
325
+ const element = this.treeAdapter.createElement(tagName, namespaceURI, attrs);
326
+ (_b = (_a = this.treeAdapter).onElementCreated) === null || _b === void 0 ? void 0 : _b.call(_a, element, {
327
+ intendedParent,
328
+ formElement: this.formElement,
329
+ isInTemplate: this.openElements.tmplCount > 0,
330
+ });
331
+ return element;
291
332
  }
292
333
  /**
293
334
  * For self-closing tags. Add an element to the tree, but skip adding it
@@ -295,37 +336,41 @@ export class Parser {
295
336
  */
296
337
  /** @protected */
297
338
  _appendElement(token, namespaceURI) {
298
- const element = this.treeAdapter.createElement(token.tagName, namespaceURI, token.attrs);
299
- this._attachElementToTree(element, token.location);
339
+ const insertionLocation = this._getInsertionLocation();
340
+ const element = this._createElement(token.tagName, namespaceURI, token.attrs, insertionLocation.parent);
341
+ this._attachElementToTree(element, token.location, insertionLocation);
300
342
  }
301
343
  /** @protected */
302
344
  _insertElement(token, namespaceURI) {
303
- const element = this.treeAdapter.createElement(token.tagName, namespaceURI, token.attrs);
304
- this._attachElementToTree(element, token.location);
345
+ const insertionLocation = this._getInsertionLocation();
346
+ const element = this._createElement(token.tagName, namespaceURI, token.attrs, insertionLocation.parent);
347
+ this._attachElementToTree(element, token.location, insertionLocation);
305
348
  this.openElements.push(element, token.tagID);
306
349
  }
307
350
  /** @protected */
308
351
  _insertFakeElement(tagName, tagID) {
309
- const element = this.treeAdapter.createElement(tagName, NS.HTML, []);
310
- this._attachElementToTree(element, null);
352
+ const insertionLocation = this._getInsertionLocation();
353
+ const element = this._createElement(tagName, NS.HTML, [], insertionLocation.parent);
354
+ this._attachElementToTree(element, null, insertionLocation);
311
355
  this.openElements.push(element, tagID);
312
356
  }
313
357
  /** @protected */
314
358
  _insertTemplate(token) {
315
- const tmpl = this.treeAdapter.createElement(token.tagName, NS.HTML, token.attrs);
359
+ const insertionLocation = this._getInsertionLocation();
360
+ const tmpl = this._createElement(token.tagName, NS.HTML, token.attrs, insertionLocation.parent);
316
361
  const content = this.treeAdapter.createDocumentFragment();
317
362
  this.treeAdapter.setTemplateContent(tmpl, content);
318
- this._attachElementToTree(tmpl, token.location);
363
+ this._attachElementToTree(tmpl, token.location, insertionLocation);
319
364
  this.openElements.push(tmpl, token.tagID);
320
365
  if (this.options.sourceCodeLocationInfo)
321
366
  this.treeAdapter.setNodeSourceCodeLocation(content, null);
322
367
  }
323
368
  /** @protected */
324
369
  _insertFakeRootElement() {
325
- const element = this.treeAdapter.createElement(TN.HTML, NS.HTML, []);
370
+ const element = this._createElement(TN.HTML, NS.HTML, [], this.document);
326
371
  if (this.options.sourceCodeLocationInfo)
327
372
  this.treeAdapter.setNodeSourceCodeLocation(element, null);
328
- this.treeAdapter.appendChild(this.openElements.current, element);
373
+ this.treeAdapter.appendChild(this.document, element);
329
374
  this.openElements.push(element, $.HTML);
330
375
  }
331
376
  /** @protected */
@@ -370,8 +415,12 @@ export class Parser {
370
415
  }
371
416
  /** @protected */
372
417
  _adoptNodes(donor, recipient) {
373
- for (let child = this.treeAdapter.getFirstChild(donor); child; child = this.treeAdapter.getFirstChild(donor)) {
374
- this.treeAdapter.detachNode(child);
418
+ // Detaching from the end keeps an array-backed child list from shifting once per child.
419
+ const children = [...this.treeAdapter.getChildNodes(donor)];
420
+ for (let i = children.length - 1; i >= 0; i--) {
421
+ this.treeAdapter.detachNode(children[i]);
422
+ }
423
+ for (const child of children) {
375
424
  this.treeAdapter.appendChild(recipient, child);
376
425
  }
377
426
  }
@@ -463,25 +512,31 @@ export class Parser {
463
512
  }
464
513
  }
465
514
  }
466
- //Integration points
467
515
  /** @protected */
468
516
  _isIntegrationPoint(tid, element, foreignNS) {
517
+ var _a;
469
518
  const ns = this.treeAdapter.getNamespaceURI(element);
470
- const attrs = this.treeAdapter.getAttrList(element);
471
- return foreignContent.isIntegrationPoint(tid, ns, attrs, foreignNS);
519
+ if (tid === $.ANNOTATION_XML && ns === NS.MATHML && foreignNS !== NS.MATHML) {
520
+ const verdicts = ((_a = this.annotationXmlIntegrationPoints) !== null && _a !== void 0 ? _a : (this.annotationXmlIntegrationPoints = new Map()));
521
+ let isIntegrationPoint = verdicts.get(element);
522
+ if (isIntegrationPoint === undefined) {
523
+ isIntegrationPoint = foreignContent.isIntegrationPoint(tid, ns, this.treeAdapter.getAttrList(element));
524
+ verdicts.set(element, isIntegrationPoint);
525
+ }
526
+ return isIntegrationPoint;
527
+ }
528
+ return foreignContent.isIntegrationPoint(tid, ns, EMPTY_ATTRS, foreignNS);
472
529
  }
473
530
  //Active formatting elements reconstruction
474
531
  /** @protected */
475
532
  _reconstructActiveFormattingElements() {
476
- const listLength = this.activeFormattingElements.entries.length;
477
- if (listLength) {
478
- const endIndex = this.activeFormattingElements.entries.findIndex((entry) => entry.type === EntryType.Marker || this.openElements.contains(entry.element));
479
- const unopenIdx = endIndex === -1 ? listLength - 1 : endIndex - 1;
480
- for (let i = unopenIdx; i >= 0; i--) {
481
- const entry = this.activeFormattingElements.entries[i];
482
- this._insertElement(entry.token, this.treeAdapter.getNamespaceURI(entry.element));
483
- entry.element = this.openElements.current;
484
- }
533
+ let oldest = null;
534
+ for (let entry = this.activeFormattingElements.newest; entry !== null && entry.type === EntryType.Element && !this.openElements.contains(entry.element); entry = entry.older) {
535
+ oldest = entry;
536
+ }
537
+ for (let entry = oldest; entry !== null && entry.type === EntryType.Element; entry = entry.newer) {
538
+ this._insertElement(entry.token, this.treeAdapter.getNamespaceURI(entry.element));
539
+ this.activeFormattingElements.replaceElement(entry, this.openElements.current);
485
540
  }
486
541
  }
487
542
  //Close elements
@@ -500,9 +555,14 @@ export class Parser {
500
555
  //Insertion modes
501
556
  /** @protected */
502
557
  _resetInsertionMode() {
503
- for (let i = this.openElements.stackTop; i >= 0; i--) {
558
+ // The topmost open HTML element that selects a mode; the fragment context stands in for the root.
559
+ const i = this.openElements.indexOfTopmostOf(INSERTION_MODE_TAGS);
560
+ const tn = i === 0 && this.fragmentContext ? this.fragmentContextID : this.openElements.tagIDs[i];
561
+ const node = i === 0 && this.fragmentContext ? this.fragmentContext : this.openElements.items[i];
562
+ // Only HTML elements select an insertion mode.
563
+ if (node !== undefined && this.treeAdapter.getNamespaceURI(node) === NS.HTML) {
504
564
  //Insertion mode reset map
505
- switch (i === 0 && this.fragmentContext ? this.fragmentContextID : this.openElements.tagIDs[i]) {
565
+ switch (tn) {
506
566
  case $.TR: {
507
567
  this.insertionMode = InsertionMode.IN_ROW;
508
568
  return;
@@ -538,7 +598,7 @@ export class Parser {
538
598
  return;
539
599
  }
540
600
  case $.TEMPLATE: {
541
- this.insertionMode = this.tmplInsertionModeStack[0];
601
+ this.insertionMode = this.tmplInsertionModeStack[this.tmplInsertionModeStack.length - 1];
542
602
  return;
543
603
  }
544
604
  case $.HTML: {
@@ -566,16 +626,12 @@ export class Parser {
566
626
  }
567
627
  /** @protected */
568
628
  _resetInsertionModeForSelect(selectIdx) {
629
+ // The select is the topmost element that selects a mode, so open tables and templates are below it.
569
630
  if (selectIdx > 0) {
570
- for (let i = selectIdx - 1; i > 0; i--) {
571
- const tn = this.openElements.tagIDs[i];
572
- if (tn === $.TEMPLATE) {
573
- break;
574
- }
575
- else if (tn === $.TABLE) {
576
- this.insertionMode = InsertionMode.IN_SELECT_IN_TABLE;
577
- return;
578
- }
631
+ const tableIdx = this.openElements.indexOfTopmost($.TABLE);
632
+ if (tableIdx > 0 && tableIdx > this.openElements.indexOfTopmost($.TEMPLATE)) {
633
+ this.insertionMode = InsertionMode.IN_SELECT_IN_TABLE;
634
+ return;
579
635
  }
580
636
  }
581
637
  this.insertionMode = InsertionMode.IN_SELECT;
@@ -593,27 +649,24 @@ export class Parser {
593
649
  }
594
650
  /** @protected */
595
651
  _findFosterParentingLocation() {
596
- for (let i = this.openElements.stackTop; i >= 0; i--) {
597
- const openElement = this.openElements.items[i];
598
- switch (this.openElements.tagIDs[i]) {
599
- case $.TEMPLATE: {
600
- if (this.treeAdapter.getNamespaceURI(openElement) === NS.HTML) {
601
- return { parent: this.treeAdapter.getTemplateContent(openElement), beforeElement: null };
602
- }
603
- break;
604
- }
605
- case $.TABLE: {
606
- const parent = this.treeAdapter.getParentNode(openElement);
607
- if (parent) {
608
- return { parent, beforeElement: openElement };
609
- }
610
- return { parent: this.openElements.items[i - 1], beforeElement: null };
611
- }
612
- default:
613
- // Do nothing
652
+ const { openElements } = this;
653
+ const tableIdx = openElements.indexOfTopmost($.TABLE);
654
+ const templateIdx = openElements.indexOfTopmost($.TEMPLATE);
655
+ if (templateIdx > tableIdx) {
656
+ return {
657
+ parent: this.treeAdapter.getTemplateContent(openElements.items[templateIdx]),
658
+ beforeElement: null,
659
+ };
660
+ }
661
+ if (tableIdx !== -1) {
662
+ const table = openElements.items[tableIdx];
663
+ const parent = this.treeAdapter.getParentNode(table);
664
+ if (parent) {
665
+ return { parent, beforeElement: table };
614
666
  }
667
+ return { parent: openElements.items[openElements.slotBelow(tableIdx)], beforeElement: null };
615
668
  }
616
- return { parent: this.openElements.items[0], beforeElement: null };
669
+ return { parent: openElements.items[0], beforeElement: null };
617
670
  }
618
671
  /** @protected */
619
672
  _fosterParentElement(element) {
@@ -1185,7 +1238,7 @@ function aaObtainFormattingElementEntry(p, token) {
1185
1238
  p.activeFormattingElements.removeEntry(formattingElementEntry);
1186
1239
  formattingElementEntry = null;
1187
1240
  }
1188
- else if (!p.openElements.hasInScope(token.tagID)) {
1241
+ else if (!p.openElements.hasInScope(token.tagID, formattingElementEntry.element)) {
1189
1242
  formattingElementEntry = null;
1190
1243
  }
1191
1244
  }
@@ -1196,30 +1249,28 @@ function aaObtainFormattingElementEntry(p, token) {
1196
1249
  }
1197
1250
  //Steps 9 and 10 of the algorithm
1198
1251
  function aaObtainFurthestBlock(p, formattingElementEntry) {
1199
- let furthestBlock = null;
1200
- let idx = p.openElements.stackTop;
1201
- for (; idx >= 0; idx--) {
1202
- const element = p.openElements.items[idx];
1203
- if (element === formattingElementEntry.element) {
1204
- break;
1205
- }
1206
- if (p._isSpecialElement(element, p.openElements.tagIDs[idx])) {
1207
- furthestBlock = element;
1252
+ const { openElements } = p;
1253
+ const formattingIdx = openElements._indexOf(formattingElementEntry.element);
1254
+ // The furthest block is the lowest special element above the formatting element.
1255
+ for (let idx = openElements.slotAbove(formattingIdx); idx !== -1; idx = openElements.slotAbove(idx)) {
1256
+ if (p._isSpecialElement(openElements.items[idx], openElements.tagIDs[idx])) {
1257
+ return idx;
1208
1258
  }
1209
1259
  }
1210
- if (!furthestBlock) {
1211
- p.openElements.shortenToLength(Math.max(idx, 0));
1212
- p.activeFormattingElements.removeEntry(formattingElementEntry);
1213
- }
1214
- return furthestBlock;
1260
+ openElements.shortenToLength(formattingIdx);
1261
+ p.activeFormattingElements.removeEntry(formattingElementEntry);
1262
+ return -1;
1215
1263
  }
1216
1264
  //Step 13 of the algorithm
1217
- function aaInnerLoop(p, furthestBlock, formattingElement) {
1265
+ function aaInnerLoop(p, furthestBlockIdx, formattingElement) {
1266
+ const furthestBlock = p.openElements.items[furthestBlockIdx];
1218
1267
  let lastElement = furthestBlock;
1219
- let nextElement = p.openElements.getCommonAncestor(furthestBlock);
1220
- for (let i = 0, element = nextElement; element !== formattingElement; i++, element = nextElement) {
1221
- //NOTE: store the next element for the next loop iteration (it may be deleted from the stack by step 9.5)
1222
- nextElement = p.openElements.getCommonAncestor(element);
1268
+ let nextIdx = p.openElements.slotBelow(furthestBlockIdx);
1269
+ const intendedParent = p.openElements.getCommonAncestor(formattingElement);
1270
+ for (let i = 0, idx = nextIdx; p.openElements.items[idx] !== formattingElement; i++, idx = nextIdx) {
1271
+ let element = p.openElements.items[idx];
1272
+ // Store the next element for the next loop iteration (this one may be removed from the stack by step 9.5).
1273
+ nextIdx = p.openElements.slotBelow(idx);
1223
1274
  const elementEntry = p.activeFormattingElements.getElementEntry(element);
1224
1275
  const counterOverflow = elementEntry && i >= AA_INNER_LOOP_ITER;
1225
1276
  const shouldRemoveFromOpenElements = !elementEntry || counterOverflow;
@@ -1227,10 +1278,10 @@ function aaInnerLoop(p, furthestBlock, formattingElement) {
1227
1278
  if (counterOverflow) {
1228
1279
  p.activeFormattingElements.removeEntry(elementEntry);
1229
1280
  }
1230
- p.openElements.remove(element);
1281
+ p.openElements.removeAt(idx);
1231
1282
  }
1232
1283
  else {
1233
- element = aaRecreateElementFromEntry(p, elementEntry);
1284
+ element = aaRecreateElementFromEntry(p, elementEntry, intendedParent);
1234
1285
  if (lastElement === furthestBlock) {
1235
1286
  p.activeFormattingElements.bookmark = elementEntry;
1236
1287
  }
@@ -1242,11 +1293,11 @@ function aaInnerLoop(p, furthestBlock, formattingElement) {
1242
1293
  return lastElement;
1243
1294
  }
1244
1295
  //Step 13.7 of the algorithm
1245
- function aaRecreateElementFromEntry(p, elementEntry) {
1296
+ function aaRecreateElementFromEntry(p, elementEntry, intendedParent) {
1246
1297
  const ns = p.treeAdapter.getNamespaceURI(elementEntry.element);
1247
- const newElement = p.treeAdapter.createElement(elementEntry.token.tagName, ns, elementEntry.token.attrs);
1298
+ const newElement = p._createElement(elementEntry.token.tagName, ns, elementEntry.token.attrs, intendedParent);
1248
1299
  p.openElements.replace(elementEntry.element, newElement);
1249
- elementEntry.element = newElement;
1300
+ p.activeFormattingElements.replaceElement(elementEntry, newElement);
1250
1301
  return newElement;
1251
1302
  }
1252
1303
  //Step 14 of the algorithm
@@ -1268,27 +1319,41 @@ function aaInsertLastNodeInCommonAncestor(p, commonAncestor, lastElement) {
1268
1319
  function aaReplaceFormattingElement(p, furthestBlock, formattingElementEntry) {
1269
1320
  const ns = p.treeAdapter.getNamespaceURI(formattingElementEntry.element);
1270
1321
  const { token } = formattingElementEntry;
1271
- const newElement = p.treeAdapter.createElement(token.tagName, ns, token.attrs);
1322
+ const newElement = p._createElement(token.tagName, ns, token.attrs, furthestBlock);
1272
1323
  p._adoptNodes(furthestBlock, newElement);
1273
1324
  p.treeAdapter.appendChild(furthestBlock, newElement);
1274
1325
  p.activeFormattingElements.insertElementAfterBookmark(newElement, token);
1275
1326
  p.activeFormattingElements.removeEntry(formattingElementEntry);
1276
- p.openElements.remove(formattingElementEntry.element);
1277
- p.openElements.insertAfter(furthestBlock, newElement, token.tagID);
1327
+ // Only the elements the inner loop kept are open between the formatting element and the furthest block.
1328
+ const formattingIdx = p.openElements._indexOf(formattingElementEntry.element);
1329
+ let furthestBlockIdx = p.openElements.slotAbove(formattingIdx);
1330
+ while (p.openElements.items[furthestBlockIdx] !== furthestBlock) {
1331
+ furthestBlockIdx = p.openElements.slotAbove(furthestBlockIdx);
1332
+ }
1333
+ p.openElements.replaceFormattingElement(formattingIdx, furthestBlockIdx, newElement, token.tagID);
1278
1334
  }
1279
1335
  //Algorithm entry point
1280
1336
  function callAdoptionAgency(p, token) {
1337
+ const { current, currentTagId } = p.openElements;
1338
+ //Step 2 of the algorithm
1339
+ if (currentTagId === token.tagID &&
1340
+ p.treeAdapter.getNamespaceURI(current) === NS.HTML &&
1341
+ !p.activeFormattingElements.getElementEntry(current)) {
1342
+ p.openElements.pop();
1343
+ return;
1344
+ }
1281
1345
  for (let i = 0; i < AA_OUTER_LOOP_ITER; i++) {
1282
1346
  const formattingElementEntry = aaObtainFormattingElementEntry(p, token);
1283
1347
  if (!formattingElementEntry) {
1284
1348
  break;
1285
1349
  }
1286
- const furthestBlock = aaObtainFurthestBlock(p, formattingElementEntry);
1287
- if (!furthestBlock) {
1350
+ const furthestBlockIdx = aaObtainFurthestBlock(p, formattingElementEntry);
1351
+ if (furthestBlockIdx === -1) {
1288
1352
  break;
1289
1353
  }
1354
+ const furthestBlock = p.openElements.items[furthestBlockIdx];
1290
1355
  p.activeFormattingElements.bookmark = formattingElementEntry;
1291
- const lastElement = aaInnerLoop(p, furthestBlock, formattingElementEntry.element);
1356
+ const lastElement = aaInnerLoop(p, furthestBlockIdx, formattingElementEntry.element);
1292
1357
  const commonAncestor = p.openElements.getCommonAncestor(formattingElementEntry.element);
1293
1358
  p.treeAdapter.detachNode(lastElement);
1294
1359
  if (commonAncestor)
@@ -1314,7 +1379,7 @@ function stopParsing(p, token) {
1314
1379
  // NOTE: If we are not in a fragment, `html` and `body` will stay on the stack.
1315
1380
  // This is a problem, as we might overwrite their end position here.
1316
1381
  const target = p.fragmentContext ? 0 : 2;
1317
- for (let i = p.openElements.stackTop; i >= target; i--) {
1382
+ for (let i = p.openElements.stackTop; i >= target; i = p.openElements.slotBelow(i)) {
1318
1383
  p._setEndLocation(p.openElements.items[i], token);
1319
1384
  }
1320
1385
  // Handle `html` and `body`
@@ -1452,7 +1517,7 @@ function startTagInHead(p, token) {
1452
1517
  p.activeFormattingElements.insertMarker();
1453
1518
  p.framesetOk = false;
1454
1519
  p.insertionMode = InsertionMode.IN_TEMPLATE;
1455
- p.tmplInsertionModeStack.unshift(InsertionMode.IN_TEMPLATE);
1520
+ p.tmplInsertionModeStack.push(InsertionMode.IN_TEMPLATE);
1456
1521
  break;
1457
1522
  }
1458
1523
  case $.HEAD: {
@@ -1494,7 +1559,7 @@ function templateEndTagInHead(p, token) {
1494
1559
  }
1495
1560
  p.openElements.popUntilTagNamePopped($.TEMPLATE);
1496
1561
  p.activeFormattingElements.clearToLastMarker();
1497
- p.tmplInsertionModeStack.shift();
1562
+ p.tmplInsertionModeStack.pop();
1498
1563
  p._resetInsertionMode();
1499
1564
  }
1500
1565
  else {
@@ -1723,21 +1788,15 @@ function formStartTagInBody(p, token) {
1723
1788
  }
1724
1789
  function listItemStartTagInBody(p, token) {
1725
1790
  p.framesetOk = false;
1726
- const tn = token.tagID;
1727
- for (let i = p.openElements.stackTop; i >= 0; i--) {
1728
- const elementId = p.openElements.tagIDs[i];
1729
- if ((tn === $.LI && elementId === $.LI) ||
1730
- ((tn === $.DD || tn === $.DT) && (elementId === $.DD || elementId === $.DT))) {
1731
- p.openElements.generateImpliedEndTagsWithExclusion(elementId);
1732
- p.openElements.popUntilTagNamePopped(elementId);
1733
- break;
1734
- }
1735
- if (elementId !== $.ADDRESS &&
1736
- elementId !== $.DIV &&
1737
- elementId !== $.P &&
1738
- p._isSpecialElement(p.openElements.items[i], elementId)) {
1739
- break;
1740
- }
1791
+ // The topmost open item of the token's kind closes, unless a special element other than address, div and p is
1792
+ // above it.
1793
+ const idx = token.tagID === $.LI
1794
+ ? p.openElements.indexOfTopmost($.LI)
1795
+ : Math.max(p.openElements.indexOfTopmost($.DD), p.openElements.indexOfTopmost($.DT));
1796
+ if (idx !== -1 && !p.openElements.hasListItemBoundaryAbove(idx)) {
1797
+ const elementId = p.openElements.tagIDs[idx];
1798
+ p.openElements.generateImpliedEndTagsWithExclusion(elementId);
1799
+ p.openElements.popUntilTagNamePopped(elementId);
1741
1800
  }
1742
1801
  if (p.openElements.hasInButtonScope($.P)) {
1743
1802
  p._closePElement();
@@ -2221,21 +2280,16 @@ function brEndTagInBody(p) {
2221
2280
  p.framesetOk = false;
2222
2281
  }
2223
2282
  function genericEndTagInBody(p, token) {
2224
- const tn = token.tagName;
2225
2283
  const tid = token.tagID;
2226
- for (let i = p.openElements.stackTop; i > 0; i--) {
2227
- const element = p.openElements.items[i];
2228
- const elementId = p.openElements.tagIDs[i];
2229
- // Compare the tag name here, as the tag might not be a known tag with an ID.
2230
- if (tid === elementId && (tid !== $.UNKNOWN || p.treeAdapter.getTagName(element) === tn)) {
2231
- p.openElements.generateImpliedEndTagsWithExclusion(tid);
2232
- if (p.openElements.stackTop >= i)
2233
- p.openElements.shortenToLength(i);
2234
- break;
2235
- }
2236
- if (p._isSpecialElement(element, elementId)) {
2237
- break;
2238
- }
2284
+ // The topmost open HTML element with the token's name closes, unless a special element is above it. A tag without
2285
+ // an ID is matched by name.
2286
+ const i = tid === $.UNKNOWN
2287
+ ? p.openElements.indexOfTopmostByName(NS.HTML, token.tagName)
2288
+ : p.openElements.indexOfTopmost(tid);
2289
+ if (i > 0 && !p.openElements.hasSpecialElementAbove(i)) {
2290
+ p.openElements.generateImpliedEndTagsWithExclusion(tid);
2291
+ if (p.openElements.stackTop >= i)
2292
+ p.openElements.shortenToLength(i);
2239
2293
  }
2240
2294
  }
2241
2295
  function endTagInBody(p, token) {
@@ -2920,7 +2974,7 @@ function endTagInSelect(p, token) {
2920
2974
  case $.OPTGROUP: {
2921
2975
  if (p.openElements.stackTop > 0 &&
2922
2976
  p.openElements.currentTagId === $.OPTION &&
2923
- p.openElements.tagIDs[p.openElements.stackTop - 1] === $.OPTGROUP) {
2977
+ p.openElements.tagIDs[p.openElements.slotBelow(p.openElements.stackTop)] === $.OPTGROUP) {
2924
2978
  p.openElements.pop();
2925
2979
  }
2926
2980
  if (p.openElements.currentTagId === $.OPTGROUP) {
@@ -3013,32 +3067,32 @@ function startTagInTemplate(p, token) {
3013
3067
  case $.TBODY:
3014
3068
  case $.TFOOT:
3015
3069
  case $.THEAD: {
3016
- p.tmplInsertionModeStack[0] = InsertionMode.IN_TABLE;
3070
+ p.tmplInsertionModeStack[p.tmplInsertionModeStack.length - 1] = InsertionMode.IN_TABLE;
3017
3071
  p.insertionMode = InsertionMode.IN_TABLE;
3018
3072
  startTagInTable(p, token);
3019
3073
  break;
3020
3074
  }
3021
3075
  case $.COL: {
3022
- p.tmplInsertionModeStack[0] = InsertionMode.IN_COLUMN_GROUP;
3076
+ p.tmplInsertionModeStack[p.tmplInsertionModeStack.length - 1] = InsertionMode.IN_COLUMN_GROUP;
3023
3077
  p.insertionMode = InsertionMode.IN_COLUMN_GROUP;
3024
3078
  startTagInColumnGroup(p, token);
3025
3079
  break;
3026
3080
  }
3027
3081
  case $.TR: {
3028
- p.tmplInsertionModeStack[0] = InsertionMode.IN_TABLE_BODY;
3082
+ p.tmplInsertionModeStack[p.tmplInsertionModeStack.length - 1] = InsertionMode.IN_TABLE_BODY;
3029
3083
  p.insertionMode = InsertionMode.IN_TABLE_BODY;
3030
3084
  startTagInTableBody(p, token);
3031
3085
  break;
3032
3086
  }
3033
3087
  case $.TD:
3034
3088
  case $.TH: {
3035
- p.tmplInsertionModeStack[0] = InsertionMode.IN_ROW;
3089
+ p.tmplInsertionModeStack[p.tmplInsertionModeStack.length - 1] = InsertionMode.IN_ROW;
3036
3090
  p.insertionMode = InsertionMode.IN_ROW;
3037
3091
  startTagInRow(p, token);
3038
3092
  break;
3039
3093
  }
3040
3094
  default: {
3041
- p.tmplInsertionModeStack[0] = InsertionMode.IN_BODY;
3095
+ p.tmplInsertionModeStack[p.tmplInsertionModeStack.length - 1] = InsertionMode.IN_BODY;
3042
3096
  p.insertionMode = InsertionMode.IN_BODY;
3043
3097
  startTagInBody(p, token);
3044
3098
  }
@@ -3051,9 +3105,12 @@ function endTagInTemplate(p, token) {
3051
3105
  }
3052
3106
  function eofInTemplate(p, token) {
3053
3107
  if (p.openElements.tmplCount > 0) {
3054
- p.openElements.popUntilTagNamePopped($.TEMPLATE);
3055
- p.activeFormattingElements.clearToLastMarker();
3056
- p.tmplInsertionModeStack.shift();
3108
+ // EOF closes every open template.
3109
+ do {
3110
+ p.openElements.popUntilTagNamePopped($.TEMPLATE);
3111
+ p.activeFormattingElements.clearToLastMarker();
3112
+ p.tmplInsertionModeStack.pop();
3113
+ } while (p.openElements.tmplCount > 0);
3057
3114
  p._resetInsertionMode();
3058
3115
  p.onEof(token);
3059
3116
  }
@@ -3228,18 +3285,24 @@ function endTagInForeignContent(p, token) {
3228
3285
  p._endTagOutsideForeignContent(token);
3229
3286
  return;
3230
3287
  }
3231
- for (let i = p.openElements.stackTop; i > 0; i--) {
3232
- const element = p.openElements.items[i];
3233
- if (p.treeAdapter.getNamespaceURI(element) === NS.HTML) {
3234
- p._endTagOutsideForeignContent(token);
3235
- break;
3236
- }
3237
- const tagName = p.treeAdapter.getTagName(element);
3238
- if (tagName.toLowerCase() === token.tagName) {
3239
- //NOTE: update the token tag name for `_setEndLocation`.
3240
- token.tagName = tagName;
3241
- p.openElements.shortenToLength(i);
3242
- break;
3243
- }
3288
+ // The current node is the root element (fragment case): ignore the token.
3289
+ if (p.openElements.stackTop === 0)
3290
+ return;
3291
+ // The topmost foreign element with the token's name closes, unless an HTML element is above it. Foreign names are
3292
+ // the lowercased start-tag name or its SVG adjustment, so these two are the ASCII case-insensitive match.
3293
+ const { tagName } = token;
3294
+ const adjustedTagName = foreignContent.SVG_TAG_NAMES_ADJUSTMENT_MAP.get(tagName);
3295
+ // Elements outside HTML, SVG and MathML only come from a fragment context in their namespace.
3296
+ const contextNs = p.fragmentContext ? p.treeAdapter.getNamespaceURI(p.fragmentContext) : NS.HTML;
3297
+ const i = Math.max(p.openElements.indexOfTopmostByName(NS.SVG, tagName), p.openElements.indexOfTopmostByName(NS.MATHML, tagName), adjustedTagName === undefined ? -1 : p.openElements.indexOfTopmostByName(NS.SVG, adjustedTagName), contextNs === NS.HTML || contextNs === NS.SVG || contextNs === NS.MATHML
3298
+ ? -1
3299
+ : p.openElements.indexOfTopmostByName(contextNs, tagName));
3300
+ if (i > p.openElements.indexOfTopmostHtmlElement()) {
3301
+ //NOTE: update the token tag name for `_setEndLocation`.
3302
+ token.tagName = p.treeAdapter.getTagName(p.openElements.items[i]);
3303
+ p.openElements.shortenToLength(i);
3304
+ }
3305
+ else {
3306
+ p._endTagOutsideForeignContent(token);
3244
3307
  }
3245
3308
  }