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.
@@ -18,6 +18,9 @@ export declare class Preprocessor {
18
18
  });
19
19
  /** The column on the current line. If we just saw a gap (eg. a surrogate pair), return the index before. */
20
20
  get col(): number;
21
+ /** The index in `html` at which the current code point starts (the high surrogate of a pair). */
22
+ get codePointStart(): number;
23
+ /** The offset of the current code point, before the second unit of a surrogate pair. */
21
24
  get offset(): number;
22
25
  getError(code: ERR, cpOffset: number): ParserError;
23
26
  private lastErrOffset;
@@ -25,7 +28,11 @@ export declare class Preprocessor {
25
28
  private _addGap;
26
29
  private _processSurrogate;
27
30
  willDropParsedChunk(): boolean;
28
- dropParsedChunk(): void;
31
+ /**
32
+ * Drop the parsed input, except for the current code point and the `keep` code units before it. The gap history is
33
+ * reset, so the kept units must contain no preprocessing gap.
34
+ */
35
+ dropParsedChunk(keep?: number): void;
29
36
  write(chunk: string, isLastChunk: boolean): void;
30
37
  insertHtmlAtCurrentPos(chunk: string): void;
31
38
  startsWith(pattern: string, caseSensitive: boolean): boolean;
@@ -28,8 +28,13 @@ export class Preprocessor {
28
28
  get col() {
29
29
  return this.pos - this.lineStartPos + Number(this.lastGapPos !== this.pos);
30
30
  }
31
+ /** The index in `html` at which the current code point starts (the high surrogate of a pair). */
32
+ get codePointStart() {
33
+ return this.pos - Number(this.lastGapPos === this.pos);
34
+ }
35
+ /** The offset of the current code point, before the second unit of a surrogate pair. */
31
36
  get offset() {
32
- return this.droppedBufferSize + this.pos;
37
+ return this.droppedBufferSize + this.codePointStart;
33
38
  }
34
39
  getError(code, cpOffset) {
35
40
  const { line, col, offset } = this;
@@ -59,7 +64,7 @@ export class Preprocessor {
59
64
  //NOTE: try to peek a surrogate pair
60
65
  if (this.pos !== this.html.length - 1) {
61
66
  const nextCp = this.html.charCodeAt(this.pos + 1);
62
- if (isSurrogatePair(nextCp)) {
67
+ if (!isSurrogatePair(cp) && isSurrogatePair(nextCp)) {
63
68
  //NOTE: we have a surrogate pair. Peek pair character and recalculate code point.
64
69
  this.pos++;
65
70
  //NOTE: add a gap that should be avoided during retreat
@@ -79,14 +84,22 @@ export class Preprocessor {
79
84
  willDropParsedChunk() {
80
85
  return this.pos > this.bufferWaterline;
81
86
  }
82
- dropParsedChunk() {
87
+ /**
88
+ * Drop the parsed input, except for the current code point and the `keep` code units before it. The gap history is
89
+ * reset, so the kept units must contain no preprocessing gap.
90
+ */
91
+ dropParsedChunk(keep = 0) {
83
92
  if (this.willDropParsedChunk()) {
84
- this.html = this.html.substring(this.pos);
85
- this.lineStartPos -= this.pos;
86
- this.droppedBufferSize += this.pos;
87
- this.pos = 0;
93
+ // Retain the entire current code point, including both units of a surrogate pair.
94
+ const droppedSize = this.offset - this.droppedBufferSize - keep;
95
+ this.html = this.html.substring(droppedSize);
96
+ this.lineStartPos -= droppedSize;
97
+ this.droppedBufferSize += droppedSize;
98
+ this.pos -= droppedSize;
88
99
  this.lastGapPos = -2;
89
100
  this.gapStack.length = 0;
101
+ if (this.pos > keep)
102
+ this._addGap();
90
103
  }
91
104
  }
92
105
  write(chunk, isLastChunk) {
@@ -1,4 +1,6 @@
1
1
  import { DOCUMENT_MODE } from '../common/html.js';
2
+ /** The position of each name in the attribute lists `adoptAttributes` merged into, kept for the next merge into that list. */
3
+ const attrListIndexes = new WeakMap();
2
4
  export const defaultTreeAdapter = {
3
5
  //Node construction
4
6
  createDocument() {
@@ -44,8 +46,12 @@ export const defaultTreeAdapter = {
44
46
  newNode.parentNode = parentNode;
45
47
  },
46
48
  insertBefore(parentNode, newNode, referenceNode) {
47
- const insertionIdx = parentNode.childNodes.indexOf(referenceNode);
48
- parentNode.childNodes.splice(insertionIdx, 0, newNode);
49
+ const { childNodes } = parentNode;
50
+ // The reference is usually the last child (fostered content precedes the table), so check that first.
51
+ const insertionIdx = childNodes[childNodes.length - 1] === referenceNode
52
+ ? childNodes.length - 1
53
+ : childNodes.indexOf(referenceNode);
54
+ childNodes.splice(insertionIdx, 0, newNode);
49
55
  newNode.parentNode = parentNode;
50
56
  },
51
57
  setTemplateContent(templateElement, contentElement) {
@@ -80,8 +86,10 @@ export const defaultTreeAdapter = {
80
86
  },
81
87
  detachNode(node) {
82
88
  if (node.parentNode) {
83
- const idx = node.parentNode.childNodes.indexOf(node);
84
- node.parentNode.childNodes.splice(idx, 1);
89
+ const { childNodes } = node.parentNode;
90
+ // The parser detaches from the end when it moves children, so check the last child first.
91
+ const idx = childNodes[childNodes.length - 1] === node ? childNodes.length - 1 : childNodes.indexOf(node);
92
+ childNodes.splice(idx, 1);
85
93
  node.parentNode = null;
86
94
  }
87
95
  },
@@ -96,7 +104,11 @@ export const defaultTreeAdapter = {
96
104
  defaultTreeAdapter.appendChild(parentNode, defaultTreeAdapter.createTextNode(text));
97
105
  },
98
106
  insertTextBefore(parentNode, text, referenceNode) {
99
- const prevNode = parentNode.childNodes[parentNode.childNodes.indexOf(referenceNode) - 1];
107
+ const { childNodes } = parentNode;
108
+ const referenceIdx = childNodes[childNodes.length - 1] === referenceNode
109
+ ? childNodes.length - 1
110
+ : childNodes.indexOf(referenceNode);
111
+ const prevNode = childNodes[referenceIdx - 1];
100
112
  if (prevNode && defaultTreeAdapter.isTextNode(prevNode)) {
101
113
  prevNode.value += text;
102
114
  }
@@ -104,13 +116,45 @@ export const defaultTreeAdapter = {
104
116
  defaultTreeAdapter.insertBefore(parentNode, defaultTreeAdapter.createTextNode(text), referenceNode);
105
117
  }
106
118
  },
119
+ /**
120
+ * Keeps the recipient's names between merges as a map from name to position. The map is rebuilt when the list changed
121
+ * length or has a repeated name, or when a merged name it holds no longer names the entry at that position. A name that
122
+ * other code gives an existing entry between two merges is missed otherwise, and merging it appends it a second time.
123
+ */
107
124
  adoptAttributes(recipient, attrs) {
108
- const recipientAttrsMap = new Set(recipient.attrs.map((attr) => attr.name));
125
+ // Merging a list into itself adds nothing: every name is already present.
126
+ if (attrs.length === 0 || attrs === recipient.attrs)
127
+ return;
128
+ const cached = attrListIndexes.get(recipient.attrs);
129
+ const isIndexed = (cached === null || cached === void 0 ? void 0 : cached.size) === recipient.attrs.length &&
130
+ attrs.every(({ name }) => {
131
+ var _a;
132
+ const index = cached.get(name);
133
+ return index === undefined || ((_a = recipient.attrs[index]) === null || _a === void 0 ? void 0 : _a.name) === name;
134
+ });
135
+ const recipientAttrsMap = isIndexed ? cached : new Map();
136
+ if (!isIndexed) {
137
+ for (let i = 0; i < recipient.attrs.length; i++) {
138
+ const attr = recipient.attrs[i];
139
+ // A sparse list is indexed again on every merge, as its size never matches its length.
140
+ if (attr !== undefined)
141
+ recipientAttrsMap.set(attr.name, i);
142
+ }
143
+ attrListIndexes.set(recipient.attrs, recipientAttrsMap);
144
+ }
145
+ const adoptedFrom = recipient.attrs.length;
109
146
  for (let j = 0; j < attrs.length; j++) {
110
147
  if (!recipientAttrsMap.has(attrs[j].name)) {
111
148
  recipient.attrs.push(attrs[j]);
112
149
  }
113
150
  }
151
+ for (let j = adoptedFrom; j < recipient.attrs.length; j++) {
152
+ recipientAttrsMap.set(recipient.attrs[j].name, j);
153
+ }
154
+ // A list with a repeated name is indexed again on the next merge.
155
+ if (recipientAttrsMap.size !== recipient.attrs.length) {
156
+ attrListIndexes.delete(recipient.attrs);
157
+ }
114
158
  },
115
159
  //Tree traversing
116
160
  getFirstChild(node) {
@@ -12,6 +12,15 @@ export interface TreeAdapterTypeMap<Node = unknown, ParentNode = unknown, ChildN
12
12
  template: Template;
13
13
  documentType: DocumentType;
14
14
  }
15
+ /** Parser state needed to establish a form-associated element's parser-established form owner. */
16
+ export interface ElementCreationContext<T extends TreeAdapterTypeMap = TreeAdapterTypeMap> {
17
+ /** The target parent from the appropriate place for inserting a node. */
18
+ intendedParent: T['parentNode'];
19
+ /** The element referenced by the parser's form element pointer, if any. */
20
+ formElement: T['element'] | null;
21
+ /** Whether the stack of open elements contains an HTML `template` element. */
22
+ isInTemplate: boolean;
23
+ }
15
24
  /**
16
25
  * Tree adapter is a set of utility functions that provides minimal required abstraction layer beetween parser and a specific AST format.
17
26
  * Note that `TreeAdapter` is not designed to be a general purpose AST manipulation library. You can build such library
@@ -240,6 +249,14 @@ export interface TreeAdapter<T extends TreeAdapterTypeMap = TreeAdapterTypeMap>
240
249
  * @param contentElement - Content element.
241
250
  */
242
251
  setTemplateContent(templateElement: T['template'], contentElement: T['documentFragment']): void;
252
+ /**
253
+ * Optional callback invoked after an element is created from a token and before it is inserted. This exposes the
254
+ * parser state needed to establish a form-associated element's parser-established form owner.
255
+ *
256
+ * @param element The created element.
257
+ * @param context Relevant parser state at the time of creation.
258
+ */
259
+ onElementCreated?: (element: T['element'], context: ElementCreationContext<T>) => void;
243
260
  /**
244
261
  * Optional callback for elements being pushed to the stack of open elements.
245
262
  *
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "parse5",
3
3
  "type": "module",
4
4
  "description": "HTML parser and serializer.",
5
- "version": "8.0.1",
5
+ "version": "8.1.1",
6
6
  "author": "Ivan Nikulin <ifaaan@gmail.com> (https://github.com/inikulin)",
7
7
  "contributors": [
8
8
  "James Garbutt (https://github.com/43081j)",
@@ -13,7 +13,7 @@
13
13
  "homepage": "https://parse5.js.org",
14
14
  "funding": "https://github.com/inikulin/parse5?sponsor=1",
15
15
  "dependencies": {
16
- "entities": "^8.0.0"
16
+ "entities": "^9.0.0"
17
17
  },
18
18
  "keywords": [
19
19
  "html",
@@ -49,5 +49,9 @@
49
49
  "files": [
50
50
  "dist/**/*.js",
51
51
  "dist/**/*.d.ts"
52
- ]
52
+ ],
53
+ "devDependencies": {
54
+ "parse5-htmlparser2-tree-adapter": "^8.0.0",
55
+ "parse5-test-utils": "1.0.0"
56
+ }
53
57
  }