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.
- package/README.md +12 -0
- package/dist/common/foreign-content.d.ts +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/parser/formatting-element-list.d.ts +38 -10
- package/dist/parser/formatting-element-list.js +153 -70
- package/dist/parser/index.d.ts +18 -9
- package/dist/parser/index.js +215 -152
- package/dist/parser/open-element-stack.d.ts +85 -8
- package/dist/parser/open-element-stack.js +500 -150
- package/dist/serializer/index.js +48 -34
- package/dist/tokenizer/index.d.ts +37 -7
- package/dist/tokenizer/index.js +321 -71
- package/dist/tokenizer/preprocessor.d.ts +8 -1
- package/dist/tokenizer/preprocessor.js +20 -7
- package/dist/tree-adapters/default.js +50 -6
- package/dist/tree-adapters/interface.d.ts +17 -0
- package/package.json +7 -3
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { TAG_ID as $, NS,
|
|
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
|
-
|
|
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
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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
|
-
|
|
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
|
-
|
|
74
|
-
|
|
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.
|
|
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
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
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,
|
|
460
|
+
this.handler.onItemPush(this.current, this.currentTagId, aboveIdx === -1);
|
|
99
461
|
}
|
|
100
462
|
}
|
|
101
463
|
popUntilTagNamePopped(tagName) {
|
|
102
|
-
|
|
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
|
-
|
|
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.
|
|
478
|
+
this.shortenToLength(Math.max(this.indexOfTopmostOf(NUMBERED_HEADERS), 0));
|
|
129
479
|
}
|
|
130
480
|
popUntilTableCellPopped() {
|
|
131
|
-
this.
|
|
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.
|
|
490
|
+
this.shortenToLength(this.indexOfTopmostOf(TABLE_CONTEXT) + 1);
|
|
153
491
|
}
|
|
154
492
|
clearBackToTableBodyContext() {
|
|
155
|
-
this.
|
|
493
|
+
this.shortenToLength(this.indexOfTopmostOf(TABLE_BODY_CONTEXT) + 1);
|
|
156
494
|
}
|
|
157
495
|
clearBackToTableRowContext() {
|
|
158
|
-
this.
|
|
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
|
|
163
|
-
|
|
164
|
-
|
|
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
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
this.
|
|
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
|
|
185
|
-
|
|
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
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
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
|
-
|
|
217
|
-
|
|
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.
|
|
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.
|
|
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
|
-
|
|
227
|
-
|
|
228
|
-
|
|
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
|
-
|
|
252
|
-
|
|
253
|
-
|
|
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
|
-
|
|
269
|
-
|
|
270
|
-
|
|
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 &&
|
|
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 &&
|
|
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
|
}
|