parse5 8.0.0 → 8.1.0

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/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 */
@@ -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
@@ -134,7 +138,11 @@ export declare class Parser<T extends TreeAdapterTypeMap> implements TokenHandle
134
138
  /** @protected */
135
139
  _setDocumentType(token: DoctypeToken): void;
136
140
  /** @protected */
137
- _attachElementToTree(element: T['element'], location: LocationWithAttributes | null): void;
141
+ _attachElementToTree(element: T['element'], location: LocationWithAttributes | null, insertionLocation: InsertionLocation<T>): void;
142
+ /** @protected */
143
+ _getInsertionLocation(): InsertionLocation<T>;
144
+ /** @protected */
145
+ _createElement(tagName: string, namespaceURI: NS, attrs: Attribute[], intendedParent: T['parentNode']): T['element'];
138
146
  /**
139
147
  * For self-closing tags. Add an element to the tree, but skip adding it
140
148
  * to the stack.
@@ -177,10 +185,7 @@ export declare class Parser<T extends TreeAdapterTypeMap> implements TokenHandle
177
185
  /** @protected */
178
186
  _shouldFosterParentOnInsertion(): boolean;
179
187
  /** @protected */
180
- _findFosterParentingLocation(): {
181
- parent: T['parentNode'];
182
- beforeElement: T['element'] | null;
183
- };
188
+ _findFosterParentingLocation(): InsertionLocation<T>;
184
189
  /** @protected */
185
190
  _fosterParentElement(element: T['element']): void;
186
191
  /** @protected */
@@ -14,7 +14,7 @@ const HIDDEN_INPUT_TYPE = 'hidden';
14
14
  const AA_OUTER_LOOP_ITER = 8;
15
15
  const AA_INNER_LOOP_ITER = 3;
16
16
  //Insertion modes
17
- var InsertionMode;
17
+ export var InsertionMode;
18
18
  (function (InsertionMode) {
19
19
  InsertionMode[InsertionMode["INITIAL"] = 0] = "INITIAL";
20
20
  InsertionMode[InsertionMode["BEFORE_HTML"] = 1] = "BEFORE_HTML";
@@ -273,7 +273,7 @@ export class Parser {
273
273
  }
274
274
  }
275
275
  /** @protected */
276
- _attachElementToTree(element, location) {
276
+ _attachElementToTree(element, location, insertionLocation) {
277
277
  if (this.options.sourceCodeLocationInfo) {
278
278
  const loc = location && {
279
279
  ...location,
@@ -281,13 +281,34 @@ export class Parser {
281
281
  };
282
282
  this.treeAdapter.setNodeSourceCodeLocation(element, loc);
283
283
  }
284
- if (this._shouldFosterParentOnInsertion()) {
285
- this._fosterParentElement(element);
284
+ if (insertionLocation.beforeElement) {
285
+ this.treeAdapter.insertBefore(insertionLocation.parent, element, insertionLocation.beforeElement);
286
286
  }
287
287
  else {
288
- const parent = this.openElements.currentTmplContentOrNode;
289
- this.treeAdapter.appendChild(parent !== null && parent !== void 0 ? parent : this.document, element);
288
+ this.treeAdapter.appendChild(insertionLocation.parent, element);
289
+ }
290
+ }
291
+ /** @protected */
292
+ _getInsertionLocation() {
293
+ var _a;
294
+ if (this._shouldFosterParentOnInsertion()) {
295
+ return this._findFosterParentingLocation();
290
296
  }
297
+ return {
298
+ parent: (_a = this.openElements.currentTmplContentOrNode) !== null && _a !== void 0 ? _a : this.document,
299
+ beforeElement: null,
300
+ };
301
+ }
302
+ /** @protected */
303
+ _createElement(tagName, namespaceURI, attrs, intendedParent) {
304
+ var _a, _b;
305
+ const element = this.treeAdapter.createElement(tagName, namespaceURI, attrs);
306
+ (_b = (_a = this.treeAdapter).onElementCreated) === null || _b === void 0 ? void 0 : _b.call(_a, element, {
307
+ intendedParent,
308
+ formElement: this.formElement,
309
+ isInTemplate: this.openElements.tmplCount > 0,
310
+ });
311
+ return element;
291
312
  }
292
313
  /**
293
314
  * For self-closing tags. Add an element to the tree, but skip adding it
@@ -295,37 +316,41 @@ export class Parser {
295
316
  */
296
317
  /** @protected */
297
318
  _appendElement(token, namespaceURI) {
298
- const element = this.treeAdapter.createElement(token.tagName, namespaceURI, token.attrs);
299
- this._attachElementToTree(element, token.location);
319
+ const insertionLocation = this._getInsertionLocation();
320
+ const element = this._createElement(token.tagName, namespaceURI, token.attrs, insertionLocation.parent);
321
+ this._attachElementToTree(element, token.location, insertionLocation);
300
322
  }
301
323
  /** @protected */
302
324
  _insertElement(token, namespaceURI) {
303
- const element = this.treeAdapter.createElement(token.tagName, namespaceURI, token.attrs);
304
- this._attachElementToTree(element, token.location);
325
+ const insertionLocation = this._getInsertionLocation();
326
+ const element = this._createElement(token.tagName, namespaceURI, token.attrs, insertionLocation.parent);
327
+ this._attachElementToTree(element, token.location, insertionLocation);
305
328
  this.openElements.push(element, token.tagID);
306
329
  }
307
330
  /** @protected */
308
331
  _insertFakeElement(tagName, tagID) {
309
- const element = this.treeAdapter.createElement(tagName, NS.HTML, []);
310
- this._attachElementToTree(element, null);
332
+ const insertionLocation = this._getInsertionLocation();
333
+ const element = this._createElement(tagName, NS.HTML, [], insertionLocation.parent);
334
+ this._attachElementToTree(element, null, insertionLocation);
311
335
  this.openElements.push(element, tagID);
312
336
  }
313
337
  /** @protected */
314
338
  _insertTemplate(token) {
315
- const tmpl = this.treeAdapter.createElement(token.tagName, NS.HTML, token.attrs);
339
+ const insertionLocation = this._getInsertionLocation();
340
+ const tmpl = this._createElement(token.tagName, NS.HTML, token.attrs, insertionLocation.parent);
316
341
  const content = this.treeAdapter.createDocumentFragment();
317
342
  this.treeAdapter.setTemplateContent(tmpl, content);
318
- this._attachElementToTree(tmpl, token.location);
343
+ this._attachElementToTree(tmpl, token.location, insertionLocation);
319
344
  this.openElements.push(tmpl, token.tagID);
320
345
  if (this.options.sourceCodeLocationInfo)
321
346
  this.treeAdapter.setNodeSourceCodeLocation(content, null);
322
347
  }
323
348
  /** @protected */
324
349
  _insertFakeRootElement() {
325
- const element = this.treeAdapter.createElement(TN.HTML, NS.HTML, []);
350
+ const element = this._createElement(TN.HTML, NS.HTML, [], this.document);
326
351
  if (this.options.sourceCodeLocationInfo)
327
352
  this.treeAdapter.setNodeSourceCodeLocation(element, null);
328
- this.treeAdapter.appendChild(this.openElements.current, element);
353
+ this.treeAdapter.appendChild(this.document, element);
329
354
  this.openElements.push(element, $.HTML);
330
355
  }
331
356
  /** @protected */
@@ -1217,6 +1242,7 @@ function aaObtainFurthestBlock(p, formattingElementEntry) {
1217
1242
  function aaInnerLoop(p, furthestBlock, formattingElement) {
1218
1243
  let lastElement = furthestBlock;
1219
1244
  let nextElement = p.openElements.getCommonAncestor(furthestBlock);
1245
+ const intendedParent = p.openElements.getCommonAncestor(formattingElement);
1220
1246
  for (let i = 0, element = nextElement; element !== formattingElement; i++, element = nextElement) {
1221
1247
  //NOTE: store the next element for the next loop iteration (it may be deleted from the stack by step 9.5)
1222
1248
  nextElement = p.openElements.getCommonAncestor(element);
@@ -1230,7 +1256,7 @@ function aaInnerLoop(p, furthestBlock, formattingElement) {
1230
1256
  p.openElements.remove(element);
1231
1257
  }
1232
1258
  else {
1233
- element = aaRecreateElementFromEntry(p, elementEntry);
1259
+ element = aaRecreateElementFromEntry(p, elementEntry, intendedParent);
1234
1260
  if (lastElement === furthestBlock) {
1235
1261
  p.activeFormattingElements.bookmark = elementEntry;
1236
1262
  }
@@ -1242,9 +1268,9 @@ function aaInnerLoop(p, furthestBlock, formattingElement) {
1242
1268
  return lastElement;
1243
1269
  }
1244
1270
  //Step 13.7 of the algorithm
1245
- function aaRecreateElementFromEntry(p, elementEntry) {
1271
+ function aaRecreateElementFromEntry(p, elementEntry, intendedParent) {
1246
1272
  const ns = p.treeAdapter.getNamespaceURI(elementEntry.element);
1247
- const newElement = p.treeAdapter.createElement(elementEntry.token.tagName, ns, elementEntry.token.attrs);
1273
+ const newElement = p._createElement(elementEntry.token.tagName, ns, elementEntry.token.attrs, intendedParent);
1248
1274
  p.openElements.replace(elementEntry.element, newElement);
1249
1275
  elementEntry.element = newElement;
1250
1276
  return newElement;
@@ -1268,7 +1294,7 @@ function aaInsertLastNodeInCommonAncestor(p, commonAncestor, lastElement) {
1268
1294
  function aaReplaceFormattingElement(p, furthestBlock, formattingElementEntry) {
1269
1295
  const ns = p.treeAdapter.getNamespaceURI(formattingElementEntry.element);
1270
1296
  const { token } = formattingElementEntry;
1271
- const newElement = p.treeAdapter.createElement(token.tagName, ns, token.attrs);
1297
+ const newElement = p._createElement(token.tagName, ns, token.attrs, furthestBlock);
1272
1298
  p._adoptNodes(furthestBlock, newElement);
1273
1299
  p.treeAdapter.appendChild(furthestBlock, newElement);
1274
1300
  p.activeFormattingElements.insertElementAfterBookmark(newElement, token);
@@ -2227,7 +2253,10 @@ function genericEndTagInBody(p, token) {
2227
2253
  const element = p.openElements.items[i];
2228
2254
  const elementId = p.openElements.tagIDs[i];
2229
2255
  // Compare the tag name here, as the tag might not be a known tag with an ID.
2230
- if (tid === elementId && (tid !== $.UNKNOWN || p.treeAdapter.getTagName(element) === tn)) {
2256
+ // Only HTML elements match; a foreign element falls through to the special-element check.
2257
+ if (tid === elementId &&
2258
+ p.treeAdapter.getNamespaceURI(element) === NS.HTML &&
2259
+ (tid !== $.UNKNOWN || p.treeAdapter.getTagName(element) === tn)) {
2231
2260
  p.openElements.generateImpliedEndTagsWithExclusion(tid);
2232
2261
  if (p.openElements.stackTop >= i)
2233
2262
  p.openElements.shortenToLength(i);
@@ -1,6 +1,6 @@
1
1
  import { Preprocessor } from './preprocessor.js';
2
2
  import { type Token, type CharacterToken, type DoctypeToken, type TagToken, type EOFToken, type CommentToken, type Attribute, type Location } from '../common/token.js';
3
- import { EntityDecoder } from 'entities/decode';
3
+ import { HtmlEntityDecoder } from 'entities/decode';
4
4
  import { ERR, type ParserErrorHandler } from '../common/error-codes.js';
5
5
  declare const enum State {
6
6
  DATA = 0,
@@ -118,9 +118,9 @@ export declare class Tokenizer {
118
118
  state: State;
119
119
  protected returnState: State;
120
120
  /**
121
- * We use `entities`' `EntityDecoder` to parse character references.
121
+ * We use `entities`' `HtmlEntityDecoder` to parse character references.
122
122
  *
123
- * All of the following states are handled by the `EntityDecoder`:
123
+ * All of the following states are handled by the `HtmlEntityDecoder`:
124
124
  *
125
125
  * - Named character reference state
126
126
  * - Numeric character reference state
@@ -129,17 +129,28 @@ export declare class Tokenizer {
129
129
  * - Decimal character reference state
130
130
  * - Numeric character reference end state
131
131
  */
132
- protected entityDecoder: EntityDecoder;
132
+ protected entityDecoder: HtmlEntityDecoder;
133
133
  protected entityStartPos: number;
134
134
  protected consumedAfterSnapshot: number;
135
135
  protected currentLocation: Location | null;
136
136
  protected currentCharacterToken: CharacterToken | null;
137
137
  protected currentToken: Token | null;
138
138
  protected currentAttr: Attribute;
139
+ private currentAttrLocation;
140
+ /**
141
+ * `[start, end)` of `preprocessor.html` still to be appended to the attribute value, comment data or character
142
+ * token being built (`_flushRun`): input that is used as is is appended as one slice, not a code point at a time.
143
+ */
144
+ protected runStart: number;
145
+ protected runEnd: number;
139
146
  constructor(options: TokenizerOptions, handler: TokenHandler);
140
147
  protected _err(code: ERR, cpOffset?: number): void;
141
148
  protected getCurrentLocation(offset: number): Location | null;
142
149
  protected _runParsingLoop(): void;
150
+ /** Emit buffered character tokens without completing the current token or emitting EOF. */
151
+ flushCharacters(): void;
152
+ private getCharacterTokenEndLocation;
153
+ private getCharacterTokenEndOffset;
143
154
  pause(): void;
144
155
  resume(writeCallback?: () => void): void;
145
156
  write(chunk: string, isLastChunk: boolean, writeCallback?: () => void): void;
@@ -152,19 +163,35 @@ export declare class Tokenizer {
152
163
  protected _createEndTagToken(): void;
153
164
  protected _createCommentToken(offset: number): void;
154
165
  protected _createDoctypeToken(initialName: string | null): void;
155
- protected _createCharacterToken(type: CharacterToken['type'], chars: string): void;
166
+ protected _createCharacterToken(type: CharacterToken['type']): void;
156
167
  protected _createAttr(attrNameFirstCh: string): void;
157
168
  protected _leaveAttrName(): void;
158
169
  protected _leaveAttrValue(): void;
170
+ /**
171
+ * Append the pending run and `text` to the attribute value, comment data or character token being built.
172
+ * The run is a string of its own: V8 copies a slice of fewer than 13 characters but lets a longer one share
173
+ * the input's buffer, which would keep that buffer alive for as long as any attribute value or text cut from
174
+ * it is; a concatenation is flattened into a buffer of its own when it is sliced.
175
+ */
176
+ protected _flushRun(text: string): void;
177
+ /** Extend the pending run by the current input code point, read as `cp`, or flush it and start a new one. */
178
+ protected _extendRun(cp: number): void;
179
+ /**
180
+ * Extend the pending run by `text`, the input that ends `cpOffset` code points after the current one, or flush
181
+ * the run and append `text` where the input differs (a NUL replacement, a decoded reference, a dropped chunk).
182
+ */
183
+ protected _extendRunWith(text: string, cpOffset: number): void;
159
184
  protected prepareToken(ct: Token): void;
160
185
  protected emitCurrentTagToken(): void;
161
186
  protected emitCurrentComment(ct: CommentToken): void;
162
187
  protected emitCurrentDoctype(ct: DoctypeToken): void;
163
188
  protected _emitCurrentCharacterToken(nextLocation: Location | null): void;
164
189
  protected _emitEOFToken(): void;
165
- protected _appendCharToCurrentCharacterToken(type: CharacterToken['type'], ch: string): void;
190
+ /** Make the current character token one of `type`, emitting the current one if it has another type. */
191
+ protected _ensureCharacterToken(type: CharacterToken['type']): void;
192
+ /** Emit the current input code point, read as `cp`. */
166
193
  protected _emitCodePoint(cp: number): void;
167
- protected _emitChars(ch: string): void;
194
+ protected _emitChars(ch: string, cpOffset?: number): void;
168
195
  protected _startCharacterReference(): void;
169
196
  protected _isCharacterReferenceInAttribute(): boolean;
170
197
  protected _flushCodePointConsumedAsCharacterReference(cp: number): void;
@@ -1,7 +1,7 @@
1
1
  import { Preprocessor } from './preprocessor.js';
2
2
  import { CODE_POINTS as $, SEQUENCES as $$, REPLACEMENT_CHARACTER, isSurrogate, isUndefinedCodePoint, isControlCodePoint, } from '../common/unicode.js';
3
3
  import { TokenType, getTokenAttr, } from '../common/token.js';
4
- import { htmlDecodeTree, EntityDecoder, DecodingMode } from 'entities/decode';
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
7
  //States
@@ -113,7 +113,7 @@ function toAsciiLower(cp) {
113
113
  return cp + 32;
114
114
  }
115
115
  function isWhitespace(cp) {
116
- return cp === $.SPACE || cp === $.LINE_FEED || cp === $.TABULATION || cp === $.FORM_FEED;
116
+ return (cp === $.SPACE || cp === $.LINE_FEED || cp === $.TABULATION || cp === $.FORM_FEED || cp === $.CARRIAGE_RETURN);
117
117
  }
118
118
  function isScriptDataDoubleEscapeSequenceEnd(cp) {
119
119
  return isWhitespace(cp) || cp === $.SOLIDUS || cp === $.GREATER_THAN_SIGN;
@@ -160,13 +160,27 @@ export class Tokenizer {
160
160
  this.currentCharacterToken = null;
161
161
  this.currentToken = null;
162
162
  this.currentAttr = { name: '', value: '' };
163
+ this.currentAttrLocation = null;
164
+ /**
165
+ * `[start, end)` of `preprocessor.html` still to be appended to the attribute value, comment data or character
166
+ * token being built (`_flushRun`): input that is used as is is appended as one slice, not a code point at a time.
167
+ */
168
+ this.runStart = 0;
169
+ this.runEnd = 0;
163
170
  this.preprocessor = new Preprocessor(handler);
164
171
  this.currentLocation = this.getCurrentLocation(-1);
165
- this.entityDecoder = new EntityDecoder(htmlDecodeTree, (cp, consumed) => {
172
+ this.entityDecoder = new HtmlEntityDecoder((cp, consumed) => {
166
173
  // Note: Set `pos` _before_ flushing, as flushing might drop
167
174
  // the current chunk and invalidate `entityStartPos`.
168
175
  this.preprocessor.pos = this.entityStartPos + consumed - 1;
169
- this._flushCodePointConsumedAsCharacterReference(cp);
176
+ if (!this._isCharacterReferenceInAttribute()) {
177
+ this._ensureCharacterToken(isWhitespace(cp)
178
+ ? TokenType.WHITESPACE_CHARACTER
179
+ : cp === $.NULL
180
+ ? TokenType.NULL_CHARACTER
181
+ : TokenType.CHARACTER);
182
+ }
183
+ this._extendRunWith(String.fromCodePoint(cp), 0);
170
184
  }, handler.onParseError
171
185
  ? {
172
186
  missingSemicolonAfterCharacterReference: () => {
@@ -216,6 +230,99 @@ export class Tokenizer {
216
230
  this.inLoop = false;
217
231
  }
218
232
  //API
233
+ /** Emit buffered character tokens without completing the current token or emitting EOF. */
234
+ flushCharacters() {
235
+ var _a;
236
+ if (this.inLoop) {
237
+ throw new Error('Cannot flush characters from a tokenizer callback');
238
+ }
239
+ const location = (_a = this.currentCharacterToken) === null || _a === void 0 ? void 0 : _a.location;
240
+ let nextLocation = null;
241
+ if (location) {
242
+ nextLocation = this.getCharacterTokenEndLocation(location);
243
+ this.currentLocation = nextLocation;
244
+ }
245
+ this.inLoop = true;
246
+ try {
247
+ this._emitCurrentCharacterToken(nextLocation);
248
+ }
249
+ finally {
250
+ this.inLoop = false;
251
+ }
252
+ this._runParsingLoop();
253
+ }
254
+ getCharacterTokenEndLocation(location) {
255
+ const endOffset = this.getCharacterTokenEndOffset();
256
+ const html = this.preprocessor.html.slice(location.startOffset - this.preprocessor.droppedBufferSize, endOffset - this.preprocessor.droppedBufferSize);
257
+ let line = location.startLine;
258
+ let lastLineBreak = -1;
259
+ for (let i = html.indexOf('\n'); i !== -1; i = html.indexOf('\n', i + 1)) {
260
+ line++;
261
+ lastLineBreak = i;
262
+ }
263
+ for (let i = html.indexOf('\r'); i !== -1; i = html.indexOf('\r', i + 1)) {
264
+ // Count CRLF as one line break by counting its LF only.
265
+ if (html.charCodeAt(i + 1) !== $.LINE_FEED) {
266
+ line++;
267
+ lastLineBreak = Math.max(lastLineBreak, i);
268
+ }
269
+ }
270
+ return {
271
+ startLine: line,
272
+ startCol: lastLineBreak === -1 ? location.startCol + html.length : html.length - lastLineBreak,
273
+ startOffset: endOffset,
274
+ endLine: -1,
275
+ endCol: -1,
276
+ endOffset: -1,
277
+ };
278
+ }
279
+ // Return the input boundary before any unfinished token or buffered delimiter.
280
+ getCharacterTokenEndOffset() {
281
+ var _a;
282
+ const end = this.preprocessor.droppedBufferSize + this.preprocessor.pos + 1;
283
+ const tokenLocation = (_a = this.currentToken) === null || _a === void 0 ? void 0 : _a.location;
284
+ switch (this.state) {
285
+ case State.TAG_OPEN:
286
+ case State.RCDATA_LESS_THAN_SIGN:
287
+ case State.RAWTEXT_LESS_THAN_SIGN:
288
+ case State.SCRIPT_DATA_LESS_THAN_SIGN:
289
+ case State.SCRIPT_DATA_ESCAPED_LESS_THAN_SIGN:
290
+ case State.CDATA_SECTION_BRACKET: {
291
+ return end - 1;
292
+ }
293
+ case State.END_TAG_OPEN:
294
+ case State.MARKUP_DECLARATION_OPEN:
295
+ case State.RCDATA_END_TAG_OPEN:
296
+ case State.RCDATA_END_TAG_NAME:
297
+ case State.RAWTEXT_END_TAG_OPEN:
298
+ case State.RAWTEXT_END_TAG_NAME:
299
+ case State.SCRIPT_DATA_END_TAG_OPEN:
300
+ case State.SCRIPT_DATA_END_TAG_NAME:
301
+ case State.SCRIPT_DATA_ESCAPED_END_TAG_OPEN:
302
+ case State.SCRIPT_DATA_ESCAPED_END_TAG_NAME:
303
+ case State.CDATA_SECTION_END: {
304
+ return end - 2;
305
+ }
306
+ case State.CHARACTER_REFERENCE: {
307
+ return this._isCharacterReferenceInAttribute() && tokenLocation
308
+ ? tokenLocation.startOffset
309
+ : this.preprocessor.droppedBufferSize + this.entityStartPos;
310
+ }
311
+ case State.DOCTYPE:
312
+ case State.BEFORE_DOCTYPE_NAME: {
313
+ return this.currentLocation.startOffset;
314
+ }
315
+ default: {
316
+ if (tokenLocation &&
317
+ (this.state === State.TAG_NAME ||
318
+ (this.state >= State.BEFORE_ATTRIBUTE_NAME && this.state <= State.BOGUS_COMMENT) ||
319
+ (this.state >= State.COMMENT_START && this.state <= State.BOGUS_DOCTYPE))) {
320
+ return tokenLocation.startOffset;
321
+ }
322
+ return end;
323
+ }
324
+ }
325
+ }
219
326
  pause() {
220
327
  this.paused = true;
221
328
  }
@@ -276,6 +383,7 @@ export class Tokenizer {
276
383
  }
277
384
  //Token creation
278
385
  _createStartTagToken() {
386
+ this._flushRun('');
279
387
  this.currentToken = {
280
388
  type: TokenType.START_TAG,
281
389
  tagName: '',
@@ -287,6 +395,7 @@ export class Tokenizer {
287
395
  };
288
396
  }
289
397
  _createEndTagToken() {
398
+ this._flushRun('');
290
399
  this.currentToken = {
291
400
  type: TokenType.END_TAG,
292
401
  tagName: '',
@@ -298,6 +407,7 @@ export class Tokenizer {
298
407
  };
299
408
  }
300
409
  _createCommentToken(offset) {
410
+ this._flushRun('');
301
411
  this.currentToken = {
302
412
  type: TokenType.COMMENT,
303
413
  data: '',
@@ -305,6 +415,7 @@ export class Tokenizer {
305
415
  };
306
416
  }
307
417
  _createDoctypeToken(initialName) {
418
+ this._flushRun('');
308
419
  this.currentToken = {
309
420
  type: TokenType.DOCTYPE,
310
421
  name: initialName,
@@ -314,20 +425,28 @@ export class Tokenizer {
314
425
  location: this.currentLocation,
315
426
  };
316
427
  }
317
- _createCharacterToken(type, chars) {
428
+ _createCharacterToken(type) {
429
+ if (this.currentLocation) {
430
+ const start = this.currentLocation.startOffset - this.preprocessor.droppedBufferSize;
431
+ // A flushed CR already advanced the line; exclude its ignored LF from the next token's range.
432
+ if (this.preprocessor.html[start] === '\n' && this.preprocessor.html[start - 1] === '\r') {
433
+ this.currentLocation.startOffset++;
434
+ }
435
+ }
318
436
  this.currentCharacterToken = {
319
437
  type,
320
- chars,
438
+ chars: '',
321
439
  location: this.currentLocation,
322
440
  };
323
441
  }
324
442
  //Tag attributes
325
443
  _createAttr(attrNameFirstCh) {
444
+ this._flushRun('');
326
445
  this.currentAttr = {
327
446
  name: attrNameFirstCh,
328
447
  value: '',
329
448
  };
330
- this.currentLocation = this.getCurrentLocation(0);
449
+ this.currentAttrLocation = this.getCurrentLocation(0);
331
450
  }
332
451
  _leaveAttrName() {
333
452
  var _a;
@@ -335,9 +454,9 @@ export class Tokenizer {
335
454
  const token = this.currentToken;
336
455
  if (getTokenAttr(token, this.currentAttr.name) === null) {
337
456
  token.attrs.push(this.currentAttr);
338
- if (token.location && this.currentLocation) {
457
+ if (token.location && this.currentAttrLocation) {
339
458
  const attrLocations = ((_a = (_b = token.location).attrs) !== null && _a !== void 0 ? _a : (_b.attrs = Object.create(null)));
340
- attrLocations[this.currentAttr.name] = this.currentLocation;
459
+ attrLocations[this.currentAttr.name] = this.currentAttrLocation;
341
460
  // Set end location
342
461
  this._leaveAttrValue();
343
462
  }
@@ -347,11 +466,71 @@ export class Tokenizer {
347
466
  }
348
467
  }
349
468
  _leaveAttrValue() {
350
- if (this.currentLocation) {
351
- this.currentLocation.endLine = this.preprocessor.line;
352
- this.currentLocation.endCol = this.preprocessor.col;
353
- this.currentLocation.endOffset = this.preprocessor.offset;
469
+ if (this.currentAttrLocation) {
470
+ this.currentAttrLocation.endLine = this.preprocessor.line;
471
+ this.currentAttrLocation.endCol = this.preprocessor.col;
472
+ this.currentAttrLocation.endOffset = this.preprocessor.offset;
473
+ }
474
+ }
475
+ /**
476
+ * Append the pending run and `text` to the attribute value, comment data or character token being built.
477
+ * The run is a string of its own: V8 copies a slice of fewer than 13 characters but lets a longer one share
478
+ * the input's buffer, which would keep that buffer alive for as long as any attribute value or text cut from
479
+ * it is; a concatenation is flattened into a buffer of its own when it is sliced.
480
+ */
481
+ _flushRun(text) {
482
+ if (this.runEnd === this.runStart && text.length === 0) {
483
+ return;
484
+ }
485
+ const slice = this.preprocessor.html.slice(this.runStart, this.runEnd);
486
+ const run = (slice.length < 13 ? slice : ` ${slice}`.slice(1)) + text;
487
+ const token = this.currentToken;
488
+ this.runStart = this.runEnd;
489
+ if (token === null) {
490
+ this.currentCharacterToken.chars += run;
491
+ }
492
+ else if (token.type === TokenType.COMMENT) {
493
+ token.data += run;
494
+ }
495
+ else {
496
+ this.currentAttr.value += run;
497
+ }
498
+ }
499
+ /** Extend the pending run by the current input code point, read as `cp`, or flush it and start a new one. */
500
+ _extendRun(cp) {
501
+ const { preprocessor } = this;
502
+ //NOTE: a CR is read as LF, which is then not in the input.
503
+ if (cp === $.LINE_FEED && preprocessor.html.charCodeAt(preprocessor.pos) !== $.LINE_FEED) {
504
+ this._flushRun('\n');
505
+ this.runStart = preprocessor.pos + 1;
506
+ }
507
+ else {
508
+ const start = preprocessor.pos - Number(cp > 65535);
509
+ if (start !== this.runEnd) {
510
+ this._flushRun('');
511
+ this.runStart = start;
512
+ }
513
+ }
514
+ this.runEnd = preprocessor.pos + 1;
515
+ }
516
+ /**
517
+ * Extend the pending run by `text`, the input that ends `cpOffset` code points after the current one, or flush
518
+ * the run and append `text` where the input differs (a NUL replacement, a decoded reference, a dropped chunk).
519
+ */
520
+ _extendRunWith(text, cpOffset) {
521
+ const { preprocessor } = this;
522
+ const start = preprocessor.codePointStart + 1 + cpOffset - text.length;
523
+ if (start >= 0 && preprocessor.html.startsWith(text, start)) {
524
+ if (start !== this.runEnd) {
525
+ this._flushRun('');
526
+ this.runStart = start;
527
+ }
528
+ }
529
+ else {
530
+ this._flushRun(text);
531
+ this.runStart = start + text.length;
354
532
  }
533
+ this.runEnd = start + text.length;
355
534
  }
356
535
  //Token emission
357
536
  prepareToken(ct) {
@@ -394,6 +573,7 @@ export class Tokenizer {
394
573
  this.preprocessor.dropParsedChunk();
395
574
  }
396
575
  _emitCurrentCharacterToken(nextLocation) {
576
+ this._flushRun('');
397
577
  if (this.currentCharacterToken) {
398
578
  //NOTE: if we have a pending character token, make it's end location equal to the
399
579
  //current token's start location.
@@ -439,32 +619,48 @@ export class Tokenizer {
439
619
  //1)TokenType.NULL_CHARACTER - \u0000-character sequences (e.g. '\u0000\u0000\u0000')
440
620
  //2)TokenType.WHITESPACE_CHARACTER - any whitespace/new-line character sequences (e.g. '\n \r\t \f')
441
621
  //3)TokenType.CHARACTER - any character sequence which don't belong to groups 1 and 2 (e.g. 'abcdef1234@@#$%^')
442
- _appendCharToCurrentCharacterToken(type, ch) {
622
+ /** Make the current character token one of `type`, emitting the current one if it has another type. */
623
+ _ensureCharacterToken(type) {
443
624
  if (this.currentCharacterToken) {
444
625
  if (this.currentCharacterToken.type === type) {
445
- this.currentCharacterToken.chars += ch;
446
626
  return;
447
627
  }
448
- else {
449
- this.currentLocation = this.getCurrentLocation(0);
450
- this._emitCurrentCharacterToken(this.currentLocation);
451
- this.preprocessor.dropParsedChunk();
452
- }
628
+ this.currentLocation = this.getCurrentLocation(0);
629
+ this._emitCurrentCharacterToken(this.currentLocation);
630
+ this.preprocessor.dropParsedChunk();
453
631
  }
454
- this._createCharacterToken(type, ch);
632
+ this._createCharacterToken(type);
455
633
  }
634
+ /** Emit the current input code point, read as `cp`. */
456
635
  _emitCodePoint(cp) {
457
636
  const type = isWhitespace(cp)
458
637
  ? TokenType.WHITESPACE_CHARACTER
459
638
  : cp === $.NULL
460
639
  ? TokenType.NULL_CHARACTER
461
640
  : TokenType.CHARACTER;
462
- this._appendCharToCurrentCharacterToken(type, String.fromCodePoint(cp));
641
+ const token = this.currentCharacterToken;
642
+ if (token === null || token.type !== type) {
643
+ this._ensureCharacterToken(type);
644
+ }
645
+ this._extendRun(cp);
463
646
  }
464
647
  //NOTE: used when we emit characters explicitly.
465
648
  //This is always for non-whitespace and non-null characters, which allows us to avoid additional checks.
466
- _emitChars(ch) {
467
- this._appendCharToCurrentCharacterToken(TokenType.CHARACTER, ch);
649
+ // Negative `cpOffset` values locate text emitted before the current input character.
650
+ _emitChars(ch, cpOffset = 0) {
651
+ if (this.options.sourceCodeLocationInfo &&
652
+ cpOffset !== 0 &&
653
+ this.currentCharacterToken &&
654
+ this.currentCharacterToken.type !== TokenType.CHARACTER) {
655
+ this.currentLocation = this.getCurrentLocation(ch.length - 1 - cpOffset);
656
+ this._emitCurrentCharacterToken(this.currentLocation);
657
+ // Keep source characters that precede the lookahead available for a subsequent flush.
658
+ this._createCharacterToken(TokenType.CHARACTER);
659
+ }
660
+ else {
661
+ this._ensureCharacterToken(TokenType.CHARACTER);
662
+ }
663
+ this._extendRunWith(ch, cpOffset);
468
664
  }
469
665
  // Character reference helpers
470
666
  _startCharacterReference() {
@@ -480,7 +676,7 @@ export class Tokenizer {
480
676
  }
481
677
  _flushCodePointConsumedAsCharacterReference(cp) {
482
678
  if (this._isCharacterReferenceInAttribute()) {
483
- this.currentAttr.value += String.fromCodePoint(cp);
679
+ this._extendRun(cp);
484
680
  }
485
681
  else {
486
682
  this._emitCodePoint(cp);
@@ -928,13 +1124,13 @@ export class Tokenizer {
928
1124
  }
929
1125
  case $.EOF: {
930
1126
  this._err(ERR.eofBeforeTagName);
931
- this._emitChars('<');
1127
+ this._emitChars('<', -1);
932
1128
  this._emitEOFToken();
933
1129
  break;
934
1130
  }
935
1131
  default: {
936
1132
  this._err(ERR.invalidFirstCharacterOfTagName);
937
- this._emitChars('<');
1133
+ this._emitChars('<', -1);
938
1134
  this.state = State.DATA;
939
1135
  this._stateData(cp);
940
1136
  }
@@ -957,7 +1153,7 @@ export class Tokenizer {
957
1153
  }
958
1154
  case $.EOF: {
959
1155
  this._err(ERR.eofBeforeTagName);
960
- this._emitChars('</');
1156
+ this._emitChars('</', -1);
961
1157
  this._emitEOFToken();
962
1158
  break;
963
1159
  }
@@ -1012,7 +1208,7 @@ export class Tokenizer {
1012
1208
  this.state = State.RCDATA_END_TAG_OPEN;
1013
1209
  }
1014
1210
  else {
1015
- this._emitChars('<');
1211
+ this._emitChars('<', -1);
1016
1212
  this.state = State.RCDATA;
1017
1213
  this._stateRcdata(cp);
1018
1214
  }
@@ -1025,7 +1221,7 @@ export class Tokenizer {
1025
1221
  this._stateRcdataEndTagName(cp);
1026
1222
  }
1027
1223
  else {
1028
- this._emitChars('</');
1224
+ this._emitChars('</', -1);
1029
1225
  this.state = State.RCDATA;
1030
1226
  this._stateRcdata(cp);
1031
1227
  }
@@ -1059,6 +1255,8 @@ export class Tokenizer {
1059
1255
  return false;
1060
1256
  }
1061
1257
  default: {
1258
+ //NOTE: the end tag is text after all, so no token is being built.
1259
+ this.currentToken = null;
1062
1260
  return !this._ensureHibernation();
1063
1261
  }
1064
1262
  }
@@ -1067,7 +1265,7 @@ export class Tokenizer {
1067
1265
  //------------------------------------------------------------------
1068
1266
  _stateRcdataEndTagName(cp) {
1069
1267
  if (this.handleSpecialEndTag(cp)) {
1070
- this._emitChars('</');
1268
+ this._emitChars('</', -1);
1071
1269
  this.state = State.RCDATA;
1072
1270
  this._stateRcdata(cp);
1073
1271
  }
@@ -1079,7 +1277,7 @@ export class Tokenizer {
1079
1277
  this.state = State.RAWTEXT_END_TAG_OPEN;
1080
1278
  }
1081
1279
  else {
1082
- this._emitChars('<');
1280
+ this._emitChars('<', -1);
1083
1281
  this.state = State.RAWTEXT;
1084
1282
  this._stateRawtext(cp);
1085
1283
  }
@@ -1092,7 +1290,7 @@ export class Tokenizer {
1092
1290
  this._stateRawtextEndTagName(cp);
1093
1291
  }
1094
1292
  else {
1095
- this._emitChars('</');
1293
+ this._emitChars('</', -1);
1096
1294
  this.state = State.RAWTEXT;
1097
1295
  this._stateRawtext(cp);
1098
1296
  }
@@ -1101,7 +1299,7 @@ export class Tokenizer {
1101
1299
  //------------------------------------------------------------------
1102
1300
  _stateRawtextEndTagName(cp) {
1103
1301
  if (this.handleSpecialEndTag(cp)) {
1104
- this._emitChars('</');
1302
+ this._emitChars('</', -1);
1105
1303
  this.state = State.RAWTEXT;
1106
1304
  this._stateRawtext(cp);
1107
1305
  }
@@ -1120,7 +1318,7 @@ export class Tokenizer {
1120
1318
  break;
1121
1319
  }
1122
1320
  default: {
1123
- this._emitChars('<');
1321
+ this._emitChars('<', -1);
1124
1322
  this.state = State.SCRIPT_DATA;
1125
1323
  this._stateScriptData(cp);
1126
1324
  }
@@ -1134,7 +1332,7 @@ export class Tokenizer {
1134
1332
  this._stateScriptDataEndTagName(cp);
1135
1333
  }
1136
1334
  else {
1137
- this._emitChars('</');
1335
+ this._emitChars('</', -1);
1138
1336
  this.state = State.SCRIPT_DATA;
1139
1337
  this._stateScriptData(cp);
1140
1338
  }
@@ -1143,7 +1341,7 @@ export class Tokenizer {
1143
1341
  //------------------------------------------------------------------
1144
1342
  _stateScriptDataEndTagName(cp) {
1145
1343
  if (this.handleSpecialEndTag(cp)) {
1146
- this._emitChars('</');
1344
+ this._emitChars('</', -1);
1147
1345
  this.state = State.SCRIPT_DATA;
1148
1346
  this._stateScriptData(cp);
1149
1347
  }
@@ -1271,12 +1469,12 @@ export class Tokenizer {
1271
1469
  this.state = State.SCRIPT_DATA_ESCAPED_END_TAG_OPEN;
1272
1470
  }
1273
1471
  else if (isAsciiLetter(cp)) {
1274
- this._emitChars('<');
1472
+ this._emitChars('<', -1);
1275
1473
  this.state = State.SCRIPT_DATA_DOUBLE_ESCAPE_START;
1276
1474
  this._stateScriptDataDoubleEscapeStart(cp);
1277
1475
  }
1278
1476
  else {
1279
- this._emitChars('<');
1477
+ this._emitChars('<', -1);
1280
1478
  this.state = State.SCRIPT_DATA_ESCAPED;
1281
1479
  this._stateScriptDataEscaped(cp);
1282
1480
  }
@@ -1289,7 +1487,7 @@ export class Tokenizer {
1289
1487
  this._stateScriptDataEscapedEndTagName(cp);
1290
1488
  }
1291
1489
  else {
1292
- this._emitChars('</');
1490
+ this._emitChars('</', -1);
1293
1491
  this.state = State.SCRIPT_DATA_ESCAPED;
1294
1492
  this._stateScriptDataEscaped(cp);
1295
1493
  }
@@ -1298,7 +1496,7 @@ export class Tokenizer {
1298
1496
  //------------------------------------------------------------------
1299
1497
  _stateScriptDataEscapedEndTagName(cp) {
1300
1498
  if (this.handleSpecialEndTag(cp)) {
1301
- this._emitChars('</');
1499
+ this._emitChars('</', -1);
1302
1500
  this.state = State.SCRIPT_DATA_ESCAPED;
1303
1501
  this._stateScriptDataEscaped(cp);
1304
1502
  }
@@ -1592,7 +1790,7 @@ export class Tokenizer {
1592
1790
  }
1593
1791
  case $.NULL: {
1594
1792
  this._err(ERR.unexpectedNullCharacter);
1595
- this.currentAttr.value += REPLACEMENT_CHARACTER;
1793
+ this._extendRunWith(REPLACEMENT_CHARACTER, 0);
1596
1794
  break;
1597
1795
  }
1598
1796
  case $.EOF: {
@@ -1601,7 +1799,7 @@ export class Tokenizer {
1601
1799
  break;
1602
1800
  }
1603
1801
  default: {
1604
- this.currentAttr.value += String.fromCodePoint(cp);
1802
+ this._extendRun(cp);
1605
1803
  }
1606
1804
  }
1607
1805
  }
@@ -1619,7 +1817,7 @@ export class Tokenizer {
1619
1817
  }
1620
1818
  case $.NULL: {
1621
1819
  this._err(ERR.unexpectedNullCharacter);
1622
- this.currentAttr.value += REPLACEMENT_CHARACTER;
1820
+ this._extendRunWith(REPLACEMENT_CHARACTER, 0);
1623
1821
  break;
1624
1822
  }
1625
1823
  case $.EOF: {
@@ -1628,7 +1826,7 @@ export class Tokenizer {
1628
1826
  break;
1629
1827
  }
1630
1828
  default: {
1631
- this.currentAttr.value += String.fromCodePoint(cp);
1829
+ this._extendRun(cp);
1632
1830
  }
1633
1831
  }
1634
1832
  }
@@ -1656,7 +1854,7 @@ export class Tokenizer {
1656
1854
  }
1657
1855
  case $.NULL: {
1658
1856
  this._err(ERR.unexpectedNullCharacter);
1659
- this.currentAttr.value += REPLACEMENT_CHARACTER;
1857
+ this._extendRunWith(REPLACEMENT_CHARACTER, 0);
1660
1858
  break;
1661
1859
  }
1662
1860
  case $.QUOTATION_MARK:
@@ -1665,7 +1863,7 @@ export class Tokenizer {
1665
1863
  case $.EQUALS_SIGN:
1666
1864
  case $.GRAVE_ACCENT: {
1667
1865
  this._err(ERR.unexpectedCharacterInUnquotedAttributeValue);
1668
- this.currentAttr.value += String.fromCodePoint(cp);
1866
+ this._extendRun(cp);
1669
1867
  break;
1670
1868
  }
1671
1869
  case $.EOF: {
@@ -1674,7 +1872,7 @@ export class Tokenizer {
1674
1872
  break;
1675
1873
  }
1676
1874
  default: {
1677
- this.currentAttr.value += String.fromCodePoint(cp);
1875
+ this._extendRun(cp);
1678
1876
  }
1679
1877
  }
1680
1878
  }
@@ -1708,6 +1906,7 @@ export class Tokenizer {
1708
1906
  }
1709
1907
  default: {
1710
1908
  this._err(ERR.missingWhitespaceBetweenAttributes);
1909
+ this._leaveAttrValue();
1711
1910
  this.state = State.BEFORE_ATTRIBUTE_NAME;
1712
1911
  this._stateBeforeAttributeName(cp);
1713
1912
  }
@@ -1753,11 +1952,11 @@ export class Tokenizer {
1753
1952
  }
1754
1953
  case $.NULL: {
1755
1954
  this._err(ERR.unexpectedNullCharacter);
1756
- token.data += REPLACEMENT_CHARACTER;
1955
+ this._extendRunWith(REPLACEMENT_CHARACTER, 0);
1757
1956
  break;
1758
1957
  }
1759
1958
  default: {
1760
- token.data += String.fromCodePoint(cp);
1959
+ this._extendRun(cp);
1761
1960
  }
1762
1961
  }
1763
1962
  }
@@ -1836,7 +2035,7 @@ export class Tokenizer {
1836
2035
  break;
1837
2036
  }
1838
2037
  default: {
1839
- token.data += '-';
2038
+ this._extendRunWith('-', -1);
1840
2039
  this.state = State.COMMENT;
1841
2040
  this._stateComment(cp);
1842
2041
  }
@@ -1852,13 +2051,13 @@ export class Tokenizer {
1852
2051
  break;
1853
2052
  }
1854
2053
  case $.LESS_THAN_SIGN: {
1855
- token.data += '<';
2054
+ this._extendRunWith('<', 0);
1856
2055
  this.state = State.COMMENT_LESS_THAN_SIGN;
1857
2056
  break;
1858
2057
  }
1859
2058
  case $.NULL: {
1860
2059
  this._err(ERR.unexpectedNullCharacter);
1861
- token.data += REPLACEMENT_CHARACTER;
2060
+ this._extendRunWith(REPLACEMENT_CHARACTER, 0);
1862
2061
  break;
1863
2062
  }
1864
2063
  case $.EOF: {
@@ -1868,22 +2067,21 @@ export class Tokenizer {
1868
2067
  break;
1869
2068
  }
1870
2069
  default: {
1871
- token.data += String.fromCodePoint(cp);
2070
+ this._extendRun(cp);
1872
2071
  }
1873
2072
  }
1874
2073
  }
1875
2074
  // Comment less-than sign state
1876
2075
  //------------------------------------------------------------------
1877
2076
  _stateCommentLessThanSign(cp) {
1878
- const token = this.currentToken;
1879
2077
  switch (cp) {
1880
2078
  case $.EXCLAMATION_MARK: {
1881
- token.data += '!';
2079
+ this._extendRunWith('!', 0);
1882
2080
  this.state = State.COMMENT_LESS_THAN_SIGN_BANG;
1883
2081
  break;
1884
2082
  }
1885
2083
  case $.LESS_THAN_SIGN: {
1886
- token.data += '<';
2084
+ this._extendRunWith('<', 0);
1887
2085
  break;
1888
2086
  }
1889
2087
  default: {
@@ -1939,7 +2137,7 @@ export class Tokenizer {
1939
2137
  break;
1940
2138
  }
1941
2139
  default: {
1942
- token.data += '-';
2140
+ this._extendRunWith('-', -1);
1943
2141
  this.state = State.COMMENT;
1944
2142
  this._stateComment(cp);
1945
2143
  }
@@ -1960,7 +2158,7 @@ export class Tokenizer {
1960
2158
  break;
1961
2159
  }
1962
2160
  case $.HYPHEN_MINUS: {
1963
- token.data += '-';
2161
+ this._extendRunWith('-', -2);
1964
2162
  break;
1965
2163
  }
1966
2164
  case $.EOF: {
@@ -1970,7 +2168,7 @@ export class Tokenizer {
1970
2168
  break;
1971
2169
  }
1972
2170
  default: {
1973
- token.data += '--';
2171
+ this._extendRunWith('--', -1);
1974
2172
  this.state = State.COMMENT;
1975
2173
  this._stateComment(cp);
1976
2174
  }
@@ -1982,7 +2180,7 @@ export class Tokenizer {
1982
2180
  const token = this.currentToken;
1983
2181
  switch (cp) {
1984
2182
  case $.HYPHEN_MINUS: {
1985
- token.data += '--!';
2183
+ this._extendRunWith('--!', -1);
1986
2184
  this.state = State.COMMENT_END_DASH;
1987
2185
  break;
1988
2186
  }
@@ -1999,7 +2197,7 @@ export class Tokenizer {
1999
2197
  break;
2000
2198
  }
2001
2199
  default: {
2002
- token.data += '--!';
2200
+ this._extendRunWith('--!', -1);
2003
2201
  this.state = State.COMMENT;
2004
2202
  this._stateComment(cp);
2005
2203
  }
@@ -2636,7 +2834,7 @@ export class Tokenizer {
2636
2834
  this.state = State.CDATA_SECTION_END;
2637
2835
  }
2638
2836
  else {
2639
- this._emitChars(']');
2837
+ this._emitChars(']', -1);
2640
2838
  this.state = State.CDATA_SECTION;
2641
2839
  this._stateCdataSection(cp);
2642
2840
  }
@@ -2650,11 +2848,11 @@ export class Tokenizer {
2650
2848
  break;
2651
2849
  }
2652
2850
  case $.RIGHT_SQUARE_BRACKET: {
2653
- this._emitChars(']');
2851
+ this._emitChars(']', -2);
2654
2852
  break;
2655
2853
  }
2656
2854
  default: {
2657
- this._emitChars(']]');
2855
+ this._emitChars(']]', -1);
2658
2856
  this.state = State.CDATA_SECTION;
2659
2857
  this._stateCdataSection(cp);
2660
2858
  }
@@ -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;
@@ -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;
@@ -81,12 +86,16 @@ export class Preprocessor {
81
86
  }
82
87
  dropParsedChunk() {
83
88
  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;
89
+ // Retain the entire current code point, including both units of a surrogate pair.
90
+ const droppedSize = this.offset - this.droppedBufferSize;
91
+ this.html = this.html.substring(droppedSize);
92
+ this.lineStartPos -= droppedSize;
93
+ this.droppedBufferSize += droppedSize;
94
+ this.pos -= droppedSize;
88
95
  this.lastGapPos = -2;
89
96
  this.gapStack.length = 0;
97
+ if (this.pos > 0)
98
+ this._addGap();
90
99
  }
91
100
  }
92
101
  write(chunk, isLastChunk) {
@@ -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,13 +2,18 @@
2
2
  "name": "parse5",
3
3
  "type": "module",
4
4
  "description": "HTML parser and serializer.",
5
- "version": "8.0.0",
5
+ "version": "8.1.0",
6
6
  "author": "Ivan Nikulin <ifaaan@gmail.com> (https://github.com/inikulin)",
7
- "contributors": "https://github.com/inikulin/parse5/graphs/contributors",
7
+ "contributors": [
8
+ "James Garbutt (https://github.com/43081j)",
9
+ "Felix Boehm (https://github.com/fb55)",
10
+ "Ivan Nikulin (https://github.com/inikulin)",
11
+ "Titus (https://github.com/wooorm)"
12
+ ],
8
13
  "homepage": "https://parse5.js.org",
9
14
  "funding": "https://github.com/inikulin/parse5?sponsor=1",
10
15
  "dependencies": {
11
- "entities": "^6.0.0"
16
+ "entities": "^9.0.0"
12
17
  },
13
18
  "keywords": [
14
19
  "html",
@@ -44,5 +49,9 @@
44
49
  "files": [
45
50
  "dist/**/*.js",
46
51
  "dist/**/*.d.ts"
47
- ]
52
+ ],
53
+ "devDependencies": {
54
+ "parse5-htmlparser2-tree-adapter": "^8.0.0",
55
+ "parse5-test-utils": "1.0.0"
56
+ }
48
57
  }