parse5 8.0.1 → 8.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,9 +1,12 @@
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
+ // The few attributes a tag usually has are scanned for a duplicate name; from this many on, the names are
8
+ // collected in a Set instead, so that the check doesn't get quadratic in the number of attributes.
9
+ const ATTR_NAME_SET_THRESHOLD = 64;
7
10
  //States
8
11
  var State;
9
12
  (function (State) {
@@ -113,7 +116,7 @@ function toAsciiLower(cp) {
113
116
  return cp + 32;
114
117
  }
115
118
  function isWhitespace(cp) {
116
- return cp === $.SPACE || cp === $.LINE_FEED || cp === $.TABULATION || cp === $.FORM_FEED;
119
+ return (cp === $.SPACE || cp === $.LINE_FEED || cp === $.TABULATION || cp === $.FORM_FEED || cp === $.CARRIAGE_RETURN);
117
120
  }
118
121
  function isScriptDataDoubleEscapeSequenceEnd(cp) {
119
122
  return isWhitespace(cp) || cp === $.SOLIDUS || cp === $.GREATER_THAN_SIGN;
@@ -160,13 +163,31 @@ export class Tokenizer {
160
163
  this.currentCharacterToken = null;
161
164
  this.currentToken = null;
162
165
  this.currentAttr = { name: '', value: '' };
166
+ this.currentAttrLocation = null;
167
+ this.currentAttrNames = null;
168
+ /**
169
+ * `[start, end)` of `preprocessor.html` still to be appended to the attribute value, comment data or character
170
+ * token being built (`_flushRun`): input that is used as is is appended as one slice, not a code point at a time.
171
+ */
172
+ this.runStart = 0;
173
+ this.runEnd = 0;
163
174
  this.preprocessor = new Preprocessor(handler);
164
175
  this.currentLocation = this.getCurrentLocation(-1);
165
- this.entityDecoder = new EntityDecoder(htmlDecodeTree, (cp, consumed) => {
176
+ this.entityDecoder = new HtmlEntityDecoder((cp, consumed) => {
166
177
  // Note: Set `pos` _before_ flushing, as flushing might drop
167
178
  // the current chunk and invalidate `entityStartPos`.
168
179
  this.preprocessor.pos = this.entityStartPos + consumed - 1;
169
- this._flushCodePointConsumedAsCharacterReference(cp);
180
+ if (!this._isCharacterReferenceInAttribute()) {
181
+ this._ensureCharacterToken(isWhitespace(cp)
182
+ ? TokenType.WHITESPACE_CHARACTER
183
+ : cp === $.NULL
184
+ ? TokenType.NULL_CHARACTER
185
+ : TokenType.CHARACTER);
186
+ }
187
+ this._extendRunWith(String.fromCodePoint(cp), 0);
188
+ // This callback runs twice for references that decode to two
189
+ // code points; keep `entityStartPos` valid if the chunk was dropped.
190
+ this.entityStartPos = this.preprocessor.pos - consumed + 1;
170
191
  }, handler.onParseError
171
192
  ? {
172
193
  missingSemicolonAfterCharacterReference: () => {
@@ -216,6 +237,99 @@ export class Tokenizer {
216
237
  this.inLoop = false;
217
238
  }
218
239
  //API
240
+ /** Emit buffered character tokens without completing the current token or emitting EOF. */
241
+ flushCharacters() {
242
+ var _a;
243
+ if (this.inLoop) {
244
+ throw new Error('Cannot flush characters from a tokenizer callback');
245
+ }
246
+ const location = (_a = this.currentCharacterToken) === null || _a === void 0 ? void 0 : _a.location;
247
+ let nextLocation = null;
248
+ if (location) {
249
+ nextLocation = this.getCharacterTokenEndLocation(location);
250
+ this.currentLocation = nextLocation;
251
+ }
252
+ this.inLoop = true;
253
+ try {
254
+ this._emitCurrentCharacterToken(nextLocation);
255
+ }
256
+ finally {
257
+ this.inLoop = false;
258
+ }
259
+ this._runParsingLoop();
260
+ }
261
+ getCharacterTokenEndLocation(location) {
262
+ const endOffset = this.getCharacterTokenEndOffset();
263
+ const html = this.preprocessor.html.slice(location.startOffset - this.preprocessor.droppedBufferSize, endOffset - this.preprocessor.droppedBufferSize);
264
+ let line = location.startLine;
265
+ let lastLineBreak = -1;
266
+ for (let i = html.indexOf('\n'); i !== -1; i = html.indexOf('\n', i + 1)) {
267
+ line++;
268
+ lastLineBreak = i;
269
+ }
270
+ for (let i = html.indexOf('\r'); i !== -1; i = html.indexOf('\r', i + 1)) {
271
+ // Count CRLF as one line break by counting its LF only.
272
+ if (html.charCodeAt(i + 1) !== $.LINE_FEED) {
273
+ line++;
274
+ lastLineBreak = Math.max(lastLineBreak, i);
275
+ }
276
+ }
277
+ return {
278
+ startLine: line,
279
+ startCol: lastLineBreak === -1 ? location.startCol + html.length : html.length - lastLineBreak,
280
+ startOffset: endOffset,
281
+ endLine: -1,
282
+ endCol: -1,
283
+ endOffset: -1,
284
+ };
285
+ }
286
+ // Return the input boundary before any unfinished token or buffered delimiter.
287
+ getCharacterTokenEndOffset() {
288
+ var _a;
289
+ const end = this.preprocessor.droppedBufferSize + this.preprocessor.pos + 1;
290
+ const tokenLocation = (_a = this.currentToken) === null || _a === void 0 ? void 0 : _a.location;
291
+ switch (this.state) {
292
+ case State.TAG_OPEN:
293
+ case State.RCDATA_LESS_THAN_SIGN:
294
+ case State.RAWTEXT_LESS_THAN_SIGN:
295
+ case State.SCRIPT_DATA_LESS_THAN_SIGN:
296
+ case State.SCRIPT_DATA_ESCAPED_LESS_THAN_SIGN:
297
+ case State.CDATA_SECTION_BRACKET: {
298
+ return end - 1;
299
+ }
300
+ case State.END_TAG_OPEN:
301
+ case State.MARKUP_DECLARATION_OPEN:
302
+ case State.RCDATA_END_TAG_OPEN:
303
+ case State.RCDATA_END_TAG_NAME:
304
+ case State.RAWTEXT_END_TAG_OPEN:
305
+ case State.RAWTEXT_END_TAG_NAME:
306
+ case State.SCRIPT_DATA_END_TAG_OPEN:
307
+ case State.SCRIPT_DATA_END_TAG_NAME:
308
+ case State.SCRIPT_DATA_ESCAPED_END_TAG_OPEN:
309
+ case State.SCRIPT_DATA_ESCAPED_END_TAG_NAME:
310
+ case State.CDATA_SECTION_END: {
311
+ return end - 2;
312
+ }
313
+ case State.CHARACTER_REFERENCE: {
314
+ return this._isCharacterReferenceInAttribute() && tokenLocation
315
+ ? tokenLocation.startOffset
316
+ : this.preprocessor.droppedBufferSize + this.entityStartPos;
317
+ }
318
+ case State.DOCTYPE:
319
+ case State.BEFORE_DOCTYPE_NAME: {
320
+ return this.currentLocation.startOffset;
321
+ }
322
+ default: {
323
+ if (tokenLocation &&
324
+ (this.state === State.TAG_NAME ||
325
+ (this.state >= State.BEFORE_ATTRIBUTE_NAME && this.state <= State.BOGUS_COMMENT) ||
326
+ (this.state >= State.COMMENT_START && this.state <= State.BOGUS_DOCTYPE))) {
327
+ return tokenLocation.startOffset;
328
+ }
329
+ return end;
330
+ }
331
+ }
332
+ }
219
333
  pause() {
220
334
  this.paused = true;
221
335
  }
@@ -245,6 +359,33 @@ export class Tokenizer {
245
359
  this.preprocessor.insertHtmlAtCurrentPos(chunk);
246
360
  this._runParsingLoop();
247
361
  }
362
+ /** @internal Flush completed streaming text without splitting an unfinished token. */
363
+ flushCurrentCharacterToken() {
364
+ // Only completed text is flushed: an unfinished token stays intact, including a doctype that has no token yet.
365
+ if (this.paused ||
366
+ this.active ||
367
+ !this.currentCharacterToken ||
368
+ this.currentToken ||
369
+ this.state === State.DOCTYPE ||
370
+ this.state === State.BEFORE_DOCTYPE_NAME ||
371
+ !this.preprocessor.willDropParsedChunk()) {
372
+ return;
373
+ }
374
+ // The last `pending` code units, a delimiter or character reference, are not text yet and stay buffered. They
375
+ // are ASCII, so they hold no preprocessing gap and no line break that the dropped gap history would have to keep.
376
+ const pending = this.preprocessor.droppedBufferSize + this.preprocessor.pos + 1 - this.getCharacterTokenEndOffset();
377
+ // Hibernation left us before the next code point (possibly an incomplete surrogate).
378
+ this.preprocessor.advance();
379
+ this.currentLocation = this.getCurrentLocation(pending);
380
+ this.preprocessor.retreat(1);
381
+ this._emitCurrentCharacterToken(this.currentLocation);
382
+ // Keep a source range for CDATA delimiters or a later tag discarded at EOF. An empty null-character token
383
+ // would become a replacement character in foreign content.
384
+ this._createCharacterToken(TokenType.CHARACTER);
385
+ this.preprocessor.dropParsedChunk(Math.max(pending - 1, 0));
386
+ // Keep `entityStartPos` valid for a pending character reference.
387
+ this.entityStartPos = this.preprocessor.pos + 1 - pending;
388
+ }
248
389
  //Hibernation
249
390
  _ensureHibernation() {
250
391
  if (this.preprocessor.endOfChunkHit) {
@@ -276,6 +417,7 @@ export class Tokenizer {
276
417
  }
277
418
  //Token creation
278
419
  _createStartTagToken() {
420
+ this._flushRun('');
279
421
  this.currentToken = {
280
422
  type: TokenType.START_TAG,
281
423
  tagName: '',
@@ -287,6 +429,7 @@ export class Tokenizer {
287
429
  };
288
430
  }
289
431
  _createEndTagToken() {
432
+ this._flushRun('');
290
433
  this.currentToken = {
291
434
  type: TokenType.END_TAG,
292
435
  tagName: '',
@@ -298,6 +441,7 @@ export class Tokenizer {
298
441
  };
299
442
  }
300
443
  _createCommentToken(offset) {
444
+ this._flushRun('');
301
445
  this.currentToken = {
302
446
  type: TokenType.COMMENT,
303
447
  data: '',
@@ -305,6 +449,7 @@ export class Tokenizer {
305
449
  };
306
450
  }
307
451
  _createDoctypeToken(initialName) {
452
+ this._flushRun('');
308
453
  this.currentToken = {
309
454
  type: TokenType.DOCTYPE,
310
455
  name: initialName,
@@ -314,44 +459,128 @@ export class Tokenizer {
314
459
  location: this.currentLocation,
315
460
  };
316
461
  }
317
- _createCharacterToken(type, chars) {
462
+ _createCharacterToken(type) {
463
+ if (this.currentLocation) {
464
+ const start = this.currentLocation.startOffset - this.preprocessor.droppedBufferSize;
465
+ // A flushed CR already advanced the line; exclude its ignored LF from the next token's range.
466
+ if (this.preprocessor.html[start] === '\n' && this.preprocessor.html[start - 1] === '\r') {
467
+ this.currentLocation.startOffset++;
468
+ }
469
+ }
318
470
  this.currentCharacterToken = {
319
471
  type,
320
- chars,
472
+ chars: '',
321
473
  location: this.currentLocation,
322
474
  };
323
475
  }
324
476
  //Tag attributes
325
477
  _createAttr(attrNameFirstCh) {
478
+ this._flushRun('');
326
479
  this.currentAttr = {
327
480
  name: attrNameFirstCh,
328
481
  value: '',
329
482
  };
330
- this.currentLocation = this.getCurrentLocation(0);
483
+ this.currentAttrLocation = this.getCurrentLocation(0);
331
484
  }
332
485
  _leaveAttrName() {
333
486
  var _a;
334
487
  var _b;
335
488
  const token = this.currentToken;
336
- if (getTokenAttr(token, this.currentAttr.name) === null) {
489
+ const { name } = this.currentAttr;
490
+ const names = this.currentAttrNames;
491
+ let duplicate;
492
+ if (names === null) {
493
+ duplicate = getTokenAttr(token, name) !== null;
494
+ // Build the name set only once a new (65th) name is accepted, so a tag with 64 names and many
495
+ // duplicates does not build it.
496
+ if (!duplicate && token.attrs.length === ATTR_NAME_SET_THRESHOLD) {
497
+ this.currentAttrNames = new Set(token.attrs.map((attr) => attr.name)).add(name);
498
+ }
499
+ }
500
+ else {
501
+ duplicate = names.has(name);
502
+ if (!duplicate)
503
+ names.add(name);
504
+ }
505
+ if (duplicate) {
506
+ this._err(ERR.duplicateAttribute);
507
+ }
508
+ else {
337
509
  token.attrs.push(this.currentAttr);
338
- if (token.location && this.currentLocation) {
510
+ if (token.location && this.currentAttrLocation) {
339
511
  const attrLocations = ((_a = (_b = token.location).attrs) !== null && _a !== void 0 ? _a : (_b.attrs = Object.create(null)));
340
- attrLocations[this.currentAttr.name] = this.currentLocation;
512
+ attrLocations[this.currentAttr.name] = this.currentAttrLocation;
341
513
  // Set end location
342
514
  this._leaveAttrValue();
343
515
  }
344
516
  }
517
+ }
518
+ _leaveAttrValue() {
519
+ if (this.currentAttrLocation) {
520
+ this.currentAttrLocation.endLine = this.preprocessor.line;
521
+ this.currentAttrLocation.endCol = this.preprocessor.col;
522
+ this.currentAttrLocation.endOffset = this.preprocessor.offset;
523
+ }
524
+ }
525
+ /**
526
+ * Append the pending run and `text` to the attribute value, comment data or character token being built.
527
+ * The run is a string of its own: V8 copies a slice of fewer than 13 characters but lets a longer one share
528
+ * the input's buffer, which would keep that buffer alive for as long as any attribute value or text cut from
529
+ * it is; a concatenation is flattened into a buffer of its own when it is sliced.
530
+ */
531
+ _flushRun(text) {
532
+ if (this.runEnd === this.runStart && text.length === 0) {
533
+ return;
534
+ }
535
+ const slice = this.preprocessor.html.slice(this.runStart, this.runEnd);
536
+ const run = (slice.length < 13 ? slice : ` ${slice}`.slice(1)) + text;
537
+ const token = this.currentToken;
538
+ this.runStart = this.runEnd;
539
+ if (token === null) {
540
+ this.currentCharacterToken.chars += run;
541
+ }
542
+ else if (token.type === TokenType.COMMENT) {
543
+ token.data += run;
544
+ }
345
545
  else {
346
- this._err(ERR.duplicateAttribute);
546
+ this.currentAttr.value += run;
347
547
  }
348
548
  }
349
- _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;
549
+ /** Extend the pending run by the current input code point, read as `cp`, or flush it and start a new one. */
550
+ _extendRun(cp) {
551
+ const { preprocessor } = this;
552
+ //NOTE: a CR is read as LF, which is then not in the input.
553
+ if (cp === $.LINE_FEED && preprocessor.html.charCodeAt(preprocessor.pos) !== $.LINE_FEED) {
554
+ this._flushRun('\n');
555
+ this.runStart = preprocessor.pos + 1;
556
+ }
557
+ else {
558
+ const start = preprocessor.pos - Number(cp > 65535);
559
+ if (start !== this.runEnd) {
560
+ this._flushRun('');
561
+ this.runStart = start;
562
+ }
563
+ }
564
+ this.runEnd = preprocessor.pos + 1;
565
+ }
566
+ /**
567
+ * Extend the pending run by `text`, the input that ends `cpOffset` code points after the current one, or flush
568
+ * the run and append `text` where the input differs (a NUL replacement, a decoded reference, a dropped chunk).
569
+ */
570
+ _extendRunWith(text, cpOffset) {
571
+ const { preprocessor } = this;
572
+ const start = preprocessor.codePointStart + 1 + cpOffset - text.length;
573
+ if (start >= 0 && preprocessor.html.startsWith(text, start)) {
574
+ if (start !== this.runEnd) {
575
+ this._flushRun('');
576
+ this.runStart = start;
577
+ }
354
578
  }
579
+ else {
580
+ this._flushRun(text);
581
+ this.runStart = start + text.length;
582
+ }
583
+ this.runEnd = start + text.length;
355
584
  }
356
585
  //Token emission
357
586
  prepareToken(ct) {
@@ -366,6 +595,7 @@ export class Tokenizer {
366
595
  }
367
596
  emitCurrentTagToken() {
368
597
  const ct = this.currentToken;
598
+ this.currentAttrNames = null;
369
599
  this.prepareToken(ct);
370
600
  ct.tagID = getTagID(ct.tagName);
371
601
  if (ct.type === TokenType.START_TAG) {
@@ -394,6 +624,7 @@ export class Tokenizer {
394
624
  this.preprocessor.dropParsedChunk();
395
625
  }
396
626
  _emitCurrentCharacterToken(nextLocation) {
627
+ this._flushRun('');
397
628
  if (this.currentCharacterToken) {
398
629
  //NOTE: if we have a pending character token, make it's end location equal to the
399
630
  //current token's start location.
@@ -420,13 +651,16 @@ export class Tokenizer {
420
651
  }
421
652
  }
422
653
  _emitEOFToken() {
654
+ var _a, _b;
423
655
  const location = this.getCurrentLocation(0);
424
656
  if (location) {
425
657
  location.endLine = location.startLine;
426
658
  location.endCol = location.startCol;
427
659
  location.endOffset = location.startOffset;
428
660
  }
429
- this._emitCurrentCharacterToken(location);
661
+ // A token that is still open at EOF (eof-in-tag) is dropped. Pending
662
+ // characters end where that token started, rather than at EOF.
663
+ this._emitCurrentCharacterToken((_b = (_a = this.currentToken) === null || _a === void 0 ? void 0 : _a.location) !== null && _b !== void 0 ? _b : location);
430
664
  this.handler.onEof({ type: TokenType.EOF, location });
431
665
  this.active = false;
432
666
  }
@@ -439,34 +673,48 @@ export class Tokenizer {
439
673
  //1)TokenType.NULL_CHARACTER - \u0000-character sequences (e.g. '\u0000\u0000\u0000')
440
674
  //2)TokenType.WHITESPACE_CHARACTER - any whitespace/new-line character sequences (e.g. '\n \r\t \f')
441
675
  //3)TokenType.CHARACTER - any character sequence which don't belong to groups 1 and 2 (e.g. 'abcdef1234@@#$%^')
442
- _appendCharToCurrentCharacterToken(type, ch) {
676
+ /** Make the current character token one of `type`, emitting the current one if it has another type. */
677
+ _ensureCharacterToken(type) {
443
678
  if (this.currentCharacterToken) {
444
679
  if (this.currentCharacterToken.type === type) {
445
- this.currentCharacterToken.chars += ch;
446
680
  return;
447
681
  }
448
- else {
449
- this.currentLocation = this.getCurrentLocation(0);
450
- this._emitCurrentCharacterToken(this.currentLocation);
451
- this.preprocessor.dropParsedChunk();
452
- }
682
+ this.currentLocation = this.getCurrentLocation(0);
683
+ this._emitCurrentCharacterToken(this.currentLocation);
684
+ this.preprocessor.dropParsedChunk();
453
685
  }
454
- this._createCharacterToken(type, ch);
686
+ this._createCharacterToken(type);
455
687
  }
688
+ /** Emit the current input code point, read as `cp`. */
456
689
  _emitCodePoint(cp) {
457
690
  const type = isWhitespace(cp)
458
691
  ? TokenType.WHITESPACE_CHARACTER
459
692
  : cp === $.NULL
460
693
  ? TokenType.NULL_CHARACTER
461
694
  : 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));
695
+ const token = this.currentCharacterToken;
696
+ if (token === null || token.type !== type) {
697
+ this._ensureCharacterToken(type);
698
+ }
699
+ this._extendRun(cp);
465
700
  }
466
701
  //NOTE: used when we emit characters explicitly.
467
702
  //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);
703
+ // Negative `cpOffset` values locate text emitted before the current input character.
704
+ _emitChars(ch, cpOffset = 0) {
705
+ if (this.options.sourceCodeLocationInfo &&
706
+ cpOffset !== 0 &&
707
+ this.currentCharacterToken &&
708
+ this.currentCharacterToken.type !== TokenType.CHARACTER) {
709
+ this.currentLocation = this.getCurrentLocation(ch.length - 1 - cpOffset);
710
+ this._emitCurrentCharacterToken(this.currentLocation);
711
+ // Keep source characters that precede the lookahead available for a subsequent flush.
712
+ this._createCharacterToken(TokenType.CHARACTER);
713
+ }
714
+ else {
715
+ this._ensureCharacterToken(TokenType.CHARACTER);
716
+ }
717
+ this._extendRunWith(ch, cpOffset);
470
718
  }
471
719
  // Character reference helpers
472
720
  _startCharacterReference() {
@@ -482,7 +730,7 @@ export class Tokenizer {
482
730
  }
483
731
  _flushCodePointConsumedAsCharacterReference(cp) {
484
732
  if (this._isCharacterReferenceInAttribute()) {
485
- this.currentAttr.value += String.fromCodePoint(cp);
733
+ this._extendRun(cp);
486
734
  }
487
735
  else {
488
736
  this._emitCodePoint(cp);
@@ -930,13 +1178,13 @@ export class Tokenizer {
930
1178
  }
931
1179
  case $.EOF: {
932
1180
  this._err(ERR.eofBeforeTagName);
933
- this._emitChars('<');
1181
+ this._emitChars('<', -1);
934
1182
  this._emitEOFToken();
935
1183
  break;
936
1184
  }
937
1185
  default: {
938
1186
  this._err(ERR.invalidFirstCharacterOfTagName);
939
- this._emitChars('<');
1187
+ this._emitChars('<', -1);
940
1188
  this.state = State.DATA;
941
1189
  this._stateData(cp);
942
1190
  }
@@ -959,7 +1207,7 @@ export class Tokenizer {
959
1207
  }
960
1208
  case $.EOF: {
961
1209
  this._err(ERR.eofBeforeTagName);
962
- this._emitChars('</');
1210
+ this._emitChars('</', -1);
963
1211
  this._emitEOFToken();
964
1212
  break;
965
1213
  }
@@ -1014,7 +1262,7 @@ export class Tokenizer {
1014
1262
  this.state = State.RCDATA_END_TAG_OPEN;
1015
1263
  }
1016
1264
  else {
1017
- this._emitChars('<');
1265
+ this._emitChars('<', -1);
1018
1266
  this.state = State.RCDATA;
1019
1267
  this._stateRcdata(cp);
1020
1268
  }
@@ -1027,7 +1275,7 @@ export class Tokenizer {
1027
1275
  this._stateRcdataEndTagName(cp);
1028
1276
  }
1029
1277
  else {
1030
- this._emitChars('</');
1278
+ this._emitChars('</', -1);
1031
1279
  this.state = State.RCDATA;
1032
1280
  this._stateRcdata(cp);
1033
1281
  }
@@ -1061,6 +1309,8 @@ export class Tokenizer {
1061
1309
  return false;
1062
1310
  }
1063
1311
  default: {
1312
+ //NOTE: the end tag is text after all, so no token is being built.
1313
+ this.currentToken = null;
1064
1314
  return !this._ensureHibernation();
1065
1315
  }
1066
1316
  }
@@ -1069,7 +1319,7 @@ export class Tokenizer {
1069
1319
  //------------------------------------------------------------------
1070
1320
  _stateRcdataEndTagName(cp) {
1071
1321
  if (this.handleSpecialEndTag(cp)) {
1072
- this._emitChars('</');
1322
+ this._emitChars('</', -1);
1073
1323
  this.state = State.RCDATA;
1074
1324
  this._stateRcdata(cp);
1075
1325
  }
@@ -1081,7 +1331,7 @@ export class Tokenizer {
1081
1331
  this.state = State.RAWTEXT_END_TAG_OPEN;
1082
1332
  }
1083
1333
  else {
1084
- this._emitChars('<');
1334
+ this._emitChars('<', -1);
1085
1335
  this.state = State.RAWTEXT;
1086
1336
  this._stateRawtext(cp);
1087
1337
  }
@@ -1094,7 +1344,7 @@ export class Tokenizer {
1094
1344
  this._stateRawtextEndTagName(cp);
1095
1345
  }
1096
1346
  else {
1097
- this._emitChars('</');
1347
+ this._emitChars('</', -1);
1098
1348
  this.state = State.RAWTEXT;
1099
1349
  this._stateRawtext(cp);
1100
1350
  }
@@ -1103,7 +1353,7 @@ export class Tokenizer {
1103
1353
  //------------------------------------------------------------------
1104
1354
  _stateRawtextEndTagName(cp) {
1105
1355
  if (this.handleSpecialEndTag(cp)) {
1106
- this._emitChars('</');
1356
+ this._emitChars('</', -1);
1107
1357
  this.state = State.RAWTEXT;
1108
1358
  this._stateRawtext(cp);
1109
1359
  }
@@ -1122,7 +1372,7 @@ export class Tokenizer {
1122
1372
  break;
1123
1373
  }
1124
1374
  default: {
1125
- this._emitChars('<');
1375
+ this._emitChars('<', -1);
1126
1376
  this.state = State.SCRIPT_DATA;
1127
1377
  this._stateScriptData(cp);
1128
1378
  }
@@ -1136,7 +1386,7 @@ export class Tokenizer {
1136
1386
  this._stateScriptDataEndTagName(cp);
1137
1387
  }
1138
1388
  else {
1139
- this._emitChars('</');
1389
+ this._emitChars('</', -1);
1140
1390
  this.state = State.SCRIPT_DATA;
1141
1391
  this._stateScriptData(cp);
1142
1392
  }
@@ -1145,7 +1395,7 @@ export class Tokenizer {
1145
1395
  //------------------------------------------------------------------
1146
1396
  _stateScriptDataEndTagName(cp) {
1147
1397
  if (this.handleSpecialEndTag(cp)) {
1148
- this._emitChars('</');
1398
+ this._emitChars('</', -1);
1149
1399
  this.state = State.SCRIPT_DATA;
1150
1400
  this._stateScriptData(cp);
1151
1401
  }
@@ -1273,12 +1523,12 @@ export class Tokenizer {
1273
1523
  this.state = State.SCRIPT_DATA_ESCAPED_END_TAG_OPEN;
1274
1524
  }
1275
1525
  else if (isAsciiLetter(cp)) {
1276
- this._emitChars('<');
1526
+ this._emitChars('<', -1);
1277
1527
  this.state = State.SCRIPT_DATA_DOUBLE_ESCAPE_START;
1278
1528
  this._stateScriptDataDoubleEscapeStart(cp);
1279
1529
  }
1280
1530
  else {
1281
- this._emitChars('<');
1531
+ this._emitChars('<', -1);
1282
1532
  this.state = State.SCRIPT_DATA_ESCAPED;
1283
1533
  this._stateScriptDataEscaped(cp);
1284
1534
  }
@@ -1291,7 +1541,7 @@ export class Tokenizer {
1291
1541
  this._stateScriptDataEscapedEndTagName(cp);
1292
1542
  }
1293
1543
  else {
1294
- this._emitChars('</');
1544
+ this._emitChars('</', -1);
1295
1545
  this.state = State.SCRIPT_DATA_ESCAPED;
1296
1546
  this._stateScriptDataEscaped(cp);
1297
1547
  }
@@ -1300,7 +1550,7 @@ export class Tokenizer {
1300
1550
  //------------------------------------------------------------------
1301
1551
  _stateScriptDataEscapedEndTagName(cp) {
1302
1552
  if (this.handleSpecialEndTag(cp)) {
1303
- this._emitChars('</');
1553
+ this._emitChars('</', -1);
1304
1554
  this.state = State.SCRIPT_DATA_ESCAPED;
1305
1555
  this._stateScriptDataEscaped(cp);
1306
1556
  }
@@ -1594,7 +1844,7 @@ export class Tokenizer {
1594
1844
  }
1595
1845
  case $.NULL: {
1596
1846
  this._err(ERR.unexpectedNullCharacter);
1597
- this.currentAttr.value += REPLACEMENT_CHARACTER;
1847
+ this._extendRunWith(REPLACEMENT_CHARACTER, 0);
1598
1848
  break;
1599
1849
  }
1600
1850
  case $.EOF: {
@@ -1603,7 +1853,7 @@ export class Tokenizer {
1603
1853
  break;
1604
1854
  }
1605
1855
  default: {
1606
- this.currentAttr.value += String.fromCodePoint(cp);
1856
+ this._extendRun(cp);
1607
1857
  }
1608
1858
  }
1609
1859
  }
@@ -1621,7 +1871,7 @@ export class Tokenizer {
1621
1871
  }
1622
1872
  case $.NULL: {
1623
1873
  this._err(ERR.unexpectedNullCharacter);
1624
- this.currentAttr.value += REPLACEMENT_CHARACTER;
1874
+ this._extendRunWith(REPLACEMENT_CHARACTER, 0);
1625
1875
  break;
1626
1876
  }
1627
1877
  case $.EOF: {
@@ -1630,7 +1880,7 @@ export class Tokenizer {
1630
1880
  break;
1631
1881
  }
1632
1882
  default: {
1633
- this.currentAttr.value += String.fromCodePoint(cp);
1883
+ this._extendRun(cp);
1634
1884
  }
1635
1885
  }
1636
1886
  }
@@ -1658,7 +1908,7 @@ export class Tokenizer {
1658
1908
  }
1659
1909
  case $.NULL: {
1660
1910
  this._err(ERR.unexpectedNullCharacter);
1661
- this.currentAttr.value += REPLACEMENT_CHARACTER;
1911
+ this._extendRunWith(REPLACEMENT_CHARACTER, 0);
1662
1912
  break;
1663
1913
  }
1664
1914
  case $.QUOTATION_MARK:
@@ -1667,7 +1917,7 @@ export class Tokenizer {
1667
1917
  case $.EQUALS_SIGN:
1668
1918
  case $.GRAVE_ACCENT: {
1669
1919
  this._err(ERR.unexpectedCharacterInUnquotedAttributeValue);
1670
- this.currentAttr.value += String.fromCodePoint(cp);
1920
+ this._extendRun(cp);
1671
1921
  break;
1672
1922
  }
1673
1923
  case $.EOF: {
@@ -1676,7 +1926,7 @@ export class Tokenizer {
1676
1926
  break;
1677
1927
  }
1678
1928
  default: {
1679
- this.currentAttr.value += String.fromCodePoint(cp);
1929
+ this._extendRun(cp);
1680
1930
  }
1681
1931
  }
1682
1932
  }
@@ -1710,6 +1960,7 @@ export class Tokenizer {
1710
1960
  }
1711
1961
  default: {
1712
1962
  this._err(ERR.missingWhitespaceBetweenAttributes);
1963
+ this._leaveAttrValue();
1713
1964
  this.state = State.BEFORE_ATTRIBUTE_NAME;
1714
1965
  this._stateBeforeAttributeName(cp);
1715
1966
  }
@@ -1755,11 +2006,11 @@ export class Tokenizer {
1755
2006
  }
1756
2007
  case $.NULL: {
1757
2008
  this._err(ERR.unexpectedNullCharacter);
1758
- token.data += REPLACEMENT_CHARACTER;
2009
+ this._extendRunWith(REPLACEMENT_CHARACTER, 0);
1759
2010
  break;
1760
2011
  }
1761
2012
  default: {
1762
- token.data += String.fromCodePoint(cp);
2013
+ this._extendRun(cp);
1763
2014
  }
1764
2015
  }
1765
2016
  }
@@ -1838,7 +2089,7 @@ export class Tokenizer {
1838
2089
  break;
1839
2090
  }
1840
2091
  default: {
1841
- token.data += '-';
2092
+ this._extendRunWith('-', -1);
1842
2093
  this.state = State.COMMENT;
1843
2094
  this._stateComment(cp);
1844
2095
  }
@@ -1854,13 +2105,13 @@ export class Tokenizer {
1854
2105
  break;
1855
2106
  }
1856
2107
  case $.LESS_THAN_SIGN: {
1857
- token.data += '<';
2108
+ this._extendRunWith('<', 0);
1858
2109
  this.state = State.COMMENT_LESS_THAN_SIGN;
1859
2110
  break;
1860
2111
  }
1861
2112
  case $.NULL: {
1862
2113
  this._err(ERR.unexpectedNullCharacter);
1863
- token.data += REPLACEMENT_CHARACTER;
2114
+ this._extendRunWith(REPLACEMENT_CHARACTER, 0);
1864
2115
  break;
1865
2116
  }
1866
2117
  case $.EOF: {
@@ -1870,22 +2121,21 @@ export class Tokenizer {
1870
2121
  break;
1871
2122
  }
1872
2123
  default: {
1873
- token.data += String.fromCodePoint(cp);
2124
+ this._extendRun(cp);
1874
2125
  }
1875
2126
  }
1876
2127
  }
1877
2128
  // Comment less-than sign state
1878
2129
  //------------------------------------------------------------------
1879
2130
  _stateCommentLessThanSign(cp) {
1880
- const token = this.currentToken;
1881
2131
  switch (cp) {
1882
2132
  case $.EXCLAMATION_MARK: {
1883
- token.data += '!';
2133
+ this._extendRunWith('!', 0);
1884
2134
  this.state = State.COMMENT_LESS_THAN_SIGN_BANG;
1885
2135
  break;
1886
2136
  }
1887
2137
  case $.LESS_THAN_SIGN: {
1888
- token.data += '<';
2138
+ this._extendRunWith('<', 0);
1889
2139
  break;
1890
2140
  }
1891
2141
  default: {
@@ -1941,7 +2191,7 @@ export class Tokenizer {
1941
2191
  break;
1942
2192
  }
1943
2193
  default: {
1944
- token.data += '-';
2194
+ this._extendRunWith('-', -1);
1945
2195
  this.state = State.COMMENT;
1946
2196
  this._stateComment(cp);
1947
2197
  }
@@ -1962,7 +2212,7 @@ export class Tokenizer {
1962
2212
  break;
1963
2213
  }
1964
2214
  case $.HYPHEN_MINUS: {
1965
- token.data += '-';
2215
+ this._extendRunWith('-', -2);
1966
2216
  break;
1967
2217
  }
1968
2218
  case $.EOF: {
@@ -1972,7 +2222,7 @@ export class Tokenizer {
1972
2222
  break;
1973
2223
  }
1974
2224
  default: {
1975
- token.data += '--';
2225
+ this._extendRunWith('--', -1);
1976
2226
  this.state = State.COMMENT;
1977
2227
  this._stateComment(cp);
1978
2228
  }
@@ -1984,7 +2234,7 @@ export class Tokenizer {
1984
2234
  const token = this.currentToken;
1985
2235
  switch (cp) {
1986
2236
  case $.HYPHEN_MINUS: {
1987
- token.data += '--!';
2237
+ this._extendRunWith('--!', -1);
1988
2238
  this.state = State.COMMENT_END_DASH;
1989
2239
  break;
1990
2240
  }
@@ -2001,7 +2251,7 @@ export class Tokenizer {
2001
2251
  break;
2002
2252
  }
2003
2253
  default: {
2004
- token.data += '--!';
2254
+ this._extendRunWith('--!', -1);
2005
2255
  this.state = State.COMMENT;
2006
2256
  this._stateComment(cp);
2007
2257
  }
@@ -2638,7 +2888,7 @@ export class Tokenizer {
2638
2888
  this.state = State.CDATA_SECTION_END;
2639
2889
  }
2640
2890
  else {
2641
- this._emitChars(']');
2891
+ this._emitChars(']', -1);
2642
2892
  this.state = State.CDATA_SECTION;
2643
2893
  this._stateCdataSection(cp);
2644
2894
  }
@@ -2652,11 +2902,11 @@ export class Tokenizer {
2652
2902
  break;
2653
2903
  }
2654
2904
  case $.RIGHT_SQUARE_BRACKET: {
2655
- this._emitChars(']');
2905
+ this._emitChars(']', -2);
2656
2906
  break;
2657
2907
  }
2658
2908
  default: {
2659
- this._emitChars(']]');
2909
+ this._emitChars(']]', -1);
2660
2910
  this.state = State.CDATA_SECTION;
2661
2911
  this._stateCdataSection(cp);
2662
2912
  }