parse5 8.0.1 → 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,34 +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
- // OPTIMIZATION: Use String.fromCharCode for BMP characters (< 0x10000) which is faster
463
- // than String.fromCodePoint. Characters outside BMP are rare in HTML.
464
- this._appendCharToCurrentCharacterToken(type, cp < 65536 ? String.fromCharCode(cp) : String.fromCodePoint(cp));
641
+ const token = this.currentCharacterToken;
642
+ if (token === null || token.type !== type) {
643
+ this._ensureCharacterToken(type);
644
+ }
645
+ this._extendRun(cp);
465
646
  }
466
647
  //NOTE: used when we emit characters explicitly.
467
648
  //This is always for non-whitespace and non-null characters, which allows us to avoid additional checks.
468
- _emitChars(ch) {
469
- 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);
470
664
  }
471
665
  // Character reference helpers
472
666
  _startCharacterReference() {
@@ -482,7 +676,7 @@ export class Tokenizer {
482
676
  }
483
677
  _flushCodePointConsumedAsCharacterReference(cp) {
484
678
  if (this._isCharacterReferenceInAttribute()) {
485
- this.currentAttr.value += String.fromCodePoint(cp);
679
+ this._extendRun(cp);
486
680
  }
487
681
  else {
488
682
  this._emitCodePoint(cp);
@@ -930,13 +1124,13 @@ export class Tokenizer {
930
1124
  }
931
1125
  case $.EOF: {
932
1126
  this._err(ERR.eofBeforeTagName);
933
- this._emitChars('<');
1127
+ this._emitChars('<', -1);
934
1128
  this._emitEOFToken();
935
1129
  break;
936
1130
  }
937
1131
  default: {
938
1132
  this._err(ERR.invalidFirstCharacterOfTagName);
939
- this._emitChars('<');
1133
+ this._emitChars('<', -1);
940
1134
  this.state = State.DATA;
941
1135
  this._stateData(cp);
942
1136
  }
@@ -959,7 +1153,7 @@ export class Tokenizer {
959
1153
  }
960
1154
  case $.EOF: {
961
1155
  this._err(ERR.eofBeforeTagName);
962
- this._emitChars('</');
1156
+ this._emitChars('</', -1);
963
1157
  this._emitEOFToken();
964
1158
  break;
965
1159
  }
@@ -1014,7 +1208,7 @@ export class Tokenizer {
1014
1208
  this.state = State.RCDATA_END_TAG_OPEN;
1015
1209
  }
1016
1210
  else {
1017
- this._emitChars('<');
1211
+ this._emitChars('<', -1);
1018
1212
  this.state = State.RCDATA;
1019
1213
  this._stateRcdata(cp);
1020
1214
  }
@@ -1027,7 +1221,7 @@ export class Tokenizer {
1027
1221
  this._stateRcdataEndTagName(cp);
1028
1222
  }
1029
1223
  else {
1030
- this._emitChars('</');
1224
+ this._emitChars('</', -1);
1031
1225
  this.state = State.RCDATA;
1032
1226
  this._stateRcdata(cp);
1033
1227
  }
@@ -1061,6 +1255,8 @@ export class Tokenizer {
1061
1255
  return false;
1062
1256
  }
1063
1257
  default: {
1258
+ //NOTE: the end tag is text after all, so no token is being built.
1259
+ this.currentToken = null;
1064
1260
  return !this._ensureHibernation();
1065
1261
  }
1066
1262
  }
@@ -1069,7 +1265,7 @@ export class Tokenizer {
1069
1265
  //------------------------------------------------------------------
1070
1266
  _stateRcdataEndTagName(cp) {
1071
1267
  if (this.handleSpecialEndTag(cp)) {
1072
- this._emitChars('</');
1268
+ this._emitChars('</', -1);
1073
1269
  this.state = State.RCDATA;
1074
1270
  this._stateRcdata(cp);
1075
1271
  }
@@ -1081,7 +1277,7 @@ export class Tokenizer {
1081
1277
  this.state = State.RAWTEXT_END_TAG_OPEN;
1082
1278
  }
1083
1279
  else {
1084
- this._emitChars('<');
1280
+ this._emitChars('<', -1);
1085
1281
  this.state = State.RAWTEXT;
1086
1282
  this._stateRawtext(cp);
1087
1283
  }
@@ -1094,7 +1290,7 @@ export class Tokenizer {
1094
1290
  this._stateRawtextEndTagName(cp);
1095
1291
  }
1096
1292
  else {
1097
- this._emitChars('</');
1293
+ this._emitChars('</', -1);
1098
1294
  this.state = State.RAWTEXT;
1099
1295
  this._stateRawtext(cp);
1100
1296
  }
@@ -1103,7 +1299,7 @@ export class Tokenizer {
1103
1299
  //------------------------------------------------------------------
1104
1300
  _stateRawtextEndTagName(cp) {
1105
1301
  if (this.handleSpecialEndTag(cp)) {
1106
- this._emitChars('</');
1302
+ this._emitChars('</', -1);
1107
1303
  this.state = State.RAWTEXT;
1108
1304
  this._stateRawtext(cp);
1109
1305
  }
@@ -1122,7 +1318,7 @@ export class Tokenizer {
1122
1318
  break;
1123
1319
  }
1124
1320
  default: {
1125
- this._emitChars('<');
1321
+ this._emitChars('<', -1);
1126
1322
  this.state = State.SCRIPT_DATA;
1127
1323
  this._stateScriptData(cp);
1128
1324
  }
@@ -1136,7 +1332,7 @@ export class Tokenizer {
1136
1332
  this._stateScriptDataEndTagName(cp);
1137
1333
  }
1138
1334
  else {
1139
- this._emitChars('</');
1335
+ this._emitChars('</', -1);
1140
1336
  this.state = State.SCRIPT_DATA;
1141
1337
  this._stateScriptData(cp);
1142
1338
  }
@@ -1145,7 +1341,7 @@ export class Tokenizer {
1145
1341
  //------------------------------------------------------------------
1146
1342
  _stateScriptDataEndTagName(cp) {
1147
1343
  if (this.handleSpecialEndTag(cp)) {
1148
- this._emitChars('</');
1344
+ this._emitChars('</', -1);
1149
1345
  this.state = State.SCRIPT_DATA;
1150
1346
  this._stateScriptData(cp);
1151
1347
  }
@@ -1273,12 +1469,12 @@ export class Tokenizer {
1273
1469
  this.state = State.SCRIPT_DATA_ESCAPED_END_TAG_OPEN;
1274
1470
  }
1275
1471
  else if (isAsciiLetter(cp)) {
1276
- this._emitChars('<');
1472
+ this._emitChars('<', -1);
1277
1473
  this.state = State.SCRIPT_DATA_DOUBLE_ESCAPE_START;
1278
1474
  this._stateScriptDataDoubleEscapeStart(cp);
1279
1475
  }
1280
1476
  else {
1281
- this._emitChars('<');
1477
+ this._emitChars('<', -1);
1282
1478
  this.state = State.SCRIPT_DATA_ESCAPED;
1283
1479
  this._stateScriptDataEscaped(cp);
1284
1480
  }
@@ -1291,7 +1487,7 @@ export class Tokenizer {
1291
1487
  this._stateScriptDataEscapedEndTagName(cp);
1292
1488
  }
1293
1489
  else {
1294
- this._emitChars('</');
1490
+ this._emitChars('</', -1);
1295
1491
  this.state = State.SCRIPT_DATA_ESCAPED;
1296
1492
  this._stateScriptDataEscaped(cp);
1297
1493
  }
@@ -1300,7 +1496,7 @@ export class Tokenizer {
1300
1496
  //------------------------------------------------------------------
1301
1497
  _stateScriptDataEscapedEndTagName(cp) {
1302
1498
  if (this.handleSpecialEndTag(cp)) {
1303
- this._emitChars('</');
1499
+ this._emitChars('</', -1);
1304
1500
  this.state = State.SCRIPT_DATA_ESCAPED;
1305
1501
  this._stateScriptDataEscaped(cp);
1306
1502
  }
@@ -1594,7 +1790,7 @@ export class Tokenizer {
1594
1790
  }
1595
1791
  case $.NULL: {
1596
1792
  this._err(ERR.unexpectedNullCharacter);
1597
- this.currentAttr.value += REPLACEMENT_CHARACTER;
1793
+ this._extendRunWith(REPLACEMENT_CHARACTER, 0);
1598
1794
  break;
1599
1795
  }
1600
1796
  case $.EOF: {
@@ -1603,7 +1799,7 @@ export class Tokenizer {
1603
1799
  break;
1604
1800
  }
1605
1801
  default: {
1606
- this.currentAttr.value += String.fromCodePoint(cp);
1802
+ this._extendRun(cp);
1607
1803
  }
1608
1804
  }
1609
1805
  }
@@ -1621,7 +1817,7 @@ export class Tokenizer {
1621
1817
  }
1622
1818
  case $.NULL: {
1623
1819
  this._err(ERR.unexpectedNullCharacter);
1624
- this.currentAttr.value += REPLACEMENT_CHARACTER;
1820
+ this._extendRunWith(REPLACEMENT_CHARACTER, 0);
1625
1821
  break;
1626
1822
  }
1627
1823
  case $.EOF: {
@@ -1630,7 +1826,7 @@ export class Tokenizer {
1630
1826
  break;
1631
1827
  }
1632
1828
  default: {
1633
- this.currentAttr.value += String.fromCodePoint(cp);
1829
+ this._extendRun(cp);
1634
1830
  }
1635
1831
  }
1636
1832
  }
@@ -1658,7 +1854,7 @@ export class Tokenizer {
1658
1854
  }
1659
1855
  case $.NULL: {
1660
1856
  this._err(ERR.unexpectedNullCharacter);
1661
- this.currentAttr.value += REPLACEMENT_CHARACTER;
1857
+ this._extendRunWith(REPLACEMENT_CHARACTER, 0);
1662
1858
  break;
1663
1859
  }
1664
1860
  case $.QUOTATION_MARK:
@@ -1667,7 +1863,7 @@ export class Tokenizer {
1667
1863
  case $.EQUALS_SIGN:
1668
1864
  case $.GRAVE_ACCENT: {
1669
1865
  this._err(ERR.unexpectedCharacterInUnquotedAttributeValue);
1670
- this.currentAttr.value += String.fromCodePoint(cp);
1866
+ this._extendRun(cp);
1671
1867
  break;
1672
1868
  }
1673
1869
  case $.EOF: {
@@ -1676,7 +1872,7 @@ export class Tokenizer {
1676
1872
  break;
1677
1873
  }
1678
1874
  default: {
1679
- this.currentAttr.value += String.fromCodePoint(cp);
1875
+ this._extendRun(cp);
1680
1876
  }
1681
1877
  }
1682
1878
  }
@@ -1710,6 +1906,7 @@ export class Tokenizer {
1710
1906
  }
1711
1907
  default: {
1712
1908
  this._err(ERR.missingWhitespaceBetweenAttributes);
1909
+ this._leaveAttrValue();
1713
1910
  this.state = State.BEFORE_ATTRIBUTE_NAME;
1714
1911
  this._stateBeforeAttributeName(cp);
1715
1912
  }
@@ -1755,11 +1952,11 @@ export class Tokenizer {
1755
1952
  }
1756
1953
  case $.NULL: {
1757
1954
  this._err(ERR.unexpectedNullCharacter);
1758
- token.data += REPLACEMENT_CHARACTER;
1955
+ this._extendRunWith(REPLACEMENT_CHARACTER, 0);
1759
1956
  break;
1760
1957
  }
1761
1958
  default: {
1762
- token.data += String.fromCodePoint(cp);
1959
+ this._extendRun(cp);
1763
1960
  }
1764
1961
  }
1765
1962
  }
@@ -1838,7 +2035,7 @@ export class Tokenizer {
1838
2035
  break;
1839
2036
  }
1840
2037
  default: {
1841
- token.data += '-';
2038
+ this._extendRunWith('-', -1);
1842
2039
  this.state = State.COMMENT;
1843
2040
  this._stateComment(cp);
1844
2041
  }
@@ -1854,13 +2051,13 @@ export class Tokenizer {
1854
2051
  break;
1855
2052
  }
1856
2053
  case $.LESS_THAN_SIGN: {
1857
- token.data += '<';
2054
+ this._extendRunWith('<', 0);
1858
2055
  this.state = State.COMMENT_LESS_THAN_SIGN;
1859
2056
  break;
1860
2057
  }
1861
2058
  case $.NULL: {
1862
2059
  this._err(ERR.unexpectedNullCharacter);
1863
- token.data += REPLACEMENT_CHARACTER;
2060
+ this._extendRunWith(REPLACEMENT_CHARACTER, 0);
1864
2061
  break;
1865
2062
  }
1866
2063
  case $.EOF: {
@@ -1870,22 +2067,21 @@ export class Tokenizer {
1870
2067
  break;
1871
2068
  }
1872
2069
  default: {
1873
- token.data += String.fromCodePoint(cp);
2070
+ this._extendRun(cp);
1874
2071
  }
1875
2072
  }
1876
2073
  }
1877
2074
  // Comment less-than sign state
1878
2075
  //------------------------------------------------------------------
1879
2076
  _stateCommentLessThanSign(cp) {
1880
- const token = this.currentToken;
1881
2077
  switch (cp) {
1882
2078
  case $.EXCLAMATION_MARK: {
1883
- token.data += '!';
2079
+ this._extendRunWith('!', 0);
1884
2080
  this.state = State.COMMENT_LESS_THAN_SIGN_BANG;
1885
2081
  break;
1886
2082
  }
1887
2083
  case $.LESS_THAN_SIGN: {
1888
- token.data += '<';
2084
+ this._extendRunWith('<', 0);
1889
2085
  break;
1890
2086
  }
1891
2087
  default: {
@@ -1941,7 +2137,7 @@ export class Tokenizer {
1941
2137
  break;
1942
2138
  }
1943
2139
  default: {
1944
- token.data += '-';
2140
+ this._extendRunWith('-', -1);
1945
2141
  this.state = State.COMMENT;
1946
2142
  this._stateComment(cp);
1947
2143
  }
@@ -1962,7 +2158,7 @@ export class Tokenizer {
1962
2158
  break;
1963
2159
  }
1964
2160
  case $.HYPHEN_MINUS: {
1965
- token.data += '-';
2161
+ this._extendRunWith('-', -2);
1966
2162
  break;
1967
2163
  }
1968
2164
  case $.EOF: {
@@ -1972,7 +2168,7 @@ export class Tokenizer {
1972
2168
  break;
1973
2169
  }
1974
2170
  default: {
1975
- token.data += '--';
2171
+ this._extendRunWith('--', -1);
1976
2172
  this.state = State.COMMENT;
1977
2173
  this._stateComment(cp);
1978
2174
  }
@@ -1984,7 +2180,7 @@ export class Tokenizer {
1984
2180
  const token = this.currentToken;
1985
2181
  switch (cp) {
1986
2182
  case $.HYPHEN_MINUS: {
1987
- token.data += '--!';
2183
+ this._extendRunWith('--!', -1);
1988
2184
  this.state = State.COMMENT_END_DASH;
1989
2185
  break;
1990
2186
  }
@@ -2001,7 +2197,7 @@ export class Tokenizer {
2001
2197
  break;
2002
2198
  }
2003
2199
  default: {
2004
- token.data += '--!';
2200
+ this._extendRunWith('--!', -1);
2005
2201
  this.state = State.COMMENT;
2006
2202
  this._stateComment(cp);
2007
2203
  }
@@ -2638,7 +2834,7 @@ export class Tokenizer {
2638
2834
  this.state = State.CDATA_SECTION_END;
2639
2835
  }
2640
2836
  else {
2641
- this._emitChars(']');
2837
+ this._emitChars(']', -1);
2642
2838
  this.state = State.CDATA_SECTION;
2643
2839
  this._stateCdataSection(cp);
2644
2840
  }
@@ -2652,11 +2848,11 @@ export class Tokenizer {
2652
2848
  break;
2653
2849
  }
2654
2850
  case $.RIGHT_SQUARE_BRACKET: {
2655
- this._emitChars(']');
2851
+ this._emitChars(']', -2);
2656
2852
  break;
2657
2853
  }
2658
2854
  default: {
2659
- this._emitChars(']]');
2855
+ this._emitChars(']]', -1);
2660
2856
  this.state = State.CDATA_SECTION;
2661
2857
  this._stateCdataSection(cp);
2662
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,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.0",
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
  }