@cjser/parse5 8.0.1-cjser.2 → 8.1.0-cjser.2

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,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 '@cjser/entities/decode';
4
+ import { HtmlEntityDecoder, DecodingMode } from '@cjser/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
  }