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.
@@ -1,4 +1,4 @@
1
- import { TAG_ID as $, NS, NUMBERED_HEADERS } from '../common/html.js';
1
+ import { TAG_ID as $, NS, SPECIAL_ELEMENTS, getTagID } from '../common/html.js';
2
2
  //Element utils
3
3
  const IMPLICIT_END_TAG_REQUIRED = new Set([$.DD, $.DT, $.LI, $.OPTGROUP, $.OPTION, $.P, $.RB, $.RP, $.RT, $.RTC]);
4
4
  const IMPLICIT_END_TAG_REQUIRED_THOROUGHLY = new Set([
@@ -12,7 +12,42 @@ const IMPLICIT_END_TAG_REQUIRED_THOROUGHLY = new Set([
12
12
  $.THEAD,
13
13
  $.TR,
14
14
  ]);
15
- const SCOPING_ELEMENTS_HTML = new Set([
15
+ /** A stack this deep starts to index its elements; a shallower one is cheaper to scan than to index. */
16
+ const INDEX_DEPTH = 64;
17
+ /*
18
+ * Every open element has a key: an HTML element its tag ID, an SVG or MathML element its tag ID plus the offsets below,
19
+ * and any other element (an unknown tag, or another namespace) a key from `NAME_KEYS` up, assigned per stack by
20
+ * namespace and tag name.
21
+ */
22
+ const SVG_KEYS = 256;
23
+ const MATHML_KEYS = 512;
24
+ const NAME_KEYS = 768;
25
+ const NUMBERED_HEADERS = [$.H1, $.H2, $.H3, $.H4, $.H5, $.H6];
26
+ const TABLE_BODIES = [$.TBODY, $.TFOOT, $.THEAD];
27
+ const TABLE_ROW_CONTEXT = [$.TR, $.TEMPLATE, $.HTML];
28
+ const TABLE_BODY_CONTEXT = [...TABLE_BODIES, $.TEMPLATE, $.HTML];
29
+ const TABLE_CONTEXT = [$.TABLE, $.TEMPLATE, $.HTML];
30
+ const TABLE_CELLS = [$.TD, $.TH];
31
+ // Classes of keys: special elements, the special elements that end the search for a list item to close (all but
32
+ // HTML `address`, `div` and `p`), and the elements that bound each kind of scope.
33
+ const SPECIAL = 1;
34
+ const LIST_ITEM_BOUNDARY = 2;
35
+ const SCOPE = 4;
36
+ const LIST_ITEM_SCOPE = 8;
37
+ const BUTTON_SCOPE = 16;
38
+ const TABLE_SCOPE = 32;
39
+ const KEY_CLASSES = new Uint8Array(NAME_KEYS);
40
+ for (const [ns, offset] of [
41
+ [NS.HTML, 0],
42
+ [NS.SVG, SVG_KEYS],
43
+ [NS.MATHML, MATHML_KEYS],
44
+ ]) {
45
+ for (const tn of SPECIAL_ELEMENTS[ns]) {
46
+ KEY_CLASSES[offset + tn] =
47
+ ns === NS.HTML && (tn === $.ADDRESS || tn === $.DIV || tn === $.P) ? SPECIAL : SPECIAL | LIST_ITEM_BOUNDARY;
48
+ }
49
+ }
50
+ for (const key of [
16
51
  $.APPLET,
17
52
  $.CAPTION,
18
53
  $.HTML,
@@ -22,16 +57,55 @@ const SCOPING_ELEMENTS_HTML = new Set([
22
57
  $.TD,
23
58
  $.TEMPLATE,
24
59
  $.TH,
25
- ]);
26
- const SCOPING_ELEMENTS_HTML_LIST = new Set([...SCOPING_ELEMENTS_HTML, $.OL, $.UL]);
27
- const SCOPING_ELEMENTS_HTML_BUTTON = new Set([...SCOPING_ELEMENTS_HTML, $.BUTTON]);
28
- const SCOPING_ELEMENTS_MATHML = new Set([$.ANNOTATION_XML, $.MI, $.MN, $.MO, $.MS, $.MTEXT]);
29
- const SCOPING_ELEMENTS_SVG = new Set([$.DESC, $.FOREIGN_OBJECT, $.TITLE]);
30
- const TABLE_ROW_CONTEXT = new Set([$.TR, $.TEMPLATE, $.HTML]);
31
- const TABLE_BODY_CONTEXT = new Set([$.TBODY, $.TFOOT, $.THEAD, $.TEMPLATE, $.HTML]);
32
- const TABLE_CONTEXT = new Set([$.TABLE, $.TEMPLATE, $.HTML]);
33
- const TABLE_CELLS = new Set([$.TD, $.TH]);
34
- //Stack of open elements
60
+ SVG_KEYS + $.DESC,
61
+ SVG_KEYS + $.FOREIGN_OBJECT,
62
+ SVG_KEYS + $.TITLE,
63
+ MATHML_KEYS + $.ANNOTATION_XML,
64
+ MATHML_KEYS + $.MI,
65
+ MATHML_KEYS + $.MN,
66
+ MATHML_KEYS + $.MO,
67
+ MATHML_KEYS + $.MS,
68
+ MATHML_KEYS + $.MTEXT,
69
+ ]) {
70
+ KEY_CLASSES[key] |= SCOPE | LIST_ITEM_SCOPE | BUTTON_SCOPE;
71
+ }
72
+ KEY_CLASSES[$.OL] |= LIST_ITEM_SCOPE;
73
+ KEY_CLASSES[$.UL] |= LIST_ITEM_SCOPE;
74
+ KEY_CLASSES[$.BUTTON] |= BUTTON_SCOPE;
75
+ KEY_CLASSES[$.TABLE] |= TABLE_SCOPE;
76
+ KEY_CLASSES[$.HTML] |= TABLE_SCOPE;
77
+ // The adoption agency and the reconstruction look up formatting elements by element.
78
+ const FORMATTING_TAGS = new Uint8Array(NAME_KEYS);
79
+ for (const tn of [$.A, $.B, $.BIG, $.CODE, $.EM, $.FONT, $.I, $.NOBR, $.S, $.SMALL, $.STRIKE, $.STRONG, $.TT, $.U]) {
80
+ FORMATTING_TAGS[tn] = 1;
81
+ }
82
+ /** The key of an element with a tag ID in HTML, SVG or MathML, or -1 for an element keyed by its name. */
83
+ function tagKey(ns, tagID) {
84
+ if (tagID === $.UNKNOWN)
85
+ return -1;
86
+ switch (ns) {
87
+ case NS.HTML: {
88
+ return tagID;
89
+ }
90
+ case NS.SVG: {
91
+ return SVG_KEYS + tagID;
92
+ }
93
+ case NS.MATHML: {
94
+ return MATHML_KEYS + tagID;
95
+ }
96
+ default: {
97
+ return -1;
98
+ }
99
+ }
100
+ }
101
+ /**
102
+ * The stack of open elements. Each element has a slot in `items` and `tagIDs`. Once the stack is `INDEX_DEPTH` deep, it
103
+ * indexes its elements so that the searches the parser repeats per token take constant time: it chains the elements
104
+ * with each key, so that the topmost one is found without a scan, and counts per slot the special elements and scope
105
+ * boundaries at or below it, which tell whether one lies between two slots. Removing an element from the middle then
106
+ * leaves its slot free instead of moving the elements above it; `slotBelow` and `slotAbove` step over free slots, and
107
+ * `depth` counts the open elements.
108
+ */
35
109
  export class OpenElementStack {
36
110
  get currentTmplContentOrNode() {
37
111
  return this._isInTemplate() ? this.treeAdapter.getTemplateContent(this.current) : this.current;
@@ -41,15 +115,53 @@ export class OpenElementStack {
41
115
  this.handler = handler;
42
116
  this.items = [];
43
117
  this.tagIDs = [];
118
+ /** The slot of the current element. */
44
119
  this.stackTop = -1;
120
+ /** The number of open elements. */
121
+ this.depth = 0;
45
122
  this.tmplCount = 0;
46
123
  this.currentTagId = $.UNKNOWN;
124
+ this.indexed = false;
125
+ /** The slot of the open element below, and above, each open element's slot; -1 for none. */
126
+ this.below = [];
127
+ this.above = [];
128
+ this.keys = [];
129
+ /** The slot of the next lower, and higher, open element with the same key; -1 for none. */
130
+ this.keyPrev = [];
131
+ this.keyNext = [];
132
+ /** The slot of the topmost open element with each key, -1 for none. */
133
+ this.keyTops = new Int32Array(0);
134
+ /** Per slot, the open special elements, list item boundaries and scope boundaries at or below it. */
135
+ this.specials = [];
136
+ this.listItemBoundaries = [];
137
+ this.scopeBoundaries = [];
138
+ /** Per slot, the slot of the topmost open HTML element at or below it, or -1. */
139
+ this.htmlSlots = [];
140
+ this.formattingSlots = new Map();
47
141
  this.current = document;
48
142
  }
49
- //Index of element
143
+ /** The slot of an open element, or -1. */
50
144
  _indexOf(element) {
145
+ if (this.indexed) {
146
+ const idx = this.formattingSlots.get(element);
147
+ if (idx !== undefined)
148
+ return idx;
149
+ // A formatting element is in `formattingSlots` while it is open.
150
+ if (FORMATTING_TAGS[getTagID(this.treeAdapter.getTagName(element))])
151
+ return -1;
152
+ }
51
153
  return this.items.lastIndexOf(element, this.stackTop);
52
154
  }
155
+ /** The slot of the open element below the one at `idx`, or -1. */
156
+ slotBelow(idx) {
157
+ return this.indexed ? this.below[idx] : idx - 1;
158
+ }
159
+ /** The slot of the open element above the one at `idx`, or -1. */
160
+ slotAbove(idx) {
161
+ if (this.indexed)
162
+ return this.above[idx];
163
+ return idx === this.stackTop ? -1 : idx + 1;
164
+ }
53
165
  //Update current element
54
166
  _isInTemplate() {
55
167
  return this.currentTagId === $.TEMPLATE && this.treeAdapter.getNamespaceURI(this.current) === NS.HTML;
@@ -58,61 +170,303 @@ export class OpenElementStack {
58
170
  this.current = this.items[this.stackTop];
59
171
  this.currentTagId = this.tagIDs[this.stackTop];
60
172
  }
173
+ //Index
174
+ _nameKey(ns, tagName, create) {
175
+ var _a, _b;
176
+ const name = `${ns} ${tagName}`;
177
+ let key = (_a = this.nameKeys) === null || _a === void 0 ? void 0 : _a.get(name);
178
+ if (key === undefined && create) {
179
+ (_b = this.nameKeys) !== null && _b !== void 0 ? _b : (this.nameKeys = new Map());
180
+ key = NAME_KEYS + this.nameKeys.size;
181
+ this.nameKeys.set(name, key);
182
+ if (key === this.keyTops.length) {
183
+ const keyTops = new Int32Array(2 * key).fill(-1);
184
+ keyTops.set(this.keyTops);
185
+ this.keyTops = keyTops;
186
+ }
187
+ }
188
+ return key !== null && key !== void 0 ? key : -1;
189
+ }
190
+ /** Indexes the open elements once the stack is `INDEX_DEPTH` deep. */
191
+ _indexAll() {
192
+ this.indexed = true;
193
+ this.keyTops = new Int32Array(NAME_KEYS).fill(-1);
194
+ for (let i = 0; i <= this.stackTop; i++)
195
+ this._index(i, i - 1);
196
+ }
197
+ /** Indexes the element at `slot`, above the open element at `belowIdx`. */
198
+ _index(slot, belowIdx) {
199
+ const element = this.items[slot];
200
+ const tagID = this.tagIDs[slot];
201
+ const ns = this.treeAdapter.getNamespaceURI(element);
202
+ let key = tagKey(ns, tagID);
203
+ if (key === -1)
204
+ key = this._nameKey(ns, this.treeAdapter.getTagName(element), true);
205
+ const keyPrev = this.keyTops[key];
206
+ this.keys[slot] = key;
207
+ this.keyPrev[slot] = keyPrev;
208
+ this.keyNext[slot] = -1;
209
+ if (keyPrev !== -1)
210
+ this.keyNext[keyPrev] = slot;
211
+ this.keyTops[key] = slot;
212
+ this.above[slot] = -1;
213
+ this._link(slot, belowIdx, ns === NS.HTML);
214
+ if (FORMATTING_TAGS[tagID])
215
+ this.formattingSlots.set(element, slot);
216
+ }
217
+ /** Puts the indexed element at `slot` above the one at `belowIdx`, and counts the elements of each class. */
218
+ _link(slot, belowIdx, isHtml) {
219
+ const key = this.keys[slot];
220
+ const classes = key < NAME_KEYS ? KEY_CLASSES[key] : 0;
221
+ this.below[slot] = belowIdx;
222
+ if (belowIdx === -1) {
223
+ this.specials[slot] = classes & SPECIAL;
224
+ this.listItemBoundaries[slot] = (classes & LIST_ITEM_BOUNDARY) >> 1;
225
+ this.scopeBoundaries[slot] = (classes & SCOPE) >> 2;
226
+ this.htmlSlots[slot] = isHtml ? slot : -1;
227
+ }
228
+ else {
229
+ this.above[belowIdx] = slot;
230
+ this.specials[slot] = this.specials[belowIdx] + (classes & SPECIAL);
231
+ this.listItemBoundaries[slot] = this.listItemBoundaries[belowIdx] + ((classes & LIST_ITEM_BOUNDARY) >> 1);
232
+ this.scopeBoundaries[slot] = this.scopeBoundaries[belowIdx] + ((classes & SCOPE) >> 2);
233
+ this.htmlSlots[slot] = isHtml ? slot : this.htmlSlots[belowIdx];
234
+ }
235
+ }
236
+ /** Takes the element at `slot` out of the index, except for the links between slots. */
237
+ _unindex(slot) {
238
+ const key = this.keys[slot];
239
+ const keyPrev = this.keyPrev[slot];
240
+ const keyNext = this.keyNext[slot];
241
+ if (keyPrev !== -1)
242
+ this.keyNext[keyPrev] = keyNext;
243
+ if (keyNext === -1)
244
+ this.keyTops[key] = keyPrev;
245
+ else
246
+ this.keyPrev[keyNext] = keyPrev;
247
+ if (FORMATTING_TAGS[this.tagIDs[slot]])
248
+ this.formattingSlots.delete(this.items[slot]);
249
+ }
250
+ /** Moves the indexed element at `from` down to the free slot `to`, keeping its key chain in order. */
251
+ _move(from, to) {
252
+ const element = this.items[from];
253
+ const key = this.keys[from];
254
+ const keyPrev = this.keyPrev[from];
255
+ const keyNext = this.keyNext[from];
256
+ this.items[to] = element;
257
+ this.tagIDs[to] = this.tagIDs[from];
258
+ this.keys[to] = key;
259
+ this.keyPrev[to] = keyPrev;
260
+ this.keyNext[to] = keyNext;
261
+ if (keyPrev !== -1)
262
+ this.keyNext[keyPrev] = to;
263
+ if (keyNext === -1)
264
+ this.keyTops[key] = to;
265
+ else
266
+ this.keyPrev[keyNext] = to;
267
+ if (FORMATTING_TAGS[this.tagIDs[to]])
268
+ this.formattingSlots.set(element, to);
269
+ }
270
+ //Search by tag
271
+ /** The slot of the topmost open HTML element with this tag, or -1. */
272
+ indexOfTopmost(tagID) {
273
+ if (this.indexed)
274
+ return this.keyTops[tagID];
275
+ for (let i = this.stackTop; i >= 0; i--) {
276
+ if (this.tagIDs[i] === tagID && this.treeAdapter.getNamespaceURI(this.items[i]) === NS.HTML)
277
+ return i;
278
+ }
279
+ return -1;
280
+ }
281
+ /** The slot of the topmost open HTML element with any of these tags, or -1. */
282
+ indexOfTopmostOf(tagIDs) {
283
+ if (this.indexed) {
284
+ let top = -1;
285
+ for (const tagID of tagIDs) {
286
+ if (this.keyTops[tagID] > top)
287
+ top = this.keyTops[tagID];
288
+ }
289
+ return top;
290
+ }
291
+ for (let i = this.stackTop; i >= 0; i--) {
292
+ if (tagIDs.includes(this.tagIDs[i]) && this.treeAdapter.getNamespaceURI(this.items[i]) === NS.HTML)
293
+ return i;
294
+ }
295
+ return -1;
296
+ }
297
+ /** The slot of the topmost open element in this namespace with this tag name, or -1. */
298
+ indexOfTopmostByName(ns, tagName) {
299
+ if (this.indexed) {
300
+ let key = tagKey(ns, getTagID(tagName));
301
+ if (key === -1)
302
+ key = this._nameKey(ns, tagName, false);
303
+ return key === -1 ? -1 : this.keyTops[key];
304
+ }
305
+ for (let i = this.stackTop; i >= 0; i--) {
306
+ const element = this.items[i];
307
+ if (this.treeAdapter.getTagName(element) === tagName && this.treeAdapter.getNamespaceURI(element) === ns) {
308
+ return i;
309
+ }
310
+ }
311
+ return -1;
312
+ }
313
+ /** The slot of the topmost open HTML element, or -1. */
314
+ indexOfTopmostHtmlElement() {
315
+ if (this.indexed)
316
+ return this.stackTop === -1 ? -1 : this.htmlSlots[this.stackTop];
317
+ let i = this.stackTop;
318
+ while (i >= 0 && this.treeAdapter.getNamespaceURI(this.items[i]) !== NS.HTML)
319
+ i--;
320
+ return i;
321
+ }
322
+ /** Whether an element of the classes is open above the slot `idx`, on a stack that is not indexed. */
323
+ _hasAbove(idx, classes) {
324
+ for (let i = this.stackTop; i > idx; i--) {
325
+ const key = tagKey(this.treeAdapter.getNamespaceURI(this.items[i]), this.tagIDs[i]);
326
+ if (key !== -1 && KEY_CLASSES[key] & classes)
327
+ return true;
328
+ }
329
+ return false;
330
+ }
331
+ /** Whether a special element is open above the slot `idx`. */
332
+ hasSpecialElementAbove(idx) {
333
+ return this.indexed ? this.specials[this.stackTop] !== this.specials[idx] : this._hasAbove(idx, SPECIAL);
334
+ }
335
+ /** Whether a special element other than HTML `address`, `div` and `p` is open above the slot `idx`. */
336
+ hasListItemBoundaryAbove(idx) {
337
+ return this.indexed
338
+ ? this.listItemBoundaries[this.stackTop] !== this.listItemBoundaries[idx]
339
+ : this._hasAbove(idx, LIST_ITEM_BOUNDARY);
340
+ }
61
341
  //Mutations
62
342
  push(element, tagID) {
343
+ const belowIdx = this.stackTop;
63
344
  this.stackTop++;
345
+ this.depth++;
64
346
  this.items[this.stackTop] = element;
65
347
  this.current = element;
66
348
  this.tagIDs[this.stackTop] = tagID;
67
349
  this.currentTagId = tagID;
350
+ if (this.indexed)
351
+ this._index(this.stackTop, belowIdx);
352
+ else if (this.depth === INDEX_DEPTH)
353
+ this._indexAll();
68
354
  if (this._isInTemplate()) {
69
355
  this.tmplCount++;
70
356
  }
71
357
  this.handler.onItemPush(element, tagID, true);
72
358
  }
73
- pop() {
74
- const popped = this.current;
359
+ /** Pops the current element without calling the handler. */
360
+ _popCurrent() {
75
361
  if (this.tmplCount > 0 && this._isInTemplate()) {
76
362
  this.tmplCount--;
77
363
  }
78
- this.stackTop--;
364
+ if (this.indexed) {
365
+ this._unindex(this.stackTop);
366
+ this.stackTop = this.below[this.stackTop];
367
+ if (this.stackTop !== -1)
368
+ this.above[this.stackTop] = -1;
369
+ }
370
+ else {
371
+ this.stackTop--;
372
+ }
373
+ this.depth--;
79
374
  this._updateCurrentElement();
375
+ }
376
+ pop() {
377
+ const popped = this.current;
378
+ this._popCurrent();
80
379
  this.handler.onItemPop(popped, true);
81
380
  }
82
381
  replace(oldElement, newElement) {
83
382
  const idx = this._indexOf(oldElement);
84
383
  this.items[idx] = newElement;
384
+ if (this.indexed && FORMATTING_TAGS[this.tagIDs[idx]]) {
385
+ this.formattingSlots.delete(oldElement);
386
+ this.formattingSlots.set(newElement, idx);
387
+ }
85
388
  if (idx === this.stackTop) {
86
389
  this.current = newElement;
87
390
  }
88
391
  }
89
- insertAfter(referenceElement, newElement, newElementID) {
90
- const insertionIdx = this._indexOf(referenceElement) + 1;
91
- this.items.splice(insertionIdx, 0, newElement);
92
- this.tagIDs.splice(insertionIdx, 0, newElementID);
93
- this.stackTop++;
94
- if (insertionIdx === this.stackTop) {
95
- this._updateCurrentElement();
392
+ /**
393
+ * The adoption agency's last stack step: removes the formatting element at `formattingIdx` and opens `element`
394
+ * directly above the furthest block at `furthestBlockIdx`. On an indexed stack, only formatting elements the agency
395
+ * kept are still open between them; the furthest block, with those directly below it, moves down by one slot, and
396
+ * `element` takes the furthest block's slot. No open element above the furthest block moves, and as all these
397
+ * elements are HTML elements and only the furthest block is special, the counts above it stay the same.
398
+ */
399
+ replaceFormattingElement(formattingIdx, furthestBlockIdx, element, tagID) {
400
+ const formattingElement = this.items[formattingIdx];
401
+ if (!this.indexed) {
402
+ this.items.splice(formattingIdx, 1);
403
+ this.tagIDs.splice(formattingIdx, 1);
404
+ this.handler.onItemPop(formattingElement, false);
405
+ this.items.splice(furthestBlockIdx, 0, element);
406
+ this.tagIDs.splice(furthestBlockIdx, 0, tagID);
407
+ if (furthestBlockIdx === this.stackTop)
408
+ this._updateCurrentElement();
409
+ if (this.current && this.currentTagId !== undefined) {
410
+ this.handler.onItemPush(this.current, this.currentTagId, furthestBlockIdx === this.stackTop);
411
+ }
412
+ return;
413
+ }
414
+ const key = this.keys[formattingIdx];
415
+ const belowIdx = this.below[formattingIdx];
416
+ const aboveIdx = this.above[furthestBlockIdx];
417
+ const kept = [];
418
+ for (let i = this.above[formattingIdx]; i !== furthestBlockIdx; i = this.above[i])
419
+ kept.push(i);
420
+ // The list keeps open formatting elements in stack order, so a kept element with the formatting element's tag
421
+ // would have been the formatting element: the next element with its key is above the furthest block.
422
+ const keyNext = this.keyNext[formattingIdx];
423
+ this._unindex(formattingIdx);
424
+ this.items[formattingIdx] = undefined;
425
+ this.handler.onItemPop(formattingElement, false);
426
+ let free = furthestBlockIdx - 1;
427
+ while (kept.length > 0 && kept[kept.length - 1] === free) {
428
+ kept.pop();
429
+ free--;
430
+ }
431
+ for (let i = free + 1; i <= furthestBlockIdx; i++)
432
+ this._move(i, i - 1);
433
+ const keyPrev = keyNext === -1 ? this.keyTops[key] : this.keyPrev[keyNext];
434
+ this.items[furthestBlockIdx] = element;
435
+ this.tagIDs[furthestBlockIdx] = tagID;
436
+ this.keys[furthestBlockIdx] = key;
437
+ this.keyPrev[furthestBlockIdx] = keyPrev;
438
+ this.keyNext[furthestBlockIdx] = keyNext;
439
+ if (keyPrev !== -1)
440
+ this.keyNext[keyPrev] = furthestBlockIdx;
441
+ if (keyNext === -1)
442
+ this.keyTops[key] = furthestBlockIdx;
443
+ else
444
+ this.keyPrev[keyNext] = furthestBlockIdx;
445
+ if (FORMATTING_TAGS[tagID])
446
+ this.formattingSlots.set(element, furthestBlockIdx);
447
+ let prev = belowIdx;
448
+ for (const i of kept) {
449
+ this._link(i, prev, true);
450
+ prev = i;
96
451
  }
452
+ for (let i = free; i < furthestBlockIdx; i++) {
453
+ this._link(i, prev, true);
454
+ prev = i;
455
+ }
456
+ this._link(furthestBlockIdx, prev, true);
457
+ if (aboveIdx === -1)
458
+ this._updateCurrentElement();
97
459
  if (this.current && this.currentTagId !== undefined) {
98
- this.handler.onItemPush(this.current, this.currentTagId, insertionIdx === this.stackTop);
460
+ this.handler.onItemPush(this.current, this.currentTagId, aboveIdx === -1);
99
461
  }
100
462
  }
101
463
  popUntilTagNamePopped(tagName) {
102
- let targetIdx = this.stackTop + 1;
103
- do {
104
- targetIdx = this.tagIDs.lastIndexOf(tagName, targetIdx - 1);
105
- } while (targetIdx > 0 && this.treeAdapter.getNamespaceURI(this.items[targetIdx]) !== NS.HTML);
106
- this.shortenToLength(Math.max(targetIdx, 0));
464
+ this.shortenToLength(Math.max(this.indexOfTopmost(tagName), 0));
107
465
  }
108
466
  shortenToLength(idx) {
109
467
  while (this.stackTop >= idx) {
110
468
  const popped = this.current;
111
- if (this.tmplCount > 0 && this._isInTemplate()) {
112
- this.tmplCount -= 1;
113
- }
114
- this.stackTop--;
115
- this._updateCurrentElement();
469
+ this._popCurrent();
116
470
  this.handler.onItemPop(popped, this.stackTop < idx);
117
471
  }
118
472
  }
@@ -120,15 +474,11 @@ export class OpenElementStack {
120
474
  const idx = this._indexOf(element);
121
475
  this.shortenToLength(Math.max(idx, 0));
122
476
  }
123
- popUntilPopped(tagNames, targetNS) {
124
- const idx = this._indexOfTagNames(tagNames, targetNS);
125
- this.shortenToLength(Math.max(idx, 0));
126
- }
127
477
  popUntilNumberedHeaderPopped() {
128
- this.popUntilPopped(NUMBERED_HEADERS, NS.HTML);
478
+ this.shortenToLength(Math.max(this.indexOfTopmostOf(NUMBERED_HEADERS), 0));
129
479
  }
130
480
  popUntilTableCellPopped() {
131
- this.popUntilPopped(TABLE_CELLS, NS.HTML);
481
+ this.shortenToLength(Math.max(this.indexOfTopmostOf(TABLE_CELLS), 0));
132
482
  }
133
483
  popAllUpToHtmlElement() {
134
484
  //NOTE: here we assume that the root <html> element is always first in the open element stack, so
@@ -136,41 +486,57 @@ export class OpenElementStack {
136
486
  this.tmplCount = 0;
137
487
  this.shortenToLength(1);
138
488
  }
139
- _indexOfTagNames(tagNames, namespace) {
140
- for (let i = this.stackTop; i >= 0; i--) {
141
- if (tagNames.has(this.tagIDs[i]) && this.treeAdapter.getNamespaceURI(this.items[i]) === namespace) {
142
- return i;
143
- }
144
- }
145
- return -1;
146
- }
147
- clearBackTo(tagNames, targetNS) {
148
- const idx = this._indexOfTagNames(tagNames, targetNS);
149
- this.shortenToLength(idx + 1);
150
- }
151
489
  clearBackToTableContext() {
152
- this.clearBackTo(TABLE_CONTEXT, NS.HTML);
490
+ this.shortenToLength(this.indexOfTopmostOf(TABLE_CONTEXT) + 1);
153
491
  }
154
492
  clearBackToTableBodyContext() {
155
- this.clearBackTo(TABLE_BODY_CONTEXT, NS.HTML);
493
+ this.shortenToLength(this.indexOfTopmostOf(TABLE_BODY_CONTEXT) + 1);
156
494
  }
157
495
  clearBackToTableRowContext() {
158
- this.clearBackTo(TABLE_ROW_CONTEXT, NS.HTML);
496
+ this.shortenToLength(this.indexOfTopmostOf(TABLE_ROW_CONTEXT) + 1);
159
497
  }
160
498
  remove(element) {
161
499
  const idx = this._indexOf(element);
162
- if (idx >= 0) {
163
- if (idx === this.stackTop) {
164
- this.pop();
500
+ if (idx !== -1)
501
+ this.removeAt(idx);
502
+ }
503
+ /** Removes the open element at the slot `idx`; on an indexed stack, the elements above it keep their slots. */
504
+ removeAt(idx) {
505
+ if (idx === this.stackTop) {
506
+ this.pop();
507
+ return;
508
+ }
509
+ const element = this.items[idx];
510
+ this.depth--;
511
+ if (this.indexed) {
512
+ const classes = this.keys[idx] < NAME_KEYS ? KEY_CLASSES[this.keys[idx]] : 0;
513
+ const belowIdx = this.below[idx];
514
+ const aboveIdx = this.above[idx];
515
+ this._unindex(idx);
516
+ this.items[idx] = undefined;
517
+ this.below[aboveIdx] = belowIdx;
518
+ if (belowIdx !== -1)
519
+ this.above[belowIdx] = aboveIdx;
520
+ if (classes !== 0) {
521
+ for (let i = aboveIdx; i !== -1; i = this.above[i]) {
522
+ this.specials[i] -= classes & SPECIAL;
523
+ this.listItemBoundaries[i] -= (classes & LIST_ITEM_BOUNDARY) >> 1;
524
+ this.scopeBoundaries[i] -= (classes & SCOPE) >> 2;
525
+ }
165
526
  }
166
- else {
167
- this.items.splice(idx, 1);
168
- this.tagIDs.splice(idx, 1);
169
- this.stackTop--;
170
- this._updateCurrentElement();
171
- this.handler.onItemPop(element, false);
527
+ // Foreign elements directly above an HTML element refer to it as the topmost HTML element below them.
528
+ const html = belowIdx === -1 ? -1 : this.htmlSlots[belowIdx];
529
+ for (let i = aboveIdx; i !== -1 && this.htmlSlots[i] === idx; i = this.above[i]) {
530
+ this.htmlSlots[i] = html;
172
531
  }
173
532
  }
533
+ else {
534
+ this.items.splice(idx, 1);
535
+ this.tagIDs.splice(idx, 1);
536
+ this.stackTop--;
537
+ this._updateCurrentElement();
538
+ }
539
+ this.handler.onItemPop(element, false);
174
540
  }
175
541
  //Search
176
542
  tryPeekProperlyNestedBodyElement() {
@@ -181,110 +547,89 @@ export class OpenElementStack {
181
547
  return this._indexOf(element) > -1;
182
548
  }
183
549
  getCommonAncestor(element) {
184
- const elementIdx = this._indexOf(element) - 1;
185
- return elementIdx >= 0 ? this.items[elementIdx] : null;
550
+ const idx = this._indexOf(element);
551
+ const belowIdx = idx === -1 ? -1 : this.slotBelow(idx);
552
+ return belowIdx === -1 ? null : this.items[belowIdx];
186
553
  }
187
554
  isRootHtmlElementCurrent() {
188
555
  return this.stackTop === 0 && this.tagIDs[0] === $.HTML;
189
556
  }
190
557
  //Element in scope
191
- hasInDynamicScope(tagName, htmlScope) {
192
- for (let i = this.stackTop; i >= 0; i--) {
193
- const tn = this.tagIDs[i];
194
- switch (this.treeAdapter.getNamespaceURI(this.items[i])) {
195
- case NS.HTML: {
196
- if (tn === tagName)
197
- return true;
198
- if (htmlScope.has(tn))
199
- return false;
200
- break;
201
- }
202
- case NS.SVG: {
203
- if (SCOPING_ELEMENTS_SVG.has(tn))
204
- return false;
205
- break;
206
- }
207
- case NS.MATHML: {
208
- if (SCOPING_ELEMENTS_MATHML.has(tn))
209
- return false;
210
- break;
211
- }
558
+ /**
559
+ * On an indexed stack: whether no element of the `scope` class is open above the slot `target`. A target that is not
560
+ * open is in scope only on a stack without such an element, as a scan from the top would end there.
561
+ */
562
+ _isInScope(target, scope) {
563
+ let boundary = -1;
564
+ switch (scope) {
565
+ case LIST_ITEM_SCOPE: {
566
+ boundary = Math.max(this.keyTops[$.OL], this.keyTops[$.UL]);
567
+ break;
568
+ }
569
+ case BUTTON_SCOPE: {
570
+ boundary = this.keyTops[$.BUTTON];
571
+ break;
572
+ }
573
+ case TABLE_SCOPE: {
574
+ boundary = Math.max(this.keyTops[$.TABLE], this.keyTops[$.HTML]);
575
+ return target === -1 ? boundary === -1 : target >= boundary;
212
576
  }
577
+ default:
578
+ // Only the scope boundaries
579
+ }
580
+ return target === -1
581
+ ? boundary === -1 && (this.stackTop === -1 || this.scopeBoundaries[this.stackTop] === 0)
582
+ : target >= boundary && this.scopeBoundaries[this.stackTop] === this.scopeBoundaries[target];
583
+ }
584
+ /**
585
+ * On a stack that is not indexed: whether an HTML element with the tag, or one of the tags, and if passed the element
586
+ * itself, is open above every element of the `scope` class; true if neither is open.
587
+ */
588
+ _scanScope(tagID, tagIDs, scope, element) {
589
+ for (let i = this.stackTop; i >= 0; i--) {
590
+ const key = tagKey(this.treeAdapter.getNamespaceURI(this.items[i]), this.tagIDs[i]);
591
+ if (key === -1)
592
+ continue;
593
+ if ((tagIDs ? tagIDs.includes(key) : key === tagID) && (!element || this.items[i] === element))
594
+ return true;
595
+ if (KEY_CLASSES[key] & scope)
596
+ return false;
213
597
  }
214
598
  return true;
215
599
  }
216
- hasInScope(tagName) {
217
- return this.hasInDynamicScope(tagName, SCOPING_ELEMENTS_HTML);
600
+ /** If `element` is passed, only that element (which has to be a `tagName` element) is looked for. */
601
+ hasInScope(tagName, element) {
602
+ if (!this.indexed)
603
+ return this._scanScope(tagName, null, SCOPE, element);
604
+ return this._isInScope(element ? this._indexOf(element) : this.keyTops[tagName], SCOPE);
218
605
  }
219
606
  hasInListItemScope(tagName) {
220
- return this.hasInDynamicScope(tagName, SCOPING_ELEMENTS_HTML_LIST);
607
+ return this.indexed
608
+ ? this._isInScope(this.keyTops[tagName], LIST_ITEM_SCOPE)
609
+ : this._scanScope(tagName, null, LIST_ITEM_SCOPE);
221
610
  }
222
611
  hasInButtonScope(tagName) {
223
- return this.hasInDynamicScope(tagName, SCOPING_ELEMENTS_HTML_BUTTON);
612
+ return this.indexed
613
+ ? this._isInScope(this.keyTops[tagName], BUTTON_SCOPE)
614
+ : this._scanScope(tagName, null, BUTTON_SCOPE);
224
615
  }
225
616
  hasNumberedHeaderInScope() {
226
- for (let i = this.stackTop; i >= 0; i--) {
227
- const tn = this.tagIDs[i];
228
- switch (this.treeAdapter.getNamespaceURI(this.items[i])) {
229
- case NS.HTML: {
230
- if (NUMBERED_HEADERS.has(tn))
231
- return true;
232
- if (SCOPING_ELEMENTS_HTML.has(tn))
233
- return false;
234
- break;
235
- }
236
- case NS.SVG: {
237
- if (SCOPING_ELEMENTS_SVG.has(tn))
238
- return false;
239
- break;
240
- }
241
- case NS.MATHML: {
242
- if (SCOPING_ELEMENTS_MATHML.has(tn))
243
- return false;
244
- break;
245
- }
246
- }
247
- }
248
- return true;
617
+ return this.indexed
618
+ ? this._isInScope(this.indexOfTopmostOf(NUMBERED_HEADERS), SCOPE)
619
+ : this._scanScope($.UNKNOWN, NUMBERED_HEADERS, SCOPE);
249
620
  }
250
621
  hasInTableScope(tagName) {
251
- for (let i = this.stackTop; i >= 0; i--) {
252
- if (this.treeAdapter.getNamespaceURI(this.items[i]) !== NS.HTML) {
253
- continue;
254
- }
255
- switch (this.tagIDs[i]) {
256
- case tagName: {
257
- return true;
258
- }
259
- case $.TABLE:
260
- case $.HTML: {
261
- return false;
262
- }
263
- }
264
- }
265
- return true;
622
+ return this.indexed
623
+ ? this._isInScope(this.keyTops[tagName], TABLE_SCOPE)
624
+ : this._scanScope(tagName, null, TABLE_SCOPE);
266
625
  }
267
626
  hasTableBodyContextInTableScope() {
268
- for (let i = this.stackTop; i >= 0; i--) {
269
- if (this.treeAdapter.getNamespaceURI(this.items[i]) !== NS.HTML) {
270
- continue;
271
- }
272
- switch (this.tagIDs[i]) {
273
- case $.TBODY:
274
- case $.THEAD:
275
- case $.TFOOT: {
276
- return true;
277
- }
278
- case $.TABLE:
279
- case $.HTML: {
280
- return false;
281
- }
282
- }
283
- }
284
- return true;
627
+ return this.indexed
628
+ ? this._isInScope(this.indexOfTopmostOf(TABLE_BODIES), TABLE_SCOPE)
629
+ : this._scanScope($.UNKNOWN, TABLE_BODIES, TABLE_SCOPE);
285
630
  }
286
631
  hasInSelectScope(tagName) {
287
- for (let i = this.stackTop; i >= 0; i--) {
632
+ for (let i = this.stackTop; i >= 0; i = this.slotBelow(i)) {
288
633
  if (this.treeAdapter.getNamespaceURI(this.items[i]) !== NS.HTML) {
289
634
  continue;
290
635
  }
@@ -305,19 +650,24 @@ export class OpenElementStack {
305
650
  }
306
651
  //Implied end tags
307
652
  generateImpliedEndTags() {
308
- while (this.currentTagId !== undefined && IMPLICIT_END_TAG_REQUIRED.has(this.currentTagId)) {
653
+ while (this.currentTagId !== undefined &&
654
+ IMPLICIT_END_TAG_REQUIRED.has(this.currentTagId) &&
655
+ this.treeAdapter.getNamespaceURI(this.current) === NS.HTML) {
309
656
  this.pop();
310
657
  }
311
658
  }
312
659
  generateImpliedEndTagsThoroughly() {
313
- while (this.currentTagId !== undefined && IMPLICIT_END_TAG_REQUIRED_THOROUGHLY.has(this.currentTagId)) {
660
+ while (this.currentTagId !== undefined &&
661
+ IMPLICIT_END_TAG_REQUIRED_THOROUGHLY.has(this.currentTagId) &&
662
+ this.treeAdapter.getNamespaceURI(this.current) === NS.HTML) {
314
663
  this.pop();
315
664
  }
316
665
  }
317
666
  generateImpliedEndTagsWithExclusion(exclusionId) {
318
667
  while (this.currentTagId !== undefined &&
319
668
  this.currentTagId !== exclusionId &&
320
- IMPLICIT_END_TAG_REQUIRED_THOROUGHLY.has(this.currentTagId)) {
669
+ IMPLICIT_END_TAG_REQUIRED_THOROUGHLY.has(this.currentTagId) &&
670
+ this.treeAdapter.getNamespaceURI(this.current) === NS.HTML) {
321
671
  this.pop();
322
672
  }
323
673
  }