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 CHANGED
@@ -19,6 +19,18 @@
19
19
  📖 <a href="https://parse5.js.org/modules/parse5.html"><b>Documentation</b></a> 📖
20
20
  </p>
21
21
 
22
+ ## Usage
23
+
24
+ Parse HTML into a document tree and serialize it back to HTML.
25
+
26
+ ```js
27
+ import { parse, serialize } from 'parse5';
28
+
29
+ const document = parse('<p>Hello, world!</p>');
30
+ console.log(serialize(document));
31
+ // <html><head></head><body><p>Hello, world!</p></body></html>
32
+ ```
33
+
22
34
  ---
23
35
 
24
36
  <p align="center">
@@ -6,4 +6,4 @@ export declare function adjustTokenMathMLAttrs(token: TagToken): void;
6
6
  export declare function adjustTokenSVGAttrs(token: TagToken): void;
7
7
  export declare function adjustTokenXMLAttrs(token: TagToken): void;
8
8
  export declare function adjustTokenSVGTagName(token: TagToken): void;
9
- export declare function isIntegrationPoint(tn: $, ns: NS, attrs: Attribute[], foreignNS?: NS): boolean;
9
+ export declare function isIntegrationPoint(tn: $, ns: NS, attrs: readonly Attribute[], foreignNS?: NS): boolean;
package/dist/index.d.ts CHANGED
@@ -16,8 +16,8 @@ export declare namespace DefaultTreeAdapterTypes {
16
16
  type Node = DefaultTreeAdapter.Node;
17
17
  type DefaultTreeAdapterMap = DefaultTreeAdapter.DefaultTreeAdapterMap;
18
18
  }
19
- export type { TreeAdapter, TreeAdapterTypeMap } from './tree-adapters/interface.js';
20
- export { type ParserOptions, /** @internal */ Parser } from './parser/index.js';
19
+ export type { ElementCreationContext, TreeAdapter, TreeAdapterTypeMap } from './tree-adapters/interface.js';
20
+ export { type ParserOptions, /** @internal */ Parser, InsertionMode } from './parser/index.js';
21
21
  export { serialize, serializeOuter, type SerializerOptions } from './serializer/index.js';
22
22
  export { ERR as ErrorCodes, type ParserError, type ParserErrorHandler } from './common/error-codes.js';
23
23
  /** @internal */
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { Parser } from './parser/index.js';
2
2
  export { defaultTreeAdapter } from './tree-adapters/default.js';
3
- export { /** @internal */ Parser } from './parser/index.js';
3
+ export { /** @internal */ Parser, InsertionMode } from './parser/index.js';
4
4
  export { serialize, serializeOuter } from './serializer/index.js';
5
5
  export { ERR as ErrorCodes } from './common/error-codes.js';
6
6
  /** @internal */
@@ -1,29 +1,57 @@
1
1
  import type { TagToken } from '../common/token.js';
2
- import type { TreeAdapter, TreeAdapterTypeMap } from '../tree-adapters/interface.js';
2
+ import type { TreeAdapterTypeMap } from '../tree-adapters/interface.js';
3
3
  export declare enum EntryType {
4
4
  Marker = 0,
5
5
  Element = 1
6
6
  }
7
- interface MarkerEntry {
7
+ interface MarkerEntry<T extends TreeAdapterTypeMap> {
8
8
  type: EntryType.Marker;
9
+ older: Entry<T> | null;
10
+ newer: Entry<T> | null;
11
+ /** The index of the entries before this marker. */
12
+ outerIndex: RegionIndex<T> | null;
9
13
  }
10
- export interface ElementEntry<T extends TreeAdapterTypeMap> {
14
+ /** The entries with one tag name after one marker are chained from a head, newest first. */
15
+ interface SameTagLink<T extends TreeAdapterTypeMap> {
16
+ olderSameTag: ElementEntry<T> | null;
17
+ }
18
+ export interface ElementEntry<T extends TreeAdapterTypeMap> extends SameTagLink<T> {
11
19
  type: EntryType.Element;
12
20
  element: T['element'];
13
21
  token: TagToken;
22
+ older: Entry<T> | null;
23
+ newer: Entry<T> | null;
24
+ /** The entry's group of entries with the same tag name and attributes; null until indexed. */
25
+ ark: Ark<T> | null;
26
+ /** The next newer entry with the same tag name, or the chain's head; null until indexed. */
27
+ newerSameTag: SameTagLink<T> | null;
28
+ }
29
+ export type Entry<T extends TreeAdapterTypeMap> = MarkerEntry<T> | ElementEntry<T>;
30
+ /** The entries after one marker with one tag name and attributes, oldest first, and their key in the region's arks. */
31
+ interface Ark<T extends TreeAdapterTypeMap> {
32
+ entries: ElementEntry<T>[];
33
+ key: string;
34
+ }
35
+ /** The entries after one marker by tag name and attributes, and the newest of them by tag name. */
36
+ interface RegionIndex<T extends TreeAdapterTypeMap> {
37
+ arks: Map<string, Ark<T>>;
38
+ tagNames: Map<string, SameTagLink<T>>;
14
39
  }
15
- export type Entry<T extends TreeAdapterTypeMap> = MarkerEntry | ElementEntry<T>;
16
40
  export declare class FormattingElementList<T extends TreeAdapterTypeMap> {
17
- private treeAdapter;
18
- entries: Entry<T>[];
41
+ newest: Entry<T> | null;
19
42
  bookmark: Entry<T> | null;
20
- constructor(treeAdapter: TreeAdapter<T>);
21
- private _getNoahArkConditionCandidates;
22
- private _ensureNoahArkCondition;
43
+ /** The index of the entries after the last marker; null only while there are at most NOAH_ARK_CAPACITY of them. */
44
+ private index;
45
+ /** The entries by element, except possibly those after the last marker while they are not indexed. */
46
+ private elementEntries;
47
+ /** Returns the entry's ark, or null if the entries after the last marker are not indexed. */
48
+ private _insertElementEntry;
49
+ private _addToIndex;
23
50
  insertMarker(): void;
24
51
  pushElement(element: T['element'], token: TagToken): void;
25
52
  insertElementAfterBookmark(element: T['element'], token: TagToken): void;
26
- removeEntry(entry: Entry<T>): void;
53
+ removeEntry(entry: ElementEntry<T>): void;
54
+ replaceElement(entry: ElementEntry<T>, element: T['element']): void;
27
55
  /**
28
56
  * Clears the list of formatting elements up to the last marker.
29
57
  *
@@ -5,85 +5,151 @@ export var EntryType;
5
5
  EntryType[EntryType["Marker"] = 0] = "Marker";
6
6
  EntryType[EntryType["Element"] = 1] = "Element";
7
7
  })(EntryType || (EntryType = {}));
8
- const MARKER = { type: EntryType.Marker };
9
8
  //List of formatting elements
10
9
  export class FormattingElementList {
11
- constructor(treeAdapter) {
12
- this.treeAdapter = treeAdapter;
13
- this.entries = [];
10
+ constructor() {
11
+ this.newest = null;
14
12
  this.bookmark = null;
13
+ /** The index of the entries after the last marker; null only while there are at most NOAH_ARK_CAPACITY of them. */
14
+ this.index = null;
15
+ /** The entries by element, except possibly those after the last marker while they are not indexed. */
16
+ this.elementEntries = new Map();
15
17
  }
16
- //Noah Ark's condition
17
- //OPTIMIZATION: at first we try to find possible candidates for exclusion using
18
- //lightweight heuristics without thorough attributes check.
19
- _getNoahArkConditionCandidates(newElement, neAttrs) {
20
- const candidates = [];
21
- const neAttrsLength = neAttrs.length;
22
- const neTagName = this.treeAdapter.getTagName(newElement);
23
- const neNamespaceURI = this.treeAdapter.getNamespaceURI(newElement);
24
- for (let i = 0; i < this.entries.length; i++) {
25
- const entry = this.entries[i];
26
- if (entry.type === EntryType.Marker) {
27
- break;
28
- }
29
- const { element } = entry;
30
- if (this.treeAdapter.getTagName(element) === neTagName &&
31
- this.treeAdapter.getNamespaceURI(element) === neNamespaceURI) {
32
- const elementAttrs = this.treeAdapter.getAttrList(element);
33
- if (elementAttrs.length === neAttrsLength) {
34
- candidates.push({ idx: i, attrs: elementAttrs });
35
- }
36
- }
18
+ /** Returns the entry's ark, or null if the entries after the last marker are not indexed. */
19
+ _insertElementEntry(element, token, older) {
20
+ const newer = older === null ? null : older.newer;
21
+ const entry = {
22
+ type: EntryType.Element,
23
+ element,
24
+ token,
25
+ older,
26
+ newer,
27
+ ark: null,
28
+ olderSameTag: null,
29
+ newerSameTag: null,
30
+ };
31
+ if (newer === null) {
32
+ this.newest = entry;
37
33
  }
38
- return candidates;
34
+ else {
35
+ newer.older = entry;
36
+ }
37
+ if (older !== null) {
38
+ older.newer = entry;
39
+ }
40
+ return this.index === null ? null : this._addToIndex(this.index, entry);
39
41
  }
40
- _ensureNoahArkCondition(newElement) {
41
- if (this.entries.length < NOAH_ARK_CAPACITY)
42
- return;
43
- const neAttrs = this.treeAdapter.getAttrList(newElement);
44
- const candidates = this._getNoahArkConditionCandidates(newElement, neAttrs);
45
- if (candidates.length < NOAH_ARK_CAPACITY)
46
- return;
47
- //NOTE: build attrs map for the new element, so we can perform fast lookups
48
- const neAttrsMap = new Map(neAttrs.map((neAttr) => [neAttr.name, neAttr.value]));
49
- let validCandidates = 0;
50
- //NOTE: remove bottommost candidates, until Noah's Ark condition will not be met
51
- for (let i = 0; i < candidates.length; i++) {
52
- const candidate = candidates[i];
53
- // We know that `candidate.attrs.length === neAttrs.length`
54
- if (candidate.attrs.every((cAttr) => neAttrsMap.get(cAttr.name) === cAttr.value)) {
55
- validCandidates += 1;
56
- if (validCandidates >= NOAH_ARK_CAPACITY) {
57
- this.entries.splice(candidate.idx, 1);
58
- }
59
- }
42
+ _addToIndex(index, entry) {
43
+ const { tagName, attrs } = entry.token;
44
+ // Names are unique and the tokenizer replaces NUL, so the sorted pairs identify the attribute set.
45
+ // Sorting costs O(k log k) comparisons for a tag with k attributes.
46
+ const key = tagName +
47
+ attrs
48
+ .map(({ name, value }) => `\0${name}\0${value}`)
49
+ .sort()
50
+ .join('');
51
+ let ark = index.arks.get(key);
52
+ if (ark === undefined) {
53
+ ark = { entries: [], key };
54
+ index.arks.set(key, ark);
55
+ }
56
+ ark.entries.push(entry);
57
+ entry.ark = ark;
58
+ this.elementEntries.set(entry.element, entry);
59
+ let head = index.tagNames.get(tagName);
60
+ if (head === undefined) {
61
+ head = { olderSameTag: null };
62
+ index.tagNames.set(tagName, head);
63
+ }
64
+ entry.olderSameTag = head.olderSameTag;
65
+ entry.newerSameTag = head;
66
+ if (head.olderSameTag !== null) {
67
+ head.olderSameTag.newerSameTag = entry;
60
68
  }
69
+ head.olderSameTag = entry;
70
+ return ark;
61
71
  }
62
72
  //Mutations
63
73
  insertMarker() {
64
- this.entries.unshift(MARKER);
74
+ if (this.index === null) {
75
+ for (let entry = this.newest; entry !== null && entry.type === EntryType.Element; entry = entry.older) {
76
+ this.elementEntries.set(entry.element, entry);
77
+ }
78
+ }
79
+ const marker = {
80
+ type: EntryType.Marker,
81
+ older: this.newest,
82
+ newer: null,
83
+ outerIndex: this.index,
84
+ };
85
+ if (this.newest !== null) {
86
+ this.newest.newer = marker;
87
+ }
88
+ this.newest = marker;
89
+ this.index = null;
65
90
  }
66
91
  pushElement(element, token) {
67
- this._ensureNoahArkCondition(element);
68
- this.entries.unshift({
69
- type: EntryType.Element,
70
- element,
71
- token,
72
- });
92
+ if (this.index === null) {
93
+ let oldest = null;
94
+ let count = 0;
95
+ for (let entry = this.newest; entry !== null && entry.type === EntryType.Element; entry = entry.older) {
96
+ oldest = entry;
97
+ count++;
98
+ }
99
+ if (count >= NOAH_ARK_CAPACITY) {
100
+ this.index = { arks: new Map(), tagNames: new Map() };
101
+ for (let entry = oldest; entry !== null && entry.type === EntryType.Element; entry = entry.newer) {
102
+ this._addToIndex(this.index, entry);
103
+ }
104
+ }
105
+ }
106
+ const ark = this._insertElementEntry(element, token, this.newest);
107
+ if (ark !== null && ark.entries.length > NOAH_ARK_CAPACITY) {
108
+ this.removeEntry(ark.entries[0]);
109
+ }
73
110
  }
74
111
  insertElementAfterBookmark(element, token) {
75
- const bookmarkIdx = this.entries.indexOf(this.bookmark);
76
- this.entries.splice(bookmarkIdx, 0, {
77
- type: EntryType.Element,
78
- element,
79
- token,
80
- });
112
+ // Like the formatting element's entry that it replaces, this entry is the newest with its tag name. The bookmark
113
+ // is used once per adoption-agency step and released here, so it does not keep a cleared region reachable.
114
+ const { bookmark } = this;
115
+ this.bookmark = null;
116
+ this._insertElementEntry(element, token, bookmark);
81
117
  }
82
118
  removeEntry(entry) {
83
- const entryIndex = this.entries.indexOf(entry);
84
- if (entryIndex !== -1) {
85
- this.entries.splice(entryIndex, 1);
119
+ var _a;
120
+ // An `a` start tag removes its entry even when the adoption agency already has, so skip unlinked entries.
121
+ if (entry.newer === null ? this.newest !== entry : entry.newer.older !== entry) {
122
+ return;
123
+ }
124
+ this.elementEntries.delete(entry.element);
125
+ if (entry.newer === null) {
126
+ this.newest = entry.older;
127
+ }
128
+ else {
129
+ entry.newer.older = entry.older;
130
+ }
131
+ if (entry.older !== null) {
132
+ entry.older.newer = entry.newer;
133
+ }
134
+ if (entry.ark !== null) {
135
+ const { entries, key } = entry.ark;
136
+ entries.splice(entries.indexOf(entry), 1);
137
+ // Entries are removed only after the last marker, so an emptied ark is dropped from the last region's index.
138
+ if (entries.length === 0)
139
+ (_a = this.index) === null || _a === void 0 ? void 0 : _a.arks.delete(key);
86
140
  }
141
+ if (entry.newerSameTag !== null) {
142
+ entry.newerSameTag.olderSameTag = entry.olderSameTag;
143
+ if (entry.olderSameTag !== null) {
144
+ entry.olderSameTag.newerSameTag = entry.newerSameTag;
145
+ }
146
+ }
147
+ }
148
+ replaceElement(entry, element) {
149
+ if (this.elementEntries.delete(entry.element)) {
150
+ this.elementEntries.set(element, entry);
151
+ }
152
+ entry.element = element;
87
153
  }
88
154
  /**
89
155
  * Clears the list of formatting elements up to the last marker.
@@ -91,20 +157,37 @@ export class FormattingElementList {
91
157
  * @see https://html.spec.whatwg.org/multipage/parsing.html#clear-the-list-of-active-formatting-elements-up-to-the-last-marker
92
158
  */
93
159
  clearToLastMarker() {
94
- const markerIdx = this.entries.indexOf(MARKER);
95
- if (markerIdx === -1) {
96
- this.entries.length = 0;
160
+ let marker = this.newest;
161
+ for (; marker !== null && marker.type === EntryType.Element; marker = marker.older) {
162
+ this.elementEntries.delete(marker.element);
97
163
  }
98
- else {
99
- this.entries.splice(0, markerIdx + 1);
164
+ this.newest = marker === null ? null : marker.older;
165
+ this.index = marker === null ? null : marker.outerIndex;
166
+ if (this.newest !== null) {
167
+ this.newest.newer = null;
100
168
  }
101
169
  }
102
170
  //Search
103
171
  getElementEntryInScopeWithTagName(tagName) {
104
- const entry = this.entries.find((entry) => entry.type === EntryType.Marker || this.treeAdapter.getTagName(entry.element) === tagName);
105
- return entry && entry.type === EntryType.Element ? entry : null;
172
+ var _a, _b;
173
+ if (this.index !== null) {
174
+ return (_b = (_a = this.index.tagNames.get(tagName)) === null || _a === void 0 ? void 0 : _a.olderSameTag) !== null && _b !== void 0 ? _b : null;
175
+ }
176
+ for (let entry = this.newest; entry !== null && entry.type === EntryType.Element; entry = entry.older) {
177
+ if (entry.token.tagName === tagName) {
178
+ return entry;
179
+ }
180
+ }
181
+ return null;
106
182
  }
107
183
  getElementEntry(element) {
108
- return this.entries.find((entry) => entry.type === EntryType.Element && entry.element === element);
184
+ if (this.index === null) {
185
+ for (let entry = this.newest; entry !== null && entry.type === EntryType.Element; entry = entry.older) {
186
+ if (entry.element === element) {
187
+ return entry;
188
+ }
189
+ }
190
+ }
191
+ return this.elementEntries.get(element);
109
192
  }
110
193
  }
@@ -4,8 +4,8 @@ import { FormattingElementList } from './formatting-element-list.js';
4
4
  import { ERR, type ParserErrorHandler } from '../common/error-codes.js';
5
5
  import { TAG_ID as $, NS } from '../common/html.js';
6
6
  import type { TreeAdapter, TreeAdapterTypeMap } from '../tree-adapters/interface.js';
7
- import { type Token, type CommentToken, type CharacterToken, type TagToken, type DoctypeToken, type EOFToken, type LocationWithAttributes } from '../common/token.js';
8
- declare enum InsertionMode {
7
+ import { type Token, type CommentToken, type CharacterToken, type TagToken, type DoctypeToken, type EOFToken, type LocationWithAttributes, type Attribute } from '../common/token.js';
8
+ export declare enum InsertionMode {
9
9
  INITIAL = 0,
10
10
  BEFORE_HTML = 1,
11
11
  BEFORE_HEAD = 2,
@@ -30,6 +30,10 @@ declare enum InsertionMode {
30
30
  AFTER_AFTER_BODY = 21,
31
31
  AFTER_AFTER_FRAMESET = 22
32
32
  }
33
+ interface InsertionLocation<T extends TreeAdapterTypeMap> {
34
+ parent: T['parentNode'];
35
+ beforeElement: T['element'] | null;
36
+ }
33
37
  export interface ParserOptions<T extends TreeAdapterTypeMap> {
34
38
  /**
35
39
  * The [scripting flag](https://html.spec.whatwg.org/multipage/parsing.html#scripting-flag). If set
@@ -100,8 +104,7 @@ export declare class Parser<T extends TreeAdapterTypeMap> implements TokenHandle
100
104
  /** Indicates that the current node is not an element in the HTML namespace */
101
105
  protected currentNotInHTML: boolean;
102
106
  /**
103
- * The template insertion mode stack is maintained from the left.
104
- * Ie. the topmost element will always have index 0.
107
+ * The topmost template insertion mode is the last element.
105
108
  *
106
109
  * @internal
107
110
  */
@@ -134,7 +137,11 @@ export declare class Parser<T extends TreeAdapterTypeMap> implements TokenHandle
134
137
  /** @protected */
135
138
  _setDocumentType(token: DoctypeToken): void;
136
139
  /** @protected */
137
- _attachElementToTree(element: T['element'], location: LocationWithAttributes | null): void;
140
+ _attachElementToTree(element: T['element'], location: LocationWithAttributes | null, insertionLocation: InsertionLocation<T>): void;
141
+ /** @protected */
142
+ _getInsertionLocation(): InsertionLocation<T>;
143
+ /** @protected */
144
+ _createElement(tagName: string, namespaceURI: NS, attrs: Attribute[], intendedParent: T['parentNode']): T['element'];
138
145
  /**
139
146
  * For self-closing tags. Add an element to the tree, but skip adding it
140
147
  * to the stack.
@@ -160,6 +167,11 @@ export declare class Parser<T extends TreeAdapterTypeMap> implements TokenHandle
160
167
  protected shouldProcessStartTagTokenInForeignContent(token: TagToken): boolean;
161
168
  /** @protected */
162
169
  _processToken(token: Token): void;
170
+ /**
171
+ * Whether each open MathML `annotation-xml` element, or the fragment context, is an HTML integration point. The
172
+ * standard decides this by the element's start tag, so its attributes are read once; `onItemPop` drops the entry.
173
+ */
174
+ private annotationXmlIntegrationPoints;
163
175
  /** @protected */
164
176
  _isIntegrationPoint(tid: $, element: T['element'], foreignNS?: NS): boolean;
165
177
  /** @protected */
@@ -177,10 +189,7 @@ export declare class Parser<T extends TreeAdapterTypeMap> implements TokenHandle
177
189
  /** @protected */
178
190
  _shouldFosterParentOnInsertion(): boolean;
179
191
  /** @protected */
180
- _findFosterParentingLocation(): {
181
- parent: T['parentNode'];
182
- beforeElement: T['element'] | null;
183
- };
192
+ _findFosterParentingLocation(): InsertionLocation<T>;
184
193
  /** @protected */
185
194
  _fosterParentElement(element: T['element']): void;
186
195
  /** @protected */