webpack 5.108.3 → 5.109.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (191) hide show
  1. package/lib/APIPlugin.js +48 -29
  2. package/lib/CacheFacade.js +8 -0
  3. package/lib/ChunkGroup.js +1 -0
  4. package/lib/CompatibilityPlugin.js +31 -29
  5. package/lib/Compilation.js +50 -8
  6. package/lib/ConstPlugin.js +11 -11
  7. package/lib/ContextModule.js +239 -6
  8. package/lib/ContextModuleFactory.js +139 -14
  9. package/lib/DefinePlugin.js +396 -101
  10. package/lib/Entrypoint.js +1 -1
  11. package/lib/ExportsInfoApiPlugin.js +2 -2
  12. package/lib/ExternalModule.js +107 -5
  13. package/lib/ExternalModuleFactoryPlugin.js +20 -1
  14. package/lib/FileSystemInfo.js +130 -74
  15. package/lib/FlagDependencyExportsPlugin.js +55 -8
  16. package/lib/HotModuleReplacementPlugin.js +18 -15
  17. package/lib/LazyBarrel.js +30 -3
  18. package/lib/Module.js +3 -0
  19. package/lib/ModuleFilenameHelpers.js +2 -2
  20. package/lib/ModuleTypeConstants.js +8 -1
  21. package/lib/MultiCompiler.js +13 -2
  22. package/lib/MultiWatching.js +1 -0
  23. package/lib/NodeStuffPlugin.js +51 -33
  24. package/lib/NormalModule.js +169 -405
  25. package/lib/NormalModuleFactory.js +76 -2
  26. package/lib/ProgressPlugin.js +211 -119
  27. package/lib/ProvidePlugin.js +2 -2
  28. package/lib/RuntimeGlobals.js +47 -0
  29. package/lib/RuntimePlugin.js +33 -0
  30. package/lib/RuntimeTemplate.js +304 -16
  31. package/lib/UseStrictPlugin.js +1 -1
  32. package/lib/WebpackIsIncludedPlugin.js +1 -1
  33. package/lib/WebpackOptionsApply.js +24 -19
  34. package/lib/asset/AssetGenerator.js +110 -28
  35. package/lib/asset/AssetModulesPlugin.js +92 -2
  36. package/lib/asset/WebManifestGenerator.js +164 -0
  37. package/lib/asset/WebManifestParser.js +130 -0
  38. package/lib/async-modules/AwaitDependenciesInitFragment.js +14 -4
  39. package/lib/async-modules/isGeneratorLowered.js +26 -0
  40. package/lib/cache/PackFileCacheStrategy.js +9 -3
  41. package/lib/config/browserslistTargetHandler.js +40 -0
  42. package/lib/config/defaults.js +222 -32
  43. package/lib/config/normalization.js +26 -0
  44. package/lib/config/target.js +9 -1
  45. package/lib/css/CssGenerator.js +73 -73
  46. package/lib/css/CssLoadingRuntimeModule.js +5 -1
  47. package/lib/css/CssModulesPlugin.js +92 -53
  48. package/lib/css/CssParser.js +843 -302
  49. package/lib/css/syntax.js +1219 -483
  50. package/lib/dependencies/AMDDefineDependencyParserPlugin.js +6 -6
  51. package/lib/dependencies/AMDPlugin.js +2 -2
  52. package/lib/dependencies/AMDRequireDependenciesBlockParserPlugin.js +13 -13
  53. package/lib/dependencies/CommonJsDependencyHelpers.js +39 -0
  54. package/lib/dependencies/CommonJsExportsDependency.js +47 -2
  55. package/lib/dependencies/CommonJsExportsParserPlugin.js +10 -10
  56. package/lib/dependencies/CommonJsImportsParserPlugin.js +18 -18
  57. package/lib/dependencies/CommonJsPlugin.js +2 -2
  58. package/lib/dependencies/CommonJsRequireDependency.js +26 -1
  59. package/lib/dependencies/CommonJsSelfReferenceDependency.js +32 -1
  60. package/lib/dependencies/ContextDependencyHelpers.js +3 -3
  61. package/lib/dependencies/CreateRequireParserPlugin.js +6 -6
  62. package/lib/dependencies/CssIcssExportDependency.js +190 -87
  63. package/lib/dependencies/CssUrlDependency.js +35 -4
  64. package/lib/dependencies/ExternalModuleInitFragmentDependency.js +2 -2
  65. package/lib/dependencies/HarmonyCompatibilityDependency.js +32 -14
  66. package/lib/dependencies/HarmonyDetectionParserPlugin.js +43 -13
  67. package/lib/dependencies/HarmonyExportDependencyParserPlugin.js +7 -19
  68. package/lib/dependencies/HarmonyImportDependency.js +12 -1
  69. package/lib/dependencies/HarmonyImportDependencyParserPlugin.js +34 -107
  70. package/lib/dependencies/HarmonyImportGuard.js +192 -20
  71. package/lib/dependencies/HarmonyModulesPlugin.js +6 -0
  72. package/lib/dependencies/HarmonyTopLevelThisParserPlugin.js +1 -1
  73. package/lib/dependencies/HtmlEntryDependency.js +797 -172
  74. package/lib/dependencies/HtmlSourceDependency.js +26 -0
  75. package/lib/dependencies/ImportDependency.js +43 -1
  76. package/lib/dependencies/ImportMetaContextDependencyParserPlugin.js +31 -18
  77. package/lib/dependencies/ImportMetaContextPlugin.js +38 -7
  78. package/lib/dependencies/ImportMetaGlobDependency.js +81 -0
  79. package/lib/dependencies/ImportMetaGlobDependencyParserPlugin.js +78 -0
  80. package/lib/dependencies/ImportMetaGlobHelpers.js +660 -0
  81. package/lib/dependencies/ImportMetaPlugin.js +532 -180
  82. package/lib/dependencies/ImportMetaResolveDependency.js +90 -0
  83. package/lib/dependencies/ImportParserPlugin.js +87 -26
  84. package/lib/dependencies/ImportPhase.js +10 -8
  85. package/lib/dependencies/LoaderPlugin.js +5 -1
  86. package/lib/dependencies/RequireContextDependencyParserPlugin.js +1 -1
  87. package/lib/dependencies/RequireEnsureDependenciesBlockParserPlugin.js +3 -6
  88. package/lib/dependencies/RequireIncludeDependencyParserPlugin.js +4 -11
  89. package/lib/dependencies/SystemPlugin.js +2 -4
  90. package/lib/dependencies/TopLevelAwaitDependency.js +93 -0
  91. package/lib/dependencies/URLDependency.js +141 -21
  92. package/lib/dependencies/{WorkerPlugin.js → WorkerAndWorkletPlugin.js} +324 -112
  93. package/lib/dependencies/WorkerDependency.js +86 -18
  94. package/lib/dependencies/WorkletDependency.js +278 -0
  95. package/lib/dll/DelegatedModuleFactoryPlugin.js +19 -0
  96. package/lib/errors/EnvironmentNotSupportAsyncWarning.js +1 -1
  97. package/lib/errors/LoaderLoadingError.js +20 -0
  98. package/lib/errors/ModuleParseError.js +39 -22
  99. package/lib/errors/NodeStuffInWebError.js +1 -1
  100. package/lib/esm/ModuleChunkFormatPlugin.js +47 -14
  101. package/lib/esm/ModuleChunkLoadingPlugin.js +8 -1
  102. package/lib/esm/ModuleChunkLoadingRuntimeModule.js +29 -1
  103. package/lib/hmr/lazyCompilationBackend.js +32 -8
  104. package/lib/html/HtmlGenerator.js +940 -91
  105. package/lib/html/HtmlModule.js +3 -2
  106. package/lib/html/HtmlModulesPlugin.js +625 -170
  107. package/lib/html/HtmlParser.js +812 -340
  108. package/lib/html/favicon.svg +1 -0
  109. package/lib/html/syntax.js +2116 -834
  110. package/lib/ids/IdHelpers.js +1 -1
  111. package/lib/index.js +6 -0
  112. package/lib/javascript/ArrayPushCallbackChunkFormatPlugin.js +4 -5
  113. package/lib/javascript/BasicEvaluatedExpression.js +366 -48
  114. package/lib/javascript/JavascriptGenerator.js +197 -49
  115. package/lib/javascript/JavascriptModulesPlugin.js +178 -24
  116. package/lib/javascript/JavascriptParser.js +711 -355
  117. package/lib/javascript/JavascriptParserHelpers.js +3 -6
  118. package/lib/javascript/syntax.js +4380 -0
  119. package/lib/library/ModuleLibraryPlugin.js +38 -1
  120. package/lib/loaders/LoaderRunner.js +714 -0
  121. package/lib/loaders/loadLoader.js +100 -0
  122. package/lib/node/ReadFileCompileAsyncWasmPlugin.js +19 -0
  123. package/lib/node/ReadFileCompileWasmPlugin.js +14 -0
  124. package/lib/optimize/ConcatenatedModule.js +120 -16
  125. package/lib/optimize/FlagIncludedChunksPlugin.js +13 -3
  126. package/lib/optimize/InnerGraphPlugin.js +50 -30
  127. package/lib/optimize/ModuleConcatenationPlugin.js +69 -3
  128. package/lib/optimize/SideEffectsFlagPlugin.js +86 -43
  129. package/lib/prefetch/ChunkPrefetchPreloadPlugin.js +15 -0
  130. package/lib/prefetch/ChunkPreloadTriggerRuntimeModule.js +24 -13
  131. package/lib/prefetch/ResourceHintPlugin.js +663 -0
  132. package/lib/prefetch/ResourceHintRuntimeModule.js +149 -0
  133. package/lib/prefetch/StartupAssetHintRuntimeModule.js +210 -0
  134. package/lib/prefetch/parseResourceHintOptions.js +103 -0
  135. package/lib/rules/RuleSetCompiler.js +114 -0
  136. package/lib/runtime/AsyncModuleGeneratorRuntimeModule.js +56 -0
  137. package/lib/runtime/AsyncModuleRuntimeModule.js +24 -8
  138. package/lib/runtime/AutoPublicPathRuntimeModule.js +7 -1
  139. package/lib/runtime/CommonJsWrapRuntimeModule.js +37 -0
  140. package/lib/runtime/GetWorkletBootstrapRuntimeModule.js +59 -0
  141. package/lib/runtime/MakeDeferredNamespaceObjectRuntime.js +204 -8
  142. package/lib/runtime/SetAnonymousDefaultNameRuntimeModule.js +4 -1
  143. package/lib/schemes/HttpUriPlugin.js +40 -1
  144. package/lib/schemes/VirtualUrlPlugin.js +1 -2
  145. package/lib/serialization/FileMiddleware.js +29 -3
  146. package/lib/stats/DefaultStatsFactoryPlugin.js +32 -3
  147. package/lib/stats/DefaultStatsPresetPlugin.js +2 -0
  148. package/lib/stats/DefaultStatsPrinterPlugin.js +12 -2
  149. package/lib/stats/StatsFactory.js +15 -8
  150. package/lib/url/URLParserPlugin.js +98 -19
  151. package/lib/util/AsyncQueue.js +122 -72
  152. package/lib/util/LocConverter.js +9 -8
  153. package/lib/util/SetHelpers.js +1 -1
  154. package/lib/util/SourceProcessor.js +41 -28
  155. package/lib/util/StackedMap.js +122 -48
  156. package/lib/util/binarySearchBounds.js +2 -2
  157. package/lib/util/concatenate.js +40 -14
  158. package/lib/util/createHooksRegistry.js +13 -5
  159. package/lib/util/dataURL.js +2 -2
  160. package/lib/util/deterministicGrouping.js +7 -5
  161. package/lib/util/formatSize.js +6 -2
  162. package/lib/util/globUtils.js +710 -0
  163. package/lib/util/identifier.js +23 -6
  164. package/lib/util/implicitTypeLoaderFallback.js +62 -0
  165. package/lib/util/internalSerializables.js +88 -71
  166. package/lib/util/magicComment.js +121 -25
  167. package/lib/util/numberHash.js +2 -2
  168. package/lib/wasm-async/AsyncWasmCompileRuntimeModule.js +24 -18
  169. package/lib/wasm-async/AsyncWasmLoadingRuntimeModule.js +33 -22
  170. package/lib/wasm-sync/WasmChunkLoadingRuntimeModule.js +79 -16
  171. package/lib/web/FetchCompileAsyncWasmPlugin.js +19 -0
  172. package/lib/web/FetchCompileWasmPlugin.js +14 -0
  173. package/lib/web/JsonpChunkLoadingRuntimeModule.js +2 -0
  174. package/lib/webpack.js +65 -0
  175. package/lib/webworker/ImportScriptsChunkLoadingRuntimeModule.js +3 -0
  176. package/module.d.ts +21 -0
  177. package/package.json +14 -13
  178. package/schemas/WebpackOptions.check.js +1 -1
  179. package/schemas/WebpackOptions.json +759 -36
  180. package/schemas/plugins/HtmlParserOptions.check.js +1 -1
  181. package/schemas/plugins/ProgressPlugin.check.js +1 -1
  182. package/schemas/plugins/ProgressPlugin.json +17 -1
  183. package/schemas/plugins/container/ContainerReferencePlugin.check.js +1 -1
  184. package/schemas/plugins/container/ContainerReferencePlugin.json +1 -0
  185. package/schemas/plugins/container/ExternalsType.check.js +1 -1
  186. package/schemas/plugins/container/ModuleFederationPlugin.check.js +1 -1
  187. package/schemas/plugins/container/ModuleFederationPlugin.json +1 -0
  188. package/schemas/plugins/css/CssAutoOrModuleParserOptions.check.js +1 -1
  189. package/schemas/plugins/css/CssModuleParserOptions.check.js +1 -1
  190. package/schemas/plugins/css/CssParserOptions.check.js +1 -1
  191. package/types.d.ts +1300 -325
@@ -147,6 +147,7 @@ const CC_QUESTION_MARK = 0x3f;
147
147
  const CC_LEFT_SQUARE_BRACKET = 0x5b;
148
148
  const CC_RIGHT_SQUARE_BRACKET = 0x5d;
149
149
  const CC_GRAVE_ACCENT = 0x60;
150
+ const CC_NO_BREAK_SPACE = 0xa0;
150
151
 
151
152
  const QUOTE_DOUBLE = 1;
152
153
  const QUOTE_SINGLE = 2;
@@ -317,7 +318,7 @@ const getContentModeForTag = (name) => {
317
318
  * @param {string} lit lowercase ASCII literal
318
319
  * @returns {boolean} true if the range equals `lit` ignoring ASCII case
319
320
  */
320
- const rangeEqualsLower = (input, start, end, lit) => {
321
+ const rangeEqualsLowerCase = (input, start, end, lit) => {
321
322
  if (end - start !== lit.length) return false;
322
323
  for (let i = 0; i < lit.length; i++) {
323
324
  let c = input.charCodeAt(start + i);
@@ -339,27 +340,37 @@ const rangeEqualsLower = (input, start, end, lit) => {
339
340
  const getContentModeForRange = (input, start, end) => {
340
341
  switch (end - start) {
341
342
  case 3:
342
- if (rangeEqualsLower(input, start, end, "xmp")) return STATE_RAWTEXT;
343
+ if (rangeEqualsLowerCase(input, start, end, "xmp")) return STATE_RAWTEXT;
343
344
  return STATE_DATA;
344
345
  case 5:
345
- if (rangeEqualsLower(input, start, end, "title")) return STATE_RCDATA;
346
- if (rangeEqualsLower(input, start, end, "style")) return STATE_RAWTEXT;
346
+ if (rangeEqualsLowerCase(input, start, end, "title")) return STATE_RCDATA;
347
+ if (rangeEqualsLowerCase(input, start, end, "style")) {
348
+ return STATE_RAWTEXT;
349
+ }
347
350
  return STATE_DATA;
348
351
  case 6:
349
- if (rangeEqualsLower(input, start, end, "script")) {
352
+ if (rangeEqualsLowerCase(input, start, end, "script")) {
350
353
  return STATE_SCRIPT_DATA;
351
354
  }
352
- if (rangeEqualsLower(input, start, end, "iframe")) return STATE_RAWTEXT;
355
+ if (rangeEqualsLowerCase(input, start, end, "iframe")) {
356
+ return STATE_RAWTEXT;
357
+ }
353
358
  return STATE_DATA;
354
359
  case 7:
355
- if (rangeEqualsLower(input, start, end, "noembed")) return STATE_RAWTEXT;
360
+ if (rangeEqualsLowerCase(input, start, end, "noembed")) {
361
+ return STATE_RAWTEXT;
362
+ }
356
363
  return STATE_DATA;
357
364
  case 8:
358
- if (rangeEqualsLower(input, start, end, "textarea")) return STATE_RCDATA;
359
- if (rangeEqualsLower(input, start, end, "noframes")) return STATE_RAWTEXT;
365
+ if (rangeEqualsLowerCase(input, start, end, "textarea")) {
366
+ return STATE_RCDATA;
367
+ }
368
+ if (rangeEqualsLowerCase(input, start, end, "noframes")) {
369
+ return STATE_RAWTEXT;
370
+ }
360
371
  return STATE_DATA;
361
372
  case 9:
362
- if (rangeEqualsLower(input, start, end, "plaintext")) {
373
+ if (rangeEqualsLowerCase(input, start, end, "plaintext")) {
363
374
  return STATE_PLAINTEXT;
364
375
  }
365
376
  return STATE_DATA;
@@ -374,7 +385,7 @@ const getContentModeForRange = (input, start, end) => {
374
385
  * @param {HtmlTokenCallbacks} callbacks callbacks
375
386
  * @returns {number} final position
376
387
  */
377
- const walkHtmlTokens = (input, pos = 0, callbacks = {}) => {
388
+ const tokenize = (input, pos = 0, callbacks = {}) => {
378
389
  const len = input.length;
379
390
  let state = STATE_DATA;
380
391
  let returnState = STATE_DATA;
@@ -383,9 +394,9 @@ const walkHtmlTokens = (input, pos = 0, callbacks = {}) => {
383
394
  let tagStart = pos;
384
395
  let tagNameStart = -1;
385
396
  let tagNameEnd = -1;
386
- let attrNameStart = -1;
387
- let attrNameEnd = -1;
388
- let attrValueStart = -1;
397
+ let attributeNameStart = -1;
398
+ let attributeNameEnd = -1;
399
+ let attributeValueStart = -1;
389
400
  let attrQuoteType = QUOTE_NONE;
390
401
  let commentStart = pos;
391
402
  let lastOpenTagName = "";
@@ -406,6 +417,18 @@ const walkHtmlTokens = (input, pos = 0, callbacks = {}) => {
406
417
  // Tracks whether the current tag has parsed any attributes — used to
407
418
  // fire the `end-tag-with-attributes` parse error when an end tag emits.
408
419
  let tagHasAttributes = false;
420
+ // Memoized next occurrence of `<` / `&` / NUL for the text-run
421
+ // fast-forwards: native `indexOf` beats a per-char JS loop, and `pos` only
422
+ // moves forward, so each memo is refreshed at most once per occurrence
423
+ // (`len` = no further occurrence).
424
+ let nextLt = -1;
425
+ let nextAmp = -1;
426
+ let nextNul = -1;
427
+ // Same memo scheme for the closing quote of quoted attribute values (long
428
+ // values: data: URIs, srcset, inline style). Memoized so a value with many
429
+ // `&` references doesn't re-run the quote scan per reference.
430
+ let nextDQuote = -1;
431
+ let nextSQuote = -1;
409
432
 
410
433
  /**
411
434
  * Reports a tokenizer parse error to the consumer. The offset range and
@@ -429,7 +452,7 @@ const walkHtmlTokens = (input, pos = 0, callbacks = {}) => {
429
452
  * the accumulated `charRefCode`, if any. Used both inline (when the
430
453
  * reference is terminated by a real next character) and at EOF (when the
431
454
  * reference runs to the end of input). The scanner only flags the error —
432
- * the spec's U+FFFD / Windows-1252 substitution is done by `decodeHtmlEntities`.
455
+ * the spec's U+FFFD / Windows-1252 substitution is done by `decodeEntities`.
433
456
  * @param {number} endPos offset just past the reference
434
457
  */
435
458
  const validateNumericReference = (endPos) => {
@@ -526,19 +549,19 @@ const walkHtmlTokens = (input, pos = 0, callbacks = {}) => {
526
549
  // the callback is expected to do the same advance based on the
527
550
  // reported `quoteType`.
528
551
  let nextPos = attrQuoteType === QUOTE_NONE ? endPos : endPos + 1;
529
- if (callbacks.attribute !== undefined && attrNameStart !== -1) {
552
+ if (callbacks.attribute !== undefined && attributeNameStart !== -1) {
530
553
  nextPos = callbacks.attribute(
531
554
  input,
532
- attrNameStart,
533
- attrNameEnd,
534
- attrValueStart,
535
- attrValueStart === -1 ? -1 : endPos,
555
+ attributeNameStart,
556
+ attributeNameEnd,
557
+ attributeValueStart,
558
+ attributeValueStart === -1 ? -1 : endPos,
536
559
  attrQuoteType
537
560
  );
538
561
  }
539
- if (attrNameStart !== -1) tagHasAttributes = true;
540
- attrNameStart = -1;
541
- attrValueStart = -1;
562
+ if (attributeNameStart !== -1) tagHasAttributes = true;
563
+ attributeNameStart = -1;
564
+ attributeValueStart = -1;
542
565
  attrQuoteType = QUOTE_NONE;
543
566
  return nextPos;
544
567
  };
@@ -600,7 +623,7 @@ const walkHtmlTokens = (input, pos = 0, callbacks = {}) => {
600
623
  // All WHATWG tokenizer states handled. Deliberately omitted parse errors
601
624
  // (need state this offset scanner lacks): duplicate-attribute,
602
625
  // cdata-in-html-content, `*-in-input-stream`. Reference substitution is
603
- // left to `decodeHtmlEntities`.
626
+ // left to `decodeEntities`.
604
627
  switch (state) {
605
628
  // https://html.spec.whatwg.org/multipage/parsing.html#data-state
606
629
  case STATE_DATA:
@@ -625,15 +648,23 @@ const walkHtmlTokens = (input, pos = 0, callbacks = {}) => {
625
648
  pos++;
626
649
  } else {
627
650
  // Fast-forward over the run of ordinary text without re-entering
628
- // the per-state switch; stop on the next significant code point.
651
+ // the per-state switch — batches only the data state's "Anything
652
+ // else — emit the current input character" arc; the stops are
653
+ // exactly the code points with their own arcs (`<`, `&`, NUL).
629
654
  pos++;
630
- while (pos < len) {
631
- const c2 = input.charCodeAt(pos);
632
- if (c2 === CC_LESS_THAN || c2 === CC_AMPERSAND || c2 === CC_NULL) {
633
- break;
634
- }
635
- pos++;
655
+ if (nextLt < pos) {
656
+ nextLt = input.indexOf("<", pos);
657
+ if (nextLt === -1) nextLt = len;
658
+ }
659
+ if (nextAmp < pos) {
660
+ nextAmp = input.indexOf("&", pos);
661
+ if (nextAmp === -1) nextAmp = len;
662
+ }
663
+ if (nextNul < pos) {
664
+ nextNul = input.indexOf("\0", pos);
665
+ if (nextNul === -1) nextNul = len;
636
666
  }
667
+ pos = Math.min(nextLt, nextAmp, nextNul);
637
668
  }
638
669
  break;
639
670
 
@@ -806,14 +837,14 @@ const walkHtmlTokens = (input, pos = 0, callbacks = {}) => {
806
837
  pos + 1,
807
838
  "warning"
808
839
  );
809
- attrNameStart = pos;
840
+ attributeNameStart = pos;
810
841
  state = STATE_ATTRIBUTE_NAME;
811
842
  pos++;
812
843
  } else {
813
844
  // Anything else
814
845
  // Start a new attribute in the current tag token. Set that attribute name
815
846
  // and value to the empty string. Reconsume in the attribute name state.
816
- attrNameStart = pos;
847
+ attributeNameStart = pos;
817
848
  state = STATE_ATTRIBUTE_NAME;
818
849
  // Reconsume
819
850
  }
@@ -831,11 +862,11 @@ const walkHtmlTokens = (input, pos = 0, callbacks = {}) => {
831
862
  // EOF
832
863
  // Reconsume in the after attribute name state.
833
864
  if (isSpace(cc) || cc === CC_SOLIDUS || cc === CC_GREATER_THAN) {
834
- attrNameEnd = pos;
865
+ attributeNameEnd = pos;
835
866
  state = STATE_AFTER_ATTRIBUTE_NAME;
836
867
  // Reconsume
837
868
  } else if (cc === CC_EQUALS) {
838
- attrNameEnd = pos;
869
+ attributeNameEnd = pos;
839
870
  state = STATE_BEFORE_ATTRIBUTE_VALUE;
840
871
  pos++;
841
872
  } else {
@@ -915,7 +946,7 @@ const walkHtmlTokens = (input, pos = 0, callbacks = {}) => {
915
946
  // Anything else
916
947
  // Start a new attribute in the current tag token.
917
948
  emitAttribute(pos);
918
- attrNameStart = pos;
949
+ attributeNameStart = pos;
919
950
  state = STATE_ATTRIBUTE_NAME;
920
951
  // Reconsume
921
952
  }
@@ -934,14 +965,14 @@ const walkHtmlTokens = (input, pos = 0, callbacks = {}) => {
934
965
  } else if (cc === CC_QUOTATION_MARK) {
935
966
  // U+0022 QUOTATION MARK (")
936
967
  // Switch to the attribute value (double-quoted) state.
937
- attrValueStart = pos + 1;
968
+ attributeValueStart = pos + 1;
938
969
  attrQuoteType = QUOTE_DOUBLE;
939
970
  state = STATE_ATTRIBUTE_VALUE_DOUBLE_QUOTED;
940
971
  pos++;
941
972
  } else if (cc === CC_APOSTROPHE) {
942
973
  // U+0027 APOSTROPHE (')
943
974
  // Switch to the attribute value (single-quoted) state.
944
- attrValueStart = pos + 1;
975
+ attributeValueStart = pos + 1;
945
976
  attrQuoteType = QUOTE_SINGLE;
946
977
  state = STATE_ATTRIBUTE_VALUE_SINGLE_QUOTED;
947
978
  pos++;
@@ -952,7 +983,7 @@ const walkHtmlTokens = (input, pos = 0, callbacks = {}) => {
952
983
  // an empty value range pointing at the `>` so the open-tag offset range
953
984
  // still includes the `>`.
954
985
  reportError("missing-attribute-value", pos, pos + 1, "warning");
955
- attrValueStart = pos;
986
+ attributeValueStart = pos;
956
987
  attrQuoteType = QUOTE_NONE;
957
988
  pos = emitAttribute(pos);
958
989
  if (input.charCodeAt(tagStart + 1) === CC_SOLIDUS) {
@@ -966,7 +997,7 @@ const walkHtmlTokens = (input, pos = 0, callbacks = {}) => {
966
997
  } else {
967
998
  // Anything else
968
999
  // Reconsume in the attribute value (unquoted) state.
969
- attrValueStart = pos;
1000
+ attributeValueStart = pos;
970
1001
  attrQuoteType = QUOTE_NONE;
971
1002
  state = STATE_ATTRIBUTE_VALUE_UNQUOTED;
972
1003
  // Reconsume
@@ -993,19 +1024,22 @@ const walkHtmlTokens = (input, pos = 0, callbacks = {}) => {
993
1024
  reportError("unexpected-null-character", pos, pos + 1, "warning");
994
1025
  pos++;
995
1026
  } else {
996
- // Fast-forward over the ordinary run of the quoted value.
1027
+ // Fast-forward over the ordinary run of the quoted value with the
1028
+ // same memoized native-scan scheme as the data state.
997
1029
  pos++;
998
- while (pos < len) {
999
- const c2 = input.charCodeAt(pos);
1000
- if (
1001
- c2 === CC_QUOTATION_MARK ||
1002
- c2 === CC_AMPERSAND ||
1003
- c2 === CC_NULL
1004
- ) {
1005
- break;
1006
- }
1007
- pos++;
1030
+ if (nextDQuote < pos) {
1031
+ nextDQuote = input.indexOf('"', pos);
1032
+ if (nextDQuote === -1) nextDQuote = len;
1033
+ }
1034
+ if (nextAmp < pos) {
1035
+ nextAmp = input.indexOf("&", pos);
1036
+ if (nextAmp === -1) nextAmp = len;
1008
1037
  }
1038
+ if (nextNul < pos) {
1039
+ nextNul = input.indexOf("\0", pos);
1040
+ if (nextNul === -1) nextNul = len;
1041
+ }
1042
+ pos = Math.min(nextDQuote, nextAmp, nextNul);
1009
1043
  }
1010
1044
  break;
1011
1045
 
@@ -1029,15 +1063,22 @@ const walkHtmlTokens = (input, pos = 0, callbacks = {}) => {
1029
1063
  reportError("unexpected-null-character", pos, pos + 1, "warning");
1030
1064
  pos++;
1031
1065
  } else {
1032
- // Fast-forward over the ordinary run of the quoted value.
1066
+ // Fast-forward over the ordinary run of the quoted value with the
1067
+ // same memoized native-scan scheme as the data state.
1033
1068
  pos++;
1034
- while (pos < len) {
1035
- const c2 = input.charCodeAt(pos);
1036
- if (c2 === CC_APOSTROPHE || c2 === CC_AMPERSAND || c2 === CC_NULL) {
1037
- break;
1038
- }
1039
- pos++;
1069
+ if (nextSQuote < pos) {
1070
+ nextSQuote = input.indexOf("'", pos);
1071
+ if (nextSQuote === -1) nextSQuote = len;
1040
1072
  }
1073
+ if (nextAmp < pos) {
1074
+ nextAmp = input.indexOf("&", pos);
1075
+ if (nextAmp === -1) nextAmp = len;
1076
+ }
1077
+ if (nextNul < pos) {
1078
+ nextNul = input.indexOf("\0", pos);
1079
+ if (nextNul === -1) nextNul = len;
1080
+ }
1081
+ pos = Math.min(nextSQuote, nextAmp, nextNul);
1041
1082
  }
1042
1083
  break;
1043
1084
 
@@ -1301,7 +1342,20 @@ const walkHtmlTokens = (input, pos = 0, callbacks = {}) => {
1301
1342
  if (cc === CC_NULL) {
1302
1343
  reportError("unexpected-null-character", pos, pos + 1, "warning");
1303
1344
  }
1345
+ // Fast-forward ordinary comment text without re-entering the
1346
+ // state switch; stop on the significant code points above.
1304
1347
  pos++;
1348
+ while (pos < len) {
1349
+ const c2 = input.charCodeAt(pos);
1350
+ if (
1351
+ c2 === CC_LESS_THAN ||
1352
+ c2 === CC_HYPHEN_MINUS ||
1353
+ c2 === CC_NULL
1354
+ ) {
1355
+ break;
1356
+ }
1357
+ pos++;
1358
+ }
1305
1359
  }
1306
1360
  break;
1307
1361
 
@@ -1403,7 +1457,14 @@ const walkHtmlTokens = (input, pos = 0, callbacks = {}) => {
1403
1457
  if (cc === CC_NULL) {
1404
1458
  reportError("unexpected-null-character", pos, pos + 1, "warning");
1405
1459
  }
1460
+ // Fast-forward ordinary bogus-comment text without re-entering the
1461
+ // state switch; stop on the significant code points above.
1406
1462
  pos++;
1463
+ while (pos < len) {
1464
+ const c2 = input.charCodeAt(pos);
1465
+ if (c2 === CC_GREATER_THAN || c2 === CC_NULL) break;
1466
+ pos++;
1467
+ }
1407
1468
  }
1408
1469
  break;
1409
1470
 
@@ -2250,7 +2311,10 @@ const walkHtmlTokens = (input, pos = 0, callbacks = {}) => {
2250
2311
  } else {
2251
2312
  // Anything else
2252
2313
  // Emit the current input character as a character token.
2253
- pos++;
2314
+ // Fast-forward to the next `]` (the only code point with its own
2315
+ // arc) in one native scan.
2316
+ pos = input.indexOf("]", pos + 1);
2317
+ if (pos === -1) pos = len;
2254
2318
  }
2255
2319
  break;
2256
2320
 
@@ -2319,15 +2383,23 @@ const walkHtmlTokens = (input, pos = 0, callbacks = {}) => {
2319
2383
  reportError("unexpected-null-character", pos, pos + 1, "warning");
2320
2384
  pos++;
2321
2385
  } else {
2322
- // Fast-forward over ordinary RCDATA text.
2386
+ // Fast-forward over ordinary RCDATA text (same memoized
2387
+ // `indexOf` scheme as the data state) — batches only this
2388
+ // state's "anything else" arc; `&` / `<` / NUL keep their own.
2323
2389
  pos++;
2324
- while (pos < len) {
2325
- const c2 = input.charCodeAt(pos);
2326
- if (c2 === CC_AMPERSAND || c2 === CC_LESS_THAN || c2 === CC_NULL) {
2327
- break;
2328
- }
2329
- pos++;
2390
+ if (nextLt < pos) {
2391
+ nextLt = input.indexOf("<", pos);
2392
+ if (nextLt === -1) nextLt = len;
2330
2393
  }
2394
+ if (nextAmp < pos) {
2395
+ nextAmp = input.indexOf("&", pos);
2396
+ if (nextAmp === -1) nextAmp = len;
2397
+ }
2398
+ if (nextNul < pos) {
2399
+ nextNul = input.indexOf("\0", pos);
2400
+ if (nextNul === -1) nextNul = len;
2401
+ }
2402
+ pos = Math.min(nextLt, nextAmp, nextNul);
2331
2403
  }
2332
2404
  break;
2333
2405
 
@@ -2382,7 +2454,12 @@ const walkHtmlTokens = (input, pos = 0, callbacks = {}) => {
2382
2454
  if (isSpace(cc)) {
2383
2455
  tagNameEnd = pos;
2384
2456
  if (
2385
- rangeEqualsLower(input, tagNameStart, tagNameEnd, lastOpenTagName)
2457
+ rangeEqualsLowerCase(
2458
+ input,
2459
+ tagNameStart,
2460
+ tagNameEnd,
2461
+ lastOpenTagName
2462
+ )
2386
2463
  ) {
2387
2464
  flushText(tagStart);
2388
2465
  state = STATE_BEFORE_ATTRIBUTE_NAME;
@@ -2398,7 +2475,12 @@ const walkHtmlTokens = (input, pos = 0, callbacks = {}) => {
2398
2475
  // "anything else" entry below.
2399
2476
  tagNameEnd = pos;
2400
2477
  if (
2401
- rangeEqualsLower(input, tagNameStart, tagNameEnd, lastOpenTagName)
2478
+ rangeEqualsLowerCase(
2479
+ input,
2480
+ tagNameStart,
2481
+ tagNameEnd,
2482
+ lastOpenTagName
2483
+ )
2402
2484
  ) {
2403
2485
  flushText(tagStart);
2404
2486
  state = STATE_SELF_CLOSING_START_TAG;
@@ -2414,7 +2496,12 @@ const walkHtmlTokens = (input, pos = 0, callbacks = {}) => {
2414
2496
  // per the "anything else" entry below.
2415
2497
  tagNameEnd = pos;
2416
2498
  if (
2417
- rangeEqualsLower(input, tagNameStart, tagNameEnd, lastOpenTagName)
2499
+ rangeEqualsLowerCase(
2500
+ input,
2501
+ tagNameStart,
2502
+ tagNameEnd,
2503
+ lastOpenTagName
2504
+ )
2418
2505
  ) {
2419
2506
  flushText(tagStart);
2420
2507
  state = STATE_DATA;
@@ -2454,13 +2541,19 @@ const walkHtmlTokens = (input, pos = 0, callbacks = {}) => {
2454
2541
  reportError("unexpected-null-character", pos, pos + 1, "warning");
2455
2542
  pos++;
2456
2543
  } else {
2457
- // Fast-forward over ordinary RAWTEXT text.
2544
+ // Fast-forward over ordinary RAWTEXT text (same memoized
2545
+ // `indexOf` scheme as the data state) — batches only this
2546
+ // state's "anything else" arc; `<` / NUL keep their own.
2458
2547
  pos++;
2459
- while (pos < len) {
2460
- const c2 = input.charCodeAt(pos);
2461
- if (c2 === CC_LESS_THAN || c2 === CC_NULL) break;
2462
- pos++;
2548
+ if (nextLt < pos) {
2549
+ nextLt = input.indexOf("<", pos);
2550
+ if (nextLt === -1) nextLt = len;
2551
+ }
2552
+ if (nextNul < pos) {
2553
+ nextNul = input.indexOf("\0", pos);
2554
+ if (nextNul === -1) nextNul = len;
2463
2555
  }
2556
+ pos = Math.min(nextLt, nextNul);
2464
2557
  }
2465
2558
  break;
2466
2559
 
@@ -2514,7 +2607,12 @@ const walkHtmlTokens = (input, pos = 0, callbacks = {}) => {
2514
2607
  if (isSpace(cc)) {
2515
2608
  tagNameEnd = pos;
2516
2609
  if (
2517
- rangeEqualsLower(input, tagNameStart, tagNameEnd, lastOpenTagName)
2610
+ rangeEqualsLowerCase(
2611
+ input,
2612
+ tagNameStart,
2613
+ tagNameEnd,
2614
+ lastOpenTagName
2615
+ )
2518
2616
  ) {
2519
2617
  flushText(tagStart);
2520
2618
  state = STATE_BEFORE_ATTRIBUTE_NAME;
@@ -2529,7 +2627,12 @@ const walkHtmlTokens = (input, pos = 0, callbacks = {}) => {
2529
2627
  // "anything else" entry below.
2530
2628
  tagNameEnd = pos;
2531
2629
  if (
2532
- rangeEqualsLower(input, tagNameStart, tagNameEnd, lastOpenTagName)
2630
+ rangeEqualsLowerCase(
2631
+ input,
2632
+ tagNameStart,
2633
+ tagNameEnd,
2634
+ lastOpenTagName
2635
+ )
2533
2636
  ) {
2534
2637
  flushText(tagStart);
2535
2638
  state = STATE_SELF_CLOSING_START_TAG;
@@ -2544,7 +2647,12 @@ const walkHtmlTokens = (input, pos = 0, callbacks = {}) => {
2544
2647
  // per the "anything else" entry below.
2545
2648
  tagNameEnd = pos;
2546
2649
  if (
2547
- rangeEqualsLower(input, tagNameStart, tagNameEnd, lastOpenTagName)
2650
+ rangeEqualsLowerCase(
2651
+ input,
2652
+ tagNameStart,
2653
+ tagNameEnd,
2654
+ lastOpenTagName
2655
+ )
2548
2656
  ) {
2549
2657
  flushText(tagStart);
2550
2658
  state = STATE_DATA;
@@ -2583,13 +2691,19 @@ const walkHtmlTokens = (input, pos = 0, callbacks = {}) => {
2583
2691
  reportError("unexpected-null-character", pos, pos + 1, "warning");
2584
2692
  pos++;
2585
2693
  } else {
2586
- // Fast-forward over ordinary script-data text.
2694
+ // Fast-forward over ordinary script-data text (same memoized
2695
+ // `indexOf` scheme as the data state) — batches only this
2696
+ // state's "anything else" arc; `<` / NUL keep their own.
2587
2697
  pos++;
2588
- while (pos < len) {
2589
- const c2 = input.charCodeAt(pos);
2590
- if (c2 === CC_LESS_THAN || c2 === CC_NULL) break;
2591
- pos++;
2698
+ if (nextLt < pos) {
2699
+ nextLt = input.indexOf("<", pos);
2700
+ if (nextLt === -1) nextLt = len;
2701
+ }
2702
+ if (nextNul < pos) {
2703
+ nextNul = input.indexOf("\0", pos);
2704
+ if (nextNul === -1) nextNul = len;
2592
2705
  }
2706
+ pos = Math.min(nextLt, nextNul);
2593
2707
  }
2594
2708
  break;
2595
2709
 
@@ -2649,7 +2763,12 @@ const walkHtmlTokens = (input, pos = 0, callbacks = {}) => {
2649
2763
  if (isSpace(cc)) {
2650
2764
  tagNameEnd = pos;
2651
2765
  if (
2652
- rangeEqualsLower(input, tagNameStart, tagNameEnd, lastOpenTagName)
2766
+ rangeEqualsLowerCase(
2767
+ input,
2768
+ tagNameStart,
2769
+ tagNameEnd,
2770
+ lastOpenTagName
2771
+ )
2653
2772
  ) {
2654
2773
  flushText(tagStart);
2655
2774
  state = STATE_BEFORE_ATTRIBUTE_NAME;
@@ -2664,7 +2783,12 @@ const walkHtmlTokens = (input, pos = 0, callbacks = {}) => {
2664
2783
  // "anything else" entry below.
2665
2784
  tagNameEnd = pos;
2666
2785
  if (
2667
- rangeEqualsLower(input, tagNameStart, tagNameEnd, lastOpenTagName)
2786
+ rangeEqualsLowerCase(
2787
+ input,
2788
+ tagNameStart,
2789
+ tagNameEnd,
2790
+ lastOpenTagName
2791
+ )
2668
2792
  ) {
2669
2793
  flushText(tagStart);
2670
2794
  state = STATE_SELF_CLOSING_START_TAG;
@@ -2679,7 +2803,12 @@ const walkHtmlTokens = (input, pos = 0, callbacks = {}) => {
2679
2803
  // per the "anything else" entry below.
2680
2804
  tagNameEnd = pos;
2681
2805
  if (
2682
- rangeEqualsLower(input, tagNameStart, tagNameEnd, lastOpenTagName)
2806
+ rangeEqualsLowerCase(
2807
+ input,
2808
+ tagNameStart,
2809
+ tagNameEnd,
2810
+ lastOpenTagName
2811
+ )
2683
2812
  ) {
2684
2813
  flushText(tagStart);
2685
2814
  state = STATE_DATA;
@@ -2757,7 +2886,20 @@ const walkHtmlTokens = (input, pos = 0, callbacks = {}) => {
2757
2886
  if (cc === CC_NULL) {
2758
2887
  reportError("unexpected-null-character", pos, pos + 1, "warning");
2759
2888
  }
2889
+ // Fast-forward ordinary escaped script text without re-entering
2890
+ // the state switch; stop on the significant code points above.
2760
2891
  pos++;
2892
+ while (pos < len) {
2893
+ const c2 = input.charCodeAt(pos);
2894
+ if (
2895
+ c2 === CC_HYPHEN_MINUS ||
2896
+ c2 === CC_LESS_THAN ||
2897
+ c2 === CC_NULL
2898
+ ) {
2899
+ break;
2900
+ }
2901
+ pos++;
2902
+ }
2761
2903
  }
2762
2904
  break;
2763
2905
 
@@ -2877,7 +3019,12 @@ const walkHtmlTokens = (input, pos = 0, callbacks = {}) => {
2877
3019
  if (isSpace(cc)) {
2878
3020
  tagNameEnd = pos;
2879
3021
  if (
2880
- rangeEqualsLower(input, tagNameStart, tagNameEnd, lastOpenTagName)
3022
+ rangeEqualsLowerCase(
3023
+ input,
3024
+ tagNameStart,
3025
+ tagNameEnd,
3026
+ lastOpenTagName
3027
+ )
2881
3028
  ) {
2882
3029
  flushText(tagStart);
2883
3030
  state = STATE_BEFORE_ATTRIBUTE_NAME;
@@ -2892,7 +3039,12 @@ const walkHtmlTokens = (input, pos = 0, callbacks = {}) => {
2892
3039
  // "anything else" entry below.
2893
3040
  tagNameEnd = pos;
2894
3041
  if (
2895
- rangeEqualsLower(input, tagNameStart, tagNameEnd, lastOpenTagName)
3042
+ rangeEqualsLowerCase(
3043
+ input,
3044
+ tagNameStart,
3045
+ tagNameEnd,
3046
+ lastOpenTagName
3047
+ )
2896
3048
  ) {
2897
3049
  flushText(tagStart);
2898
3050
  state = STATE_SELF_CLOSING_START_TAG;
@@ -2907,7 +3059,12 @@ const walkHtmlTokens = (input, pos = 0, callbacks = {}) => {
2907
3059
  // per the "anything else" entry below.
2908
3060
  tagNameEnd = pos;
2909
3061
  if (
2910
- rangeEqualsLower(input, tagNameStart, tagNameEnd, lastOpenTagName)
3062
+ rangeEqualsLowerCase(
3063
+ input,
3064
+ tagNameStart,
3065
+ tagNameEnd,
3066
+ lastOpenTagName
3067
+ )
2911
3068
  ) {
2912
3069
  flushText(tagStart);
2913
3070
  state = STATE_DATA;
@@ -2989,7 +3146,20 @@ const walkHtmlTokens = (input, pos = 0, callbacks = {}) => {
2989
3146
  if (cc === CC_NULL) {
2990
3147
  reportError("unexpected-null-character", pos, pos + 1, "warning");
2991
3148
  }
3149
+ // Fast-forward ordinary double-escaped script text without re-entering
3150
+ // the state switch; stop on the significant code points above.
2992
3151
  pos++;
3152
+ while (pos < len) {
3153
+ const c2 = input.charCodeAt(pos);
3154
+ if (
3155
+ c2 === CC_HYPHEN_MINUS ||
3156
+ c2 === CC_LESS_THAN ||
3157
+ c2 === CC_NULL
3158
+ ) {
3159
+ break;
3160
+ }
3161
+ pos++;
3162
+ }
2993
3163
  }
2994
3164
  break;
2995
3165
 
@@ -3175,25 +3345,25 @@ const walkHtmlTokens = (input, pos = 0, callbacks = {}) => {
3175
3345
  pos + runLen < len && input.charCodeAt(pos + runLen) === CC_SEMICOLON;
3176
3346
  namedEntityConsumed = 0;
3177
3347
  let matchedWithSemicolon = false;
3178
- // Slice the candidate run from `input` once; prefixes are taken from
3179
- // this short string instead of re-slicing the (potentially huge)
3180
- // input per length.
3181
- const run = input.slice(pos, pos + runLen);
3182
- for (let n = runLen; n > 0; n--) {
3183
- const bare = n === runLen ? run : run.slice(0, n);
3184
- // Try with trailing `;` first if one is present after the run.
3185
- if (
3186
- n === runLen &&
3187
- hasSemicolon &&
3188
- HTML_ENTITIES[`${bare};`] !== undefined
3189
- ) {
3190
- namedEntityConsumed = n + 1;
3348
+ // Try the full run with its trailing `;` first — the overwhelmingly
3349
+ // common case (`&amp;`, `&nbsp;`, …) then needs exactly one slice.
3350
+ if (hasSemicolon && runLen > 0) {
3351
+ const withSemicolon = input.slice(pos, pos + runLen + 1);
3352
+ if (HTML_ENTITIES[withSemicolon] !== undefined) {
3353
+ namedEntityConsumed = runLen + 1;
3191
3354
  matchedWithSemicolon = true;
3192
- break;
3193
3355
  }
3194
- if (HTML_ENTITIES[bare] !== undefined) {
3195
- namedEntityConsumed = n;
3196
- break;
3356
+ }
3357
+ if (namedEntityConsumed === 0) {
3358
+ // Slice the candidate run once; prefixes come from this short
3359
+ // string instead of re-slicing the input per length.
3360
+ const run = input.slice(pos, pos + runLen);
3361
+ for (let n = runLen; n > 0; n--) {
3362
+ const bare = n === runLen ? run : run.slice(0, n);
3363
+ if (HTML_ENTITIES[bare] !== undefined) {
3364
+ namedEntityConsumed = n;
3365
+ break;
3366
+ }
3197
3367
  }
3198
3368
  }
3199
3369
  if (namedEntityConsumed > 0) {
@@ -3208,12 +3378,10 @@ const walkHtmlTokens = (input, pos = 0, callbacks = {}) => {
3208
3378
  returnState === STATE_ATTRIBUTE_VALUE_DOUBLE_QUOTED ||
3209
3379
  returnState === STATE_ATTRIBUTE_VALUE_SINGLE_QUOTED ||
3210
3380
  returnState === STATE_ATTRIBUTE_VALUE_UNQUOTED;
3211
- if (
3212
- !(
3213
- inAttribute &&
3214
- (next === CC_EQUALS || isAsciiAlphanumeric(next))
3215
- )
3216
- ) {
3381
+ if (!(
3382
+ inAttribute &&
3383
+ (next === CC_EQUALS || isAsciiAlphanumeric(next))
3384
+ )) {
3217
3385
  reportError(
3218
3386
  "missing-semicolon-after-character-reference",
3219
3387
  pos + namedEntityConsumed,
@@ -3470,11 +3638,11 @@ const walkHtmlTokens = (input, pos = 0, callbacks = {}) => {
3470
3638
  // dropping the in-progress tag, we emit its offset range up to EOF.
3471
3639
  reportError("eof-in-tag", len, len, "error");
3472
3640
  // If we hit EOF mid-attribute-name, the name runs to EOF. Set
3473
- // attrNameEnd here so the emitted attribute range is valid.
3474
- if (state === STATE_ATTRIBUTE_NAME && attrNameStart !== -1) {
3475
- attrNameEnd = len;
3641
+ // attributeNameEnd here so the emitted attribute range is valid.
3642
+ if (state === STATE_ATTRIBUTE_NAME && attributeNameStart !== -1) {
3643
+ attributeNameEnd = len;
3476
3644
  }
3477
- if (attrNameStart !== -1) emitAttribute(len);
3645
+ if (attributeNameStart !== -1) emitAttribute(len);
3478
3646
  // If we hit EOF before the tag-name end was recorded, the name runs
3479
3647
  // to EOF. `tagNameEnd` may carry over from a previously emitted tag,
3480
3648
  // so reset it whenever it's missing or stale (less than `tagNameStart`)
@@ -3607,15 +3775,6 @@ const decodeNumericReference = (code) => {
3607
3775
  return String.fromCodePoint(code);
3608
3776
  };
3609
3777
 
3610
- // Match one of three forms (each with an optional trailing `;`):
3611
- // `&#x<hex>` - hex numeric reference (requires the `x`/`X`).
3612
- // `&#<dec>` - decimal numeric reference (digits only).
3613
- // `&<name>` - named reference (letter followed by alphanumerics).
3614
- // The three alternatives are kept separate so a decimal reference like
3615
- // `&#65b` doesn't greedily eat the trailing `b` as if it were hex.
3616
- const CHARACTER_REFERENCE_REGEXP =
3617
- /&(?:#[xX][0-9a-fA-F]+|#[0-9]+|[a-zA-Z][a-zA-Z0-9]*);?/g;
3618
-
3619
3778
  /**
3620
3779
  * Decode a single matched character reference.
3621
3780
  * @param {string} match the matched reference text
@@ -3623,45 +3782,34 @@ const CHARACTER_REFERENCE_REGEXP =
3623
3782
  * @param {boolean=} isAttribute true when the match came from an attribute value
3624
3783
  * @returns {string} decoded text, or `match` itself when it stays literal
3625
3784
  */
3626
- const decodeOneReference = (match, nextCharCode, isAttribute) => {
3627
- // Numeric reference: &#65; or &#x41;
3628
- if (match.charCodeAt(1) === 0x23 /* # */) {
3629
- const lastChar = match.charAt(match.length - 1);
3630
- const isHex = match.charCodeAt(2) === 0x78 || match.charCodeAt(2) === 0x58;
3631
- const body = isHex
3632
- ? lastChar === ";"
3633
- ? match.slice(3, -1)
3634
- : match.slice(3)
3635
- : lastChar === ";"
3636
- ? match.slice(2, -1)
3637
- : match.slice(2);
3638
- // The regex above guarantees at least one digit in `body`,
3639
- // so `parseInt` always returns a finite number here.
3640
- return decodeNumericReference(Number.parseInt(body, isHex ? 16 : 10));
3641
- }
3642
-
3643
- // Named reference. Try the full captured name first, then
3644
- // progressively shorter prefixes - this handles direct matches
3645
- // like `&amp;` as well as WHATWG longest-prefix semantics where
3646
- // e.g. `&notpre;` decodes as `&not` (a legacy bare entity)
3647
- // followed by `pre;` as literal text.
3648
- const name = match.slice(1);
3785
+ /**
3786
+ * Decode a named character reference spanning `[start, end)` of `str` (the
3787
+ * `&` at `start`, the optional `;` included). Slices the name once; WHATWG
3788
+ * longest-prefix and consumed-as-part-of-an-attribute semantics as before.
3789
+ * @param {string} str the raw string
3790
+ * @param {number} start offset of the reference's `&`
3791
+ * @param {number} end offset just past the reference (past the `;` if present)
3792
+ * @param {number} nextCharCode char code following the reference (NaN at the end)
3793
+ * @param {boolean=} isAttribute true when the reference came from an attribute value
3794
+ * @returns {string | undefined} decoded text, or `undefined` when it stays literal
3795
+ */
3796
+ const decodeNamedReference = (str, start, end, nextCharCode, isAttribute) => {
3797
+ // Mirrors the old `match.slice(1)`: the name keeps its trailing `;`.
3798
+ const name = str.slice(start + 1, end);
3649
3799
  const matchEndsWithSemi = name.charCodeAt(name.length - 1) === 0x3b;
3650
3800
 
3651
3801
  // Attribute-context guard: if the entity match didn't end with `;`
3652
3802
  // and the next character in the source is `=` or ASCII
3653
3803
  // alphanumeric, the WHATWG spec says to flush the literal text
3654
- // rather than decode. The greedy regex already absorbed any
3804
+ // rather than decode. The greedy scan already absorbed any
3655
3805
  // trailing alphanumerics, so the only candidate "next char" here
3656
3806
  // is `=` (or any non-alphanumeric).
3657
3807
  if (isAttribute && !matchEndsWithSemi && nextCharCode === 0x3d /* = */) {
3658
- return match;
3808
+ return undefined;
3659
3809
  }
3660
3810
 
3661
- // Fast path: the regex usually captures exactly one entity (`&amp;`,
3662
- // `&lt;`, `&nbsp;`, …), so the whole `name` is the match — avoid the
3663
- // full-length `name.slice(0, name.length)` the loop's first iteration
3664
- // would allocate. No leftover, so the attribute guard never applies.
3811
+ // Fast path: the scan usually captures exactly one entity (`&amp;`,
3812
+ // `&lt;`, `&nbsp;`, ...), so the whole `name` is the match.
3665
3813
  if (name.length <= MAX_ENTITY_NAME_LEN) {
3666
3814
  const full = HTML_ENTITIES[name];
3667
3815
  if (full !== undefined) return full;
@@ -3680,23 +3828,21 @@ const decodeOneReference = (match, nextCharCode, isAttribute) => {
3680
3828
  // Attribute-context longest-prefix guard: if the matched
3681
3829
  // prefix doesn't end with `;` and the leftover starts with
3682
3830
  // an alphanumeric character, leave literal per WHATWG.
3683
- // (The regex greedy-consumes alphanumerics, so any leftover
3684
- // within `name` is itself alphanumeric — we only need to
3685
- // check non-empty leftover here; the `=` case is handled
3686
- // above against the source character after the match.)
3687
3831
  if (
3688
3832
  isAttribute &&
3689
3833
  i < name.length &&
3690
3834
  prefix.charCodeAt(prefix.length - 1) !== 0x3b
3691
3835
  ) {
3692
- return match;
3836
+ return undefined;
3693
3837
  }
3694
3838
  return HTML_ENTITIES[prefix] + name.slice(i);
3695
3839
  }
3696
3840
  }
3697
- return match;
3841
+ return undefined;
3698
3842
  };
3699
3843
 
3844
+ /** @typedef {{ text: string, map: number[] | undefined }} DecodedEntitiesWithMap */
3845
+
3700
3846
  /**
3701
3847
  * Decode HTML character references in a string. Handles all numeric
3702
3848
  * references (with WHATWG remap of 0x00, surrogates, out-of-range, and the
@@ -3708,70 +3854,246 @@ const decodeOneReference = (match, nextCharCode, isAttribute) => {
3708
3854
  * trailing `;` whose next character is `=` or ASCII alphanumeric is left
3709
3855
  * undecoded, so e.g. `&amp=foo` stays literal in an attribute value but
3710
3856
  * decodes to `&=foo` in text.
3857
+ *
3858
+ * With `withMap` the result also carries a boundary map from the decoded
3859
+ * string back to raw offsets, so spans computed on the decoded text (e.g.
3860
+ * srcset candidate URLs) can be translated to source ranges. `map[i]` is the
3861
+ * raw offset of decoded boundary `i` (`0..text.length`); boundaries inside a
3862
+ * reference's decoded text map to the reference start. `map` is `undefined`
3863
+ * when nothing was decoded (then `text === str`).
3864
+ * @overload
3711
3865
  * @param {string} str the raw string from the token slice
3712
3866
  * @param {boolean=} isAttribute true if `str` came from an attribute value
3713
3867
  * @returns {string} decoded string
3714
3868
  */
3715
- const decodeHtmlEntities = (str, isAttribute) => {
3716
- if (!str.includes("&")) return str;
3717
-
3718
- return str.replace(CHARACTER_REFERENCE_REGEXP, (match, offset, source) =>
3719
- decodeOneReference(
3720
- match,
3721
- source.charCodeAt(offset + match.length),
3722
- isAttribute
3723
- )
3724
- );
3725
- };
3726
-
3727
3869
  /**
3728
- * Like `decodeHtmlEntities`, but also returns a boundary map from the
3729
- * decoded string back to raw offsets, so spans computed on the decoded
3730
- * text (e.g. srcset candidate URLs) can be translated to source ranges.
3731
- * `map[i]` is the raw offset of decoded boundary `i` (`0..text.length`);
3732
- * boundaries inside a reference's decoded text map to the reference start.
3733
- * `map` is `undefined` when nothing was decoded (then `text === str`).
3870
+ * @overload
3871
+ * @param {string} str the raw string from the token slice
3872
+ * @param {boolean | undefined} isAttribute true if `str` came from an attribute value
3873
+ * @param {true} withMap also build the decoded-to-raw boundary map
3874
+ * @returns {DecodedEntitiesWithMap} decoded text and offset map
3875
+ */
3876
+ /**
3734
3877
  * @param {string} str the raw string from the token slice
3735
3878
  * @param {boolean=} isAttribute true if `str` came from an attribute value
3736
- * @returns {{ text: string, map: number[] | undefined }} decoded text and offset map
3879
+ * @param {boolean=} withMap also build the decoded-to-raw boundary map
3880
+ * @returns {string | DecodedEntitiesWithMap} decoded string (with map when `withMap`)
3737
3881
  */
3738
- const decodeHtmlEntitiesWithMap = (str, isAttribute) => {
3882
+ const decodeEntities = (str, isAttribute, withMap) => {
3883
+ // Hand-rolled scan of one of three reference forms, each with an optional
3884
+ // trailing `;` — `&#x<hex>+` / `&#<dec>+` / `&<alpha><alnum>*` (kept separate
3885
+ // so `&#65b` doesn't eat the `b` as hex): entity-dense text pays no regex
3886
+ // machinery or per-match object, and a string whose references all stay
3887
+ // literal is returned unchanged without rebuilding. Map bookkeeping is
3888
+ // gated on `withMap` at the rare per-decoded-reference points, so the
3889
+ // dominant no-map mode pays nothing for the unified implementation.
3890
+ let amp = str.indexOf("&");
3891
+ if (amp === -1) return withMap ? { text: str, map: undefined } : str;
3892
+ let out = "";
3893
+ let last = 0;
3739
3894
  /** @type {number[] | undefined} */
3740
3895
  let map;
3741
- if (str.includes("&")) {
3742
- CHARACTER_REFERENCE_REGEXP.lastIndex = 0;
3743
- let text = "";
3744
- let last = 0;
3745
- /** @type {RegExpExecArray | null} */
3746
- let m;
3747
- while ((m = CHARACTER_REFERENCE_REGEXP.exec(str)) !== null) {
3748
- const match = m[0];
3749
- const decoded = decodeOneReference(
3750
- match,
3751
- str.charCodeAt(m.index + match.length),
3752
- isAttribute
3753
- );
3754
- if (decoded === match) continue;
3755
- if (map === undefined) map = [];
3756
- for (let r = last; r < m.index; r++) map.push(r);
3757
- text += str.slice(last, m.index);
3758
- for (let i = 0; i < decoded.length; i++) map.push(m.index);
3759
- text += decoded;
3760
- last = m.index + match.length;
3761
- }
3762
- if (map !== undefined) {
3763
- for (let r = last; r < str.length; r++) map.push(r);
3764
- map.push(str.length);
3765
- return { text: text + str.slice(last), map };
3896
+ do {
3897
+ // `end` stays 0 when the `&` doesn't start a well-formed reference; a
3898
+ // numeric reference's code point accumulates during the digit scan
3899
+ // (saturating above the code-point range — every overflow decodes the
3900
+ // same replacement) so it needs no slice at all.
3901
+ let end = 0;
3902
+ let code = -1;
3903
+ let cc = str.charCodeAt(amp + 1);
3904
+ if (cc === 0x23 /* # */) {
3905
+ cc = str.charCodeAt(amp + 2);
3906
+ let p;
3907
+ if (cc === 0x78 /* x */ || cc === 0x58 /* X */) {
3908
+ code = 0;
3909
+ p = amp + 3;
3910
+ while (isAsciiHexDigit((cc = str.charCodeAt(p)))) {
3911
+ code = code * 16 + (cc <= 0x39 ? cc - 0x30 : (cc | 0x20) - 0x57);
3912
+ if (code > 0x10ffff) code = 0x110000;
3913
+ p++;
3914
+ }
3915
+ if (p > amp + 3) end = p;
3916
+ } else {
3917
+ code = 0;
3918
+ p = amp + 2;
3919
+ while (isAsciiDigit((cc = str.charCodeAt(p)))) {
3920
+ code = code * 10 + (cc - 0x30);
3921
+ if (code > 0x10ffff) code = 0x110000;
3922
+ p++;
3923
+ }
3924
+ if (p > amp + 2) end = p;
3925
+ }
3926
+ } else if (isAsciiAlpha(cc)) {
3927
+ code = -1;
3928
+ let p = amp + 2;
3929
+ while (isAsciiAlphanumeric(str.charCodeAt(p))) p++;
3930
+ end = p;
3931
+ }
3932
+ if (end !== 0) {
3933
+ if (str.charCodeAt(end) === 0x3b /* ; */) end++;
3934
+ // Numeric references always decode; named ones may stay literal.
3935
+ const decoded =
3936
+ code >= 0
3937
+ ? decodeNumericReference(code)
3938
+ : decodeNamedReference(
3939
+ str,
3940
+ amp,
3941
+ end,
3942
+ str.charCodeAt(end),
3943
+ isAttribute
3944
+ );
3945
+ if (decoded !== undefined) {
3946
+ if (withMap) map = _mapReference(map, last, amp, decoded.length);
3947
+ out += str.slice(last, amp);
3948
+ out += decoded;
3949
+ last = end;
3950
+ }
3951
+ amp = str.indexOf("&", end);
3952
+ } else {
3953
+ amp = str.indexOf("&", amp + 1);
3954
+ }
3955
+ } while (amp !== -1);
3956
+ if (last === 0) return withMap ? { text: str, map: undefined } : str;
3957
+ const text = out + str.slice(last);
3958
+ return withMap
3959
+ ? _finishMap(text, /** @type {number[]} */ (map), last, str.length)
3960
+ : text;
3961
+ };
3962
+
3963
+ // Map-mode bookkeeping for `decodeEntities`, kept out of the scanner body so
3964
+ // the dominant no-map mode stays small enough to inline.
3965
+ /**
3966
+ * @param {number[] | undefined} map boundary map so far
3967
+ * @param {number} last raw offset the literal run starts at
3968
+ * @param {number} amp raw offset of the decoded reference
3969
+ * @param {number} decodedLength length of the decoded text
3970
+ * @returns {number[]} the map, with the literal run's and reference's boundaries appended
3971
+ */
3972
+ const _mapReference = (map, last, amp, decodedLength) => {
3973
+ if (map === undefined) map = [];
3974
+ for (let r = last; r < amp; r++) map.push(r);
3975
+ for (let i = 0; i < decodedLength; i++) map.push(amp);
3976
+ return map;
3977
+ };
3978
+
3979
+ /**
3980
+ * @param {string} text decoded text
3981
+ * @param {number[]} map boundary map so far
3982
+ * @param {number} last raw offset of the trailing literal run
3983
+ * @param {number} rawLength raw string length
3984
+ * @returns {DecodedEntitiesWithMap} the finished result
3985
+ */
3986
+ const _finishMap = (text, map, last, rawLength) => {
3987
+ for (let r = last; r < rawLength; r++) map.push(r);
3988
+ map.push(rawLength);
3989
+ return { text, map };
3990
+ };
3991
+
3992
+ /**
3993
+ * Escape a string per the WHATWG "escape a string" algorithm
3994
+ * (https://html.spec.whatwg.org/multipage/parsing.html#escapingString) in
3995
+ * attribute mode: `&`, U+00A0 and `"`. CR/LF are additionally encoded as
3996
+ * numeric references so the result stays single-line (usable in a `data:`
3997
+ * URI). Single pass, returns the input unchanged when nothing needs escaping.
3998
+ * @param {string} s string to escape
3999
+ * @returns {string} HTML attribute-safe string
4000
+ */
4001
+ const escapeAttribute = (s) => {
4002
+ // fast path: native indexOf scans beat any per-character loop
4003
+ if (
4004
+ !s.includes("&") &&
4005
+ !s.includes('"') &&
4006
+ !s.includes("\u00A0") &&
4007
+ !s.includes("\n") &&
4008
+ !s.includes("\r")
4009
+ ) {
4010
+ return s;
4011
+ }
4012
+ let out = "";
4013
+ let last = 0;
4014
+ for (let i = 0; i < s.length; i++) {
4015
+ let rep;
4016
+ switch (s.charCodeAt(i)) {
4017
+ case CC_AMPERSAND:
4018
+ rep = "&amp;";
4019
+ break;
4020
+ case CC_QUOTATION_MARK:
4021
+ rep = "&quot;";
4022
+ break;
4023
+ case CC_NO_BREAK_SPACE:
4024
+ rep = "&nbsp;";
4025
+ break;
4026
+ case CC_LF:
4027
+ rep = "&#10;";
4028
+ break;
4029
+ case CC_CR:
4030
+ rep = "&#13;";
4031
+ break;
4032
+ default:
4033
+ continue;
3766
4034
  }
4035
+ out += s.slice(last, i) + rep;
4036
+ last = i + 1;
3767
4037
  }
3768
- return { text: str, map };
4038
+ return out + s.slice(last);
4039
+ };
4040
+
4041
+ /**
4042
+ * Escape a string per the WHATWG "escape a string" algorithm in text mode:
4043
+ * `&`, U+00A0, `<` and `>`. CR/LF are additionally encoded as numeric
4044
+ * references so the result stays single-line (usable in a `data:` URI).
4045
+ * Single pass, returns the input unchanged when nothing needs escaping.
4046
+ * @param {string} s string to escape
4047
+ * @returns {string} HTML text-content-safe string
4048
+ */
4049
+ const escapeText = (s) => {
4050
+ // fast path: native indexOf scans beat any per-character loop
4051
+ if (
4052
+ !s.includes("&") &&
4053
+ !s.includes("<") &&
4054
+ !s.includes(">") &&
4055
+ !s.includes("\u00A0") &&
4056
+ !s.includes("\n") &&
4057
+ !s.includes("\r")
4058
+ ) {
4059
+ return s;
4060
+ }
4061
+ let out = "";
4062
+ let last = 0;
4063
+ for (let i = 0; i < s.length; i++) {
4064
+ let rep;
4065
+ switch (s.charCodeAt(i)) {
4066
+ case CC_AMPERSAND:
4067
+ rep = "&amp;";
4068
+ break;
4069
+ case CC_LESS_THAN:
4070
+ rep = "&lt;";
4071
+ break;
4072
+ case CC_GREATER_THAN:
4073
+ rep = "&gt;";
4074
+ break;
4075
+ case CC_NO_BREAK_SPACE:
4076
+ rep = "&nbsp;";
4077
+ break;
4078
+ case CC_LF:
4079
+ rep = "&#10;";
4080
+ break;
4081
+ case CC_CR:
4082
+ rep = "&#13;";
4083
+ break;
4084
+ default:
4085
+ continue;
4086
+ }
4087
+ out += s.slice(last, i) + rep;
4088
+ last = i + 1;
4089
+ }
4090
+ return out + s.slice(last);
3769
4091
  };
3770
4092
 
3771
4093
  // cspell:ignore advasoft altglyph altglyphdef altglyphitem animatecolor animatemotion animatetransform arcrole aswedit attributename attributetype basefrequency baseprofile bgsound calcmode clippathunits definitionurl diffuseconstant fedropshadow filterunits glyphref gradienttransform gradientunits hotjava hotmetal kernelmatrix kernelunitlength keypoints keysplines keytimes limitingconeangle malignmark markerheight markerwidth maskcontentunits maskunits metrius mglyph mtext numoctaves pathlength patterncontentunits patterntransform patternunits pointsatx pointsaty pointsatz preservealpha primitiveunits refx refy repeatcount repeatdur requiredextensions requiredfeatures selectedcontent silmaril softquad specularconstant specularexponent startoffset stddeviation stitchtiles surfacescale systemlanguage tablevalues targetx targety textlength viewbox viewtarget webtechs xchannelselector ychannelselector megamorphic attributeless rowspan imagesizes novalidate maxlength
3772
4094
 
3773
4095
  // WHATWG HTML tree construction (https://html.spec.whatwg.org/multipage/parsing.html#tree-construction)
3774
- // on top of walkHtmlTokens. Scripting is always disabled (webpack is a build tool).
4096
+ // on top of tokenize. Scripting is always disabled (webpack is a build tool).
3775
4097
 
3776
4098
  // Namespaces (mirrors swc_html_ast::Namespace)
3777
4099
  const NS_HTML = 0;
@@ -3779,6 +4101,389 @@ const NS_MATHML = 1;
3779
4101
  const NS_SVG = 2;
3780
4102
 
3781
4103
  /**
4104
+ * AST node `type` discriminators. Numeric for the same reason as the CSS
4105
+ * `NodeType`: compact integer `===` dispatch on the tree-construction and
4106
+ * visitor-walk hot paths.
4107
+ * @type {{ Document: 1, DocumentFragment: 2, Element: 3, Text: 4, Comment: 5, Doctype: 6 }}
4108
+ */
4109
+ const NodeType = {
4110
+ Document: 1,
4111
+ DocumentFragment: 2,
4112
+ Element: 3,
4113
+ Text: 4,
4114
+ Comment: 5,
4115
+ Doctype: 6
4116
+ };
4117
+
4118
+ /**
4119
+ * A contiguous run of attribute ids in the attribute columns — how a start-tag
4120
+ * token and an element refer to their attributes. `start` is the first id
4121
+ * (`count` 0 = none).
4122
+ * @typedef {{ start: number, count: number }} AttributeRun
4123
+ */
4124
+
4125
+ // Shared frozen empty run for attributeless elements and synthesized tags.
4126
+ const EMPTY_ATTRS = /** @type {AttributeRun} */ (
4127
+ Object.freeze({ start: 0, count: 0 })
4128
+ );
4129
+
4130
+ // === Struct-of-arrays AST backend ===
4131
+ // One AST node = one integer id (`HtmlNodeRef`) indexing the parallel columns
4132
+ // below — no per-node object and no per-parent children array. Tree shape
4133
+ // lives in the four link columns (parent / firstChild / lastChild /
4134
+ // nextSibling); the only heap references are the string payload / attribute
4135
+ // name-and-value side arrays. Columns are module-level and reused
4136
+ // across parses (grown, shrunk on release only past `_COLUMN_SHRINK_CAPACITY` —
4137
+ // the CSS parser's `_soa*` strategy), so a steady-state parse allocates almost
4138
+ // nothing per node; consumers must fully read a tree before the next
4139
+ // `parseHtml` call. Id 0 is reserved as "no node" so the link columns can use
4140
+ // 0 as null.
4141
+ let _nodeCapacity = 0;
4142
+ let _nodeCount = 0;
4143
+ /** `NodeType` per node */
4144
+ let _nodeTypes = new Uint8Array(0);
4145
+ /** bits 0-1 namespace (`NS_*`), bit 2 self-closing (void element) */
4146
+ let _nodeFlags = new Uint8Array(0);
4147
+ let _nodeStarts = new Int32Array(0);
4148
+ let _nodeEnds = new Int32Array(0);
4149
+ /** end offset of an element's opening tag (after `>`) */
4150
+ let _nodeTagEnds = new Int32Array(0);
4151
+ /** end offset of an element's tag name */
4152
+ let _nodeNameEnds = new Int32Array(0);
4153
+ /** under `skip.text`, end offset of a raw-text element's body (`HtmlAstSkip`) */
4154
+ let _nodeContentEnds = new Int32Array(0);
4155
+ /** a `<template>`'s content DocumentFragment (0 = none) */
4156
+ let _nodeTemplateContents = new Int32Array(0);
4157
+ let _nodeParents = new Int32Array(0);
4158
+ let _nodeFirstChildren = new Int32Array(0);
4159
+ let _nodeLastChildren = new Int32Array(0);
4160
+ let _nodeNextSiblings = new Int32Array(0);
4161
+ /** @type {string[]} tag name / text data / comment data / doctype name */
4162
+ const _nodeStrings = [];
4163
+ /** first attribute id of an element's contiguous run */
4164
+ let _nodeAttrStarts = new Int32Array(0);
4165
+ /** attribute count of an element's run */
4166
+ let _nodeAttrCounts = new Int32Array(0);
4167
+ // The single doctype node's public/system ids (a document inserts at most one
4168
+ // doctype node — later doctype tokens are ignored — so no column is needed).
4169
+ /** @type {string | null} */
4170
+ let _doctypePublicId = null;
4171
+ /** @type {string | null} */
4172
+ let _doctypeSystemId = null;
4173
+
4174
+ const NS_MASK = 3;
4175
+ const FLAG_SELF_CLOSING = 4;
4176
+
4177
+ /** @type {(el: HtmlNodeRef) => string} */
4178
+ const _tagNameOf = (el) => _nodeStrings[el];
4179
+ /** @type {(el: HtmlNodeRef) => number} */
4180
+ const _namespaceOf = (el) => _nodeFlags[el] & NS_MASK;
4181
+
4182
+ // === Attribute columns ===
4183
+ // One attribute = one integer id into these columns; an element (and a
4184
+ // start-tag token) holds a contiguous run. The value string is derived from
4185
+ // the source by offset on read — `_attrValues` carries an override only for
4186
+ // valueless attributes (`""`) and offset-less adoption-agency clones — and the
4187
+ // html5lib serializer name is derived from the adjusted name plus one flag
4188
+ // bit, so per attribute only the interned name pointer is retained.
4189
+ let _attrCapacity = 0;
4190
+ let _attrCount = 0;
4191
+ let _attrNameStarts = new Int32Array(0);
4192
+ let _attrNameEnds = new Int32Array(0);
4193
+ let _attrValueStarts = new Int32Array(0);
4194
+ let _attrValueEnds = new Int32Array(0);
4195
+ /** bit 0: name has a `FOREIGN_ATTR_NS` serializer name (set on foreign adjust) */
4196
+ let _attrFlags = new Uint8Array(0);
4197
+ /** @type {string[]} lowercased (foreign-content: adjusted) attribute name */
4198
+ const _attrNames = [];
4199
+ /** @type {(string | null)[]} value override (null = slice the source by offset) */
4200
+ const _attrValues = [];
4201
+ /** source of the current parse, for by-offset attribute values */
4202
+ let _htmlSource = "";
4203
+
4204
+ /** @param {number} need minimum capacity */
4205
+ const _growAttrColumns = (need) => {
4206
+ let cap = _attrCapacity || 4096;
4207
+ while (cap < need) cap *= 2;
4208
+ const nameStart = new Int32Array(cap);
4209
+ nameStart.set(_attrNameStarts);
4210
+ _attrNameStarts = nameStart;
4211
+ const nameEnd = new Int32Array(cap);
4212
+ nameEnd.set(_attrNameEnds);
4213
+ _attrNameEnds = nameEnd;
4214
+ const valStart = new Int32Array(cap);
4215
+ valStart.set(_attrValueStarts);
4216
+ _attrValueStarts = valStart;
4217
+ const valEnd = new Int32Array(cap);
4218
+ valEnd.set(_attrValueEnds);
4219
+ _attrValueEnds = valEnd;
4220
+ const fl = new Uint8Array(cap);
4221
+ fl.set(_attrFlags);
4222
+ _attrFlags = fl;
4223
+ _attrCapacity = cap;
4224
+ };
4225
+
4226
+ /** @type {(name: string, value: string | null, nameStart: number, nameEnd: number, valueStart: number, valueEnd: number) => number} */
4227
+ const _allocAttr = (name, value, nameStart, nameEnd, valueStart, valueEnd) => {
4228
+ const i = ++_attrCount;
4229
+ if (i >= _attrCapacity) _growAttrColumns(i + 1);
4230
+ _attrNameStarts[i] = nameStart;
4231
+ _attrNameEnds[i] = nameEnd;
4232
+ _attrValueStarts[i] = valueStart;
4233
+ _attrValueEnds[i] = valueEnd;
4234
+ _attrFlags[i] = 0;
4235
+ // Ids are sequential, so these indexed writes append (arrays stay packed).
4236
+ _attrNames[i] = name;
4237
+ _attrValues[i] = value;
4238
+ return i;
4239
+ };
4240
+
4241
+ /** @type {(i: number) => string} */
4242
+ const _attrValueOf = (i) => {
4243
+ const v = _attrValues[i];
4244
+ return v !== null
4245
+ ? v
4246
+ : _htmlSource.slice(_attrValueStarts[i], _attrValueEnds[i]);
4247
+ };
4248
+
4249
+ // Linear name lookup in a run — attribute lists are short, a loop beats a Map.
4250
+ /** @type {(start: number, count: number, name: string) => number} */
4251
+ const _findAttr = (start, count, name) => {
4252
+ for (let i = start; i < start + count; i++) {
4253
+ if (_attrNames[i] === name) return i;
4254
+ }
4255
+ return 0;
4256
+ };
4257
+
4258
+ /** @type {(i: number) => number} exact copy of an attribute into a new id */
4259
+ const _copyAttr = (i) => {
4260
+ const c = _allocAttr(
4261
+ _attrNames[i],
4262
+ _attrValues[i],
4263
+ _attrNameStarts[i],
4264
+ _attrNameEnds[i],
4265
+ _attrValueStarts[i],
4266
+ _attrValueEnds[i]
4267
+ );
4268
+ _attrFlags[c] = _attrFlags[i];
4269
+ return c;
4270
+ };
4271
+
4272
+ // html5lib serializer name, derived: a `FOREIGN_ATTR_NS`-adjusted attribute is
4273
+ // flagged (its name is the table key), and a camelCase-adjusted name contains
4274
+ // an uppercase letter (unadjusted names are always lowercased), serializing as
4275
+ // itself. Everything else serializes as the plain name (undefined here).
4276
+ /** @type {(i: number) => string | undefined} */
4277
+ const _attrSerializedName = (i) => {
4278
+ if ((_attrFlags[i] & 1) !== 0) return FOREIGN_ATTR_NS[_attrNames[i]];
4279
+ const name = _attrNames[i];
4280
+ return /[A-Z]/.test(name) ? name : undefined;
4281
+ };
4282
+
4283
+ /** @param {number} need minimum capacity */
4284
+ const _growNodeColumns = (need) => {
4285
+ let cap = _nodeCapacity || 4096;
4286
+ while (cap < need) cap *= 2;
4287
+ const ty = new Uint8Array(cap);
4288
+ ty.set(_nodeTypes);
4289
+ _nodeTypes = ty;
4290
+ const fl = new Uint8Array(cap);
4291
+ fl.set(_nodeFlags);
4292
+ _nodeFlags = fl;
4293
+ const st = new Int32Array(cap);
4294
+ st.set(_nodeStarts);
4295
+ _nodeStarts = st;
4296
+ const en = new Int32Array(cap);
4297
+ en.set(_nodeEnds);
4298
+ _nodeEnds = en;
4299
+ const tagEnd = new Int32Array(cap);
4300
+ tagEnd.set(_nodeTagEnds);
4301
+ _nodeTagEnds = tagEnd;
4302
+ const nameEnd = new Int32Array(cap);
4303
+ nameEnd.set(_nodeNameEnds);
4304
+ _nodeNameEnds = nameEnd;
4305
+ const cEnd = new Int32Array(cap);
4306
+ cEnd.set(_nodeContentEnds);
4307
+ _nodeContentEnds = cEnd;
4308
+ const tc = new Int32Array(cap);
4309
+ tc.set(_nodeTemplateContents);
4310
+ _nodeTemplateContents = tc;
4311
+ const parent = new Int32Array(cap);
4312
+ parent.set(_nodeParents);
4313
+ _nodeParents = parent;
4314
+ const first = new Int32Array(cap);
4315
+ first.set(_nodeFirstChildren);
4316
+ _nodeFirstChildren = first;
4317
+ const last = new Int32Array(cap);
4318
+ last.set(_nodeLastChildren);
4319
+ _nodeLastChildren = last;
4320
+ const next = new Int32Array(cap);
4321
+ next.set(_nodeNextSiblings);
4322
+ _nodeNextSiblings = next;
4323
+ const aStart = new Int32Array(cap);
4324
+ aStart.set(_nodeAttrStarts);
4325
+ _nodeAttrStarts = aStart;
4326
+ const aCount = new Int32Array(cap);
4327
+ aCount.set(_nodeAttrCounts);
4328
+ _nodeAttrCounts = aCount;
4329
+ _nodeCapacity = cap;
4330
+ };
4331
+
4332
+ /** Start a new parse: invalidate all prior refs, release prior heap refs. */
4333
+ const _resetAstColumns = () => {
4334
+ _nodeCount = 0;
4335
+ _attrCount = 0;
4336
+ _nodeStrings.length = 0;
4337
+ _attrNames.length = 0;
4338
+ _attrValues.length = 0;
4339
+ // Keep id 0 ("no node" / "no attribute") occupied so writes stay packed.
4340
+ _nodeStrings.push("");
4341
+ _attrNames.push("");
4342
+ _attrValues.push(null);
4343
+ _doctypePublicId = null;
4344
+ _doctypeSystemId = null;
4345
+ };
4346
+
4347
+ // The typed-array columns grow to the largest document ever parsed; above this
4348
+ // capacity they are re-shrunk on release so one pathological file can't pin
4349
+ // tens of MB at module level for the process lifetime (~50 bytes/node,
4350
+ // ~17 bytes/attribute of capacity across the columns).
4351
+ const _COLUMN_SHRINK_CAPACITY = 65536;
4352
+
4353
+ // Release the side arrays' heap references (strings, attribute names/values)
4354
+ // once a walk has consumed the tree, so the retained columns don't pin the
4355
+ // parsed source until the next parse.
4356
+ const _releaseAstColumns = () => {
4357
+ _nodeCount = 0;
4358
+ _attrCount = 0;
4359
+ _nodeStrings.length = 0;
4360
+ _attrNames.length = 0;
4361
+ _attrValues.length = 0;
4362
+ _htmlSource = "";
4363
+ _doctypePublicId = null;
4364
+ _doctypeSystemId = null;
4365
+ if (_nodeCapacity > _COLUMN_SHRINK_CAPACITY) {
4366
+ _nodeCapacity = 0;
4367
+ _nodeTypes = new Uint8Array(0);
4368
+ _nodeFlags = new Uint8Array(0);
4369
+ _nodeStarts = new Int32Array(0);
4370
+ _nodeEnds = new Int32Array(0);
4371
+ _nodeTagEnds = new Int32Array(0);
4372
+ _nodeNameEnds = new Int32Array(0);
4373
+ _nodeContentEnds = new Int32Array(0);
4374
+ _nodeTemplateContents = new Int32Array(0);
4375
+ _nodeParents = new Int32Array(0);
4376
+ _nodeFirstChildren = new Int32Array(0);
4377
+ _nodeLastChildren = new Int32Array(0);
4378
+ _nodeNextSiblings = new Int32Array(0);
4379
+ _nodeAttrStarts = new Int32Array(0);
4380
+ _nodeAttrCounts = new Int32Array(0);
4381
+ }
4382
+ if (_attrCapacity > _COLUMN_SHRINK_CAPACITY) {
4383
+ _attrCapacity = 0;
4384
+ _attrNameStarts = new Int32Array(0);
4385
+ _attrNameEnds = new Int32Array(0);
4386
+ _attrValueStarts = new Int32Array(0);
4387
+ _attrValueEnds = new Int32Array(0);
4388
+ _attrFlags = new Uint8Array(0);
4389
+ }
4390
+ };
4391
+
4392
+ /** @type {(type: number, start: number, end: number) => HtmlNodeRef} */
4393
+ const _allocNode = (type, start, end) => {
4394
+ const i = ++_nodeCount;
4395
+ if (i >= _nodeCapacity) _growNodeColumns(i + 1);
4396
+ _nodeTypes[i] = type;
4397
+ _nodeFlags[i] = 0;
4398
+ _nodeStarts[i] = start;
4399
+ _nodeEnds[i] = end;
4400
+ _nodeTagEnds[i] = 0;
4401
+ _nodeNameEnds[i] = 0;
4402
+ _nodeContentEnds[i] = 0;
4403
+ _nodeTemplateContents[i] = 0;
4404
+ _nodeParents[i] = 0;
4405
+ _nodeFirstChildren[i] = 0;
4406
+ _nodeLastChildren[i] = 0;
4407
+ _nodeNextSiblings[i] = 0;
4408
+ _nodeAttrStarts[i] = 0;
4409
+ _nodeAttrCounts[i] = 0;
4410
+ // `_nodeStrings[i]` is written by every caller right after this returns
4411
+ // (tag name / text / comment data; "" at the few string-less sites), as a
4412
+ // packed sequential append — no placeholder double-write here.
4413
+ return i;
4414
+ };
4415
+
4416
+ // Raw child append — no text merging, no `<template>` content redirect (the
4417
+ // tree builder layers those on top). `node` must be detached (`next` = 0).
4418
+ /** @type {(parent: HtmlNodeRef, node: HtmlNodeRef) => void} */
4419
+ const _appendChild = (parent, node) => {
4420
+ _nodeParents[node] = parent;
4421
+ const last = _nodeLastChildren[parent];
4422
+ if (last === 0) _nodeFirstChildren[parent] = node;
4423
+ else _nodeNextSiblings[last] = node;
4424
+ _nodeLastChildren[parent] = node;
4425
+ };
4426
+
4427
+ /** @type {(data: string, start: number, end: number) => HtmlNodeRef} */
4428
+ const _makeTextNode = (data, start, end) => {
4429
+ const i = _allocNode(NodeType.Text, start, end);
4430
+ _nodeStrings[i] = data;
4431
+ return i;
4432
+ };
4433
+
4434
+ /** @type {(data: string, start: number, end: number) => HtmlNodeRef} */
4435
+ const _makeCommentNode = (data, start, end) => {
4436
+ const i = _allocNode(NodeType.Comment, start, end);
4437
+ _nodeStrings[i] = data;
4438
+ return i;
4439
+ };
4440
+
4441
+ // Marker entry in the active-formatting-elements list (never a valid ref).
4442
+ const AFE_MARKER = -1;
4443
+
4444
+ // Clone of an element's attribute run: keep name/value (and the serializer
4445
+ // name flag) but drop source offsets so the consumer doesn't emit a duplicate
4446
+ // dependency for the reopened element's spans. Values are materialized since
4447
+ // the offsets are gone.
4448
+ const cloneAttrs = (/** @type {HtmlElement} */ el) => {
4449
+ const start = _nodeAttrStarts[el];
4450
+ const count = _nodeAttrCounts[el];
4451
+ const newStart = _attrCount + 1;
4452
+ for (let i = start; i < start + count; i++) {
4453
+ const c = _allocAttr(_attrNames[i], _attrValueOf(i), -1, -1, -1, -1);
4454
+ _attrFlags[c] = _attrFlags[i];
4455
+ }
4456
+ return { start: newStart, count };
4457
+ };
4458
+
4459
+ // Merge a repeated `<html>`/`<body>` tag's attributes into the element: only
4460
+ // names not already present are added (in source order after the existing
4461
+ // ones). Runs are contiguous, so any addition re-allocates the whole run; the
4462
+ // old slots are orphaned (at most once per repeated tag, rare).
4463
+ const mergeAttrs = (
4464
+ /** @type {HtmlElement} */ el,
4465
+ /** @type {AttributeRun} */ run
4466
+ ) => {
4467
+ const start = _nodeAttrStarts[el];
4468
+ const count = _nodeAttrCounts[el];
4469
+ let extra = 0;
4470
+ for (let i = run.start; i < run.start + run.count; i++) {
4471
+ if (_findAttr(start, count, _attrNames[i]) === 0) extra++;
4472
+ }
4473
+ if (extra === 0) return;
4474
+ const newStart = _attrCount + 1;
4475
+ for (let i = start; i < start + count; i++) _copyAttr(i);
4476
+ for (let i = run.start; i < run.start + run.count; i++) {
4477
+ if (_findAttr(start, count, _attrNames[i]) === 0) _copyAttr(i);
4478
+ }
4479
+ _nodeAttrStarts[el] = newStart;
4480
+ _nodeAttrCounts[el] = count + extra;
4481
+ };
4482
+
4483
+ /**
4484
+ * A materialized attribute as returned by `A.attributes` (tests/tooling) —
4485
+ * the parser-facing representation is an id into the attribute columns, read
4486
+ * through the scalar `A.attr*` accessors.
3782
4487
  * @typedef {object} HtmlAttribute
3783
4488
  * @property {string} name lowercased (and, in foreign content, adjusted) attribute name
3784
4489
  * @property {string} value
@@ -3790,60 +4495,27 @@ const NS_SVG = 2;
3790
4495
  */
3791
4496
 
3792
4497
  /**
3793
- * @typedef {object} HtmlElement
3794
- * @property {typeof NodeType.Element} type
3795
- * @property {string} tagName
3796
- * @property {number} namespace
3797
- * @property {HtmlAttribute[]} attributes
3798
- * @property {HtmlNode[]} children
3799
- * @property {boolean} selfClosing
3800
- * @property {number} start
3801
- * @property {number} end
3802
- * @property {number} tagEnd end offset of the opening tag (after `>`)
3803
- * @property {number} nameEnd end offset of the tag name
3804
- * @property {HtmlDocumentFragment=} templateContent `<template>` content fragment
3805
- */
3806
-
3807
- /**
3808
- * @typedef {object} HtmlText
3809
- * @property {typeof NodeType.Text} type
3810
- * @property {string} data
3811
- * @property {number} start
3812
- * @property {number} end
4498
+ * A node reference into the struct-of-arrays AST: an integer id indexing the
4499
+ * parallel `_h*` columns. Read fields through the exported accessor `A`. Refs
4500
+ * are only valid until the next `parseHtml` call — the columns are reused
4501
+ * across parses — so consume a tree fully before parsing again.
4502
+ * @typedef {number} HtmlNodeRef
3813
4503
  */
3814
4504
 
3815
- /**
3816
- * @typedef {object} HtmlComment
3817
- * @property {typeof NodeType.Comment} type
3818
- * @property {string} data
3819
- * @property {number} start
3820
- * @property {number} end
3821
- */
3822
-
3823
- /**
3824
- * @typedef {object} HtmlDoctype
3825
- * @property {typeof NodeType.Doctype} type
3826
- * @property {string} name
3827
- * @property {string | null} publicId
3828
- * @property {string | null} systemId
3829
- * @property {number} start
3830
- * @property {number} end
3831
- */
3832
-
3833
- /**
3834
- * @typedef {object} HtmlDocument
3835
- * @property {typeof NodeType.Document} type
3836
- * @property {HtmlNode[]} children
3837
- */
4505
+ /** @typedef {HtmlNodeRef} HtmlElement ref to an Element node */
4506
+ /** @typedef {HtmlNodeRef} HtmlText ref to a Text node */
4507
+ /** @typedef {HtmlNodeRef} HtmlComment ref to a Comment node */
4508
+ /** @typedef {HtmlNodeRef} HtmlDoctype ref to a Doctype node */
4509
+ /** @typedef {HtmlNodeRef} HtmlDocument ref to the Document node */
4510
+ /** @typedef {HtmlNodeRef} HtmlDocumentFragment ref to a DocumentFragment node */
4511
+ /** @typedef {HtmlNodeRef} HtmlNode */
3838
4512
 
3839
4513
  /**
3840
- * @typedef {object} HtmlDocumentFragment
3841
- * @property {typeof NodeType.DocumentFragment} type
3842
- * @property {HtmlNode[]} children
4514
+ * An attribute reference: an integer id into the attribute columns, read
4515
+ * through the `A.attr*` accessors. Same validity contract as `HtmlNodeRef`.
4516
+ * @typedef {number} HtmlAttributeRef
3843
4517
  */
3844
4518
 
3845
- /** @typedef {HtmlElement | HtmlText | HtmlComment | HtmlDoctype} HtmlNode */
3846
-
3847
4519
  /** @typedef {{ start: number, end: number, tagEnd: number, nameEnd: number }} TagPos */
3848
4520
 
3849
4521
  // Tree-construction token `type` discriminators. Numeric for the same reason
@@ -3859,7 +4531,7 @@ const TOKEN_EOF = 6;
3859
4531
  /** @typedef {{ type: typeof TOKEN_CHAR, data: string, start: number, end: number }} CharToken */
3860
4532
  /** @typedef {{ type: typeof TOKEN_COMMENT, data: string, start: number, end: number }} CommentToken */
3861
4533
  /** @typedef {{ type: typeof TOKEN_DOCTYPE, name: string, publicId: (string | null), systemId: (string | null), start: number, end: number }} DoctypeToken */
3862
- /** @typedef {{ type: typeof TOKEN_START_TAG, name: string, attrs: HtmlAttribute[], selfClosing: boolean, pos: TagPos, swallowNewline?: boolean }} StartTagToken */
4534
+ /** @typedef {{ type: typeof TOKEN_START_TAG, name: string, attrs: AttributeRun, selfClosing: boolean, pos: TagPos, swallowNewline?: boolean }} StartTagToken */
3863
4535
  /** @typedef {{ type: typeof TOKEN_END_TAG, name: string, pos: TagPos }} EndTagToken */
3864
4536
  /** @typedef {{ type: typeof TOKEN_EOF }} EofToken */
3865
4537
 
@@ -3876,10 +4548,10 @@ const TOKEN_EOF = 6;
3876
4548
  * stale values that those handlers never read. Tokens that must outlive the
3877
4549
  * current callback (buffered table characters, synthesized re-dispatches) are
3878
4550
  * copied into fresh plain objects instead.
3879
- * @typedef {{ type: number, name: string, data: string, attrs: HtmlAttribute[], selfClosing: boolean, start: number, end: number, publicId: (string | null), systemId: (string | null), swallowNewline: boolean, pos: TagPos }} MutableToken
4551
+ * @typedef {{ type: number, name: string, data: string, attrs: AttributeRun, selfClosing: boolean, start: number, end: number, publicId: (string | null), systemId: (string | null), swallowNewline: boolean, pos: TagPos }} MutableToken
3880
4552
  */
3881
4553
 
3882
- /** @typedef {{ parent: HtmlElement | HtmlDocument | HtmlDocumentFragment, beforeNode: (HtmlNode | null) }} InsertionPlace */
4554
+ /** @typedef {{ parent: HtmlNodeRef, beforeNode: HtmlNodeRef }} InsertionPlace `beforeNode` 0 = plain append */
3883
4555
 
3884
4556
  // Insertion modes (§13.2.4.1). Numeric for the same reason as the token and
3885
4557
  // `NodeType` enums: `runMode` dispatches on `mode` once per token.
@@ -4043,9 +4715,11 @@ const FORMATTING = new Set([
4043
4715
  const HEADING = new Set(["h1", "h2", "h3", "h4", "h5", "h6"]);
4044
4716
 
4045
4717
  const isSpecial = (/** @type {HtmlElement} */ el) => {
4046
- if (el.namespace === NS_HTML) return SPECIAL.has(el.tagName);
4047
- if (el.namespace === NS_MATHML) return MATHML_SPECIAL.has(el.tagName);
4048
- if (el.namespace === NS_SVG) return SVG_SPECIAL.has(el.tagName.toLowerCase());
4718
+ const ns = _namespaceOf(el);
4719
+ const tag = _tagNameOf(el);
4720
+ if (ns === NS_HTML) return SPECIAL.has(tag);
4721
+ if (ns === NS_MATHML) return MATHML_SPECIAL.has(tag);
4722
+ if (ns === NS_SVG) return SVG_SPECIAL.has(tag.toLowerCase());
4049
4723
  return false;
4050
4724
  };
4051
4725
 
@@ -4493,14 +5167,13 @@ const FOREIGN_BREAKOUT = new Set([
4493
5167
  ]);
4494
5168
  const FONT_BREAKOUT_ATTRS = new Set(["color", "face", "size"]);
4495
5169
 
4496
- // Shared frozen empty attribute list for attributeless elements (the common
4497
- // case). Only `<html>`/`<body>` ever receive merged attributes and are always
4498
- // built with their own mutable array, so sharing this everywhere else is safe
4499
- // and avoids an empty array per element. Frozen so an accidental mutation
4500
- // throws instead of corrupting other elements.
4501
- const EMPTY_ATTRS = /** @type {HtmlAttribute[]} */ (
4502
- /** @type {unknown} */ (Object.freeze([]))
4503
- );
5170
+ // `<font color|face|size>` breaks out of foreign content (§13.2.6.5).
5171
+ const hasFontBreakoutAttr = (/** @type {AttributeRun} */ run) => {
5172
+ for (let i = run.start; i < run.start + run.count; i++) {
5173
+ if (FONT_BREAKOUT_ATTRS.has(_attrNames[i])) return true;
5174
+ }
5175
+ return false;
5176
+ };
4504
5177
 
4505
5178
  /**
4506
5179
  * Hash of the ASCII-lowercased `name` for the intern tables below; must stay
@@ -4518,33 +5191,58 @@ const hashLowerName = (name) => {
4518
5191
  return h;
4519
5192
  };
4520
5193
 
5194
+ // Text-run scan classes for the `skip.text` fast path: 2 = stop the fast
5195
+ // path (& / NUL / CR), 1 = ASCII whitespace, 0 = ordinary text.
5196
+ const _TEXT_SCAN_CLASS = new Uint8Array(128);
5197
+ _TEXT_SCAN_CLASS[0x09] = 1;
5198
+ _TEXT_SCAN_CLASS[0x0a] = 1;
5199
+ _TEXT_SCAN_CLASS[0x0c] = 1;
5200
+ _TEXT_SCAN_CLASS[0x20] = 1;
5201
+ _TEXT_SCAN_CLASS[0x26] = 2;
5202
+ _TEXT_SCAN_CLASS[0x00] = 2;
5203
+ _TEXT_SCAN_CLASS[0x0d] = 2;
5204
+
4521
5205
  /**
4522
5206
  * @param {Iterable<string>} names lowercase names to intern
4523
- * @returns {Map<number, string | string[]>} hash → name(s)
5207
+ * @returns {{ mask: number, hashes: Int32Array, values: (string | string[] | undefined)[] }} open-addressed intern table
4524
5208
  */
4525
5209
  const buildNameInternTable = (names) => {
4526
- /** @type {Map<number, string | string[]>} */
4527
- const table = new Map();
4528
- for (const name of names) {
5210
+ // Open-addressed table (~25% load): the per-name probe is one or two array
5211
+ // reads instead of a `Map#get`, and the tables are built once at startup.
5212
+ const unique = [...new Set(names)];
5213
+ let size = 8;
5214
+ while (size < unique.length * 4) size <<= 1;
5215
+ const mask = size - 1;
5216
+ const hashes = new Int32Array(size);
5217
+ /** @type {(string | string[] | undefined)[]} */
5218
+ const values = Array.from({ length: size });
5219
+ for (const name of unique) {
4529
5220
  const h = hashLowerName(name);
4530
- const cur = table.get(h);
5221
+ let slot = h & mask;
5222
+ while (values[slot] !== undefined && hashes[slot] !== h) {
5223
+ slot = (slot + 1) & mask;
5224
+ }
5225
+ const cur = values[slot];
4531
5226
  if (cur === undefined) {
4532
- table.set(h, name);
5227
+ hashes[slot] = h;
5228
+ values[slot] = name;
4533
5229
  } else if (typeof cur === "string") {
4534
- if (cur !== name) table.set(h, [cur, name]);
4535
- } else if (!cur.includes(name)) {
5230
+ values[slot] = [cur, name];
5231
+ } else {
4536
5232
  cur.push(name);
4537
5233
  }
4538
5234
  }
4539
- return table;
5235
+ return { mask, hashes, values };
4540
5236
  };
4541
5237
 
5238
+ /** @typedef {ReturnType<typeof buildNameInternTable>} NameInternTable */
5239
+
4542
5240
  /**
4543
5241
  * The lowercased name for `input[start..end)`, returning the shared interned
4544
5242
  * string for known names — skipping the per-tag `slice().toLowerCase()`
4545
5243
  * allocation, and making the tree builder's many Set/Map lookups and `===`
4546
5244
  * comparisons on the name hit one string instance with a cached hash.
4547
- * @param {Map<number, string | string[]>} table intern table
5245
+ * @param {NameInternTable} table intern table
4548
5246
  * @param {string} input source text
4549
5247
  * @param {number} start name start
4550
5248
  * @param {number} end name end
@@ -4557,15 +5255,22 @@ const internLowerName = (table, input, start, end) => {
4557
5255
  if (c >= 0x41 && c <= 0x5a) c += 0x20;
4558
5256
  h = ((h << 5) - h + c) | 0;
4559
5257
  }
4560
- const hit = table.get(h);
4561
- if (hit !== undefined) {
4562
- if (typeof hit === "string") {
4563
- if (rangeEqualsLower(input, start, end, hit)) return hit;
4564
- } else {
4565
- for (let i = 0; i < hit.length; i++) {
4566
- if (rangeEqualsLower(input, start, end, hit[i])) return hit[i];
5258
+ const { mask, hashes, values } = table;
5259
+ let slot = h & mask;
5260
+ for (;;) {
5261
+ const hit = values[slot];
5262
+ if (hit === undefined) break;
5263
+ if (hashes[slot] === h) {
5264
+ if (typeof hit === "string") {
5265
+ if (rangeEqualsLowerCase(input, start, end, hit)) return hit;
5266
+ } else {
5267
+ for (let i = 0; i < hit.length; i++) {
5268
+ if (rangeEqualsLowerCase(input, start, end, hit[i])) return hit[i];
5269
+ }
4567
5270
  }
5271
+ break;
4568
5272
  }
5273
+ slot = (slot + 1) & mask;
4569
5274
  }
4570
5275
  // Unknown name (custom element, data-* attribute, non-ASCII, …).
4571
5276
  return input.slice(start, end).toLowerCase();
@@ -4604,50 +5309,96 @@ const ATTR_NAME_INTERN = buildNameInternTable(
4604
5309
  // Hoisted so the many `open.some(...)` "is there an open HTML <template>?"
4605
5310
  // checks reuse one predicate instead of allocating an arrow per call.
4606
5311
  const isHtmlTemplateEl = (/** @type {HtmlElement} */ e) =>
4607
- e.tagName === "template" && e.namespace === NS_HTML;
5312
+ _tagNameOf(e) === "template" && _namespaceOf(e) === NS_HTML;
4608
5313
 
4609
- // Linear attribute lookup by name — avoids the per-call arrow `Array#find`
4610
- // would allocate (attribute lists are short, so a plain loop is faster too).
4611
- const findAttr = (
4612
- /** @type {HtmlAttribute[]} */ attrs,
4613
- /** @type {string} */ name
4614
- ) => {
4615
- for (let i = 0; i < attrs.length; i++) {
4616
- if (attrs[i].name === name) return attrs[i];
4617
- }
4618
- return undefined;
4619
- };
5314
+ // Raw text / escapable raw text elements (WHATWG §13.1.2) plus the other
5315
+ // RAWTEXT/RCDATA/PLAINTEXT-tokenized elements. Under `skip.text` their body's
5316
+ // end offset is recorded on the element (see `insertCharacters`) so a consumer
5317
+ // can read the raw content span without a `Text` node.
5318
+ const RAW_TEXT_ELEMENTS = new Set([
5319
+ "script",
5320
+ "style",
5321
+ "textarea",
5322
+ "title",
5323
+ "xmp",
5324
+ "iframe",
5325
+ "noembed",
5326
+ "noframes",
5327
+ "noscript",
5328
+ "plaintext"
5329
+ ]);
5330
+
5331
+ /**
5332
+ * Shared empty skip set so the common (no-skip) call allocates nothing.
5333
+ * @type {HtmlAstSkip}
5334
+ */
5335
+ const EMPTY_SKIP = Object.freeze({});
5336
+
5337
+ /**
5338
+ * Optional node kinds a consumer can drop from the AST for speed/memory. Each
5339
+ * is a pure output reduction — tree construction (and quirks detection) runs
5340
+ * unchanged, so element structure and offsets are identical either way.
5341
+ * @typedef {object} HtmlAstSkip
5342
+ * @property {boolean=} text drop every `Text` node. Raw-text element bodies (`<script>`/`<style>`/…) aren't emitted either — their content span is recorded as the element's `contentEnd` (see `RAW_TEXT_ELEMENTS`) so a consumer can read `[tagEnd, contentEnd]` by offset. For consumers that read text by offset (e.g. `HtmlParser`), never the html5lib serializer.
5343
+ * @property {boolean=} comments drop comment nodes entirely. Not for consumers that read comments (e.g. webpack magic comments).
5344
+ * @property {boolean=} doctype drop the doctype node; quirks-mode detection is unaffected.
5345
+ */
4620
5346
 
4621
5347
  /**
4622
- * @param {string} source HTML source
4623
- * @param {string=} fragmentContext context element name for fragment parsing (e.g. `td`, `svg path`); omit for a full document
4624
- * @returns {HtmlDocument} document AST
5348
+ * @typedef {object} HtmlParseOptions
5349
+ * @property {string=} fragmentContext context element name for fragment parsing (e.g. `td`, `svg path`); omit for a full document
5350
+ * @property {HtmlAstSkip=} skip node kinds to omit from the AST (see `HtmlAstSkip`); omit to build the full tree
4625
5351
  */
4626
- const buildHtmlAst = (source, fragmentContext) => {
4627
- /** @type {HtmlDocument} */
4628
- const doc = { type: NodeType.Document, children: [] };
5352
+
5353
+ /**
5354
+ * @param {string} input HTML source
5355
+ * @param {number=} pos start byte offset (string input only; default `0`)
5356
+ * @param {HtmlParseOptions=} options parse options (fragment context, AST skips)
5357
+ * @returns {HtmlDocument} ref to the document node — read through `A`; valid until the next parse
5358
+ */
5359
+ const parseHtml = (input, pos = 0, options = {}) => {
5360
+ const source = input;
5361
+ const { fragmentContext, skip = EMPTY_SKIP } = options;
5362
+ const skipText = skip.text === true;
5363
+ const skipComments = skip.comments === true;
5364
+ const skipDoctype = skip.doctype === true;
5365
+ _resetAstColumns();
5366
+ // Pre-size the columns from the input length: a document big enough to
5367
+ // exceed `_COLUMN_SHRINK_CAPACITY` re-enters with released (empty) columns,
5368
+ // and growing through the doubling cascade re-allocates and copies every
5369
+ // column several times per parse. ~12 bytes/node and ~50 bytes/attribute
5370
+ // are the densest realistic HTML, so `len/12` (`len/48`) reaches the final
5371
+ // capacity in one allocation; a sparser document over-allocates bounded
5372
+ // scratch that the post-walk release frees, and the doubling growth remains
5373
+ // as the safety net when the estimate is short.
5374
+ const _sizeEstimate = (input.length / 12) | 0;
5375
+ if (_sizeEstimate > _nodeCapacity) _growNodeColumns(_sizeEstimate);
5376
+ if (_sizeEstimate > _attrCapacity << 2) {
5377
+ _growAttrColumns(_sizeEstimate >> 2);
5378
+ }
5379
+ _htmlSource = source;
5380
+ const doc = _allocNode(NodeType.Document, 0, 0);
5381
+ _nodeStrings[doc] = "";
4629
5382
  let mode = MODE_INITIAL;
4630
5383
  // Mode to return to after MODE_TEXT / MODE_IN_TABLE_TEXT (0 = unset).
4631
5384
  let originalMode = 0;
4632
5385
  /** @type {HtmlElement[]} stack of open elements (bottom .. top) */
4633
5386
  const open = [];
4634
- /** @type {{ element?: HtmlElement, marker?: boolean }[]} active formatting elements */
5387
+ /** @type {HtmlNodeRef[]} active formatting elements (AFE_MARKER = marker) */
4635
5388
  const afe = [];
4636
- const afeEl = (/** @type {{ element?: HtmlElement }} */ e) =>
4637
- /** @type {HtmlElement} */ (e.element);
4638
- /** @type {HtmlElement | null} */
4639
- let head = null;
4640
- /** @type {HtmlElement | null} */
4641
- let form = null;
5389
+ /** @type {HtmlElement} 0 = none */
5390
+ let head = 0;
5391
+ /** @type {HtmlElement} 0 = none */
5392
+ let form = 0;
4642
5393
  let framesetOk = true;
4643
- /** @type {HtmlAttribute[]} */
4644
- let pendingAttrs = [];
5394
+ // First attribute id of the tag currently being tokenized (0 = none).
5395
+ let pendAttrStart = 0;
4645
5396
  let fosterParenting = false;
4646
5397
  /** @type {number[]} */
4647
5398
  const templateModes = [];
4648
5399
  let quirks = false;
4649
- /** @type {HtmlElement | null} */
4650
- let fragment = null;
5400
+ /** @type {HtmlElement} fragment context element (0 = document parse) */
5401
+ let fragment = 0;
4651
5402
  // End offset of the token currently being processed (for element `.end`).
4652
5403
  let tokenEnd = 0;
4653
5404
 
@@ -4660,70 +5411,68 @@ const buildHtmlAst = (source, fragmentContext) => {
4660
5411
  const mkEl = (
4661
5412
  /** @type {string} */ tagName,
4662
5413
  /** @type {number} */ ns,
4663
- /** @type {HtmlAttribute[] | undefined} */ attributes,
5414
+ /** @type {AttributeRun} */ attrs,
4664
5415
  /** @type {TagPos | null | undefined} */ pos
4665
- ) =>
4666
- /** @type {HtmlElement} */ ({
4667
- type: NodeType.Element,
4668
- tagName,
4669
- namespace: ns,
4670
- attributes: attributes || EMPTY_ATTRS,
4671
- children: [],
4672
- selfClosing: ns === NS_HTML && VOID.has(tagName),
4673
- start: pos ? pos.start : 0,
4674
- end: pos ? pos.end : 0,
4675
- tagEnd: pos ? pos.tagEnd : 0,
4676
- nameEnd: pos ? pos.nameEnd : 0,
4677
- // Present from creation (only `<template>` fills it) so every element
4678
- // keeps one monomorphic hidden class for the open-stack/scope loops.
4679
- templateContent: undefined
4680
- });
4681
-
4682
- // Clone of a formatting element's attributes: keep name/value (and the
4683
- // serializer name) but drop source offsets so the consumer doesn't emit a
4684
- // duplicate dependency for the reopened element's spans.
4685
- const cloneAttrs = (/** @type {HtmlAttribute[]} */ attributes) =>
4686
- attributes.map((a) => ({
4687
- name: a.name,
4688
- value: a.value,
4689
- serializedName: a.serializedName,
4690
- nameStart: -1,
4691
- nameEnd: -1,
4692
- valueStart: -1,
4693
- valueEnd: -1
4694
- }));
5416
+ ) => {
5417
+ const el = _allocNode(
5418
+ NodeType.Element,
5419
+ pos ? pos.start : 0,
5420
+ pos ? pos.end : 0
5421
+ );
5422
+ // Void HTML elements are marked self-closing and never receive children.
5423
+ _nodeFlags[el] =
5424
+ ns === NS_HTML && VOID.has(tagName) ? ns | FLAG_SELF_CLOSING : ns;
5425
+ _nodeStrings[el] = tagName;
5426
+ _nodeAttrStarts[el] = attrs.start;
5427
+ _nodeAttrCounts[el] = attrs.count;
5428
+ if (pos) {
5429
+ _nodeTagEnds[el] = pos.tagEnd;
5430
+ _nodeNameEnds[el] = pos.nameEnd;
5431
+ // End of a raw-text element's body under `skip.text` (defaults to the
5432
+ // body start, i.e. empty); lets consumers read `<script>`/`<style>`
5433
+ // content as [`tagEnd`, `contentEnd`] without a `Text` node.
5434
+ _nodeContentEnds[el] = pos.tagEnd;
5435
+ }
5436
+ return el;
5437
+ };
4695
5438
 
4696
5439
  /**
4697
- * @param {HtmlElement | HtmlDocument | HtmlDocumentFragment} parent container
4698
- * @returns {HtmlNode[]} children array to insert into
5440
+ * A `<template>`'s children live in its content fragment; inserting into a
5441
+ * template really inserts there (the spec's "template contents" redirect).
5442
+ * @param {HtmlNodeRef} parent container
5443
+ * @returns {HtmlNodeRef} effective container to link children into
4699
5444
  */
4700
- const childrenOf = (parent) =>
4701
- parent.type === NodeType.Element && parent.templateContent
4702
- ? parent.templateContent.children
4703
- : parent.children;
5445
+ const effParent = (parent) => {
5446
+ const tc = _nodeTemplateContents[parent];
5447
+ return tc !== 0 ? tc : parent;
5448
+ };
4704
5449
 
4705
5450
  const appendTo = (
4706
- /** @type {HtmlElement | HtmlDocument | HtmlDocumentFragment} */ parent,
4707
- /** @type {HtmlNode} */ node
5451
+ /** @type {HtmlNodeRef} */ parent,
5452
+ /** @type {HtmlNodeRef} */ node
4708
5453
  ) => {
4709
- const arr = childrenOf(parent);
4710
- const last = arr[arr.length - 1];
4711
- if (node.type === NodeType.Text && last && last.type === NodeType.Text) {
4712
- last.data += node.data;
4713
- if (node.end !== undefined) last.end = node.end;
5454
+ const p = effParent(parent);
5455
+ const last = _nodeLastChildren[p];
5456
+ if (
5457
+ _nodeTypes[node] === NodeType.Text &&
5458
+ last !== 0 &&
5459
+ _nodeTypes[last] === NodeType.Text
5460
+ ) {
5461
+ _nodeStrings[last] += _nodeStrings[node];
5462
+ _nodeEnds[last] = _nodeEnds[node];
4714
5463
  return;
4715
5464
  }
4716
- arr.push(node);
5465
+ _appendChild(p, node);
4717
5466
  };
4718
5467
 
4719
5468
  // Reused result of `appropriatePlace` — consumed synchronously by
4720
5469
  // `insertAtPlace` and never retained, so one shared object avoids an
4721
5470
  // allocation per inserted node.
4722
5471
  /** @type {InsertionPlace} */
4723
- const sharedPlace = { parent: doc, beforeNode: null };
5472
+ const sharedPlace = { parent: doc, beforeNode: 0 };
4724
5473
  const placeAt = (
4725
- /** @type {HtmlElement | HtmlDocument | HtmlDocumentFragment} */ parent,
4726
- /** @type {HtmlNode | null} */ beforeNode
5474
+ /** @type {HtmlNodeRef} */ parent,
5475
+ /** @type {HtmlNodeRef} */ beforeNode
4727
5476
  ) => {
4728
5477
  sharedPlace.parent = parent;
4729
5478
  sharedPlace.beforeNode = beforeNode;
@@ -4735,23 +5484,23 @@ const buildHtmlAst = (source, fragmentContext) => {
4735
5484
  const target = cur();
4736
5485
  if (
4737
5486
  fosterParenting &&
4738
- TABLE_CONTEXT.has(target.tagName) &&
4739
- target.namespace === NS_HTML
5487
+ TABLE_CONTEXT.has(_tagNameOf(target)) &&
5488
+ _namespaceOf(target) === NS_HTML
4740
5489
  ) {
4741
5490
  // find last template / last table
4742
5491
  let lastTemplate = -1;
4743
5492
  let lastTable = -1;
4744
5493
  for (let i = open.length - 1; i >= 0; i--) {
4745
5494
  if (
4746
- open[i].tagName === "template" &&
4747
- open[i].namespace === NS_HTML &&
5495
+ _tagNameOf(open[i]) === "template" &&
5496
+ _namespaceOf(open[i]) === NS_HTML &&
4748
5497
  lastTemplate === -1
4749
5498
  ) {
4750
5499
  lastTemplate = i;
4751
5500
  }
4752
5501
  if (
4753
- open[i].tagName === "table" &&
4754
- open[i].namespace === NS_HTML &&
5502
+ _tagNameOf(open[i]) === "table" &&
5503
+ _namespaceOf(open[i]) === NS_HTML &&
4755
5504
  lastTable === -1
4756
5505
  ) {
4757
5506
  lastTable = i;
@@ -4761,53 +5510,52 @@ const buildHtmlAst = (source, fragmentContext) => {
4761
5510
  lastTemplate !== -1 &&
4762
5511
  (lastTable === -1 || lastTemplate > lastTable)
4763
5512
  ) {
4764
- return placeAt(open[lastTemplate], null);
5513
+ return placeAt(open[lastTemplate], 0);
4765
5514
  }
4766
5515
  if (lastTable === -1) {
4767
- return placeAt(open[0], null);
5516
+ return placeAt(open[0], 0);
4768
5517
  }
4769
5518
  const table = open[lastTable];
4770
- // find table's parent in tree
4771
- const tp = findParent(table);
4772
- if (tp) return placeAt(tp, table);
4773
- return placeAt(open[lastTable - 1], null);
5519
+ const tp = _nodeParents[table];
5520
+ if (tp !== 0) return placeAt(tp, table);
5521
+ return placeAt(open[lastTable - 1], 0);
4774
5522
  }
4775
- return placeAt(target, null);
4776
- };
4777
-
4778
- const findParent = (/** @type {HtmlNode} */ node) => {
4779
- // search the whole document tree for the parent of node
4780
- /** @type {(HtmlElement | HtmlDocument | HtmlDocumentFragment)[]} */
4781
- const stack = [doc];
4782
- while (stack.length) {
4783
- const x =
4784
- /** @type {HtmlElement | HtmlDocument | HtmlDocumentFragment} */ (
4785
- stack.pop()
4786
- );
4787
- for (const k of childrenOf(x)) {
4788
- if (k === node) return x;
4789
- if (k.type === NodeType.Element) stack.push(k);
4790
- }
4791
- }
4792
- return null;
5523
+ return placeAt(target, 0);
4793
5524
  };
4794
5525
 
4795
5526
  const insertAtPlace = (
4796
5527
  /** @type {InsertionPlace} */ place,
4797
- /** @type {HtmlNode} */ node
5528
+ /** @type {HtmlNodeRef} */ node
4798
5529
  ) => {
4799
- if (place.beforeNode) {
4800
- const arr = childrenOf(place.parent);
4801
- const idx = arr.indexOf(place.beforeNode);
5530
+ const before = place.beforeNode;
5531
+ if (before !== 0) {
5532
+ const p = effParent(place.parent);
5533
+ // Find `before`'s previous sibling (insert-before is a rare foster/
5534
+ // adoption path, so the sibling scan stays off the hot path).
5535
+ let prev = 0;
5536
+ let c = _nodeFirstChildren[p];
5537
+ while (c !== 0 && c !== before) {
5538
+ prev = c;
5539
+ c = _nodeNextSiblings[c];
5540
+ }
5541
+ if (c === 0) {
5542
+ // `before` not under `parent` (not reachable from the spec paths).
5543
+ appendTo(place.parent, node);
5544
+ return;
5545
+ }
4802
5546
  if (
4803
- node.type === NodeType.Text &&
4804
- idx > 0 &&
4805
- arr[idx - 1].type === NodeType.Text
5547
+ _nodeTypes[node] === NodeType.Text &&
5548
+ prev !== 0 &&
5549
+ _nodeTypes[prev] === NodeType.Text
4806
5550
  ) {
4807
- /** @type {HtmlText} */ (arr[idx - 1]).data += node.data;
5551
+ // Before-node merge deliberately does not bump the sibling's `end`.
5552
+ _nodeStrings[prev] += _nodeStrings[node];
4808
5553
  return;
4809
5554
  }
4810
- arr.splice(idx, 0, node);
5555
+ _nodeParents[node] = p;
5556
+ _nodeNextSiblings[node] = before;
5557
+ if (prev === 0) _nodeFirstChildren[p] = node;
5558
+ else _nodeNextSiblings[prev] = node;
4811
5559
  } else {
4812
5560
  appendTo(place.parent, node);
4813
5561
  }
@@ -4819,28 +5567,58 @@ const buildHtmlAst = (source, fragmentContext) => {
4819
5567
  /** @type {number} */ end
4820
5568
  ) => {
4821
5569
  const place = appropriatePlace();
4822
- if (place.parent.type === NodeType.Document) return; // never insert text into document
5570
+ if (_nodeTypes[place.parent] === NodeType.Document) return; // never insert text into document
5571
+ // `skip.text`: drop every `Text` node — construction already used the
5572
+ // decoded token, so removing the node never affects element structure.
5573
+ // For a raw-text element record the body end so a consumer reads the span
5574
+ // [`tagEnd`, `contentEnd`] without a `Text` node (see `HtmlParser`).
5575
+ // Namespace-agnostic: `HtmlParser` extracts `<script>`/`<style>` bodies in
5576
+ // foreign content (e.g. SVG `<style>`) too.
5577
+ if (skipText) {
5578
+ const p = place.parent;
5579
+ if (
5580
+ _nodeTypes[p] === NodeType.Element &&
5581
+ RAW_TEXT_ELEMENTS.has(_tagNameOf(p))
5582
+ ) {
5583
+ _nodeContentEnds[p] = end;
5584
+ }
5585
+ return;
5586
+ }
4823
5587
  // Inlined text insert: when the run merges into the adjacent text sibling
4824
5588
  // (common with inline formatting) only the string is appended — no
4825
5589
  // throwaway text node is allocated. Mirrors `insertAtPlace`/`appendTo`,
4826
5590
  // including that the before-node merge does not bump `end`.
4827
- const arr = childrenOf(place.parent);
4828
- if (place.beforeNode) {
4829
- const idx = arr.indexOf(place.beforeNode);
4830
- const prev = idx > 0 ? arr[idx - 1] : undefined;
4831
- if (prev && prev.type === NodeType.Text) {
4832
- prev.data += data;
5591
+ const p = effParent(place.parent);
5592
+ const before = place.beforeNode;
5593
+ if (before !== 0) {
5594
+ let prev = 0;
5595
+ let c = _nodeFirstChildren[p];
5596
+ while (c !== 0 && c !== before) {
5597
+ prev = c;
5598
+ c = _nodeNextSiblings[c];
5599
+ }
5600
+ if (c === 0) {
5601
+ // `before` not under `parent` (not reachable from the spec paths).
5602
+ _appendChild(p, _makeTextNode(data, start, end));
4833
5603
  return;
4834
5604
  }
4835
- arr.splice(idx, 0, { type: NodeType.Text, data, start, end });
5605
+ if (prev !== 0 && _nodeTypes[prev] === NodeType.Text) {
5606
+ _nodeStrings[prev] += data;
5607
+ return;
5608
+ }
5609
+ const node = _makeTextNode(data, start, end);
5610
+ _nodeParents[node] = p;
5611
+ _nodeNextSiblings[node] = before;
5612
+ if (prev === 0) _nodeFirstChildren[p] = node;
5613
+ else _nodeNextSiblings[prev] = node;
4836
5614
  } else {
4837
- const last = arr[arr.length - 1];
4838
- if (last && last.type === NodeType.Text) {
4839
- last.data += data;
4840
- last.end = end;
5615
+ const last = _nodeLastChildren[p];
5616
+ if (last !== 0 && _nodeTypes[last] === NodeType.Text) {
5617
+ _nodeStrings[last] += data;
5618
+ _nodeEnds[last] = end;
4841
5619
  return;
4842
5620
  }
4843
- arr.push({ type: NodeType.Text, data, start, end });
5621
+ _appendChild(p, _makeTextNode(data, start, end));
4844
5622
  }
4845
5623
  };
4846
5624
 
@@ -4851,16 +5629,17 @@ const buildHtmlAst = (source, fragmentContext) => {
4851
5629
  * @param {InsertionPlace=} place explicit insertion place
4852
5630
  */
4853
5631
  const insertComment = (data, start, end, place) => {
5632
+ if (skipComments) return;
4854
5633
  const p = place || appropriatePlace();
4855
- insertAtPlace(p, { type: NodeType.Comment, data, start, end });
5634
+ insertAtPlace(p, _makeCommentNode(data, start, end));
4856
5635
  };
4857
5636
 
4858
5637
  const insertHtmlElement = (
4859
5638
  /** @type {string} */ tagName,
4860
- /** @type {HtmlAttribute[]} */ attributes,
5639
+ /** @type {AttributeRun} */ attrs,
4861
5640
  /** @type {TagPos | null} */ pos
4862
5641
  ) => {
4863
- const el = mkEl(tagName, NS_HTML, attributes, pos);
5642
+ const el = mkEl(tagName, NS_HTML, attrs, pos);
4864
5643
  const place = appropriatePlace();
4865
5644
  insertAtPlace(place, el);
4866
5645
  open.push(el);
@@ -4870,10 +5649,10 @@ const buildHtmlAst = (source, fragmentContext) => {
4870
5649
  const insertForeignElement = (
4871
5650
  /** @type {string} */ tagName,
4872
5651
  /** @type {number} */ ns,
4873
- /** @type {HtmlAttribute[]} */ attributes,
5652
+ /** @type {AttributeRun} */ attrs,
4874
5653
  /** @type {TagPos | null} */ pos
4875
5654
  ) => {
4876
- const el = mkEl(tagName, ns, attributes, pos);
5655
+ const el = mkEl(tagName, ns, attrs, pos);
4877
5656
  const place = appropriatePlace();
4878
5657
  insertAtPlace(place, el);
4879
5658
  open.push(el);
@@ -4882,10 +5661,12 @@ const buildHtmlAst = (source, fragmentContext) => {
4882
5661
 
4883
5662
  // ---- scopes ----
4884
5663
  const isScopeBoundary = (/** @type {HtmlElement} */ el) => {
4885
- if (el.namespace === NS_HTML) return HTML_SCOPE.has(el.tagName);
4886
- if (el.namespace === NS_MATHML) return MATHML_SPECIAL.has(el.tagName);
4887
- if (el.namespace === NS_SVG) {
4888
- return SVG_SPECIAL.has(el.tagName.toLowerCase());
5664
+ if (_namespaceOf(el) === NS_HTML) return HTML_SCOPE.has(_tagNameOf(el));
5665
+ if (_namespaceOf(el) === NS_MATHML) {
5666
+ return MATHML_SPECIAL.has(_tagNameOf(el));
5667
+ }
5668
+ if (_namespaceOf(el) === NS_SVG) {
5669
+ return SVG_SPECIAL.has(_tagNameOf(el).toLowerCase());
4889
5670
  }
4890
5671
  return false;
4891
5672
  };
@@ -4900,10 +5681,10 @@ const buildHtmlAst = (source, fragmentContext) => {
4900
5681
  /** @type {number} */ kind
4901
5682
  ) => {
4902
5683
  if (isScopeBoundary(el)) return true;
4903
- if (el.namespace !== NS_HTML) return false;
4904
- if (kind === SCOPE_BUTTON) return el.tagName === "button";
5684
+ if (_namespaceOf(el) !== NS_HTML) return false;
5685
+ if (kind === SCOPE_BUTTON) return _tagNameOf(el) === "button";
4905
5686
  if (kind === SCOPE_LIST_ITEM) {
4906
- return el.tagName === "ol" || el.tagName === "ul";
5687
+ return _tagNameOf(el) === "ol" || _tagNameOf(el) === "ul";
4907
5688
  }
4908
5689
  return false;
4909
5690
  };
@@ -4915,7 +5696,9 @@ const buildHtmlAst = (source, fragmentContext) => {
4915
5696
  ) => {
4916
5697
  for (let i = open.length - 1; i >= 0; i--) {
4917
5698
  const el = open[i];
4918
- if (el.namespace === NS_HTML && el.tagName === tagName) return true;
5699
+ if (_namespaceOf(el) === NS_HTML && _tagNameOf(el) === tagName) {
5700
+ return true;
5701
+ }
4919
5702
  if (isBoundaryForKind(el, kind)) return false;
4920
5703
  }
4921
5704
  return false;
@@ -4939,9 +5722,11 @@ const buildHtmlAst = (source, fragmentContext) => {
4939
5722
  const set = typeof target === "string" ? null : target;
4940
5723
  for (let i = open.length - 1; i >= 0; i--) {
4941
5724
  const el = open[i];
4942
- if (el.namespace === NS_HTML) {
4943
- if (set ? set.has(el.tagName) : el.tagName === target) return true;
4944
- if (TABLE_SCOPE_STOP.has(el.tagName)) return false;
5725
+ if (_namespaceOf(el) === NS_HTML) {
5726
+ if (set ? set.has(_tagNameOf(el)) : _tagNameOf(el) === target) {
5727
+ return true;
5728
+ }
5729
+ if (TABLE_SCOPE_STOP.has(_tagNameOf(el))) return false;
4945
5730
  }
4946
5731
  }
4947
5732
  return false;
@@ -4950,9 +5735,9 @@ const buildHtmlAst = (source, fragmentContext) => {
4950
5735
  while (open.length) {
4951
5736
  const el = cur();
4952
5737
  if (
4953
- el.namespace === NS_HTML &&
4954
- IMPLIED.has(el.tagName) &&
4955
- el.tagName !== except
5738
+ _namespaceOf(el) === NS_HTML &&
5739
+ IMPLIED.has(_tagNameOf(el)) &&
5740
+ _tagNameOf(el) !== except
4956
5741
  ) {
4957
5742
  open.pop();
4958
5743
  } else {
@@ -4963,7 +5748,10 @@ const buildHtmlAst = (source, fragmentContext) => {
4963
5748
  const generateImpliedEndTagsThorough = () => {
4964
5749
  while (open.length) {
4965
5750
  const el = cur();
4966
- if (el.namespace === NS_HTML && IMPLIED_THOROUGH.has(el.tagName)) {
5751
+ if (
5752
+ _namespaceOf(el) === NS_HTML &&
5753
+ IMPLIED_THOROUGH.has(_tagNameOf(el))
5754
+ ) {
4967
5755
  open.pop();
4968
5756
  } else {
4969
5757
  break;
@@ -4976,11 +5764,11 @@ const buildHtmlAst = (source, fragmentContext) => {
4976
5764
  let count = 0;
4977
5765
  for (let i = afe.length - 1; i >= 0; i--) {
4978
5766
  const e = afe[i];
4979
- if (e.marker) break;
5767
+ if (e === AFE_MARKER) break;
4980
5768
  if (
4981
- afeEl(e).tagName === el.tagName &&
4982
- afeEl(e).namespace === el.namespace &&
4983
- sameAttrs(afeEl(e), el)
5769
+ _tagNameOf(e) === _tagNameOf(el) &&
5770
+ _namespaceOf(e) === _namespaceOf(el) &&
5771
+ sameAttrs(e, el)
4984
5772
  ) {
4985
5773
  count++;
4986
5774
  if (count === 3) {
@@ -4989,60 +5777,49 @@ const buildHtmlAst = (source, fragmentContext) => {
4989
5777
  }
4990
5778
  }
4991
5779
  }
4992
- afe.push({ element: el });
5780
+ afe.push(el);
4993
5781
  };
4994
5782
  const sameAttrs = (
4995
5783
  /** @type {HtmlElement} */ a,
4996
5784
  /** @type {HtmlElement} */ b
4997
5785
  ) => {
4998
- const aa = a.attributes;
4999
- const ba = b.attributes;
5000
- if (aa.length !== ba.length) return false;
5786
+ const aStart = _nodeAttrStarts[a];
5787
+ const aCount = _nodeAttrCounts[a];
5788
+ const bStart = _nodeAttrStarts[b];
5789
+ const bCount = _nodeAttrCounts[b];
5790
+ if (aCount !== bCount) return false;
5001
5791
  // Names are unique (deduped), counts tiny — a nested scan beats a Map
5002
- // here and allocates nothing on this formatting-element hot path.
5003
- for (let i = 0; i < ba.length; i++) {
5004
- const x = ba[i];
5005
- let ok = false;
5006
- for (let j = 0; j < aa.length; j++) {
5007
- if (aa[j].name === x.name) {
5008
- ok = aa[j].value === x.value;
5009
- break;
5010
- }
5011
- }
5012
- if (!ok) return false;
5792
+ // here on this formatting-element hot path.
5793
+ for (let i = bStart; i < bStart + bCount; i++) {
5794
+ const j = _findAttr(aStart, aCount, _attrNames[i]);
5795
+ if (j === 0 || _attrValueOf(j) !== _attrValueOf(i)) return false;
5013
5796
  }
5014
5797
  return true;
5015
5798
  };
5016
- const insertMarker = () => afe.push({ marker: true });
5799
+ const insertMarker = () => afe.push(AFE_MARKER);
5017
5800
  const clearAfeToMarker = () => {
5018
5801
  while (afe.length) {
5019
- const e = /** @type {{ marker?: boolean }} */ (afe.pop());
5020
- if (e.marker) break;
5802
+ if (afe.pop() === AFE_MARKER) break;
5021
5803
  }
5022
5804
  };
5023
5805
  const reconstructAfe = () => {
5024
5806
  if (afe.length === 0) return;
5025
5807
  let i = afe.length - 1;
5026
- if (afe[i].marker || open.includes(afeEl(afe[i]))) return;
5808
+ if (afe[i] === AFE_MARKER || open.includes(afe[i])) return;
5027
5809
  while (i > 0) {
5028
5810
  i--;
5029
- if (afe[i].marker || open.includes(afeEl(afe[i]))) {
5811
+ if (afe[i] === AFE_MARKER || open.includes(afe[i])) {
5030
5812
  i++;
5031
5813
  break;
5032
5814
  }
5033
5815
  }
5034
5816
  for (; i < afe.length; i++) {
5035
5817
  const e = afe[i];
5036
- const el = mkEl(
5037
- afeEl(e).tagName,
5038
- afeEl(e).namespace,
5039
- cloneAttrs(afeEl(e).attributes),
5040
- null
5041
- );
5818
+ const el = mkEl(_tagNameOf(e), _namespaceOf(e), cloneAttrs(e), null);
5042
5819
  const place = appropriatePlace();
5043
5820
  insertAtPlace(place, el);
5044
5821
  open.push(el);
5045
- afe[i] = { element: el };
5822
+ afe[i] = el;
5046
5823
  }
5047
5824
  };
5048
5825
 
@@ -5052,23 +5829,23 @@ const buildHtmlAst = (source, fragmentContext) => {
5052
5829
  // pop until a p has been popped
5053
5830
  while (open.length) {
5054
5831
  const el = /** @type {HtmlElement} */ (open.pop());
5055
- el.end = tokenEnd;
5056
- if (el.namespace === NS_HTML && el.tagName === "p") break;
5832
+ _nodeEnds[el] = tokenEnd;
5833
+ if (_namespaceOf(el) === NS_HTML && _tagNameOf(el) === "p") break;
5057
5834
  }
5058
5835
  };
5059
5836
 
5060
5837
  const popUntil = (/** @type {string} */ tagName) => {
5061
5838
  while (open.length) {
5062
5839
  const el = /** @type {HtmlElement} */ (open.pop());
5063
- el.end = tokenEnd;
5064
- if (el.namespace === NS_HTML && el.tagName === tagName) break;
5840
+ _nodeEnds[el] = tokenEnd;
5841
+ if (_namespaceOf(el) === NS_HTML && _tagNameOf(el) === tagName) break;
5065
5842
  }
5066
5843
  };
5067
5844
  const popUntilOneOf = (/** @type {Set<string>} */ set) => {
5068
5845
  while (open.length) {
5069
5846
  const el = /** @type {HtmlElement} */ (open.pop());
5070
- el.end = tokenEnd;
5071
- if (el.namespace === NS_HTML && set.has(el.tagName)) break;
5847
+ _nodeEnds[el] = tokenEnd;
5848
+ if (_namespaceOf(el) === NS_HTML && set.has(_tagNameOf(el))) break;
5072
5849
  }
5073
5850
  };
5074
5851
 
@@ -5081,8 +5858,8 @@ const buildHtmlAst = (source, fragmentContext) => {
5081
5858
  last = true;
5082
5859
  if (fragment) node = fragment;
5083
5860
  }
5084
- const tn = node.tagName;
5085
- if (node.namespace === NS_HTML) {
5861
+ const tn = _tagNameOf(node);
5862
+ if (_namespaceOf(node) === NS_HTML) {
5086
5863
  if ((tn === "td" || tn === "th") && !last) {
5087
5864
  mode = MODE_IN_CELL;
5088
5865
  return;
@@ -5175,7 +5952,7 @@ const buildHtmlAst = (source, fragmentContext) => {
5175
5952
  const useForeign =
5176
5953
  open.length > 0 &&
5177
5954
  ac &&
5178
- ac.namespace !== NS_HTML &&
5955
+ _namespaceOf(ac) !== NS_HTML &&
5179
5956
  ty !== TOKEN_EOF &&
5180
5957
  shouldUseForeignRules(ac, t);
5181
5958
  if (useForeign) {
@@ -5189,7 +5966,7 @@ const buildHtmlAst = (source, fragmentContext) => {
5189
5966
  /** @type {HtmlElement} */ ac,
5190
5967
  /** @type {Token} */ t
5191
5968
  ) => {
5192
- if (ac.namespace === NS_HTML) return false;
5969
+ if (_namespaceOf(ac) === NS_HTML) return false;
5193
5970
  if (t.type === TOKEN_START_TAG) {
5194
5971
  if (
5195
5972
  mathmlTextIntegrationPoint(ac) &&
@@ -5199,8 +5976,8 @@ const buildHtmlAst = (source, fragmentContext) => {
5199
5976
  return false;
5200
5977
  }
5201
5978
  if (
5202
- ac.namespace === NS_MATHML &&
5203
- ac.tagName === "annotation-xml" &&
5979
+ _namespaceOf(ac) === NS_MATHML &&
5980
+ _tagNameOf(ac) === "annotation-xml" &&
5204
5981
  t.name === "svg"
5205
5982
  ) {
5206
5983
  return false;
@@ -5219,20 +5996,27 @@ const buildHtmlAst = (source, fragmentContext) => {
5219
5996
  };
5220
5997
 
5221
5998
  const mathmlTextIntegrationPoint = (/** @type {HtmlElement} */ el) =>
5222
- el.namespace === NS_MATHML && MATHML_TEXT_INTEGRATION.has(el.tagName);
5999
+ _namespaceOf(el) === NS_MATHML &&
6000
+ MATHML_TEXT_INTEGRATION.has(_tagNameOf(el));
5223
6001
  const htmlIntegrationPoint = (/** @type {HtmlElement} */ el) => {
5224
- if (el.namespace === NS_MATHML && el.tagName === "annotation-xml") {
5225
- const enc = findAttr(el.attributes, "encoding");
5226
- if (
5227
- enc &&
5228
- (enc.value.toLowerCase() === "text/html" ||
5229
- enc.value.toLowerCase() === "application/xhtml+xml")
5230
- ) {
5231
- return true;
6002
+ if (_namespaceOf(el) === NS_MATHML && _tagNameOf(el) === "annotation-xml") {
6003
+ const enc = _findAttr(
6004
+ _nodeAttrStarts[el],
6005
+ _nodeAttrCounts[el],
6006
+ "encoding"
6007
+ );
6008
+ if (enc !== 0) {
6009
+ const value = _attrValueOf(enc).toLowerCase();
6010
+ if (value === "text/html" || value === "application/xhtml+xml") {
6011
+ return true;
6012
+ }
5232
6013
  }
5233
6014
  return false;
5234
6015
  }
5235
- if (el.namespace === NS_SVG && SVG_SPECIAL.has(el.tagName.toLowerCase())) {
6016
+ if (
6017
+ _namespaceOf(el) === NS_SVG &&
6018
+ SVG_SPECIAL.has(_tagNameOf(el).toLowerCase())
6019
+ ) {
5236
6020
  return true;
5237
6021
  }
5238
6022
  return false;
@@ -5240,37 +6024,28 @@ const buildHtmlAst = (source, fragmentContext) => {
5240
6024
 
5241
6025
  const adjustSvgTag = (/** @type {string} */ name) =>
5242
6026
  /** @type {Record<string, string>} */ (SVG_TAG_ADJUST)[name] || name;
6027
+ // Adjust a foreign start tag's attribute run in place (the run is consumed
6028
+ // only by this tag's element): SVG camelCase names are rewritten, and
6029
+ // namespaced names get the serializer-name flag (see `_attrSerializedName`).
5243
6030
  const adjustForeignAttrs = (
5244
- /** @type {HtmlAttribute[]} */ attrs,
6031
+ /** @type {AttributeRun} */ run,
5245
6032
  /** @type {number} */ ns
5246
6033
  ) => {
5247
- // Reuse the original array/objects until an attribute actually needs
5248
- // adjusting; unchanged attributes keep serializedName === undefined and
5249
- // the serializer falls back to `name`. Avoids a new array + object per
5250
- // attribute on every foreign element.
5251
- let out = attrs;
5252
- for (let i = 0; i < attrs.length; i++) {
5253
- const a = attrs[i];
5254
- let name = a.name;
5255
- /** @type {string | undefined} */
5256
- let serializedName;
6034
+ for (let i = run.start; i < run.start + run.count; i++) {
6035
+ const name = _attrNames[i];
5257
6036
  if (
5258
6037
  ns === NS_SVG &&
5259
6038
  /** @type {Record<string, string>} */ (SVG_ATTR_ADJUST)[name]
5260
6039
  ) {
5261
- name = /** @type {Record<string, string>} */ (SVG_ATTR_ADJUST)[name];
5262
- serializedName = name;
6040
+ _attrNames[i] = /** @type {Record<string, string>} */ (SVG_ATTR_ADJUST)[
6041
+ name
6042
+ ];
5263
6043
  }
5264
- if (/** @type {Record<string, string>} */ (FOREIGN_ATTR_NS)[a.name]) {
5265
- serializedName = /** @type {Record<string, string>} */ (
5266
- FOREIGN_ATTR_NS
5267
- )[a.name];
6044
+ if (/** @type {Record<string, string>} */ (FOREIGN_ATTR_NS)[name]) {
6045
+ _attrFlags[i] |= 1;
5268
6046
  }
5269
- if (name === a.name && serializedName === undefined) continue;
5270
- if (out === attrs) out = [...attrs];
5271
- out[i] = { ...a, name, serializedName };
5272
6047
  }
5273
- return out;
6048
+ return run;
5274
6049
  };
5275
6050
 
5276
6051
  const foreignContent = (/** @type {Token} */ t) => {
@@ -5287,17 +6062,16 @@ const buildHtmlAst = (source, fragmentContext) => {
5287
6062
  }
5288
6063
  if (t.type === TOKEN_DOCTYPE) return;
5289
6064
  if (t.type === TOKEN_START_TAG) {
5290
- const acn = adjustedCurrent().namespace;
6065
+ const acn = _namespaceOf(adjustedCurrent());
5291
6066
  if (
5292
6067
  FOREIGN_BREAKOUT.has(t.name) ||
5293
- (t.name === "font" &&
5294
- t.attrs.some((a) => FONT_BREAKOUT_ATTRS.has(a.name)))
6068
+ (t.name === "font" && hasFontBreakoutAttr(t.attrs))
5295
6069
  ) {
5296
6070
  // parse error; pop until integration point / html / mathml-text-integration
5297
6071
  while (open.length > 1) {
5298
6072
  const c = cur();
5299
6073
  if (
5300
- c.namespace === NS_HTML ||
6074
+ _namespaceOf(c) === NS_HTML ||
5301
6075
  mathmlTextIntegrationPoint(c) ||
5302
6076
  htmlIntegrationPoint(c)
5303
6077
  ) {
@@ -5327,8 +6101,8 @@ const buildHtmlAst = (source, fragmentContext) => {
5327
6101
  if (t.type === TOKEN_END_TAG) {
5328
6102
  if (
5329
6103
  t.name === "script" &&
5330
- cur().tagName === "script" &&
5331
- cur().namespace === NS_SVG
6104
+ _tagNameOf(cur()) === "script" &&
6105
+ _namespaceOf(cur()) === NS_SVG
5332
6106
  ) {
5333
6107
  open.pop();
5334
6108
  return;
@@ -5338,7 +6112,7 @@ const buildHtmlAst = (source, fragmentContext) => {
5338
6112
  if (t.name === "p" || t.name === "br") {
5339
6113
  while (
5340
6114
  open.length > 1 &&
5341
- cur().namespace !== NS_HTML &&
6115
+ _namespaceOf(cur()) !== NS_HTML &&
5342
6116
  !mathmlTextIntegrationPoint(cur()) &&
5343
6117
  !htmlIntegrationPoint(cur())
5344
6118
  ) {
@@ -5350,21 +6124,21 @@ const buildHtmlAst = (source, fragmentContext) => {
5350
6124
  // any other end tag
5351
6125
  let i = open.length - 1;
5352
6126
  let node = open[i];
5353
- if (node.tagName.toLowerCase() !== t.name) {
6127
+ if (_tagNameOf(node).toLowerCase() !== t.name) {
5354
6128
  /* parse error */
5355
6129
  }
5356
6130
  while (i >= 0) {
5357
6131
  node = open[i];
5358
6132
  if (i === 0) return;
5359
6133
  if (
5360
- node.namespace !== NS_HTML &&
5361
- node.tagName.toLowerCase() === t.name
6134
+ _namespaceOf(node) !== NS_HTML &&
6135
+ _tagNameOf(node).toLowerCase() === t.name
5362
6136
  ) {
5363
6137
  while (open.length > i) open.pop();
5364
6138
  return;
5365
6139
  }
5366
6140
  i--;
5367
- if (open[i] && open[i].namespace === NS_HTML) {
6141
+ if (open[i] && _namespaceOf(open[i]) === NS_HTML) {
5368
6142
  runMode(t);
5369
6143
  return;
5370
6144
  }
@@ -5380,9 +6154,9 @@ const buildHtmlAst = (source, fragmentContext) => {
5380
6154
  // step 1
5381
6155
  const c = cur();
5382
6156
  if (
5383
- c.namespace === NS_HTML &&
5384
- c.tagName === subject &&
5385
- !afe.some((e) => !e.marker && e.element === c)
6157
+ _namespaceOf(c) === NS_HTML &&
6158
+ _tagNameOf(c) === subject &&
6159
+ !afe.includes(c)
5386
6160
  ) {
5387
6161
  open.pop();
5388
6162
  return true;
@@ -5393,17 +6167,17 @@ const buildHtmlAst = (source, fragmentContext) => {
5393
6167
  // find formatting element
5394
6168
  let fmtIdx = -1;
5395
6169
  for (let i = afe.length - 1; i >= 0; i--) {
5396
- if (afe[i].marker) break;
6170
+ if (afe[i] === AFE_MARKER) break;
5397
6171
  if (
5398
- afeEl(afe[i]).tagName === subject &&
5399
- afeEl(afe[i]).namespace === NS_HTML
6172
+ _tagNameOf(afe[i]) === subject &&
6173
+ _namespaceOf(afe[i]) === NS_HTML
5400
6174
  ) {
5401
6175
  fmtIdx = i;
5402
6176
  break;
5403
6177
  }
5404
6178
  }
5405
6179
  if (fmtIdx === -1) return false; // act as any other end tag
5406
- const fmt = afeEl(afe[fmtIdx]);
6180
+ const fmt = afe[fmtIdx];
5407
6181
  const openIdx = open.indexOf(fmt);
5408
6182
  if (openIdx === -1) {
5409
6183
  afe.splice(fmtIdx, 1);
@@ -5435,10 +6209,7 @@ const buildHtmlAst = (source, fragmentContext) => {
5435
6209
  nodeIdx--;
5436
6210
  node = open[nodeIdx];
5437
6211
  if (node === fmt) break;
5438
- const nodeRef = node;
5439
- let nodeAfeIdx = afe.findIndex(
5440
- (e) => !e.marker && e.element === nodeRef
5441
- );
6212
+ let nodeAfeIdx = afe.indexOf(node);
5442
6213
  if (inner > 3 && nodeAfeIdx !== -1) {
5443
6214
  afe.splice(nodeAfeIdx, 1);
5444
6215
  if (nodeAfeIdx < bookmark) bookmark--;
@@ -5450,12 +6221,12 @@ const buildHtmlAst = (source, fragmentContext) => {
5450
6221
  }
5451
6222
  // create clone
5452
6223
  const clone = mkEl(
5453
- node.tagName,
5454
- node.namespace,
5455
- cloneAttrs(node.attributes),
6224
+ _tagNameOf(node),
6225
+ _namespaceOf(node),
6226
+ cloneAttrs(node),
5456
6227
  null
5457
6228
  );
5458
- afe[nodeAfeIdx] = { element: clone };
6229
+ afe[nodeAfeIdx] = clone;
5459
6230
  open[nodeIdx] = clone;
5460
6231
  node = clone;
5461
6232
  if (lastNode === furthest) bookmark = nodeAfeIdx + 1;
@@ -5470,22 +6241,31 @@ const buildHtmlAst = (source, fragmentContext) => {
5470
6241
  insertAtPlace(place, lastNode);
5471
6242
  // create element for fmt token, take children of furthest
5472
6243
  const cloneFmt = mkEl(
5473
- fmt.tagName,
5474
- fmt.namespace,
5475
- cloneAttrs(fmt.attributes),
6244
+ _tagNameOf(fmt),
6245
+ _namespaceOf(fmt),
6246
+ cloneAttrs(fmt),
5476
6247
  null
5477
6248
  );
5478
- const kids = [...furthest.children];
5479
- furthest.children.length = 0;
5480
- for (const k of kids) appendTo(cloneFmt, k);
6249
+ // Take all direct children of `furthest` (a template's content fragment
6250
+ // deliberately stays put — mirrors childrenOf-less spec behavior here).
6251
+ let k = _nodeFirstChildren[furthest];
6252
+ _nodeFirstChildren[furthest] = 0;
6253
+ _nodeLastChildren[furthest] = 0;
6254
+ while (k !== 0) {
6255
+ const next = _nodeNextSiblings[k];
6256
+ _nodeNextSiblings[k] = 0;
6257
+ _nodeParents[k] = 0;
6258
+ appendTo(cloneFmt, k);
6259
+ k = next;
6260
+ }
5481
6261
  appendTo(furthest, cloneFmt);
5482
6262
  // remove fmt from afe, insert clone at bookmark
5483
- const curFmtIdx = afe.findIndex((e) => !e.marker && e.element === fmt);
6263
+ const curFmtIdx = afe.indexOf(fmt);
5484
6264
  if (curFmtIdx !== -1) {
5485
6265
  afe.splice(curFmtIdx, 1);
5486
6266
  if (curFmtIdx < bookmark) bookmark--;
5487
6267
  }
5488
- afe.splice(bookmark, 0, { element: cloneFmt });
6268
+ afe.splice(bookmark, 0, cloneFmt);
5489
6269
  // remove fmt from open, insert clone below furthest
5490
6270
  const ofi = open.indexOf(fmt);
5491
6271
  if (ofi !== -1) open.splice(ofi, 1);
@@ -5499,8 +6279,8 @@ const buildHtmlAst = (source, fragmentContext) => {
5499
6279
  /** @type {HtmlElement} */ commonAncestor
5500
6280
  ) => {
5501
6281
  if (
5502
- TABLE_CONTEXT.has(commonAncestor.tagName) &&
5503
- commonAncestor.namespace === NS_HTML
6282
+ TABLE_CONTEXT.has(_tagNameOf(commonAncestor)) &&
6283
+ _namespaceOf(commonAncestor) === NS_HTML
5504
6284
  ) {
5505
6285
  // foster
5506
6286
  // reuse appropriatePlace logic but rooted differently: emulate
@@ -5508,15 +6288,15 @@ const buildHtmlAst = (source, fragmentContext) => {
5508
6288
  let lastTable = -1;
5509
6289
  for (let i = open.length - 1; i >= 0; i--) {
5510
6290
  if (
5511
- open[i].tagName === "template" &&
5512
- open[i].namespace === NS_HTML &&
6291
+ _tagNameOf(open[i]) === "template" &&
6292
+ _namespaceOf(open[i]) === NS_HTML &&
5513
6293
  lastTemplate === -1
5514
6294
  ) {
5515
6295
  lastTemplate = i;
5516
6296
  }
5517
6297
  if (
5518
- open[i].tagName === "table" &&
5519
- open[i].namespace === NS_HTML &&
6298
+ _tagNameOf(open[i]) === "table" &&
6299
+ _namespaceOf(open[i]) === NS_HTML &&
5520
6300
  lastTable === -1
5521
6301
  ) {
5522
6302
  lastTable = i;
@@ -5526,23 +6306,32 @@ const buildHtmlAst = (source, fragmentContext) => {
5526
6306
  lastTemplate !== -1 &&
5527
6307
  (lastTable === -1 || lastTemplate > lastTable)
5528
6308
  ) {
5529
- return { parent: open[lastTemplate], beforeNode: null };
6309
+ return { parent: open[lastTemplate], beforeNode: 0 };
5530
6310
  }
5531
- if (lastTable === -1) return { parent: open[0], beforeNode: null };
6311
+ if (lastTable === -1) return { parent: open[0], beforeNode: 0 };
5532
6312
  const table = open[lastTable];
5533
- const tp = findParent(table);
5534
- if (tp) return { parent: tp, beforeNode: table };
5535
- return { parent: open[lastTable - 1], beforeNode: null };
6313
+ const tp = _nodeParents[table];
6314
+ if (tp !== 0) return { parent: tp, beforeNode: table };
6315
+ return { parent: open[lastTable - 1], beforeNode: 0 };
5536
6316
  }
5537
- return { parent: commonAncestor, beforeNode: null };
6317
+ return { parent: commonAncestor, beforeNode: 0 };
5538
6318
  };
5539
6319
 
5540
- const detach = (/** @type {HtmlNode} */ node) => {
5541
- const p = findParent(node);
5542
- if (!p) return;
5543
- const arr = childrenOf(p);
5544
- const idx = arr.indexOf(node);
5545
- if (idx !== -1) arr.splice(idx, 1);
6320
+ const detach = (/** @type {HtmlNodeRef} */ node) => {
6321
+ const p = _nodeParents[node];
6322
+ if (p === 0) return;
6323
+ let prev = 0;
6324
+ let c = _nodeFirstChildren[p];
6325
+ while (c !== 0 && c !== node) {
6326
+ prev = c;
6327
+ c = _nodeNextSiblings[c];
6328
+ }
6329
+ if (c === 0) return;
6330
+ if (prev === 0) _nodeFirstChildren[p] = _nodeNextSiblings[node];
6331
+ else _nodeNextSiblings[prev] = _nodeNextSiblings[node];
6332
+ if (_nodeLastChildren[p] === node) _nodeLastChildren[p] = prev;
6333
+ _nodeNextSiblings[node] = 0;
6334
+ _nodeParents[node] = 0;
5546
6335
  };
5547
6336
 
5548
6337
  // ---------- insertion modes ----------
@@ -5700,18 +6489,20 @@ const buildHtmlAst = (source, fragmentContext) => {
5700
6489
  return;
5701
6490
  }
5702
6491
  if (t.type === TOKEN_COMMENT) {
5703
- insertComment(t.data, t.start, t.end, { parent: doc, beforeNode: null });
6492
+ insertComment(t.data, t.start, t.end, { parent: doc, beforeNode: 0 });
5704
6493
  return;
5705
6494
  }
5706
6495
  if (t.type === TOKEN_DOCTYPE) {
5707
- doc.children.push({
5708
- type: NodeType.Doctype,
5709
- name: t.name,
5710
- publicId: t.publicId,
5711
- systemId: t.systemId,
5712
- start: t.start,
5713
- end: t.end
5714
- });
6496
+ // `skip.doctype` drops the node only; quirks detection below is unaffected.
6497
+ if (!skipDoctype) {
6498
+ const dt = _allocNode(NodeType.Doctype, t.start, t.end);
6499
+ _nodeStrings[dt] = t.name;
6500
+ // At most one doctype node is ever inserted (later doctype tokens
6501
+ // are ignored), so its ids live in two per-parse scalars.
6502
+ _doctypePublicId = t.publicId;
6503
+ _doctypeSystemId = t.systemId;
6504
+ _appendChild(doc, dt);
6505
+ }
5715
6506
  quirks = isQuirky(t.name, t.publicId, t.systemId);
5716
6507
  mode = MODE_BEFORE_HTML;
5717
6508
  return;
@@ -5724,7 +6515,7 @@ const buildHtmlAst = (source, fragmentContext) => {
5724
6515
  modes.beforeHtml = (t) => {
5725
6516
  if (t.type === TOKEN_DOCTYPE) return;
5726
6517
  if (t.type === TOKEN_COMMENT) {
5727
- insertComment(t.data, t.start, t.end, { parent: doc, beforeNode: null });
6518
+ insertComment(t.data, t.start, t.end, { parent: doc, beforeNode: 0 });
5728
6519
  return;
5729
6520
  }
5730
6521
  if (t.type === TOKEN_CHAR) {
@@ -5734,7 +6525,7 @@ const buildHtmlAst = (source, fragmentContext) => {
5734
6525
  }
5735
6526
  if (t.type === TOKEN_START_TAG && t.name === "html") {
5736
6527
  const el = mkEl("html", NS_HTML, t.attrs, t.pos);
5737
- doc.children.push(el);
6528
+ _appendChild(doc, el);
5738
6529
  open.push(el);
5739
6530
  mode = MODE_BEFORE_HEAD;
5740
6531
  return;
@@ -5742,8 +6533,8 @@ const buildHtmlAst = (source, fragmentContext) => {
5742
6533
  if (t.type === TOKEN_END_TAG && !HEAD_BODY_HTML_BR.has(t.name)) {
5743
6534
  return;
5744
6535
  }
5745
- const el = mkEl("html", NS_HTML, [], null);
5746
- doc.children.push(el);
6536
+ const el = mkEl("html", NS_HTML, EMPTY_ATTRS, null);
6537
+ _appendChild(doc, el);
5747
6538
  open.push(el);
5748
6539
  mode = MODE_BEFORE_HEAD;
5749
6540
  process(t);
@@ -5819,7 +6610,11 @@ const buildHtmlAst = (source, fragmentContext) => {
5819
6610
  mode = MODE_IN_TEMPLATE;
5820
6611
  templateModes.push(MODE_IN_TEMPLATE);
5821
6612
  const el = cur();
5822
- el.templateContent = { type: NodeType.DocumentFragment, children: [] };
6613
+ const fragment = _allocNode(NodeType.DocumentFragment, 0, 0);
6614
+ _nodeStrings[fragment] = "";
6615
+ // Parent link so the iterative walk can ascend out of the content.
6616
+ _nodeParents[fragment] = el;
6617
+ _nodeTemplateContents[el] = fragment;
5823
6618
  return;
5824
6619
  }
5825
6620
  if (t.name === "head") return;
@@ -5887,7 +6682,7 @@ const buildHtmlAst = (source, fragmentContext) => {
5887
6682
  if (t.type === TOKEN_CHAR) {
5888
6683
  const r = leadingWs(t, true);
5889
6684
  if (!r) return;
5890
- insertHtmlElement("body", [], null);
6685
+ insertHtmlElement("body", EMPTY_ATTRS, null);
5891
6686
  mode = MODE_IN_BODY;
5892
6687
  process(r);
5893
6688
  return;
@@ -5924,7 +6719,7 @@ const buildHtmlAst = (source, fragmentContext) => {
5924
6719
  if (t.name === "template") return modes.inHead(t);
5925
6720
  if (!BODY_HTML_BR.has(t.name)) return;
5926
6721
  }
5927
- insertHtmlElement("body", [], null);
6722
+ insertHtmlElement("body", EMPTY_ATTRS, null);
5928
6723
  mode = MODE_IN_BODY;
5929
6724
  process(t);
5930
6725
  };
@@ -5962,10 +6757,10 @@ const buildHtmlAst = (source, fragmentContext) => {
5962
6757
  const anyOtherEndTag = (/** @type {string} */ name) => {
5963
6758
  for (let i = open.length - 1; i >= 0; i--) {
5964
6759
  const node = open[i];
5965
- if (node.namespace === NS_HTML && node.tagName === name) {
6760
+ if (_namespaceOf(node) === NS_HTML && _tagNameOf(node) === name) {
5966
6761
  generateImpliedEndTags(name);
5967
6762
  while (open.length > i) {
5968
- open[open.length - 1].end = tokenEnd;
6763
+ _nodeEnds[open[open.length - 1]] = tokenEnd;
5969
6764
  open.pop();
5970
6765
  }
5971
6766
  return;
@@ -5980,12 +6775,7 @@ const buildHtmlAst = (source, fragmentContext) => {
5980
6775
  if (open.some(isHtmlTemplateEl)) {
5981
6776
  return;
5982
6777
  }
5983
- const htmlEl = open[0];
5984
- for (const a of t.attrs) {
5985
- if (!htmlEl.attributes.some((x) => x.name === a.name)) {
5986
- htmlEl.attributes.push(a);
5987
- }
5988
- }
6778
+ mergeAttrs(open[0], t.attrs);
5989
6779
  return;
5990
6780
  }
5991
6781
  if (HEAD_ELEMENTS.has(name)) {
@@ -5993,20 +6783,20 @@ const buildHtmlAst = (source, fragmentContext) => {
5993
6783
  }
5994
6784
  if (name === "body") {
5995
6785
  const second = open[1];
5996
- if (!second || second.tagName !== "body" || open.some(isHtmlTemplateEl)) {
6786
+ if (
6787
+ !second ||
6788
+ _tagNameOf(second) !== "body" ||
6789
+ open.some(isHtmlTemplateEl)
6790
+ ) {
5997
6791
  return;
5998
6792
  }
5999
6793
  framesetOk = false;
6000
- for (const a of t.attrs) {
6001
- if (!second.attributes.some((x) => x.name === a.name)) {
6002
- second.attributes.push(a);
6003
- }
6004
- }
6794
+ mergeAttrs(second, t.attrs);
6005
6795
  return;
6006
6796
  }
6007
6797
  if (name === "frameset") {
6008
6798
  const second = open[1];
6009
- if (!second || second.tagName !== "body") return;
6799
+ if (!second || _tagNameOf(second) !== "body") return;
6010
6800
  if (!framesetOk) return;
6011
6801
  detach(second);
6012
6802
  while (open.length > 1) open.pop();
@@ -6021,7 +6811,9 @@ const buildHtmlAst = (source, fragmentContext) => {
6021
6811
  }
6022
6812
  if (HEADING.has(name)) {
6023
6813
  closeIfPInButtonScope();
6024
- if (cur().namespace === NS_HTML && HEADING.has(cur().tagName)) open.pop();
6814
+ if (_namespaceOf(cur()) === NS_HTML && HEADING.has(_tagNameOf(cur()))) {
6815
+ open.pop();
6816
+ }
6025
6817
  insertHtmlElement(name, t.attrs, t.pos);
6026
6818
  return;
6027
6819
  }
@@ -6047,14 +6839,17 @@ const buildHtmlAst = (source, fragmentContext) => {
6047
6839
  framesetOk = false;
6048
6840
  for (let i = open.length - 1; i >= 0; i--) {
6049
6841
  const node = open[i];
6050
- if (node.namespace === NS_HTML && node.tagName === "li") {
6842
+ if (_namespaceOf(node) === NS_HTML && _tagNameOf(node) === "li") {
6051
6843
  generateImpliedEndTags("li");
6052
6844
  popUntil("li");
6053
6845
  break;
6054
6846
  }
6055
6847
  if (
6056
6848
  isSpecial(node) &&
6057
- !(node.namespace === NS_HTML && ADDRESS_DIV_P.has(node.tagName))
6849
+ !(
6850
+ _namespaceOf(node) === NS_HTML &&
6851
+ ADDRESS_DIV_P.has(_tagNameOf(node))
6852
+ )
6058
6853
  ) {
6059
6854
  break;
6060
6855
  }
@@ -6068,16 +6863,19 @@ const buildHtmlAst = (source, fragmentContext) => {
6068
6863
  for (let i = open.length - 1; i >= 0; i--) {
6069
6864
  const node = open[i];
6070
6865
  if (
6071
- node.namespace === NS_HTML &&
6072
- (node.tagName === "dd" || node.tagName === "dt")
6866
+ _namespaceOf(node) === NS_HTML &&
6867
+ (_tagNameOf(node) === "dd" || _tagNameOf(node) === "dt")
6073
6868
  ) {
6074
- generateImpliedEndTags(node.tagName);
6075
- popUntil(node.tagName);
6869
+ generateImpliedEndTags(_tagNameOf(node));
6870
+ popUntil(_tagNameOf(node));
6076
6871
  break;
6077
6872
  }
6078
6873
  if (
6079
6874
  isSpecial(node) &&
6080
- !(node.namespace === NS_HTML && ADDRESS_DIV_P.has(node.tagName))
6875
+ !(
6876
+ _namespaceOf(node) === NS_HTML &&
6877
+ ADDRESS_DIV_P.has(_tagNameOf(node))
6878
+ )
6081
6879
  ) {
6082
6880
  break;
6083
6881
  }
@@ -6104,14 +6902,14 @@ const buildHtmlAst = (source, fragmentContext) => {
6104
6902
  if (name === "a") {
6105
6903
  // if there's an <a> in afe after last marker
6106
6904
  for (let i = afe.length - 1; i >= 0; i--) {
6107
- if (afe[i].marker) break;
6108
- if (afeEl(afe[i]).tagName === "a") {
6905
+ if (afe[i] === AFE_MARKER) break;
6906
+ if (_tagNameOf(afe[i]) === "a") {
6109
6907
  adoptionAgency("a", t.pos);
6110
6908
  const idx = afe.findIndex(
6111
- (e) => !e.marker && afeEl(e).tagName === "a"
6909
+ (e) => e !== AFE_MARKER && _tagNameOf(e) === "a"
6112
6910
  );
6113
6911
  if (idx !== -1) {
6114
- const el = afeEl(afe[idx]);
6912
+ const el = afe[idx];
6115
6913
  afe.splice(idx, 1);
6116
6914
  const oi = open.indexOf(el);
6117
6915
  if (oi !== -1) open.splice(oi, 1);
@@ -6171,16 +6969,18 @@ const buildHtmlAst = (source, fragmentContext) => {
6171
6969
  resetInsertionMode();
6172
6970
  } else if (
6173
6971
  fragment &&
6174
- fragment.namespace === NS_HTML &&
6175
- fragment.tagName === "select"
6972
+ _namespaceOf(fragment) === NS_HTML &&
6973
+ _tagNameOf(fragment) === "select"
6176
6974
  ) {
6177
6975
  return;
6178
6976
  }
6179
6977
  reconstructAfe();
6180
6978
  insertHtmlElement("input", t.attrs, t.pos);
6181
6979
  open.pop();
6182
- const ty = findAttr(t.attrs, "type");
6183
- if (!ty || ty.value.toLowerCase() !== "hidden") framesetOk = false;
6980
+ const ty = _findAttr(t.attrs.start, t.attrs.count, "type");
6981
+ if (ty === 0 || _attrValueOf(ty).toLowerCase() !== "hidden") {
6982
+ framesetOk = false;
6983
+ }
6184
6984
  return;
6185
6985
  }
6186
6986
  if (PARAM_SOURCE_TRACK.has(name)) {
@@ -6189,8 +6989,10 @@ const buildHtmlAst = (source, fragmentContext) => {
6189
6989
  return;
6190
6990
  }
6191
6991
  if (name === "hr") {
6192
- if (cur().namespace === NS_HTML && cur().tagName === "option") open.pop();
6193
- if (cur().namespace === NS_HTML && cur().tagName === "optgroup") {
6992
+ if (_namespaceOf(cur()) === NS_HTML && _tagNameOf(cur()) === "option") {
6993
+ open.pop();
6994
+ }
6995
+ if (_namespaceOf(cur()) === NS_HTML && _tagNameOf(cur()) === "optgroup") {
6194
6996
  open.pop();
6195
6997
  }
6196
6998
  closeIfPInButtonScope();
@@ -6239,11 +7041,13 @@ const buildHtmlAst = (source, fragmentContext) => {
6239
7041
  return;
6240
7042
  }
6241
7043
  if (name === "optgroup" || name === "option") {
6242
- if (cur().namespace === NS_HTML && cur().tagName === "option") open.pop();
7044
+ if (_namespaceOf(cur()) === NS_HTML && _tagNameOf(cur()) === "option") {
7045
+ open.pop();
7046
+ }
6243
7047
  if (
6244
7048
  name === "optgroup" &&
6245
- cur().namespace === NS_HTML &&
6246
- cur().tagName === "optgroup"
7049
+ _namespaceOf(cur()) === NS_HTML &&
7050
+ _tagNameOf(cur()) === "optgroup"
6247
7051
  ) {
6248
7052
  open.pop();
6249
7053
  }
@@ -6283,20 +7087,13 @@ const buildHtmlAst = (source, fragmentContext) => {
6283
7087
  insertHtmlElement(name, t.attrs, t.pos);
6284
7088
  };
6285
7089
 
6286
- const adjustMathmlAttrs = (/** @type {HtmlAttribute[]} */ attrs) => {
6287
- // Only `definitionurl` is rewritten; reuse the original array otherwise.
6288
- let out = attrs;
6289
- for (let i = 0; i < attrs.length; i++) {
6290
- if (attrs[i].name === "definitionurl") {
6291
- if (out === attrs) out = [...attrs];
6292
- out[i] = {
6293
- ...attrs[i],
6294
- name: "definitionURL",
6295
- serializedName: "definitionURL"
6296
- };
6297
- }
7090
+ // Only `definitionurl` is rewritten (in place — the run is consumed only by
7091
+ // this tag's element); the camelCase name serializes as itself.
7092
+ const adjustMathmlAttrs = (/** @type {AttributeRun} */ run) => {
7093
+ for (let i = run.start; i < run.start + run.count; i++) {
7094
+ if (_attrNames[i] === "definitionurl") _attrNames[i] = "definitionURL";
6298
7095
  }
6299
- return out;
7096
+ return run;
6300
7097
  };
6301
7098
 
6302
7099
  const endTagInBody = (/** @type {EndTagToken} */ t) => {
@@ -6323,7 +7120,7 @@ const buildHtmlAst = (source, fragmentContext) => {
6323
7120
  if (name === "form") {
6324
7121
  if (!open.some(isHtmlTemplateEl)) {
6325
7122
  const node = form;
6326
- form = null;
7123
+ form = 0;
6327
7124
  if (!node || !inScopeEl(node)) return;
6328
7125
  generateImpliedEndTags();
6329
7126
  const idx = open.indexOf(node);
@@ -6420,7 +7217,10 @@ const buildHtmlAst = (source, fragmentContext) => {
6420
7217
  // current element: a non-matching name (e.g. a fragment context), or
6421
7218
  // a name that ran straight to EOF with no delimiter (`</script` at
6422
7219
  // EOF — the tokenizer still emits a partial tag there).
6423
- if (cur() && (t.name !== cur().tagName || t.pos.end === t.pos.nameEnd)) {
7220
+ if (
7221
+ cur() &&
7222
+ (t.name !== _tagNameOf(cur()) || t.pos.end === t.pos.nameEnd)
7223
+ ) {
6424
7224
  insertCharacters(
6425
7225
  source.slice(t.pos.start, t.pos.end),
6426
7226
  t.pos.start,
@@ -6428,7 +7228,8 @@ const buildHtmlAst = (source, fragmentContext) => {
6428
7228
  );
6429
7229
  return;
6430
7230
  }
6431
- open.pop();
7231
+ // span the close tag, like every other explicit-close pop
7232
+ _nodeEnds[/** @type {HtmlElement} */ (open.pop())] = tokenEnd;
6432
7233
  mode = originalMode;
6433
7234
  }
6434
7235
  };
@@ -6440,7 +7241,7 @@ const buildHtmlAst = (source, fragmentContext) => {
6440
7241
  modes.inTable = (t) => {
6441
7242
  if (t.type === TOKEN_CHAR) {
6442
7243
  const c = cur();
6443
- if (TABLE_CONTEXT.has(c.tagName) && c.namespace === NS_HTML) {
7244
+ if (TABLE_CONTEXT.has(_tagNameOf(c)) && _namespaceOf(c) === NS_HTML) {
6444
7245
  pendingTableChars = { list: [], hasNonWs: false };
6445
7246
  originalMode = mode;
6446
7247
  mode = MODE_IN_TABLE_TEXT;
@@ -6495,8 +7296,8 @@ const buildHtmlAst = (source, fragmentContext) => {
6495
7296
  return modes.inHead(t);
6496
7297
  }
6497
7298
  if (name === "input") {
6498
- const ty = findAttr(t.attrs, "type");
6499
- if (ty && ty.value.toLowerCase() === "hidden") {
7299
+ const ty = _findAttr(t.attrs.start, t.attrs.count, "type");
7300
+ if (ty !== 0 && _attrValueOf(ty).toLowerCase() === "hidden") {
6500
7301
  insertHtmlElement("input", t.attrs, t.pos);
6501
7302
  open.pop();
6502
7303
  return;
@@ -6566,7 +7367,7 @@ const buildHtmlAst = (source, fragmentContext) => {
6566
7367
  const clearStackToTableContext = () => {
6567
7368
  while (open.length) {
6568
7369
  const c = cur();
6569
- if (c.namespace === NS_HTML && CLEAR_TABLE.has(c.tagName)) {
7370
+ if (_namespaceOf(c) === NS_HTML && CLEAR_TABLE.has(_tagNameOf(c))) {
6570
7371
  break;
6571
7372
  }
6572
7373
  open.pop();
@@ -6575,7 +7376,7 @@ const buildHtmlAst = (source, fragmentContext) => {
6575
7376
  const clearStackToTableBodyContext = () => {
6576
7377
  while (open.length) {
6577
7378
  const c = cur();
6578
- if (c.namespace === NS_HTML && CLEAR_TABLE_BODY.has(c.tagName)) {
7379
+ if (_namespaceOf(c) === NS_HTML && CLEAR_TABLE_BODY.has(_tagNameOf(c))) {
6579
7380
  break;
6580
7381
  }
6581
7382
  open.pop();
@@ -6584,7 +7385,7 @@ const buildHtmlAst = (source, fragmentContext) => {
6584
7385
  const clearStackToTableRowContext = () => {
6585
7386
  while (open.length) {
6586
7387
  const c = cur();
6587
- if (c.namespace === NS_HTML && CLEAR_TABLE_ROW.has(c.tagName)) {
7388
+ if (_namespaceOf(c) === NS_HTML && CLEAR_TABLE_ROW.has(_tagNameOf(c))) {
6588
7389
  break;
6589
7390
  }
6590
7391
  open.pop();
@@ -6617,7 +7418,7 @@ const buildHtmlAst = (source, fragmentContext) => {
6617
7418
  if (t.type === TOKEN_CHAR) {
6618
7419
  const r = leadingWs(t, true);
6619
7420
  if (!r) return;
6620
- if (cur().tagName !== "colgroup") return;
7421
+ if (_tagNameOf(cur()) !== "colgroup") return;
6621
7422
  open.pop();
6622
7423
  mode = MODE_IN_TABLE;
6623
7424
  process(r);
@@ -6635,7 +7436,7 @@ const buildHtmlAst = (source, fragmentContext) => {
6635
7436
  return;
6636
7437
  }
6637
7438
  if (t.type === TOKEN_END_TAG && t.name === "colgroup") {
6638
- if (cur().tagName !== "colgroup") return;
7439
+ if (_tagNameOf(cur()) !== "colgroup") return;
6639
7440
  open.pop();
6640
7441
  mode = MODE_IN_TABLE;
6641
7442
  return;
@@ -6648,7 +7449,7 @@ const buildHtmlAst = (source, fragmentContext) => {
6648
7449
  return modes.inHead(t);
6649
7450
  }
6650
7451
  if (t.type === TOKEN_EOF) return modes.inBody(t);
6651
- if (cur().tagName !== "colgroup") return;
7452
+ if (_tagNameOf(cur()) !== "colgroup") return;
6652
7453
  open.pop();
6653
7454
  mode = MODE_IN_TABLE;
6654
7455
  process(t);
@@ -6798,7 +7599,7 @@ const buildHtmlAst = (source, fragmentContext) => {
6798
7599
  if (t.type === TOKEN_COMMENT) {
6799
7600
  insertComment(t.data, t.start, t.end, {
6800
7601
  parent: open[0],
6801
- beforeNode: null
7602
+ beforeNode: 0
6802
7603
  });
6803
7604
  return;
6804
7605
  }
@@ -6831,9 +7632,11 @@ const buildHtmlAst = (source, fragmentContext) => {
6831
7632
  return;
6832
7633
  }
6833
7634
  if (t.type === TOKEN_END_TAG && t.name === "frameset") {
6834
- if (cur().tagName === "html") return;
7635
+ if (_tagNameOf(cur()) === "html") return;
6835
7636
  open.pop();
6836
- if (!fragment && cur().tagName !== "frameset") mode = MODE_AFTER_FRAMESET;
7637
+ if (!fragment && _tagNameOf(cur()) !== "frameset") {
7638
+ mode = MODE_AFTER_FRAMESET;
7639
+ }
6837
7640
  return;
6838
7641
  }
6839
7642
  if (t.type === TOKEN_START_TAG && t.name === "frame") {
@@ -6869,7 +7672,7 @@ const buildHtmlAst = (source, fragmentContext) => {
6869
7672
 
6870
7673
  modes.afterAfterBody = (t) => {
6871
7674
  if (t.type === TOKEN_COMMENT) {
6872
- insertComment(t.data, t.start, t.end, { parent: doc, beforeNode: null });
7675
+ insertComment(t.data, t.start, t.end, { parent: doc, beforeNode: 0 });
6873
7676
  return;
6874
7677
  }
6875
7678
  if (t.type === TOKEN_DOCTYPE) return modes.inBody(t);
@@ -6882,7 +7685,7 @@ const buildHtmlAst = (source, fragmentContext) => {
6882
7685
 
6883
7686
  modes.afterAfterFrameset = (t) => {
6884
7687
  if (t.type === TOKEN_COMMENT) {
6885
- insertComment(t.data, t.start, t.end, { parent: doc, beforeNode: null });
7688
+ insertComment(t.data, t.start, t.end, { parent: doc, beforeNode: 0 });
6886
7689
  return;
6887
7690
  }
6888
7691
  if (t.type === TOKEN_DOCTYPE) return modes.inBody(t);
@@ -6904,9 +7707,9 @@ const buildHtmlAst = (source, fragmentContext) => {
6904
7707
  ctxNs = NS_MATHML;
6905
7708
  ctxName = ctxName.slice(5);
6906
7709
  }
6907
- fragment = mkEl(ctxName, ctxNs, [], null);
6908
- const htmlEl = mkEl("html", NS_HTML, [], null);
6909
- doc.children.push(htmlEl);
7710
+ fragment = mkEl(ctxName, ctxNs, EMPTY_ATTRS, null);
7711
+ const htmlEl = mkEl("html", NS_HTML, EMPTY_ATTRS, null);
7712
+ _appendChild(doc, htmlEl);
6910
7713
  open.push(htmlEl);
6911
7714
  if (ctxNs !== NS_HTML) {
6912
7715
  mode = MODE_IN_BODY;
@@ -6932,7 +7735,7 @@ const buildHtmlAst = (source, fragmentContext) => {
6932
7735
  }
6933
7736
 
6934
7737
  // ---------- tokenizer callbacks ----------
6935
- const decode = decodeHtmlEntities;
7738
+ const decode = decodeEntities;
6936
7739
 
6937
7740
  let swallowNextNewline = false;
6938
7741
  // Set by the tokenizer when it hits EOF mid-tag; such a partial tag is
@@ -6949,7 +7752,7 @@ const buildHtmlAst = (source, fragmentContext) => {
6949
7752
  type: TOKEN_EOF,
6950
7753
  name: "",
6951
7754
  data: "",
6952
- attrs: [],
7755
+ attrs: { start: 0, count: 0 },
6953
7756
  selfClosing: false,
6954
7757
  start: 0,
6955
7758
  end: 0,
@@ -6961,12 +7764,12 @@ const buildHtmlAst = (source, fragmentContext) => {
6961
7764
  const dispatch = () =>
6962
7765
  process(/** @type {Token} */ (/** @type {unknown} */ (tok)));
6963
7766
 
6964
- walkHtmlTokens(source, 0, {
7767
+ tokenize(source, pos, {
6965
7768
  isForeign: () => {
6966
7769
  const ac = adjustedCurrent();
6967
- return open.length > 0 && ac && ac.namespace !== NS_HTML;
7770
+ return open.length > 0 && ac !== 0 && _namespaceOf(ac) !== NS_HTML;
6968
7771
  },
6969
- fragmentContext: fragment ? fragment.tagName : undefined,
7772
+ fragmentContext: fragment ? _tagNameOf(fragment) : undefined,
6970
7773
  parseError: (input, code) => {
6971
7774
  if (code === "eof-in-tag") eofInTag = true;
6972
7775
  },
@@ -6988,7 +7791,7 @@ const buildHtmlAst = (source, fragmentContext) => {
6988
7791
  // The tokenizer emits CDATA sections through this callback too.
6989
7792
  if (input.startsWith("<![CDATA[", start)) {
6990
7793
  const ac = adjustedCurrent();
6991
- if (open.length > 0 && ac && ac.namespace !== NS_HTML) {
7794
+ if (open.length > 0 && ac && _namespaceOf(ac) !== NS_HTML) {
6992
7795
  const innerEnd = input.endsWith("]]>", end) ? end - 3 : end;
6993
7796
  const data = input.slice(start + 9, innerEnd).replace(/\r\n?/g, "\n");
6994
7797
  if (data !== "") {
@@ -7030,17 +7833,42 @@ const buildHtmlAst = (source, fragmentContext) => {
7030
7833
  return end;
7031
7834
  },
7032
7835
  text: (input, start, end) => {
7836
+ // `skip.text` fast path: node dropped, so only whitespace-ness matters.
7837
+ // With no `&`/`\0`/`\r` (and no pending newline-swallow) the decoded value
7838
+ // equals the raw range — dispatch a canonical marker (`" "`/`"x"`) without
7839
+ // the slice + entity decode; anything trickier falls through.
7840
+ if (skipText && !swallowNextNewline) {
7841
+ let hasNonWs = false;
7842
+ let i = start;
7843
+ for (; i < end; i++) {
7844
+ const c = input.charCodeAt(i);
7845
+ // 2 = break (& / NUL / CR), 1 = whitespace, 0 = other.
7846
+ const cls = c < 128 ? _TEXT_SCAN_CLASS[c] : 0;
7847
+ if (cls === 2) break;
7848
+ if (cls === 0) hasNonWs = true;
7849
+ }
7850
+ if (i === end) {
7851
+ tok.type = TOKEN_CHAR;
7852
+ tok.data = hasNonWs ? "x" : " ";
7853
+ tok.start = start;
7854
+ tok.end = end;
7855
+ dispatch();
7856
+ return end;
7857
+ }
7858
+ }
7033
7859
  const raw = input.slice(start, end);
7034
7860
  // CR normalization only when a CR is actually present (common case has none).
7035
7861
  const s = !raw.includes("\r") ? raw : raw.replace(/\r\n?/g, "\n");
7036
7862
  const top = adjustedCurrent();
7037
7863
  const rawMode =
7038
7864
  mode === MODE_TEXT ||
7039
- (top && top.namespace === NS_HTML && top.tagName === "plaintext");
7865
+ (top &&
7866
+ _namespaceOf(top) === NS_HTML &&
7867
+ _tagNameOf(top) === "plaintext");
7040
7868
  const noDecode =
7041
7869
  top &&
7042
- top.namespace === NS_HTML &&
7043
- NO_DECODE_TEXT.has(top.tagName) &&
7870
+ _namespaceOf(top) === NS_HTML &&
7871
+ NO_DECODE_TEXT.has(_tagNameOf(top)) &&
7044
7872
  (mode === MODE_TEXT || mode === MODE_IN_BODY);
7045
7873
  let data = noDecode ? s : decode(s, false);
7046
7874
  // In RAWTEXT/RCDATA/script/PLAINTEXT, NULL becomes U+FFFD (tokenizer
@@ -7061,32 +7889,29 @@ const buildHtmlAst = (source, fragmentContext) => {
7061
7889
  },
7062
7890
  attribute: (input, nameStart, nameEnd, valueStart, valueEnd, quoteType) => {
7063
7891
  const name = internLowerName(ATTR_NAME_INTERN, input, nameStart, nameEnd);
7064
- // Raw (undecoded) value: consumers re-resolve requests from it and the
7065
- // offsets must stay aligned with the source. Entity decoding for the
7066
- // html5lib serializer happens there, not here.
7067
- const value = valueStart !== -1 ? input.slice(valueStart, valueEnd) : "";
7892
+ if (pendAttrStart === 0) pendAttrStart = _attrCount + 1;
7068
7893
  // Drop duplicate attribute names (per spec). Plain loop avoids a
7069
7894
  // per-attribute closure allocation on this hot path.
7070
7895
  let dup = false;
7071
- for (let i = 0; i < pendingAttrs.length; i++) {
7072
- if (pendingAttrs[i].name === name) {
7896
+ for (let i = pendAttrStart; i <= _attrCount; i++) {
7897
+ if (_attrNames[i] === name) {
7073
7898
  dup = true;
7074
7899
  break;
7075
7900
  }
7076
7901
  }
7077
7902
  if (!dup) {
7078
- // Field order/shape matches `cloneAttrs` / `adjustForeignAttrs` output
7079
- // (serializedName present from creation) so all attribute objects
7080
- // share one hidden class.
7081
- pendingAttrs.push({
7903
+ // The raw (undecoded) value is read from the source by offset on
7904
+ // demand: consumers re-resolve requests from it and the offsets must
7905
+ // stay aligned with the source. Only a valueless attribute stores an
7906
+ // override ("").
7907
+ _allocAttr(
7082
7908
  name,
7083
- value,
7084
- serializedName: undefined,
7909
+ valueStart !== -1 ? null : "",
7085
7910
  nameStart,
7086
7911
  nameEnd,
7087
7912
  valueStart,
7088
7913
  valueEnd
7089
- });
7914
+ );
7090
7915
  }
7091
7916
  if (valueStart === -1) return nameEnd;
7092
7917
  return quoteType !== QUOTE_NONE ? valueEnd + 1 : valueEnd;
@@ -7095,25 +7920,20 @@ const buildHtmlAst = (source, fragmentContext) => {
7095
7920
  // A start tag the tokenizer only emitted because it hit EOF mid-tag
7096
7921
  // is dropped, matching the spec's eof-in-tag handling.
7097
7922
  if (eofInTag) {
7098
- if (pendingAttrs.length !== 0) pendingAttrs = [];
7923
+ // Any attribute slots already allocated for it are orphaned.
7924
+ pendAttrStart = 0;
7099
7925
  return end;
7100
7926
  }
7101
7927
  const name = internLowerName(TAG_NAME_INTERN, input, nameStart, nameEnd);
7102
7928
  if (name === "selectedcontent") sawSelectedContent = true;
7103
- // Attributeless elements share the frozen EMPTY_ATTRS (no allocation);
7104
- // the empty pendingAttrs buffer is reused for the next tag. `<html>`
7105
- // and `<body>` keep their own mutable array since later duplicate tags
7106
- // merge attributes into it.
7107
- let attrs;
7108
- if (pendingAttrs.length === 0 && name !== "html" && name !== "body") {
7109
- attrs = EMPTY_ATTRS;
7110
- } else {
7111
- attrs = pendingAttrs;
7112
- pendingAttrs = [];
7113
- }
7114
7929
  tok.type = TOKEN_START_TAG;
7115
7930
  tok.name = name;
7116
- tok.attrs = attrs;
7931
+ // The reused token carries the tag's attribute run; every consumer of
7932
+ // `t.attrs` runs synchronously within this dispatch.
7933
+ tok.attrs.start = pendAttrStart;
7934
+ tok.attrs.count =
7935
+ pendAttrStart === 0 ? 0 : _attrCount + 1 - pendAttrStart;
7936
+ pendAttrStart = 0;
7117
7937
  tok.selfClosing = selfClosing;
7118
7938
  tok.swallowNewline = false;
7119
7939
  tok.pos.start = start;
@@ -7126,8 +7946,8 @@ const buildHtmlAst = (source, fragmentContext) => {
7126
7946
  },
7127
7947
  closeTag: (input, start, end, nameStart, nameEnd) => {
7128
7948
  const name = internLowerName(TAG_NAME_INTERN, input, nameStart, nameEnd);
7129
- // End tags drop any parsed attributes; only reallocate if non-empty.
7130
- if (pendingAttrs.length !== 0) pendingAttrs = [];
7949
+ // End tags drop any parsed attributes (the slots are orphaned).
7950
+ pendAttrStart = 0;
7131
7951
  tok.type = TOKEN_END_TAG;
7132
7952
  tok.name = name;
7133
7953
  tok.pos.start = start;
@@ -7141,63 +7961,73 @@ const buildHtmlAst = (source, fragmentContext) => {
7141
7961
  tok.type = TOKEN_EOF;
7142
7962
  dispatch();
7143
7963
 
7144
- if (sawSelectedContent) mirrorSelectedContent(doc, null);
7964
+ if (sawSelectedContent) mirrorSelectedContent(doc, 0);
7145
7965
 
7146
7966
  return doc;
7147
7967
  };
7148
7968
 
7149
7969
  /**
7150
- * Deep-clone a node, dropping source offsets so cloned content does not
7151
- * re-emit dependencies.
7152
- * @param {HtmlNode} node node
7153
- * @returns {HtmlNode} clone
7970
+ * Deep-clone a node into fresh ids, dropping attribute source offsets and the
7971
+ * raw-text body span so cloned content does not re-emit dependencies.
7972
+ * @param {HtmlNodeRef} node node
7973
+ * @returns {HtmlNodeRef} clone
7154
7974
  */
7155
7975
  const cloneSubtree = (node) => {
7156
- if (node.type !== NodeType.Element) return { ...node };
7157
- return {
7158
- type: NodeType.Element,
7159
- tagName: node.tagName,
7160
- namespace: node.namespace,
7161
- attributes: node.attributes.map((a) => ({
7162
- name: a.name,
7163
- value: a.value,
7164
- serializedName: a.serializedName,
7165
- nameStart: -1,
7166
- nameEnd: -1,
7167
- valueStart: -1,
7168
- valueEnd: -1
7169
- })),
7170
- children: node.children.map(cloneSubtree),
7171
- selfClosing: node.selfClosing,
7172
- start: node.start,
7173
- end: node.end,
7174
- tagEnd: node.tagEnd,
7175
- nameEnd: node.nameEnd
7176
- };
7976
+ const ty = _nodeTypes[node];
7977
+ const clone = _allocNode(ty, _nodeStarts[node], _nodeEnds[node]);
7978
+ _nodeStrings[clone] = _nodeStrings[node];
7979
+ if (ty !== NodeType.Element) return clone;
7980
+ _nodeFlags[clone] = _nodeFlags[node];
7981
+ const attrs = cloneAttrs(node);
7982
+ _nodeAttrStarts[clone] = attrs.start;
7983
+ _nodeAttrCounts[clone] = attrs.count;
7984
+ _nodeTagEnds[clone] = _nodeTagEnds[node];
7985
+ _nodeNameEnds[clone] = _nodeNameEnds[node];
7986
+ // Empty body span: the clone re-emits no raw-text dependency.
7987
+ _nodeContentEnds[clone] = _nodeTagEnds[node];
7988
+ for (let k = _nodeFirstChildren[node]; k !== 0; k = _nodeNextSiblings[k]) {
7989
+ _appendChild(clone, cloneSubtree(k));
7990
+ }
7991
+ // Clone `<template>` content into the clone's own fragment.
7992
+ const tc = _nodeTemplateContents[node];
7993
+ if (tc !== 0) {
7994
+ const fragment = _allocNode(NodeType.DocumentFragment, 0, 0);
7995
+ _nodeStrings[fragment] = "";
7996
+ // Parent link so the iterative walk can ascend out of the content.
7997
+ _nodeParents[fragment] = clone;
7998
+ _nodeTemplateContents[clone] = fragment;
7999
+ for (let k = _nodeFirstChildren[tc]; k !== 0; k = _nodeNextSiblings[k]) {
8000
+ _appendChild(fragment, cloneSubtree(k));
8001
+ }
8002
+ }
8003
+ return clone;
7177
8004
  };
7178
8005
 
7179
8006
  /**
7180
8007
  * The selected option of a select: the last `<option selected>`, else the
7181
8008
  * first option (scanning direct children and `<optgroup>` children).
7182
8009
  * @param {HtmlElement} select select element
7183
- * @returns {HtmlElement | null} selected option
8010
+ * @returns {HtmlElement} selected option (0 = none)
7184
8011
  */
7185
8012
  const selectedOption = (select) => {
7186
8013
  /** @type {HtmlElement[]} */
7187
8014
  const options = [];
7188
8015
  /** @param {HtmlElement} el element */
7189
8016
  const collect = (el) => {
7190
- for (const c of el.children) {
7191
- if (c.type !== NodeType.Element || c.namespace !== NS_HTML) continue;
7192
- if (c.tagName === "option") options.push(c);
7193
- else if (c.tagName === "optgroup") collect(c);
8017
+ for (let c = _nodeFirstChildren[el]; c !== 0; c = _nodeNextSiblings[c]) {
8018
+ if (_nodeTypes[c] !== NodeType.Element || _namespaceOf(c) !== NS_HTML) {
8019
+ continue;
8020
+ }
8021
+ if (_nodeStrings[c] === "option") options.push(c);
8022
+ else if (_nodeStrings[c] === "optgroup") collect(c);
7194
8023
  }
7195
8024
  };
7196
8025
  collect(select);
7197
- if (options.length === 0) return null;
8026
+ if (options.length === 0) return 0;
7198
8027
  for (let i = options.length - 1; i >= 0; i--) {
7199
- if (options[i].attributes.some((a) => a.name === "selected")) {
7200
- return options[i];
8028
+ const el = options[i];
8029
+ if (_findAttr(_nodeAttrStarts[el], _nodeAttrCounts[el], "selected") !== 0) {
8030
+ return el;
7201
8031
  }
7202
8032
  }
7203
8033
  return options[0];
@@ -7206,25 +8036,39 @@ const selectedOption = (select) => {
7206
8036
  /**
7207
8037
  * Fill each `<selectedcontent>` with a clone of its `<select>`'s selected
7208
8038
  * option subtree (the customizable-select mirroring behavior).
7209
- * @param {HtmlElement | HtmlDocument | HtmlDocumentFragment} node node
7210
- * @param {HtmlElement | null} select nearest ancestor select
8039
+ * @param {HtmlNodeRef} node node
8040
+ * @param {HtmlElement} select nearest ancestor select (0 = none)
7211
8041
  */
7212
8042
  const mirrorSelectedContent = (node, select) => {
7213
- const children =
7214
- node.type === NodeType.Element && node.templateContent
7215
- ? node.templateContent.children
7216
- : node.children;
7217
- for (const child of children) {
7218
- if (child.type !== NodeType.Element) continue;
7219
- if (child.namespace === NS_HTML && child.tagName === "select") {
8043
+ // A `<template>`'s children live in its content fragment.
8044
+ const tc = _nodeTemplateContents[node];
8045
+ const container = tc !== 0 ? tc : node;
8046
+ for (
8047
+ let child = _nodeFirstChildren[container];
8048
+ child !== 0;
8049
+ child = _nodeNextSiblings[child]
8050
+ ) {
8051
+ if (_nodeTypes[child] !== NodeType.Element) continue;
8052
+ if (_namespaceOf(child) === NS_HTML && _nodeStrings[child] === "select") {
7220
8053
  mirrorSelectedContent(child, child);
7221
8054
  } else if (
7222
- select &&
7223
- child.namespace === NS_HTML &&
7224
- child.tagName === "selectedcontent"
8055
+ select !== 0 &&
8056
+ _namespaceOf(child) === NS_HTML &&
8057
+ _nodeStrings[child] === "selectedcontent"
7225
8058
  ) {
7226
8059
  const option = selectedOption(select);
7227
- if (option) child.children = option.children.map(cloneSubtree);
8060
+ if (option !== 0) {
8061
+ // Replace the children with clones of the option's subtree.
8062
+ _nodeFirstChildren[child] = 0;
8063
+ _nodeLastChildren[child] = 0;
8064
+ for (
8065
+ let k = _nodeFirstChildren[option];
8066
+ k !== 0;
8067
+ k = _nodeNextSiblings[k]
8068
+ ) {
8069
+ _appendChild(child, cloneSubtree(k));
8070
+ }
8071
+ }
7228
8072
  } else {
7229
8073
  mirrorSelectedContent(child, select);
7230
8074
  }
@@ -7257,37 +8101,20 @@ const parseDoctype = (/** @type {string} */ raw) => {
7257
8101
  return { name, publicId, systemId };
7258
8102
  };
7259
8103
 
7260
- /**
7261
- * AST node `type` discriminators. Numeric for the same reason as the CSS
7262
- * `NodeType`: compact integer `===` dispatch on the tree-construction and
7263
- * visitor-walk hot paths. Members are literal-typed so `node.type ===
7264
- * NodeType.X` still narrows the `HtmlNode` union.
7265
- * @type {{ Document: 1, DocumentFragment: 2, Element: 3, Text: 4, Comment: 5, Doctype: 6 }}
7266
- */
7267
- const NodeType = {
7268
- Document: 1,
7269
- DocumentFragment: 2,
7270
- Element: 3,
7271
- Text: 4,
7272
- Comment: 5,
7273
- Doctype: 6
7274
- };
7275
-
7276
8104
  /** @typedef {HtmlNode | HtmlDocument | HtmlDocumentFragment} HtmlVisitableNode */
7277
8105
 
7278
8106
  // HTML-typed views over the generic visitor machinery (`util/SourceProcessor`).
7279
8107
  /**
7280
- * @typedef {import("../util/SourceProcessor").VisitorContext} VisitorContext
7281
- * @typedef {import("../util/SourceProcessor").VisitorFn<HtmlVisitableNode>} VisitorFn
7282
- * @typedef {import("../util/SourceProcessor").VisitorBucket<HtmlVisitableNode>} VisitorBucket
7283
- * @typedef {import("../util/SourceProcessor").VisitorMap<HtmlVisitableNode>} VisitorMap
7284
- * @typedef {import("../util/SourceProcessor").CompiledVisitorMap<HtmlVisitableNode>} CompiledVisitorMap
8108
+ * @typedef {import("../util/SourceProcessor").VisitorFn<HtmlPath>} VisitorFn
8109
+ * @typedef {import("../util/SourceProcessor").VisitorBucket<HtmlPath>} VisitorBucket
8110
+ * @typedef {import("../util/SourceProcessor").VisitorMap<HtmlPath>} VisitorMap
8111
+ * @typedef {import("../util/SourceProcessor").CompiledVisitorMap<HtmlPath>} CompiledVisitorMap
7285
8112
  */
7286
8113
 
7287
8114
  /**
7288
8115
  * @typedef {object} HtmlProcessOptions
7289
- * @property {string=} fragmentContext context element tag name for fragment parsing (see `buildHtmlAst`)
7290
- * @property {(HtmlDocument | HtmlDocumentFragment)=} ast walk this pre-built AST instead of parsing the input
8116
+ * @property {string=} fragmentContext context element tag name for fragment parsing (see `parseHtml`); the HTML analog of the CSS parser's `as` parse-mode option
8117
+ * @property {HtmlAstSkip=} skip node kinds to omit from the AST for speed/memory (see `HtmlAstSkip`)
7291
8118
  */
7292
8119
 
7293
8120
  /**
@@ -7299,54 +8126,97 @@ const NodeType = {
7299
8126
  * @param {HtmlProcessOptions} options process options
7300
8127
  */
7301
8128
  const grammar = (input, visitors, options) => {
7302
- // Babel's `path.skip()`, children-only. Reset per `enter` dispatch.
7303
- let skipFlag = false;
7304
- const visitorCtx = {
7305
- skipChildren() {
7306
- skipFlag = true;
7307
- }
8129
+ const root = parseHtml(input, 0, options);
8130
+
8131
+ // Iterative depth-first walk over the link columns: `firstChild`
8132
+ // / `nextSibling` descend, the `parent` column ascends, so arbitrarily deep
8133
+ // markup can't overflow the call stack (the old recursive walk died at
8134
+ // ~10⁵ nesting). A `<template>`'s content fragment is visited before the
8135
+ // element's children; ascending out of it continues with those children
8136
+ // (the fragment is never in a sibling chain, `_nodeNextSiblings` = 0).
8137
+ /**
8138
+ * Fire a node's `enter` visitors; true when the walk may descend.
8139
+ * @param {HtmlNodeRef} node node
8140
+ * @returns {boolean} false when a visitor called `skipChildren()`
8141
+ */
8142
+ const enter = (node) => {
8143
+ const b = visitors[_nodeTypes[node]];
8144
+ if (b === undefined || b.enter.length === 0) return true;
8145
+ _walkSkip = false;
8146
+ _currentNode = node;
8147
+ const p = _nodeParents[node];
8148
+ _currentParent = p === 0 ? null : p;
8149
+ const e = b.enter;
8150
+ for (let i = 0; i < e.length; i++) e[i](A);
8151
+ const skip = _walkSkip;
8152
+ _walkSkip = false;
8153
+ return !skip;
7308
8154
  };
7309
-
7310
8155
  /**
7311
- * Fetches the node's visitor bucket once (reused for enter + exit) and uses
7312
- * index loops — `for…of` would allocate an iterator per node on this hot path.
7313
- * @param {HtmlVisitableNode} node subtree root
7314
- * @param {HtmlVisitableNode | null} parent enclosing node
8156
+ * Fire a node's `exit` visitors.
8157
+ * @param {HtmlNodeRef} node node
7315
8158
  */
7316
- const walk = (node, parent) => {
7317
- const b = visitors[node.type];
7318
- let skip = false;
7319
- if (b !== undefined && b.enter.length !== 0) {
7320
- skipFlag = false;
7321
- const e = b.enter;
7322
- for (let i = 0; i < e.length; i++) e[i](node, parent, visitorCtx);
7323
- skip = skipFlag;
7324
- skipFlag = false;
7325
- }
7326
- if (!skip) {
7327
- if (node.type === NodeType.Element) {
7328
- // `<template>` children live in a separate content fragment, not in
7329
- // `children` — walk it first so template content is not skipped.
7330
- const tc = node.templateContent;
7331
- if (tc !== undefined) walk(tc, node);
7332
- const c = node.children;
7333
- for (let i = 0; i < c.length; i++) walk(c[i], node);
7334
- } else if (
7335
- node.type === NodeType.Document ||
7336
- node.type === NodeType.DocumentFragment
7337
- ) {
7338
- const c = node.children;
7339
- for (let i = 0; i < c.length; i++) walk(c[i], node);
7340
- }
7341
- // All other types are leaves.
8159
+ const exit = (node) => {
8160
+ const b = visitors[_nodeTypes[node]];
8161
+ if (b === undefined) return;
8162
+ _currentNode = node;
8163
+ const p = _nodeParents[node];
8164
+ _currentParent = p === 0 ? null : p;
8165
+ const x = b.exit;
8166
+ for (let i = 0; i < x.length; i++) x[i](A);
8167
+ };
8168
+ /**
8169
+ * First node to visit inside `node` (template content before children).
8170
+ * @param {HtmlNodeRef} node node
8171
+ * @returns {HtmlNodeRef} first inner node (0 = leaf)
8172
+ */
8173
+ const firstInner = (node) => {
8174
+ const ty = _nodeTypes[node];
8175
+ if (ty === NodeType.Element) {
8176
+ const tc = _nodeTemplateContents[node];
8177
+ return tc !== 0 ? tc : _nodeFirstChildren[node];
7342
8178
  }
7343
- if (b !== undefined) {
7344
- const x = b.exit;
7345
- for (let i = 0; i < x.length; i++) x[i](node, parent, visitorCtx);
8179
+ if (ty === NodeType.Document || ty === NodeType.DocumentFragment) {
8180
+ return _nodeFirstChildren[node];
7346
8181
  }
8182
+ return 0;
7347
8183
  };
7348
8184
 
7349
- walk(options.ast || buildHtmlAst(input, options.fragmentContext), null);
8185
+ let node = root;
8186
+ descend: for (;;) {
8187
+ if (enter(node)) {
8188
+ const inner = firstInner(node);
8189
+ if (inner !== 0) {
8190
+ node = inner;
8191
+ continue;
8192
+ }
8193
+ }
8194
+ // Leaf (or skipped): exit and move sideways / upwards.
8195
+ for (;;) {
8196
+ exit(node);
8197
+ if (node === root) break descend;
8198
+ const parent = _nodeParents[node];
8199
+ // Out of a template's content fragment: the element's children follow.
8200
+ if (_nodeTemplateContents[parent] === node) {
8201
+ const first = _nodeFirstChildren[parent];
8202
+ if (first !== 0) {
8203
+ node = first;
8204
+ continue descend;
8205
+ }
8206
+ } else {
8207
+ const sibling = _nodeNextSiblings[node];
8208
+ if (sibling !== 0) {
8209
+ node = sibling;
8210
+ continue descend;
8211
+ }
8212
+ }
8213
+ node = parent;
8214
+ }
8215
+ }
8216
+
8217
+ // The walk consumed the tree: release the side arrays' heap references so
8218
+ // the reused columns don't pin this parse's strings until the next parse.
8219
+ _releaseAstColumns();
7350
8220
  };
7351
8221
 
7352
8222
  /**
@@ -7354,14 +8224,16 @@ const grammar = (input, visitors, options) => {
7354
8224
  * `grammar`. Babel-style usage:
7355
8225
  *
7356
8226
  * ```
7357
- * processor.use({ [NodeType.Element]: (el) => {}, [NodeType.Comment]: { enter, exit } });
7358
- * processor.process(source);
8227
+ * new SourceProcessor({ skip }).use({ [NodeType.Element]: (path) => {}, [NodeType.Comment]: { enter, exit } }).process(source);
7359
8228
  * ```
7360
- * @extends {GenericSourceProcessor<HtmlVisitableNode, HtmlProcessOptions>}
8229
+ * @extends {GenericSourceProcessor<HtmlPath, HtmlProcessOptions>}
7361
8230
  */
7362
8231
  class SourceProcessor extends GenericSourceProcessor {
7363
- constructor() {
7364
- super(grammar);
8232
+ /**
8233
+ * @param {HtmlProcessOptions=} options default process options (`skip`, `fragmentContext`) merged under each `process` call's own options
8234
+ */
8235
+ constructor(options) {
8236
+ super(grammar, options);
7365
8237
  }
7366
8238
  }
7367
8239
 
@@ -7377,12 +8249,14 @@ const SMALL_LETTER_W = "w".charCodeAt(0);
7377
8249
  const SMALL_LETTER_X = "x".charCodeAt(0);
7378
8250
  const SMALL_LETTER_H = "h".charCodeAt(0);
7379
8251
  // (Don't use \s, to avoid matching non-breaking space)
8252
+ // Sticky so `collectCharacters` matches at an offset without slicing the
8253
+ // whole remaining input per call (which made srcset parsing quadratic).
7380
8254
  // eslint-disable-next-line no-control-regex
7381
- const LEADING_SPACES_REGEXP = /^[ \t\n\r\u000C]+/;
8255
+ const LEADING_SPACES_REGEXP = /[ \t\n\r\u000C]+/y;
7382
8256
  // eslint-disable-next-line no-control-regex
7383
- const LEADING_COMMAS_OR_SPACES_REGEXP = /^[, \t\n\r\u000C]+/;
8257
+ const LEADING_COMMAS_OR_SPACES_REGEXP = /[, \t\n\r\u000C]+/y;
7384
8258
  // eslint-disable-next-line no-control-regex
7385
- const LEADING_NOT_SPACES = /^[^ \t\n\r\u000C]+/;
8259
+ const LEADING_NOT_SPACES = /[^ \t\n\r\u000C]+/y;
7386
8260
  const TRAILING_COMMAS_REGEXP = /[,]+$/;
7387
8261
  const NON_NEGATIVE_INTEGER_REGEXP = /^\d+$/;
7388
8262
  // ( Positive or negative or unsigned integers or decimals, without or without exponents.
@@ -7405,8 +8279,8 @@ const parseSrcset = (input) => {
7405
8279
  let url;
7406
8280
  /** @type {string[]} */
7407
8281
  let descriptors;
7408
- /** @type {string} */
7409
- let currentDescriptor;
8282
+ /** @type {number} */
8283
+ let descriptorStart;
7410
8284
  /** @type {string} */
7411
8285
  let state;
7412
8286
  /** @type {number} */
@@ -7420,16 +8294,15 @@ const parseSrcset = (input) => {
7420
8294
  const candidates = [];
7421
8295
 
7422
8296
  /**
7423
- * @param {RegExp} regExp reg exp to collect characters
8297
+ * @param {RegExp} regExp sticky reg exp to collect characters
7424
8298
  * @returns {string | undefined} characters
7425
8299
  */
7426
8300
  function collectCharacters(regExp) {
7427
- /** @type {string} */
7428
- let chars;
7429
- const match = regExp.exec(input.slice(Math.max(0, position)));
8301
+ regExp.lastIndex = Math.max(0, position);
8302
+ const match = regExp.exec(input);
7430
8303
 
7431
8304
  if (match) {
7432
- [chars] = match;
8305
+ const [chars] = match;
7433
8306
  position += chars.length;
7434
8307
 
7435
8308
  return chars;
@@ -7555,12 +8428,13 @@ const parseSrcset = (input) => {
7555
8428
  /**
7556
8429
  * @returns {void}
7557
8430
  */
7558
- function tokenize() {
8431
+ function tokenizeDescriptor() {
7559
8432
  // 8.1. Descriptor tokenizer: Skip whitespace
7560
8433
  collectCharacters(LEADING_SPACES_REGEXP);
7561
8434
 
7562
8435
  // 8.2. Let current descriptor be the empty string.
7563
- currentDescriptor = "";
8436
+ // (Tracked as a start offset, `-1` = empty; sliced once per descriptor.)
8437
+ descriptorStart = -1;
7564
8438
 
7565
8439
  // 8.3. Let state be in descriptor.
7566
8440
  state = "in descriptor";
@@ -7582,9 +8456,9 @@ const parseSrcset = (input) => {
7582
8456
  // descriptors and let current descriptor be the empty string.
7583
8457
  // Set state to after descriptor.
7584
8458
  if (isSpace(charCode)) {
7585
- if (currentDescriptor) {
7586
- descriptors.push(currentDescriptor);
7587
- currentDescriptor = "";
8459
+ if (descriptorStart !== -1) {
8460
+ descriptors.push(input.slice(descriptorStart, position));
8461
+ descriptorStart = -1;
7588
8462
  state = "after descriptor";
7589
8463
  }
7590
8464
  }
@@ -7595,8 +8469,8 @@ const parseSrcset = (input) => {
7595
8469
  else if (charCode === COMMA) {
7596
8470
  position += 1;
7597
8471
 
7598
- if (currentDescriptor) {
7599
- descriptors.push(currentDescriptor);
8472
+ if (descriptorStart !== -1) {
8473
+ descriptors.push(input.slice(descriptorStart, position - 1));
7600
8474
  }
7601
8475
 
7602
8476
  parseDescriptors();
@@ -7606,15 +8480,15 @@ const parseSrcset = (input) => {
7606
8480
  // U+0028 LEFT PARENTHESIS (()
7607
8481
  // Append charCode to current descriptor. Set state to in parens.
7608
8482
  else if (charCode === LEFT_PARENTHESIS) {
7609
- currentDescriptor += input.charAt(position);
8483
+ if (descriptorStart === -1) descriptorStart = position;
7610
8484
  state = "in parens";
7611
8485
  }
7612
8486
  // EOF
7613
8487
  // If current descriptor is not empty, append current descriptor to
7614
8488
  // descriptors. Jump to the step labeled descriptor parser.
7615
8489
  else if (Number.isNaN(charCode)) {
7616
- if (currentDescriptor) {
7617
- descriptors.push(currentDescriptor);
8490
+ if (descriptorStart !== -1) {
8491
+ descriptors.push(input.slice(descriptorStart, position));
7618
8492
  }
7619
8493
 
7620
8494
  parseDescriptors();
@@ -7623,8 +8497,8 @@ const parseSrcset = (input) => {
7623
8497
 
7624
8498
  // Anything else
7625
8499
  // Append charCode to current descriptor.
7626
- } else {
7627
- currentDescriptor += input.charAt(position);
8500
+ } else if (descriptorStart === -1) {
8501
+ descriptorStart = position;
7628
8502
  }
7629
8503
  }
7630
8504
  // In parens
@@ -7632,22 +8506,18 @@ const parseSrcset = (input) => {
7632
8506
  // U+0029 RIGHT PARENTHESIS ())
7633
8507
  // Append charCode to current descriptor. Set state to in descriptor.
7634
8508
  if (charCode === RIGHT_PARENTHESIS) {
7635
- currentDescriptor += input.charAt(position);
7636
8509
  state = "in descriptor";
7637
8510
  }
7638
8511
  // EOF
7639
8512
  // Append current descriptor to descriptors. Jump to the step labeled
7640
8513
  // descriptor parser.
7641
8514
  else if (Number.isNaN(charCode)) {
7642
- descriptors.push(currentDescriptor);
8515
+ descriptors.push(input.slice(descriptorStart, position));
7643
8516
  parseDescriptors();
7644
8517
  return;
7645
8518
  }
7646
8519
  // Anything else
7647
- // Append charCode to current descriptor.
7648
- else {
7649
- currentDescriptor += input.charAt(position);
7650
- }
8520
+ // Append charCode to current descriptor. (Covered by the tracked range.)
7651
8521
  }
7652
8522
  // After descriptor
7653
8523
  else if (state === "after descriptor") {
@@ -7712,13 +8582,419 @@ const parseSrcset = (input) => {
7712
8582
  }
7713
8583
  // Otherwise, follow these sub steps:
7714
8584
  else {
7715
- tokenize();
8585
+ tokenizeDescriptor();
7716
8586
  }
7717
8587
 
7718
8588
  // 16. Return to the step labeled splitting loop.
7719
8589
  }
7720
8590
  };
7721
8591
 
8592
+ // The spec's "URL-potentially-surrounded-by-spaces" cleanup: C0 controls,
8593
+ // DEL..C1 and U+00A0 are stripped from the value after the whitespace trim.
8594
+ // eslint-disable-next-line no-control-regex
8595
+ const IGNORE_CHARS_REGEXP = /[\u0000-\u001F\u007F-\u009F\u00A0]/g;
8596
+
8597
+ /**
8598
+ * Parse a `src`-like URL attribute value: trim ASCII whitespace / U+00A0 from
8599
+ * both ends (offsets preserved for source rewriting), then strip ignorable
8600
+ * control characters. Throws when nothing remains.
8601
+ * @param {string} input attribute value
8602
+ * @returns {ParsedSource[]} parsed src
8603
+ */
8604
+ const parseSrc = (input) => {
8605
+ const len = input.length;
8606
+ if (len === 0) throw new Error("Must be non-empty");
8607
+
8608
+ let start = 0;
8609
+ let end = len;
8610
+
8611
+ while (start < end) {
8612
+ const code = input.charCodeAt(start);
8613
+ if (code > 32 && code !== 160) break;
8614
+ start++;
8615
+ }
8616
+
8617
+ if (start === end) throw new Error("Must be non-empty");
8618
+
8619
+ while (end > start) {
8620
+ const code = input.charCodeAt(end - 1);
8621
+ if (code > 32 && code !== 160) break;
8622
+ end--;
8623
+ }
8624
+
8625
+ let value = input.slice(start, end);
8626
+
8627
+ if (IGNORE_CHARS_REGEXP.test(value)) {
8628
+ value = value.replace(IGNORE_CHARS_REGEXP, "");
8629
+ if (value.length === 0) throw new Error("Must be non-empty");
8630
+ }
8631
+
8632
+ return [[value, start, end]];
8633
+ };
8634
+
8635
+ /**
8636
+ * Extracts the `icon-uri` value of an `msapplication-task` meta content
8637
+ * (`name=…;action-uri=…;icon-uri=…`) — the other parts are page URLs, not assets.
8638
+ * @param {string} input input
8639
+ * @returns {ParsedSource[]} parsed icon-uri
8640
+ */
8641
+ const parseMsapplicationTask = (input) => {
8642
+ const len = input.length;
8643
+ let pos = 0;
8644
+ while (pos < len) {
8645
+ let sep = input.indexOf(";", pos);
8646
+ if (sep === -1) sep = len;
8647
+ const eq = input.indexOf("=", pos);
8648
+ if (eq !== -1 && eq < sep) {
8649
+ const key = input.slice(pos, eq).trim().toLowerCase();
8650
+ if (key === "icon-uri") {
8651
+ let start = eq + 1;
8652
+ let end = sep;
8653
+ while (start < end && isSpace(input.charCodeAt(start))) {
8654
+ start++;
8655
+ }
8656
+ while (end > start && isSpace(input.charCodeAt(end - 1))) {
8657
+ end--;
8658
+ }
8659
+ if (start === end) return [];
8660
+ return [[input.slice(start, end), start, end]];
8661
+ }
8662
+ }
8663
+ pos = sep + 1;
8664
+ }
8665
+ return [];
8666
+ };
8667
+
8668
+ // CSS syntax is loaded on first `parseCssUrls` call, not at module load —
8669
+ // `HtmlGenerator` deliberately defers it the same way, and most documents
8670
+ // never carry a URL-bearing SVG presentation attribute.
8671
+ /** @type {typeof import("../css/syntax") | undefined} */
8672
+ let _cssSyntax;
8673
+
8674
+ /**
8675
+ * Extracts `url(...)` references from a CSS value (an SVG presentation
8676
+ * attribute). Reuses webpack's CSS lexer so quoting/escaping match the CSS
8677
+ * spec; returns the `parseSrc` shape so the shared emit path maps and rewrites
8678
+ * the spans. Unquoted `url(path)` rewrites the content span; quoted
8679
+ * `url("path")` rewrites the inner string span (quotes preserved).
8680
+ * @param {string} input attribute value (a CSS component-value list)
8681
+ * @returns {ParsedSource[]} url references
8682
+ */
8683
+ const parseCssUrls = (input) => {
8684
+ const {
8685
+ TT_EOF,
8686
+ TT_FUNCTION,
8687
+ TT_STRING,
8688
+ TT_URL,
8689
+ TT_WHITESPACE,
8690
+ TokenStream,
8691
+ equalsLowerCase
8692
+ } = _cssSyntax || (_cssSyntax = require("../css/syntax"));
8693
+ const ts = new TokenStream(input);
8694
+ /** @type {ParsedSource[]} */
8695
+ const result = [];
8696
+ for (;;) {
8697
+ const t = ts.consume();
8698
+ if (t.type === TT_EOF) break;
8699
+ if (t.type === TT_URL) {
8700
+ if (t.contentEnd > t.contentStart) {
8701
+ result.push([
8702
+ input.slice(t.contentStart, t.contentEnd),
8703
+ t.contentStart,
8704
+ t.contentEnd
8705
+ ]);
8706
+ }
8707
+ } else if (
8708
+ t.type === TT_FUNCTION &&
8709
+ equalsLowerCase(input.slice(t.start, t.end - 1), "url")
8710
+ ) {
8711
+ let s = ts.consume();
8712
+ while (s.type === TT_WHITESPACE) s = ts.consume();
8713
+ if (s.type === TT_STRING) {
8714
+ const quote = input.charCodeAt(s.start);
8715
+ const innerStart = s.start + 1;
8716
+ // Drop the closing quote, unless the string is unterminated at EOF.
8717
+ const innerEnd =
8718
+ input.charCodeAt(s.end - 1) === quote ? s.end - 1 : s.end;
8719
+ if (innerEnd > innerStart) {
8720
+ result.push([
8721
+ input.slice(innerStart, innerEnd),
8722
+ innerStart,
8723
+ innerEnd
8724
+ ]);
8725
+ }
8726
+ }
8727
+ }
8728
+ }
8729
+ return result;
8730
+ };
8731
+
8732
+ // Babel's `path.skip()`, children-only: set by `A.skipChildren()` during an
8733
+ // `enter` dispatch, consumed by the walk.
8734
+ let _walkSkip = false;
8735
+ // The walk's current position (`A.node` / `A.parent` read these; module-level
8736
+ // so the accessor methods' defaults avoid self-referential `this` typing).
8737
+ /** @type {HtmlNodeRef} */
8738
+ let _currentNode = 0;
8739
+ /** @type {HtmlNodeRef | null} */
8740
+ let _currentParent = null;
8741
+
8742
+ /* eslint-disable jsdoc/require-template -- `A` below is the accessor const, not a type parameter */
8743
+ /**
8744
+ * The HTML path (Babel's `path` shape): the AST accessor with the walk's
8745
+ * current position on it — the single argument every visitor receives.
8746
+ * @typedef {typeof A} HtmlPath
8747
+ */
8748
+ /* eslint-enable jsdoc/require-template */
8749
+
8750
+ // AST field-access seam (mirrors the CSS parser's `A`): every AST field a
8751
+ // consumer reads goes through one of these accessors, so the node
8752
+ // representation can change underneath without touching consumers. `n` is an
8753
+ // `HtmlNodeRef`; results are valid until the next `parseHtml` call.
8754
+ const A = {
8755
+ // === path position (rebound by the walk before every visitor call) ===
8756
+ /**
8757
+ * @returns {HtmlNodeRef} current node — only valid during a visitor callback
8758
+ */
8759
+ get node() {
8760
+ return _currentNode;
8761
+ },
8762
+ /**
8763
+ * @returns {HtmlNodeRef | null} enclosing node (null = the document root)
8764
+ */
8765
+ get parent() {
8766
+ return _currentParent;
8767
+ },
8768
+ /** Stop the walk descending into the current node (enter only). */
8769
+ skipChildren() {
8770
+ _walkSkip = true;
8771
+ },
8772
+ // === field reads — `n` defaults to the current node ===
8773
+ /**
8774
+ * @param {HtmlNodeRef=} n node
8775
+ * @returns {number} `NodeType`
8776
+ */
8777
+ type(n = _currentNode) {
8778
+ return _nodeTypes[n];
8779
+ },
8780
+ /**
8781
+ * @param {HtmlNodeRef=} n node
8782
+ * @returns {number} start offset
8783
+ */
8784
+ start(n = _currentNode) {
8785
+ return _nodeStarts[n];
8786
+ },
8787
+ /**
8788
+ * @param {HtmlNodeRef=} n node
8789
+ * @returns {number} end offset
8790
+ */
8791
+ end(n = _currentNode) {
8792
+ return _nodeEnds[n];
8793
+ },
8794
+ /**
8795
+ * @param {HtmlElement=} n element
8796
+ * @returns {string} lowercased (foreign-content: adjusted) tag name
8797
+ */
8798
+ tagName(n = _currentNode) {
8799
+ return _nodeStrings[n];
8800
+ },
8801
+ /**
8802
+ * @param {HtmlElement=} n element
8803
+ * @returns {number} `NS_*` namespace
8804
+ */
8805
+ namespace(n = _currentNode) {
8806
+ return _nodeFlags[n] & NS_MASK;
8807
+ },
8808
+ /**
8809
+ * @param {HtmlElement=} n element
8810
+ * @returns {boolean} true for void elements
8811
+ */
8812
+ selfClosing(n = _currentNode) {
8813
+ return (_nodeFlags[n] & FLAG_SELF_CLOSING) !== 0;
8814
+ },
8815
+ // materialized attribute list — test/tooling convenience, allocates; the
8816
+ // parser reads attributes through the scalar accessors below
8817
+ /**
8818
+ * @param {HtmlElement=} n element
8819
+ * @returns {HtmlAttribute[]} materialized attributes
8820
+ */
8821
+ attributes(n = _currentNode) {
8822
+ const out = [];
8823
+ const start = _nodeAttrStarts[n];
8824
+ for (let i = start; i < start + _nodeAttrCounts[n]; i++) {
8825
+ out.push({
8826
+ name: _attrNames[i],
8827
+ value: _attrValueOf(i),
8828
+ serializedName: _attrSerializedName(i),
8829
+ nameStart: _attrNameStarts[i],
8830
+ nameEnd: _attrNameEnds[i],
8831
+ valueStart: _attrValueStarts[i],
8832
+ valueEnd: _attrValueEnds[i]
8833
+ });
8834
+ }
8835
+ return out;
8836
+ },
8837
+ /**
8838
+ * @param {HtmlElement=} n element
8839
+ * @returns {number} attribute count
8840
+ */
8841
+ attributeCount(n = _currentNode) {
8842
+ return _nodeAttrCounts[n];
8843
+ },
8844
+ /**
8845
+ * The i-th attribute of an element, as an id for the `attribute*` reads.
8846
+ * @param {number} i attribute index
8847
+ * @param {HtmlElement=} n element
8848
+ * @returns {HtmlAttributeRef} attribute ref
8849
+ */
8850
+ attributeAt(i, n = _currentNode) {
8851
+ return _nodeAttrStarts[n] + i;
8852
+ },
8853
+ /**
8854
+ * Linear lookup by (lowercased) name.
8855
+ * @param {string} name attribute name
8856
+ * @param {HtmlElement=} n element
8857
+ * @returns {HtmlAttributeRef} attribute ref (0 = not present)
8858
+ */
8859
+ findAttribute(name, n = _currentNode) {
8860
+ return _findAttr(_nodeAttrStarts[n], _nodeAttrCounts[n], name);
8861
+ },
8862
+ /**
8863
+ * @param {HtmlAttributeRef} a attribute ref
8864
+ * @returns {string} lowercased (foreign-content: adjusted) attribute name
8865
+ */
8866
+ attributeName(a) {
8867
+ return _attrNames[a];
8868
+ },
8869
+ /**
8870
+ * @param {HtmlAttributeRef} a attribute ref
8871
+ * @returns {string} raw (undecoded) attribute value ("" when valueless)
8872
+ */
8873
+ attributeValue(a) {
8874
+ return _attrValueOf(a);
8875
+ },
8876
+ /**
8877
+ * @param {HtmlAttributeRef} a attribute ref
8878
+ * @returns {number} name start offset
8879
+ */
8880
+ attributeNameStart(a) {
8881
+ return _attrNameStarts[a];
8882
+ },
8883
+ /**
8884
+ * @param {HtmlAttributeRef} a attribute ref
8885
+ * @returns {number} name end offset
8886
+ */
8887
+ attributeNameEnd(a) {
8888
+ return _attrNameEnds[a];
8889
+ },
8890
+ /**
8891
+ * @param {HtmlAttributeRef} a attribute ref
8892
+ * @returns {number} value start offset (-1 when valueless or on adoption-agency clones)
8893
+ */
8894
+ attributeValueStart(a) {
8895
+ return _attrValueStarts[a];
8896
+ },
8897
+ /**
8898
+ * @param {HtmlAttributeRef} a attribute ref
8899
+ * @returns {number} value end offset
8900
+ */
8901
+ attributeValueEnd(a) {
8902
+ return _attrValueEnds[a];
8903
+ },
8904
+ /**
8905
+ * @param {HtmlElement=} n element
8906
+ * @returns {number} end offset of the opening tag (after `>`)
8907
+ */
8908
+ tagEnd(n = _currentNode) {
8909
+ return _nodeTagEnds[n];
8910
+ },
8911
+ /**
8912
+ * @param {HtmlElement=} n element
8913
+ * @returns {number} end offset of the tag name
8914
+ */
8915
+ nameEnd(n = _currentNode) {
8916
+ return _nodeNameEnds[n];
8917
+ },
8918
+ /**
8919
+ * @param {HtmlElement=} n element
8920
+ * @returns {number} under `skip.text`, end offset of a raw-text element's body (`tagEnd` when empty)
8921
+ */
8922
+ contentEnd(n = _currentNode) {
8923
+ return _nodeContentEnds[n];
8924
+ },
8925
+ /**
8926
+ * @param {HtmlElement=} n element
8927
+ * @returns {HtmlDocumentFragment} `<template>` content fragment (0 = none)
8928
+ */
8929
+ templateContent(n = _currentNode) {
8930
+ return _nodeTemplateContents[n];
8931
+ },
8932
+ /**
8933
+ * @param {HtmlText | HtmlComment=} n text / comment node
8934
+ * @returns {string} decoded text / comment data
8935
+ */
8936
+ data(n = _currentNode) {
8937
+ return _nodeStrings[n];
8938
+ },
8939
+ /**
8940
+ * @param {HtmlDoctype=} n doctype node
8941
+ * @returns {string} doctype name
8942
+ */
8943
+ doctypeName(n = _currentNode) {
8944
+ return _nodeStrings[n];
8945
+ },
8946
+ // The doctype ids are per-parse scalars (a document has at most one
8947
+ // doctype node); the node parameter is accepted for call-shape uniformity.
8948
+ /**
8949
+ * @param {HtmlDoctype=} _n doctype node
8950
+ * @returns {string | null} doctype public id
8951
+ */
8952
+ doctypePublicId(_n) {
8953
+ return _doctypePublicId;
8954
+ },
8955
+ /**
8956
+ * @param {HtmlDoctype=} _n doctype node
8957
+ * @returns {string | null} doctype system id
8958
+ */
8959
+ doctypeSystemId(_n) {
8960
+ return _doctypeSystemId;
8961
+ },
8962
+ // === tree links (0 = none) ===
8963
+ /**
8964
+ * @param {HtmlNodeRef=} n node
8965
+ * @returns {HtmlNodeRef} first child
8966
+ */
8967
+ firstChild(n = _currentNode) {
8968
+ return _nodeFirstChildren[n];
8969
+ },
8970
+ /**
8971
+ * @param {HtmlNodeRef=} n node
8972
+ * @returns {HtmlNodeRef} next sibling
8973
+ */
8974
+ nextSibling(n = _currentNode) {
8975
+ return _nodeNextSiblings[n];
8976
+ },
8977
+ /**
8978
+ * @param {HtmlNodeRef=} n node
8979
+ * @returns {HtmlNodeRef} parent node (a `<template>`'s content links to its fragment)
8980
+ */
8981
+ parentOf(n = _currentNode) {
8982
+ return _nodeParents[n];
8983
+ },
8984
+ /**
8985
+ * @param {HtmlNodeRef=} n node
8986
+ * @returns {HtmlNodeRef[]} materialized child list — test/tooling convenience, allocates
8987
+ */
8988
+ children(n = _currentNode) {
8989
+ const out = [];
8990
+ for (let c = _nodeFirstChildren[n]; c !== 0; c = _nodeNextSiblings[c]) {
8991
+ out.push(c);
8992
+ }
8993
+ return out;
8994
+ }
8995
+ };
8996
+
8997
+ module.exports.A = A;
7722
8998
  module.exports.NS_HTML = NS_HTML;
7723
8999
  module.exports.NS_MATHML = NS_MATHML;
7724
9000
  module.exports.NS_SVG = NS_SVG;
@@ -7726,13 +9002,19 @@ module.exports.NodeType = NodeType;
7726
9002
  module.exports.QUOTE_DOUBLE = QUOTE_DOUBLE;
7727
9003
  module.exports.QUOTE_NONE = QUOTE_NONE;
7728
9004
  module.exports.QUOTE_SINGLE = QUOTE_SINGLE;
7729
- module.exports.SVG_TAG_ADJUST = SVG_TAG_ADJUST;
7730
- module.exports.SourceProcessor = SourceProcessor;
7731
9005
  // Exposed so HtmlParser can map user-configured (lowercased) tag names to
7732
9006
  // the adjusted camelCase names the AST carries for foreign content.
7733
- module.exports.buildHtmlAst = buildHtmlAst;
7734
- module.exports.decodeHtmlEntities = decodeHtmlEntities;
7735
- module.exports.decodeHtmlEntitiesWithMap = decodeHtmlEntitiesWithMap;
7736
- module.exports.findAttr = findAttr;
9007
+ module.exports.SVG_TAG_ADJUST = SVG_TAG_ADJUST;
9008
+ module.exports.SourceProcessor = SourceProcessor;
9009
+ module.exports.decodeEntities = decodeEntities;
9010
+ module.exports.escapeAttribute = escapeAttribute;
9011
+ module.exports.escapeText = escapeText;
9012
+ // WHATWG "ASCII whitespace" (tab / LF / FF / CR / space) — the tokenizer's
9013
+ // whitespace class, exported under the spec's name.
9014
+ module.exports.isAsciiWhitespace = isSpace;
9015
+ module.exports.parseCssUrls = parseCssUrls;
9016
+ module.exports.parseHtml = parseHtml;
9017
+ module.exports.parseMsapplicationTask = parseMsapplicationTask;
9018
+ module.exports.parseSrc = parseSrc;
7737
9019
  module.exports.parseSrcset = parseSrcset;
7738
- module.exports.walkHtmlTokens = walkHtmlTokens;
9020
+ module.exports.tokenize = tokenize;