parse5 8.1.0 → 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,7 +9,27 @@ 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;
@@ -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
  }
@@ -395,8 +415,12 @@ export class Parser {
395
415
  }
396
416
  /** @protected */
397
417
  _adoptNodes(donor, recipient) {
398
- for (let child = this.treeAdapter.getFirstChild(donor); child; child = this.treeAdapter.getFirstChild(donor)) {
399
- 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) {
400
424
  this.treeAdapter.appendChild(recipient, child);
401
425
  }
402
426
  }
@@ -488,25 +512,31 @@ export class Parser {
488
512
  }
489
513
  }
490
514
  }
491
- //Integration points
492
515
  /** @protected */
493
516
  _isIntegrationPoint(tid, element, foreignNS) {
517
+ var _a;
494
518
  const ns = this.treeAdapter.getNamespaceURI(element);
495
- const attrs = this.treeAdapter.getAttrList(element);
496
- 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);
497
529
  }
498
530
  //Active formatting elements reconstruction
499
531
  /** @protected */
500
532
  _reconstructActiveFormattingElements() {
501
- const listLength = this.activeFormattingElements.entries.length;
502
- if (listLength) {
503
- const endIndex = this.activeFormattingElements.entries.findIndex((entry) => entry.type === EntryType.Marker || this.openElements.contains(entry.element));
504
- const unopenIdx = endIndex === -1 ? listLength - 1 : endIndex - 1;
505
- for (let i = unopenIdx; i >= 0; i--) {
506
- const entry = this.activeFormattingElements.entries[i];
507
- this._insertElement(entry.token, this.treeAdapter.getNamespaceURI(entry.element));
508
- entry.element = this.openElements.current;
509
- }
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);
510
540
  }
511
541
  }
512
542
  //Close elements
@@ -525,9 +555,14 @@ export class Parser {
525
555
  //Insertion modes
526
556
  /** @protected */
527
557
  _resetInsertionMode() {
528
- 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) {
529
564
  //Insertion mode reset map
530
- switch (i === 0 && this.fragmentContext ? this.fragmentContextID : this.openElements.tagIDs[i]) {
565
+ switch (tn) {
531
566
  case $.TR: {
532
567
  this.insertionMode = InsertionMode.IN_ROW;
533
568
  return;
@@ -563,7 +598,7 @@ export class Parser {
563
598
  return;
564
599
  }
565
600
  case $.TEMPLATE: {
566
- this.insertionMode = this.tmplInsertionModeStack[0];
601
+ this.insertionMode = this.tmplInsertionModeStack[this.tmplInsertionModeStack.length - 1];
567
602
  return;
568
603
  }
569
604
  case $.HTML: {
@@ -591,16 +626,12 @@ export class Parser {
591
626
  }
592
627
  /** @protected */
593
628
  _resetInsertionModeForSelect(selectIdx) {
629
+ // The select is the topmost element that selects a mode, so open tables and templates are below it.
594
630
  if (selectIdx > 0) {
595
- for (let i = selectIdx - 1; i > 0; i--) {
596
- const tn = this.openElements.tagIDs[i];
597
- if (tn === $.TEMPLATE) {
598
- break;
599
- }
600
- else if (tn === $.TABLE) {
601
- this.insertionMode = InsertionMode.IN_SELECT_IN_TABLE;
602
- return;
603
- }
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;
604
635
  }
605
636
  }
606
637
  this.insertionMode = InsertionMode.IN_SELECT;
@@ -618,27 +649,24 @@ export class Parser {
618
649
  }
619
650
  /** @protected */
620
651
  _findFosterParentingLocation() {
621
- for (let i = this.openElements.stackTop; i >= 0; i--) {
622
- const openElement = this.openElements.items[i];
623
- switch (this.openElements.tagIDs[i]) {
624
- case $.TEMPLATE: {
625
- if (this.treeAdapter.getNamespaceURI(openElement) === NS.HTML) {
626
- return { parent: this.treeAdapter.getTemplateContent(openElement), beforeElement: null };
627
- }
628
- break;
629
- }
630
- case $.TABLE: {
631
- const parent = this.treeAdapter.getParentNode(openElement);
632
- if (parent) {
633
- return { parent, beforeElement: openElement };
634
- }
635
- return { parent: this.openElements.items[i - 1], beforeElement: null };
636
- }
637
- default:
638
- // 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 };
639
666
  }
667
+ return { parent: openElements.items[openElements.slotBelow(tableIdx)], beforeElement: null };
640
668
  }
641
- return { parent: this.openElements.items[0], beforeElement: null };
669
+ return { parent: openElements.items[0], beforeElement: null };
642
670
  }
643
671
  /** @protected */
644
672
  _fosterParentElement(element) {
@@ -1210,7 +1238,7 @@ function aaObtainFormattingElementEntry(p, token) {
1210
1238
  p.activeFormattingElements.removeEntry(formattingElementEntry);
1211
1239
  formattingElementEntry = null;
1212
1240
  }
1213
- else if (!p.openElements.hasInScope(token.tagID)) {
1241
+ else if (!p.openElements.hasInScope(token.tagID, formattingElementEntry.element)) {
1214
1242
  formattingElementEntry = null;
1215
1243
  }
1216
1244
  }
@@ -1221,31 +1249,28 @@ function aaObtainFormattingElementEntry(p, token) {
1221
1249
  }
1222
1250
  //Steps 9 and 10 of the algorithm
1223
1251
  function aaObtainFurthestBlock(p, formattingElementEntry) {
1224
- let furthestBlock = null;
1225
- let idx = p.openElements.stackTop;
1226
- for (; idx >= 0; idx--) {
1227
- const element = p.openElements.items[idx];
1228
- if (element === formattingElementEntry.element) {
1229
- break;
1230
- }
1231
- if (p._isSpecialElement(element, p.openElements.tagIDs[idx])) {
1232
- 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;
1233
1258
  }
1234
1259
  }
1235
- if (!furthestBlock) {
1236
- p.openElements.shortenToLength(Math.max(idx, 0));
1237
- p.activeFormattingElements.removeEntry(formattingElementEntry);
1238
- }
1239
- return furthestBlock;
1260
+ openElements.shortenToLength(formattingIdx);
1261
+ p.activeFormattingElements.removeEntry(formattingElementEntry);
1262
+ return -1;
1240
1263
  }
1241
1264
  //Step 13 of the algorithm
1242
- function aaInnerLoop(p, furthestBlock, formattingElement) {
1265
+ function aaInnerLoop(p, furthestBlockIdx, formattingElement) {
1266
+ const furthestBlock = p.openElements.items[furthestBlockIdx];
1243
1267
  let lastElement = furthestBlock;
1244
- let nextElement = p.openElements.getCommonAncestor(furthestBlock);
1268
+ let nextIdx = p.openElements.slotBelow(furthestBlockIdx);
1245
1269
  const intendedParent = p.openElements.getCommonAncestor(formattingElement);
1246
- for (let i = 0, element = nextElement; element !== formattingElement; i++, element = nextElement) {
1247
- //NOTE: store the next element for the next loop iteration (it may be deleted from the stack by step 9.5)
1248
- nextElement = p.openElements.getCommonAncestor(element);
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);
1249
1274
  const elementEntry = p.activeFormattingElements.getElementEntry(element);
1250
1275
  const counterOverflow = elementEntry && i >= AA_INNER_LOOP_ITER;
1251
1276
  const shouldRemoveFromOpenElements = !elementEntry || counterOverflow;
@@ -1253,7 +1278,7 @@ function aaInnerLoop(p, furthestBlock, formattingElement) {
1253
1278
  if (counterOverflow) {
1254
1279
  p.activeFormattingElements.removeEntry(elementEntry);
1255
1280
  }
1256
- p.openElements.remove(element);
1281
+ p.openElements.removeAt(idx);
1257
1282
  }
1258
1283
  else {
1259
1284
  element = aaRecreateElementFromEntry(p, elementEntry, intendedParent);
@@ -1272,7 +1297,7 @@ function aaRecreateElementFromEntry(p, elementEntry, intendedParent) {
1272
1297
  const ns = p.treeAdapter.getNamespaceURI(elementEntry.element);
1273
1298
  const newElement = p._createElement(elementEntry.token.tagName, ns, elementEntry.token.attrs, intendedParent);
1274
1299
  p.openElements.replace(elementEntry.element, newElement);
1275
- elementEntry.element = newElement;
1300
+ p.activeFormattingElements.replaceElement(elementEntry, newElement);
1276
1301
  return newElement;
1277
1302
  }
1278
1303
  //Step 14 of the algorithm
@@ -1299,22 +1324,36 @@ function aaReplaceFormattingElement(p, furthestBlock, formattingElementEntry) {
1299
1324
  p.treeAdapter.appendChild(furthestBlock, newElement);
1300
1325
  p.activeFormattingElements.insertElementAfterBookmark(newElement, token);
1301
1326
  p.activeFormattingElements.removeEntry(formattingElementEntry);
1302
- p.openElements.remove(formattingElementEntry.element);
1303
- 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);
1304
1334
  }
1305
1335
  //Algorithm entry point
1306
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
+ }
1307
1345
  for (let i = 0; i < AA_OUTER_LOOP_ITER; i++) {
1308
1346
  const formattingElementEntry = aaObtainFormattingElementEntry(p, token);
1309
1347
  if (!formattingElementEntry) {
1310
1348
  break;
1311
1349
  }
1312
- const furthestBlock = aaObtainFurthestBlock(p, formattingElementEntry);
1313
- if (!furthestBlock) {
1350
+ const furthestBlockIdx = aaObtainFurthestBlock(p, formattingElementEntry);
1351
+ if (furthestBlockIdx === -1) {
1314
1352
  break;
1315
1353
  }
1354
+ const furthestBlock = p.openElements.items[furthestBlockIdx];
1316
1355
  p.activeFormattingElements.bookmark = formattingElementEntry;
1317
- const lastElement = aaInnerLoop(p, furthestBlock, formattingElementEntry.element);
1356
+ const lastElement = aaInnerLoop(p, furthestBlockIdx, formattingElementEntry.element);
1318
1357
  const commonAncestor = p.openElements.getCommonAncestor(formattingElementEntry.element);
1319
1358
  p.treeAdapter.detachNode(lastElement);
1320
1359
  if (commonAncestor)
@@ -1340,7 +1379,7 @@ function stopParsing(p, token) {
1340
1379
  // NOTE: If we are not in a fragment, `html` and `body` will stay on the stack.
1341
1380
  // This is a problem, as we might overwrite their end position here.
1342
1381
  const target = p.fragmentContext ? 0 : 2;
1343
- for (let i = p.openElements.stackTop; i >= target; i--) {
1382
+ for (let i = p.openElements.stackTop; i >= target; i = p.openElements.slotBelow(i)) {
1344
1383
  p._setEndLocation(p.openElements.items[i], token);
1345
1384
  }
1346
1385
  // Handle `html` and `body`
@@ -1478,7 +1517,7 @@ function startTagInHead(p, token) {
1478
1517
  p.activeFormattingElements.insertMarker();
1479
1518
  p.framesetOk = false;
1480
1519
  p.insertionMode = InsertionMode.IN_TEMPLATE;
1481
- p.tmplInsertionModeStack.unshift(InsertionMode.IN_TEMPLATE);
1520
+ p.tmplInsertionModeStack.push(InsertionMode.IN_TEMPLATE);
1482
1521
  break;
1483
1522
  }
1484
1523
  case $.HEAD: {
@@ -1520,7 +1559,7 @@ function templateEndTagInHead(p, token) {
1520
1559
  }
1521
1560
  p.openElements.popUntilTagNamePopped($.TEMPLATE);
1522
1561
  p.activeFormattingElements.clearToLastMarker();
1523
- p.tmplInsertionModeStack.shift();
1562
+ p.tmplInsertionModeStack.pop();
1524
1563
  p._resetInsertionMode();
1525
1564
  }
1526
1565
  else {
@@ -1749,21 +1788,15 @@ function formStartTagInBody(p, token) {
1749
1788
  }
1750
1789
  function listItemStartTagInBody(p, token) {
1751
1790
  p.framesetOk = false;
1752
- const tn = token.tagID;
1753
- for (let i = p.openElements.stackTop; i >= 0; i--) {
1754
- const elementId = p.openElements.tagIDs[i];
1755
- if ((tn === $.LI && elementId === $.LI) ||
1756
- ((tn === $.DD || tn === $.DT) && (elementId === $.DD || elementId === $.DT))) {
1757
- p.openElements.generateImpliedEndTagsWithExclusion(elementId);
1758
- p.openElements.popUntilTagNamePopped(elementId);
1759
- break;
1760
- }
1761
- if (elementId !== $.ADDRESS &&
1762
- elementId !== $.DIV &&
1763
- elementId !== $.P &&
1764
- p._isSpecialElement(p.openElements.items[i], elementId)) {
1765
- break;
1766
- }
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);
1767
1800
  }
1768
1801
  if (p.openElements.hasInButtonScope($.P)) {
1769
1802
  p._closePElement();
@@ -2247,24 +2280,16 @@ function brEndTagInBody(p) {
2247
2280
  p.framesetOk = false;
2248
2281
  }
2249
2282
  function genericEndTagInBody(p, token) {
2250
- const tn = token.tagName;
2251
2283
  const tid = token.tagID;
2252
- for (let i = p.openElements.stackTop; i > 0; i--) {
2253
- const element = p.openElements.items[i];
2254
- const elementId = p.openElements.tagIDs[i];
2255
- // Compare the tag name here, as the tag might not be a known tag with an ID.
2256
- // Only HTML elements match; a foreign element falls through to the special-element check.
2257
- if (tid === elementId &&
2258
- p.treeAdapter.getNamespaceURI(element) === NS.HTML &&
2259
- (tid !== $.UNKNOWN || p.treeAdapter.getTagName(element) === tn)) {
2260
- p.openElements.generateImpliedEndTagsWithExclusion(tid);
2261
- if (p.openElements.stackTop >= i)
2262
- p.openElements.shortenToLength(i);
2263
- break;
2264
- }
2265
- if (p._isSpecialElement(element, elementId)) {
2266
- break;
2267
- }
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);
2268
2293
  }
2269
2294
  }
2270
2295
  function endTagInBody(p, token) {
@@ -2949,7 +2974,7 @@ function endTagInSelect(p, token) {
2949
2974
  case $.OPTGROUP: {
2950
2975
  if (p.openElements.stackTop > 0 &&
2951
2976
  p.openElements.currentTagId === $.OPTION &&
2952
- p.openElements.tagIDs[p.openElements.stackTop - 1] === $.OPTGROUP) {
2977
+ p.openElements.tagIDs[p.openElements.slotBelow(p.openElements.stackTop)] === $.OPTGROUP) {
2953
2978
  p.openElements.pop();
2954
2979
  }
2955
2980
  if (p.openElements.currentTagId === $.OPTGROUP) {
@@ -3042,32 +3067,32 @@ function startTagInTemplate(p, token) {
3042
3067
  case $.TBODY:
3043
3068
  case $.TFOOT:
3044
3069
  case $.THEAD: {
3045
- p.tmplInsertionModeStack[0] = InsertionMode.IN_TABLE;
3070
+ p.tmplInsertionModeStack[p.tmplInsertionModeStack.length - 1] = InsertionMode.IN_TABLE;
3046
3071
  p.insertionMode = InsertionMode.IN_TABLE;
3047
3072
  startTagInTable(p, token);
3048
3073
  break;
3049
3074
  }
3050
3075
  case $.COL: {
3051
- p.tmplInsertionModeStack[0] = InsertionMode.IN_COLUMN_GROUP;
3076
+ p.tmplInsertionModeStack[p.tmplInsertionModeStack.length - 1] = InsertionMode.IN_COLUMN_GROUP;
3052
3077
  p.insertionMode = InsertionMode.IN_COLUMN_GROUP;
3053
3078
  startTagInColumnGroup(p, token);
3054
3079
  break;
3055
3080
  }
3056
3081
  case $.TR: {
3057
- p.tmplInsertionModeStack[0] = InsertionMode.IN_TABLE_BODY;
3082
+ p.tmplInsertionModeStack[p.tmplInsertionModeStack.length - 1] = InsertionMode.IN_TABLE_BODY;
3058
3083
  p.insertionMode = InsertionMode.IN_TABLE_BODY;
3059
3084
  startTagInTableBody(p, token);
3060
3085
  break;
3061
3086
  }
3062
3087
  case $.TD:
3063
3088
  case $.TH: {
3064
- p.tmplInsertionModeStack[0] = InsertionMode.IN_ROW;
3089
+ p.tmplInsertionModeStack[p.tmplInsertionModeStack.length - 1] = InsertionMode.IN_ROW;
3065
3090
  p.insertionMode = InsertionMode.IN_ROW;
3066
3091
  startTagInRow(p, token);
3067
3092
  break;
3068
3093
  }
3069
3094
  default: {
3070
- p.tmplInsertionModeStack[0] = InsertionMode.IN_BODY;
3095
+ p.tmplInsertionModeStack[p.tmplInsertionModeStack.length - 1] = InsertionMode.IN_BODY;
3071
3096
  p.insertionMode = InsertionMode.IN_BODY;
3072
3097
  startTagInBody(p, token);
3073
3098
  }
@@ -3080,9 +3105,12 @@ function endTagInTemplate(p, token) {
3080
3105
  }
3081
3106
  function eofInTemplate(p, token) {
3082
3107
  if (p.openElements.tmplCount > 0) {
3083
- p.openElements.popUntilTagNamePopped($.TEMPLATE);
3084
- p.activeFormattingElements.clearToLastMarker();
3085
- 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);
3086
3114
  p._resetInsertionMode();
3087
3115
  p.onEof(token);
3088
3116
  }
@@ -3257,18 +3285,24 @@ function endTagInForeignContent(p, token) {
3257
3285
  p._endTagOutsideForeignContent(token);
3258
3286
  return;
3259
3287
  }
3260
- for (let i = p.openElements.stackTop; i > 0; i--) {
3261
- const element = p.openElements.items[i];
3262
- if (p.treeAdapter.getNamespaceURI(element) === NS.HTML) {
3263
- p._endTagOutsideForeignContent(token);
3264
- break;
3265
- }
3266
- const tagName = p.treeAdapter.getTagName(element);
3267
- if (tagName.toLowerCase() === token.tagName) {
3268
- //NOTE: update the token tag name for `_setEndLocation`.
3269
- token.tagName = tagName;
3270
- p.openElements.shortenToLength(i);
3271
- break;
3272
- }
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);
3273
3307
  }
3274
3308
  }