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.
@@ -51,11 +51,19 @@ const defaultOpts = { treeAdapter: defaultTreeAdapter, scriptingEnabled: true };
51
51
  * @param options Serialization options.
52
52
  */
53
53
  export function serialize(node, options) {
54
+ var _a;
54
55
  const opts = { ...defaultOpts, ...options };
55
56
  if (isVoidElement(node, opts)) {
56
57
  return '';
57
58
  }
58
- return serializeChildNodes(node, opts);
59
+ const { treeAdapter } = opts;
60
+ // A template's children are those of its content fragment.
61
+ const container = treeAdapter.isElementNode(node) &&
62
+ treeAdapter.getTagName(node) === $.TEMPLATE &&
63
+ treeAdapter.getNamespaceURI(node) === NS.HTML
64
+ ? treeAdapter.getTemplateContent(node)
65
+ : node;
66
+ return serializeNodes((_a = treeAdapter.getChildNodes(container)) !== null && _a !== void 0 ? _a : [], opts);
59
67
  }
60
68
  /**
61
69
  * Serializes an AST element node to an HTML string, including the element node.
@@ -78,43 +86,49 @@ export function serialize(node, options) {
78
86
  */
79
87
  export function serializeOuter(node, options) {
80
88
  const opts = { ...defaultOpts, ...options };
81
- return serializeNode(node, opts);
89
+ return serializeNodes([node], opts);
82
90
  }
83
- function serializeChildNodes(parentNode, options) {
91
+ function serializeNodes(nodes, options) {
92
+ var _a;
93
+ const { treeAdapter } = options;
94
+ // The depth of the tree is not bounded, so the open elements (their
95
+ // remaining siblings and end tags) are kept on explicit stacks.
96
+ const stack = [];
84
97
  let html = '';
85
- // Get container of the child nodes
86
- const container = options.treeAdapter.isElementNode(parentNode) &&
87
- options.treeAdapter.getTagName(parentNode) === $.TEMPLATE &&
88
- options.treeAdapter.getNamespaceURI(parentNode) === NS.HTML
89
- ? options.treeAdapter.getTemplateContent(parentNode)
90
- : parentNode;
91
- const childNodes = options.treeAdapter.getChildNodes(container);
92
- if (childNodes) {
93
- for (const currentNode of childNodes) {
94
- html += serializeNode(currentNode, options);
98
+ let index = 0;
99
+ for (;;) {
100
+ if (index < nodes.length) {
101
+ const node = nodes[index++];
102
+ if (treeAdapter.isElementNode(node)) {
103
+ const tn = treeAdapter.getTagName(node);
104
+ html += `<${tn}${serializeAttributes(node, options)}>`;
105
+ const isHtml = treeAdapter.getNamespaceURI(node) === NS.HTML;
106
+ if (!(isHtml && VOID_ELEMENTS.has(tn))) {
107
+ stack.push({ nodes, index, endTag: `</${tn}>` });
108
+ nodes =
109
+ (_a = treeAdapter.getChildNodes(isHtml && tn === $.TEMPLATE ? treeAdapter.getTemplateContent(node) : node)) !== null && _a !== void 0 ? _a : [];
110
+ index = 0;
111
+ }
112
+ }
113
+ else if (treeAdapter.isTextNode(node)) {
114
+ html += serializeTextNode(node, options);
115
+ }
116
+ else if (treeAdapter.isCommentNode(node)) {
117
+ html += serializeCommentNode(node, options);
118
+ }
119
+ else if (treeAdapter.isDocumentTypeNode(node)) {
120
+ html += serializeDocumentTypeNode(node, options);
121
+ }
122
+ }
123
+ else {
124
+ const parent = stack.pop();
125
+ if (!parent) {
126
+ return html;
127
+ }
128
+ html += parent.endTag;
129
+ ({ nodes, index } = parent);
95
130
  }
96
131
  }
97
- return html;
98
- }
99
- function serializeNode(node, options) {
100
- if (options.treeAdapter.isElementNode(node)) {
101
- return serializeElement(node, options);
102
- }
103
- if (options.treeAdapter.isTextNode(node)) {
104
- return serializeTextNode(node, options);
105
- }
106
- if (options.treeAdapter.isCommentNode(node)) {
107
- return serializeCommentNode(node, options);
108
- }
109
- if (options.treeAdapter.isDocumentTypeNode(node)) {
110
- return serializeDocumentTypeNode(node, options);
111
- }
112
- // Return an empty string for unknown nodes
113
- return '';
114
- }
115
- function serializeElement(node, options) {
116
- const tn = options.treeAdapter.getTagName(node);
117
- return `<${tn}${serializeAttributes(node, options)}>${isVoidElement(node, options) ? '' : `${serializeChildNodes(node, options)}</${tn}>`}`;
118
132
  }
119
133
  function serializeAttributes(node, { treeAdapter }) {
120
134
  let html = '';
@@ -137,6 +137,7 @@ export declare class Tokenizer {
137
137
  protected currentToken: Token | null;
138
138
  protected currentAttr: Attribute;
139
139
  private currentAttrLocation;
140
+ private currentAttrNames;
140
141
  /**
141
142
  * `[start, end)` of `preprocessor.html` still to be appended to the attribute value, comment data or character
142
143
  * token being built (`_flushRun`): input that is used as is is appended as one slice, not a code point at a time.
@@ -155,6 +156,8 @@ export declare class Tokenizer {
155
156
  resume(writeCallback?: () => void): void;
156
157
  write(chunk: string, isLastChunk: boolean, writeCallback?: () => void): void;
157
158
  insertHtmlAtCurrentPos(chunk: string): void;
159
+ /** @internal Flush completed streaming text without splitting an unfinished token. */
160
+ flushCurrentCharacterToken(): void;
158
161
  protected _ensureHibernation(): boolean;
159
162
  protected _consume(): number;
160
163
  protected _advanceBy(count: number): void;
@@ -4,6 +4,9 @@ import { TokenType, getTokenAttr, } from '../common/token.js';
4
4
  import { HtmlEntityDecoder, DecodingMode } from 'entities/decode';
5
5
  import { ERR } from '../common/error-codes.js';
6
6
  import { TAG_ID, getTagID } from '../common/html.js';
7
+ // The few attributes a tag usually has are scanned for a duplicate name; from this many on, the names are
8
+ // collected in a Set instead, so that the check doesn't get quadratic in the number of attributes.
9
+ const ATTR_NAME_SET_THRESHOLD = 64;
7
10
  //States
8
11
  var State;
9
12
  (function (State) {
@@ -161,6 +164,7 @@ export class Tokenizer {
161
164
  this.currentToken = null;
162
165
  this.currentAttr = { name: '', value: '' };
163
166
  this.currentAttrLocation = null;
167
+ this.currentAttrNames = null;
164
168
  /**
165
169
  * `[start, end)` of `preprocessor.html` still to be appended to the attribute value, comment data or character
166
170
  * token being built (`_flushRun`): input that is used as is is appended as one slice, not a code point at a time.
@@ -181,6 +185,9 @@ export class Tokenizer {
181
185
  : TokenType.CHARACTER);
182
186
  }
183
187
  this._extendRunWith(String.fromCodePoint(cp), 0);
188
+ // This callback runs twice for references that decode to two
189
+ // code points; keep `entityStartPos` valid if the chunk was dropped.
190
+ this.entityStartPos = this.preprocessor.pos - consumed + 1;
184
191
  }, handler.onParseError
185
192
  ? {
186
193
  missingSemicolonAfterCharacterReference: () => {
@@ -352,6 +359,33 @@ export class Tokenizer {
352
359
  this.preprocessor.insertHtmlAtCurrentPos(chunk);
353
360
  this._runParsingLoop();
354
361
  }
362
+ /** @internal Flush completed streaming text without splitting an unfinished token. */
363
+ flushCurrentCharacterToken() {
364
+ // Only completed text is flushed: an unfinished token stays intact, including a doctype that has no token yet.
365
+ if (this.paused ||
366
+ this.active ||
367
+ !this.currentCharacterToken ||
368
+ this.currentToken ||
369
+ this.state === State.DOCTYPE ||
370
+ this.state === State.BEFORE_DOCTYPE_NAME ||
371
+ !this.preprocessor.willDropParsedChunk()) {
372
+ return;
373
+ }
374
+ // The last `pending` code units, a delimiter or character reference, are not text yet and stay buffered. They
375
+ // are ASCII, so they hold no preprocessing gap and no line break that the dropped gap history would have to keep.
376
+ const pending = this.preprocessor.droppedBufferSize + this.preprocessor.pos + 1 - this.getCharacterTokenEndOffset();
377
+ // Hibernation left us before the next code point (possibly an incomplete surrogate).
378
+ this.preprocessor.advance();
379
+ this.currentLocation = this.getCurrentLocation(pending);
380
+ this.preprocessor.retreat(1);
381
+ this._emitCurrentCharacterToken(this.currentLocation);
382
+ // Keep a source range for CDATA delimiters or a later tag discarded at EOF. An empty null-character token
383
+ // would become a replacement character in foreign content.
384
+ this._createCharacterToken(TokenType.CHARACTER);
385
+ this.preprocessor.dropParsedChunk(Math.max(pending - 1, 0));
386
+ // Keep `entityStartPos` valid for a pending character reference.
387
+ this.entityStartPos = this.preprocessor.pos + 1 - pending;
388
+ }
355
389
  //Hibernation
356
390
  _ensureHibernation() {
357
391
  if (this.preprocessor.endOfChunkHit) {
@@ -452,7 +486,26 @@ export class Tokenizer {
452
486
  var _a;
453
487
  var _b;
454
488
  const token = this.currentToken;
455
- if (getTokenAttr(token, this.currentAttr.name) === null) {
489
+ const { name } = this.currentAttr;
490
+ const names = this.currentAttrNames;
491
+ let duplicate;
492
+ if (names === null) {
493
+ duplicate = getTokenAttr(token, name) !== null;
494
+ // Build the name set only once a new (65th) name is accepted, so a tag with 64 names and many
495
+ // duplicates does not build it.
496
+ if (!duplicate && token.attrs.length === ATTR_NAME_SET_THRESHOLD) {
497
+ this.currentAttrNames = new Set(token.attrs.map((attr) => attr.name)).add(name);
498
+ }
499
+ }
500
+ else {
501
+ duplicate = names.has(name);
502
+ if (!duplicate)
503
+ names.add(name);
504
+ }
505
+ if (duplicate) {
506
+ this._err(ERR.duplicateAttribute);
507
+ }
508
+ else {
456
509
  token.attrs.push(this.currentAttr);
457
510
  if (token.location && this.currentAttrLocation) {
458
511
  const attrLocations = ((_a = (_b = token.location).attrs) !== null && _a !== void 0 ? _a : (_b.attrs = Object.create(null)));
@@ -461,9 +514,6 @@ export class Tokenizer {
461
514
  this._leaveAttrValue();
462
515
  }
463
516
  }
464
- else {
465
- this._err(ERR.duplicateAttribute);
466
- }
467
517
  }
468
518
  _leaveAttrValue() {
469
519
  if (this.currentAttrLocation) {
@@ -545,6 +595,7 @@ export class Tokenizer {
545
595
  }
546
596
  emitCurrentTagToken() {
547
597
  const ct = this.currentToken;
598
+ this.currentAttrNames = null;
548
599
  this.prepareToken(ct);
549
600
  ct.tagID = getTagID(ct.tagName);
550
601
  if (ct.type === TokenType.START_TAG) {
@@ -600,13 +651,16 @@ export class Tokenizer {
600
651
  }
601
652
  }
602
653
  _emitEOFToken() {
654
+ var _a, _b;
603
655
  const location = this.getCurrentLocation(0);
604
656
  if (location) {
605
657
  location.endLine = location.startLine;
606
658
  location.endCol = location.startCol;
607
659
  location.endOffset = location.startOffset;
608
660
  }
609
- this._emitCurrentCharacterToken(location);
661
+ // A token that is still open at EOF (eof-in-tag) is dropped. Pending
662
+ // characters end where that token started, rather than at EOF.
663
+ this._emitCurrentCharacterToken((_b = (_a = this.currentToken) === null || _a === void 0 ? void 0 : _a.location) !== null && _b !== void 0 ? _b : location);
610
664
  this.handler.onEof({ type: TokenType.EOF, location });
611
665
  this.active = false;
612
666
  }
@@ -28,7 +28,11 @@ export declare class Preprocessor {
28
28
  private _addGap;
29
29
  private _processSurrogate;
30
30
  willDropParsedChunk(): boolean;
31
- 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;
32
36
  write(chunk: string, isLastChunk: boolean): void;
33
37
  insertHtmlAtCurrentPos(chunk: string): void;
34
38
  startsWith(pattern: string, caseSensitive: boolean): boolean;
@@ -64,7 +64,7 @@ export class Preprocessor {
64
64
  //NOTE: try to peek a surrogate pair
65
65
  if (this.pos !== this.html.length - 1) {
66
66
  const nextCp = this.html.charCodeAt(this.pos + 1);
67
- if (isSurrogatePair(nextCp)) {
67
+ if (!isSurrogatePair(cp) && isSurrogatePair(nextCp)) {
68
68
  //NOTE: we have a surrogate pair. Peek pair character and recalculate code point.
69
69
  this.pos++;
70
70
  //NOTE: add a gap that should be avoided during retreat
@@ -84,17 +84,21 @@ export class Preprocessor {
84
84
  willDropParsedChunk() {
85
85
  return this.pos > this.bufferWaterline;
86
86
  }
87
- 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) {
88
92
  if (this.willDropParsedChunk()) {
89
93
  // Retain the entire current code point, including both units of a surrogate pair.
90
- const droppedSize = this.offset - this.droppedBufferSize;
94
+ const droppedSize = this.offset - this.droppedBufferSize - keep;
91
95
  this.html = this.html.substring(droppedSize);
92
96
  this.lineStartPos -= droppedSize;
93
97
  this.droppedBufferSize += droppedSize;
94
98
  this.pos -= droppedSize;
95
99
  this.lastGapPos = -2;
96
100
  this.gapStack.length = 0;
97
- if (this.pos > 0)
101
+ if (this.pos > keep)
98
102
  this._addGap();
99
103
  }
100
104
  }
@@ -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) {
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.1.0",
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)",