webpack 5.111.0 → 5.111.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.
Files changed (144) hide show
  1. package/lib/APIPlugin.js +3 -4
  2. package/lib/CompatibilityPlugin.js +3 -4
  3. package/lib/Compilation.js +16 -35
  4. package/lib/Compiler.js +4 -5
  5. package/lib/ConstPlugin.js +16 -132
  6. package/lib/DependencyTemplate.js +2 -1
  7. package/lib/DotenvPlugin.js +24 -28
  8. package/lib/ErrorHelpers.js +113 -0
  9. package/lib/ExportsInfo.js +20 -16
  10. package/lib/ExternalModule.js +6 -8
  11. package/lib/FlagDependencyExportsPlugin.js +8 -13
  12. package/lib/FlagDependencyUsagePlugin.js +9 -13
  13. package/lib/Generator.js +24 -2
  14. package/lib/HotModuleReplacementPlugin.js +2 -0
  15. package/lib/IgnorePlugin.js +1 -1
  16. package/lib/LoaderOptionsPlugin.js +1 -1
  17. package/lib/Module.js +3 -3
  18. package/lib/ModuleGraph.js +6 -9
  19. package/lib/MultiCompiler.js +6 -16
  20. package/lib/NormalModule.js +96 -44
  21. package/lib/RuntimeTemplate.js +33 -43
  22. package/lib/SourceMapDevToolPlugin.js +53 -103
  23. package/lib/TemplatedPathPlugin.js +16 -59
  24. package/lib/asset/AssetBytesGenerator.js +7 -1
  25. package/lib/asset/AssetGenerator.js +25 -16
  26. package/lib/asset/AssetSourceGenerator.js +7 -1
  27. package/lib/buildChunkGraph.js +11 -15
  28. package/lib/cache/MemoryCachePlugin.js +4 -4
  29. package/lib/cache/MemoryWithGcCachePlugin.js +7 -8
  30. package/lib/cache/PackFileCacheStrategy.js +3 -18
  31. package/lib/cache/getLazyHashedEtag.js +3 -5
  32. package/lib/config/browserslistTargetHandler.js +2 -8
  33. package/lib/config/defaults.js +52 -78
  34. package/lib/config/getClaimedAssetTypes.js +10 -11
  35. package/lib/config/target.js +3 -4
  36. package/lib/{ContextExclusionPlugin.js → context/ContextExclusionPlugin.js} +1 -1
  37. package/lib/{ContextModule.js → context/ContextModule.js} +37 -34
  38. package/lib/{ContextModuleFactory.js → context/ContextModuleFactory.js} +19 -21
  39. package/lib/{ContextReplacementPlugin.js → context/ContextReplacementPlugin.js} +4 -4
  40. package/lib/css/CssGenerator.js +19 -11
  41. package/lib/css/CssParser.js +18 -20
  42. package/lib/css/cssMinify.js +32 -29
  43. package/lib/css/data.js +797 -288
  44. package/lib/css/syntax.js +2250 -934
  45. package/lib/debug/ProfilingPlugin.js +3 -4
  46. package/lib/dependencies/CommonJsExportRequireDependency.js +10 -11
  47. package/lib/dependencies/CommonJsImportsParserPlugin.js +4 -5
  48. package/lib/dependencies/CommonJsSelfReferenceDependency.js +2 -4
  49. package/lib/dependencies/ContextDependency.js +1 -1
  50. package/lib/dependencies/ContextDependencyHelpers.js +3 -9
  51. package/lib/dependencies/ContextElementDependency.js +1 -1
  52. package/lib/dependencies/CssIcssExportDependency.js +6 -8
  53. package/lib/dependencies/CssIcssImportDependency.js +3 -6
  54. package/lib/dependencies/CssUrlDependency.js +3 -5
  55. package/lib/dependencies/HarmonyCompatibilityDependency.js +11 -20
  56. package/lib/dependencies/HarmonyDetectionParserPlugin.js +3 -4
  57. package/lib/dependencies/HarmonyExportDependencyParserPlugin.js +2 -4
  58. package/lib/dependencies/HarmonyExportExpressionDependency.js +3 -4
  59. package/lib/dependencies/HarmonyExportImportedSpecifierDependency.js +3 -4
  60. package/lib/dependencies/HarmonyExportInitFragment.js +26 -19
  61. package/lib/dependencies/HarmonyExportSpecifierDependency.js +7 -28
  62. package/lib/dependencies/HarmonyImportDependencyParserPlugin.js +103 -16
  63. package/lib/dependencies/HarmonyImportSideEffectDependency.js +3 -4
  64. package/lib/dependencies/HarmonyImportSpecifierDependency.js +16 -2
  65. package/lib/dependencies/HtmlEntryDependency.js +74 -102
  66. package/lib/dependencies/HtmlInlineScriptDependency.js +3 -4
  67. package/lib/dependencies/HtmlInlineStyleDependency.js +3 -4
  68. package/lib/dependencies/ImportMetaContextDependencyParserPlugin.js +1 -1
  69. package/lib/dependencies/ImportMetaGlobHelpers.js +1 -1
  70. package/lib/dependencies/ImportMetaPlugin.js +8 -10
  71. package/lib/dependencies/ImportParserPlugin.js +1 -1
  72. package/lib/dependencies/LoaderPlugin.js +26 -1
  73. package/lib/dependencies/RequireContextDependencyParserPlugin.js +1 -1
  74. package/lib/dependencies/URLContextDependency.js +1 -1
  75. package/lib/dependencies/URLDependency.js +6 -5
  76. package/lib/dependencies/WorkerAndWorkletPlugin.js +15 -21
  77. package/lib/dependencies/WorkerDependency.js +6 -5
  78. package/lib/dependencies/WorkletDependency.js +6 -9
  79. package/lib/errors/HookWebpackError.js +3 -2
  80. package/lib/hmr/LazyCompilationPlugin.js +3 -5
  81. package/lib/html/HtmlGenerator.js +61 -82
  82. package/lib/html/HtmlModulesPlugin.js +88 -139
  83. package/lib/html/HtmlParser.js +97 -141
  84. package/lib/html/data.js +21 -0
  85. package/lib/html/htmlMinify.js +10 -11
  86. package/lib/html/syntax.js +883 -993
  87. package/lib/index.js +46 -3
  88. package/lib/javascript/ChunkFormatHelpers.js +4 -3
  89. package/lib/javascript/JavascriptGenerator.js +8 -1
  90. package/lib/javascript/JavascriptModulesPlugin.js +21 -48
  91. package/lib/javascript/JavascriptParser.js +721 -178
  92. package/lib/javascript/parser.js +6 -0
  93. package/lib/javascript/syntax.js +420 -92
  94. package/lib/json/JsonGenerator.js +7 -1
  95. package/lib/library/ModuleLibraryPlugin.js +64 -136
  96. package/lib/loaders/LoaderRunner.js +137 -9
  97. package/lib/node/NodeWatchFileSystem.js +3 -9
  98. package/lib/optimize/AggressiveMergingPlugin.js +3 -5
  99. package/lib/optimize/ConcatenatedModule.js +81 -100
  100. package/lib/optimize/FlagIncludedChunksPlugin.js +3 -11
  101. package/lib/optimize/InnerGraphPlugin.js +7 -13
  102. package/lib/optimize/LimitChunkCountPlugin.js +5 -8
  103. package/lib/optimize/MangleExportsPlugin.js +3 -4
  104. package/lib/optimize/ModuleConcatenationPlugin.js +3 -4
  105. package/lib/optimize/SideEffectsFlagPlugin.js +7 -19
  106. package/lib/optimize/SplitChunksPlugin.js +2 -4
  107. package/lib/performance/BroadContextsPlugin.js +1 -1
  108. package/lib/performance/CacheEffectivenessPlugin.js +3 -4
  109. package/lib/performance/SizeLimitsPlugin.js +2 -4
  110. package/lib/prefetch/ChunkPrefetchPreloadPlugin.js +6 -8
  111. package/lib/prefetch/ResourceHintPlugin.js +17 -21
  112. package/lib/prefetch/StartupAssetHintRuntimeModule.js +7 -10
  113. package/lib/runtime/AsyncModuleRuntimeModule.js +3 -5
  114. package/lib/runtime/GetChunkFilenameRuntimeModule.js +3 -4
  115. package/lib/runtime/GlobalRuntimeModule.js +2 -4
  116. package/lib/runtime/MakeDeferredNamespaceObjectRuntime.js +55 -85
  117. package/lib/schemes/DataUriPlugin.js +3 -6
  118. package/lib/schemes/VirtualUrlPlugin.js +1 -1
  119. package/lib/serialization/BinaryMiddleware.js +9 -9
  120. package/lib/serialization/FileMiddleware.js +16 -16
  121. package/lib/serialization/ObjectMiddleware.js +30 -33
  122. package/lib/stats/DefaultStatsFactoryPlugin.js +3 -4
  123. package/lib/typescript/TypeScriptPlugin.js +2 -3
  124. package/lib/url/URLParserPlugin.js +6 -8
  125. package/lib/util/LocConverter.js +4 -7
  126. package/lib/util/SourceProcessor.js +15 -22
  127. package/lib/util/chainedImports.js +3 -4
  128. package/lib/util/concatenate.js +4 -4
  129. package/lib/util/createHooksRegistry.js +1 -2
  130. package/lib/util/dataURL.js +2 -3
  131. package/lib/util/deterministicGrouping.js +8 -13
  132. package/lib/util/findGraphRoots.js +6 -13
  133. package/lib/util/globUtils.js +1 -1
  134. package/lib/util/hash/wasm-hash.js +3 -4
  135. package/lib/util/internalSerializables.js +1 -1
  136. package/lib/util/semver.js +5 -3
  137. package/lib/wasm-async/AsyncWebAssemblyGenerator.js +6 -1
  138. package/lib/wasm-async/AsyncWebAssemblyJavascriptGenerator.js +5 -1
  139. package/lib/wasm-sync/WasmChunkLoadingRuntimeModule.js +3 -4
  140. package/lib/wasm-sync/WebAssemblyGenerator.js +6 -1
  141. package/lib/wasm-sync/WebAssemblyJavascriptGenerator.js +5 -1
  142. package/lib/wasm-sync/WebAssemblyUtils.js +2 -4
  143. package/package.json +30 -23
  144. package/types.d.ts +299 -280
@@ -114,6 +114,7 @@ const {
114
114
  IMPLIED,
115
115
  IMPLIED_THOROUGH,
116
116
  INTEGER_ATTRIBUTES,
117
+ INVALID_VALUE_DEFAULT_DIFFERS,
117
118
  IN_HEAD_NOSCRIPT_PASSTHROUGH,
118
119
  IN_TABLE_IGNORED_ENDS,
119
120
  JAVASCRIPT_SCRIPT_TYPES,
@@ -314,18 +315,14 @@ const QUOTE_DOUBLE = 1;
314
315
  const QUOTE_SINGLE = 2;
315
316
  const QUOTE_NONE = 0;
316
317
 
317
- // Longest WHATWG named entity name *including* the trailing `;` is 32 chars
318
- // (`CounterClockwiseContourIntegral;`); without the trailing `;` it's 31.
319
- // Used to cap both the tokenizer's named-character-reference run length and
320
- // the decoder's longest-prefix backtrack so pathological inputs (e.g. `&`
321
- // followed by thousands of alphanumerics) stay linear-time.
318
+ // The longest WHATWG named entity is 32 chars with its `;`, 31 without. Capping
319
+ // the tokenizer's reference run and the decoder's longest-prefix backtrack there
320
+ // keeps `&` followed by thousands of alphanumerics linear-time.
322
321
  const MAX_ENTITY_NAME_LEN = 32;
323
322
 
324
- // ASCII character-class bit flags packed into one lookup table. The tokenizer
325
- // runs these predicates per code point (tag names, attribute names, character
326
- // references, whitespace skipping), so a single table load + mask replaces the
327
- // per-call comparison chains. Code points >= 0x80 are never in any of these
328
- // classes, so callers short-circuit on `cc < 0x80` before indexing.
323
+ // ASCII character-class bit flags in one table: a load and a mask in place of
324
+ // the comparison chain these per-code-point predicates would otherwise run. No
325
+ // code point >= 0x80 is in any class, so callers short-circuit on `cc < 0x80`.
329
326
  const CCLS_SPACE = 1;
330
327
  const CCLS_DIGIT = 2;
331
328
  const CCLS_UPPER = 4;
@@ -581,10 +578,9 @@ const tokenize = (input, pos = 0, callbacks = {}) => {
581
578
  let attributeValueStart = -1;
582
579
  let attrQuoteType = QUOTE_NONE;
583
580
  let commentStart = pos;
584
- // Data range of the comment being scanned. The comment states append
585
- // characters that are not the ones just consumed (the `--` a `-->` inside a
586
- // comment turns into, the `--!` of a comment-end-bang), so the range is
587
- // tracked here rather than re-derived by trimming delimiters off the token.
581
+ // Data range of the comment being scanned, tracked rather than re-derived by
582
+ // trimming delimiters: the comment states append characters that were never
583
+ // consumed, such as the `--` an inner `-->` turns into.
588
584
  let commentDataStart = pos;
589
585
  let commentDataEnd = pos;
590
586
  // Sub-ranges of the DOCTYPE token being scanned (-1 = the spec's
@@ -601,10 +597,9 @@ const tokenize = (input, pos = 0, callbacks = {}) => {
601
597
  // derived from these lazily (only for special-content tags).
602
598
  let lastOpenTagStart = -1;
603
599
  let lastOpenTagEnd = -1;
604
- // Counter used by SCRIPT_DATA_DOUBLE_ESCAPE_{START,END} to detect whether
605
- // the ASCII-alpha run after `<` / `</` spells exactly `"script"`. Values
606
- // 0..6 = number of chars matched so far; 7 = no longer matches (sentinel).
607
- // Avoids growing a buffer for pathological inputs with long alpha runs.
600
+ // How much of `"script"` the ASCII-alpha run after `<` / `</` has matched,
601
+ // for SCRIPT_DATA_DOUBLE_ESCAPE_{START,END}: 0..6 chars, or 7 for no longer
602
+ // matching. A counter rather than a buffer, which a long run would grow.
608
603
  let scriptMatch = 0;
609
604
  let namedEntityConsumed = 0;
610
605
  // Offset of the opening `&` and the running numeric value (clamped past the
@@ -614,10 +609,9 @@ const tokenize = (input, pos = 0, callbacks = {}) => {
614
609
  // Tracks whether the current tag has parsed any attributes — used to
615
610
  // fire the `end-tag-with-attributes` parse error when an end tag emits.
616
611
  let tagHasAttributes = false;
617
- // Memoized next occurrence of `<` / `&` / NUL for the text-run
618
- // fast-forwards: native `indexOf` beats a per-char JS loop, and `pos` only
619
- // moves forward, so each memo is refreshed at most once per occurrence
620
- // (`len` = no further occurrence).
612
+ // Memoized next `<` / `&` / NUL for the text-run fast-forwards, where native
613
+ // `indexOf` beats a per-char loop. `pos` only moves forward, so each memo is
614
+ // refreshed at most once per occurrence; `len` means no further one.
621
615
  let nextLt = -1;
622
616
  let nextAmp = -1;
623
617
  let nextNul = -1;
@@ -688,12 +682,9 @@ const tokenize = (input, pos = 0, callbacks = {}) => {
688
682
  }
689
683
  };
690
684
 
691
- // Content mode for the tag just opened (name at `lastOpenTagStart..End`). In
692
- // foreign content (SVG/MathML) the tree builder vetoes RAWTEXT/RCDATA/script
693
- // switching via `isForeign`, so e.g. an SVG `<title>`/`<style>` is parsed as
694
- // normal markup. `lastOpenTagName` (the lowercased name compared by the
695
- // special end-tag states) is materialized only when a special mode is
696
- // actually entered — ordinary tags never allocate it.
685
+ // Content mode for the tag just opened, named at `lastOpenTagStart..End`. In
686
+ // foreign content `isForeign` vetoes the RAWTEXT/RCDATA/script switch, so an
687
+ // SVG `<title>` is markup; `lastOpenTagName` is materialized only there.
697
688
  const contentModeAfterOpenTag = () => {
698
689
  const m = getContentModeForRange(input, lastOpenTagStart, lastOpenTagEnd);
699
690
  // Ordinary tags stay in the data state whatever the tree builder did, so
@@ -731,11 +722,9 @@ const tokenize = (input, pos = 0, callbacks = {}) => {
731
722
  if (callbacks.text !== undefined) {
732
723
  callbacks.text(input, textStart, endPos);
733
724
  }
734
- // Advance `textStart` so a second `flushText` for the same span
735
- // (e.g. from the EOF handler after a tag-open transition already
736
- // flushed the pending text) is a no-op rather than a duplicate
737
- // emit. emitOpenTag / emitCloseTag overwrite `textStart` with
738
- // their own `nextPos` anyway, so this doesn't shift their start.
725
+ // Advancing `textStart` makes a second `flushText` over the same span a
726
+ // no-op instead of a duplicate emit. `emitOpenTag` / `emitCloseTag`
727
+ // overwrite it with their own `nextPos`, so their start is unaffected.
739
728
  textStart = endPos;
740
729
  }
741
730
  };
@@ -745,11 +734,9 @@ const tokenize = (input, pos = 0, callbacks = {}) => {
745
734
  * @returns {number} next position
746
735
  */
747
736
  const emitAttribute = (endPos) => {
748
- // Default `nextPos` advances past the closing quote (if any) so the
749
- // state machine can continue when no `attribute` callback is provided.
750
- // When a callback IS provided, its return value overrides the default —
751
- // the callback is expected to do the same advance based on the
752
- // reported `quoteType`.
737
+ // The default `nextPos` advances past the closing quote, if any, so the
738
+ // machine continues without an `attribute` callback. A callback's return
739
+ // value overrides it, and owes the same advance from its `quoteType`.
753
740
  let nextPos = attrQuoteType === QUOTE_NONE ? endPos : endPos + 1;
754
741
  if (callbacks.attribute !== undefined && attributeNameStart !== -1) {
755
742
  nextPos = callbacks.attribute(
@@ -822,10 +809,9 @@ const tokenize = (input, pos = 0, callbacks = {}) => {
822
809
  while (pos < len) {
823
810
  const cc = input.charCodeAt(pos);
824
811
 
825
- // All WHATWG tokenizer states handled. Deliberately omitted parse errors
826
- // (need state this offset scanner lacks): duplicate-attribute,
827
- // cdata-in-html-content, `*-in-input-stream`. Reference substitution is
828
- // left to `decodeEntities`.
812
+ // Every WHATWG tokenizer state is handled. Omitted: the parse errors
813
+ // needing state an offset scanner lacks (duplicate-attribute,
814
+ // cdata-in-html-content, `*-in-input-stream`) and reference substitution.
829
815
  switch (state) {
830
816
  // https://html.spec.whatwg.org/multipage/parsing.html#data-state
831
817
  case STATE_DATA:
@@ -849,10 +835,9 @@ const tokenize = (input, pos = 0, callbacks = {}) => {
849
835
  reportError("unexpected-null-character", pos, pos + 1, "warning");
850
836
  pos++;
851
837
  } else {
852
- // Fast-forward over the run of ordinary text without re-entering
853
- // the per-state switch — batches only the data state's "Anything
854
- // else — emit the current input character" arc; the stops are
855
- // exactly the code points with their own arcs (`<`, `&`, NUL).
838
+ // Fast-forward over ordinary text without re-entering the switch.
839
+ // Only the data state's "anything else" arc is batched: the stops
840
+ // are exactly the code points with arcs of their own.
856
841
  pos++;
857
842
  if (nextLt < pos) {
858
843
  nextLt = input.indexOf("<", pos);
@@ -925,10 +910,8 @@ const tokenize = (input, pos = 0, callbacks = {}) => {
925
910
  pos++;
926
911
  }
927
912
  } else if (cc === CC_QUESTION_MARK) {
928
- // U+003F QUESTION MARK (?)
929
- // This is an unexpected-question-mark-instead-of-tag-name parse error.
930
- // Create a comment token whose data is the empty string. Reconsume in the
931
- // bogus comment state.
913
+ // `?`: unexpected-question-mark-instead-of-tag-name parse error. Create a
914
+ // comment token with empty data, reconsume in bogus comment.
932
915
  reportError(
933
916
  "unexpected-question-mark-instead-of-tag-name",
934
917
  pos,
@@ -959,26 +942,20 @@ const tokenize = (input, pos = 0, callbacks = {}) => {
959
942
 
960
943
  // https://html.spec.whatwg.org/multipage/parsing.html#end-tag-open-state
961
944
  case STATE_END_TAG_OPEN:
962
- // Consume the next input character. The spec's ASCII-alpha arc (create
963
- // an end tag token, reconsume in the tag name state) is taken inline by
964
- // the `<` `/` arc above, the only way into this state, so `cc` is never
965
- // ASCII alpha here.
945
+ // The spec's ASCII-alpha arc is taken inline by the `<` `/` arc above,
946
+ // the only way into this state, so `cc` is never ASCII alpha here.
966
947
  if (cc === CC_GREATER_THAN) {
967
- // U+003E GREATER-THAN SIGN (>)
968
- // This is a missing-end-tag-name parse error. Switch to the data state.
969
- // No token is emitted, so `</>` is dropped rather than left in the
970
- // text run: a browser renders nothing for it, and keeping it would
971
- // resurface as visible text once the text node is re-escaped.
948
+ // `>`: missing-end-tag-name parse error, switch to data. Emitting no
949
+ // token drops `</>` rather than keeping it in the text run, where
950
+ // re-escaping the text node would resurface it as visible text.
972
951
  reportError("missing-end-tag-name", pos, pos + 1, "warning");
973
952
  flushText(tagStart);
974
953
  state = STATE_DATA;
975
954
  pos++;
976
955
  textStart = pos;
977
956
  } else {
978
- // Anything else
979
- // This is an invalid-first-character-of-tag-name parse error. Create a
980
- // comment token whose data is the empty string. Reconsume in the bogus
981
- // comment state.
957
+ // Anything else: invalid-first-character-of-tag-name parse error. Create a
958
+ // comment token with empty data, reconsume in bogus comment.
982
959
  reportError(
983
960
  "invalid-first-character-of-tag-name",
984
961
  pos,
@@ -996,11 +973,7 @@ const tokenize = (input, pos = 0, callbacks = {}) => {
996
973
 
997
974
  // https://html.spec.whatwg.org/multipage/parsing.html#tag-name-state
998
975
  case STATE_TAG_NAME:
999
- // Consume the next input character:
1000
- // U+0009 CHARACTER TABULATION (tab)
1001
- // U+000A LINE FEED (LF)
1002
- // U+000C FORM FEED (FF)
1003
- // U+0020 SPACE
976
+ // Whitespace (tab, LF, FF, space)
1004
977
  // Switch to the before attribute name state.
1005
978
  if (isSpace(cc)) {
1006
979
  tagNameEnd = pos;
@@ -1043,20 +1016,13 @@ const tokenize = (input, pos = 0, callbacks = {}) => {
1043
1016
 
1044
1017
  // https://html.spec.whatwg.org/multipage/parsing.html#before-attribute-name-state
1045
1018
  case STATE_BEFORE_ATTRIBUTE_NAME:
1046
- // Consume the next input character:
1047
- // U+0009 CHARACTER TABULATION (tab)
1048
- // U+000A LINE FEED (LF)
1049
- // U+000C FORM FEED (FF)
1050
- // U+0020 SPACE
1019
+ // Whitespace (tab, LF, FF, space)
1051
1020
  // Ignore the character.
1052
1021
  // Reconsume so space is handled in BEFORE_ATTRIBUTE_NAME
1053
1022
  if (isSpace(cc)) {
1054
1023
  pos++;
1055
1024
  } else if (cc === CC_SOLIDUS || cc === CC_GREATER_THAN) {
1056
- // U+002F SOLIDUS (/)
1057
- // U+003E GREATER-THAN SIGN (>)
1058
- // EOF
1059
- // Reconsume in the after attribute name state.
1025
+ // `/` / `>` / EOF: reconsume in after attribute name.
1060
1026
  state = STATE_AFTER_ATTRIBUTE_NAME;
1061
1027
  // Reconsume
1062
1028
  } else if (cc === CC_EQUALS) {
@@ -1108,15 +1074,8 @@ const tokenize = (input, pos = 0, callbacks = {}) => {
1108
1074
 
1109
1075
  // https://html.spec.whatwg.org/multipage/parsing.html#attribute-name-state
1110
1076
  case STATE_ATTRIBUTE_NAME:
1111
- // Consume the next input character:
1112
- // U+0009 CHARACTER TABULATION (tab)
1113
- // U+000A LINE FEED (LF)
1114
- // U+000C FORM FEED (FF)
1115
- // U+0020 SPACE
1116
- // U+002F SOLIDUS (/)
1117
- // U+003E GREATER-THAN SIGN (>)
1118
- // EOF
1119
- // Reconsume in the after attribute name state.
1077
+ // Whitespace (tab, LF, FF, space) / `/` / `>` / EOF: reconsume in after
1078
+ // attribute name.
1120
1079
  if (isSpace(cc) || cc === CC_SOLIDUS || cc === CC_GREATER_THAN) {
1121
1080
  attributeNameEnd = pos;
1122
1081
  state = STATE_AFTER_ATTRIBUTE_NAME;
@@ -1141,10 +1100,9 @@ const tokenize = (input, pos = 0, callbacks = {}) => {
1141
1100
  "warning"
1142
1101
  );
1143
1102
  }
1144
- // Fast-forward over the ordinary run of the attribute name; stop on
1145
- // any terminator (space / `/` / `>` / `=`) or a char that needs a
1146
- // per-occurrence parse error (NULL / `"` / `'` / `<`), which the
1147
- // outer switch then re-handles.
1103
+ // Fast-forward over the attribute name, stopping on a terminator
1104
+ // (space, `/`, `>`, `=`) or on a char needing a per-occurrence parse
1105
+ // error (NUL, `"`, `'`, `<`), which the outer switch re-handles.
1148
1106
  pos++;
1149
1107
  while (pos < len) {
1150
1108
  const c2 = input.charCodeAt(pos);
@@ -1158,11 +1116,7 @@ const tokenize = (input, pos = 0, callbacks = {}) => {
1158
1116
 
1159
1117
  // https://html.spec.whatwg.org/multipage/parsing.html#after-attribute-name-state
1160
1118
  case STATE_AFTER_ATTRIBUTE_NAME:
1161
- // Consume the next input character:
1162
- // U+0009 CHARACTER TABULATION (tab)
1163
- // U+000A LINE FEED (LF)
1164
- // U+000C FORM FEED (FF)
1165
- // U+0020 SPACE
1119
+ // Whitespace (tab, LF, FF, space)
1166
1120
  // Ignore the character.
1167
1121
  if (isSpace(cc)) {
1168
1122
  pos++;
@@ -1201,11 +1155,7 @@ const tokenize = (input, pos = 0, callbacks = {}) => {
1201
1155
 
1202
1156
  // https://html.spec.whatwg.org/multipage/parsing.html#before-attribute-value-state
1203
1157
  case STATE_BEFORE_ATTRIBUTE_VALUE:
1204
- // Consume the next input character:
1205
- // U+0009 CHARACTER TABULATION (tab)
1206
- // U+000A LINE FEED (LF)
1207
- // U+000C FORM FEED (FF)
1208
- // U+0020 SPACE
1158
+ // Whitespace (tab, LF, FF, space)
1209
1159
  // Ignore the character.
1210
1160
  if (isSpace(cc)) {
1211
1161
  pos++;
@@ -1254,11 +1204,9 @@ const tokenize = (input, pos = 0, callbacks = {}) => {
1254
1204
  }
1255
1205
  pos = Math.min(nextSQuote, nextAmp, nextNul);
1256
1206
  } else if (cc === CC_GREATER_THAN) {
1257
- // U+003E GREATER-THAN SIGN (>)
1258
- // This is a missing-attribute-value parse error. Switch to the data
1259
- // state. Emit the current tag token. The attribute is reported with
1260
- // an empty value range pointing at the `>` so the open-tag offset range
1261
- // still includes the `>`.
1207
+ // `>`: missing-attribute-value parse error, switch to data and emit
1208
+ // the tag token. The attribute's empty value range points at the `>`,
1209
+ // so the open tag's own range still includes it.
1262
1210
  reportError("missing-attribute-value", pos, pos + 1, "warning");
1263
1211
  attributeValueStart = pos;
1264
1212
  attrQuoteType = QUOTE_NONE;
@@ -1409,11 +1357,7 @@ const tokenize = (input, pos = 0, callbacks = {}) => {
1409
1357
 
1410
1358
  // https://html.spec.whatwg.org/multipage/parsing.html#after-attribute-value-(quoted)-state
1411
1359
  case STATE_AFTER_ATTRIBUTE_VALUE_QUOTED:
1412
- // Consume the next input character:
1413
- // U+0009 CHARACTER TABULATION (tab)
1414
- // U+000A LINE FEED (LF)
1415
- // U+000C FORM FEED (FF)
1416
- // U+0020 SPACE
1360
+ // Whitespace (tab, LF, FF, space)
1417
1361
  // Switch to the before attribute name state.
1418
1362
  if (isSpace(cc)) {
1419
1363
  state = STATE_BEFORE_ATTRIBUTE_NAME;
@@ -1449,10 +1393,8 @@ const tokenize = (input, pos = 0, callbacks = {}) => {
1449
1393
 
1450
1394
  // https://html.spec.whatwg.org/multipage/parsing.html#self-closing-start-tag-state
1451
1395
  case STATE_SELF_CLOSING_START_TAG:
1452
- // Consume the next input character:
1453
- // U+003E GREATER-THAN SIGN (>)
1454
- // Set the self-closing flag of the current tag token. Switch to the data
1455
- // state. Emit the current tag token.
1396
+ // `>`: Set the self-closing flag of the tag token, switch to data. Emit the
1397
+ // tag token.
1456
1398
  if (cc === CC_GREATER_THAN) {
1457
1399
  if (input.charCodeAt(tagStart + 1) === CC_SOLIDUS) {
1458
1400
  // An end tag emitted with the self-closing flag set is an
@@ -1481,10 +1423,8 @@ const tokenize = (input, pos = 0, callbacks = {}) => {
1481
1423
 
1482
1424
  // https://html.spec.whatwg.org/multipage/parsing.html#markup-declaration-open-state
1483
1425
  case STATE_MARKUP_DECLARATION_OPEN:
1484
- // If the next few characters are:
1485
- // Two U+002D HYPHEN-MINUS characters (-)
1486
- // Consume those two characters, create a comment token whose data
1487
- // is the empty string, and switch to the comment start state.
1426
+ // `--`: Consume those two characters, create a comment token with empty
1427
+ // data, and switch to the comment start state.
1488
1428
  if (
1489
1429
  cc === CC_HYPHEN_MINUS &&
1490
1430
  input.charCodeAt(pos + 1) === CC_HYPHEN_MINUS
@@ -1516,13 +1456,9 @@ const tokenize = (input, pos = 0, callbacks = {}) => {
1516
1456
  doctypeForceQuirks = false;
1517
1457
  state = STATE_DOCTYPE;
1518
1458
  } else if (
1519
- // The string "[CDATA[" (the five uppercase letters "CDATA" with a
1520
- // U+005B LEFT SQUARE BRACKET character before and after)
1521
- // Consume those characters and switch to the CDATA section state.
1522
- // Only when there is an adjusted current node and it is not an
1523
- // element in the HTML namespace: everywhere else this is the
1524
- // "anything else" branch below, a bogus comment ending at the first
1525
- // `>`, which leaves the rest (typically `]]>`) as text.
1459
+ // `[CDATA[`: consume it and switch to CDATA section, but only with an
1460
+ // adjusted current node outside the HTML namespace. Elsewhere it is a
1461
+ // bogus comment, leaving the rest as text.
1526
1462
  cc === CC_LEFT_SQUARE_BRACKET &&
1527
1463
  input.charCodeAt(pos + 1) === 0x43 /* C */ &&
1528
1464
  input.charCodeAt(pos + 2) === 0x44 /* D */ &&
@@ -1539,10 +1475,9 @@ const tokenize = (input, pos = 0, callbacks = {}) => {
1539
1475
  commentDataEnd = pos;
1540
1476
  state = STATE_CDATA_SECTION;
1541
1477
  } else {
1542
- // Anything else
1543
- // This is an incorrectly-opened-comment parse error. Create a comment token
1544
- // whose data is the empty string. Switch to the bogus comment state (don't
1545
- // consume anything in the current state).
1478
+ // Anything else: incorrectly-opened-comment parse error. Create a comment
1479
+ // token with empty data, switch to bogus comment (don't consume anything
1480
+ // in the current state).
1546
1481
  reportError("incorrectly-opened-comment", tagStart, pos, "warning");
1547
1482
  commentStart = tagStart;
1548
1483
  commentDataStart = pos;
@@ -1681,10 +1616,8 @@ const tokenize = (input, pos = 0, callbacks = {}) => {
1681
1616
  state = STATE_COMMENT_END;
1682
1617
  pos++;
1683
1618
  } else {
1684
- // Anything else
1685
- // Append a U+002D HYPHEN-MINUS character (-) to the comment token's data.
1686
- // Reconsume in the comment state (so e.g. NULL and `<` are
1687
- // handled there).
1619
+ // Anything else: append a `-` to the comment data and reconsume in
1620
+ // comment, where NUL and `<` are handled.
1688
1621
  commentDataEnd = pos;
1689
1622
  state = STATE_COMMENT;
1690
1623
  // Reconsume
@@ -1721,10 +1654,8 @@ const tokenize = (input, pos = 0, callbacks = {}) => {
1721
1654
  commentDataEnd++;
1722
1655
  pos++;
1723
1656
  } else {
1724
- // Anything else
1725
- // Append two U+002D HYPHEN-MINUS characters (-) to the comment token's
1726
- // data. Reconsume in the comment state (so NULL and `<` are
1727
- // handled there).
1657
+ // Anything else: append two `-` to the comment data and reconsume in
1658
+ // comment, where NUL and `<` are handled.
1728
1659
  commentDataEnd = pos;
1729
1660
  state = STATE_COMMENT;
1730
1661
  // Reconsume
@@ -1733,11 +1664,8 @@ const tokenize = (input, pos = 0, callbacks = {}) => {
1733
1664
 
1734
1665
  // https://html.spec.whatwg.org/multipage/parsing.html#comment-end-bang-state
1735
1666
  case STATE_COMMENT_END_BANG:
1736
- // Consume the next input character:
1737
- // U+002D HYPHEN-MINUS (-)
1738
- // Append two U+002D HYPHEN-MINUS characters (-) and a U+0021 EXCLAMATION
1739
- // MARK character (!) to the comment token's data. Switch to the comment end
1740
- // dash state.
1667
+ // `-`: Append two `-` and a `!` to the comment data, switch to comment end
1668
+ // dash.
1741
1669
  if (cc === CC_HYPHEN_MINUS) {
1742
1670
  commentDataEnd = pos;
1743
1671
  state = STATE_COMMENT_END_DASH;
@@ -1761,10 +1689,8 @@ const tokenize = (input, pos = 0, callbacks = {}) => {
1761
1689
  textStart = nextPos;
1762
1690
  pos = nextPos;
1763
1691
  } else {
1764
- // Anything else
1765
- // Append two U+002D HYPHEN-MINUS characters (-) and a U+0021 EXCLAMATION
1766
- // MARK character (!) to the comment token's data. Reconsume in the comment
1767
- // state (so NULL and `<` are handled there).
1692
+ // Anything else: append two `-` and a `!` to the comment data and
1693
+ // reconsume in comment, where NUL and `<` are handled.
1768
1694
  commentDataEnd = pos;
1769
1695
  state = STATE_COMMENT;
1770
1696
  // Reconsume
@@ -1813,10 +1739,8 @@ const tokenize = (input, pos = 0, callbacks = {}) => {
1813
1739
 
1814
1740
  // https://html.spec.whatwg.org/multipage/parsing.html#comment-less-than-sign-state
1815
1741
  case STATE_COMMENT_LESS_THAN_SIGN:
1816
- // Consume the next input character:
1817
- // U+0021 EXCLAMATION MARK (!)
1818
- // Append the current input character to the comment token's data. Switch to
1819
- // the comment less-than sign bang state.
1742
+ // `!`: Append the input character to the comment data, switch to comment
1743
+ // less-than sign bang.
1820
1744
  if (cc === CC_EXCLAMATION_MARK) {
1821
1745
  state = STATE_COMMENT_LESS_THAN_SIGN_BANG;
1822
1746
  pos++;
@@ -1868,12 +1792,8 @@ const tokenize = (input, pos = 0, callbacks = {}) => {
1868
1792
 
1869
1793
  // https://html.spec.whatwg.org/multipage/parsing.html#comment-less-than-sign-bang-dash-dash-state
1870
1794
  case STATE_COMMENT_LESS_THAN_SIGN_BANG_DASH_DASH:
1871
- // Consume the next input character:
1872
- // U+003E GREATER-THAN SIGN (>)
1873
- // EOF
1874
- // Reconsume in the comment end state.
1875
- // Anything else
1876
- // This is a nested-comment parse error. Reconsume in the comment end state.
1795
+ // `>` / EOF: reconsume in comment end. Anything else: nested-comment parse
1796
+ // error, reconsume in comment end.
1877
1797
  if (cc !== CC_GREATER_THAN) {
1878
1798
  reportError("nested-comment", pos, pos + 1, "warning");
1879
1799
  }
@@ -1883,11 +1803,7 @@ const tokenize = (input, pos = 0, callbacks = {}) => {
1883
1803
 
1884
1804
  // https://html.spec.whatwg.org/multipage/parsing.html#doctype-state
1885
1805
  case STATE_DOCTYPE:
1886
- // Consume the next input character:
1887
- // U+0009 CHARACTER TABULATION (tab)
1888
- // U+000A LINE FEED (LF)
1889
- // U+000C FORM FEED (FF)
1890
- // U+0020 SPACE
1806
+ // Whitespace (tab, LF, FF, space)
1891
1807
  // Switch to the before DOCTYPE name state.
1892
1808
  if (isSpace(cc)) {
1893
1809
  state = STATE_BEFORE_DOCTYPE_NAME;
@@ -1912,29 +1828,21 @@ const tokenize = (input, pos = 0, callbacks = {}) => {
1912
1828
 
1913
1829
  // https://html.spec.whatwg.org/multipage/parsing.html#before-doctype-name-state
1914
1830
  case STATE_BEFORE_DOCTYPE_NAME:
1915
- // Consume the next input character:
1916
- // U+0009 CHARACTER TABULATION (tab)
1917
- // U+000A LINE FEED (LF)
1918
- // U+000C FORM FEED (FF)
1919
- // U+0020 SPACE
1831
+ // Whitespace (tab, LF, FF, space)
1920
1832
  // Ignore the character.
1921
1833
  if (isSpace(cc)) {
1922
1834
  pos++;
1923
1835
  } else if (cc === CC_NULL) {
1924
- // U+0000 NULL
1925
- // This is an unexpected-null-character parse error. Create a new DOCTYPE
1926
- // token. Set the token's name to a U+FFFD REPLACEMENT CHARACTER character.
1927
- // Switch to the DOCTYPE name state.
1836
+ // NUL: unexpected-null-character parse error. Create a new DOCTYPE token.
1837
+ // Set the token's name to a U+FFFD, switch to DOCTYPE name.
1928
1838
  reportError("unexpected-null-character", pos, pos + 1, "warning");
1929
1839
  state = STATE_DOCTYPE_NAME;
1930
1840
  doctypeNameStart = pos;
1931
1841
  pos++;
1932
1842
  doctypeNameEnd = pos;
1933
1843
  } else if (cc === CC_GREATER_THAN) {
1934
- // U+003E GREATER-THAN SIGN (>)
1935
- // This is a missing-doctype-name parse error. Create a new DOCTYPE token.
1936
- // Set its force-quirks flag to on. Switch to the data state. Emit the
1937
- // current token.
1844
+ // `>`: missing-doctype-name parse error. Create a new DOCTYPE token, force
1845
+ // quirks, switch to data. Emit the token.
1938
1846
  reportError("missing-doctype-name", pos, pos + 1, "warning");
1939
1847
  doctypeForceQuirks = true;
1940
1848
  let nextPos = pos + 1;
@@ -1956,13 +1864,9 @@ const tokenize = (input, pos = 0, callbacks = {}) => {
1956
1864
  textStart = nextPos;
1957
1865
  pos = nextPos;
1958
1866
  } else {
1959
- // ASCII upper alpha
1960
- // Create a new DOCTYPE token. Set the token's name to the lowercase version
1961
- // of the current input character (add 0x0020 to the character's code
1962
- // point). Switch to the DOCTYPE name state.
1963
- // Anything else
1964
- // Create a new DOCTYPE token. Set the token's name to the current input
1965
- // character. Switch to the DOCTYPE name state.
1867
+ // ASCII upper alpha, and anything else: create a DOCTYPE token, set
1868
+ // its name to the input character — lowercased in the first case —
1869
+ // and switch to DOCTYPE name.
1966
1870
  state = STATE_DOCTYPE_NAME;
1967
1871
  doctypeNameStart = pos;
1968
1872
  pos++;
@@ -1972,11 +1876,7 @@ const tokenize = (input, pos = 0, callbacks = {}) => {
1972
1876
 
1973
1877
  // https://html.spec.whatwg.org/multipage/parsing.html#doctype-name-state
1974
1878
  case STATE_DOCTYPE_NAME:
1975
- // Consume the next input character:
1976
- // U+0009 CHARACTER TABULATION (tab)
1977
- // U+000A LINE FEED (LF)
1978
- // U+000C FORM FEED (FF)
1979
- // U+0020 SPACE
1879
+ // Whitespace (tab, LF, FF, space)
1980
1880
  // Switch to the after DOCTYPE name state.
1981
1881
  if (isSpace(cc)) {
1982
1882
  state = STATE_AFTER_DOCTYPE_NAME;
@@ -2010,11 +1910,9 @@ const tokenize = (input, pos = 0, callbacks = {}) => {
2010
1910
  pos++;
2011
1911
  doctypeNameEnd = pos;
2012
1912
  } else {
2013
- // ASCII upper alpha
2014
- // Append the lowercase version of the current input character (add 0x0020
2015
- // to the character's code point) to the current DOCTYPE token's name.
2016
- // Anything else
2017
- // Append the current input character to the current DOCTYPE token's name.
1913
+ // ASCII upper alpha: Append the lowercased input character to the DOCTYPE
1914
+ // token's name. Anything else: Append the input character to the DOCTYPE
1915
+ // token's name.
2018
1916
  pos++;
2019
1917
  doctypeNameEnd = pos;
2020
1918
  }
@@ -2024,10 +1922,7 @@ const tokenize = (input, pos = 0, callbacks = {}) => {
2024
1922
  case STATE_AFTER_DOCTYPE_NAME:
2025
1923
  // Consume the next input character:
2026
1924
  if (isSpace(cc)) {
2027
- // U+0009 CHARACTER TABULATION (tab)
2028
- // U+000A LINE FEED (LF)
2029
- // U+000C FORM FEED (FF)
2030
- // U+0020 SPACE
1925
+ // Whitespace (tab, LF, FF, space)
2031
1926
  // Ignore the character.
2032
1927
  pos++;
2033
1928
  } else if (cc === CC_GREATER_THAN) {
@@ -2076,10 +1971,8 @@ const tokenize = (input, pos = 0, callbacks = {}) => {
2076
1971
  pos += 6;
2077
1972
  state = STATE_AFTER_DOCTYPE_SYSTEM_KEYWORD;
2078
1973
  } else {
2079
- // Anything else
2080
- // This is an invalid-character-sequence-after-doctype-name parse error. Set
2081
- // the current DOCTYPE token's force-quirks flag to on. Reconsume in the
2082
- // bogus DOCTYPE state.
1974
+ // Anything else: invalid-character-sequence-after-doctype-name parse
1975
+ // error, force quirks, reconsume in bogus DOCTYPE.
2083
1976
  reportError(
2084
1977
  "invalid-character-sequence-after-doctype-name",
2085
1978
  pos,
@@ -2095,19 +1988,14 @@ const tokenize = (input, pos = 0, callbacks = {}) => {
2095
1988
  case STATE_AFTER_DOCTYPE_PUBLIC_KEYWORD:
2096
1989
  // Consume the next input character:
2097
1990
  if (isSpace(cc)) {
2098
- // U+0009 CHARACTER TABULATION (tab)
2099
- // U+000A LINE FEED (LF)
2100
- // U+000C FORM FEED (FF)
2101
- // U+0020 SPACE
1991
+ // Whitespace (tab, LF, FF, space)
2102
1992
  // Switch to the before DOCTYPE public identifier state.
2103
1993
  state = STATE_BEFORE_DOCTYPE_PUBLIC_IDENTIFIER;
2104
1994
  pos++;
2105
1995
  } else if (cc === CC_QUOTATION_MARK) {
2106
- // U+0022 QUOTATION MARK (")
2107
- // This is a missing-whitespace-after-doctype-public-keyword parse error.
2108
- // Set the current DOCTYPE token's public identifier to the empty string
2109
- // (not missing), then switch to the DOCTYPE public identifier
2110
- // (double-quoted) state.
1996
+ // `"`: missing-whitespace-after-doctype-public-keyword parse error. Set
1997
+ // the DOCTYPE token's public identifier to the empty string (not missing),
1998
+ // then switch to the DOCTYPE public identifier (double-quoted) state.
2111
1999
  reportError(
2112
2000
  "missing-whitespace-after-doctype-public-keyword",
2113
2001
  pos,
@@ -2119,11 +2007,9 @@ const tokenize = (input, pos = 0, callbacks = {}) => {
2119
2007
  doctypePublicStart = pos;
2120
2008
  doctypePublicEnd = pos;
2121
2009
  } else if (cc === CC_APOSTROPHE) {
2122
- // U+0027 APOSTROPHE (')
2123
- // This is a missing-whitespace-after-doctype-public-keyword parse error.
2124
- // Set the current DOCTYPE token's public identifier to the empty string
2125
- // (not missing), then switch to the DOCTYPE public identifier
2126
- // (single-quoted) state.
2010
+ // `'`: missing-whitespace-after-doctype-public-keyword parse error. Set
2011
+ // the DOCTYPE token's public identifier to the empty string (not missing),
2012
+ // then switch to the DOCTYPE public identifier (single-quoted) state.
2127
2013
  reportError(
2128
2014
  "missing-whitespace-after-doctype-public-keyword",
2129
2015
  pos,
@@ -2135,10 +2021,8 @@ const tokenize = (input, pos = 0, callbacks = {}) => {
2135
2021
  doctypePublicStart = pos;
2136
2022
  doctypePublicEnd = pos;
2137
2023
  } else if (cc === CC_GREATER_THAN) {
2138
- // U+003E GREATER-THAN SIGN (>)
2139
- // This is a missing-doctype-public-identifier parse error. Set the current
2140
- // DOCTYPE token's force-quirks flag to on. Switch to the data state. Emit
2141
- // the current DOCTYPE token.
2024
+ // `>`: missing-doctype-public-identifier parse error, force quirks, switch
2025
+ // to data. Emit the DOCTYPE token.
2142
2026
  reportError(
2143
2027
  "missing-doctype-public-identifier",
2144
2028
  pos,
@@ -2165,10 +2049,8 @@ const tokenize = (input, pos = 0, callbacks = {}) => {
2165
2049
  textStart = nextPos;
2166
2050
  pos = nextPos;
2167
2051
  } else {
2168
- // Anything else
2169
- // This is a missing-quote-before-doctype-public-identifier parse error. Set
2170
- // the current DOCTYPE token's force-quirks flag to on. Reconsume in the
2171
- // bogus DOCTYPE state.
2052
+ // Anything else: missing-quote-before-doctype-public-identifier parse
2053
+ // error, force quirks, reconsume in bogus DOCTYPE.
2172
2054
  reportError(
2173
2055
  "missing-quote-before-doctype-public-identifier",
2174
2056
  pos,
@@ -2184,35 +2066,28 @@ const tokenize = (input, pos = 0, callbacks = {}) => {
2184
2066
  case STATE_BEFORE_DOCTYPE_PUBLIC_IDENTIFIER:
2185
2067
  // Consume the next input character:
2186
2068
  if (isSpace(cc)) {
2187
- // U+0009 CHARACTER TABULATION (tab)
2188
- // U+000A LINE FEED (LF)
2189
- // U+000C FORM FEED (FF)
2190
- // U+0020 SPACE
2069
+ // Whitespace (tab, LF, FF, space)
2191
2070
  // Ignore the character.
2192
2071
  pos++;
2193
2072
  } else if (cc === CC_QUOTATION_MARK) {
2194
- // U+0022 QUOTATION MARK (")
2195
- // Set the current DOCTYPE token's public identifier to the empty string
2196
- // (not missing), then switch to the DOCTYPE public identifier
2197
- // (double-quoted) state.
2073
+ // `"`: Set the DOCTYPE token's public identifier to the empty string (not
2074
+ // missing), then switch to the DOCTYPE public identifier (double-quoted)
2075
+ // state.
2198
2076
  state = STATE_DOCTYPE_PUBLIC_IDENTIFIER_DOUBLE_QUOTED;
2199
2077
  pos++;
2200
2078
  doctypePublicStart = pos;
2201
2079
  doctypePublicEnd = pos;
2202
2080
  } else if (cc === CC_APOSTROPHE) {
2203
- // U+0027 APOSTROPHE (')
2204
- // Set the current DOCTYPE token's public identifier to the empty string
2205
- // (not missing), then switch to the DOCTYPE public identifier
2206
- // (single-quoted) state.
2081
+ // `'`: Set the DOCTYPE token's public identifier to the empty string (not
2082
+ // missing), then switch to the DOCTYPE public identifier (single-quoted)
2083
+ // state.
2207
2084
  state = STATE_DOCTYPE_PUBLIC_IDENTIFIER_SINGLE_QUOTED;
2208
2085
  pos++;
2209
2086
  doctypePublicStart = pos;
2210
2087
  doctypePublicEnd = pos;
2211
2088
  } else if (cc === CC_GREATER_THAN) {
2212
- // U+003E GREATER-THAN SIGN (>)
2213
- // This is a missing-doctype-public-identifier parse error. Set the current
2214
- // DOCTYPE token's force-quirks flag to on. Switch to the data state. Emit
2215
- // the current DOCTYPE token.
2089
+ // `>`: missing-doctype-public-identifier parse error, force quirks, switch
2090
+ // to data. Emit the DOCTYPE token.
2216
2091
  reportError(
2217
2092
  "missing-doctype-public-identifier",
2218
2093
  pos,
@@ -2239,10 +2114,8 @@ const tokenize = (input, pos = 0, callbacks = {}) => {
2239
2114
  textStart = nextPos;
2240
2115
  pos = nextPos;
2241
2116
  } else {
2242
- // Anything else
2243
- // This is a missing-quote-before-doctype-public-identifier parse error. Set
2244
- // the current DOCTYPE token's force-quirks flag to on. Reconsume in the
2245
- // bogus DOCTYPE state.
2117
+ // Anything else: missing-quote-before-doctype-public-identifier parse
2118
+ // error, force quirks, reconsume in bogus DOCTYPE.
2246
2119
  reportError(
2247
2120
  "missing-quote-before-doctype-public-identifier",
2248
2121
  pos,
@@ -2263,18 +2136,14 @@ const tokenize = (input, pos = 0, callbacks = {}) => {
2263
2136
  state = STATE_AFTER_DOCTYPE_PUBLIC_IDENTIFIER;
2264
2137
  pos++;
2265
2138
  } else if (cc === CC_NULL) {
2266
- // U+0000 NULL
2267
- // This is an unexpected-null-character parse error. Append a U+FFFD
2268
- // REPLACEMENT CHARACTER character to the current DOCTYPE token's public
2269
- // identifier.
2139
+ // NUL: unexpected-null-character parse error. Append a U+FFFD to the
2140
+ // DOCTYPE token's public identifier.
2270
2141
  reportError("unexpected-null-character", pos, pos + 1, "warning");
2271
2142
  pos++;
2272
2143
  doctypePublicEnd = pos;
2273
2144
  } else if (cc === CC_GREATER_THAN) {
2274
- // U+003E GREATER-THAN SIGN (>)
2275
- // This is an abrupt-doctype-public-identifier parse error. Set the current
2276
- // DOCTYPE token's force-quirks flag to on. Switch to the data state. Emit
2277
- // the current DOCTYPE token.
2145
+ // `>`: abrupt-doctype-public-identifier parse error, force quirks, switch
2146
+ // to data. Emit the DOCTYPE token.
2278
2147
  reportError(
2279
2148
  "abrupt-doctype-public-identifier",
2280
2149
  pos,
@@ -2318,18 +2187,14 @@ const tokenize = (input, pos = 0, callbacks = {}) => {
2318
2187
  state = STATE_AFTER_DOCTYPE_PUBLIC_IDENTIFIER;
2319
2188
  pos++;
2320
2189
  } else if (cc === CC_NULL) {
2321
- // U+0000 NULL
2322
- // This is an unexpected-null-character parse error. Append a U+FFFD
2323
- // REPLACEMENT CHARACTER character to the current DOCTYPE token's public
2324
- // identifier.
2190
+ // NUL: unexpected-null-character parse error. Append a U+FFFD to the
2191
+ // DOCTYPE token's public identifier.
2325
2192
  reportError("unexpected-null-character", pos, pos + 1, "warning");
2326
2193
  pos++;
2327
2194
  doctypePublicEnd = pos;
2328
2195
  } else if (cc === CC_GREATER_THAN) {
2329
- // U+003E GREATER-THAN SIGN (>)
2330
- // This is an abrupt-doctype-public-identifier parse error. Set the current
2331
- // DOCTYPE token's force-quirks flag to on. Switch to the data state. Emit
2332
- // the current DOCTYPE token.
2196
+ // `>`: abrupt-doctype-public-identifier parse error, force quirks, switch
2197
+ // to data. Emit the DOCTYPE token.
2333
2198
  reportError(
2334
2199
  "abrupt-doctype-public-identifier",
2335
2200
  pos,
@@ -2368,10 +2233,7 @@ const tokenize = (input, pos = 0, callbacks = {}) => {
2368
2233
  case STATE_AFTER_DOCTYPE_PUBLIC_IDENTIFIER:
2369
2234
  // Consume the next input character:
2370
2235
  if (isSpace(cc)) {
2371
- // U+0009 CHARACTER TABULATION (tab)
2372
- // U+000A LINE FEED (LF)
2373
- // U+000C FORM FEED (FF)
2374
- // U+0020 SPACE
2236
+ // Whitespace (tab, LF, FF, space)
2375
2237
  // Switch to the between DOCTYPE public and system identifiers state.
2376
2238
  state = STATE_BETWEEN_DOCTYPE_PUBLIC_AND_SYSTEM_IDENTIFIERS;
2377
2239
  pos++;
@@ -2397,11 +2259,9 @@ const tokenize = (input, pos = 0, callbacks = {}) => {
2397
2259
  textStart = nextPos;
2398
2260
  pos = nextPos;
2399
2261
  } else if (cc === CC_QUOTATION_MARK) {
2400
- // U+0022 QUOTATION MARK (")
2401
- // This is a missing-whitespace-between-doctype-public-and-system-identifiers
2402
- // parse error. Set the current DOCTYPE token's system
2403
- // identifier to the empty string (not missing), then switch
2404
- // to the DOCTYPE system identifier (double-quoted) state.
2262
+ // `"`: missing-whitespace-between-doctype-public-and-system-identifiers
2263
+ // parse error. Set the system identifier to empty (not missing), then
2264
+ // switch to DOCTYPE system identifier (double-quoted).
2405
2265
  reportError(
2406
2266
  "missing-whitespace-between-doctype-public-and-system-identifiers",
2407
2267
  pos,
@@ -2413,11 +2273,9 @@ const tokenize = (input, pos = 0, callbacks = {}) => {
2413
2273
  doctypeSystemStart = pos;
2414
2274
  doctypeSystemEnd = pos;
2415
2275
  } else if (cc === CC_APOSTROPHE) {
2416
- // U+0027 APOSTROPHE (')
2417
- // This is a missing-whitespace-between-doctype-public-and-system-identifiers
2418
- // parse error. Set the current DOCTYPE token's system
2419
- // identifier to the empty string (not missing), then switch
2420
- // to the DOCTYPE system identifier (single-quoted) state.
2276
+ // `'`: missing-whitespace-between-doctype-public-and-system-identifiers
2277
+ // parse error. Set the system identifier to empty (not missing), then
2278
+ // switch to DOCTYPE system identifier (single-quoted).
2421
2279
  reportError(
2422
2280
  "missing-whitespace-between-doctype-public-and-system-identifiers",
2423
2281
  pos,
@@ -2429,10 +2287,8 @@ const tokenize = (input, pos = 0, callbacks = {}) => {
2429
2287
  doctypeSystemStart = pos;
2430
2288
  doctypeSystemEnd = pos;
2431
2289
  } else {
2432
- // Anything else
2433
- // This is a missing-quote-before-doctype-system-identifier parse error. Set
2434
- // the current DOCTYPE token's force-quirks flag to on. Reconsume in the
2435
- // bogus DOCTYPE state.
2290
+ // Anything else: missing-quote-before-doctype-system-identifier parse
2291
+ // error, force quirks, reconsume in bogus DOCTYPE.
2436
2292
  reportError(
2437
2293
  "missing-quote-before-doctype-system-identifier",
2438
2294
  pos,
@@ -2448,10 +2304,7 @@ const tokenize = (input, pos = 0, callbacks = {}) => {
2448
2304
  case STATE_BETWEEN_DOCTYPE_PUBLIC_AND_SYSTEM_IDENTIFIERS:
2449
2305
  // Consume the next input character:
2450
2306
  if (isSpace(cc)) {
2451
- // U+0009 CHARACTER TABULATION (tab)
2452
- // U+000A LINE FEED (LF)
2453
- // U+000C FORM FEED (FF)
2454
- // U+0020 SPACE
2307
+ // Whitespace (tab, LF, FF, space)
2455
2308
  // Ignore the character.
2456
2309
  pos++;
2457
2310
  } else if (cc === CC_GREATER_THAN) {
@@ -2476,28 +2329,24 @@ const tokenize = (input, pos = 0, callbacks = {}) => {
2476
2329
  textStart = nextPos;
2477
2330
  pos = nextPos;
2478
2331
  } else if (cc === CC_QUOTATION_MARK) {
2479
- // U+0022 QUOTATION MARK (")
2480
- // Set the current DOCTYPE token's system identifier to the empty string
2481
- // (not missing), then switch to the DOCTYPE system identifier
2482
- // (double-quoted) state.
2332
+ // `"`: Set the DOCTYPE token's system identifier to the empty string (not
2333
+ // missing), then switch to the DOCTYPE system identifier (double-quoted)
2334
+ // state.
2483
2335
  state = STATE_DOCTYPE_SYSTEM_IDENTIFIER_DOUBLE_QUOTED;
2484
2336
  pos++;
2485
2337
  doctypeSystemStart = pos;
2486
2338
  doctypeSystemEnd = pos;
2487
2339
  } else if (cc === CC_APOSTROPHE) {
2488
- // U+0027 APOSTROPHE (')
2489
- // Set the current DOCTYPE token's system identifier to the empty string
2490
- // (not missing), then switch to the DOCTYPE system identifier
2491
- // (single-quoted) state.
2340
+ // `'`: Set the DOCTYPE token's system identifier to the empty string (not
2341
+ // missing), then switch to the DOCTYPE system identifier (single-quoted)
2342
+ // state.
2492
2343
  state = STATE_DOCTYPE_SYSTEM_IDENTIFIER_SINGLE_QUOTED;
2493
2344
  pos++;
2494
2345
  doctypeSystemStart = pos;
2495
2346
  doctypeSystemEnd = pos;
2496
2347
  } else {
2497
- // Anything else
2498
- // This is a missing-quote-before-doctype-system-identifier parse error. Set
2499
- // the current DOCTYPE token's force-quirks flag to on. Reconsume in the
2500
- // bogus DOCTYPE state.
2348
+ // Anything else: missing-quote-before-doctype-system-identifier parse
2349
+ // error, force quirks, reconsume in bogus DOCTYPE.
2501
2350
  reportError(
2502
2351
  "missing-quote-before-doctype-system-identifier",
2503
2352
  pos,
@@ -2513,19 +2362,14 @@ const tokenize = (input, pos = 0, callbacks = {}) => {
2513
2362
  case STATE_AFTER_DOCTYPE_SYSTEM_KEYWORD:
2514
2363
  // Consume the next input character:
2515
2364
  if (isSpace(cc)) {
2516
- // U+0009 CHARACTER TABULATION (tab)
2517
- // U+000A LINE FEED (LF)
2518
- // U+000C FORM FEED (FF)
2519
- // U+0020 SPACE
2365
+ // Whitespace (tab, LF, FF, space)
2520
2366
  // Switch to the before DOCTYPE system identifier state.
2521
2367
  state = STATE_BEFORE_DOCTYPE_SYSTEM_IDENTIFIER;
2522
2368
  pos++;
2523
2369
  } else if (cc === CC_QUOTATION_MARK) {
2524
- // U+0022 QUOTATION MARK (")
2525
- // This is a missing-whitespace-after-doctype-system-keyword parse error.
2526
- // Set the current DOCTYPE token's system identifier to the empty string
2527
- // (not missing), then switch to the DOCTYPE system identifier
2528
- // (double-quoted) state.
2370
+ // `"`: missing-whitespace-after-doctype-system-keyword parse error. Set
2371
+ // the DOCTYPE token's system identifier to the empty string (not missing),
2372
+ // then switch to the DOCTYPE system identifier (double-quoted) state.
2529
2373
  reportError(
2530
2374
  "missing-whitespace-after-doctype-system-keyword",
2531
2375
  pos,
@@ -2537,11 +2381,9 @@ const tokenize = (input, pos = 0, callbacks = {}) => {
2537
2381
  doctypeSystemStart = pos;
2538
2382
  doctypeSystemEnd = pos;
2539
2383
  } else if (cc === CC_APOSTROPHE) {
2540
- // U+0027 APOSTROPHE (')
2541
- // This is a missing-whitespace-after-doctype-system-keyword parse error.
2542
- // Set the current DOCTYPE token's system identifier to the empty string
2543
- // (not missing), then switch to the DOCTYPE system identifier
2544
- // (single-quoted) state.
2384
+ // `'`: missing-whitespace-after-doctype-system-keyword parse error. Set
2385
+ // the DOCTYPE token's system identifier to the empty string (not missing),
2386
+ // then switch to the DOCTYPE system identifier (single-quoted) state.
2545
2387
  reportError(
2546
2388
  "missing-whitespace-after-doctype-system-keyword",
2547
2389
  pos,
@@ -2553,10 +2395,8 @@ const tokenize = (input, pos = 0, callbacks = {}) => {
2553
2395
  doctypeSystemStart = pos;
2554
2396
  doctypeSystemEnd = pos;
2555
2397
  } else if (cc === CC_GREATER_THAN) {
2556
- // U+003E GREATER-THAN SIGN (>)
2557
- // This is a missing-doctype-system-identifier parse error. Set the current
2558
- // DOCTYPE token's force-quirks flag to on. Switch to the data state. Emit
2559
- // the current DOCTYPE token.
2398
+ // `>`: missing-doctype-system-identifier parse error, force quirks, switch
2399
+ // to data. Emit the DOCTYPE token.
2560
2400
  reportError(
2561
2401
  "missing-doctype-system-identifier",
2562
2402
  pos,
@@ -2583,10 +2423,8 @@ const tokenize = (input, pos = 0, callbacks = {}) => {
2583
2423
  textStart = nextPos;
2584
2424
  pos = nextPos;
2585
2425
  } else {
2586
- // Anything else
2587
- // This is a missing-quote-before-doctype-system-identifier parse error. Set
2588
- // the current DOCTYPE token's force-quirks flag to on. Reconsume in the
2589
- // bogus DOCTYPE state.
2426
+ // Anything else: missing-quote-before-doctype-system-identifier parse
2427
+ // error, force quirks, reconsume in bogus DOCTYPE.
2590
2428
  reportError(
2591
2429
  "missing-quote-before-doctype-system-identifier",
2592
2430
  pos,
@@ -2602,35 +2440,28 @@ const tokenize = (input, pos = 0, callbacks = {}) => {
2602
2440
  case STATE_BEFORE_DOCTYPE_SYSTEM_IDENTIFIER:
2603
2441
  // Consume the next input character:
2604
2442
  if (isSpace(cc)) {
2605
- // U+0009 CHARACTER TABULATION (tab)
2606
- // U+000A LINE FEED (LF)
2607
- // U+000C FORM FEED (FF)
2608
- // U+0020 SPACE
2443
+ // Whitespace (tab, LF, FF, space)
2609
2444
  // Ignore the character.
2610
2445
  pos++;
2611
2446
  } else if (cc === CC_QUOTATION_MARK) {
2612
- // U+0022 QUOTATION MARK (")
2613
- // Set the current DOCTYPE token's system identifier to the empty string
2614
- // (not missing), then switch to the DOCTYPE system identifier
2615
- // (double-quoted) state.
2447
+ // `"`: Set the DOCTYPE token's system identifier to the empty string (not
2448
+ // missing), then switch to the DOCTYPE system identifier (double-quoted)
2449
+ // state.
2616
2450
  state = STATE_DOCTYPE_SYSTEM_IDENTIFIER_DOUBLE_QUOTED;
2617
2451
  pos++;
2618
2452
  doctypeSystemStart = pos;
2619
2453
  doctypeSystemEnd = pos;
2620
2454
  } else if (cc === CC_APOSTROPHE) {
2621
- // U+0027 APOSTROPHE (')
2622
- // Set the current DOCTYPE token's system identifier to the empty string
2623
- // (not missing), then switch to the DOCTYPE system identifier
2624
- // (single-quoted) state.
2455
+ // `'`: Set the DOCTYPE token's system identifier to the empty string (not
2456
+ // missing), then switch to the DOCTYPE system identifier (single-quoted)
2457
+ // state.
2625
2458
  state = STATE_DOCTYPE_SYSTEM_IDENTIFIER_SINGLE_QUOTED;
2626
2459
  pos++;
2627
2460
  doctypeSystemStart = pos;
2628
2461
  doctypeSystemEnd = pos;
2629
2462
  } else if (cc === CC_GREATER_THAN) {
2630
- // U+003E GREATER-THAN SIGN (>)
2631
- // This is a missing-doctype-system-identifier parse error. Set the current
2632
- // DOCTYPE token's force-quirks flag to on. Switch to the data state. Emit
2633
- // the current DOCTYPE token.
2463
+ // `>`: missing-doctype-system-identifier parse error, force quirks, switch
2464
+ // to data. Emit the DOCTYPE token.
2634
2465
  reportError(
2635
2466
  "missing-doctype-system-identifier",
2636
2467
  pos,
@@ -2657,10 +2488,8 @@ const tokenize = (input, pos = 0, callbacks = {}) => {
2657
2488
  textStart = nextPos;
2658
2489
  pos = nextPos;
2659
2490
  } else {
2660
- // Anything else
2661
- // This is a missing-quote-before-doctype-system-identifier parse error. Set
2662
- // the current DOCTYPE token's force-quirks flag to on. Reconsume in the
2663
- // bogus DOCTYPE state.
2491
+ // Anything else: missing-quote-before-doctype-system-identifier parse
2492
+ // error, force quirks, reconsume in bogus DOCTYPE.
2664
2493
  reportError(
2665
2494
  "missing-quote-before-doctype-system-identifier",
2666
2495
  pos,
@@ -2681,18 +2510,14 @@ const tokenize = (input, pos = 0, callbacks = {}) => {
2681
2510
  state = STATE_AFTER_DOCTYPE_SYSTEM_IDENTIFIER;
2682
2511
  pos++;
2683
2512
  } else if (cc === CC_NULL) {
2684
- // U+0000 NULL
2685
- // This is an unexpected-null-character parse error. Append a U+FFFD
2686
- // REPLACEMENT CHARACTER character to the current DOCTYPE token's system
2687
- // identifier.
2513
+ // NUL: unexpected-null-character parse error. Append a U+FFFD to the
2514
+ // DOCTYPE token's system identifier.
2688
2515
  reportError("unexpected-null-character", pos, pos + 1, "warning");
2689
2516
  pos++;
2690
2517
  doctypeSystemEnd = pos;
2691
2518
  } else if (cc === CC_GREATER_THAN) {
2692
- // U+003E GREATER-THAN SIGN (>)
2693
- // This is an abrupt-doctype-system-identifier parse error. Set the current
2694
- // DOCTYPE token's force-quirks flag to on. Switch to the data state. Emit
2695
- // the current DOCTYPE token.
2519
+ // `>`: abrupt-doctype-system-identifier parse error, force quirks, switch
2520
+ // to data. Emit the DOCTYPE token.
2696
2521
  reportError(
2697
2522
  "abrupt-doctype-system-identifier",
2698
2523
  pos,
@@ -2736,18 +2561,14 @@ const tokenize = (input, pos = 0, callbacks = {}) => {
2736
2561
  state = STATE_AFTER_DOCTYPE_SYSTEM_IDENTIFIER;
2737
2562
  pos++;
2738
2563
  } else if (cc === CC_NULL) {
2739
- // U+0000 NULL
2740
- // This is an unexpected-null-character parse error. Append a U+FFFD
2741
- // REPLACEMENT CHARACTER character to the current DOCTYPE token's system
2742
- // identifier.
2564
+ // NUL: unexpected-null-character parse error. Append a U+FFFD to the
2565
+ // DOCTYPE token's system identifier.
2743
2566
  reportError("unexpected-null-character", pos, pos + 1, "warning");
2744
2567
  pos++;
2745
2568
  doctypeSystemEnd = pos;
2746
2569
  } else if (cc === CC_GREATER_THAN) {
2747
- // U+003E GREATER-THAN SIGN (>)
2748
- // This is an abrupt-doctype-system-identifier parse error. Set the current
2749
- // DOCTYPE token's force-quirks flag to on. Switch to the data state. Emit
2750
- // the current DOCTYPE token.
2570
+ // `>`: abrupt-doctype-system-identifier parse error, force quirks, switch
2571
+ // to data. Emit the DOCTYPE token.
2751
2572
  reportError(
2752
2573
  "abrupt-doctype-system-identifier",
2753
2574
  pos,
@@ -2786,10 +2607,7 @@ const tokenize = (input, pos = 0, callbacks = {}) => {
2786
2607
  case STATE_AFTER_DOCTYPE_SYSTEM_IDENTIFIER:
2787
2608
  // Consume the next input character:
2788
2609
  if (isSpace(cc)) {
2789
- // U+0009 CHARACTER TABULATION (tab)
2790
- // U+000A LINE FEED (LF)
2791
- // U+000C FORM FEED (FF)
2792
- // U+0020 SPACE
2610
+ // Whitespace (tab, LF, FF, space)
2793
2611
  // Ignore the character.
2794
2612
  pos++;
2795
2613
  } else if (cc === CC_GREATER_THAN) {
@@ -2814,10 +2632,9 @@ const tokenize = (input, pos = 0, callbacks = {}) => {
2814
2632
  textStart = nextPos;
2815
2633
  pos = nextPos;
2816
2634
  } else {
2817
- // Anything else
2818
- // This is an unexpected-character-after-doctype-system-identifier parse
2819
- // error. Reconsume in the bogus DOCTYPE state. (This does not set the
2820
- // current DOCTYPE token's force-quirks flag to on.)
2635
+ // Anything else: unexpected-character-after-doctype-system-identifier
2636
+ // parse error, reconsume in bogus DOCTYPE. (This does not set the DOCTYPE
2637
+ // token's force-quirks flag to on.)
2821
2638
  reportError(
2822
2639
  "unexpected-character-after-doctype-system-identifier",
2823
2640
  pos,
@@ -2873,10 +2690,8 @@ const tokenize = (input, pos = 0, callbacks = {}) => {
2873
2690
  state = STATE_CDATA_SECTION_BRACKET;
2874
2691
  pos++;
2875
2692
  } else {
2876
- // Anything else
2877
- // Emit the current input character as a character token.
2878
- // Fast-forward to the next `]` (the only code point with its own
2879
- // arc) in one native scan.
2693
+ // Anything else: emit the input character. Fast-forwards to the next
2694
+ // `]`, the only code point with an arc of its own, in one scan.
2880
2695
  pos = input.indexOf("]", pos + 1);
2881
2696
  if (pos === -1) pos = len;
2882
2697
  }
@@ -2937,10 +2752,9 @@ const tokenize = (input, pos = 0, callbacks = {}) => {
2937
2752
  case STATE_RCDATA:
2938
2753
  // Consume the next input character:
2939
2754
  if (cc === CC_AMPERSAND) {
2940
- // U+0026 AMPERSAND (&)
2941
- // Set the return state to the RCDATA state. Switch to the
2942
- // character reference state. (RCDATA processes references;
2943
- // RAWTEXT/script/PLAINTEXT do not.)
2755
+ // `&`: Set the return state to the RCDATA state, switch to character
2756
+ // reference. (RCDATA processes references; RAWTEXT/script/PLAINTEXT do
2757
+ // not.)
2944
2758
  returnState = STATE_RCDATA;
2945
2759
  state = STATE_CHARACTER_REFERENCE;
2946
2760
  pos++;
@@ -2983,11 +2797,8 @@ const tokenize = (input, pos = 0, callbacks = {}) => {
2983
2797
 
2984
2798
  // https://html.spec.whatwg.org/multipage/parsing.html#rcdata-less-than-sign-state
2985
2799
  case STATE_RCDATA_LESS_THAN_SIGN:
2986
- // Consume the next input character:
2987
- // U+002F SOLIDUS (/)
2988
- // Switch to the RCDATA end tag open state. (Spec sets a
2989
- // temporary buffer here; we track the would-be content via
2990
- // offset ranges instead.)
2800
+ // `/`: switch to RCDATA end tag open. (Spec sets a temporary buffer here;
2801
+ // we track the would-be content via offset ranges instead.)
2991
2802
  if (cc === CC_SOLIDUS) {
2992
2803
  state = STATE_RCDATA_END_TAG_OPEN;
2993
2804
  pos++;
@@ -3002,10 +2813,8 @@ const tokenize = (input, pos = 0, callbacks = {}) => {
3002
2813
 
3003
2814
  // https://html.spec.whatwg.org/multipage/parsing.html#rcdata-end-tag-open-state
3004
2815
  case STATE_RCDATA_END_TAG_OPEN:
3005
- // Consume the next input character:
3006
- // ASCII alpha
3007
- // Create a new end tag token, set its tag name to the empty string.
3008
- // Reconsume in the RCDATA end tag name state.
2816
+ // ASCII alpha: Create a new end tag token, set its tag name to the empty
2817
+ // string, reconsume in RCDATA end tag name.
3009
2818
  if (isAsciiAlpha(cc)) {
3010
2819
  tagNameStart = pos;
3011
2820
  state = STATE_RCDATA_END_TAG_NAME;
@@ -3021,14 +2830,9 @@ const tokenize = (input, pos = 0, callbacks = {}) => {
3021
2830
 
3022
2831
  // https://html.spec.whatwg.org/multipage/parsing.html#rcdata-end-tag-name-state
3023
2832
  case STATE_RCDATA_END_TAG_NAME:
3024
- // Consume the next input character:
3025
- // U+0009 CHARACTER TABULATION (tab)
3026
- // U+000A LINE FEED (LF)
3027
- // U+000C FORM FEED (FF)
3028
- // U+0020 SPACE
3029
- // If the current end tag token is an appropriate end tag token, then switch
3030
- // to the before attribute name state. Otherwise, treat it as per the
3031
- // "anything else" entry below.
2833
+ // Whitespace (tab, LF, FF, space): If the current end tag token is an
2834
+ // appropriate end tag token, then switch to the before attribute name
2835
+ // state. Otherwise, treat it as per the "anything else" entry below.
3032
2836
  if (isSpace(cc)) {
3033
2837
  tagNameEnd = pos;
3034
2838
  if (
@@ -3047,10 +2851,9 @@ const tokenize = (input, pos = 0, callbacks = {}) => {
3047
2851
  // Reconsume
3048
2852
  }
3049
2853
  } else if (cc === CC_SOLIDUS) {
3050
- // U+002F SOLIDUS (/)
3051
- // If the current end tag token is an appropriate end tag token, then switch
3052
- // to the self-closing start tag state. Otherwise, treat it as per the
3053
- // "anything else" entry below.
2854
+ // `/`: If the current end tag token is an appropriate end tag token, then
2855
+ // switch to the self-closing start tag state. Otherwise, treat it as per
2856
+ // the "anything else" entry below.
3054
2857
  tagNameEnd = pos;
3055
2858
  if (
3056
2859
  rangeEqualsLowerCase(
@@ -3068,9 +2871,8 @@ const tokenize = (input, pos = 0, callbacks = {}) => {
3068
2871
  // Reconsume
3069
2872
  }
3070
2873
  } else if (cc === CC_GREATER_THAN) {
3071
- // U+003E GREATER-THAN SIGN (>)
3072
- // If the current end tag token is an appropriate end tag token, then switch
3073
- // to the data state and emit the current tag token. Otherwise, treat it as
2874
+ // `>`: If the current end tag token is an appropriate end tag token, then
2875
+ // switch to the data state and emit the tag token. Otherwise, treat it as
3074
2876
  // per the "anything else" entry below.
3075
2877
  tagNameEnd = pos;
3076
2878
  if (
@@ -3089,19 +2891,13 @@ const tokenize = (input, pos = 0, callbacks = {}) => {
3089
2891
  // Reconsume
3090
2892
  }
3091
2893
  } else if (isAsciiAlpha(cc)) {
3092
- // ASCII upper alpha / ASCII lower alpha
3093
- // Append the lowercase version of the current input character to the
3094
- // current tag token's tag name. Append the current input character to
3095
- // the temporary buffer.
3096
- // Fused: consume the whole alpha run in one dispatch.
2894
+ // ASCII alpha: append it lowercased to the tag name and as written to
2895
+ // the temporary buffer. Fused, so the whole run is one dispatch.
3097
2896
  pos++;
3098
2897
  while (pos < len && isAsciiAlpha(input.charCodeAt(pos))) pos++;
3099
2898
  } else {
3100
- // Anything else
3101
- // Emit a U+003C LESS-THAN SIGN character token, a U+002F SOLIDUS character
3102
- // token, and a character token for each of the characters in the temporary
3103
- // buffer (in the order they were added to the buffer). Reconsume in the
3104
- // RCDATA state.
2899
+ // Anything else: emit `<`, `/` and the temporary buffer in order,
2900
+ // then reconsume in RCDATA.
3105
2901
  state = STATE_RCDATA;
3106
2902
  // Reconsume
3107
2903
  }
@@ -3145,10 +2941,8 @@ const tokenize = (input, pos = 0, callbacks = {}) => {
3145
2941
 
3146
2942
  // https://html.spec.whatwg.org/multipage/parsing.html#rawtext-less-than-sign-state
3147
2943
  case STATE_RAWTEXT_LESS_THAN_SIGN:
3148
- // Consume the next input character:
3149
- // U+002F SOLIDUS (/)
3150
- // Switch to the RAWTEXT end tag open state. (Spec sets a
3151
- // temporary buffer here; we track via offset ranges instead.)
2944
+ // `/`: switch to RAWTEXT end tag open. (Spec sets a temporary buffer here;
2945
+ // we track via offset ranges instead.)
3152
2946
  if (cc === CC_SOLIDUS) {
3153
2947
  state = STATE_RAWTEXT_END_TAG_OPEN;
3154
2948
  pos++;
@@ -3163,10 +2957,8 @@ const tokenize = (input, pos = 0, callbacks = {}) => {
3163
2957
 
3164
2958
  // https://html.spec.whatwg.org/multipage/parsing.html#rawtext-end-tag-open-state
3165
2959
  case STATE_RAWTEXT_END_TAG_OPEN:
3166
- // Consume the next input character:
3167
- // ASCII alpha
3168
- // Create a new end tag token, set its tag name to the empty string.
3169
- // Reconsume in the RAWTEXT end tag name state.
2960
+ // ASCII alpha: Create a new end tag token, set its tag name to the empty
2961
+ // string, reconsume in RAWTEXT end tag name.
3170
2962
  if (isAsciiAlpha(cc)) {
3171
2963
  tagNameStart = pos;
3172
2964
  state = STATE_RAWTEXT_END_TAG_NAME;
@@ -3182,14 +2974,9 @@ const tokenize = (input, pos = 0, callbacks = {}) => {
3182
2974
 
3183
2975
  // https://html.spec.whatwg.org/multipage/parsing.html#rawtext-end-tag-name-state
3184
2976
  case STATE_RAWTEXT_END_TAG_NAME:
3185
- // Consume the next input character:
3186
- // U+0009 CHARACTER TABULATION (tab)
3187
- // U+000A LINE FEED (LF)
3188
- // U+000C FORM FEED (FF)
3189
- // U+0020 SPACE
3190
- // If the current end tag token is an appropriate end tag token, then switch
3191
- // to the before attribute name state. Otherwise, treat it as per the
3192
- // "anything else" entry below.
2977
+ // Whitespace (tab, LF, FF, space): If the current end tag token is an
2978
+ // appropriate end tag token, then switch to the before attribute name
2979
+ // state. Otherwise, treat it as per the "anything else" entry below.
3193
2980
  if (isSpace(cc)) {
3194
2981
  tagNameEnd = pos;
3195
2982
  if (
@@ -3207,10 +2994,9 @@ const tokenize = (input, pos = 0, callbacks = {}) => {
3207
2994
  state = STATE_RAWTEXT;
3208
2995
  }
3209
2996
  } else if (cc === CC_SOLIDUS) {
3210
- // U+002F SOLIDUS (/)
3211
- // If the current end tag token is an appropriate end tag token, then switch
3212
- // to the self-closing start tag state. Otherwise, treat it as per the
3213
- // "anything else" entry below.
2997
+ // `/`: If the current end tag token is an appropriate end tag token, then
2998
+ // switch to the self-closing start tag state. Otherwise, treat it as per
2999
+ // the "anything else" entry below.
3214
3000
  tagNameEnd = pos;
3215
3001
  if (
3216
3002
  rangeEqualsLowerCase(
@@ -3227,9 +3013,8 @@ const tokenize = (input, pos = 0, callbacks = {}) => {
3227
3013
  state = STATE_RAWTEXT;
3228
3014
  }
3229
3015
  } else if (cc === CC_GREATER_THAN) {
3230
- // U+003E GREATER-THAN SIGN (>)
3231
- // If the current end tag token is an appropriate end tag token, then switch
3232
- // to the data state and emit the current tag token. Otherwise, treat it as
3016
+ // `>`: If the current end tag token is an appropriate end tag token, then
3017
+ // switch to the data state and emit the tag token. Otherwise, treat it as
3233
3018
  // per the "anything else" entry below.
3234
3019
  tagNameEnd = pos;
3235
3020
  if (
@@ -3247,19 +3032,13 @@ const tokenize = (input, pos = 0, callbacks = {}) => {
3247
3032
  state = STATE_RAWTEXT;
3248
3033
  }
3249
3034
  } else if (isAsciiAlpha(cc)) {
3250
- // ASCII upper alpha / ASCII lower alpha
3251
- // Append the lowercase version of the current input character to the
3252
- // current tag token's tag name. Append the current input character to
3253
- // the temporary buffer.
3254
- // Fused: consume the whole alpha run in one dispatch.
3035
+ // ASCII alpha: append it lowercased to the tag name and as written to
3036
+ // the temporary buffer. Fused, so the whole run is one dispatch.
3255
3037
  pos++;
3256
3038
  while (pos < len && isAsciiAlpha(input.charCodeAt(pos))) pos++;
3257
3039
  } else {
3258
- // Anything else
3259
- // Emit a U+003C LESS-THAN SIGN character token, a U+002F SOLIDUS character
3260
- // token, and a character token for each of the characters in the temporary
3261
- // buffer (in the order they were added to the buffer). Reconsume in the
3262
- // RAWTEXT state.
3040
+ // Anything else: emit `<`, `/` and the temporary buffer in order,
3041
+ // then reconsume in RAWTEXT.
3263
3042
  state = STATE_RAWTEXT;
3264
3043
  // Reconsume
3265
3044
  }
@@ -3303,10 +3082,8 @@ const tokenize = (input, pos = 0, callbacks = {}) => {
3303
3082
 
3304
3083
  // https://html.spec.whatwg.org/multipage/parsing.html#script-data-less-than-sign-state
3305
3084
  case STATE_SCRIPT_DATA_LESS_THAN_SIGN:
3306
- // Consume the next input character:
3307
- // U+002F SOLIDUS (/)
3308
- // Switch to the script data end tag open state. (Spec sets a
3309
- // temporary buffer here; we track via offset ranges instead.)
3085
+ // `/`: switch to script data end tag open. (Spec sets a temporary buffer
3086
+ // here; we track via offset ranges instead.)
3310
3087
  if (cc === CC_SOLIDUS) {
3311
3088
  state = STATE_SCRIPT_DATA_END_TAG_OPEN;
3312
3089
  pos++;
@@ -3327,10 +3104,8 @@ const tokenize = (input, pos = 0, callbacks = {}) => {
3327
3104
 
3328
3105
  // https://html.spec.whatwg.org/multipage/parsing.html#script-data-end-tag-open-state
3329
3106
  case STATE_SCRIPT_DATA_END_TAG_OPEN:
3330
- // Consume the next input character:
3331
- // ASCII alpha
3332
- // Create a new end tag token, set its tag name to the empty string.
3333
- // Reconsume in the script data end tag name state.
3107
+ // ASCII alpha: Create a new end tag token, set its tag name to the empty
3108
+ // string, reconsume in script data end tag name.
3334
3109
  if (isAsciiAlpha(cc)) {
3335
3110
  tagNameStart = pos;
3336
3111
  state = STATE_SCRIPT_DATA_END_TAG_NAME;
@@ -3346,14 +3121,9 @@ const tokenize = (input, pos = 0, callbacks = {}) => {
3346
3121
 
3347
3122
  // https://html.spec.whatwg.org/multipage/parsing.html#script-data-end-tag-name-state
3348
3123
  case STATE_SCRIPT_DATA_END_TAG_NAME:
3349
- // Consume the next input character:
3350
- // U+0009 CHARACTER TABULATION (tab)
3351
- // U+000A LINE FEED (LF)
3352
- // U+000C FORM FEED (FF)
3353
- // U+0020 SPACE
3354
- // If the current end tag token is an appropriate end tag token, then switch
3355
- // to the before attribute name state. Otherwise, treat it as per the
3356
- // "anything else" entry below.
3124
+ // Whitespace (tab, LF, FF, space): If the current end tag token is an
3125
+ // appropriate end tag token, then switch to the before attribute name
3126
+ // state. Otherwise, treat it as per the "anything else" entry below.
3357
3127
  if (isSpace(cc)) {
3358
3128
  tagNameEnd = pos;
3359
3129
  if (
@@ -3371,10 +3141,9 @@ const tokenize = (input, pos = 0, callbacks = {}) => {
3371
3141
  state = STATE_SCRIPT_DATA;
3372
3142
  }
3373
3143
  } else if (cc === CC_SOLIDUS) {
3374
- // U+002F SOLIDUS (/)
3375
- // If the current end tag token is an appropriate end tag token, then switch
3376
- // to the self-closing start tag state. Otherwise, treat it as per the
3377
- // "anything else" entry below.
3144
+ // `/`: If the current end tag token is an appropriate end tag token, then
3145
+ // switch to the self-closing start tag state. Otherwise, treat it as per
3146
+ // the "anything else" entry below.
3378
3147
  tagNameEnd = pos;
3379
3148
  if (
3380
3149
  rangeEqualsLowerCase(
@@ -3391,9 +3160,8 @@ const tokenize = (input, pos = 0, callbacks = {}) => {
3391
3160
  state = STATE_SCRIPT_DATA;
3392
3161
  }
3393
3162
  } else if (cc === CC_GREATER_THAN) {
3394
- // U+003E GREATER-THAN SIGN (>)
3395
- // If the current end tag token is an appropriate end tag token, then switch
3396
- // to the data state and emit the current tag token. Otherwise, treat it as
3163
+ // `>`: If the current end tag token is an appropriate end tag token, then
3164
+ // switch to the data state and emit the tag token. Otherwise, treat it as
3397
3165
  // per the "anything else" entry below.
3398
3166
  tagNameEnd = pos;
3399
3167
  if (
@@ -3416,11 +3184,8 @@ const tokenize = (input, pos = 0, callbacks = {}) => {
3416
3184
  pos++;
3417
3185
  while (pos < len && isAsciiAlpha(input.charCodeAt(pos))) pos++;
3418
3186
  } else {
3419
- // Anything else
3420
- // Emit a U+003C LESS-THAN SIGN character token, a U+002F SOLIDUS character
3421
- // token, and a character token for each of the characters in the temporary
3422
- // buffer (in the order they were added to the buffer). Reconsume in the
3423
- // script data state.
3187
+ // Anything else: emit `<`, `/` and the temporary buffer in order,
3188
+ // then reconsume in script data.
3424
3189
  state = STATE_SCRIPT_DATA;
3425
3190
  // Reconsume
3426
3191
  }
@@ -3428,10 +3193,8 @@ const tokenize = (input, pos = 0, callbacks = {}) => {
3428
3193
 
3429
3194
  // https://html.spec.whatwg.org/multipage/parsing.html#script-data-escape-start-state
3430
3195
  case STATE_SCRIPT_DATA_ESCAPE_START:
3431
- // Consume the next input character:
3432
- // U+002D HYPHEN-MINUS (-)
3433
- // Switch to the script data escape start dash state. Emit a U+002D
3434
- // HYPHEN-MINUS character token.
3196
+ // `-`: switch to script data escape start dash. Emit a U+002D HYPHEN-MINUS
3197
+ // character token.
3435
3198
  if (cc === CC_HYPHEN_MINUS) {
3436
3199
  state = STATE_SCRIPT_DATA_ESCAPE_START_DASH;
3437
3200
  pos++;
@@ -3445,10 +3208,8 @@ const tokenize = (input, pos = 0, callbacks = {}) => {
3445
3208
 
3446
3209
  // https://html.spec.whatwg.org/multipage/parsing.html#script-data-escape-start-dash-state
3447
3210
  case STATE_SCRIPT_DATA_ESCAPE_START_DASH:
3448
- // Consume the next input character:
3449
- // U+002D HYPHEN-MINUS (-)
3450
- // Switch to the script data escaped dash dash state. Emit a U+002D
3451
- // HYPHEN-MINUS character token.
3211
+ // `-`: switch to script data escaped dash dash. Emit a U+002D HYPHEN-MINUS
3212
+ // character token.
3452
3213
  if (cc === CC_HYPHEN_MINUS) {
3453
3214
  state = STATE_SCRIPT_DATA_ESCAPED_DASH_DASH;
3454
3215
  pos++;
@@ -3462,9 +3223,7 @@ const tokenize = (input, pos = 0, callbacks = {}) => {
3462
3223
 
3463
3224
  // https://html.spec.whatwg.org/multipage/parsing.html#script-data-escaped-state
3464
3225
  case STATE_SCRIPT_DATA_ESCAPED:
3465
- // Consume the next input character:
3466
- // U+002D HYPHEN-MINUS (-)
3467
- // Switch to the script data escaped dash state. Emit a U+002D HYPHEN-MINUS
3226
+ // `-`: switch to script data escaped dash. Emit a U+002D HYPHEN-MINUS
3468
3227
  // character token.
3469
3228
  if (cc === CC_HYPHEN_MINUS) {
3470
3229
  state = STATE_SCRIPT_DATA_ESCAPED_DASH;
@@ -3501,10 +3260,8 @@ const tokenize = (input, pos = 0, callbacks = {}) => {
3501
3260
 
3502
3261
  // https://html.spec.whatwg.org/multipage/parsing.html#script-data-escaped-dash-state
3503
3262
  case STATE_SCRIPT_DATA_ESCAPED_DASH:
3504
- // Consume the next input character:
3505
- // U+002D HYPHEN-MINUS (-)
3506
- // Switch to the script data escaped dash dash state. Emit a U+002D
3507
- // HYPHEN-MINUS character token.
3263
+ // `-`: switch to script data escaped dash dash. Emit a U+002D HYPHEN-MINUS
3264
+ // character token.
3508
3265
  if (cc === CC_HYPHEN_MINUS) {
3509
3266
  state = STATE_SCRIPT_DATA_ESCAPED_DASH_DASH;
3510
3267
  pos++;
@@ -3559,18 +3316,15 @@ const tokenize = (input, pos = 0, callbacks = {}) => {
3559
3316
 
3560
3317
  // https://html.spec.whatwg.org/multipage/parsing.html#script-data-escaped-less-than-sign-state
3561
3318
  case STATE_SCRIPT_DATA_ESCAPED_LESS_THAN_SIGN:
3562
- // Consume the next input character:
3563
- // U+002F SOLIDUS (/)
3564
- // Switch to the script data escaped end tag open state.
3565
- // (Spec sets a temporary buffer; we track via offset ranges.)
3319
+ // `/`: switch to script data escaped end tag open. (Spec sets a temporary
3320
+ // buffer; we track via offset ranges.)
3566
3321
  if (cc === CC_SOLIDUS) {
3567
3322
  state = STATE_SCRIPT_DATA_ESCAPED_END_TAG_OPEN;
3568
3323
  pos++;
3569
3324
  } else if (isAsciiAlpha(cc)) {
3570
- // ASCII alpha
3571
- // Set the temporary buffer to the empty string. Emit a U+003C LESS-THAN
3572
- // SIGN character token. Reconsume in the script data double escape start
3573
- // state.
3325
+ // ASCII alpha: Set the temporary buffer to the empty string. Emit a U+003C
3326
+ // LESS-THAN SIGN character token, reconsume in script data double escape
3327
+ // start.
3574
3328
  scriptMatch = 0;
3575
3329
  state = STATE_SCRIPT_DATA_DOUBLE_ESCAPE_START;
3576
3330
  // Reconsume
@@ -3585,10 +3339,8 @@ const tokenize = (input, pos = 0, callbacks = {}) => {
3585
3339
 
3586
3340
  // https://html.spec.whatwg.org/multipage/parsing.html#script-data-escaped-end-tag-open-state
3587
3341
  case STATE_SCRIPT_DATA_ESCAPED_END_TAG_OPEN:
3588
- // Consume the next input character:
3589
- // ASCII alpha
3590
- // Create a new end tag token, set its tag name to the empty string.
3591
- // Reconsume in the script data escaped end tag name state.
3342
+ // ASCII alpha: Create a new end tag token, set its tag name to the empty
3343
+ // string, reconsume in script data escaped end tag name.
3592
3344
  if (isAsciiAlpha(cc)) {
3593
3345
  tagNameStart = pos;
3594
3346
  state = STATE_SCRIPT_DATA_ESCAPED_END_TAG_NAME;
@@ -3604,14 +3356,9 @@ const tokenize = (input, pos = 0, callbacks = {}) => {
3604
3356
 
3605
3357
  // https://html.spec.whatwg.org/multipage/parsing.html#script-data-escaped-end-tag-name-state
3606
3358
  case STATE_SCRIPT_DATA_ESCAPED_END_TAG_NAME:
3607
- // Consume the next input character:
3608
- // U+0009 CHARACTER TABULATION (tab)
3609
- // U+000A LINE FEED (LF)
3610
- // U+000C FORM FEED (FF)
3611
- // U+0020 SPACE
3612
- // If the current end tag token is an appropriate end tag token, then switch
3613
- // to the before attribute name state. Otherwise, treat it as per the
3614
- // "anything else" entry below.
3359
+ // Whitespace (tab, LF, FF, space): If the current end tag token is an
3360
+ // appropriate end tag token, then switch to the before attribute name
3361
+ // state. Otherwise, treat it as per the "anything else" entry below.
3615
3362
  if (isSpace(cc)) {
3616
3363
  tagNameEnd = pos;
3617
3364
  if (
@@ -3629,10 +3376,9 @@ const tokenize = (input, pos = 0, callbacks = {}) => {
3629
3376
  state = STATE_SCRIPT_DATA_ESCAPED;
3630
3377
  }
3631
3378
  } else if (cc === CC_SOLIDUS) {
3632
- // U+002F SOLIDUS (/)
3633
- // If the current end tag token is an appropriate end tag token, then switch
3634
- // to the self-closing start tag state. Otherwise, treat it as per the
3635
- // "anything else" entry below.
3379
+ // `/`: If the current end tag token is an appropriate end tag token, then
3380
+ // switch to the self-closing start tag state. Otherwise, treat it as per
3381
+ // the "anything else" entry below.
3636
3382
  tagNameEnd = pos;
3637
3383
  if (
3638
3384
  rangeEqualsLowerCase(
@@ -3649,9 +3395,8 @@ const tokenize = (input, pos = 0, callbacks = {}) => {
3649
3395
  state = STATE_SCRIPT_DATA_ESCAPED;
3650
3396
  }
3651
3397
  } else if (cc === CC_GREATER_THAN) {
3652
- // U+003E GREATER-THAN SIGN (>)
3653
- // If the current end tag token is an appropriate end tag token, then switch
3654
- // to the data state and emit the current tag token. Otherwise, treat it as
3398
+ // `>`: If the current end tag token is an appropriate end tag token, then
3399
+ // switch to the data state and emit the tag token. Otherwise, treat it as
3655
3400
  // per the "anything else" entry below.
3656
3401
  tagNameEnd = pos;
3657
3402
  if (
@@ -3674,11 +3419,8 @@ const tokenize = (input, pos = 0, callbacks = {}) => {
3674
3419
  pos++;
3675
3420
  while (pos < len && isAsciiAlpha(input.charCodeAt(pos))) pos++;
3676
3421
  } else {
3677
- // Anything else
3678
- // Emit a U+003C LESS-THAN SIGN character token, a U+002F SOLIDUS character
3679
- // token, and a character token for each of the characters in the temporary
3680
- // buffer (in the order they were added to the buffer). Reconsume in the
3681
- // script data escaped state.
3422
+ // Anything else: emit `<`, `/` and the temporary buffer in order,
3423
+ // then reconsume in script data escaped.
3682
3424
  state = STATE_SCRIPT_DATA_ESCAPED;
3683
3425
  // Reconsume
3684
3426
  }
@@ -3686,16 +3428,9 @@ const tokenize = (input, pos = 0, callbacks = {}) => {
3686
3428
 
3687
3429
  // https://html.spec.whatwg.org/multipage/parsing.html#script-data-double-escape-start-state
3688
3430
  case STATE_SCRIPT_DATA_DOUBLE_ESCAPE_START:
3689
- // Consume the next input character:
3690
- // U+0009 CHARACTER TABULATION (tab)
3691
- // U+000A LINE FEED (LF)
3692
- // U+000C FORM FEED (FF)
3693
- // U+0020 SPACE
3694
- // U+002F SOLIDUS (/)
3695
- // U+003E GREATER-THAN SIGN (>)
3696
- // If the temporary buffer is the string "script", then switch to the script
3697
- // data double escaped state. Otherwise, switch to the script data escaped
3698
- // state. Emit the current input character as a character token.
3431
+ // Whitespace, `/` or `>`: switch to script data double escaped where the
3432
+ // temporary buffer is "script", otherwise to script data escaped, and
3433
+ // emit the input character.
3699
3434
  if (isSpace(cc) || cc === CC_SOLIDUS || cc === CC_GREATER_THAN) {
3700
3435
  state =
3701
3436
  scriptMatch === 6
@@ -3703,10 +3438,9 @@ const tokenize = (input, pos = 0, callbacks = {}) => {
3703
3438
  : STATE_SCRIPT_DATA_ESCAPED;
3704
3439
  pos++;
3705
3440
  } else if (isAsciiUpperAlpha(cc) || isAsciiLowerAlpha(cc)) {
3706
- // ASCII alpha — advance the `"script"` match counter if the
3707
- // lowercase form matches the next expected char, otherwise
3708
- // snap to the sentinel so further chars can't revive a
3709
- // match. No buffer allocation.
3441
+ // ASCII alpha: advance the `"script"` match counter while the
3442
+ // lowercased char is the expected one, otherwise snap to the
3443
+ // sentinel so a later char cannot revive the match.
3710
3444
  const lower = isAsciiUpperAlpha(cc) ? cc + 0x20 : cc;
3711
3445
  if (scriptMatch < 6 && lower === "script".charCodeAt(scriptMatch)) {
3712
3446
  scriptMatch++;
@@ -3724,9 +3458,7 @@ const tokenize = (input, pos = 0, callbacks = {}) => {
3724
3458
 
3725
3459
  // https://html.spec.whatwg.org/multipage/parsing.html#script-data-double-escaped-state
3726
3460
  case STATE_SCRIPT_DATA_DOUBLE_ESCAPED:
3727
- // Consume the next input character:
3728
- // U+002D HYPHEN-MINUS (-)
3729
- // Switch to the script data double escaped dash state. Emit a U+002D
3461
+ // `-`: switch to script data double escaped dash. Emit a U+002D
3730
3462
  // HYPHEN-MINUS character token.
3731
3463
  if (cc === CC_HYPHEN_MINUS) {
3732
3464
  state = STATE_SCRIPT_DATA_DOUBLE_ESCAPED_DASH;
@@ -3763,9 +3495,7 @@ const tokenize = (input, pos = 0, callbacks = {}) => {
3763
3495
 
3764
3496
  // https://html.spec.whatwg.org/multipage/parsing.html#script-data-double-escaped-dash-state
3765
3497
  case STATE_SCRIPT_DATA_DOUBLE_ESCAPED_DASH:
3766
- // Consume the next input character:
3767
- // U+002D HYPHEN-MINUS (-)
3768
- // Switch to the script data double escaped dash dash state. Emit a U+002D
3498
+ // `-`: switch to script data double escaped dash dash. Emit a U+002D
3769
3499
  // HYPHEN-MINUS character token.
3770
3500
  if (cc === CC_HYPHEN_MINUS) {
3771
3501
  state = STATE_SCRIPT_DATA_DOUBLE_ESCAPED_DASH_DASH;
@@ -3821,10 +3551,8 @@ const tokenize = (input, pos = 0, callbacks = {}) => {
3821
3551
 
3822
3552
  // https://html.spec.whatwg.org/multipage/parsing.html#script-data-double-escaped-less-than-sign-state
3823
3553
  case STATE_SCRIPT_DATA_DOUBLE_ESCAPED_LESS_THAN_SIGN:
3824
- // Consume the next input character:
3825
- // U+002F SOLIDUS (/)
3826
- // Set the temporary buffer to the empty string. Switch to the script data
3827
- // double escape end state. Emit a U+002F SOLIDUS character token.
3554
+ // `/`: Set the temporary buffer to the empty string, switch to script data
3555
+ // double escape end. Emit a U+002F SOLIDUS character token.
3828
3556
  if (cc === CC_SOLIDUS) {
3829
3557
  scriptMatch = 0;
3830
3558
  state = STATE_SCRIPT_DATA_DOUBLE_ESCAPE_END;
@@ -3839,16 +3567,9 @@ const tokenize = (input, pos = 0, callbacks = {}) => {
3839
3567
 
3840
3568
  // https://html.spec.whatwg.org/multipage/parsing.html#script-data-double-escape-end-state
3841
3569
  case STATE_SCRIPT_DATA_DOUBLE_ESCAPE_END:
3842
- // Consume the next input character:
3843
- // U+0009 CHARACTER TABULATION (tab)
3844
- // U+000A LINE FEED (LF)
3845
- // U+000C FORM FEED (FF)
3846
- // U+0020 SPACE
3847
- // U+002F SOLIDUS (/)
3848
- // U+003E GREATER-THAN SIGN (>)
3849
- // If the temporary buffer is the string "script", then switch to the script
3850
- // data escaped state. Otherwise, switch to the script data double escaped
3851
- // state. Emit the current input character as a character token.
3570
+ // Whitespace, `/` or `>`: switch to script data escaped where the
3571
+ // temporary buffer is "script", otherwise to script data double escaped,
3572
+ // and emit the input character.
3852
3573
  if (isSpace(cc) || cc === CC_SOLIDUS || cc === CC_GREATER_THAN) {
3853
3574
  state =
3854
3575
  scriptMatch === 6
@@ -3856,10 +3577,9 @@ const tokenize = (input, pos = 0, callbacks = {}) => {
3856
3577
  : STATE_SCRIPT_DATA_DOUBLE_ESCAPED;
3857
3578
  pos++;
3858
3579
  } else if (isAsciiUpperAlpha(cc) || isAsciiLowerAlpha(cc)) {
3859
- // ASCII alpha — advance the `"script"` match counter if the
3860
- // lowercase form matches the next expected char, otherwise
3861
- // snap to the sentinel so further chars can't revive a
3862
- // match. No buffer allocation.
3580
+ // ASCII alpha: advance the `"script"` match counter while the
3581
+ // lowercased char is the expected one, otherwise snap to the
3582
+ // sentinel so a later char cannot revive the match.
3863
3583
  const lower = isAsciiUpperAlpha(cc) ? cc + 0x20 : cc;
3864
3584
  if (scriptMatch < 6 && lower === "script".charCodeAt(scriptMatch)) {
3865
3585
  scriptMatch++;
@@ -3904,10 +3624,8 @@ const tokenize = (input, pos = 0, callbacks = {}) => {
3904
3624
  state = STATE_NAMED_CHARACTER_REFERENCE;
3905
3625
  // Reconsume
3906
3626
  } else if (cc === CC_NUMBER_SIGN) {
3907
- // U+0023 NUMBER SIGN (#)
3908
- // Append the current input character to the temporary buffer.
3909
- // Set the character reference code to zero. Switch to the
3910
- // numeric character reference state.
3627
+ // `#`: Append the input character to the temporary buffer. Set the
3628
+ // character reference code to zero, switch to numeric character reference.
3911
3629
  charRefCode = 0;
3912
3630
  state = STATE_NUMERIC_CHARACTER_REFERENCE;
3913
3631
  pos++;
@@ -3922,16 +3640,9 @@ const tokenize = (input, pos = 0, callbacks = {}) => {
3922
3640
 
3923
3641
  // https://html.spec.whatwg.org/multipage/parsing.html#named-character-reference-state
3924
3642
  case STATE_NAMED_CHARACTER_REFERENCE: {
3925
- // Consume the maximum number of characters possible where the
3926
- // consumed characters are one of the identifiers in the first
3927
- // column of the named character references table.
3928
- //
3929
- // We measure the longest run of ASCII alphanumeric characters
3930
- // (capped at MAX_ENTITY_NAME_LEN - 1 since the optional `;` is
3931
- // handled separately), then walk that run from longest to
3932
- // shortest looking for the first prefix that exists in the
3933
- // entity table (with a trailing `;` if present, otherwise the
3934
- // legacy bare form).
3643
+ // Consume the longest run naming a character reference: measure the
3644
+ // alphanumeric run, capped at MAX_ENTITY_NAME_LEN - 1 as the `;` is
3645
+ // separate, then walk it longest-first for the first prefix in the table.
3935
3646
  let runLen = 0;
3936
3647
  while (
3937
3648
  pos + runLen < len &&
@@ -3966,11 +3677,9 @@ const tokenize = (input, pos = 0, callbacks = {}) => {
3966
3677
  }
3967
3678
  }
3968
3679
  if (namedEntityConsumed > 0) {
3969
- // A legacy match without a trailing `;` is a
3970
- // missing-semicolon-after-character-reference parse error,
3971
- // except for the spec's historical attribute rule: when
3972
- // consumed in an attribute value and the next char is `=` or
3973
- // ASCII alphanumeric, the reference is left undecoded silently.
3680
+ // A legacy match without its `;` is a
3681
+ // missing-semicolon-after-character-reference parse error, except
3682
+ // where the spec's attribute rule silences it.
3974
3683
  if (!matchedWithSemicolon) {
3975
3684
  const next = input.charCodeAt(pos + namedEntityConsumed);
3976
3685
  const inAttribute =
@@ -4003,11 +3712,8 @@ const tokenize = (input, pos = 0, callbacks = {}) => {
4003
3712
  case STATE_AMBIGUOUS_AMPERSAND:
4004
3713
  // Consume the next input character:
4005
3714
  if (isAsciiAlphanumeric(cc)) {
4006
- // ASCII alphanumeric
4007
- // If the character reference was consumed as part of an
4008
- // attribute, then append the current input character to the
4009
- // current attribute's value. Otherwise, emit the current
4010
- // input character as a character token.
3715
+ // ASCII alphanumeric: append the input character to the attribute's
3716
+ // value where the reference was consumed in one, otherwise emit it.
4011
3717
  pos++;
4012
3718
  } else if (cc === CC_SEMICOLON) {
4013
3719
  // U+003B SEMICOLON (;)
@@ -4034,11 +3740,8 @@ const tokenize = (input, pos = 0, callbacks = {}) => {
4034
3740
  // Set the character reference code to zero (0).
4035
3741
  // Consume the next input character:
4036
3742
  if (cc === 0x78 || cc === 0x58) {
4037
- // U+0078 LATIN SMALL LETTER X
4038
- // U+0058 LATIN CAPITAL LETTER X
4039
- // Append the current input character to the temporary
4040
- // buffer. Switch to the hexadecimal character reference
4041
- // start state.
3743
+ // U+0078 / U+0058: Append the input character to the temporary buffer,
3744
+ // switch to hexadecimal character reference start.
4042
3745
  state = STATE_HEXADECIMAL_CHARACTER_REFERENCE_START;
4043
3746
  pos++;
4044
3747
  } else {
@@ -4051,11 +3754,9 @@ const tokenize = (input, pos = 0, callbacks = {}) => {
4051
3754
 
4052
3755
  // https://html.spec.whatwg.org/multipage/parsing.html#hexadecimal-character-reference-start-state
4053
3756
  case STATE_HEXADECIMAL_CHARACTER_REFERENCE_START:
4054
- // Consume the next input character:
4055
- // ASCII hex digit: reconsume in the hexadecimal character reference state.
3757
+ // ASCII hex digit: reconsume in hexadecimal character reference.
4056
3758
  // Anything else: absence-of-digits-in-numeric-character-reference parse
4057
- // error. Flush code points consumed as a character reference. Reconsume
4058
- // in the return state.
3759
+ // error, flush the consumed code points and reconsume in the return.
4059
3760
  if (isAsciiHexDigit(cc)) {
4060
3761
  state = STATE_HEXADECIMAL_CHARACTER_REFERENCE;
4061
3762
  } else {
@@ -4072,11 +3773,9 @@ const tokenize = (input, pos = 0, callbacks = {}) => {
4072
3773
 
4073
3774
  // https://html.spec.whatwg.org/multipage/parsing.html#decimal-character-reference-start-state
4074
3775
  case STATE_DECIMAL_CHARACTER_REFERENCE_START:
4075
- // Consume the next input character:
4076
- // ASCII digit: reconsume in the decimal character reference state.
4077
- // Anything else: absence-of-digits-in-numeric-character-reference parse
4078
- // error. Flush code points consumed as a character reference. Reconsume
4079
- // in the return state.
3776
+ // ASCII digit: reconsume in the decimal character reference state. Anything
3777
+ // else: absence-of-digits-in-numeric-character-reference parse error. Flush
3778
+ // code points consumed as a character reference, reconsume in return.
4080
3779
  if (isAsciiDigit(cc)) {
4081
3780
  state = STATE_DECIMAL_CHARACTER_REFERENCE;
4082
3781
  } else {
@@ -4095,12 +3794,9 @@ const tokenize = (input, pos = 0, callbacks = {}) => {
4095
3794
  case STATE_HEXADECIMAL_CHARACTER_REFERENCE:
4096
3795
  // Consume the next input character:
4097
3796
  if (isAsciiHexDigit(cc)) {
4098
- // ASCII digit / upper hex / lower hex
4099
- // Multiply the character reference code by 16. Add a numeric
4100
- // version of the current input character to the character
4101
- // reference code. Stop accumulating once past the Unicode
4102
- // range so the value can't overflow (still flags as
4103
- // outside-range at the end).
3797
+ // ASCII hex digit: multiply the reference code by 16 and add the
3798
+ // input character's value. Accumulation stops past the Unicode range
3799
+ // so it cannot overflow, and still flags as outside-range.
4104
3800
  if (charRefCode < 0x110000) {
4105
3801
  const v = cc <= 0x39 ? cc - 0x30 : (cc | 0x20) - 0x61 + 10;
4106
3802
  charRefCode = charRefCode * 16 + v;
@@ -4112,10 +3808,8 @@ const tokenize = (input, pos = 0, callbacks = {}) => {
4112
3808
  state = STATE_NUMERIC_CHARACTER_REFERENCE_END;
4113
3809
  pos++;
4114
3810
  } else {
4115
- // Anything else
4116
- // This is a missing-semicolon-after-character-reference
4117
- // parse error. Reconsume in the numeric character reference
4118
- // end state.
3811
+ // Anything else: missing-semicolon-after-character-reference parse error,
3812
+ // reconsume in numeric character reference end.
4119
3813
  reportError(
4120
3814
  "missing-semicolon-after-character-reference",
4121
3815
  pos,
@@ -4131,12 +3825,9 @@ const tokenize = (input, pos = 0, callbacks = {}) => {
4131
3825
  case STATE_DECIMAL_CHARACTER_REFERENCE:
4132
3826
  // Consume the next input character:
4133
3827
  if (isAsciiDigit(cc)) {
4134
- // ASCII digit
4135
- // Multiply the character reference code by 10. Add a numeric
4136
- // version of the current input character (subtract 0x0030
4137
- // from the character's code point) to the character reference
4138
- // code. Stop accumulating once past the Unicode range so the
4139
- // value can't overflow (still flags as outside-range).
3828
+ // ASCII digit: multiply the reference code by 10 and add the input
3829
+ // character's value. Accumulation stops past the Unicode range so it
3830
+ // cannot overflow, and still flags as outside-range.
4140
3831
  if (charRefCode < 0x110000) {
4141
3832
  charRefCode = charRefCode * 10 + (cc - 0x30);
4142
3833
  }
@@ -4147,10 +3838,8 @@ const tokenize = (input, pos = 0, callbacks = {}) => {
4147
3838
  state = STATE_NUMERIC_CHARACTER_REFERENCE_END;
4148
3839
  pos++;
4149
3840
  } else {
4150
- // Anything else
4151
- // This is a missing-semicolon-after-character-reference
4152
- // parse error. Reconsume in the numeric character reference
4153
- // end state.
3841
+ // Anything else: missing-semicolon-after-character-reference parse error,
3842
+ // reconsume in numeric character reference end.
4154
3843
  reportError(
4155
3844
  "missing-semicolon-after-character-reference",
4156
3845
  pos,
@@ -4179,17 +3868,13 @@ const tokenize = (input, pos = 0, callbacks = {}) => {
4179
3868
  }
4180
3869
  }
4181
3870
 
4182
- // Handle EOF in non-data states per the WHATWG spec.
4183
- //
4184
- // Each in-progress comment / doctype / cdata / tag emits its partial
4185
- // token range plus a corresponding `eof-in-X` parse error. Severity is
4186
- // `"error"` because the emitted token offset range is incomplete (missing
4187
- // trailing `-->`, `>`, `]]>`, etc.). For data / `<` / `</` / `<!`-only
4188
- // inputs we emit `eof-before-tag-name` and fall through to flush the
4189
- // pending text span (which still contains the lone `<`).
4190
- // EOF inside a character-reference state: run the end-of-reference
4191
- // processing (numeric end states never run when the reference ends at
4192
- // EOF), then resume in the return state for the branches below.
3871
+ // WHY: EOF in a non-data state, per the WHATWG spec. An in-progress comment,
3872
+ // doctype, cdata or tag emits its partial range and an `eof-in-X` parse
3873
+ // error, at severity `"error"` because that range is missing its terminator.
3874
+ // A data / `<` / `</` / `<!` input emits `eof-before-tag-name` and falls
3875
+ // through to flush the pending text, which still holds the lone `<`. Inside a
3876
+ // character reference, the end-of-reference processing runs first — the
3877
+ // numeric end states never do at EOF — then the return state's branch below.
4193
3878
  if (
4194
3879
  state >= STATE_CHARACTER_REFERENCE &&
4195
3880
  state <= STATE_NUMERIC_CHARACTER_REFERENCE_END
@@ -4231,10 +3916,9 @@ const tokenize = (input, pos = 0, callbacks = {}) => {
4231
3916
  state === STATE_SCRIPT_DATA_END_TAG_NAME ||
4232
3917
  state === STATE_SCRIPT_DATA_ESCAPED_END_TAG_NAME
4233
3918
  ) {
4234
- // EOF mid-tag — emit the partial open/close tag at EOF so the
4235
- // consumer still sees the tag. This is a deliberate deviation
4236
- // from the spec's per-character emission model: rather than
4237
- // dropping the in-progress tag, we emit its offset range up to EOF.
3919
+ // A deliberate deviation from the spec's per-character emission
3920
+ // model — the in-progress tag is emitted as its offset range up to EOF
3921
+ // rather than dropped, so the consumer still sees it.
4238
3922
  reportError("eof-in-tag", len, len, "error");
4239
3923
  // If we hit EOF mid-attribute-name, the name runs to EOF. Set
4240
3924
  // attributeNameEnd here so the emitted attribute range is valid.
@@ -4242,11 +3926,9 @@ const tokenize = (input, pos = 0, callbacks = {}) => {
4242
3926
  attributeNameEnd = len;
4243
3927
  }
4244
3928
  if (attributeNameStart !== -1) emitAttribute(len);
4245
- // If we hit EOF before the tag-name end was recorded, the name runs
4246
- // to EOF. `tagNameEnd` may carry over from a previously emitted tag,
4247
- // so reset it whenever it's missing or stale (less than `tagNameStart`)
4248
- // — covers `<div` open-tag EOFs as well as `<title>x</tit` and other
4249
- // content-mode end-tag-name EOFs.
3929
+ // At EOF before the tag-name end was recorded the name runs to EOF, and
3930
+ // `tagNameEnd` may carry over from a previous tag — so it is reset
3931
+ // whenever missing or stale, below `tagNameStart`.
4250
3932
  if (tagNameStart !== -1 && tagNameEnd < tagNameStart) {
4251
3933
  tagNameEnd = len;
4252
3934
  }
@@ -4319,11 +4001,9 @@ const tokenize = (input, pos = 0, callbacks = {}) => {
4319
4001
  state === STATE_SCRIPT_DATA_DOUBLE_ESCAPED_LESS_THAN_SIGN ||
4320
4002
  state === STATE_SCRIPT_DATA_DOUBLE_ESCAPE_END
4321
4003
  ) {
4322
- // Inside `<script><!-- … ` at EOF — spec calls this an
4323
- // eof-in-script-html-comment-like-text parse error. The
4324
- // less-than-sign and double-escape-end states reconsume back
4325
- // into the (double-)escaped state on EOF per spec, which then
4326
- // hits this same error.
4004
+ // Inside `<script><!-- … ` at EOF: an
4005
+ // eof-in-script-html-comment-like-text parse error. The less-than-sign
4006
+ // and double-escape-end states reconsume into it at EOF, per spec.
4327
4007
  reportError("eof-in-script-html-comment-like-text", len, len, "error");
4328
4008
  } else if (state === STATE_TAG_OPEN || state === STATE_END_TAG_OPEN) {
4329
4009
  // `<` or `</` with nothing after; spec calls this
@@ -4339,10 +4019,9 @@ const tokenize = (input, pos = 0, callbacks = {}) => {
4339
4019
  return pos;
4340
4020
  };
4341
4021
 
4342
- // WHATWG numeric-character-reference-end Windows-1252 remap table for the
4343
- // 0x80-0x9F range. Per spec these C1 control code points decode to the
4344
- // corresponding Windows-1252 glyph (with a parse error) rather than to the
4345
- // raw C1 control character.
4022
+ // WHATWG numeric-character-reference-end remap for 0x80-0x9F: these C1 control
4023
+ // code points decode to the matching Windows-1252 glyph, with a parse error,
4024
+ // rather than to the control character itself.
4346
4025
  const NUMERIC_C1_REMAP = {
4347
4026
  0x80: "€",
4348
4027
  0x82: "‚",
@@ -4378,12 +4057,10 @@ const NUMERIC_C1_REMAP = {
4378
4057
  * @returns {string} decoded character per WHATWG remap rules
4379
4058
  */
4380
4059
  const decodeNumericReference = (code) => {
4381
- // Per WHATWG numeric-character-reference-end-state:
4382
- // - 0x00, > 0x10FFFF, or surrogate (0xD800-0xDFFF) -> U+FFFD.
4383
- // - 0x80-0x9F -> Windows-1252 remap (above).
4384
- // - Anything else (including noncharacters and C0 controls) -> the
4385
- // code point itself; we don't surface the spec's parse-error
4386
- // classes here since decoding is happening after the scanner ran.
4060
+ // WHY: numeric-character-reference-end-state maps 0x00, anything above
4061
+ // 0x10FFFF and the surrogates to U+FFFD, 0x80-0x9F through the table above,
4062
+ // and everything else — noncharacters and C0 controls included — to itself.
4063
+ // Its parse-error classes are not raised here: the scanner has already run.
4387
4064
  if (code === 0 || code > 0x10ffff || (code >= 0xd800 && code <= 0xdfff)) {
4388
4065
  return "�";
4389
4066
  }
@@ -4419,12 +4096,9 @@ const decodeNamedReference = (str, start, end, nextCharCode, isAttribute) => {
4419
4096
  const name = str.slice(start + 1, end);
4420
4097
  const matchEndsWithSemi = name.charCodeAt(name.length - 1) === 0x3b;
4421
4098
 
4422
- // Attribute-context guard: if the entity match didn't end with `;`
4423
- // and the next character in the source is `=` or ASCII
4424
- // alphanumeric, the WHATWG spec says to flush the literal text
4425
- // rather than decode. The greedy scan already absorbed any
4426
- // trailing alphanumerics, so the only candidate "next char" here
4427
- // is `=` (or any non-alphanumeric).
4099
+ // Attribute-context guard: a match not ending in `;` whose next source
4100
+ // character is `=` or alphanumeric is flushed literally rather than decoded.
4101
+ // The greedy scan absorbed the alphanumerics, so only `=` can reach here.
4428
4102
  if (isAttribute && !matchEndsWithSemi && nextCharCode === 0x3d /* = */) {
4429
4103
  return undefined;
4430
4104
  }
@@ -4436,11 +4110,9 @@ const decodeNamedReference = (str, start, end, nextCharCode, isAttribute) => {
4436
4110
  if (full !== undefined) return full;
4437
4111
  }
4438
4112
 
4439
- // Cap the longest-prefix search at MAX_ENTITY_NAME_LEN so pathological
4440
- // inputs like `&` + thousands of alphanumerics stay linear-time.
4441
- // Anything past that cap can't possibly match and is appended
4442
- // verbatim as part of `name.slice(i)`. The full-length case was just
4443
- // handled above, so start one shorter when it's the cap.
4113
+ // Capping the longest-prefix search at MAX_ENTITY_NAME_LEN keeps `&` plus
4114
+ // thousands of alphanumerics linear-time; nothing past it can match, so it
4115
+ // rides along in `name.slice(i)`. The full length was handled just above.
4444
4116
  const searchLen =
4445
4117
  name.length > MAX_ENTITY_NAME_LEN ? MAX_ENTITY_NAME_LEN : name.length - 1;
4446
4118
  for (let i = searchLen; i > 0; i--) {
@@ -4501,13 +4173,12 @@ const decodeNamedReference = (str, start, end, nextCharCode, isAttribute) => {
4501
4173
  * @returns {string | DecodedEntitiesWithMap} decoded string (with map when `withMap`)
4502
4174
  */
4503
4175
  const decodeEntities = (str, isAttribute, withMap) => {
4504
- // Hand-rolled scan of one of three reference forms, each with an optional
4505
- // trailing `;` — `&#x<hex>+` / `&#<dec>+` / `&<alpha><alnum>*` (kept separate
4506
- // so `&#65b` doesn't eat the `b` as hex): entity-dense text pays no regex
4507
- // machinery or per-match object, and a string whose references all stay
4508
- // literal is returned unchanged without rebuilding. Map bookkeeping is
4509
- // gated on `withMap` at the rare per-decoded-reference points, so the
4510
- // dominant no-map mode pays nothing for the unified implementation.
4176
+ // WHY: a hand-rolled scan of the three reference forms — `&#x<hex>+`,
4177
+ // `&#<dec>+`, `&<alpha><alnum>*`, kept apart so `&#65b` does not eat the `b`
4178
+ // as hex. Entity-dense text then pays no regex machinery and no per-match
4179
+ // object, and a string whose references all stay literal is returned as it
4180
+ // came. Map bookkeeping is gated on `withMap` at the rare per-reference
4181
+ // points, so the dominant no-map mode pays nothing for sharing this code.
4511
4182
  let amp = str.indexOf("&");
4512
4183
  if (amp === -1) return withMap ? { text: str, map: undefined } : str;
4513
4184
  let out = "";
@@ -4515,10 +4186,9 @@ const decodeEntities = (str, isAttribute, withMap) => {
4515
4186
  /** @type {number[] | undefined} */
4516
4187
  let map;
4517
4188
  do {
4518
- // `end` stays 0 when the `&` doesn't start a well-formed reference; a
4519
- // numeric reference's code point accumulates during the digit scan
4520
- // (saturating above the code-point range — every overflow decodes the
4521
- // same replacement) so it needs no slice at all.
4189
+ // `end` stays 0 where the `&` starts no well-formed reference. A numeric
4190
+ // reference accumulates during the digit scan, saturating above the
4191
+ // code-point range since every overflow decodes alike, so it needs no slice.
4522
4192
  let end = 0;
4523
4193
  let code = -1;
4524
4194
  let cc = str.charCodeAt(amp + 1);
@@ -4642,17 +4312,9 @@ const escapeAttribute = (s, delimiter, minimal) => {
4642
4312
  switch (s.charCodeAt(i)) {
4643
4313
  case CC_AMPERSAND: {
4644
4314
  // §13.2.5.41: a `&` the parser cannot continue into a reference is
4645
- // data. The minifier writes it bare; generated markup escapes it.
4646
- const next = s.charCodeAt(i + 1);
4647
- if (
4648
- minimal === true &&
4649
- next !== CC_NUMBER_SIGN &&
4650
- !(
4651
- (next >= 0x30 && next <= 0x39) ||
4652
- (next >= 0x41 && next <= 0x5a) ||
4653
- (next >= 0x61 && next <= 0x7a)
4654
- )
4655
- ) {
4315
+ // data — asked of the table, since `&c` names none. The minifier
4316
+ // writes it bare; generated markup escapes it.
4317
+ if (minimal === true && !_startsCharacterReference(s, i, true)) {
4656
4318
  continue;
4657
4319
  }
4658
4320
  rep = "&amp;";
@@ -4827,23 +4489,20 @@ const EMPTY_ATTRS = /** @type {AttributeRun} */ (
4827
4489
  Object.freeze({ start: 0, count: 0 })
4828
4490
  );
4829
4491
 
4830
- // === Struct-of-arrays AST backend ===
4831
- // One AST node = one integer id (`HtmlNodeRef`) indexing the parallel columns
4832
- // below — no per-node object and no per-parent children array. Tree shape
4833
- // lives in the four link columns (parent / firstChild / lastChild /
4834
- // nextSibling); the only heap references are the string payload / attribute
4835
- // name-and-value side arrays. Columns are module-level and reused
4836
- // across parses (grown, shrunk on release only past `_COLUMN_SHRINK_CAPACITY`,
4837
- // mirroring the CSS parser's column backend), so a steady-state parse
4838
- // allocates almost nothing per node; consumers must fully read a tree before
4839
- // the next `parseHtml` call. Id 0 is reserved as "no node" so the link columns
4840
- // can use 0 as null.
4492
+ // WHY: struct-of-arrays AST backend. One node is one integer id
4493
+ // (`HtmlNodeRef`) into the parallel columns below, so there is no per-node
4494
+ // object and no per-parent children array: shape lives in the four link
4495
+ // columns, and the only heap references are the payload and attribute side
4496
+ // arrays. The columns are module-level and reused across parses — grown, and
4497
+ // shrunk on release only past `_COLUMN_SHRINK_CAPACITY` — so a steady-state
4498
+ // parse allocates almost nothing per node, at the price of a tree being valid
4499
+ // only until the next `parseHtml`. Id 0 is reserved so the links can use it as
4500
+ // null.
4841
4501
  let _nodeCapacity = 0;
4842
4502
  let _nodeCount = 0;
4843
- // Highest id `_nodeCount` ever reached this parse. Streaming rewinds the
4844
- // allocator, so `_nodeCount` alone no longer bounds the ids that were written —
4845
- // the release pass has to clear up to here or the recycled slots keep their
4846
- // strings alive until a later parse happens to overwrite them.
4503
+ // Highest id `_nodeCount` reached this parse. Streaming rewinds the allocator,
4504
+ // so `_nodeCount` no longer bounds the ids written, and the release pass has to
4505
+ // clear up to here or a recycled slot keeps its strings alive.
4847
4506
  let _nodeHighWater = 0;
4848
4507
  /** `NodeType` per node */
4849
4508
  let _nodeTypes = new Uint8Array(0);
@@ -4917,12 +4576,10 @@ const _templateContentOf = (el) =>
4917
4576
  const _namespaceOf = (el) => _nodeFlags[el] & NS_MASK;
4918
4577
 
4919
4578
  // === Attribute columns ===
4920
- // One attribute = one integer id into these columns; an element (and a
4921
- // start-tag token) holds a contiguous run. The value string is derived from
4922
- // the source by offset on read — `_attrValues` carries an override only for
4923
- // valueless attributes (`""`) and offset-less adoption-agency clones — and the
4924
- // html5lib serializer name is derived from the adjusted name plus one flag
4925
- // bit, so per attribute only the interned name pointer is retained.
4579
+
4580
+ // One attribute is one integer id here, and an element holds a contiguous run.
4581
+ // Values are read from the source by offset and the serializer name from the
4582
+ // adjusted name, so only the name pointer is retained per attribute.
4926
4583
  let _attrCapacity = 0;
4927
4584
  let _attrCount = 0;
4928
4585
  // Elements a repeated `<html>` / `<body>` start tag merged attributes onto
@@ -5038,6 +4695,15 @@ const _attrValueOf = (i) => {
5038
4695
  : _htmlSource.slice(_attrValueStarts[i], _attrValueEnds[i]);
5039
4696
  };
5040
4697
 
4698
+ /**
4699
+ * An attribute's value as the element is handed it. The tree construction
4700
+ * matches values the tokenizer has already resolved references in, so how one
4701
+ * is spelled must not decide which branch a token takes.
4702
+ * @param {number} i the attribute
4703
+ * @returns {string} the value with its character references resolved
4704
+ */
4705
+ const _attrValueDecoded = (i) => decodeEntities(_attrValueOf(i), true);
4706
+
5041
4707
  // Linear name lookup in a run — attribute lists are short, a loop beats a Map.
5042
4708
  /** @type {(start: number, count: number, name: string) => number} */
5043
4709
  const _findAttr = (start, count, name) => {
@@ -5061,10 +4727,9 @@ const _copyAttr = (i) => {
5061
4727
  return c;
5062
4728
  };
5063
4729
 
5064
- // html5lib serializer name, derived: a `FOREIGN_ATTR_NS`-adjusted attribute is
5065
- // flagged (its name is the table key), and a camelCase-adjusted name contains
5066
- // an uppercase letter (unadjusted names are always lowercased), serializing as
5067
- // itself. Everything else serializes as the plain name (undefined here).
4730
+ // Derived html5lib serializer name: a `FOREIGN_ATTR_NS`-adjusted attribute is
4731
+ // flagged and keyed by its name, a camelCase-adjusted one carries an uppercase
4732
+ // letter and serializes as itself, and everything else is the plain name.
5068
4733
  /** @type {(i: number) => string | undefined} */
5069
4734
  const _attrSerializedName = (i) => {
5070
4735
  if ((_attrFlags[i] & 1) !== 0) return FOREIGN_ATTR_NS[_attrNames[i]];
@@ -5159,10 +4824,9 @@ const _resetAstColumns = () => {
5159
4824
  _doctypeSystemId = null;
5160
4825
  };
5161
4826
 
5162
- // The typed-array columns grow to the largest document ever parsed; above this
5163
- // capacity they are re-shrunk on release so one pathological file can't pin
5164
- // tens of MB at module level for the process lifetime (~46 bytes/node,
5165
- // ~17 bytes/attribute of capacity across the columns).
4827
+ // The typed-array columns grow to the largest document parsed, and are shrunk
4828
+ // on release above this capacity so one pathological file cannot pin tens of MB
4829
+ // at module level for the process lifetime.
5166
4830
  const _COLUMN_SHRINK_CAPACITY = 65536;
5167
4831
 
5168
4832
  // Release the side arrays' heap references (strings, attribute names/values)
@@ -5531,10 +5195,9 @@ const buildNameInternTable = (names) => {
5531
5195
  hashes[slot] = h;
5532
5196
  values[slot] = name;
5533
5197
  }
5534
- // `memo` short-circuits the hash walk for a name seen a moment ago (see
5535
- // `internLowerName`); "" marks an empty slot so the load stays monomorphic.
5536
- // `sourceBacked` lists the slots holding a slice of the parsed input — the
5537
- // only ones the next parse drops — and `noted` keeps each in it once.
5198
+ // `memo` short-circuits the hash walk for a name just seen, with `""` as the
5199
+ // empty slot so the load stays monomorphic. `sourceBacked` lists the slots
5200
+ // holding a slice of the input — the only ones the next parse drops.
5538
5201
  return {
5539
5202
  mask,
5540
5203
  hashes,
@@ -5583,10 +5246,9 @@ const _dropSourceBackedNames = (table) => {
5583
5246
  * @returns {string} lowercased name
5584
5247
  */
5585
5248
  const internLowerName = (table, input, start, end) => {
5586
- // A document spells the same few names over and over, so a direct-mapped
5587
- // memo keyed on folded first char + length answers most calls with one
5588
- // verified compare instead of the full hash walk. Every hit re-verifies
5589
- // against the current range, so a collision only ever misses, never lies.
5249
+ // A document spells the same few names over and over, so a direct-mapped memo
5250
+ // keyed on folded first char and length answers most calls in one compare.
5251
+ // Every hit re-verifies, so a collision misses rather than lies.
5590
5252
  let c0 = input.charCodeAt(start);
5591
5253
  if (c0 >= 0x41 && c0 <= 0x5a) c0 += 0x20;
5592
5254
  const memo = table.memo;
@@ -5912,7 +5574,7 @@ const htmlIntegrationPoint = (el) => {
5912
5574
  if (_namespaceOf(el) === NS_MATHML && _tagNameOf(el) === "annotation-xml") {
5913
5575
  const enc = _findAttr(_nodeAttrStarts[el], _nodeAttrCounts[el], "encoding");
5914
5576
  if (enc !== 0) {
5915
- const value = _attrValueOf(enc).toLowerCase();
5577
+ const value = _attrValueDecoded(enc).toLowerCase();
5916
5578
  if (value === "text/html" || value === "application/xhtml+xml") {
5917
5579
  return true;
5918
5580
  }
@@ -5967,14 +5629,12 @@ const adjustMathmlAttrs = (run) => {
5967
5629
  };
5968
5630
 
5969
5631
  // ---- HTML tree-construction parser state ----
5970
- // One active parse at a time (results are valid only until the next
5971
- // `parseHtml`); kept at module scope so the helpers below are defined once
5972
- // instead of re-created as closures on every parse.
5632
+ // One active parse at a time, its result valid only until the next `parseHtml`:
5633
+ // module scope is what defines the helpers below once rather than per parse.
5973
5634
  let source = "";
5974
- // Whole-document properties, so a substring cannot contain what the input does
5975
- // not. Decoding cannot introduce either: a numeric reference to 0 yields U+FFFD,
5976
- // and CR normalization runs on the raw slice before decoding. Default to `true`
5977
- // so an unset flag only ever costs the old scan.
5635
+ // Whole-document properties, so a substring cannot hold what the input does not,
5636
+ // and decoding introduces neither: a numeric reference to 0 yields U+FFFD, and
5637
+ // CR normalization runs on the raw slice first. An unset flag costs the old scan.
5978
5638
  let inputHasCr = true;
5979
5639
  let inputHasNul = true;
5980
5640
  let skipText = false;
@@ -6003,8 +5663,21 @@ const templateModes = [];
6003
5663
  let quirks = false;
6004
5664
  /** @type {HtmlElement} fragment context element (0 = document parse) */
6005
5665
  let fragment = 0;
5666
+
5667
+ /**
5668
+ * @returns {boolean} whether the parser is parsing template contents
5669
+ */
5670
+ const parsingTemplateContents = () =>
5671
+ open.some(isHtmlTemplateEl) || (fragment !== 0 && isHtmlTemplateEl(fragment));
5672
+
6006
5673
  let tokenEnd = 0;
6007
5674
  let swallowNextNewline = false;
5675
+ // Which of `</html>` / `</head>` / `</body>` the source spelled, per parse: the
5676
+ // tree records no end tag, and `removeImpliedTags` keeps the ones that were written.
5677
+ let sourceShellEnds = 0;
5678
+ const SHELL_END_HTML = 1;
5679
+ const SHELL_END_HEAD = 2;
5680
+ const SHELL_END_BODY = 4;
6008
5681
  let eofInTag = false;
6009
5682
  let sawSelectedContent = false;
6010
5683
  const decode = decodeEntities;
@@ -6208,12 +5881,9 @@ const _recordFostered = (table, node) => {
6208
5881
  const insertCharacters = (data, start, end) => {
6209
5882
  const place = appropriatePlace();
6210
5883
  if (_nodeTypes[place.parent] === NodeType.Document) return; // never insert text into document
6211
- // `skip.text`: drop every `Text` node — construction already used the
6212
- // decoded token, so removing the node never affects element structure.
6213
- // For a raw-text element record the body end so a consumer reads the span
6214
- // [`tagEnd`, `contentEnd`] without a `Text` node (see `HtmlParser`).
6215
- // Namespace-agnostic: `HtmlParser` extracts `<script>`/`<style>` bodies in
6216
- // foreign content (e.g. SVG `<style>`) too.
5884
+ // `skip.text` drops every `Text` node, which construction has already used,
5885
+ // so element structure is unaffected. A raw-text element instead records its
5886
+ // body end, giving a consumer the span [`tagEnd`, `contentEnd`].
6217
5887
  if (skipText) {
6218
5888
  const p = place.parent;
6219
5889
  if (
@@ -6224,10 +5894,9 @@ const insertCharacters = (data, start, end) => {
6224
5894
  }
6225
5895
  return;
6226
5896
  }
6227
- // Inlined text insert: when the run merges into the adjacent text sibling
6228
- // (common with inline formatting) only the string is appended — no
6229
- // throwaway text node is allocated. Mirrors `insertAtPlace`/`appendTo`,
6230
- // including that the before-node merge does not bump `end`.
5897
+ // Inlined text insert: a run merging into the adjacent text sibling, common
5898
+ // with inline formatting, appends the string alone rather than allocating a
5899
+ // throwaway node. Mirrors `insertAtPlace` / `appendTo`, `end` included.
6231
5900
  const p = effParent(place.parent);
6232
5901
  const before = place.beforeNode;
6233
5902
  if (before !== 0) {
@@ -6316,9 +5985,8 @@ const insertForeignElement = (tagName, ns, attrs, pos) => {
6316
5985
  };
6317
5986
 
6318
5987
  // ---- scopes ----
6319
- // Scope "kind" selects which extra elements act as boundaries. Passed as a
6320
- // small int so the scope checks below allocate no per-call predicate closure
6321
- // (these run several times per body tag).
5988
+ // Scope "kind" selects which extra elements act as boundaries, as a small int so
5989
+ // the checks below — several per body tag — allocate no predicate closure.
6322
5990
  const SCOPE_DEFAULT = 0;
6323
5991
  const SCOPE_BUTTON = 1;
6324
5992
  const SCOPE_LIST_ITEM = 2;
@@ -6606,10 +6274,9 @@ const resetInsertionMode = () => {
6606
6274
  const leadingWs = (t, insert) => {
6607
6275
  const m = /^[\t\n\f\r ]+/.exec(t.data);
6608
6276
  const ws = m ? m[0] : "";
6609
- // `t.data` is decoded, the offsets are raw, so counting one in the other
6610
- // only lines up when decoding changed no length (no CRLF, no reference).
6611
- // Otherwise keep the whole span: too wide is harmless, too narrow names
6612
- // source that is not this text (see the `Text` printer).
6277
+ // `t.data` is decoded and the offsets are raw, so counting one in the other
6278
+ // lines up only where decoding changed no length. Otherwise the whole span is
6279
+ // kept: too wide is harmless, too narrow names source that is not this text.
6613
6280
  const aligned = t.end - t.start === t.data.length;
6614
6281
  if (ws && insert) {
6615
6282
  insertCharacters(ws, t.start, aligned ? t.start + ws.length : t.end);
@@ -6895,10 +6562,9 @@ const adoptionAgency = (subject, pos) => {
6895
6562
  cloneAttrs(fmt),
6896
6563
  null
6897
6564
  );
6898
- // Take all direct children of `furthest` (a template's content fragment
6899
- // deliberately stays put — mirrors childrenOf-less spec behavior here). A
6900
- // declarative shadow root is not one of them: the engine took it out of
6901
- // the tree when it attached, so it stays with the host it hangs off.
6565
+ // Take the direct children of `furthest`; a template's content fragment
6566
+ // stays put. A declarative shadow root is not one of them — the engine took
6567
+ // it out of the tree on attaching, so it stays with its host.
6902
6568
  let k = _nodeFirstChildren[furthest];
6903
6569
  _nodeFirstChildren[furthest] = 0;
6904
6570
  _nodeLastChildren[furthest] = 0;
@@ -7185,7 +6851,7 @@ modes.inHead = (t) => {
7185
6851
  _nodeTypes[host] === NodeType.Element &&
7186
6852
  _isShadowHost(host)
7187
6853
  ) {
7188
- const mode = _attrValueOf(shadowRootMode).toLowerCase();
6854
+ const mode = _attrValueDecoded(shadowRootMode).toLowerCase();
7189
6855
  if (mode === "open" || mode === "closed") {
7190
6856
  _nodeFlags[el] |= FLAG_SHADOW_ROOT;
7191
6857
  _nodeFlags[host] |= FLAG_SHADOW_HOST;
@@ -7197,6 +6863,7 @@ modes.inHead = (t) => {
7197
6863
  }
7198
6864
  if (t.type === TOKEN_END_TAG) {
7199
6865
  if (t.name === "head") {
6866
+ sourceShellEnds |= SHELL_END_HEAD;
7200
6867
  popOpen();
7201
6868
  mode = MODE_AFTER_HEAD;
7202
6869
  return;
@@ -7256,6 +6923,7 @@ modes.afterHead = (t) => {
7256
6923
  const r = leadingWs(t, true);
7257
6924
  if (!r) return;
7258
6925
  insertHtmlElement("body", EMPTY_ATTRS, null);
6926
+ framesetOk = true;
7259
6927
  mode = MODE_IN_BODY;
7260
6928
  process(r);
7261
6929
  return;
@@ -7293,6 +6961,7 @@ modes.afterHead = (t) => {
7293
6961
  if (!BODY_HTML_BR.has(t.name)) return;
7294
6962
  }
7295
6963
  insertHtmlElement("body", EMPTY_ATTRS, null);
6964
+ framesetOk = true;
7296
6965
  mode = MODE_IN_BODY;
7297
6966
  process(t);
7298
6967
  };
@@ -7305,9 +6974,8 @@ modes.inBody = (t) => {
7305
6974
  if (t.data === "") return;
7306
6975
  reconstructAfe();
7307
6976
  insertCharacters(t.data, t.start, t.end);
7308
- // `framesetOk` only ever goes true→false, so once it's false skip the
7309
- // per-text-token whitespace scan entirely (it flips false very early in
7310
- // real documents).
6977
+ // After the implicit-body reset in "after head", `framesetOk` only goes
6978
+ // true→false, so once false skip this per-text-token whitespace scan.
7311
6979
  if (framesetOk && !isAllWs(t.data)) framesetOk = false;
7312
6980
  return;
7313
6981
  }
@@ -7409,7 +7077,7 @@ const startTagInBody = (t) => {
7409
7077
  return;
7410
7078
  }
7411
7079
  if (name === "form") {
7412
- if (form && !open.some(isHtmlTemplateEl)) {
7080
+ if (form && !parsingTemplateContents()) {
7413
7081
  return;
7414
7082
  }
7415
7083
  closeIfPInButtonScope();
@@ -7417,7 +7085,7 @@ const startTagInBody = (t) => {
7417
7085
  // not, so anything open between the two forms moves.
7418
7086
  if (_formResetWouldPop()) _printFromSource = true;
7419
7087
  const el = insertHtmlElement("form", t.attrs, t.pos);
7420
- if (!open.some(isHtmlTemplateEl)) {
7088
+ if (!parsingTemplateContents()) {
7421
7089
  form = el;
7422
7090
  }
7423
7091
  return;
@@ -7578,7 +7246,7 @@ const startTagInBody = (t) => {
7578
7246
  insertHtmlElement("input", t.attrs, t.pos);
7579
7247
  popOpen();
7580
7248
  const ty = _findAttr(t.attrs.start, t.attrs.count, "type");
7581
- if (ty === 0 || _attrValueOf(ty).toLowerCase() !== "hidden") {
7249
+ if (ty === 0 || _attrValueDecoded(ty).toLowerCase() !== "hidden") {
7582
7250
  framesetOk = false;
7583
7251
  }
7584
7252
  return;
@@ -7697,6 +7365,7 @@ const endTagInBody = (t) => {
7697
7365
  }
7698
7366
  if (name === "body" || name === "html") {
7699
7367
  if (!inScope("body")) return;
7368
+ sourceShellEnds |= name === "html" ? SHELL_END_HTML : SHELL_END_BODY;
7700
7369
  mode = MODE_AFTER_BODY;
7701
7370
  if (name === "html") process(t);
7702
7371
  return;
@@ -7708,7 +7377,7 @@ const endTagInBody = (t) => {
7708
7377
  return;
7709
7378
  }
7710
7379
  if (name === "form") {
7711
- if (!open.some(isHtmlTemplateEl)) {
7380
+ if (!parsingTemplateContents()) {
7712
7381
  const node = form;
7713
7382
  form = 0;
7714
7383
  if (!node || !inScopeEl(node)) return;
@@ -7756,10 +7425,9 @@ const endTagInBody = (t) => {
7756
7425
  /* take a deep breath */
7757
7426
  }
7758
7427
  if (FORMATTING.has(name)) {
7759
- // With no such element in the list, the algorithm hands the token back
7760
- // (§13.2.6.4.7, "any other end tag") rather than swallowing it — which is
7761
- // what Noah's Ark leaves behind: an element still open that the list no
7762
- // longer names.
7428
+ // With no such element in the list, §13.2.6.4.7 hands the token back rather
7429
+ // than swallowing it — which is what Noah's Ark leaves behind: an element
7430
+ // still open that the list no longer names.
7763
7431
  if (!adoptionAgency(name, t.pos)) anyOtherEndTag(name);
7764
7432
  return;
7765
7433
  }
@@ -7816,10 +7484,9 @@ modes.text = (t) => {
7816
7484
  return;
7817
7485
  }
7818
7486
  if (t.type === TOKEN_END_TAG) {
7819
- // Treat as rawtext rather than an end tag when it can't close the
7820
- // current element: a non-matching name (e.g. a fragment context), or
7821
- // a name that ran straight to EOF with no delimiter (`</script` at
7822
- // EOF — the tokenizer still emits a partial tag there).
7487
+ // Rawtext rather than an end tag where it cannot close the current
7488
+ // element: a name that does not match, or one running to EOF with no
7489
+ // delimiter, which the tokenizer still emits as a partial tag.
7823
7490
  if (
7824
7491
  cur() &&
7825
7492
  (t.name !== _tagNameOf(cur()) || t.pos.end === t.pos.nameEnd)
@@ -7900,17 +7567,19 @@ modes.inTable = (t) => {
7900
7567
  }
7901
7568
  if (name === "input") {
7902
7569
  const ty = _findAttr(t.attrs.start, t.attrs.count, "type");
7903
- if (ty !== 0 && _attrValueOf(ty).toLowerCase() === "hidden") {
7570
+ if (ty !== 0 && _attrValueDecoded(ty).toLowerCase() === "hidden") {
7904
7571
  insertHtmlElement("input", t.attrs, t.pos);
7905
7572
  popOpen();
7906
7573
  return;
7907
7574
  }
7908
7575
  }
7909
7576
  if (name === "form") {
7910
- if (form || open.some(isHtmlTemplateEl)) {
7577
+ const inTemplate = parsingTemplateContents();
7578
+ if (form && !inTemplate) {
7911
7579
  return;
7912
7580
  }
7913
- form = insertHtmlElement("form", t.attrs, t.pos);
7581
+ const el = insertHtmlElement("form", t.attrs, t.pos);
7582
+ if (!inTemplate) form = el;
7914
7583
  popOpen();
7915
7584
  return;
7916
7585
  }
@@ -8211,6 +7880,7 @@ modes.afterBody = (t) => {
8211
7880
  if (t.type === TOKEN_START_TAG && t.name === "html") return modes.inBody(t);
8212
7881
  if (t.type === TOKEN_END_TAG && t.name === "html") {
8213
7882
  if (fragment) return;
7883
+ sourceShellEnds |= SHELL_END_HTML;
8214
7884
  mode = MODE_AFTER_AFTER_BODY;
8215
7885
  return;
8216
7886
  }
@@ -8317,11 +7987,11 @@ const dispatch = () => {
8317
7987
  }
8318
7988
  };
8319
7989
 
8320
- // Tokenizer callbacks, hoisted to module scope and closing over the module-
8321
- // scope parser state. `tokenize`'s indirect `callbacks.text(...)` / `openTag`
8322
- // / `attribute(...)` sites then see one stable function identity across every
8323
- // parse, staying monomorphic so V8 can inline them — a fresh per-parse closure
8324
- // made them megamorphic and blocked inlining. `parseHtml` only refreshes
7990
+ // WHY: tokenizer callbacks hoisted to module scope, over the module-scope
7991
+ // parser state, so `tokenize`'s indirect `callbacks.text(...)` / `openTag` /
7992
+ // `attribute(...)` sites see one stable function identity across every parse.
7993
+ // They stay monomorphic and V8 inlines them; a fresh per-parse closure made
7994
+ // them megamorphic and blocked that. `parseHtml` refreshes only
8325
7995
  // `fragmentContext` on the shared object before each tokenize.
8326
7996
 
8327
7997
  /** @type {() => boolean} */
@@ -8380,10 +8050,9 @@ const handleDoctypeToken = (
8380
8050
  // newline; anything else clears the flag (§13.2.6.4.7 "the next token").
8381
8051
  swallowNextNewline = false;
8382
8052
  tok.type = TOKEN_DOCTYPE;
8383
- // A missing name and an empty one are the same to every consumer here: the
8384
- // spec's name states always append at least one character.
8385
- // ASCII-only folding: the name states lowercase ASCII upper alpha and append
8386
- // every other character unchanged, so a name of `Ð` stays `Ð`.
8053
+ // A missing name and an empty one are alike to every consumer here, since the
8054
+ // spec's name states always append a character. Folding is ASCII-only, as
8055
+ // those states lowercase upper alpha and append the rest as written.
8387
8056
  tok.name =
8388
8057
  nameStart === -1
8389
8058
  ? ""
@@ -8506,10 +8175,9 @@ const handleCommentToken = (input, start, end, dataStart, dataEnd) => {
8506
8175
  };
8507
8176
  /** @type {(input: string, start: number, end: number) => number} */
8508
8177
  const handleTextToken = (input, start, end) => {
8509
- // `skip.text` fast path: node dropped, so only whitespace-ness matters.
8510
- // With no `&`/`\0`/`\r` (and no pending newline-swallow) the decoded value
8511
- // equals the raw range — dispatch a canonical marker (`" "`/`"x"`) without
8512
- // the slice + entity decode; anything trickier falls through.
8178
+ // `skip.text` drops the node, so only whitespace-ness matters. Absent `&`,
8179
+ // `\0`, `\r` and a pending newline-swallow the decoded value is the raw range,
8180
+ // so a canonical marker is dispatched without slicing or decoding.
8513
8181
  if (skipText && !swallowNextNewline) {
8514
8182
  let hasNonWhitespace = false;
8515
8183
  let i = start;
@@ -8586,10 +8254,9 @@ const handleAttributeToken = (
8586
8254
  }
8587
8255
  if (dup) pendAttrDropped = true;
8588
8256
  if (!dup) {
8589
- // The raw (undecoded) value is read from the source by offset on
8590
- // demand: consumers re-resolve requests from it and the offsets must
8591
- // stay aligned with the source. Only a valueless attribute stores an
8592
- // override ("").
8257
+ // The raw value is read from the source by offset on demand, since
8258
+ // consumers re-resolve requests from it and the offsets have to stay
8259
+ // aligned. Only a valueless attribute stores an override.
8593
8260
  _allocAttr(
8594
8261
  name,
8595
8262
  valueStart !== -1 ? null : "",
@@ -8710,6 +8377,7 @@ const parseHtml = (input, pos = 0, options = {}) => {
8710
8377
  _dropSourceBackedNames(TAG_NAME_INTERN);
8711
8378
  _dropSourceBackedNames(ATTR_NAME_INTERN);
8712
8379
  const { fragmentContext, skip = EMPTY_SKIP } = options;
8380
+ sourceShellEnds = 0;
8713
8381
  skipText = skip.text === true;
8714
8382
  skipComments = skip.comments === true;
8715
8383
  skipDoctype = skip.doctype === true;
@@ -8721,26 +8389,21 @@ const parseHtml = (input, pos = 0, options = {}) => {
8721
8389
  _openStackChanged = false;
8722
8390
  _openSpliced = false;
8723
8391
  _streamHalted = false;
8724
- // Pre-size the columns from the input length: a document big enough to
8725
- // exceed `_COLUMN_SHRINK_CAPACITY` re-enters with released (empty) columns,
8726
- // and growing through the doubling cascade re-allocates and copies every
8727
- // column several times per parse. ~12 bytes/node and ~50 bytes/attribute
8728
- // are the densest realistic HTML, so `len/12` (`len/48`) reaches the final
8729
- // capacity in one allocation; a sparser document over-allocates bounded
8730
- // scratch that the post-walk release frees, and the doubling growth remains
8731
- // as the safety net when the estimate is short. `skip.text` (what
8732
- // `HtmlParser` uses) drops every text node — roughly half of all nodes — so
8733
- // its node estimate uses `len/24`; attributes are unaffected by the skip and
8734
- // keep `len/48`.
8392
+ // WHY: pre-size the columns from the input length. A document past
8393
+ // `_COLUMN_SHRINK_CAPACITY` re-enters with released columns, and the doubling
8394
+ // cascade would re-allocate and copy every one of them several times per
8395
+ // parse. The densest realistic HTML runs about 12 bytes per node and 50 per
8396
+ // attribute, so `len/12` and `len/48` reach the final capacity in one
8397
+ // allocation, a sparser document over-allocates scratch the release frees,
8398
+ // and doubling stays as the net under a short estimate. `skip.text`, which
8399
+ // `HtmlParser` uses, drops roughly half the nodes and so estimates `len/24`;
8400
+ // attributes are untouched by it.
8735
8401
  let nodeEstimate = (input.length / (skipText ? 24 : 12)) | 0;
8736
8402
  const attrEstimate = (input.length / 48) | 0;
8737
8403
  if (_streamEligible) {
8738
- // Node ids are recycled once streaming starts, so a document past the
8739
- // threshold never needs the whole estimate — one allocation covering the
8740
- // activation point plus a batch of headroom is its high-water mark. A
8741
- // document below it is unaffected and still pre-sizes exactly. Attributes
8742
- // are not recycled (`_attrCount` only resets between parses), so their
8743
- // estimate stands either way.
8404
+ // Streaming recycles node ids, so a document past the threshold peaks at
8405
+ // the activation point plus a batch of headroom rather than at the whole
8406
+ // estimate. Attributes are not recycled, so their estimate stands.
8744
8407
  const cap = _STREAM_MIN_NODES + _FLUSH_BATCH * 2;
8745
8408
  if (nodeEstimate > cap) nodeEstimate = cap;
8746
8409
  }
@@ -8835,10 +8498,10 @@ const parseHtml = (input, pos = 0, options = {}) => {
8835
8498
  tok.type = TOKEN_EOF;
8836
8499
  dispatch();
8837
8500
 
8838
- // Not while printing: the mirror is content the engine makes, with no source
8839
- // text behind it, and the printer emits what it walks — so serializing it
8840
- // would write the selected option's subtree into the document, where the
8841
- // engine that reads the output mirrors it a second time.
8501
+ // WHY: not while printing. The mirror is content the engine makes, with no
8502
+ // source text behind it, and the printer emits what it walks — so serializing
8503
+ // it would write the selected option's subtree into the document, which the
8504
+ // engine reading that output would then mirror a second time.
8842
8505
  if (sawSelectedContent && _writer === undefined) {
8843
8506
  mirrorSelectedContent(doc, 0);
8844
8507
  }
@@ -9009,7 +8672,7 @@ const mirrorSelectedContent = (node, select) => {
9009
8672
  * @property {boolean=} mergeScripts print a run of adjacent bare `<script>` elements whose bodies this print writes itself as one, which removes elements, hoists a later body's declarations into the ones before it, and lets a failing body end the whole run (default false)
9010
8673
  * @property {boolean=} sortAttributes print an element's attributes commonest name first, ties by name, which nothing in HTML reads (default false)
9011
8674
  * @property {boolean=} sortTokenLists print every token list the DOM reads as a set (`class`, `rel`, `part`, …) in token order (default false)
9012
- * @property {(boolean | "smart" | "all")=} removeRedundantAttributes drop an attribute whose value is the element's own default; `true` is `"smart"`, and `"all"` also drops the spec defaults a selector can match (default false)
8675
+ * @property {(boolean | "smart" | "all")=} removeRedundantAttributes drop an attribute whose value is the element's own default; `true` is `"smart"`, and `"all"` also drops the spec defaults a selector can match, and with them a value naming no keyword where that says the same (default false)
9013
8676
  * @property {(boolean | "smart" | "all")=} removeImpliedTags how much of the `<html>` / `<head>` / `<body>` shell §13.1.2.4 lets the parser imply may be left out: `"smart"` leaves out only the `<html>` start tag, `true` (or `"all"`) all six, `false` none (default `"smart"`)
9014
8677
  * @property {boolean=} deferSrcdoc whether an `<iframe srcdoc>` is among what `deferEmbeddedSource` collects (default true); false for a caller that minifies them itself, which keeps the attribute on the normal path and its shorter delimiter
9015
8678
  * @property {DeferredEmbeddedSource[]=} deferEmbeddedSource collects what `renderEmbeddedSource` would be offered instead of offering it, for a caller whose renderer is asynchronous: the print leaves a marker for each and `finish` puts the answers in their place, so one parse serves both. Takes precedence over `renderEmbeddedSource`
@@ -9063,19 +8726,20 @@ const _tailFusesWithNextTag = (s) => {
9063
8726
  * escape — and neither does the `&` in a query string.
9064
8727
  * @param {string} s decoded text
9065
8728
  * @param {number} i the `&`'s index
8729
+ * @param {boolean=} terminated whether nothing can follow `s` in the same run
9066
8730
  * @returns {boolean} true when it has to be escaped
9067
8731
  */
9068
- const _startsCharacterReference = (s, i) => {
9069
- // Nothing terminates the run before the node ends, so the next sibling's
9070
- // first characters could complete it once a comment between them is dropped
9071
- // (`a &am` + `p;`) — the same reason the source fast path keeps out.
8732
+ const _startsCharacterReference = (s, i, terminated) => {
8733
+ // Unterminated, the next sibling's first characters could complete the run
8734
+ // once a comment between them is dropped (`a &am` + `p;`) — the same reason
8735
+ // the source fast path keeps out. An attribute value ends at its delimiter.
9072
8736
  let end = i + 1;
9073
8737
  while (end < s.length) {
9074
8738
  const c = s.charCodeAt(end);
9075
8739
  if (!isAsciiAlphanumeric(c) && c !== 0x23) break;
9076
8740
  end++;
9077
8741
  }
9078
- if (end === s.length) return true;
8742
+ if (end === s.length && terminated !== true) return true;
9079
8743
  const next = s.charCodeAt(i + 1);
9080
8744
  // `#` opens a numeric reference.
9081
8745
  if (next === 0x23) return true;
@@ -9089,6 +8753,26 @@ const _startsCharacterReference = (s, i) => {
9089
8753
  return false;
9090
8754
  };
9091
8755
 
8756
+ /**
8757
+ * Whether the `<` at `at` would leave §13.2.5.6's data state rather than stand
8758
+ * as a character. A trailing one counts: what follows it in the output is the
8759
+ * next sibling's first character, which this string does not hold.
8760
+ * @param {string} s decoded text
8761
+ * @param {number} at index of the `<`
8762
+ * @returns {boolean} true when the `<` has to be written as a reference
8763
+ */
8764
+ const _opensMarkup = (s, at) => {
8765
+ if (at + 1 === s.length) return true;
8766
+ const next = s.charCodeAt(at + 1);
8767
+ return (
8768
+ next === 0x21 ||
8769
+ next === 0x2f ||
8770
+ next === 0x3f ||
8771
+ (next >= 0x41 && next <= 0x5a) ||
8772
+ (next >= 0x61 && next <= 0x7a)
8773
+ );
8774
+ };
8775
+
9092
8776
  /**
9093
8777
  * Escape a text node's decoded data for re-serialization: `&` and `<`, which
9094
8778
  * start a character reference and a tag, plus `>` outside minification, where
@@ -9103,15 +8787,15 @@ const _startsCharacterReference = (s, i) => {
9103
8787
  */
9104
8788
  const _escapeTextContent = (s, minify) => {
9105
8789
  // A bare `>` is only ever a character in text, so §13.3's escape of it buys
9106
- // nothing but bytes. `<` always starts a tag, and an `&` only sometimes
9107
- // starts a character reference — §13.2.5.72 flushes the rest as literal text.
8790
+ // nothing but bytes. A `<` only opens markup before what §13.2.5.6 leaves the
8791
+ // data state for, and an `&` only sometimes starts a character reference.
9108
8792
  if (minify) {
9109
8793
  if (!s.includes("&") && !s.includes("<") && !s.includes("\r")) return s;
9110
8794
  let out = "";
9111
8795
  let last = 0;
9112
8796
  for (let i = 0; i < s.length; i++) {
9113
8797
  const c = s.charCodeAt(i);
9114
- if (c === 0x3c) {
8798
+ if (c === 0x3c && _opensMarkup(s, i)) {
9115
8799
  out += `${s.slice(last, i)}&lt;`;
9116
8800
  last = i + 1;
9117
8801
  } else if (c === 0x26 && _startsCharacterReference(s, i)) {
@@ -9430,7 +9114,12 @@ const _impliedStartTagShape = (path, node, child) => {
9430
9114
  if (path.attributeCount(node) !== 0 || path.namespace(node) !== NS_HTML) {
9431
9115
  return false;
9432
9116
  }
9433
- const first = path.firstChild(node);
9117
+ // What will print, not what the tree holds: a comment minifying drops is not
9118
+ // content the tag has to stay for, the same way the end tags read it.
9119
+ let first = path.firstChild(node);
9120
+ while (first !== 0 && _printsNothing(path, first)) {
9121
+ first = path.nextSibling(first);
9122
+ }
9434
9123
  return (
9435
9124
  first !== 0 &&
9436
9125
  path.type(first) === NodeType.Element &&
@@ -9599,24 +9288,34 @@ let _deferEmbeddedSource;
9599
9288
  // them itself keeps the attribute on the normal path, which re-quotes it to
9600
9289
  // whichever delimiter its content makes shorter.
9601
9290
  /**
9602
- * One attribute, written with whichever delimiter its value needs fewer escapes
9603
- * inside — a minified document is mostly `"`, so one holding a `"` is shorter
9604
- * quoted with `'`. Where neither costs anything the source's own is kept.
9605
- * @param {string} name the attribute name
9291
+ * One value, written with whichever delimiter it needs fewer escapes inside — a
9292
+ * minified document is mostly `"`, so a value holding a `"` is shorter quoted
9293
+ * with `'`. Where neither costs anything the source's own delimiter is kept.
9606
9294
  * @param {string} value the value to write
9607
9295
  * @param {string} source the delimiter the source spelled it with, if any
9608
- * @returns {string} the attribute, delimiter and all
9296
+ * @returns {string} the value, delimiters and all
9609
9297
  */
9610
- const _writeQuotedAttribute = (name, value, source) => {
9298
+ const _quoteDecodedValue = (value, source) => {
9611
9299
  const double = value.includes('"');
9612
9300
  const single = value.includes("'");
9613
9301
  const quote =
9614
9302
  double !== single ? (double ? 39 : 34) : source === "'" ? 39 : 34;
9615
9303
  const delimiter = String.fromCharCode(quote);
9616
9304
 
9617
- return ` ${name}=${delimiter}${escapeAttribute(value, quote, true)}${delimiter}`;
9305
+ return `${delimiter}${escapeAttribute(value, quote, true)}${delimiter}`;
9618
9306
  };
9619
9307
 
9308
+ /**
9309
+ * One attribute, written with the delimiter `_quoteDecodedValue` picks for its
9310
+ * value.
9311
+ * @param {string} name the attribute name
9312
+ * @param {string} value the value to write
9313
+ * @param {string} source the delimiter the source spelled it with, if any
9314
+ * @returns {string} the attribute, delimiter and all
9315
+ */
9316
+ const _writeQuotedAttribute = (name, value, source) =>
9317
+ ` ${name}=${_quoteDecodedValue(value, source)}`;
9318
+
9620
9319
  let _deferSrcdoc = true;
9621
9320
 
9622
9321
  /**
@@ -9748,9 +9447,8 @@ let _mergeScripts = false;
9748
9447
  let _absorbed = null;
9749
9448
 
9750
9449
  // Foster parenting puts a node before the table it was written inside, so tree
9751
- // order and source order disagree there. Recorded where it happens.
9752
- // The tree holds something no arrangement of tags reaches: an element no source
9753
- // tag wrote, or a list item a special element stopped the loop from closing.
9450
+ // and source order disagree there; it is recorded where it happens. The tree
9451
+ // then holds what no arrangement of tags reaches.
9754
9452
  let _printFromSource = false;
9755
9453
  /** @type {HtmlNodeRef[] | null} this parse's fosters, flat: table, then node */
9756
9454
  let _fosteredLog = null;
@@ -9773,14 +9471,14 @@ let _fosterOpenTail = null;
9773
9471
  /** @type {number} how far the furthest verbatim span reached */
9774
9472
  let _fosterSpanEnd = 0;
9775
9473
 
9776
- // How much of the `<html>` / `<head>` / `<body>` shell §13.1.2.4 lets the
9777
- // parser imply may be left out (see `HtmlProcessOptions`). Every *other*
9778
- // optional tag goes unconditionally — nothing can observe that — but these six
9779
- // are what a consumer reading the page with a regexp rather than a parser looks
9780
- // for. `"smart"` leaves out the one such a reader never matches: the `<html>`
9781
- // start tag, omittable only when it carries no attribute at all, so the
9782
- // `<html lang=en>` anyone greps for keeps its tag anyway. `</html>` stays with
9783
- // it — a truncation check reads a page as complete by finding one.
9474
+ // WHY: how much of the `<html>` / `<head>` / `<body>` shell §13.1.2.4 lets the
9475
+ // parser imply may be left out. Every other optional tag goes unconditionally,
9476
+ // since nothing can observe that, but these six are what a consumer reading the
9477
+ // page with a regexp rather than a parser looks for. `"smart"` drops the one
9478
+ // such a reader never matches: the `<html>` start tag, omittable only with no
9479
+ // attribute at all, so the `<html lang=en>` anyone greps for keeps its tag.
9480
+ // `</html>` stays with it, since a truncation check reads a page as complete by
9481
+ // finding one.
9784
9482
  /** @type {"none" | "smart" | "all"} */
9785
9483
  let _removeImpliedTags = "smart";
9786
9484
 
@@ -9807,7 +9505,11 @@ const _canOmitShellStart = (path, name, node) => {
9807
9505
  if (name === "html") return true;
9808
9506
  if (_removeImpliedTags !== "all") return false;
9809
9507
  if (name === "head") return type === NodeType.Element;
9810
- if (type === NodeType.Text) return !startsWithWs(path.data(first));
9508
+ // Whitespace `"all"` drops is not whitespace the tag has to stay for; any
9509
+ // lesser tier keeps it, and without the tag the parser drops it on the way in.
9510
+ if (type === NodeType.Text) {
9511
+ return !startsWithWs(path.data(first)) || _collapseWhitespace === "all";
9512
+ }
9811
9513
  return !BODY_START_KEPT_BEFORE.has(path.tagName(first));
9812
9514
  };
9813
9515
 
@@ -9847,6 +9549,32 @@ const _shellEndTagOmittable = (path, name, node) => {
9847
9549
  const _canOmitShellEnd = (path, name, node) =>
9848
9550
  _removeImpliedTags === "all" && _shellEndTagOmittable(path, name, node);
9849
9551
 
9552
+ /**
9553
+ * The end tag a transparent implied element still prints: the parser re-implies
9554
+ * the start tag behind it, never the end tag, and outside `"all"` a shell tag
9555
+ * the source spelled stays — `</html>` is what a truncation check looks for.
9556
+ * @param {HtmlPath} path the accessor
9557
+ * @param {string} name the element's lowercased tag name
9558
+ * @param {HtmlNodeRef} node the element
9559
+ * @returns {string} the end tag, or `""` when nothing needs it
9560
+ */
9561
+ const _impliedCloseTag = (path, name, node) => {
9562
+ if (!SHELL_ELEMENTS.has(name)) {
9563
+ return _canOmitEndTag(path, name, node) ? "" : `</${name}>`;
9564
+ }
9565
+ const spelled =
9566
+ name === "html"
9567
+ ? SHELL_END_HTML
9568
+ : name === "head"
9569
+ ? SHELL_END_HEAD
9570
+ : SHELL_END_BODY;
9571
+ return (sourceShellEnds & spelled) === 0 || _removeImpliedTags === "all"
9572
+ ? _shellEndTagOmittable(path, name, node)
9573
+ ? ""
9574
+ : `</${name}>`
9575
+ : `</${name}>`;
9576
+ };
9577
+
9850
9578
  /**
9851
9579
  * Whether a parser-inserted table group has to print a start tag the source
9852
9580
  * never spelled. With `removeOptionalTags` off its end tag prints, and a
@@ -9901,9 +9629,8 @@ const _minifyStyleBody = (css) =>
9901
9629
  css.trim() === "" ? css : _renderEmbedded(css, CSS_TYPE);
9902
9630
 
9903
9631
  // The throwaway rule a `style` attribute's declaration list is minified inside.
9904
- // The selector is only ever written and read back here, so it is the shortest
9905
- // one the printer cannot rewrite — and the two halves are named so the read back
9906
- // out cannot drift from what was written in.
9632
+ // Its selector is only written and read back here, so it is the shortest one
9633
+ // the printer cannot rewrite, and both halves are named from one place.
9907
9634
 
9908
9635
  /**
9909
9636
  * How one attribute is written out, and whether it leaves the tag ending in an
@@ -9914,15 +9641,27 @@ const _minifyStyleBody = (css) =>
9914
9641
  * @param {string} rawValue its value as the source spelled it
9915
9642
  * @param {string | null} decoded the value decoded, for one carrying character references, or null
9916
9643
  * @param {boolean} quoted whether the source spelled the value with a delimiter, which is two characters this has to beat
9644
+ * @param {string} delimiter the delimiter the source spelled it with, if any
9917
9645
  * @returns {{ text: string, unquotedTail: boolean } | null} what to emit, or null to leave the source alone
9918
9646
  */
9919
- const _writeAttribute = (attributeName, value, rawValue, decoded, quoted) => {
9647
+ const _writeAttribute = (
9648
+ attributeName,
9649
+ value,
9650
+ rawValue,
9651
+ decoded,
9652
+ quoted,
9653
+ delimiter
9654
+ ) => {
9655
+ // `normalizeAttributeQuotes` holds back the delimiters, not the spelling
9656
+ // inside them, so a frozen value keeps the delimiter the source chose.
9657
+ const frozen = !_transforms.normalizeAttributeQuotes;
9920
9658
  const minimal =
9921
- _transforms.normalizeAttributeQuotes && decoded !== null
9922
- ? _minimalAttributeValue(decoded)
9923
- : null;
9924
- const unquotable =
9925
- _transforms.normalizeAttributeQuotes && _canUnquoteAttributeValue(value);
9659
+ decoded === null
9660
+ ? null
9661
+ : frozen
9662
+ ? _quoteDecodedValue(decoded, delimiter)
9663
+ : _minimalAttributeValue(decoded);
9664
+ const unquotable = !frozen && _canUnquoteAttributeValue(value);
9926
9665
 
9927
9666
  if (
9928
9667
  minimal !== null &&
@@ -9942,7 +9681,9 @@ const _writeAttribute = (attributeName, value, rawValue, decoded, quoted) => {
9942
9681
  }
9943
9682
 
9944
9683
  if (value !== rawValue) {
9945
- const quotedValue = _quoteAttributeValue(value);
9684
+ const quotedValue = frozen
9685
+ ? _quoteDecodedValue(value, delimiter)
9686
+ : _quoteAttributeValue(value);
9946
9687
 
9947
9688
  if (quotedValue !== null) {
9948
9689
  return { text: ` ${attributeName}=${quotedValue}`, unquotedTail: false };
@@ -10029,11 +9770,12 @@ const _minimalAttributeValue = (value) => {
10029
9770
  };
10030
9771
 
10031
9772
  /**
10032
- * Whether an attribute only restates the element's own default, so dropping it
10033
- * leaves the DOM reading the same value — but leaves an attribute selector,
10034
- * which matches the content attribute, with nothing to match. Both tables are
10035
- * looked up per element by the caller, not per attribute here: the answer is the
10036
- * same for every attribute of one tag.
9773
+ * Whether an attribute only restates the element's own default — its value
9774
+ * naming that default, or naming no keyword at all where the invalid value
9775
+ * default is that same state — so dropping it leaves the DOM reading the same
9776
+ * value, but leaves an attribute selector, which matches the content attribute,
9777
+ * with nothing to match. Both tables are looked up per element by the caller,
9778
+ * not per attribute here: the answer is the same for every attribute of one tag.
10037
9779
  * @param {Record<string, string> | undefined} markers the element's marker table
10038
9780
  * @param {Record<string, string> | undefined} defaults the element's spec-default table
10039
9781
  * @param {string} tagName lowercased element name
@@ -10042,19 +9784,30 @@ const _minimalAttributeValue = (value) => {
10042
9784
  * @returns {boolean} true when the attribute may be dropped
10043
9785
  */
10044
9786
  const _isRedundantAttribute = (markers, defaults, tagName, name, value) => {
10045
- const lowered = _asciiLowerCase(_asciiTrim(value));
9787
+ // Looked up by what the value says, not how it was spelled — guarded on the
9788
+ // `&` so a value carrying no reference pays nothing.
9789
+ const lowered = _asciiLowerCase(
9790
+ _asciiTrim(value.includes("&") ? decodeEntities(value, true) : value)
9791
+ );
10046
9792
  // `<script type>`: only a spelling that names JavaScript, which is what the
10047
9793
  // element already runs. `module` and the empty spelling say something.
10048
9794
  if (tagName === "script" && name === "type") {
10049
- // Read the way `_scriptType` reads it, or the two disagree about which
10050
- // `<script>` this is and the body's handler and this one part ways.
10051
- const type = _asciiLowerCase(_asciiTrim(decodeEntities(value, true)));
10052
9795
  return (
10053
- type !== "" && type !== "module" && JAVASCRIPT_SCRIPT_TYPES.has(type)
9796
+ lowered !== "" &&
9797
+ lowered !== "module" &&
9798
+ JAVASCRIPT_SCRIPT_TYPES.has(lowered)
10054
9799
  );
10055
9800
  }
10056
9801
  if (markers !== undefined && markers[name] === lowered) return true;
10057
- return defaults !== undefined && defaults[name] === lowered;
9802
+ if (defaults === undefined || defaults[name] === undefined) return false;
9803
+ if (defaults[name] === lowered) return true;
9804
+ // A value naming no keyword takes the attribute's invalid value default,
9805
+ // which is the state a missing attribute names except where
9806
+ // `INVALID_VALUE_DEFAULT_DIFFERS` says the two part.
9807
+ const enumerated = ENUMERATED_KEYWORDS[tagName];
9808
+ const keywords = enumerated === undefined ? undefined : enumerated[name];
9809
+ if (keywords === undefined || keywords.has(lowered)) return false;
9810
+ return !INVALID_VALUE_DEFAULT_DIFFERS.has(`${tagName} ${name}`);
10058
9811
  };
10059
9812
 
10060
9813
  /**
@@ -10069,9 +9822,12 @@ const _isRemovableEmptyAttribute = (elementName, name, value) => {
10069
9822
  if (!_removeEmptyAttributes) return false;
10070
9823
  const on = EMPTY_REMOVABLE_ATTRIBUTES.get(name);
10071
9824
  if (on === undefined || (on !== null && !on.has(elementName))) return false;
10072
- // Read raw, so a value spelled with a character reference is kept whatever it
10073
- // decodes to. `_asciiTrim` returns the input when there is nothing to trim.
10074
- return _asciiTrim(value) === "";
9825
+ // Read decoded: a reference is how a value is spelled, not what it says, and
9826
+ // the tokenizer hands the element the same empty either way. Guarded on the
9827
+ // `&`, and `_asciiTrim` returns the input when there is nothing to trim.
9828
+ return (
9829
+ _asciiTrim(value.includes("&") ? decodeEntities(value, true) : value) === ""
9830
+ );
10075
9831
  };
10076
9832
 
10077
9833
  /**
@@ -10483,7 +10239,12 @@ const _scriptType = (path, element) => {
10483
10239
  const _isViewportMeta = (path) => {
10484
10240
  for (const attribute of path.attributes()) {
10485
10241
  if (attribute.name === "name") {
10486
- return _asciiLowerCase(_asciiTrim(attribute.value)) === "viewport";
10242
+ const value = attribute.value;
10243
+ return (
10244
+ _asciiLowerCase(
10245
+ _asciiTrim(value.includes("&") ? decodeEntities(value, true) : value)
10246
+ ) === "viewport"
10247
+ );
10487
10248
  }
10488
10249
  }
10489
10250
  return false;
@@ -10511,7 +10272,12 @@ const _minifyInlineJson = (json) =>
10511
10272
  const _isCssStyleElement = (path, element) => {
10512
10273
  for (const attribute of path.attributes(element)) {
10513
10274
  if (attribute.name !== "type") continue;
10514
- const value = _asciiLowerCase(_asciiTrim(attribute.value));
10275
+ // Read the way `_scriptType` reads its own: the value the spec matches is
10276
+ // the one the parser read, so a reference is decoded first.
10277
+ const raw = attribute.value;
10278
+ const value = _asciiLowerCase(
10279
+ _asciiTrim(raw.includes("&") ? decodeEntities(raw, true) : raw)
10280
+ );
10515
10281
  return value === "" || value === "text/css";
10516
10282
  }
10517
10283
  return true;
@@ -10536,6 +10302,47 @@ const _onlyTextChild = (element) => {
10536
10302
  : 0;
10537
10303
  };
10538
10304
 
10305
+ /**
10306
+ * Whether an attribute reaches the output: one this print drops is not a
10307
+ * difference between two elements, nor an attribute that keeps a run apart.
10308
+ * @param {string} elementName lowercased element name
10309
+ * @param {string} name lowercased attribute name
10310
+ * @param {string} value its raw value
10311
+ * @returns {boolean} true when it prints
10312
+ */
10313
+ const _attributePrints = (elementName, name, value) => {
10314
+ const markers =
10315
+ _removeRedundantAttributes !== "none"
10316
+ ? REDUNDANT_TYPE_ATTRIBUTES[elementName]
10317
+ : undefined;
10318
+ const defaults =
10319
+ _removeRedundantAttributes === "all"
10320
+ ? REDUNDANT_DEFAULT_ATTRIBUTES[elementName]
10321
+ : undefined;
10322
+ if (
10323
+ (markers !== undefined || defaults !== undefined) &&
10324
+ _isRedundantAttribute(markers, defaults, elementName, name, value)
10325
+ ) {
10326
+ return false;
10327
+ }
10328
+ return !_isRemovableEmptyAttribute(elementName, name, value);
10329
+ };
10330
+
10331
+ /**
10332
+ * How many of an element's attributes this print keeps.
10333
+ * @param {HtmlPath} path the accessor
10334
+ * @param {HtmlNodeRef} element the element
10335
+ * @returns {number} the count
10336
+ */
10337
+ const _printedAttributeCount = (path, element) => {
10338
+ const name = path.tagName(element);
10339
+ let kept = 0;
10340
+ for (const attribute of path.attributes(element)) {
10341
+ if (_attributePrints(name, attribute.name, attribute.value)) kept++;
10342
+ }
10343
+ return kept;
10344
+ };
10345
+
10539
10346
  /**
10540
10347
  * Whether two elements carry exactly the same attributes. Values compare raw:
10541
10348
  * two spellings of one value are rare enough not to be worth decoding for, and
@@ -10546,19 +10353,21 @@ const _onlyTextChild = (element) => {
10546
10353
  * @returns {boolean} true when the two agree on every attribute
10547
10354
  */
10548
10355
  const _sameAttributes = (path, one, other) => {
10549
- const count = path.attributeCount(one);
10550
- if (count !== path.attributeCount(other)) return false;
10551
- for (let i = 0; i < count; i++) {
10552
- const attribute = path.attributeAt(i, one);
10553
- const match = path.findAttribute(path.attributeName(attribute), other);
10554
- if (
10555
- match === 0 ||
10556
- path.attributeValue(match) !== path.attributeValue(attribute)
10557
- ) {
10356
+ // What each will spell, not what the tree holds: a `type` this print drops
10357
+ // as redundant is not a difference between them.
10358
+ const elementName = path.tagName(one);
10359
+ let count = 0;
10360
+ for (const attribute of path.attributes(one)) {
10361
+ if (!_attributePrints(elementName, attribute.name, attribute.value)) {
10362
+ continue;
10363
+ }
10364
+ count++;
10365
+ const match = path.findAttribute(attribute.name, other);
10366
+ if (match === 0 || path.attributeValue(match) !== attribute.value) {
10558
10367
  return false;
10559
10368
  }
10560
10369
  }
10561
- return true;
10370
+ return count === _printedAttributeCount(path, other);
10562
10371
  };
10563
10372
 
10564
10373
  /**
@@ -10582,6 +10391,12 @@ const _mergeableStyleAfter = (path, element) => {
10582
10391
  if (!isAllWs(_nodeStrings[next])) return 0;
10583
10392
  continue;
10584
10393
  }
10394
+ // A comment this print drops leaves the two adjacent, so it is not what
10395
+ // stands between them; one it keeps still is.
10396
+ if (type === NodeType.Comment) {
10397
+ if (_printsNothing(path, next)) continue;
10398
+ return 0;
10399
+ }
10585
10400
  return type === NodeType.Element &&
10586
10401
  path.namespace(next) === NS_HTML &&
10587
10402
  path.tagName(next) === "style" &&
@@ -10797,7 +10612,7 @@ const _opensWithHtmlCloseComment = (js) => {
10797
10612
  * @returns {HtmlElement} the next `<script>` of the run, or 0
10798
10613
  */
10799
10614
  const _mergeableScriptAfter = (path, element) => {
10800
- if (path.attributeCount(element) !== 0) return 0;
10615
+ if (_printedAttributeCount(path, element) !== 0) return 0;
10801
10616
  for (
10802
10617
  let next = _nodeNextSiblings[element];
10803
10618
  next !== 0;
@@ -10808,10 +10623,16 @@ const _mergeableScriptAfter = (path, element) => {
10808
10623
  if (!isAllWs(_nodeStrings[next])) return 0;
10809
10624
  continue;
10810
10625
  }
10626
+ // A comment this print drops leaves the two adjacent, so it is not what
10627
+ // stands between them; one it keeps still is.
10628
+ if (type === NodeType.Comment) {
10629
+ if (_printsNothing(path, next)) continue;
10630
+ return 0;
10631
+ }
10811
10632
  return type === NodeType.Element &&
10812
10633
  path.namespace(next) === NS_HTML &&
10813
10634
  path.tagName(next) === "script" &&
10814
- path.attributeCount(next) === 0 &&
10635
+ _printedAttributeCount(path, next) === 0 &&
10815
10636
  _onlyTextChild(next) !== 0
10816
10637
  ? next
10817
10638
  : 0;
@@ -10933,7 +10754,7 @@ const _canUnquoteAttributeValue = (raw) => {
10933
10754
  c === 0x3d ||
10934
10755
  c === 0x3e ||
10935
10756
  c === 0x60 ||
10936
- (c >= 0x09 && c <= 0x0d)
10757
+ (c >= 0x09 && c <= 0x0d && c !== 0x0b)
10937
10758
  ) {
10938
10759
  return false;
10939
10760
  }
@@ -10942,9 +10763,8 @@ const _canUnquoteAttributeValue = (raw) => {
10942
10763
  };
10943
10764
 
10944
10765
  // How often each attribute name occurs in the document being printed, or null
10945
- // until `sortAttributes` asks for it. Built from the parse's own attribute
10946
- // column rather than by walking the tree: every attribute of every element is
10947
- // already interned there, in one flat run.
10766
+ // until `sortAttributes` asks. Built from the parse's attribute column rather
10767
+ // than by walking the tree, which already holds every one in a flat run.
10948
10768
  /** @type {Map<string, number> | null} */
10949
10769
  let _attributeFrequencies = null;
10950
10770
 
@@ -10982,14 +10802,53 @@ const _attributeRank = () => {
10982
10802
  };
10983
10803
 
10984
10804
  /**
10805
+ * How often each attribute name reaches the output. Counted over the runs the
10806
+ * elements own rather than the whole column, because an attribute a transform
10807
+ * drops is not one the print order should be ranked on.
10985
10808
  * @returns {Map<string, number>} how often each attribute name occurs
10986
10809
  */
10987
10810
  const _attributeFrequency = () => {
10988
10811
  if (_attributeFrequencies !== null) return _attributeFrequencies;
10989
10812
  const frequency = new Map();
10990
- for (let i = 1; i <= _attrCount; i++) {
10991
- const name = _attrNames[i];
10992
- frequency.set(name, (frequency.get(name) || 0) + 1);
10813
+ for (let node = 1; node <= _nodeCount; node++) {
10814
+ if (_nodeTypes[node] !== NodeType.Element) continue;
10815
+ const count = _nodeAttrCounts[node];
10816
+ if (count === 0) continue;
10817
+ const start = _nodeAttrStarts[node];
10818
+ // Foreign content keeps every attribute it was written with, so only an
10819
+ // HTML element is asked what it drops — the same test the printer makes.
10820
+ const html = _namespaceOf(node) === NS_HTML;
10821
+ const elementName = _tagNameOf(node);
10822
+ const markers =
10823
+ html && _removeRedundantAttributes !== "none"
10824
+ ? REDUNDANT_TYPE_ATTRIBUTES[elementName]
10825
+ : undefined;
10826
+ const defaults =
10827
+ html && _removeRedundantAttributes === "all"
10828
+ ? REDUNDANT_DEFAULT_ATTRIBUTES[elementName]
10829
+ : undefined;
10830
+ const mayDrop = markers !== undefined || defaults !== undefined;
10831
+ for (let i = start; i < start + count; i++) {
10832
+ const name = _attrNames[i];
10833
+ if (html) {
10834
+ // Valueless reads as the empty value here as it does there.
10835
+ const value = _attrValueOf(i);
10836
+ if (
10837
+ (mayDrop &&
10838
+ _isRedundantAttribute(
10839
+ markers,
10840
+ defaults,
10841
+ elementName,
10842
+ name,
10843
+ value
10844
+ )) ||
10845
+ _isRemovableEmptyAttribute(elementName, name, value)
10846
+ ) {
10847
+ continue;
10848
+ }
10849
+ }
10850
+ frequency.set(name, (frequency.get(name) || 0) + 1);
10851
+ }
10993
10852
  }
10994
10853
  _attributeFrequencies = frequency;
10995
10854
  return frequency;
@@ -11092,10 +10951,9 @@ const _minifyOpenTag = (path, open) => {
11092
10951
  // deferred answer held to the shape the built-in was written in.
11093
10952
  const tailIsRead = !html && open.charCodeAt(open.length - 2) === 47;
11094
10953
  let consumedEnd = path.nameEnd();
11095
- // HTML only, which is also what keeps `consumedEnd` meaningful: it is read
11096
- // once more below, and only for a foreign element's `/>`.
11097
- // Per element, not per attribute: every attribute of one tag gets the same
11098
- // answer out of these, and they are the hottest lookups in the printer.
10954
+ // HTML only, which is what keeps `consumedEnd` meaningful: it is read once
10955
+ // more below, for a foreign element's `/>` alone. Per element rather than per
10956
+ // attribute, these being the printer's hottest lookups.
11099
10957
  const markers =
11100
10958
  html && _removeRedundantAttributes !== "none"
11101
10959
  ? REDUNDANT_TYPE_ATTRIBUTES[elementName]
@@ -11137,6 +10995,7 @@ const _minifyOpenTag = (path, open) => {
11137
10995
  // at the closing one, so a quoted value's span is one wider on each side.
11138
10996
  const quote = open.charCodeAt(valueStart - 1 - tagStart);
11139
10997
  const quoted = quote === 34 || quote === 39;
10998
+ const sourceDelimiter = quoted ? open[valueStart - tagStart - 1] : "";
11140
10999
  const rawEnd = path.attributeValueEnd(attribute);
11141
11000
  const valueEnd = rawEnd + (quoted ? 1 : 0);
11142
11001
  const rawValue = open.slice(valueStart - tagStart, rawEnd - tagStart);
@@ -11173,7 +11032,7 @@ const _minifyOpenTag = (path, open) => {
11173
11032
  if (_deferEmbeddedSource !== undefined && _deferSrcdoc) {
11174
11033
  // Declining leaves the attribute as written, delimiter and all: a bare
11175
11034
  // value has none, and quoting one would only lengthen it.
11176
- const delimiter = quoted ? open[valueStart - tagStart - 1] : "";
11035
+ const delimiter = sourceDelimiter;
11177
11036
  const asWritten = ` ${attributeName}=${delimiter}${rawValue}${delimiter}`;
11178
11037
 
11179
11038
  out += _deferEmbedded(document, HTML_TYPE, (rendered) =>
@@ -11204,8 +11063,8 @@ const _minifyOpenTag = (path, open) => {
11204
11063
  // `style`, and a bare value is already the shortest spelling of itself.
11205
11064
  if (quoted || html || attributeName === "style" || rawValue.includes("&")) {
11206
11065
  const hasReference = rawValue.includes("&");
11207
- // A character reference would decode to something these grammars read
11208
- // differently, so only a reference-free value is rewritten.
11066
+ // Rewritten from the source text where it carries no reference; one that
11067
+ // does is rewritten from what it decodes to, below.
11209
11068
  let value = rawValue;
11210
11069
  if (!hasReference) {
11211
11070
  if (html) {
@@ -11223,22 +11082,42 @@ const _minifyOpenTag = (path, open) => {
11223
11082
  value = _minifyStyleAttribute(rawValue);
11224
11083
  }
11225
11084
  }
11226
- // An empty value is the bare name: the tokenizer's after-attribute-name
11227
- // state reads `x` in `<p x>` and `<p x="">` as the same attribute with
11228
- // the same empty value, in foreign content too. A boolean attribute
11229
- // spelled with its own name (`disabled="disabled"`) is the same empty
11230
- // presence, and that one the spec does canonicalize.
11231
- // Both spellings are the same empty presence, and writing either as the
11232
- // bare name is the `=""` going — a quoting decision, so it answers to
11233
- // the switch that names the rest of them.
11085
+ // WHY: an empty value is the bare name — the after-attribute-name state
11086
+ // reads `x` in `<p x>` and `<p x="">` as one attribute with one empty
11087
+ // value, in foreign content too — and a boolean spelled by its own name
11088
+ // is the same presence, which the spec does canonicalize. Writing either
11089
+ // as the bare name is just the `=""` going, a quoting decision, so it
11090
+ // answers to the switch naming the rest of them. The decode is guarded on
11091
+ // the reference to stay off the hot path, and tried before unquoting so
11092
+ // every reference it writes stays bare.
11093
+ const decoded = hasReference ? decodeEntities(rawValue, true) : null;
11094
+ const spellable = decoded;
11095
+ // The name the presence is spelled with is what it decodes to, so
11096
+ // `checked="&#x63;…"` is the same presence as `checked="checked"`.
11097
+ const presence = spellable !== null ? spellable : rawValue;
11098
+ // The grammars read what the value decodes to, so a referenced one is
11099
+ // rewritten from that and spelled back. `style` has its own path below.
11100
+ const rewritten =
11101
+ spellable !== null && html && attributeName !== "style"
11102
+ ? _rewriteAttributeValue(
11103
+ elementName,
11104
+ attributeName,
11105
+ spellable,
11106
+ isViewportMeta,
11107
+ enumeratedOn
11108
+ )
11109
+ : spellable;
11110
+ // What the attribute ends up saying, whichever spelling it arrived in:
11111
+ // the bare name answers to that, not to the side the value was written.
11112
+ const settled = rewritten !== null ? rewritten : value;
11234
11113
  if (
11235
11114
  _transforms.normalizeAttributeQuotes &&
11236
- (value === "" ||
11115
+ (settled === "" ||
11237
11116
  (html &&
11238
11117
  _transforms.collapseBooleanAttributes &&
11239
11118
  (_transforms.collapseBooleanAttributes === "all" ||
11240
- (rawValue.length === attributeName.length &&
11241
- _asciiLowerCase(rawValue) === attributeName)) &&
11119
+ (presence.length === attributeName.length &&
11120
+ _asciiLowerCase(presence) === attributeName)) &&
11242
11121
  _isBooleanAttribute(elementName, attributeName)))
11243
11122
  ) {
11244
11123
  out += ` ${attributeName}`;
@@ -11246,16 +11125,6 @@ const _minifyOpenTag = (path, open) => {
11246
11125
  consumedEnd = valueEnd;
11247
11126
  continue;
11248
11127
  }
11249
- // Guarded on the reference, so the decode stays off the hot path — and
11250
- // tried before unquoting, which keeps every reference it writes bare.
11251
- const decoded = hasReference ? decodeEntities(rawValue, true) : null;
11252
- // A reference spelling whitespace is deliberate: writing it out would
11253
- // let `removeEmptyAttributes` read the attribute as empty on a re-run.
11254
- const spellable =
11255
- decoded !== null &&
11256
- (_asciiTrim(decoded) !== "" || _asciiTrim(rawValue) === "")
11257
- ? decoded
11258
- : null;
11259
11128
  const isStyle = attributeName === "style";
11260
11129
  // A `style=""` carrying a character reference is a declaration list like
11261
11130
  // any other once decoded — `&quot;` is a quote to CSS, not an entity — so
@@ -11268,17 +11137,32 @@ const _minifyOpenTag = (path, open) => {
11268
11137
  */
11269
11138
  const writeStyle = (minified) =>
11270
11139
  declarations !== null
11271
- ? _writeAttribute(attributeName, rawValue, rawValue, minified, quoted)
11140
+ ? _writeAttribute(
11141
+ attributeName,
11142
+ rawValue,
11143
+ rawValue,
11144
+ minified,
11145
+ quoted,
11146
+ sourceDelimiter
11147
+ )
11272
11148
  : _writeAttribute(
11273
11149
  attributeName,
11274
11150
  minified,
11275
11151
  rawValue,
11276
11152
  spellable,
11277
- quoted
11153
+ quoted,
11154
+ sourceDelimiter
11278
11155
  );
11279
11156
  const written = isStyle
11280
11157
  ? writeStyle(declarations !== null ? declarations : value)
11281
- : _writeAttribute(attributeName, value, rawValue, spellable, quoted);
11158
+ : _writeAttribute(
11159
+ attributeName,
11160
+ value,
11161
+ rawValue,
11162
+ rewritten,
11163
+ quoted,
11164
+ sourceDelimiter
11165
+ );
11282
11166
  // The JavaScript an event handler holds, offered decoded — `&amp;&amp;`
11283
11167
  // is an operator to JavaScript rather than an entity. No built-in.
11284
11168
  const handler =
@@ -11292,7 +11176,7 @@ const _minifyOpenTag = (path, open) => {
11292
11176
  : null;
11293
11177
 
11294
11178
  if (handler !== null && _asciiTrim(handler) !== "") {
11295
- const delimiter = quoted ? open[valueStart - tagStart - 1] : "";
11179
+ const delimiter = sourceDelimiter;
11296
11180
  // A foreign `/>` reads back whether this value ends unquoted, which a
11297
11181
  // deferred answer is not there to decide: the source's quoting stands.
11298
11182
  const holdQuoted = tailIsRead && _deferEmbeddedSource !== undefined;
@@ -11326,7 +11210,8 @@ const _minifyOpenTag = (path, open) => {
11326
11210
  escapeAttribute(rendered, CC_QUOTATION_MARK, true),
11327
11211
  rawValue,
11328
11212
  rendered,
11329
- quoted
11213
+ quoted,
11214
+ sourceDelimiter
11330
11215
  );
11331
11216
 
11332
11217
  return answered !== null && answered.text.length < asWritten.length
@@ -11364,7 +11249,7 @@ const _minifyOpenTag = (path, open) => {
11364
11249
  if (isStyle && _deferEmbeddedSource !== undefined) {
11365
11250
  // Declining leaves the attribute as written, delimiter and all — an
11366
11251
  // answer nothing beats is the renderer keeping what it was given.
11367
- const delimiter = quoted ? open[valueStart - tagStart - 1] : "";
11252
+ const delimiter = sourceDelimiter;
11368
11253
  const asWritten = ` ${attributeName}=${delimiter}${rawValue}${delimiter}`;
11369
11254
  // What stands until an answer arrives, and the tag ending it commits
11370
11255
  // to: the built-in's text where it beat the source, else the source.
@@ -11402,6 +11287,14 @@ const _minifyOpenTag = (path, open) => {
11402
11287
  consumedEnd = valueEnd;
11403
11288
  continue;
11404
11289
  }
11290
+ // Quoting may be frozen, but a rewrite changed what the value says, so
11291
+ // the source's own spelling of it cannot stand.
11292
+ if (rewritten !== null && rewritten !== spellable) {
11293
+ out += _writeQuotedAttribute(attributeName, rewritten, sourceDelimiter);
11294
+ unquotedTail = false;
11295
+ consumedEnd = valueEnd;
11296
+ continue;
11297
+ }
11405
11298
  }
11406
11299
  // A quoted value carries its quotes over; an unquoted one starts at the
11407
11300
  // value itself.
@@ -11412,10 +11305,10 @@ const _minifyOpenTag = (path, open) => {
11412
11305
  unquotedTail = !quoted;
11413
11306
  consumedEnd = valueEnd;
11414
11307
  }
11415
- // A source `/>` self-closes a foreign element, so it survives there — but only
11416
- // when the `/` really is the flag: in `<a href=x/>` it is the last character of
11417
- // an unquoted value instead, and after one of those it needs a space or it
11418
- // would fuse into the value. An HTML element's parser ignores it, so it goes.
11308
+ // WHY: a source `/>` self-closes a foreign element and survives there, but
11309
+ // only where the `/` is the flag: in `<a href=x/>` it ends an unquoted value,
11310
+ // and after one of those it needs a space or it fuses into the value. An HTML
11311
+ // element's parser ignores it, so there it goes.
11419
11312
  if (
11420
11313
  path.namespace() !== NS_HTML &&
11421
11314
  open.charCodeAt(open.length - 2) === 47 &&
@@ -11437,10 +11330,9 @@ const _minifyOpenTag = (path, open) => {
11437
11330
  * @returns {boolean} true to keep the comment
11438
11331
  */
11439
11332
  const _keepComment = (data, source) => {
11440
- // `<?php … ?>` / `<%… %>` reach the tree as bogus comments (§13.2.5.42) but
11441
- // are server-side directives, and the forms below drive IE branching or a
11442
- // server-side include — every one of them is code rather than a comment, so
11443
- // no level drops it.
11333
+ // `<?php … ?>` and `<%… %>` reach the tree as bogus comments (§13.2.5.42),
11334
+ // and the forms below drive IE branching or a server-side include. Every one
11335
+ // is code rather than a comment, so no level drops it.
11444
11336
  if (source.charCodeAt(1) === 63) return true;
11445
11337
  const trimmed = data.trimStart();
11446
11338
  // `<![` covers the downlevel-revealed forms: `<!--<![endif]-->` closers and
@@ -11558,6 +11450,13 @@ const _contentOpensOutside = (node) => {
11558
11450
  // so each streamed element costs no result object.
11559
11451
  let _streamOpen = "";
11560
11452
  let _streamClose = "";
11453
+ // The close that goes with a held-back start tag once it prints: an element
11454
+ // whose tag the parser no longer re-implies answers to `removeImpliedTags`.
11455
+ let _streamCloseIfOpened = "";
11456
+ // Which held-back opener `_emitPiece` settled into a start tag, `-1` for none.
11457
+ let _materializedPendingSlot = -1;
11458
+ /** @type {string[]} */
11459
+ const _printCloseIfOpened = [];
11561
11460
  let _streamImplied = "";
11562
11461
 
11563
11462
  /**
@@ -11628,11 +11527,8 @@ const _streamTags = (path, minify) => {
11628
11527
  if (path.attributeCount() === 0 && TRANSPARENT_IMPLIED_ELEMENTS.has(name)) {
11629
11528
  // The parser re-implies the start tag, but not the end tag the source
11630
11529
  // carried: without it whatever follows moves inside this element.
11631
- _streamClose = (
11632
- SHELL_ELEMENTS.has(name)
11633
- ? _shellEndTagOmittable(path, name, path.node)
11634
- : _canOmitEndTag(path, name, path.node)
11635
- )
11530
+ _streamClose = _impliedCloseTag(path, name, path.node);
11531
+ _streamCloseIfOpened = _canOmitEndTag(path, name, path.node)
11636
11532
  ? ""
11637
11533
  : `</${name}>`;
11638
11534
  // Leading whitespace is decided on the first piece emitted; what the modes
@@ -12091,10 +11987,9 @@ const _digest = (root) => {
12091
11987
  attributes.push(`${name}=${_DIGEST_OPAQUE}`);
12092
11988
  continue;
12093
11989
  }
12094
- // The value as the DOM holds it — the same value can be written with a
12095
- // different quote and a different escape and still be the same
12096
- // attribute — with a boolean spelled by its own name as the bare
12097
- // presence the printer writes it back as.
11990
+ // The value as the DOM holds it, since another quote and another escape
11991
+ // spell the same attribute, with a boolean named by itself as the bare
11992
+ // presence the printer writes back.
12098
11993
  attributes.push(
12099
11994
  `${name}=${
12100
11995
  html &&
@@ -12349,13 +12244,7 @@ const printer = (path, writer) => {
12349
12244
  }
12350
12245
  // The parser re-implies the start tag, but not the end tag the
12351
12246
  // source carried: without it whatever follows moves inside.
12352
- return (
12353
- SHELL_ELEMENTS.has(name)
12354
- ? _shellEndTagOmittable(path, name, path.node)
12355
- : _canOmitEndTag(path, name, path.node)
12356
- )
12357
- ? inner
12358
- : `${inner}</${name}>`;
12247
+ return inner + _impliedCloseTag(path, name, path.node);
12359
12248
  }
12360
12249
  // Anything else tag-less (adoption-agency clone, reconstructed
12361
12250
  // formatting element) must materialize or its formatting is lost.
@@ -12401,7 +12290,7 @@ const printer = (path, writer) => {
12401
12290
  // A bare `<script>` is the classic one a run may fold into, and
12402
12291
  // the run's first prints all of it — the rest printed into it here.
12403
12292
  let body = data;
12404
- if (_mergeScripts && path.attributeCount(parent) === 0) {
12293
+ if (_mergeScripts && _printedAttributeCount(path, parent) === 0) {
12405
12294
  if (_absorbed !== null && _absorbed.has(parent)) return "";
12406
12295
  body = _mergedScriptRun(path, parent, data);
12407
12296
  }
@@ -12466,18 +12355,17 @@ const printer = (path, writer) => {
12466
12355
  }
12467
12356
  }
12468
12357
  const raw = path.source();
12469
- // Off-spec workaround: §13.3 escapes `<`/`>` in text, which would rewrite
12470
- // `<%= x %>` to `&lt;%= x %&gt;` in HTML webpack only passes through. Text
12471
- // that decoded to itself re-tokenizes to itself, so emit its source bytes.
12472
- // The guards are what keep that equivalence: every character reference is
12473
- // at least two characters longer than what it decodes to, so equal lengths
12474
- // mean nothing decoded — an unequal one means the node was merged or
12475
- // foster-parented and its range no longer describes `data`. The tail is
12476
- // checked separately because it is the only place concatenation can change
12477
- // tokenization: a `<`, or a `&` still open on a reference, would fuse with
12478
- // the next sibling once a comment between them is dropped. A CR is the one
12479
- // rewrite the length test cannot see — preprocessing maps a lone one to LF
12480
- // without changing length — so it is excluded by name.
12358
+ // WHY: off-spec workaround. §13.3 escapes `<` and `>` in text, which
12359
+ // would rewrite `<%= x %>` to `&lt;%= x %&gt;` in HTML webpack only passes
12360
+ // through, so text that decoded to itself is emitted as its source bytes.
12361
+ // The guards keep that equivalence: a character reference is at least two
12362
+ // characters longer than what it decodes to, so equal lengths mean nothing
12363
+ // decoded, while unequal ones mean the node was merged or foster-parented
12364
+ // and its range no longer describes `data`. The tail is checked separately
12365
+ // as the one place concatenation can change tokenization — a `<`, or a `&`
12366
+ // still open on a reference, fuses with the next sibling once a comment
12367
+ // between them is dropped. A CR is the rewrite the length test cannot see,
12368
+ // preprocessing mapping a lone one to LF, so it is excluded by name.
12481
12369
  if (
12482
12370
  raw.length === data.length &&
12483
12371
  !_tailFusesWithNextTag(raw) &&
@@ -12533,12 +12421,12 @@ let _visitors = EMPTY_VISITORS;
12533
12421
  /** @type {PrintContext | undefined} */
12534
12422
  let _writer;
12535
12423
 
12536
- // Iterative depth-first walk over the link columns: `firstChild` /
12537
- // `nextSibling` descend, the `parent` column ascends, so arbitrarily deep
12538
- // markup can't overflow the call stack (the old recursive walk died at
12539
- // ~10⁵ nesting). A `<template>`'s content fragment is visited before the
12540
- // element's children; ascending out of it continues with those children
12541
- // (the fragment is never in a sibling chain, `_nodeNextSiblings` = 0).
12424
+ // WHY: an iterative depth-first walk over the link columns — `firstChild` and
12425
+ // `nextSibling` descend, `parent` ascends — so arbitrarily deep markup cannot
12426
+ // overflow the call stack, where the old recursive walk died at ~10⁵ nesting. A
12427
+ // `<template>`'s content fragment is visited before the element's children, and
12428
+ // ascending out of it continues with those: the fragment is never in a sibling
12429
+ // chain, so its `_nodeNextSiblings` is 0.
12542
12430
 
12543
12431
  /**
12544
12432
  * Fire a node's `enter` visitors; true when the walk may descend.
@@ -12567,13 +12455,13 @@ const _enterNode = (node) => {
12567
12455
  return !skip;
12568
12456
  };
12569
12457
 
12570
- // === Printing in pieces ===
12571
- // The printer composes a node from its children's text, so the store holds every
12572
- // node's until the root is taken — several times the output on a document of any
12573
- // size. A node whose text is exactly `open + children + close` instead emits its
12574
- // opener as it opens and its closer as it exits, and each child goes straight out
12575
- // as it finishes; the store then only ever holds the subtrees that cannot (see
12576
- // `_streamTags`). Indexed by print depth, which is the open path.
12458
+ // WHY: printing in pieces.
12459
+ // The printer composes a node from its children's text, so the store would
12460
+ // hold every node's until the root is taken — several times the output on a
12461
+ // document of any size. A node whose text is exactly `open + children + close`
12462
+ // instead emits its opener as it opens and its closer as it exits, with each
12463
+ // child going straight out as it finishes, so the store only ever holds the
12464
+ // subtrees that cannot. Indexed by print depth, which is the open path.
12577
12465
  /** @type {number} depth of the node the printing walk is on (maintained by `_walkRun`) */
12578
12466
  let _printDepth = 0;
12579
12467
  /** @type {number[]} 1 while the node at that depth prints in pieces */
@@ -12604,6 +12492,7 @@ const _emitPiece = (text) => {
12604
12492
  const c = text.charCodeAt(0);
12605
12493
  if (c === 0x09 || c === 0x0a || c === 0x0c || c === 0x0d || c === 0x20) {
12606
12494
  w.setPending(held - 1, `<${_printPendingNames[held - 1]}>`);
12495
+ _materializedPendingSlot = held - 1;
12607
12496
  }
12608
12497
  _printPendingNames.length = 0;
12609
12498
  w.flushPending();
@@ -12641,6 +12530,7 @@ const _openStreamed = (node, depth, descending) => {
12641
12530
  if (_streamImplied === "") {
12642
12531
  _emitPiece(_streamOpen);
12643
12532
  } else {
12533
+ _printCloseIfOpened[depth] = _streamCloseIfOpened;
12644
12534
  _printPendingSlot[depth] = w.pushPending("");
12645
12535
  _printPendingNames.push(_streamImplied);
12646
12536
  }
@@ -12693,8 +12583,14 @@ const _exitNode = (node) => {
12693
12583
  if (_printOpen[depth] === 1) {
12694
12584
  // Printed in pieces: its opener went out as it opened and its children as
12695
12585
  // they finished, so only its closer is left.
12696
- _emitPiece(_printClose[depth]);
12697
12586
  const slot = _printPendingSlot[depth];
12587
+ // Its start tag printed after all, so the end tag is no longer the one a
12588
+ // transparent element leaves behind.
12589
+ _emitPiece(
12590
+ slot !== -1 && slot === _materializedPendingSlot
12591
+ ? _printCloseIfOpened[depth]
12592
+ : _printClose[depth]
12593
+ );
12698
12594
  // Nothing inside it printed, so its held-back opener never had to.
12699
12595
  if (slot !== -1 && _writer.isPending(slot)) {
12700
12596
  _printPendingNames.pop();
@@ -12729,8 +12625,8 @@ const _firstInner = (node) => {
12729
12625
  return 0;
12730
12626
  };
12731
12627
 
12732
- // === Streaming walk ===
12733
- // The walk follows the parser's open element stack: everything under the
12628
+ // WHY: the streaming walk.
12629
+ // It follows the parser's open element stack: everything under the
12734
12630
  // deepest *entered* element is finished markup, so it is visited and its node
12735
12631
  // ids recycled, and peak storage is the open subtree rather than the whole
12736
12632
  // document — the HTML analog of the CSS grammar's per-top-level-rule recycling.
@@ -12763,12 +12659,12 @@ const _entered = [];
12763
12659
  let _skipFrom = -1;
12764
12660
  /** set when `open` was spliced mid-stack, so the next sync scans it in full */
12765
12661
  let _openSpliced = false;
12766
- // Set when a mid-stack `open.splice` removes an element the walk already
12767
- // entered (`</form>` with content still open inside it). The element leaves the
12768
- // open stack without its subtree ending, so the open stack no longer describes
12662
+ // WHY: set when a mid-stack `open.splice` removes an element the walk already
12663
+ // entered, such as a `</form>` with content still open inside it. The element
12664
+ // leaves the stack without its subtree ending, so the stack no longer describes
12769
12665
  // what has finished and reconciling against it would close and re-enter live
12770
- // elements. Rare enough to simply stop streaming for the rest of the parse: what
12771
- // is already visited stays visited, and the remainder is walked at EOF.
12666
+ // elements. Rare enough to stop streaming for the rest of the parse: what is
12667
+ // visited stays visited, and the remainder is walked at EOF.
12772
12668
  let _streamHalted = false;
12773
12669
  /** set by `popOpen`: the open stack changed, so the walk has work at token end */
12774
12670
  let _openStackChanged = false;
@@ -12776,11 +12672,11 @@ let _openStackChanged = false;
12776
12672
  // earns its walk setup. Closing an entered element always flushes, so this only
12777
12673
  // bounds how much an un-entered subtree holds — a few KB of columns.
12778
12674
  const _FLUSH_BATCH = 4096;
12779
- // Documents below this many nodes are walked once at EOF instead: recycling
12780
- // only pays once the columns are big enough that not growing them beats the
12781
- // per-token sync, and a page that never reaches it must not carry the cost.
12782
- // Chosen so the streamed cap below stays under `_COLUMN_SHRINK_CAPACITY`,
12783
- // keeping a streamed parse's columns reusable by the next one.
12675
+ // WHY: below this many nodes a document is walked once at EOF instead.
12676
+ // Recycling only pays once the columns are big enough that not growing them
12677
+ // beats the per-token sync, and a page never reaching that must not carry the
12678
+ // cost. Chosen so the streamed cap below stays under
12679
+ // `_COLUMN_SHRINK_CAPACITY`, keeping the columns reusable by the next parse.
12784
12680
  const _STREAM_MIN_NODES = 49152;
12785
12681
 
12786
12682
  /**
@@ -12974,9 +12870,8 @@ const _streamSync = () => {
12974
12870
 
12975
12871
  let i = _enteredPrefix();
12976
12872
  // Close: everything entered below the divergence has finished. A descendant
12977
- // of a `skipChildren()` element was tracked without being entered, so it must
12978
- // not exit either — the buffered walk fires `exit` for the skipped element
12979
- // itself and nothing below it.
12873
+ // of a `skipChildren()` element was tracked without being entered, so it does
12874
+ // not exit either — only the skipped element itself does.
12980
12875
  for (let k = _entered.length - 1; k >= i; k--) {
12981
12876
  const el = _entered[k];
12982
12877
  const belowSkip = _skipFrom !== -1 && k > _skipFrom;
@@ -13032,12 +12927,12 @@ const _streamFinish = (root) => {
13032
12927
  * @param {HtmlProcessOptions} options process options
13033
12928
  */
13034
12929
  const grammar = (input, visitors, writer, options) => {
13035
- // The walk's state — the visitors, the print context, the open path, the AST
13036
- // columns — is module-scoped so the tokenizer's call sites stay monomorphic,
13037
- // which means one parse at a time: a visitor that started another would take
13038
- // this one's state over and release its columns on the way out. Nesting the
13039
- // *other* language is fine (a `<style>`'s CSS has its own module state), and
13040
- // an `<iframe srcdoc>` is a separate module webpack parses on its own.
12930
+ // WHY: the walk's state — visitors, print context, open path, AST columns —
12931
+ // is module-scoped so the tokenizer's call sites stay monomorphic, which means
12932
+ // one parse at a time: a visitor starting another would take this one's state
12933
+ // over and release its columns on the way out. Nesting the other language is
12934
+ // fine, a `<style>`'s CSS having module state of its own, and an
12935
+ // `<iframe srcdoc>` is a separate module webpack parses on its own.
13041
12936
  if (_parsing) {
13042
12937
  throw new Error(
13043
12938
  "html syntax: process() is already running; a visitor cannot start another HTML parse"
@@ -13160,6 +13055,8 @@ const grammar = (input, visitors, writer, options) => {
13160
13055
  _printClose.length = 0;
13161
13056
  _printPendingNames.length = 0;
13162
13057
  _printPendingSlot.length = 0;
13058
+ _printCloseIfOpened.length = 0;
13059
+ _materializedPendingSlot = -1;
13163
13060
  // Hoisting these to module scope outlives the call, so drop the caller's
13164
13061
  // visitors and print context here — they used to fall away with `grammar`.
13165
13062
  _visitors = EMPTY_VISITORS;
@@ -13197,9 +13094,8 @@ const RIGHT_PARENTHESIS = ")".charCodeAt(0);
13197
13094
  const SMALL_LETTER_W = "w".charCodeAt(0);
13198
13095
  const SMALL_LETTER_X = "x".charCodeAt(0);
13199
13096
  const SMALL_LETTER_H = "h".charCodeAt(0);
13200
- // (Don't use \s, to avoid matching non-breaking space)
13201
- // Sticky so `collectCharacters` matches at an offset without slicing the
13202
- // whole remaining input per call (which made srcset parsing quadratic).
13097
+ // Not `\s`, which matches a non-breaking space. Sticky so `collectCharacters`
13098
+ // matches at an offset rather than slicing per call, which was quadratic.
13203
13099
  // eslint-disable-next-line no-control-regex
13204
13100
  const LEADING_SPACES_REGEXP = /[ \t\n\r\u000C]+/y;
13205
13101
  // eslint-disable-next-line no-control-regex
@@ -13208,10 +13104,8 @@ const LEADING_COMMAS_OR_SPACES_REGEXP = /[, \t\n\r\u000C]+/y;
13208
13104
  const LEADING_NOT_SPACES = /[^ \t\n\r\u000C]+/y;
13209
13105
  const TRAILING_COMMAS_REGEXP = /[,]+$/;
13210
13106
  const NON_NEGATIVE_INTEGER_REGEXP = /^\d+$/;
13211
- // ( Positive or negative or unsigned integers or decimals, without or without exponents.
13212
- // Must include at least one digit.
13213
- // According to spec tests any decimal point must be followed by a digit.
13214
- // No leading plus sign is allowed.)
13107
+ // A signed or unsigned integer or decimal, with or without an exponent, holding
13108
+ // at least one digit, no leading plus, and a digit after any decimal point.
13215
13109
  // https://html.spec.whatwg.org/multipage/infrastructure.html#valid-floating-point-number
13216
13110
  const FLOATING_POINT_REGEXP =
13217
13111
  /^-?(?:[0-9]+|[0-9]*\.[0-9]+)(?:[eE][+-]?[0-9]+)?$/;
@@ -13400,10 +13294,8 @@ const parseSrcset = (input) => {
13400
13294
  if (state === "in descriptor") {
13401
13295
  // Do the following, depending on the value of charCode:
13402
13296
 
13403
- // Space character
13404
- // If current descriptor is not empty, append current descriptor to
13405
- // descriptors and let current descriptor be the empty string.
13406
- // Set state to after descriptor.
13297
+ // Space: append a non-empty current descriptor to descriptors and empty
13298
+ // it, then set state to after descriptor.
13407
13299
  if (isSpace(charCode)) {
13408
13300
  if (descriptorStart !== -1) {
13409
13301
  descriptors.push(input.slice(descriptorStart, position));
@@ -13411,10 +13303,9 @@ const parseSrcset = (input) => {
13411
13303
  state = "after descriptor";
13412
13304
  }
13413
13305
  }
13414
- // U+002C COMMA (,)
13415
- // Advance position to the next character in input. If current descriptor
13416
- // is not empty, append current descriptor to descriptors. Jump to the step
13417
- // labeled descriptor parser.
13306
+ // `,`: Advance position to the next character in input. If current
13307
+ // descriptor is not empty, append current descriptor to descriptors. Jump
13308
+ // to the step labeled descriptor parser.
13418
13309
  else if (charCode === COMMA) {
13419
13310
  position += 1;
13420
13311
 
@@ -13696,10 +13587,9 @@ let _currentParent = null;
13696
13587
  */
13697
13588
  /* eslint-enable jsdoc/require-template */
13698
13589
 
13699
- // AST field-access seam (mirrors the CSS parser's `A`): every AST field a
13700
- // consumer reads goes through one of these accessors, so the node
13701
- // representation can change underneath without touching consumers. `n` is an
13702
- // `HtmlNodeRef`; results are valid until the next `parseHtml` call.
13590
+ // AST field-access seam, mirroring the CSS parser's `A`: every field a consumer
13591
+ // reads goes through an accessor here, so the node representation can change
13592
+ // underneath. `n` is an `HtmlNodeRef`, valid until the next `parseHtml`.
13703
13593
  const A = {
13704
13594
  // === path position (rebound by the walk before every visitor call) ===
13705
13595
  /**