webpack 5.108.2 → 5.108.4

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.
@@ -383,9 +383,9 @@ const walkHtmlTokens = (input, pos = 0, callbacks = {}) => {
383
383
  let tagStart = pos;
384
384
  let tagNameStart = -1;
385
385
  let tagNameEnd = -1;
386
- let attrNameStart = -1;
387
- let attrNameEnd = -1;
388
- let attrValueStart = -1;
386
+ let attributeNameStart = -1;
387
+ let attributeNameEnd = -1;
388
+ let attributeValueStart = -1;
389
389
  let attrQuoteType = QUOTE_NONE;
390
390
  let commentStart = pos;
391
391
  let lastOpenTagName = "";
@@ -526,19 +526,19 @@ const walkHtmlTokens = (input, pos = 0, callbacks = {}) => {
526
526
  // the callback is expected to do the same advance based on the
527
527
  // reported `quoteType`.
528
528
  let nextPos = attrQuoteType === QUOTE_NONE ? endPos : endPos + 1;
529
- if (callbacks.attribute !== undefined && attrNameStart !== -1) {
529
+ if (callbacks.attribute !== undefined && attributeNameStart !== -1) {
530
530
  nextPos = callbacks.attribute(
531
531
  input,
532
- attrNameStart,
533
- attrNameEnd,
534
- attrValueStart,
535
- attrValueStart === -1 ? -1 : endPos,
532
+ attributeNameStart,
533
+ attributeNameEnd,
534
+ attributeValueStart,
535
+ attributeValueStart === -1 ? -1 : endPos,
536
536
  attrQuoteType
537
537
  );
538
538
  }
539
- if (attrNameStart !== -1) tagHasAttributes = true;
540
- attrNameStart = -1;
541
- attrValueStart = -1;
539
+ if (attributeNameStart !== -1) tagHasAttributes = true;
540
+ attributeNameStart = -1;
541
+ attributeValueStart = -1;
542
542
  attrQuoteType = QUOTE_NONE;
543
543
  return nextPos;
544
544
  };
@@ -806,14 +806,14 @@ const walkHtmlTokens = (input, pos = 0, callbacks = {}) => {
806
806
  pos + 1,
807
807
  "warning"
808
808
  );
809
- attrNameStart = pos;
809
+ attributeNameStart = pos;
810
810
  state = STATE_ATTRIBUTE_NAME;
811
811
  pos++;
812
812
  } else {
813
813
  // Anything else
814
814
  // Start a new attribute in the current tag token. Set that attribute name
815
815
  // and value to the empty string. Reconsume in the attribute name state.
816
- attrNameStart = pos;
816
+ attributeNameStart = pos;
817
817
  state = STATE_ATTRIBUTE_NAME;
818
818
  // Reconsume
819
819
  }
@@ -831,11 +831,11 @@ const walkHtmlTokens = (input, pos = 0, callbacks = {}) => {
831
831
  // EOF
832
832
  // Reconsume in the after attribute name state.
833
833
  if (isSpace(cc) || cc === CC_SOLIDUS || cc === CC_GREATER_THAN) {
834
- attrNameEnd = pos;
834
+ attributeNameEnd = pos;
835
835
  state = STATE_AFTER_ATTRIBUTE_NAME;
836
836
  // Reconsume
837
837
  } else if (cc === CC_EQUALS) {
838
- attrNameEnd = pos;
838
+ attributeNameEnd = pos;
839
839
  state = STATE_BEFORE_ATTRIBUTE_VALUE;
840
840
  pos++;
841
841
  } else {
@@ -915,7 +915,7 @@ const walkHtmlTokens = (input, pos = 0, callbacks = {}) => {
915
915
  // Anything else
916
916
  // Start a new attribute in the current tag token.
917
917
  emitAttribute(pos);
918
- attrNameStart = pos;
918
+ attributeNameStart = pos;
919
919
  state = STATE_ATTRIBUTE_NAME;
920
920
  // Reconsume
921
921
  }
@@ -934,14 +934,14 @@ const walkHtmlTokens = (input, pos = 0, callbacks = {}) => {
934
934
  } else if (cc === CC_QUOTATION_MARK) {
935
935
  // U+0022 QUOTATION MARK (")
936
936
  // Switch to the attribute value (double-quoted) state.
937
- attrValueStart = pos + 1;
937
+ attributeValueStart = pos + 1;
938
938
  attrQuoteType = QUOTE_DOUBLE;
939
939
  state = STATE_ATTRIBUTE_VALUE_DOUBLE_QUOTED;
940
940
  pos++;
941
941
  } else if (cc === CC_APOSTROPHE) {
942
942
  // U+0027 APOSTROPHE (')
943
943
  // Switch to the attribute value (single-quoted) state.
944
- attrValueStart = pos + 1;
944
+ attributeValueStart = pos + 1;
945
945
  attrQuoteType = QUOTE_SINGLE;
946
946
  state = STATE_ATTRIBUTE_VALUE_SINGLE_QUOTED;
947
947
  pos++;
@@ -952,7 +952,7 @@ const walkHtmlTokens = (input, pos = 0, callbacks = {}) => {
952
952
  // an empty value range pointing at the `>` so the open-tag offset range
953
953
  // still includes the `>`.
954
954
  reportError("missing-attribute-value", pos, pos + 1, "warning");
955
- attrValueStart = pos;
955
+ attributeValueStart = pos;
956
956
  attrQuoteType = QUOTE_NONE;
957
957
  pos = emitAttribute(pos);
958
958
  if (input.charCodeAt(tagStart + 1) === CC_SOLIDUS) {
@@ -966,7 +966,7 @@ const walkHtmlTokens = (input, pos = 0, callbacks = {}) => {
966
966
  } else {
967
967
  // Anything else
968
968
  // Reconsume in the attribute value (unquoted) state.
969
- attrValueStart = pos;
969
+ attributeValueStart = pos;
970
970
  attrQuoteType = QUOTE_NONE;
971
971
  state = STATE_ATTRIBUTE_VALUE_UNQUOTED;
972
972
  // Reconsume
@@ -1301,7 +1301,20 @@ const walkHtmlTokens = (input, pos = 0, callbacks = {}) => {
1301
1301
  if (cc === CC_NULL) {
1302
1302
  reportError("unexpected-null-character", pos, pos + 1, "warning");
1303
1303
  }
1304
+ // Fast-forward ordinary comment text without re-entering the
1305
+ // state switch; stop on the significant code points above.
1304
1306
  pos++;
1307
+ while (pos < len) {
1308
+ const c2 = input.charCodeAt(pos);
1309
+ if (
1310
+ c2 === CC_LESS_THAN ||
1311
+ c2 === CC_HYPHEN_MINUS ||
1312
+ c2 === CC_NULL
1313
+ ) {
1314
+ break;
1315
+ }
1316
+ pos++;
1317
+ }
1305
1318
  }
1306
1319
  break;
1307
1320
 
@@ -3175,25 +3188,25 @@ const walkHtmlTokens = (input, pos = 0, callbacks = {}) => {
3175
3188
  pos + runLen < len && input.charCodeAt(pos + runLen) === CC_SEMICOLON;
3176
3189
  namedEntityConsumed = 0;
3177
3190
  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;
3191
+ // Try the full run with its trailing `;` first the overwhelmingly
3192
+ // common case (`&amp;`, `&nbsp;`, …) then needs exactly one slice.
3193
+ if (hasSemicolon && runLen > 0) {
3194
+ const withSemicolon = input.slice(pos, pos + runLen + 1);
3195
+ if (HTML_ENTITIES[withSemicolon] !== undefined) {
3196
+ namedEntityConsumed = runLen + 1;
3191
3197
  matchedWithSemicolon = true;
3192
- break;
3193
3198
  }
3194
- if (HTML_ENTITIES[bare] !== undefined) {
3195
- namedEntityConsumed = n;
3196
- break;
3199
+ }
3200
+ if (namedEntityConsumed === 0) {
3201
+ // Slice the candidate run once; prefixes come from this short
3202
+ // string instead of re-slicing the input per length.
3203
+ const run = input.slice(pos, pos + runLen);
3204
+ for (let n = runLen; n > 0; n--) {
3205
+ const bare = n === runLen ? run : run.slice(0, n);
3206
+ if (HTML_ENTITIES[bare] !== undefined) {
3207
+ namedEntityConsumed = n;
3208
+ break;
3209
+ }
3197
3210
  }
3198
3211
  }
3199
3212
  if (namedEntityConsumed > 0) {
@@ -3208,12 +3221,10 @@ const walkHtmlTokens = (input, pos = 0, callbacks = {}) => {
3208
3221
  returnState === STATE_ATTRIBUTE_VALUE_DOUBLE_QUOTED ||
3209
3222
  returnState === STATE_ATTRIBUTE_VALUE_SINGLE_QUOTED ||
3210
3223
  returnState === STATE_ATTRIBUTE_VALUE_UNQUOTED;
3211
- if (
3212
- !(
3213
- inAttribute &&
3214
- (next === CC_EQUALS || isAsciiAlphanumeric(next))
3215
- )
3216
- ) {
3224
+ if (!(
3225
+ inAttribute &&
3226
+ (next === CC_EQUALS || isAsciiAlphanumeric(next))
3227
+ )) {
3217
3228
  reportError(
3218
3229
  "missing-semicolon-after-character-reference",
3219
3230
  pos + namedEntityConsumed,
@@ -3470,11 +3481,11 @@ const walkHtmlTokens = (input, pos = 0, callbacks = {}) => {
3470
3481
  // dropping the in-progress tag, we emit its offset range up to EOF.
3471
3482
  reportError("eof-in-tag", len, len, "error");
3472
3483
  // 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;
3484
+ // attributeNameEnd here so the emitted attribute range is valid.
3485
+ if (state === STATE_ATTRIBUTE_NAME && attributeNameStart !== -1) {
3486
+ attributeNameEnd = len;
3476
3487
  }
3477
- if (attrNameStart !== -1) emitAttribute(len);
3488
+ if (attributeNameStart !== -1) emitAttribute(len);
3478
3489
  // If we hit EOF before the tag-name end was recorded, the name runs
3479
3490
  // to EOF. `tagNameEnd` may carry over from a previously emitted tag,
3480
3491
  // so reset it whenever it's missing or stale (less than `tagNameStart`)
@@ -3697,6 +3708,15 @@ const decodeOneReference = (match, nextCharCode, isAttribute) => {
3697
3708
  return match;
3698
3709
  };
3699
3710
 
3711
+ // Hoisted `replace` callbacks (one per `isAttribute` mode) — no closure
3712
+ // per decode call, and the callback stays monomorphic.
3713
+ /** @type {(match: string, offset: number, source: string) => string} */
3714
+ const _decodeReferenceInText = (match, offset, source) =>
3715
+ decodeOneReference(match, source.charCodeAt(offset + match.length), false);
3716
+ /** @type {(match: string, offset: number, source: string) => string} */
3717
+ const _decodeReferenceInAttribute = (match, offset, source) =>
3718
+ decodeOneReference(match, source.charCodeAt(offset + match.length), true);
3719
+
3700
3720
  /**
3701
3721
  * Decode HTML character references in a string. Handles all numeric
3702
3722
  * references (with WHATWG remap of 0x00, surrogates, out-of-range, and the
@@ -3715,12 +3735,9 @@ const decodeOneReference = (match, nextCharCode, isAttribute) => {
3715
3735
  const decodeHtmlEntities = (str, isAttribute) => {
3716
3736
  if (!str.includes("&")) return str;
3717
3737
 
3718
- return str.replace(CHARACTER_REFERENCE_REGEXP, (match, offset, source) =>
3719
- decodeOneReference(
3720
- match,
3721
- source.charCodeAt(offset + match.length),
3722
- isAttribute
3723
- )
3738
+ return str.replace(
3739
+ CHARACTER_REFERENCE_REGEXP,
3740
+ isAttribute ? _decodeReferenceInAttribute : _decodeReferenceInText
3724
3741
  );
3725
3742
  };
3726
3743
 
@@ -3779,6 +3796,354 @@ const NS_MATHML = 1;
3779
3796
  const NS_SVG = 2;
3780
3797
 
3781
3798
  /**
3799
+ * AST node `type` discriminators. Numeric for the same reason as the CSS
3800
+ * `NodeType`: compact integer `===` dispatch on the tree-construction and
3801
+ * visitor-walk hot paths.
3802
+ * @type {{ Document: 1, DocumentFragment: 2, Element: 3, Text: 4, Comment: 5, Doctype: 6 }}
3803
+ */
3804
+ const NodeType = {
3805
+ Document: 1,
3806
+ DocumentFragment: 2,
3807
+ Element: 3,
3808
+ Text: 4,
3809
+ Comment: 5,
3810
+ Doctype: 6
3811
+ };
3812
+
3813
+ /**
3814
+ * A contiguous run of attribute ids in the attribute columns — how a start-tag
3815
+ * token and an element refer to their attributes. `start` is the first id
3816
+ * (`count` 0 = none).
3817
+ * @typedef {{ start: number, count: number }} AttributeRun
3818
+ */
3819
+
3820
+ // Shared frozen empty run for attributeless elements and synthesized tags.
3821
+ const EMPTY_ATTRS = /** @type {AttributeRun} */ (
3822
+ Object.freeze({ start: 0, count: 0 })
3823
+ );
3824
+
3825
+ // === Struct-of-arrays AST backend ===
3826
+ // One AST node = one integer id (`HtmlNodeRef`) indexing the parallel columns
3827
+ // below — no per-node object and no per-parent children array. Tree shape
3828
+ // lives in the four link columns (parent / firstChild / lastChild /
3829
+ // nextSibling); the only heap references are the string payload / attribute
3830
+ // name-and-value side arrays. Columns are module-level and reused
3831
+ // across parses (grown, never shrunk — the CSS parser's `_soa*` strategy), so
3832
+ // a steady-state parse allocates almost nothing per node; consumers must fully
3833
+ // read a tree before the next `buildHtmlAst` call. Id 0 is reserved as
3834
+ // "no node" so the link columns can use 0 as null.
3835
+ let _hCap = 0;
3836
+ let _hN = 0;
3837
+ /** `NodeType` per node */
3838
+ let _hTy = new Uint8Array(0);
3839
+ /** bits 0-1 namespace (`NS_*`), bit 2 self-closing (void element) */
3840
+ let _hFl = new Uint8Array(0);
3841
+ let _hSt = new Int32Array(0);
3842
+ let _hEn = new Int32Array(0);
3843
+ /** end offset of an element's opening tag (after `>`) */
3844
+ let _hTagEnd = new Int32Array(0);
3845
+ /** end offset of an element's tag name */
3846
+ let _hNameEnd = new Int32Array(0);
3847
+ /** under `skip.text`, end offset of a raw-text element's body (`HtmlAstSkip`) */
3848
+ let _hCEnd = new Int32Array(0);
3849
+ /** a `<template>`'s content DocumentFragment (0 = none) */
3850
+ let _hTc = new Int32Array(0);
3851
+ let _hParent = new Int32Array(0);
3852
+ let _hFirst = new Int32Array(0);
3853
+ let _hLast = new Int32Array(0);
3854
+ let _hNext = new Int32Array(0);
3855
+ /** @type {string[]} tag name / text data / comment data / doctype name */
3856
+ const _hStr = [];
3857
+ /** first attribute id of an element's contiguous run */
3858
+ let _hAStart = new Int32Array(0);
3859
+ /** attribute count of an element's run */
3860
+ let _hACount = new Int32Array(0);
3861
+ // The single doctype node's public/system ids (a document inserts at most one
3862
+ // doctype node — later doctype tokens are ignored — so no column is needed).
3863
+ /** @type {string | null} */
3864
+ let _hDocPub = null;
3865
+ /** @type {string | null} */
3866
+ let _hDocSys = null;
3867
+
3868
+ const NS_MASK = 3;
3869
+ const FLAG_SELF_CLOSING = 4;
3870
+
3871
+ /** @type {(el: HtmlNodeRef) => string} */
3872
+ const _tag = (el) => _hStr[el];
3873
+ /** @type {(el: HtmlNodeRef) => number} */
3874
+ const _ns = (el) => _hFl[el] & NS_MASK;
3875
+
3876
+ // === Attribute columns ===
3877
+ // One attribute = one integer id into these columns; an element (and a
3878
+ // start-tag token) holds a contiguous run. The value string is derived from
3879
+ // the source by offset on read — `_aVal` carries an override only for
3880
+ // valueless attributes (`""`) and offset-less adoption-agency clones — and the
3881
+ // html5lib serializer name is derived from the adjusted name plus one flag
3882
+ // bit, so per attribute only the interned name pointer is retained.
3883
+ let _aCap = 0;
3884
+ let _aN = 0;
3885
+ let _aNameStart = new Int32Array(0);
3886
+ let _aNameEnd = new Int32Array(0);
3887
+ let _aValStart = new Int32Array(0);
3888
+ let _aValEnd = new Int32Array(0);
3889
+ /** bit 0: name has a `FOREIGN_ATTR_NS` serializer name (set on foreign adjust) */
3890
+ let _aFl = new Uint8Array(0);
3891
+ /** @type {string[]} lowercased (foreign-content: adjusted) attribute name */
3892
+ const _aName = [];
3893
+ /** @type {(string | null)[]} value override (null = slice the source by offset) */
3894
+ const _aVal = [];
3895
+ /** source of the current parse, for by-offset attribute values */
3896
+ let _hSrc = "";
3897
+
3898
+ /** @param {number} need minimum capacity */
3899
+ const _aGrow = (need) => {
3900
+ let cap = _aCap || 4096;
3901
+ while (cap < need) cap *= 2;
3902
+ const nameStart = new Int32Array(cap);
3903
+ nameStart.set(_aNameStart);
3904
+ _aNameStart = nameStart;
3905
+ const nameEnd = new Int32Array(cap);
3906
+ nameEnd.set(_aNameEnd);
3907
+ _aNameEnd = nameEnd;
3908
+ const valStart = new Int32Array(cap);
3909
+ valStart.set(_aValStart);
3910
+ _aValStart = valStart;
3911
+ const valEnd = new Int32Array(cap);
3912
+ valEnd.set(_aValEnd);
3913
+ _aValEnd = valEnd;
3914
+ const fl = new Uint8Array(cap);
3915
+ fl.set(_aFl);
3916
+ _aFl = fl;
3917
+ _aCap = cap;
3918
+ };
3919
+
3920
+ /** @type {(name: string, value: string | null, nameStart: number, nameEnd: number, valueStart: number, valueEnd: number) => number} */
3921
+ const _aAlloc = (name, value, nameStart, nameEnd, valueStart, valueEnd) => {
3922
+ const i = ++_aN;
3923
+ if (i >= _aCap) _aGrow(i + 1);
3924
+ _aNameStart[i] = nameStart;
3925
+ _aNameEnd[i] = nameEnd;
3926
+ _aValStart[i] = valueStart;
3927
+ _aValEnd[i] = valueEnd;
3928
+ _aFl[i] = 0;
3929
+ // Ids are sequential, so these indexed writes append (arrays stay packed).
3930
+ _aName[i] = name;
3931
+ _aVal[i] = value;
3932
+ return i;
3933
+ };
3934
+
3935
+ /** @type {(i: number) => string} */
3936
+ const _aValueOf = (i) => {
3937
+ const v = _aVal[i];
3938
+ return v !== null ? v : _hSrc.slice(_aValStart[i], _aValEnd[i]);
3939
+ };
3940
+
3941
+ // Linear name lookup in a run — attribute lists are short, a loop beats a Map.
3942
+ /** @type {(start: number, count: number, name: string) => number} */
3943
+ const _aFind = (start, count, name) => {
3944
+ for (let i = start; i < start + count; i++) {
3945
+ if (_aName[i] === name) return i;
3946
+ }
3947
+ return 0;
3948
+ };
3949
+
3950
+ /** @type {(i: number) => number} exact copy of an attribute into a new id */
3951
+ const _aCopy = (i) => {
3952
+ const c = _aAlloc(
3953
+ _aName[i],
3954
+ _aVal[i],
3955
+ _aNameStart[i],
3956
+ _aNameEnd[i],
3957
+ _aValStart[i],
3958
+ _aValEnd[i]
3959
+ );
3960
+ _aFl[c] = _aFl[i];
3961
+ return c;
3962
+ };
3963
+
3964
+ // html5lib serializer name, derived: a `FOREIGN_ATTR_NS`-adjusted attribute is
3965
+ // flagged (its name is the table key), and a camelCase-adjusted name contains
3966
+ // an uppercase letter (unadjusted names are always lowercased), serializing as
3967
+ // itself. Everything else serializes as the plain name (undefined here).
3968
+ /** @type {(i: number) => string | undefined} */
3969
+ const _aSerializedName = (i) => {
3970
+ if ((_aFl[i] & 1) !== 0) return FOREIGN_ATTR_NS[_aName[i]];
3971
+ const name = _aName[i];
3972
+ return /[A-Z]/.test(name) ? name : undefined;
3973
+ };
3974
+
3975
+ /** @param {number} need minimum capacity */
3976
+ const _hGrow = (need) => {
3977
+ let cap = _hCap || 4096;
3978
+ while (cap < need) cap *= 2;
3979
+ const ty = new Uint8Array(cap);
3980
+ ty.set(_hTy);
3981
+ _hTy = ty;
3982
+ const fl = new Uint8Array(cap);
3983
+ fl.set(_hFl);
3984
+ _hFl = fl;
3985
+ const st = new Int32Array(cap);
3986
+ st.set(_hSt);
3987
+ _hSt = st;
3988
+ const en = new Int32Array(cap);
3989
+ en.set(_hEn);
3990
+ _hEn = en;
3991
+ const tagEnd = new Int32Array(cap);
3992
+ tagEnd.set(_hTagEnd);
3993
+ _hTagEnd = tagEnd;
3994
+ const nameEnd = new Int32Array(cap);
3995
+ nameEnd.set(_hNameEnd);
3996
+ _hNameEnd = nameEnd;
3997
+ const cEnd = new Int32Array(cap);
3998
+ cEnd.set(_hCEnd);
3999
+ _hCEnd = cEnd;
4000
+ const tc = new Int32Array(cap);
4001
+ tc.set(_hTc);
4002
+ _hTc = tc;
4003
+ const parent = new Int32Array(cap);
4004
+ parent.set(_hParent);
4005
+ _hParent = parent;
4006
+ const first = new Int32Array(cap);
4007
+ first.set(_hFirst);
4008
+ _hFirst = first;
4009
+ const last = new Int32Array(cap);
4010
+ last.set(_hLast);
4011
+ _hLast = last;
4012
+ const next = new Int32Array(cap);
4013
+ next.set(_hNext);
4014
+ _hNext = next;
4015
+ const aStart = new Int32Array(cap);
4016
+ aStart.set(_hAStart);
4017
+ _hAStart = aStart;
4018
+ const aCount = new Int32Array(cap);
4019
+ aCount.set(_hACount);
4020
+ _hACount = aCount;
4021
+ _hCap = cap;
4022
+ };
4023
+
4024
+ /** Start a new parse: invalidate all prior refs, release prior heap refs. */
4025
+ const _hReset = () => {
4026
+ _hN = 0;
4027
+ _aN = 0;
4028
+ _hStr.length = 0;
4029
+ _aName.length = 0;
4030
+ _aVal.length = 0;
4031
+ // Keep id 0 ("no node" / "no attribute") occupied so writes stay packed.
4032
+ _hStr.push("");
4033
+ _aName.push("");
4034
+ _aVal.push(null);
4035
+ _hDocPub = null;
4036
+ _hDocSys = null;
4037
+ };
4038
+
4039
+ // Release the side arrays' heap references (strings, attribute names/values)
4040
+ // once a walk has consumed the tree, so the retained columns don't pin the
4041
+ // parsed source until the next parse.
4042
+ const _hRelease = () => {
4043
+ _hN = 0;
4044
+ _aN = 0;
4045
+ _hStr.length = 0;
4046
+ _aName.length = 0;
4047
+ _aVal.length = 0;
4048
+ _hSrc = "";
4049
+ _hDocPub = null;
4050
+ _hDocSys = null;
4051
+ };
4052
+
4053
+ /** @type {(type: number, start: number, end: number) => HtmlNodeRef} */
4054
+ const _hAlloc = (type, start, end) => {
4055
+ const i = ++_hN;
4056
+ if (i >= _hCap) _hGrow(i + 1);
4057
+ _hTy[i] = type;
4058
+ _hFl[i] = 0;
4059
+ _hSt[i] = start;
4060
+ _hEn[i] = end;
4061
+ _hTagEnd[i] = 0;
4062
+ _hNameEnd[i] = 0;
4063
+ _hCEnd[i] = 0;
4064
+ _hTc[i] = 0;
4065
+ _hParent[i] = 0;
4066
+ _hFirst[i] = 0;
4067
+ _hLast[i] = 0;
4068
+ _hNext[i] = 0;
4069
+ _hAStart[i] = 0;
4070
+ _hACount[i] = 0;
4071
+ // Ids are sequential, so this indexed write appends (array stays packed).
4072
+ _hStr[i] = "";
4073
+ return i;
4074
+ };
4075
+
4076
+ // Raw child append — no text merging, no `<template>` content redirect (the
4077
+ // tree builder layers those on top). `node` must be detached (`next` = 0).
4078
+ /** @type {(parent: HtmlNodeRef, node: HtmlNodeRef) => void} */
4079
+ const _hAppend = (parent, node) => {
4080
+ _hParent[node] = parent;
4081
+ const last = _hLast[parent];
4082
+ if (last === 0) _hFirst[parent] = node;
4083
+ else _hNext[last] = node;
4084
+ _hLast[parent] = node;
4085
+ };
4086
+
4087
+ /** @type {(data: string, start: number, end: number) => HtmlNodeRef} */
4088
+ const _mkText = (data, start, end) => {
4089
+ const i = _hAlloc(NodeType.Text, start, end);
4090
+ _hStr[i] = data;
4091
+ return i;
4092
+ };
4093
+
4094
+ /** @type {(data: string, start: number, end: number) => HtmlNodeRef} */
4095
+ const _mkComment = (data, start, end) => {
4096
+ const i = _hAlloc(NodeType.Comment, start, end);
4097
+ _hStr[i] = data;
4098
+ return i;
4099
+ };
4100
+
4101
+ // Marker entry in the active-formatting-elements list (never a valid ref).
4102
+ const AFE_MARKER = -1;
4103
+
4104
+ // Clone of an element's attribute run: keep name/value (and the serializer
4105
+ // name flag) but drop source offsets so the consumer doesn't emit a duplicate
4106
+ // dependency for the reopened element's spans. Values are materialized since
4107
+ // the offsets are gone.
4108
+ const cloneAttrs = (/** @type {HtmlElement} */ el) => {
4109
+ const start = _hAStart[el];
4110
+ const count = _hACount[el];
4111
+ const newStart = _aN + 1;
4112
+ for (let i = start; i < start + count; i++) {
4113
+ const c = _aAlloc(_aName[i], _aValueOf(i), -1, -1, -1, -1);
4114
+ _aFl[c] = _aFl[i];
4115
+ }
4116
+ return { start: newStart, count };
4117
+ };
4118
+
4119
+ // Merge a repeated `<html>`/`<body>` tag's attributes into the element: only
4120
+ // names not already present are added (in source order after the existing
4121
+ // ones). Runs are contiguous, so any addition re-allocates the whole run; the
4122
+ // old slots are orphaned (at most once per repeated tag, rare).
4123
+ const mergeAttrs = (
4124
+ /** @type {HtmlElement} */ el,
4125
+ /** @type {AttributeRun} */ run
4126
+ ) => {
4127
+ const start = _hAStart[el];
4128
+ const count = _hACount[el];
4129
+ let extra = 0;
4130
+ for (let i = run.start; i < run.start + run.count; i++) {
4131
+ if (_aFind(start, count, _aName[i]) === 0) extra++;
4132
+ }
4133
+ if (extra === 0) return;
4134
+ const newStart = _aN + 1;
4135
+ for (let i = start; i < start + count; i++) _aCopy(i);
4136
+ for (let i = run.start; i < run.start + run.count; i++) {
4137
+ if (_aFind(start, count, _aName[i]) === 0) _aCopy(i);
4138
+ }
4139
+ _hAStart[el] = newStart;
4140
+ _hACount[el] = count + extra;
4141
+ };
4142
+
4143
+ /**
4144
+ * A materialized attribute as returned by `A.attributes` (tests/tooling) —
4145
+ * the parser-facing representation is an id into the attribute columns, read
4146
+ * through the scalar `A.attr*` accessors.
3782
4147
  * @typedef {object} HtmlAttribute
3783
4148
  * @property {string} name lowercased (and, in foreign content, adjusted) attribute name
3784
4149
  * @property {string} value
@@ -3790,60 +4155,27 @@ const NS_SVG = 2;
3790
4155
  */
3791
4156
 
3792
4157
  /**
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
4158
+ * A node reference into the struct-of-arrays AST: an integer id indexing the
4159
+ * parallel `_h*` columns. Read fields through the exported accessor `A`. Refs
4160
+ * are only valid until the next `buildHtmlAst` call — the columns are reused
4161
+ * across parses — so consume a tree fully before parsing again.
4162
+ * @typedef {number} HtmlNodeRef
3813
4163
  */
3814
4164
 
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
- */
4165
+ /** @typedef {HtmlNodeRef} HtmlElement ref to an Element node */
4166
+ /** @typedef {HtmlNodeRef} HtmlText ref to a Text node */
4167
+ /** @typedef {HtmlNodeRef} HtmlComment ref to a Comment node */
4168
+ /** @typedef {HtmlNodeRef} HtmlDoctype ref to a Doctype node */
4169
+ /** @typedef {HtmlNodeRef} HtmlDocument ref to the Document node */
4170
+ /** @typedef {HtmlNodeRef} HtmlDocumentFragment ref to a DocumentFragment node */
4171
+ /** @typedef {HtmlNodeRef} HtmlNode */
3832
4172
 
3833
4173
  /**
3834
- * @typedef {object} HtmlDocument
3835
- * @property {typeof NodeType.Document} type
3836
- * @property {HtmlNode[]} children
4174
+ * An attribute reference: an integer id into the attribute columns, read
4175
+ * through the `A.attr*` accessors. Same validity contract as `HtmlNodeRef`.
4176
+ * @typedef {number} HtmlAttributeRef
3837
4177
  */
3838
4178
 
3839
- /**
3840
- * @typedef {object} HtmlDocumentFragment
3841
- * @property {typeof NodeType.DocumentFragment} type
3842
- * @property {HtmlNode[]} children
3843
- */
3844
-
3845
- /** @typedef {HtmlElement | HtmlText | HtmlComment | HtmlDoctype} HtmlNode */
3846
-
3847
4179
  /** @typedef {{ start: number, end: number, tagEnd: number, nameEnd: number }} TagPos */
3848
4180
 
3849
4181
  // Tree-construction token `type` discriminators. Numeric for the same reason
@@ -3859,7 +4191,7 @@ const TOKEN_EOF = 6;
3859
4191
  /** @typedef {{ type: typeof TOKEN_CHAR, data: string, start: number, end: number }} CharToken */
3860
4192
  /** @typedef {{ type: typeof TOKEN_COMMENT, data: string, start: number, end: number }} CommentToken */
3861
4193
  /** @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 */
4194
+ /** @typedef {{ type: typeof TOKEN_START_TAG, name: string, attrs: AttributeRun, selfClosing: boolean, pos: TagPos, swallowNewline?: boolean }} StartTagToken */
3863
4195
  /** @typedef {{ type: typeof TOKEN_END_TAG, name: string, pos: TagPos }} EndTagToken */
3864
4196
  /** @typedef {{ type: typeof TOKEN_EOF }} EofToken */
3865
4197
 
@@ -3876,10 +4208,10 @@ const TOKEN_EOF = 6;
3876
4208
  * stale values that those handlers never read. Tokens that must outlive the
3877
4209
  * current callback (buffered table characters, synthesized re-dispatches) are
3878
4210
  * 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
4211
+ * @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
4212
  */
3881
4213
 
3882
- /** @typedef {{ parent: HtmlElement | HtmlDocument | HtmlDocumentFragment, beforeNode: (HtmlNode | null) }} InsertionPlace */
4214
+ /** @typedef {{ parent: HtmlNodeRef, beforeNode: HtmlNodeRef }} InsertionPlace `beforeNode` 0 = plain append */
3883
4215
 
3884
4216
  // Insertion modes (§13.2.4.1). Numeric for the same reason as the token and
3885
4217
  // `NodeType` enums: `runMode` dispatches on `mode` once per token.
@@ -4043,9 +4375,11 @@ const FORMATTING = new Set([
4043
4375
  const HEADING = new Set(["h1", "h2", "h3", "h4", "h5", "h6"]);
4044
4376
 
4045
4377
  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());
4378
+ const ns = _ns(el);
4379
+ const tag = _tag(el);
4380
+ if (ns === NS_HTML) return SPECIAL.has(tag);
4381
+ if (ns === NS_MATHML) return MATHML_SPECIAL.has(tag);
4382
+ if (ns === NS_SVG) return SVG_SPECIAL.has(tag.toLowerCase());
4049
4383
  return false;
4050
4384
  };
4051
4385
 
@@ -4493,14 +4827,13 @@ const FOREIGN_BREAKOUT = new Set([
4493
4827
  ]);
4494
4828
  const FONT_BREAKOUT_ATTRS = new Set(["color", "face", "size"]);
4495
4829
 
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
- );
4830
+ // `<font color|face|size>` breaks out of foreign content (§13.2.6.5).
4831
+ const hasFontBreakoutAttr = (/** @type {AttributeRun} */ run) => {
4832
+ for (let i = run.start; i < run.start + run.count; i++) {
4833
+ if (FONT_BREAKOUT_ATTRS.has(_aName[i])) return true;
4834
+ }
4835
+ return false;
4836
+ };
4504
4837
 
4505
4838
  /**
4506
4839
  * Hash of the ASCII-lowercased `name` for the intern tables below; must stay
@@ -4518,33 +4851,58 @@ const hashLowerName = (name) => {
4518
4851
  return h;
4519
4852
  };
4520
4853
 
4854
+ // Text-run scan classes for the `skip.text` fast path: 2 = stop the fast
4855
+ // path (& / NUL / CR), 1 = ASCII whitespace, 0 = ordinary text.
4856
+ const _TEXT_SCAN_CLASS = new Uint8Array(128);
4857
+ _TEXT_SCAN_CLASS[0x09] = 1;
4858
+ _TEXT_SCAN_CLASS[0x0a] = 1;
4859
+ _TEXT_SCAN_CLASS[0x0c] = 1;
4860
+ _TEXT_SCAN_CLASS[0x20] = 1;
4861
+ _TEXT_SCAN_CLASS[0x26] = 2;
4862
+ _TEXT_SCAN_CLASS[0x00] = 2;
4863
+ _TEXT_SCAN_CLASS[0x0d] = 2;
4864
+
4521
4865
  /**
4522
4866
  * @param {Iterable<string>} names lowercase names to intern
4523
- * @returns {Map<number, string | string[]>} hash name(s)
4867
+ * @returns {{ mask: number, hashes: Int32Array, values: (string | string[] | undefined)[] }} open-addressed intern table
4524
4868
  */
4525
4869
  const buildNameInternTable = (names) => {
4526
- /** @type {Map<number, string | string[]>} */
4527
- const table = new Map();
4528
- for (const name of names) {
4870
+ // Open-addressed table (~25% load): the per-name probe is one or two array
4871
+ // reads instead of a `Map#get`, and the tables are built once at startup.
4872
+ const unique = [...new Set(names)];
4873
+ let size = 8;
4874
+ while (size < unique.length * 4) size <<= 1;
4875
+ const mask = size - 1;
4876
+ const hashes = new Int32Array(size);
4877
+ /** @type {(string | string[] | undefined)[]} */
4878
+ const values = Array.from({ length: size });
4879
+ for (const name of unique) {
4529
4880
  const h = hashLowerName(name);
4530
- const cur = table.get(h);
4881
+ let slot = h & mask;
4882
+ while (values[slot] !== undefined && hashes[slot] !== h) {
4883
+ slot = (slot + 1) & mask;
4884
+ }
4885
+ const cur = values[slot];
4531
4886
  if (cur === undefined) {
4532
- table.set(h, name);
4887
+ hashes[slot] = h;
4888
+ values[slot] = name;
4533
4889
  } else if (typeof cur === "string") {
4534
- if (cur !== name) table.set(h, [cur, name]);
4535
- } else if (!cur.includes(name)) {
4890
+ values[slot] = [cur, name];
4891
+ } else {
4536
4892
  cur.push(name);
4537
4893
  }
4538
4894
  }
4539
- return table;
4895
+ return { mask, hashes, values };
4540
4896
  };
4541
4897
 
4898
+ /** @typedef {ReturnType<typeof buildNameInternTable>} NameInternTable */
4899
+
4542
4900
  /**
4543
4901
  * The lowercased name for `input[start..end)`, returning the shared interned
4544
4902
  * string for known names — skipping the per-tag `slice().toLowerCase()`
4545
4903
  * allocation, and making the tree builder's many Set/Map lookups and `===`
4546
4904
  * comparisons on the name hit one string instance with a cached hash.
4547
- * @param {Map<number, string | string[]>} table intern table
4905
+ * @param {NameInternTable} table intern table
4548
4906
  * @param {string} input source text
4549
4907
  * @param {number} start name start
4550
4908
  * @param {number} end name end
@@ -4557,15 +4915,22 @@ const internLowerName = (table, input, start, end) => {
4557
4915
  if (c >= 0x41 && c <= 0x5a) c += 0x20;
4558
4916
  h = ((h << 5) - h + c) | 0;
4559
4917
  }
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];
4918
+ const { mask, hashes, values } = table;
4919
+ let slot = h & mask;
4920
+ for (;;) {
4921
+ const hit = values[slot];
4922
+ if (hit === undefined) break;
4923
+ if (hashes[slot] === h) {
4924
+ if (typeof hit === "string") {
4925
+ if (rangeEqualsLower(input, start, end, hit)) return hit;
4926
+ } else {
4927
+ for (let i = 0; i < hit.length; i++) {
4928
+ if (rangeEqualsLower(input, start, end, hit[i])) return hit[i];
4929
+ }
4567
4930
  }
4931
+ break;
4568
4932
  }
4933
+ slot = (slot + 1) & mask;
4569
4934
  }
4570
4935
  // Unknown name (custom element, data-* attribute, non-ASCII, …).
4571
4936
  return input.slice(start, end).toLowerCase();
@@ -4604,50 +4969,71 @@ const ATTR_NAME_INTERN = buildNameInternTable(
4604
4969
  // Hoisted so the many `open.some(...)` "is there an open HTML <template>?"
4605
4970
  // checks reuse one predicate instead of allocating an arrow per call.
4606
4971
  const isHtmlTemplateEl = (/** @type {HtmlElement} */ e) =>
4607
- e.tagName === "template" && e.namespace === NS_HTML;
4972
+ _tag(e) === "template" && _ns(e) === NS_HTML;
4608
4973
 
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
- };
4974
+ // Raw text / escapable raw text elements (WHATWG §13.1.2) plus the other
4975
+ // RAWTEXT/RCDATA/PLAINTEXT-tokenized elements. Under `skip.text` their body's
4976
+ // end offset is recorded on the element (see `insertCharacters`) so a consumer
4977
+ // can read the raw content span without a `Text` node.
4978
+ const RAW_TEXT_ELEMENTS = new Set([
4979
+ "script",
4980
+ "style",
4981
+ "textarea",
4982
+ "title",
4983
+ "xmp",
4984
+ "iframe",
4985
+ "noembed",
4986
+ "noframes",
4987
+ "noscript",
4988
+ "plaintext"
4989
+ ]);
4990
+
4991
+ /** Shared empty skip set so the common (no-skip) call allocates nothing. */
4992
+ const EMPTY_SKIP = Object.freeze({});
4993
+
4994
+ /**
4995
+ * Optional node kinds a consumer can drop from the AST for speed/memory. Each
4996
+ * is a pure output reduction — tree construction (and quirks detection) runs
4997
+ * unchanged, so element structure and offsets are identical either way.
4998
+ * @typedef {object} HtmlAstSkip
4999
+ * @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.
5000
+ * @property {boolean=} comments drop comment nodes entirely. Not for consumers that read comments (e.g. webpack magic comments).
5001
+ * @property {boolean=} doctype drop the doctype node; quirks-mode detection is unaffected.
5002
+ */
4620
5003
 
4621
5004
  /**
4622
5005
  * @param {string} source HTML source
4623
5006
  * @param {string=} fragmentContext context element name for fragment parsing (e.g. `td`, `svg path`); omit for a full document
4624
- * @returns {HtmlDocument} document AST
5007
+ * @param {HtmlAstSkip=} skip node kinds to omit from the AST (see `HtmlAstSkip`); omit to build the full tree
5008
+ * @returns {HtmlDocument} ref to the document node — read through `A`; valid until the next parse
4625
5009
  */
4626
- const buildHtmlAst = (source, fragmentContext) => {
4627
- /** @type {HtmlDocument} */
4628
- const doc = { type: NodeType.Document, children: [] };
5010
+ const buildHtmlAst = (source, fragmentContext, skip = EMPTY_SKIP) => {
5011
+ const skipText = skip.text === true;
5012
+ const skipComments = skip.comments === true;
5013
+ const skipDoctype = skip.doctype === true;
5014
+ _hReset();
5015
+ _hSrc = source;
5016
+ const doc = _hAlloc(NodeType.Document, 0, 0);
4629
5017
  let mode = MODE_INITIAL;
4630
5018
  // Mode to return to after MODE_TEXT / MODE_IN_TABLE_TEXT (0 = unset).
4631
5019
  let originalMode = 0;
4632
5020
  /** @type {HtmlElement[]} stack of open elements (bottom .. top) */
4633
5021
  const open = [];
4634
- /** @type {{ element?: HtmlElement, marker?: boolean }[]} active formatting elements */
5022
+ /** @type {HtmlNodeRef[]} active formatting elements (AFE_MARKER = marker) */
4635
5023
  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;
5024
+ /** @type {HtmlElement} 0 = none */
5025
+ let head = 0;
5026
+ /** @type {HtmlElement} 0 = none */
5027
+ let form = 0;
4642
5028
  let framesetOk = true;
4643
- /** @type {HtmlAttribute[]} */
4644
- let pendingAttrs = [];
5029
+ // First attribute id of the tag currently being tokenized (0 = none).
5030
+ let pendAttrStart = 0;
4645
5031
  let fosterParenting = false;
4646
5032
  /** @type {number[]} */
4647
5033
  const templateModes = [];
4648
5034
  let quirks = false;
4649
- /** @type {HtmlElement | null} */
4650
- let fragment = null;
5035
+ /** @type {HtmlElement} fragment context element (0 = document parse) */
5036
+ let fragment = 0;
4651
5037
  // End offset of the token currently being processed (for element `.end`).
4652
5038
  let tokenEnd = 0;
4653
5039
 
@@ -4660,70 +5046,68 @@ const buildHtmlAst = (source, fragmentContext) => {
4660
5046
  const mkEl = (
4661
5047
  /** @type {string} */ tagName,
4662
5048
  /** @type {number} */ ns,
4663
- /** @type {HtmlAttribute[] | undefined} */ attributes,
5049
+ /** @type {AttributeRun} */ attrs,
4664
5050
  /** @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
- }));
5051
+ ) => {
5052
+ const el = _hAlloc(
5053
+ NodeType.Element,
5054
+ pos ? pos.start : 0,
5055
+ pos ? pos.end : 0
5056
+ );
5057
+ // Void HTML elements are marked self-closing and never receive children.
5058
+ _hFl[el] =
5059
+ ns === NS_HTML && VOID.has(tagName) ? ns | FLAG_SELF_CLOSING : ns;
5060
+ _hStr[el] = tagName;
5061
+ _hAStart[el] = attrs.start;
5062
+ _hACount[el] = attrs.count;
5063
+ if (pos) {
5064
+ _hTagEnd[el] = pos.tagEnd;
5065
+ _hNameEnd[el] = pos.nameEnd;
5066
+ // End of a raw-text element's body under `skip.text` (defaults to the
5067
+ // body start, i.e. empty); lets consumers read `<script>`/`<style>`
5068
+ // content as [`tagEnd`, `contentEnd`] without a `Text` node.
5069
+ _hCEnd[el] = pos.tagEnd;
5070
+ }
5071
+ return el;
5072
+ };
4695
5073
 
4696
5074
  /**
4697
- * @param {HtmlElement | HtmlDocument | HtmlDocumentFragment} parent container
4698
- * @returns {HtmlNode[]} children array to insert into
5075
+ * A `<template>`'s children live in its content fragment; inserting into a
5076
+ * template really inserts there (the spec's "template contents" redirect).
5077
+ * @param {HtmlNodeRef} parent container
5078
+ * @returns {HtmlNodeRef} effective container to link children into
4699
5079
  */
4700
- const childrenOf = (parent) =>
4701
- parent.type === NodeType.Element && parent.templateContent
4702
- ? parent.templateContent.children
4703
- : parent.children;
5080
+ const effParent = (parent) => {
5081
+ const tc = _hTc[parent];
5082
+ return tc !== 0 ? tc : parent;
5083
+ };
4704
5084
 
4705
5085
  const appendTo = (
4706
- /** @type {HtmlElement | HtmlDocument | HtmlDocumentFragment} */ parent,
4707
- /** @type {HtmlNode} */ node
5086
+ /** @type {HtmlNodeRef} */ parent,
5087
+ /** @type {HtmlNodeRef} */ node
4708
5088
  ) => {
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;
5089
+ const p = effParent(parent);
5090
+ const last = _hLast[p];
5091
+ if (
5092
+ _hTy[node] === NodeType.Text &&
5093
+ last !== 0 &&
5094
+ _hTy[last] === NodeType.Text
5095
+ ) {
5096
+ _hStr[last] += _hStr[node];
5097
+ _hEn[last] = _hEn[node];
4714
5098
  return;
4715
5099
  }
4716
- arr.push(node);
5100
+ _hAppend(p, node);
4717
5101
  };
4718
5102
 
4719
5103
  // Reused result of `appropriatePlace` — consumed synchronously by
4720
5104
  // `insertAtPlace` and never retained, so one shared object avoids an
4721
5105
  // allocation per inserted node.
4722
5106
  /** @type {InsertionPlace} */
4723
- const sharedPlace = { parent: doc, beforeNode: null };
5107
+ const sharedPlace = { parent: doc, beforeNode: 0 };
4724
5108
  const placeAt = (
4725
- /** @type {HtmlElement | HtmlDocument | HtmlDocumentFragment} */ parent,
4726
- /** @type {HtmlNode | null} */ beforeNode
5109
+ /** @type {HtmlNodeRef} */ parent,
5110
+ /** @type {HtmlNodeRef} */ beforeNode
4727
5111
  ) => {
4728
5112
  sharedPlace.parent = parent;
4729
5113
  sharedPlace.beforeNode = beforeNode;
@@ -4735,23 +5119,23 @@ const buildHtmlAst = (source, fragmentContext) => {
4735
5119
  const target = cur();
4736
5120
  if (
4737
5121
  fosterParenting &&
4738
- TABLE_CONTEXT.has(target.tagName) &&
4739
- target.namespace === NS_HTML
5122
+ TABLE_CONTEXT.has(_tag(target)) &&
5123
+ _ns(target) === NS_HTML
4740
5124
  ) {
4741
5125
  // find last template / last table
4742
5126
  let lastTemplate = -1;
4743
5127
  let lastTable = -1;
4744
5128
  for (let i = open.length - 1; i >= 0; i--) {
4745
5129
  if (
4746
- open[i].tagName === "template" &&
4747
- open[i].namespace === NS_HTML &&
5130
+ _tag(open[i]) === "template" &&
5131
+ _ns(open[i]) === NS_HTML &&
4748
5132
  lastTemplate === -1
4749
5133
  ) {
4750
5134
  lastTemplate = i;
4751
5135
  }
4752
5136
  if (
4753
- open[i].tagName === "table" &&
4754
- open[i].namespace === NS_HTML &&
5137
+ _tag(open[i]) === "table" &&
5138
+ _ns(open[i]) === NS_HTML &&
4755
5139
  lastTable === -1
4756
5140
  ) {
4757
5141
  lastTable = i;
@@ -4761,53 +5145,52 @@ const buildHtmlAst = (source, fragmentContext) => {
4761
5145
  lastTemplate !== -1 &&
4762
5146
  (lastTable === -1 || lastTemplate > lastTable)
4763
5147
  ) {
4764
- return placeAt(open[lastTemplate], null);
5148
+ return placeAt(open[lastTemplate], 0);
4765
5149
  }
4766
5150
  if (lastTable === -1) {
4767
- return placeAt(open[0], null);
5151
+ return placeAt(open[0], 0);
4768
5152
  }
4769
5153
  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);
4774
- }
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
- }
5154
+ const tp = _hParent[table];
5155
+ if (tp !== 0) return placeAt(tp, table);
5156
+ return placeAt(open[lastTable - 1], 0);
4791
5157
  }
4792
- return null;
5158
+ return placeAt(target, 0);
4793
5159
  };
4794
5160
 
4795
5161
  const insertAtPlace = (
4796
5162
  /** @type {InsertionPlace} */ place,
4797
- /** @type {HtmlNode} */ node
5163
+ /** @type {HtmlNodeRef} */ node
4798
5164
  ) => {
4799
- if (place.beforeNode) {
4800
- const arr = childrenOf(place.parent);
4801
- const idx = arr.indexOf(place.beforeNode);
5165
+ const before = place.beforeNode;
5166
+ if (before !== 0) {
5167
+ const p = effParent(place.parent);
5168
+ // Find `before`'s previous sibling (insert-before is a rare foster/
5169
+ // adoption path, so the sibling scan stays off the hot path).
5170
+ let prev = 0;
5171
+ let c = _hFirst[p];
5172
+ while (c !== 0 && c !== before) {
5173
+ prev = c;
5174
+ c = _hNext[c];
5175
+ }
5176
+ if (c === 0) {
5177
+ // `before` not under `parent` (not reachable from the spec paths).
5178
+ appendTo(place.parent, node);
5179
+ return;
5180
+ }
4802
5181
  if (
4803
- node.type === NodeType.Text &&
4804
- idx > 0 &&
4805
- arr[idx - 1].type === NodeType.Text
5182
+ _hTy[node] === NodeType.Text &&
5183
+ prev !== 0 &&
5184
+ _hTy[prev] === NodeType.Text
4806
5185
  ) {
4807
- /** @type {HtmlText} */ (arr[idx - 1]).data += node.data;
5186
+ // Before-node merge deliberately does not bump the sibling's `end`.
5187
+ _hStr[prev] += _hStr[node];
4808
5188
  return;
4809
5189
  }
4810
- arr.splice(idx, 0, node);
5190
+ _hParent[node] = p;
5191
+ _hNext[node] = before;
5192
+ if (prev === 0) _hFirst[p] = node;
5193
+ else _hNext[prev] = node;
4811
5194
  } else {
4812
5195
  appendTo(place.parent, node);
4813
5196
  }
@@ -4819,28 +5202,55 @@ const buildHtmlAst = (source, fragmentContext) => {
4819
5202
  /** @type {number} */ end
4820
5203
  ) => {
4821
5204
  const place = appropriatePlace();
4822
- if (place.parent.type === NodeType.Document) return; // never insert text into document
5205
+ if (_hTy[place.parent] === NodeType.Document) return; // never insert text into document
5206
+ // `skip.text`: drop every `Text` node — construction already used the
5207
+ // decoded token, so removing the node never affects element structure.
5208
+ // For a raw-text element record the body end so a consumer reads the span
5209
+ // [`tagEnd`, `contentEnd`] without a `Text` node (see `HtmlParser`).
5210
+ // Namespace-agnostic: `HtmlParser` extracts `<script>`/`<style>` bodies in
5211
+ // foreign content (e.g. SVG `<style>`) too.
5212
+ if (skipText) {
5213
+ const p = place.parent;
5214
+ if (_hTy[p] === NodeType.Element && RAW_TEXT_ELEMENTS.has(_tag(p))) {
5215
+ _hCEnd[p] = end;
5216
+ }
5217
+ return;
5218
+ }
4823
5219
  // Inlined text insert: when the run merges into the adjacent text sibling
4824
5220
  // (common with inline formatting) only the string is appended — no
4825
5221
  // throwaway text node is allocated. Mirrors `insertAtPlace`/`appendTo`,
4826
5222
  // 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;
5223
+ const p = effParent(place.parent);
5224
+ const before = place.beforeNode;
5225
+ if (before !== 0) {
5226
+ let prev = 0;
5227
+ let c = _hFirst[p];
5228
+ while (c !== 0 && c !== before) {
5229
+ prev = c;
5230
+ c = _hNext[c];
5231
+ }
5232
+ if (c === 0) {
5233
+ // `before` not under `parent` (not reachable from the spec paths).
5234
+ _hAppend(p, _mkText(data, start, end));
5235
+ return;
5236
+ }
5237
+ if (prev !== 0 && _hTy[prev] === NodeType.Text) {
5238
+ _hStr[prev] += data;
4833
5239
  return;
4834
5240
  }
4835
- arr.splice(idx, 0, { type: NodeType.Text, data, start, end });
5241
+ const node = _mkText(data, start, end);
5242
+ _hParent[node] = p;
5243
+ _hNext[node] = before;
5244
+ if (prev === 0) _hFirst[p] = node;
5245
+ else _hNext[prev] = node;
4836
5246
  } else {
4837
- const last = arr[arr.length - 1];
4838
- if (last && last.type === NodeType.Text) {
4839
- last.data += data;
4840
- last.end = end;
5247
+ const last = _hLast[p];
5248
+ if (last !== 0 && _hTy[last] === NodeType.Text) {
5249
+ _hStr[last] += data;
5250
+ _hEn[last] = end;
4841
5251
  return;
4842
5252
  }
4843
- arr.push({ type: NodeType.Text, data, start, end });
5253
+ _hAppend(p, _mkText(data, start, end));
4844
5254
  }
4845
5255
  };
4846
5256
 
@@ -4851,16 +5261,17 @@ const buildHtmlAst = (source, fragmentContext) => {
4851
5261
  * @param {InsertionPlace=} place explicit insertion place
4852
5262
  */
4853
5263
  const insertComment = (data, start, end, place) => {
5264
+ if (skipComments) return;
4854
5265
  const p = place || appropriatePlace();
4855
- insertAtPlace(p, { type: NodeType.Comment, data, start, end });
5266
+ insertAtPlace(p, _mkComment(data, start, end));
4856
5267
  };
4857
5268
 
4858
5269
  const insertHtmlElement = (
4859
5270
  /** @type {string} */ tagName,
4860
- /** @type {HtmlAttribute[]} */ attributes,
5271
+ /** @type {AttributeRun} */ attrs,
4861
5272
  /** @type {TagPos | null} */ pos
4862
5273
  ) => {
4863
- const el = mkEl(tagName, NS_HTML, attributes, pos);
5274
+ const el = mkEl(tagName, NS_HTML, attrs, pos);
4864
5275
  const place = appropriatePlace();
4865
5276
  insertAtPlace(place, el);
4866
5277
  open.push(el);
@@ -4870,10 +5281,10 @@ const buildHtmlAst = (source, fragmentContext) => {
4870
5281
  const insertForeignElement = (
4871
5282
  /** @type {string} */ tagName,
4872
5283
  /** @type {number} */ ns,
4873
- /** @type {HtmlAttribute[]} */ attributes,
5284
+ /** @type {AttributeRun} */ attrs,
4874
5285
  /** @type {TagPos | null} */ pos
4875
5286
  ) => {
4876
- const el = mkEl(tagName, ns, attributes, pos);
5287
+ const el = mkEl(tagName, ns, attrs, pos);
4877
5288
  const place = appropriatePlace();
4878
5289
  insertAtPlace(place, el);
4879
5290
  open.push(el);
@@ -4882,10 +5293,10 @@ const buildHtmlAst = (source, fragmentContext) => {
4882
5293
 
4883
5294
  // ---- scopes ----
4884
5295
  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());
5296
+ if (_ns(el) === NS_HTML) return HTML_SCOPE.has(_tag(el));
5297
+ if (_ns(el) === NS_MATHML) return MATHML_SPECIAL.has(_tag(el));
5298
+ if (_ns(el) === NS_SVG) {
5299
+ return SVG_SPECIAL.has(_tag(el).toLowerCase());
4889
5300
  }
4890
5301
  return false;
4891
5302
  };
@@ -4900,10 +5311,10 @@ const buildHtmlAst = (source, fragmentContext) => {
4900
5311
  /** @type {number} */ kind
4901
5312
  ) => {
4902
5313
  if (isScopeBoundary(el)) return true;
4903
- if (el.namespace !== NS_HTML) return false;
4904
- if (kind === SCOPE_BUTTON) return el.tagName === "button";
5314
+ if (_ns(el) !== NS_HTML) return false;
5315
+ if (kind === SCOPE_BUTTON) return _tag(el) === "button";
4905
5316
  if (kind === SCOPE_LIST_ITEM) {
4906
- return el.tagName === "ol" || el.tagName === "ul";
5317
+ return _tag(el) === "ol" || _tag(el) === "ul";
4907
5318
  }
4908
5319
  return false;
4909
5320
  };
@@ -4915,7 +5326,7 @@ const buildHtmlAst = (source, fragmentContext) => {
4915
5326
  ) => {
4916
5327
  for (let i = open.length - 1; i >= 0; i--) {
4917
5328
  const el = open[i];
4918
- if (el.namespace === NS_HTML && el.tagName === tagName) return true;
5329
+ if (_ns(el) === NS_HTML && _tag(el) === tagName) return true;
4919
5330
  if (isBoundaryForKind(el, kind)) return false;
4920
5331
  }
4921
5332
  return false;
@@ -4939,9 +5350,9 @@ const buildHtmlAst = (source, fragmentContext) => {
4939
5350
  const set = typeof target === "string" ? null : target;
4940
5351
  for (let i = open.length - 1; i >= 0; i--) {
4941
5352
  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;
5353
+ if (_ns(el) === NS_HTML) {
5354
+ if (set ? set.has(_tag(el)) : _tag(el) === target) return true;
5355
+ if (TABLE_SCOPE_STOP.has(_tag(el))) return false;
4945
5356
  }
4946
5357
  }
4947
5358
  return false;
@@ -4949,11 +5360,7 @@ const buildHtmlAst = (source, fragmentContext) => {
4949
5360
  const generateImpliedEndTags = (except = "") => {
4950
5361
  while (open.length) {
4951
5362
  const el = cur();
4952
- if (
4953
- el.namespace === NS_HTML &&
4954
- IMPLIED.has(el.tagName) &&
4955
- el.tagName !== except
4956
- ) {
5363
+ if (_ns(el) === NS_HTML && IMPLIED.has(_tag(el)) && _tag(el) !== except) {
4957
5364
  open.pop();
4958
5365
  } else {
4959
5366
  break;
@@ -4963,7 +5370,7 @@ const buildHtmlAst = (source, fragmentContext) => {
4963
5370
  const generateImpliedEndTagsThorough = () => {
4964
5371
  while (open.length) {
4965
5372
  const el = cur();
4966
- if (el.namespace === NS_HTML && IMPLIED_THOROUGH.has(el.tagName)) {
5373
+ if (_ns(el) === NS_HTML && IMPLIED_THOROUGH.has(_tag(el))) {
4967
5374
  open.pop();
4968
5375
  } else {
4969
5376
  break;
@@ -4976,12 +5383,8 @@ const buildHtmlAst = (source, fragmentContext) => {
4976
5383
  let count = 0;
4977
5384
  for (let i = afe.length - 1; i >= 0; i--) {
4978
5385
  const e = afe[i];
4979
- if (e.marker) break;
4980
- if (
4981
- afeEl(e).tagName === el.tagName &&
4982
- afeEl(e).namespace === el.namespace &&
4983
- sameAttrs(afeEl(e), el)
4984
- ) {
5386
+ if (e === AFE_MARKER) break;
5387
+ if (_tag(e) === _tag(el) && _ns(e) === _ns(el) && sameAttrs(e, el)) {
4985
5388
  count++;
4986
5389
  if (count === 3) {
4987
5390
  afe.splice(i, 1);
@@ -4989,60 +5392,49 @@ const buildHtmlAst = (source, fragmentContext) => {
4989
5392
  }
4990
5393
  }
4991
5394
  }
4992
- afe.push({ element: el });
5395
+ afe.push(el);
4993
5396
  };
4994
5397
  const sameAttrs = (
4995
5398
  /** @type {HtmlElement} */ a,
4996
5399
  /** @type {HtmlElement} */ b
4997
5400
  ) => {
4998
- const aa = a.attributes;
4999
- const ba = b.attributes;
5000
- if (aa.length !== ba.length) return false;
5401
+ const aStart = _hAStart[a];
5402
+ const aCount = _hACount[a];
5403
+ const bStart = _hAStart[b];
5404
+ const bCount = _hACount[b];
5405
+ if (aCount !== bCount) return false;
5001
5406
  // 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;
5407
+ // here on this formatting-element hot path.
5408
+ for (let i = bStart; i < bStart + bCount; i++) {
5409
+ const j = _aFind(aStart, aCount, _aName[i]);
5410
+ if (j === 0 || _aValueOf(j) !== _aValueOf(i)) return false;
5013
5411
  }
5014
5412
  return true;
5015
5413
  };
5016
- const insertMarker = () => afe.push({ marker: true });
5414
+ const insertMarker = () => afe.push(AFE_MARKER);
5017
5415
  const clearAfeToMarker = () => {
5018
5416
  while (afe.length) {
5019
- const e = /** @type {{ marker?: boolean }} */ (afe.pop());
5020
- if (e.marker) break;
5417
+ if (afe.pop() === AFE_MARKER) break;
5021
5418
  }
5022
5419
  };
5023
5420
  const reconstructAfe = () => {
5024
5421
  if (afe.length === 0) return;
5025
5422
  let i = afe.length - 1;
5026
- if (afe[i].marker || open.includes(afeEl(afe[i]))) return;
5423
+ if (afe[i] === AFE_MARKER || open.includes(afe[i])) return;
5027
5424
  while (i > 0) {
5028
5425
  i--;
5029
- if (afe[i].marker || open.includes(afeEl(afe[i]))) {
5426
+ if (afe[i] === AFE_MARKER || open.includes(afe[i])) {
5030
5427
  i++;
5031
5428
  break;
5032
5429
  }
5033
5430
  }
5034
5431
  for (; i < afe.length; i++) {
5035
5432
  const e = afe[i];
5036
- const el = mkEl(
5037
- afeEl(e).tagName,
5038
- afeEl(e).namespace,
5039
- cloneAttrs(afeEl(e).attributes),
5040
- null
5041
- );
5433
+ const el = mkEl(_tag(e), _ns(e), cloneAttrs(e), null);
5042
5434
  const place = appropriatePlace();
5043
5435
  insertAtPlace(place, el);
5044
5436
  open.push(el);
5045
- afe[i] = { element: el };
5437
+ afe[i] = el;
5046
5438
  }
5047
5439
  };
5048
5440
 
@@ -5052,23 +5444,23 @@ const buildHtmlAst = (source, fragmentContext) => {
5052
5444
  // pop until a p has been popped
5053
5445
  while (open.length) {
5054
5446
  const el = /** @type {HtmlElement} */ (open.pop());
5055
- el.end = tokenEnd;
5056
- if (el.namespace === NS_HTML && el.tagName === "p") break;
5447
+ _hEn[el] = tokenEnd;
5448
+ if (_ns(el) === NS_HTML && _tag(el) === "p") break;
5057
5449
  }
5058
5450
  };
5059
5451
 
5060
5452
  const popUntil = (/** @type {string} */ tagName) => {
5061
5453
  while (open.length) {
5062
5454
  const el = /** @type {HtmlElement} */ (open.pop());
5063
- el.end = tokenEnd;
5064
- if (el.namespace === NS_HTML && el.tagName === tagName) break;
5455
+ _hEn[el] = tokenEnd;
5456
+ if (_ns(el) === NS_HTML && _tag(el) === tagName) break;
5065
5457
  }
5066
5458
  };
5067
5459
  const popUntilOneOf = (/** @type {Set<string>} */ set) => {
5068
5460
  while (open.length) {
5069
5461
  const el = /** @type {HtmlElement} */ (open.pop());
5070
- el.end = tokenEnd;
5071
- if (el.namespace === NS_HTML && set.has(el.tagName)) break;
5462
+ _hEn[el] = tokenEnd;
5463
+ if (_ns(el) === NS_HTML && set.has(_tag(el))) break;
5072
5464
  }
5073
5465
  };
5074
5466
 
@@ -5081,8 +5473,8 @@ const buildHtmlAst = (source, fragmentContext) => {
5081
5473
  last = true;
5082
5474
  if (fragment) node = fragment;
5083
5475
  }
5084
- const tn = node.tagName;
5085
- if (node.namespace === NS_HTML) {
5476
+ const tn = _tag(node);
5477
+ if (_ns(node) === NS_HTML) {
5086
5478
  if ((tn === "td" || tn === "th") && !last) {
5087
5479
  mode = MODE_IN_CELL;
5088
5480
  return;
@@ -5175,7 +5567,7 @@ const buildHtmlAst = (source, fragmentContext) => {
5175
5567
  const useForeign =
5176
5568
  open.length > 0 &&
5177
5569
  ac &&
5178
- ac.namespace !== NS_HTML &&
5570
+ _ns(ac) !== NS_HTML &&
5179
5571
  ty !== TOKEN_EOF &&
5180
5572
  shouldUseForeignRules(ac, t);
5181
5573
  if (useForeign) {
@@ -5189,7 +5581,7 @@ const buildHtmlAst = (source, fragmentContext) => {
5189
5581
  /** @type {HtmlElement} */ ac,
5190
5582
  /** @type {Token} */ t
5191
5583
  ) => {
5192
- if (ac.namespace === NS_HTML) return false;
5584
+ if (_ns(ac) === NS_HTML) return false;
5193
5585
  if (t.type === TOKEN_START_TAG) {
5194
5586
  if (
5195
5587
  mathmlTextIntegrationPoint(ac) &&
@@ -5199,8 +5591,8 @@ const buildHtmlAst = (source, fragmentContext) => {
5199
5591
  return false;
5200
5592
  }
5201
5593
  if (
5202
- ac.namespace === NS_MATHML &&
5203
- ac.tagName === "annotation-xml" &&
5594
+ _ns(ac) === NS_MATHML &&
5595
+ _tag(ac) === "annotation-xml" &&
5204
5596
  t.name === "svg"
5205
5597
  ) {
5206
5598
  return false;
@@ -5219,20 +5611,19 @@ const buildHtmlAst = (source, fragmentContext) => {
5219
5611
  };
5220
5612
 
5221
5613
  const mathmlTextIntegrationPoint = (/** @type {HtmlElement} */ el) =>
5222
- el.namespace === NS_MATHML && MATHML_TEXT_INTEGRATION.has(el.tagName);
5614
+ _ns(el) === NS_MATHML && MATHML_TEXT_INTEGRATION.has(_tag(el));
5223
5615
  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;
5616
+ if (_ns(el) === NS_MATHML && _tag(el) === "annotation-xml") {
5617
+ const enc = _aFind(_hAStart[el], _hACount[el], "encoding");
5618
+ if (enc !== 0) {
5619
+ const value = _aValueOf(enc).toLowerCase();
5620
+ if (value === "text/html" || value === "application/xhtml+xml") {
5621
+ return true;
5622
+ }
5232
5623
  }
5233
5624
  return false;
5234
5625
  }
5235
- if (el.namespace === NS_SVG && SVG_SPECIAL.has(el.tagName.toLowerCase())) {
5626
+ if (_ns(el) === NS_SVG && SVG_SPECIAL.has(_tag(el).toLowerCase())) {
5236
5627
  return true;
5237
5628
  }
5238
5629
  return false;
@@ -5240,37 +5631,28 @@ const buildHtmlAst = (source, fragmentContext) => {
5240
5631
 
5241
5632
  const adjustSvgTag = (/** @type {string} */ name) =>
5242
5633
  /** @type {Record<string, string>} */ (SVG_TAG_ADJUST)[name] || name;
5634
+ // Adjust a foreign start tag's attribute run in place (the run is consumed
5635
+ // only by this tag's element): SVG camelCase names are rewritten, and
5636
+ // namespaced names get the serializer-name flag (see `_aSerializedName`).
5243
5637
  const adjustForeignAttrs = (
5244
- /** @type {HtmlAttribute[]} */ attrs,
5638
+ /** @type {AttributeRun} */ run,
5245
5639
  /** @type {number} */ ns
5246
5640
  ) => {
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;
5641
+ for (let i = run.start; i < run.start + run.count; i++) {
5642
+ const name = _aName[i];
5257
5643
  if (
5258
5644
  ns === NS_SVG &&
5259
5645
  /** @type {Record<string, string>} */ (SVG_ATTR_ADJUST)[name]
5260
5646
  ) {
5261
- name = /** @type {Record<string, string>} */ (SVG_ATTR_ADJUST)[name];
5262
- serializedName = name;
5647
+ _aName[i] = /** @type {Record<string, string>} */ (SVG_ATTR_ADJUST)[
5648
+ name
5649
+ ];
5263
5650
  }
5264
- if (/** @type {Record<string, string>} */ (FOREIGN_ATTR_NS)[a.name]) {
5265
- serializedName = /** @type {Record<string, string>} */ (
5266
- FOREIGN_ATTR_NS
5267
- )[a.name];
5651
+ if (/** @type {Record<string, string>} */ (FOREIGN_ATTR_NS)[name]) {
5652
+ _aFl[i] |= 1;
5268
5653
  }
5269
- if (name === a.name && serializedName === undefined) continue;
5270
- if (out === attrs) out = [...attrs];
5271
- out[i] = { ...a, name, serializedName };
5272
5654
  }
5273
- return out;
5655
+ return run;
5274
5656
  };
5275
5657
 
5276
5658
  const foreignContent = (/** @type {Token} */ t) => {
@@ -5287,17 +5669,16 @@ const buildHtmlAst = (source, fragmentContext) => {
5287
5669
  }
5288
5670
  if (t.type === TOKEN_DOCTYPE) return;
5289
5671
  if (t.type === TOKEN_START_TAG) {
5290
- const acn = adjustedCurrent().namespace;
5672
+ const acn = _ns(adjustedCurrent());
5291
5673
  if (
5292
5674
  FOREIGN_BREAKOUT.has(t.name) ||
5293
- (t.name === "font" &&
5294
- t.attrs.some((a) => FONT_BREAKOUT_ATTRS.has(a.name)))
5675
+ (t.name === "font" && hasFontBreakoutAttr(t.attrs))
5295
5676
  ) {
5296
5677
  // parse error; pop until integration point / html / mathml-text-integration
5297
5678
  while (open.length > 1) {
5298
5679
  const c = cur();
5299
5680
  if (
5300
- c.namespace === NS_HTML ||
5681
+ _ns(c) === NS_HTML ||
5301
5682
  mathmlTextIntegrationPoint(c) ||
5302
5683
  htmlIntegrationPoint(c)
5303
5684
  ) {
@@ -5327,8 +5708,8 @@ const buildHtmlAst = (source, fragmentContext) => {
5327
5708
  if (t.type === TOKEN_END_TAG) {
5328
5709
  if (
5329
5710
  t.name === "script" &&
5330
- cur().tagName === "script" &&
5331
- cur().namespace === NS_SVG
5711
+ _tag(cur()) === "script" &&
5712
+ _ns(cur()) === NS_SVG
5332
5713
  ) {
5333
5714
  open.pop();
5334
5715
  return;
@@ -5338,7 +5719,7 @@ const buildHtmlAst = (source, fragmentContext) => {
5338
5719
  if (t.name === "p" || t.name === "br") {
5339
5720
  while (
5340
5721
  open.length > 1 &&
5341
- cur().namespace !== NS_HTML &&
5722
+ _ns(cur()) !== NS_HTML &&
5342
5723
  !mathmlTextIntegrationPoint(cur()) &&
5343
5724
  !htmlIntegrationPoint(cur())
5344
5725
  ) {
@@ -5350,21 +5731,18 @@ const buildHtmlAst = (source, fragmentContext) => {
5350
5731
  // any other end tag
5351
5732
  let i = open.length - 1;
5352
5733
  let node = open[i];
5353
- if (node.tagName.toLowerCase() !== t.name) {
5734
+ if (_tag(node).toLowerCase() !== t.name) {
5354
5735
  /* parse error */
5355
5736
  }
5356
5737
  while (i >= 0) {
5357
5738
  node = open[i];
5358
5739
  if (i === 0) return;
5359
- if (
5360
- node.namespace !== NS_HTML &&
5361
- node.tagName.toLowerCase() === t.name
5362
- ) {
5740
+ if (_ns(node) !== NS_HTML && _tag(node).toLowerCase() === t.name) {
5363
5741
  while (open.length > i) open.pop();
5364
5742
  return;
5365
5743
  }
5366
5744
  i--;
5367
- if (open[i] && open[i].namespace === NS_HTML) {
5745
+ if (open[i] && _ns(open[i]) === NS_HTML) {
5368
5746
  runMode(t);
5369
5747
  return;
5370
5748
  }
@@ -5379,11 +5757,7 @@ const buildHtmlAst = (source, fragmentContext) => {
5379
5757
  ) => {
5380
5758
  // step 1
5381
5759
  const c = cur();
5382
- if (
5383
- c.namespace === NS_HTML &&
5384
- c.tagName === subject &&
5385
- !afe.some((e) => !e.marker && e.element === c)
5386
- ) {
5760
+ if (_ns(c) === NS_HTML && _tag(c) === subject && !afe.includes(c)) {
5387
5761
  open.pop();
5388
5762
  return true;
5389
5763
  }
@@ -5393,17 +5767,14 @@ const buildHtmlAst = (source, fragmentContext) => {
5393
5767
  // find formatting element
5394
5768
  let fmtIdx = -1;
5395
5769
  for (let i = afe.length - 1; i >= 0; i--) {
5396
- if (afe[i].marker) break;
5397
- if (
5398
- afeEl(afe[i]).tagName === subject &&
5399
- afeEl(afe[i]).namespace === NS_HTML
5400
- ) {
5770
+ if (afe[i] === AFE_MARKER) break;
5771
+ if (_tag(afe[i]) === subject && _ns(afe[i]) === NS_HTML) {
5401
5772
  fmtIdx = i;
5402
5773
  break;
5403
5774
  }
5404
5775
  }
5405
5776
  if (fmtIdx === -1) return false; // act as any other end tag
5406
- const fmt = afeEl(afe[fmtIdx]);
5777
+ const fmt = afe[fmtIdx];
5407
5778
  const openIdx = open.indexOf(fmt);
5408
5779
  if (openIdx === -1) {
5409
5780
  afe.splice(fmtIdx, 1);
@@ -5435,10 +5806,7 @@ const buildHtmlAst = (source, fragmentContext) => {
5435
5806
  nodeIdx--;
5436
5807
  node = open[nodeIdx];
5437
5808
  if (node === fmt) break;
5438
- const nodeRef = node;
5439
- let nodeAfeIdx = afe.findIndex(
5440
- (e) => !e.marker && e.element === nodeRef
5441
- );
5809
+ let nodeAfeIdx = afe.indexOf(node);
5442
5810
  if (inner > 3 && nodeAfeIdx !== -1) {
5443
5811
  afe.splice(nodeAfeIdx, 1);
5444
5812
  if (nodeAfeIdx < bookmark) bookmark--;
@@ -5449,13 +5817,8 @@ const buildHtmlAst = (source, fragmentContext) => {
5449
5817
  continue;
5450
5818
  }
5451
5819
  // create clone
5452
- const clone = mkEl(
5453
- node.tagName,
5454
- node.namespace,
5455
- cloneAttrs(node.attributes),
5456
- null
5457
- );
5458
- afe[nodeAfeIdx] = { element: clone };
5820
+ const clone = mkEl(_tag(node), _ns(node), cloneAttrs(node), null);
5821
+ afe[nodeAfeIdx] = clone;
5459
5822
  open[nodeIdx] = clone;
5460
5823
  node = clone;
5461
5824
  if (lastNode === furthest) bookmark = nodeAfeIdx + 1;
@@ -5469,23 +5832,27 @@ const buildHtmlAst = (source, fragmentContext) => {
5469
5832
  const place = placeForCommonAncestor(commonAncestor);
5470
5833
  insertAtPlace(place, lastNode);
5471
5834
  // create element for fmt token, take children of furthest
5472
- const cloneFmt = mkEl(
5473
- fmt.tagName,
5474
- fmt.namespace,
5475
- cloneAttrs(fmt.attributes),
5476
- null
5477
- );
5478
- const kids = [...furthest.children];
5479
- furthest.children.length = 0;
5480
- for (const k of kids) appendTo(cloneFmt, k);
5835
+ const cloneFmt = mkEl(_tag(fmt), _ns(fmt), cloneAttrs(fmt), null);
5836
+ // Take all direct children of `furthest` (a template's content fragment
5837
+ // deliberately stays put — mirrors childrenOf-less spec behavior here).
5838
+ let k = _hFirst[furthest];
5839
+ _hFirst[furthest] = 0;
5840
+ _hLast[furthest] = 0;
5841
+ while (k !== 0) {
5842
+ const next = _hNext[k];
5843
+ _hNext[k] = 0;
5844
+ _hParent[k] = 0;
5845
+ appendTo(cloneFmt, k);
5846
+ k = next;
5847
+ }
5481
5848
  appendTo(furthest, cloneFmt);
5482
5849
  // remove fmt from afe, insert clone at bookmark
5483
- const curFmtIdx = afe.findIndex((e) => !e.marker && e.element === fmt);
5850
+ const curFmtIdx = afe.indexOf(fmt);
5484
5851
  if (curFmtIdx !== -1) {
5485
5852
  afe.splice(curFmtIdx, 1);
5486
5853
  if (curFmtIdx < bookmark) bookmark--;
5487
5854
  }
5488
- afe.splice(bookmark, 0, { element: cloneFmt });
5855
+ afe.splice(bookmark, 0, cloneFmt);
5489
5856
  // remove fmt from open, insert clone below furthest
5490
5857
  const ofi = open.indexOf(fmt);
5491
5858
  if (ofi !== -1) open.splice(ofi, 1);
@@ -5499,8 +5866,8 @@ const buildHtmlAst = (source, fragmentContext) => {
5499
5866
  /** @type {HtmlElement} */ commonAncestor
5500
5867
  ) => {
5501
5868
  if (
5502
- TABLE_CONTEXT.has(commonAncestor.tagName) &&
5503
- commonAncestor.namespace === NS_HTML
5869
+ TABLE_CONTEXT.has(_tag(commonAncestor)) &&
5870
+ _ns(commonAncestor) === NS_HTML
5504
5871
  ) {
5505
5872
  // foster
5506
5873
  // reuse appropriatePlace logic but rooted differently: emulate
@@ -5508,15 +5875,15 @@ const buildHtmlAst = (source, fragmentContext) => {
5508
5875
  let lastTable = -1;
5509
5876
  for (let i = open.length - 1; i >= 0; i--) {
5510
5877
  if (
5511
- open[i].tagName === "template" &&
5512
- open[i].namespace === NS_HTML &&
5878
+ _tag(open[i]) === "template" &&
5879
+ _ns(open[i]) === NS_HTML &&
5513
5880
  lastTemplate === -1
5514
5881
  ) {
5515
5882
  lastTemplate = i;
5516
5883
  }
5517
5884
  if (
5518
- open[i].tagName === "table" &&
5519
- open[i].namespace === NS_HTML &&
5885
+ _tag(open[i]) === "table" &&
5886
+ _ns(open[i]) === NS_HTML &&
5520
5887
  lastTable === -1
5521
5888
  ) {
5522
5889
  lastTable = i;
@@ -5526,23 +5893,32 @@ const buildHtmlAst = (source, fragmentContext) => {
5526
5893
  lastTemplate !== -1 &&
5527
5894
  (lastTable === -1 || lastTemplate > lastTable)
5528
5895
  ) {
5529
- return { parent: open[lastTemplate], beforeNode: null };
5896
+ return { parent: open[lastTemplate], beforeNode: 0 };
5530
5897
  }
5531
- if (lastTable === -1) return { parent: open[0], beforeNode: null };
5898
+ if (lastTable === -1) return { parent: open[0], beforeNode: 0 };
5532
5899
  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 };
5900
+ const tp = _hParent[table];
5901
+ if (tp !== 0) return { parent: tp, beforeNode: table };
5902
+ return { parent: open[lastTable - 1], beforeNode: 0 };
5536
5903
  }
5537
- return { parent: commonAncestor, beforeNode: null };
5904
+ return { parent: commonAncestor, beforeNode: 0 };
5538
5905
  };
5539
5906
 
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);
5907
+ const detach = (/** @type {HtmlNodeRef} */ node) => {
5908
+ const p = _hParent[node];
5909
+ if (p === 0) return;
5910
+ let prev = 0;
5911
+ let c = _hFirst[p];
5912
+ while (c !== 0 && c !== node) {
5913
+ prev = c;
5914
+ c = _hNext[c];
5915
+ }
5916
+ if (c === 0) return;
5917
+ if (prev === 0) _hFirst[p] = _hNext[node];
5918
+ else _hNext[prev] = _hNext[node];
5919
+ if (_hLast[p] === node) _hLast[p] = prev;
5920
+ _hNext[node] = 0;
5921
+ _hParent[node] = 0;
5546
5922
  };
5547
5923
 
5548
5924
  // ---------- insertion modes ----------
@@ -5700,18 +6076,20 @@ const buildHtmlAst = (source, fragmentContext) => {
5700
6076
  return;
5701
6077
  }
5702
6078
  if (t.type === TOKEN_COMMENT) {
5703
- insertComment(t.data, t.start, t.end, { parent: doc, beforeNode: null });
6079
+ insertComment(t.data, t.start, t.end, { parent: doc, beforeNode: 0 });
5704
6080
  return;
5705
6081
  }
5706
6082
  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
- });
6083
+ // `skip.doctype` drops the node only; quirks detection below is unaffected.
6084
+ if (!skipDoctype) {
6085
+ const dt = _hAlloc(NodeType.Doctype, t.start, t.end);
6086
+ _hStr[dt] = t.name;
6087
+ // At most one doctype node is ever inserted (later doctype tokens
6088
+ // are ignored), so its ids live in two per-parse scalars.
6089
+ _hDocPub = t.publicId;
6090
+ _hDocSys = t.systemId;
6091
+ _hAppend(doc, dt);
6092
+ }
5715
6093
  quirks = isQuirky(t.name, t.publicId, t.systemId);
5716
6094
  mode = MODE_BEFORE_HTML;
5717
6095
  return;
@@ -5724,7 +6102,7 @@ const buildHtmlAst = (source, fragmentContext) => {
5724
6102
  modes.beforeHtml = (t) => {
5725
6103
  if (t.type === TOKEN_DOCTYPE) return;
5726
6104
  if (t.type === TOKEN_COMMENT) {
5727
- insertComment(t.data, t.start, t.end, { parent: doc, beforeNode: null });
6105
+ insertComment(t.data, t.start, t.end, { parent: doc, beforeNode: 0 });
5728
6106
  return;
5729
6107
  }
5730
6108
  if (t.type === TOKEN_CHAR) {
@@ -5734,7 +6112,7 @@ const buildHtmlAst = (source, fragmentContext) => {
5734
6112
  }
5735
6113
  if (t.type === TOKEN_START_TAG && t.name === "html") {
5736
6114
  const el = mkEl("html", NS_HTML, t.attrs, t.pos);
5737
- doc.children.push(el);
6115
+ _hAppend(doc, el);
5738
6116
  open.push(el);
5739
6117
  mode = MODE_BEFORE_HEAD;
5740
6118
  return;
@@ -5742,8 +6120,8 @@ const buildHtmlAst = (source, fragmentContext) => {
5742
6120
  if (t.type === TOKEN_END_TAG && !HEAD_BODY_HTML_BR.has(t.name)) {
5743
6121
  return;
5744
6122
  }
5745
- const el = mkEl("html", NS_HTML, [], null);
5746
- doc.children.push(el);
6123
+ const el = mkEl("html", NS_HTML, EMPTY_ATTRS, null);
6124
+ _hAppend(doc, el);
5747
6125
  open.push(el);
5748
6126
  mode = MODE_BEFORE_HEAD;
5749
6127
  process(t);
@@ -5819,7 +6197,10 @@ const buildHtmlAst = (source, fragmentContext) => {
5819
6197
  mode = MODE_IN_TEMPLATE;
5820
6198
  templateModes.push(MODE_IN_TEMPLATE);
5821
6199
  const el = cur();
5822
- el.templateContent = { type: NodeType.DocumentFragment, children: [] };
6200
+ const fragment = _hAlloc(NodeType.DocumentFragment, 0, 0);
6201
+ // Parent link so the iterative walk can ascend out of the content.
6202
+ _hParent[fragment] = el;
6203
+ _hTc[el] = fragment;
5823
6204
  return;
5824
6205
  }
5825
6206
  if (t.name === "head") return;
@@ -5887,7 +6268,7 @@ const buildHtmlAst = (source, fragmentContext) => {
5887
6268
  if (t.type === TOKEN_CHAR) {
5888
6269
  const r = leadingWs(t, true);
5889
6270
  if (!r) return;
5890
- insertHtmlElement("body", [], null);
6271
+ insertHtmlElement("body", EMPTY_ATTRS, null);
5891
6272
  mode = MODE_IN_BODY;
5892
6273
  process(r);
5893
6274
  return;
@@ -5924,7 +6305,7 @@ const buildHtmlAst = (source, fragmentContext) => {
5924
6305
  if (t.name === "template") return modes.inHead(t);
5925
6306
  if (!BODY_HTML_BR.has(t.name)) return;
5926
6307
  }
5927
- insertHtmlElement("body", [], null);
6308
+ insertHtmlElement("body", EMPTY_ATTRS, null);
5928
6309
  mode = MODE_IN_BODY;
5929
6310
  process(t);
5930
6311
  };
@@ -5962,10 +6343,10 @@ const buildHtmlAst = (source, fragmentContext) => {
5962
6343
  const anyOtherEndTag = (/** @type {string} */ name) => {
5963
6344
  for (let i = open.length - 1; i >= 0; i--) {
5964
6345
  const node = open[i];
5965
- if (node.namespace === NS_HTML && node.tagName === name) {
6346
+ if (_ns(node) === NS_HTML && _tag(node) === name) {
5966
6347
  generateImpliedEndTags(name);
5967
6348
  while (open.length > i) {
5968
- open[open.length - 1].end = tokenEnd;
6349
+ _hEn[open[open.length - 1]] = tokenEnd;
5969
6350
  open.pop();
5970
6351
  }
5971
6352
  return;
@@ -5980,12 +6361,7 @@ const buildHtmlAst = (source, fragmentContext) => {
5980
6361
  if (open.some(isHtmlTemplateEl)) {
5981
6362
  return;
5982
6363
  }
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
- }
6364
+ mergeAttrs(open[0], t.attrs);
5989
6365
  return;
5990
6366
  }
5991
6367
  if (HEAD_ELEMENTS.has(name)) {
@@ -5993,20 +6369,16 @@ const buildHtmlAst = (source, fragmentContext) => {
5993
6369
  }
5994
6370
  if (name === "body") {
5995
6371
  const second = open[1];
5996
- if (!second || second.tagName !== "body" || open.some(isHtmlTemplateEl)) {
6372
+ if (!second || _tag(second) !== "body" || open.some(isHtmlTemplateEl)) {
5997
6373
  return;
5998
6374
  }
5999
6375
  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
- }
6376
+ mergeAttrs(second, t.attrs);
6005
6377
  return;
6006
6378
  }
6007
6379
  if (name === "frameset") {
6008
6380
  const second = open[1];
6009
- if (!second || second.tagName !== "body") return;
6381
+ if (!second || _tag(second) !== "body") return;
6010
6382
  if (!framesetOk) return;
6011
6383
  detach(second);
6012
6384
  while (open.length > 1) open.pop();
@@ -6021,7 +6393,7 @@ const buildHtmlAst = (source, fragmentContext) => {
6021
6393
  }
6022
6394
  if (HEADING.has(name)) {
6023
6395
  closeIfPInButtonScope();
6024
- if (cur().namespace === NS_HTML && HEADING.has(cur().tagName)) open.pop();
6396
+ if (_ns(cur()) === NS_HTML && HEADING.has(_tag(cur()))) open.pop();
6025
6397
  insertHtmlElement(name, t.attrs, t.pos);
6026
6398
  return;
6027
6399
  }
@@ -6047,14 +6419,14 @@ const buildHtmlAst = (source, fragmentContext) => {
6047
6419
  framesetOk = false;
6048
6420
  for (let i = open.length - 1; i >= 0; i--) {
6049
6421
  const node = open[i];
6050
- if (node.namespace === NS_HTML && node.tagName === "li") {
6422
+ if (_ns(node) === NS_HTML && _tag(node) === "li") {
6051
6423
  generateImpliedEndTags("li");
6052
6424
  popUntil("li");
6053
6425
  break;
6054
6426
  }
6055
6427
  if (
6056
6428
  isSpecial(node) &&
6057
- !(node.namespace === NS_HTML && ADDRESS_DIV_P.has(node.tagName))
6429
+ !(_ns(node) === NS_HTML && ADDRESS_DIV_P.has(_tag(node)))
6058
6430
  ) {
6059
6431
  break;
6060
6432
  }
@@ -6068,16 +6440,16 @@ const buildHtmlAst = (source, fragmentContext) => {
6068
6440
  for (let i = open.length - 1; i >= 0; i--) {
6069
6441
  const node = open[i];
6070
6442
  if (
6071
- node.namespace === NS_HTML &&
6072
- (node.tagName === "dd" || node.tagName === "dt")
6443
+ _ns(node) === NS_HTML &&
6444
+ (_tag(node) === "dd" || _tag(node) === "dt")
6073
6445
  ) {
6074
- generateImpliedEndTags(node.tagName);
6075
- popUntil(node.tagName);
6446
+ generateImpliedEndTags(_tag(node));
6447
+ popUntil(_tag(node));
6076
6448
  break;
6077
6449
  }
6078
6450
  if (
6079
6451
  isSpecial(node) &&
6080
- !(node.namespace === NS_HTML && ADDRESS_DIV_P.has(node.tagName))
6452
+ !(_ns(node) === NS_HTML && ADDRESS_DIV_P.has(_tag(node)))
6081
6453
  ) {
6082
6454
  break;
6083
6455
  }
@@ -6104,14 +6476,12 @@ const buildHtmlAst = (source, fragmentContext) => {
6104
6476
  if (name === "a") {
6105
6477
  // if there's an <a> in afe after last marker
6106
6478
  for (let i = afe.length - 1; i >= 0; i--) {
6107
- if (afe[i].marker) break;
6108
- if (afeEl(afe[i]).tagName === "a") {
6479
+ if (afe[i] === AFE_MARKER) break;
6480
+ if (_tag(afe[i]) === "a") {
6109
6481
  adoptionAgency("a", t.pos);
6110
- const idx = afe.findIndex(
6111
- (e) => !e.marker && afeEl(e).tagName === "a"
6112
- );
6482
+ const idx = afe.findIndex((e) => e !== AFE_MARKER && _tag(e) === "a");
6113
6483
  if (idx !== -1) {
6114
- const el = afeEl(afe[idx]);
6484
+ const el = afe[idx];
6115
6485
  afe.splice(idx, 1);
6116
6486
  const oi = open.indexOf(el);
6117
6487
  if (oi !== -1) open.splice(oi, 1);
@@ -6171,16 +6541,18 @@ const buildHtmlAst = (source, fragmentContext) => {
6171
6541
  resetInsertionMode();
6172
6542
  } else if (
6173
6543
  fragment &&
6174
- fragment.namespace === NS_HTML &&
6175
- fragment.tagName === "select"
6544
+ _ns(fragment) === NS_HTML &&
6545
+ _tag(fragment) === "select"
6176
6546
  ) {
6177
6547
  return;
6178
6548
  }
6179
6549
  reconstructAfe();
6180
6550
  insertHtmlElement("input", t.attrs, t.pos);
6181
6551
  open.pop();
6182
- const ty = findAttr(t.attrs, "type");
6183
- if (!ty || ty.value.toLowerCase() !== "hidden") framesetOk = false;
6552
+ const ty = _aFind(t.attrs.start, t.attrs.count, "type");
6553
+ if (ty === 0 || _aValueOf(ty).toLowerCase() !== "hidden") {
6554
+ framesetOk = false;
6555
+ }
6184
6556
  return;
6185
6557
  }
6186
6558
  if (PARAM_SOURCE_TRACK.has(name)) {
@@ -6189,8 +6561,8 @@ const buildHtmlAst = (source, fragmentContext) => {
6189
6561
  return;
6190
6562
  }
6191
6563
  if (name === "hr") {
6192
- if (cur().namespace === NS_HTML && cur().tagName === "option") open.pop();
6193
- if (cur().namespace === NS_HTML && cur().tagName === "optgroup") {
6564
+ if (_ns(cur()) === NS_HTML && _tag(cur()) === "option") open.pop();
6565
+ if (_ns(cur()) === NS_HTML && _tag(cur()) === "optgroup") {
6194
6566
  open.pop();
6195
6567
  }
6196
6568
  closeIfPInButtonScope();
@@ -6239,11 +6611,11 @@ const buildHtmlAst = (source, fragmentContext) => {
6239
6611
  return;
6240
6612
  }
6241
6613
  if (name === "optgroup" || name === "option") {
6242
- if (cur().namespace === NS_HTML && cur().tagName === "option") open.pop();
6614
+ if (_ns(cur()) === NS_HTML && _tag(cur()) === "option") open.pop();
6243
6615
  if (
6244
6616
  name === "optgroup" &&
6245
- cur().namespace === NS_HTML &&
6246
- cur().tagName === "optgroup"
6617
+ _ns(cur()) === NS_HTML &&
6618
+ _tag(cur()) === "optgroup"
6247
6619
  ) {
6248
6620
  open.pop();
6249
6621
  }
@@ -6283,20 +6655,13 @@ const buildHtmlAst = (source, fragmentContext) => {
6283
6655
  insertHtmlElement(name, t.attrs, t.pos);
6284
6656
  };
6285
6657
 
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
- }
6658
+ // Only `definitionurl` is rewritten (in place the run is consumed only by
6659
+ // this tag's element); the camelCase name serializes as itself.
6660
+ const adjustMathmlAttrs = (/** @type {AttributeRun} */ run) => {
6661
+ for (let i = run.start; i < run.start + run.count; i++) {
6662
+ if (_aName[i] === "definitionurl") _aName[i] = "definitionURL";
6298
6663
  }
6299
- return out;
6664
+ return run;
6300
6665
  };
6301
6666
 
6302
6667
  const endTagInBody = (/** @type {EndTagToken} */ t) => {
@@ -6323,7 +6688,7 @@ const buildHtmlAst = (source, fragmentContext) => {
6323
6688
  if (name === "form") {
6324
6689
  if (!open.some(isHtmlTemplateEl)) {
6325
6690
  const node = form;
6326
- form = null;
6691
+ form = 0;
6327
6692
  if (!node || !inScopeEl(node)) return;
6328
6693
  generateImpliedEndTags();
6329
6694
  const idx = open.indexOf(node);
@@ -6420,7 +6785,7 @@ const buildHtmlAst = (source, fragmentContext) => {
6420
6785
  // current element: a non-matching name (e.g. a fragment context), or
6421
6786
  // a name that ran straight to EOF with no delimiter (`</script` at
6422
6787
  // EOF — the tokenizer still emits a partial tag there).
6423
- if (cur() && (t.name !== cur().tagName || t.pos.end === t.pos.nameEnd)) {
6788
+ if (cur() && (t.name !== _tag(cur()) || t.pos.end === t.pos.nameEnd)) {
6424
6789
  insertCharacters(
6425
6790
  source.slice(t.pos.start, t.pos.end),
6426
6791
  t.pos.start,
@@ -6440,7 +6805,7 @@ const buildHtmlAst = (source, fragmentContext) => {
6440
6805
  modes.inTable = (t) => {
6441
6806
  if (t.type === TOKEN_CHAR) {
6442
6807
  const c = cur();
6443
- if (TABLE_CONTEXT.has(c.tagName) && c.namespace === NS_HTML) {
6808
+ if (TABLE_CONTEXT.has(_tag(c)) && _ns(c) === NS_HTML) {
6444
6809
  pendingTableChars = { list: [], hasNonWs: false };
6445
6810
  originalMode = mode;
6446
6811
  mode = MODE_IN_TABLE_TEXT;
@@ -6495,8 +6860,8 @@ const buildHtmlAst = (source, fragmentContext) => {
6495
6860
  return modes.inHead(t);
6496
6861
  }
6497
6862
  if (name === "input") {
6498
- const ty = findAttr(t.attrs, "type");
6499
- if (ty && ty.value.toLowerCase() === "hidden") {
6863
+ const ty = _aFind(t.attrs.start, t.attrs.count, "type");
6864
+ if (ty !== 0 && _aValueOf(ty).toLowerCase() === "hidden") {
6500
6865
  insertHtmlElement("input", t.attrs, t.pos);
6501
6866
  open.pop();
6502
6867
  return;
@@ -6566,7 +6931,7 @@ const buildHtmlAst = (source, fragmentContext) => {
6566
6931
  const clearStackToTableContext = () => {
6567
6932
  while (open.length) {
6568
6933
  const c = cur();
6569
- if (c.namespace === NS_HTML && CLEAR_TABLE.has(c.tagName)) {
6934
+ if (_ns(c) === NS_HTML && CLEAR_TABLE.has(_tag(c))) {
6570
6935
  break;
6571
6936
  }
6572
6937
  open.pop();
@@ -6575,7 +6940,7 @@ const buildHtmlAst = (source, fragmentContext) => {
6575
6940
  const clearStackToTableBodyContext = () => {
6576
6941
  while (open.length) {
6577
6942
  const c = cur();
6578
- if (c.namespace === NS_HTML && CLEAR_TABLE_BODY.has(c.tagName)) {
6943
+ if (_ns(c) === NS_HTML && CLEAR_TABLE_BODY.has(_tag(c))) {
6579
6944
  break;
6580
6945
  }
6581
6946
  open.pop();
@@ -6584,7 +6949,7 @@ const buildHtmlAst = (source, fragmentContext) => {
6584
6949
  const clearStackToTableRowContext = () => {
6585
6950
  while (open.length) {
6586
6951
  const c = cur();
6587
- if (c.namespace === NS_HTML && CLEAR_TABLE_ROW.has(c.tagName)) {
6952
+ if (_ns(c) === NS_HTML && CLEAR_TABLE_ROW.has(_tag(c))) {
6588
6953
  break;
6589
6954
  }
6590
6955
  open.pop();
@@ -6617,7 +6982,7 @@ const buildHtmlAst = (source, fragmentContext) => {
6617
6982
  if (t.type === TOKEN_CHAR) {
6618
6983
  const r = leadingWs(t, true);
6619
6984
  if (!r) return;
6620
- if (cur().tagName !== "colgroup") return;
6985
+ if (_tag(cur()) !== "colgroup") return;
6621
6986
  open.pop();
6622
6987
  mode = MODE_IN_TABLE;
6623
6988
  process(r);
@@ -6635,7 +7000,7 @@ const buildHtmlAst = (source, fragmentContext) => {
6635
7000
  return;
6636
7001
  }
6637
7002
  if (t.type === TOKEN_END_TAG && t.name === "colgroup") {
6638
- if (cur().tagName !== "colgroup") return;
7003
+ if (_tag(cur()) !== "colgroup") return;
6639
7004
  open.pop();
6640
7005
  mode = MODE_IN_TABLE;
6641
7006
  return;
@@ -6648,7 +7013,7 @@ const buildHtmlAst = (source, fragmentContext) => {
6648
7013
  return modes.inHead(t);
6649
7014
  }
6650
7015
  if (t.type === TOKEN_EOF) return modes.inBody(t);
6651
- if (cur().tagName !== "colgroup") return;
7016
+ if (_tag(cur()) !== "colgroup") return;
6652
7017
  open.pop();
6653
7018
  mode = MODE_IN_TABLE;
6654
7019
  process(t);
@@ -6798,7 +7163,7 @@ const buildHtmlAst = (source, fragmentContext) => {
6798
7163
  if (t.type === TOKEN_COMMENT) {
6799
7164
  insertComment(t.data, t.start, t.end, {
6800
7165
  parent: open[0],
6801
- beforeNode: null
7166
+ beforeNode: 0
6802
7167
  });
6803
7168
  return;
6804
7169
  }
@@ -6831,9 +7196,9 @@ const buildHtmlAst = (source, fragmentContext) => {
6831
7196
  return;
6832
7197
  }
6833
7198
  if (t.type === TOKEN_END_TAG && t.name === "frameset") {
6834
- if (cur().tagName === "html") return;
7199
+ if (_tag(cur()) === "html") return;
6835
7200
  open.pop();
6836
- if (!fragment && cur().tagName !== "frameset") mode = MODE_AFTER_FRAMESET;
7201
+ if (!fragment && _tag(cur()) !== "frameset") mode = MODE_AFTER_FRAMESET;
6837
7202
  return;
6838
7203
  }
6839
7204
  if (t.type === TOKEN_START_TAG && t.name === "frame") {
@@ -6869,7 +7234,7 @@ const buildHtmlAst = (source, fragmentContext) => {
6869
7234
 
6870
7235
  modes.afterAfterBody = (t) => {
6871
7236
  if (t.type === TOKEN_COMMENT) {
6872
- insertComment(t.data, t.start, t.end, { parent: doc, beforeNode: null });
7237
+ insertComment(t.data, t.start, t.end, { parent: doc, beforeNode: 0 });
6873
7238
  return;
6874
7239
  }
6875
7240
  if (t.type === TOKEN_DOCTYPE) return modes.inBody(t);
@@ -6882,7 +7247,7 @@ const buildHtmlAst = (source, fragmentContext) => {
6882
7247
 
6883
7248
  modes.afterAfterFrameset = (t) => {
6884
7249
  if (t.type === TOKEN_COMMENT) {
6885
- insertComment(t.data, t.start, t.end, { parent: doc, beforeNode: null });
7250
+ insertComment(t.data, t.start, t.end, { parent: doc, beforeNode: 0 });
6886
7251
  return;
6887
7252
  }
6888
7253
  if (t.type === TOKEN_DOCTYPE) return modes.inBody(t);
@@ -6904,9 +7269,9 @@ const buildHtmlAst = (source, fragmentContext) => {
6904
7269
  ctxNs = NS_MATHML;
6905
7270
  ctxName = ctxName.slice(5);
6906
7271
  }
6907
- fragment = mkEl(ctxName, ctxNs, [], null);
6908
- const htmlEl = mkEl("html", NS_HTML, [], null);
6909
- doc.children.push(htmlEl);
7272
+ fragment = mkEl(ctxName, ctxNs, EMPTY_ATTRS, null);
7273
+ const htmlEl = mkEl("html", NS_HTML, EMPTY_ATTRS, null);
7274
+ _hAppend(doc, htmlEl);
6910
7275
  open.push(htmlEl);
6911
7276
  if (ctxNs !== NS_HTML) {
6912
7277
  mode = MODE_IN_BODY;
@@ -6949,7 +7314,7 @@ const buildHtmlAst = (source, fragmentContext) => {
6949
7314
  type: TOKEN_EOF,
6950
7315
  name: "",
6951
7316
  data: "",
6952
- attrs: [],
7317
+ attrs: { start: 0, count: 0 },
6953
7318
  selfClosing: false,
6954
7319
  start: 0,
6955
7320
  end: 0,
@@ -6964,9 +7329,9 @@ const buildHtmlAst = (source, fragmentContext) => {
6964
7329
  walkHtmlTokens(source, 0, {
6965
7330
  isForeign: () => {
6966
7331
  const ac = adjustedCurrent();
6967
- return open.length > 0 && ac && ac.namespace !== NS_HTML;
7332
+ return open.length > 0 && ac !== 0 && _ns(ac) !== NS_HTML;
6968
7333
  },
6969
- fragmentContext: fragment ? fragment.tagName : undefined,
7334
+ fragmentContext: fragment ? _tag(fragment) : undefined,
6970
7335
  parseError: (input, code) => {
6971
7336
  if (code === "eof-in-tag") eofInTag = true;
6972
7337
  },
@@ -6988,7 +7353,7 @@ const buildHtmlAst = (source, fragmentContext) => {
6988
7353
  // The tokenizer emits CDATA sections through this callback too.
6989
7354
  if (input.startsWith("<![CDATA[", start)) {
6990
7355
  const ac = adjustedCurrent();
6991
- if (open.length > 0 && ac && ac.namespace !== NS_HTML) {
7356
+ if (open.length > 0 && ac && _ns(ac) !== NS_HTML) {
6992
7357
  const innerEnd = input.endsWith("]]>", end) ? end - 3 : end;
6993
7358
  const data = input.slice(start + 9, innerEnd).replace(/\r\n?/g, "\n");
6994
7359
  if (data !== "") {
@@ -7030,17 +7395,40 @@ const buildHtmlAst = (source, fragmentContext) => {
7030
7395
  return end;
7031
7396
  },
7032
7397
  text: (input, start, end) => {
7398
+ // `skip.text` fast path: node dropped, so only whitespace-ness matters.
7399
+ // With no `&`/`\0`/`\r` (and no pending newline-swallow) the decoded value
7400
+ // equals the raw range — dispatch a canonical marker (`" "`/`"x"`) without
7401
+ // the slice + entity decode; anything trickier falls through.
7402
+ if (skipText && !swallowNextNewline) {
7403
+ let hasNonWs = false;
7404
+ let i = start;
7405
+ for (; i < end; i++) {
7406
+ const c = input.charCodeAt(i);
7407
+ // 2 = break (& / NUL / CR), 1 = whitespace, 0 = other.
7408
+ const cls = c < 128 ? _TEXT_SCAN_CLASS[c] : 0;
7409
+ if (cls === 2) break;
7410
+ if (cls === 0) hasNonWs = true;
7411
+ }
7412
+ if (i === end) {
7413
+ tok.type = TOKEN_CHAR;
7414
+ tok.data = hasNonWs ? "x" : " ";
7415
+ tok.start = start;
7416
+ tok.end = end;
7417
+ dispatch();
7418
+ return end;
7419
+ }
7420
+ }
7033
7421
  const raw = input.slice(start, end);
7034
7422
  // CR normalization only when a CR is actually present (common case has none).
7035
7423
  const s = !raw.includes("\r") ? raw : raw.replace(/\r\n?/g, "\n");
7036
7424
  const top = adjustedCurrent();
7037
7425
  const rawMode =
7038
7426
  mode === MODE_TEXT ||
7039
- (top && top.namespace === NS_HTML && top.tagName === "plaintext");
7427
+ (top && _ns(top) === NS_HTML && _tag(top) === "plaintext");
7040
7428
  const noDecode =
7041
7429
  top &&
7042
- top.namespace === NS_HTML &&
7043
- NO_DECODE_TEXT.has(top.tagName) &&
7430
+ _ns(top) === NS_HTML &&
7431
+ NO_DECODE_TEXT.has(_tag(top)) &&
7044
7432
  (mode === MODE_TEXT || mode === MODE_IN_BODY);
7045
7433
  let data = noDecode ? s : decode(s, false);
7046
7434
  // In RAWTEXT/RCDATA/script/PLAINTEXT, NULL becomes U+FFFD (tokenizer
@@ -7061,32 +7449,29 @@ const buildHtmlAst = (source, fragmentContext) => {
7061
7449
  },
7062
7450
  attribute: (input, nameStart, nameEnd, valueStart, valueEnd, quoteType) => {
7063
7451
  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) : "";
7452
+ if (pendAttrStart === 0) pendAttrStart = _aN + 1;
7068
7453
  // Drop duplicate attribute names (per spec). Plain loop avoids a
7069
7454
  // per-attribute closure allocation on this hot path.
7070
7455
  let dup = false;
7071
- for (let i = 0; i < pendingAttrs.length; i++) {
7072
- if (pendingAttrs[i].name === name) {
7456
+ for (let i = pendAttrStart; i <= _aN; i++) {
7457
+ if (_aName[i] === name) {
7073
7458
  dup = true;
7074
7459
  break;
7075
7460
  }
7076
7461
  }
7077
7462
  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({
7463
+ // The raw (undecoded) value is read from the source by offset on
7464
+ // demand: consumers re-resolve requests from it and the offsets must
7465
+ // stay aligned with the source. Only a valueless attribute stores an
7466
+ // override ("").
7467
+ _aAlloc(
7082
7468
  name,
7083
- value,
7084
- serializedName: undefined,
7469
+ valueStart !== -1 ? null : "",
7085
7470
  nameStart,
7086
7471
  nameEnd,
7087
7472
  valueStart,
7088
7473
  valueEnd
7089
- });
7474
+ );
7090
7475
  }
7091
7476
  if (valueStart === -1) return nameEnd;
7092
7477
  return quoteType !== QUOTE_NONE ? valueEnd + 1 : valueEnd;
@@ -7095,25 +7480,19 @@ const buildHtmlAst = (source, fragmentContext) => {
7095
7480
  // A start tag the tokenizer only emitted because it hit EOF mid-tag
7096
7481
  // is dropped, matching the spec's eof-in-tag handling.
7097
7482
  if (eofInTag) {
7098
- if (pendingAttrs.length !== 0) pendingAttrs = [];
7483
+ // Any attribute slots already allocated for it are orphaned.
7484
+ pendAttrStart = 0;
7099
7485
  return end;
7100
7486
  }
7101
7487
  const name = internLowerName(TAG_NAME_INTERN, input, nameStart, nameEnd);
7102
7488
  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
7489
  tok.type = TOKEN_START_TAG;
7115
7490
  tok.name = name;
7116
- tok.attrs = attrs;
7491
+ // The reused token carries the tag's attribute run; every consumer of
7492
+ // `t.attrs` runs synchronously within this dispatch.
7493
+ tok.attrs.start = pendAttrStart;
7494
+ tok.attrs.count = pendAttrStart === 0 ? 0 : _aN + 1 - pendAttrStart;
7495
+ pendAttrStart = 0;
7117
7496
  tok.selfClosing = selfClosing;
7118
7497
  tok.swallowNewline = false;
7119
7498
  tok.pos.start = start;
@@ -7126,8 +7505,8 @@ const buildHtmlAst = (source, fragmentContext) => {
7126
7505
  },
7127
7506
  closeTag: (input, start, end, nameStart, nameEnd) => {
7128
7507
  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 = [];
7508
+ // End tags drop any parsed attributes (the slots are orphaned).
7509
+ pendAttrStart = 0;
7131
7510
  tok.type = TOKEN_END_TAG;
7132
7511
  tok.name = name;
7133
7512
  tok.pos.start = start;
@@ -7141,63 +7520,70 @@ const buildHtmlAst = (source, fragmentContext) => {
7141
7520
  tok.type = TOKEN_EOF;
7142
7521
  dispatch();
7143
7522
 
7144
- if (sawSelectedContent) mirrorSelectedContent(doc, null);
7523
+ if (sawSelectedContent) mirrorSelectedContent(doc, 0);
7145
7524
 
7146
7525
  return doc;
7147
7526
  };
7148
7527
 
7149
7528
  /**
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
7529
+ * Deep-clone a node into fresh ids, dropping attribute source offsets and the
7530
+ * raw-text body span so cloned content does not re-emit dependencies.
7531
+ * @param {HtmlNodeRef} node node
7532
+ * @returns {HtmlNodeRef} clone
7154
7533
  */
7155
7534
  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
- };
7535
+ const ty = _hTy[node];
7536
+ const clone = _hAlloc(ty, _hSt[node], _hEn[node]);
7537
+ _hStr[clone] = _hStr[node];
7538
+ if (ty !== NodeType.Element) return clone;
7539
+ _hFl[clone] = _hFl[node];
7540
+ const attrs = cloneAttrs(node);
7541
+ _hAStart[clone] = attrs.start;
7542
+ _hACount[clone] = attrs.count;
7543
+ _hTagEnd[clone] = _hTagEnd[node];
7544
+ _hNameEnd[clone] = _hNameEnd[node];
7545
+ // Empty body span: the clone re-emits no raw-text dependency.
7546
+ _hCEnd[clone] = _hTagEnd[node];
7547
+ for (let k = _hFirst[node]; k !== 0; k = _hNext[k]) {
7548
+ _hAppend(clone, cloneSubtree(k));
7549
+ }
7550
+ // Clone `<template>` content into the clone's own fragment.
7551
+ const tc = _hTc[node];
7552
+ if (tc !== 0) {
7553
+ const fragment = _hAlloc(NodeType.DocumentFragment, 0, 0);
7554
+ // Parent link so the iterative walk can ascend out of the content.
7555
+ _hParent[fragment] = clone;
7556
+ _hTc[clone] = fragment;
7557
+ for (let k = _hFirst[tc]; k !== 0; k = _hNext[k]) {
7558
+ _hAppend(fragment, cloneSubtree(k));
7559
+ }
7560
+ }
7561
+ return clone;
7177
7562
  };
7178
7563
 
7179
7564
  /**
7180
7565
  * The selected option of a select: the last `<option selected>`, else the
7181
7566
  * first option (scanning direct children and `<optgroup>` children).
7182
7567
  * @param {HtmlElement} select select element
7183
- * @returns {HtmlElement | null} selected option
7568
+ * @returns {HtmlElement} selected option (0 = none)
7184
7569
  */
7185
7570
  const selectedOption = (select) => {
7186
7571
  /** @type {HtmlElement[]} */
7187
7572
  const options = [];
7188
7573
  /** @param {HtmlElement} el element */
7189
7574
  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);
7575
+ for (let c = _hFirst[el]; c !== 0; c = _hNext[c]) {
7576
+ if (_hTy[c] !== NodeType.Element || _ns(c) !== NS_HTML) continue;
7577
+ if (_hStr[c] === "option") options.push(c);
7578
+ else if (_hStr[c] === "optgroup") collect(c);
7194
7579
  }
7195
7580
  };
7196
7581
  collect(select);
7197
- if (options.length === 0) return null;
7582
+ if (options.length === 0) return 0;
7198
7583
  for (let i = options.length - 1; i >= 0; i--) {
7199
- if (options[i].attributes.some((a) => a.name === "selected")) {
7200
- return options[i];
7584
+ const el = options[i];
7585
+ if (_aFind(_hAStart[el], _hACount[el], "selected") !== 0) {
7586
+ return el;
7201
7587
  }
7202
7588
  }
7203
7589
  return options[0];
@@ -7206,25 +7592,31 @@ const selectedOption = (select) => {
7206
7592
  /**
7207
7593
  * Fill each `<selectedcontent>` with a clone of its `<select>`'s selected
7208
7594
  * option subtree (the customizable-select mirroring behavior).
7209
- * @param {HtmlElement | HtmlDocument | HtmlDocumentFragment} node node
7210
- * @param {HtmlElement | null} select nearest ancestor select
7595
+ * @param {HtmlNodeRef} node node
7596
+ * @param {HtmlElement} select nearest ancestor select (0 = none)
7211
7597
  */
7212
7598
  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") {
7599
+ // A `<template>`'s children live in its content fragment.
7600
+ const tc = _hTc[node];
7601
+ const container = tc !== 0 ? tc : node;
7602
+ for (let child = _hFirst[container]; child !== 0; child = _hNext[child]) {
7603
+ if (_hTy[child] !== NodeType.Element) continue;
7604
+ if (_ns(child) === NS_HTML && _hStr[child] === "select") {
7220
7605
  mirrorSelectedContent(child, child);
7221
7606
  } else if (
7222
- select &&
7223
- child.namespace === NS_HTML &&
7224
- child.tagName === "selectedcontent"
7607
+ select !== 0 &&
7608
+ _ns(child) === NS_HTML &&
7609
+ _hStr[child] === "selectedcontent"
7225
7610
  ) {
7226
7611
  const option = selectedOption(select);
7227
- if (option) child.children = option.children.map(cloneSubtree);
7612
+ if (option !== 0) {
7613
+ // Replace the children with clones of the option's subtree.
7614
+ _hFirst[child] = 0;
7615
+ _hLast[child] = 0;
7616
+ for (let k = _hFirst[option]; k !== 0; k = _hNext[k]) {
7617
+ _hAppend(child, cloneSubtree(k));
7618
+ }
7619
+ }
7228
7620
  } else {
7229
7621
  mirrorSelectedContent(child, select);
7230
7622
  }
@@ -7257,37 +7649,20 @@ const parseDoctype = (/** @type {string} */ raw) => {
7257
7649
  return { name, publicId, systemId };
7258
7650
  };
7259
7651
 
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
7652
  /** @typedef {HtmlNode | HtmlDocument | HtmlDocumentFragment} HtmlVisitableNode */
7277
7653
 
7278
7654
  // HTML-typed views over the generic visitor machinery (`util/SourceProcessor`).
7279
7655
  /**
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
7656
+ * @typedef {import("../util/SourceProcessor").VisitorFn<HtmlPath>} VisitorFn
7657
+ * @typedef {import("../util/SourceProcessor").VisitorBucket<HtmlPath>} VisitorBucket
7658
+ * @typedef {import("../util/SourceProcessor").VisitorMap<HtmlPath>} VisitorMap
7659
+ * @typedef {import("../util/SourceProcessor").CompiledVisitorMap<HtmlPath>} CompiledVisitorMap
7285
7660
  */
7286
7661
 
7287
7662
  /**
7288
7663
  * @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
7664
+ * @property {string=} fragmentContext context element tag name for fragment parsing (see `buildHtmlAst`); the HTML analog of the CSS parser's `as` parse-mode option
7665
+ * @property {HtmlAstSkip=} skip node kinds to omit from the AST for speed/memory (see `HtmlAstSkip`)
7291
7666
  */
7292
7667
 
7293
7668
  /**
@@ -7299,54 +7674,97 @@ const NodeType = {
7299
7674
  * @param {HtmlProcessOptions} options process options
7300
7675
  */
7301
7676
  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
- }
7677
+ const root = buildHtmlAst(input, options.fragmentContext, options.skip);
7678
+
7679
+ // Iterative depth-first walk over the link columns: `firstChild`
7680
+ // / `nextSibling` descend, the `parent` column ascends, so arbitrarily deep
7681
+ // markup can't overflow the call stack (the old recursive walk died at
7682
+ // ~10⁵ nesting). A `<template>`'s content fragment is visited before the
7683
+ // element's children; ascending out of it continues with those children
7684
+ // (the fragment is never in a sibling chain, `_hNext` = 0).
7685
+ /**
7686
+ * Fire a node's `enter` visitors; true when the walk may descend.
7687
+ * @param {HtmlNodeRef} node node
7688
+ * @returns {boolean} false when a visitor called `skipChildren()`
7689
+ */
7690
+ const enter = (node) => {
7691
+ const b = visitors[_hTy[node]];
7692
+ if (b === undefined || b.enter.length === 0) return true;
7693
+ _walkSkip = false;
7694
+ _curNode = node;
7695
+ const p = _hParent[node];
7696
+ _curParent = p === 0 ? null : p;
7697
+ const e = b.enter;
7698
+ for (let i = 0; i < e.length; i++) e[i](A);
7699
+ const skip = _walkSkip;
7700
+ _walkSkip = false;
7701
+ return !skip;
7308
7702
  };
7309
-
7310
7703
  /**
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
7704
+ * Fire a node's `exit` visitors.
7705
+ * @param {HtmlNodeRef} node node
7315
7706
  */
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.
7707
+ const exit = (node) => {
7708
+ const b = visitors[_hTy[node]];
7709
+ if (b === undefined) return;
7710
+ _curNode = node;
7711
+ const p = _hParent[node];
7712
+ _curParent = p === 0 ? null : p;
7713
+ const x = b.exit;
7714
+ for (let i = 0; i < x.length; i++) x[i](A);
7715
+ };
7716
+ /**
7717
+ * First node to visit inside `node` (template content before children).
7718
+ * @param {HtmlNodeRef} node node
7719
+ * @returns {HtmlNodeRef} first inner node (0 = leaf)
7720
+ */
7721
+ const firstInner = (node) => {
7722
+ const ty = _hTy[node];
7723
+ if (ty === NodeType.Element) {
7724
+ const tc = _hTc[node];
7725
+ return tc !== 0 ? tc : _hFirst[node];
7342
7726
  }
7343
- if (b !== undefined) {
7344
- const x = b.exit;
7345
- for (let i = 0; i < x.length; i++) x[i](node, parent, visitorCtx);
7727
+ if (ty === NodeType.Document || ty === NodeType.DocumentFragment) {
7728
+ return _hFirst[node];
7346
7729
  }
7730
+ return 0;
7347
7731
  };
7348
7732
 
7349
- walk(options.ast || buildHtmlAst(input, options.fragmentContext), null);
7733
+ let node = root;
7734
+ descend: for (;;) {
7735
+ if (enter(node)) {
7736
+ const inner = firstInner(node);
7737
+ if (inner !== 0) {
7738
+ node = inner;
7739
+ continue;
7740
+ }
7741
+ }
7742
+ // Leaf (or skipped): exit and move sideways / upwards.
7743
+ for (;;) {
7744
+ exit(node);
7745
+ if (node === root) break descend;
7746
+ const parent = _hParent[node];
7747
+ // Out of a template's content fragment: the element's children follow.
7748
+ if (_hTc[parent] === node) {
7749
+ const first = _hFirst[parent];
7750
+ if (first !== 0) {
7751
+ node = first;
7752
+ continue descend;
7753
+ }
7754
+ } else {
7755
+ const sibling = _hNext[node];
7756
+ if (sibling !== 0) {
7757
+ node = sibling;
7758
+ continue descend;
7759
+ }
7760
+ }
7761
+ node = parent;
7762
+ }
7763
+ }
7764
+
7765
+ // The walk consumed the tree: release the side arrays' heap references so
7766
+ // the reused columns don't pin this parse's strings until the next parse.
7767
+ _hRelease();
7350
7768
  };
7351
7769
 
7352
7770
  /**
@@ -7354,10 +7772,10 @@ const grammar = (input, visitors, options) => {
7354
7772
  * `grammar`. Babel-style usage:
7355
7773
  *
7356
7774
  * ```
7357
- * processor.use({ [NodeType.Element]: (el) => {}, [NodeType.Comment]: { enter, exit } });
7775
+ * processor.use({ [NodeType.Element]: (path) => {}, [NodeType.Comment]: { enter, exit } });
7358
7776
  * processor.process(source);
7359
7777
  * ```
7360
- * @extends {GenericSourceProcessor<HtmlVisitableNode, HtmlProcessOptions>}
7778
+ * @extends {GenericSourceProcessor<HtmlPath, HtmlProcessOptions>}
7361
7779
  */
7362
7780
  class SourceProcessor extends GenericSourceProcessor {
7363
7781
  constructor() {
@@ -7377,12 +7795,14 @@ const SMALL_LETTER_W = "w".charCodeAt(0);
7377
7795
  const SMALL_LETTER_X = "x".charCodeAt(0);
7378
7796
  const SMALL_LETTER_H = "h".charCodeAt(0);
7379
7797
  // (Don't use \s, to avoid matching non-breaking space)
7798
+ // Sticky so `collectCharacters` matches at an offset without slicing the
7799
+ // whole remaining input per call (which made srcset parsing quadratic).
7380
7800
  // eslint-disable-next-line no-control-regex
7381
- const LEADING_SPACES_REGEXP = /^[ \t\n\r\u000C]+/;
7801
+ const LEADING_SPACES_REGEXP = /[ \t\n\r\u000C]+/y;
7382
7802
  // eslint-disable-next-line no-control-regex
7383
- const LEADING_COMMAS_OR_SPACES_REGEXP = /^[, \t\n\r\u000C]+/;
7803
+ const LEADING_COMMAS_OR_SPACES_REGEXP = /[, \t\n\r\u000C]+/y;
7384
7804
  // eslint-disable-next-line no-control-regex
7385
- const LEADING_NOT_SPACES = /^[^ \t\n\r\u000C]+/;
7805
+ const LEADING_NOT_SPACES = /[^ \t\n\r\u000C]+/y;
7386
7806
  const TRAILING_COMMAS_REGEXP = /[,]+$/;
7387
7807
  const NON_NEGATIVE_INTEGER_REGEXP = /^\d+$/;
7388
7808
  // ( Positive or negative or unsigned integers or decimals, without or without exponents.
@@ -7405,8 +7825,8 @@ const parseSrcset = (input) => {
7405
7825
  let url;
7406
7826
  /** @type {string[]} */
7407
7827
  let descriptors;
7408
- /** @type {string} */
7409
- let currentDescriptor;
7828
+ /** @type {number} */
7829
+ let descriptorStart;
7410
7830
  /** @type {string} */
7411
7831
  let state;
7412
7832
  /** @type {number} */
@@ -7420,16 +7840,15 @@ const parseSrcset = (input) => {
7420
7840
  const candidates = [];
7421
7841
 
7422
7842
  /**
7423
- * @param {RegExp} regExp reg exp to collect characters
7843
+ * @param {RegExp} regExp sticky reg exp to collect characters
7424
7844
  * @returns {string | undefined} characters
7425
7845
  */
7426
7846
  function collectCharacters(regExp) {
7427
- /** @type {string} */
7428
- let chars;
7429
- const match = regExp.exec(input.slice(Math.max(0, position)));
7847
+ regExp.lastIndex = Math.max(0, position);
7848
+ const match = regExp.exec(input);
7430
7849
 
7431
7850
  if (match) {
7432
- [chars] = match;
7851
+ const [chars] = match;
7433
7852
  position += chars.length;
7434
7853
 
7435
7854
  return chars;
@@ -7560,7 +7979,8 @@ const parseSrcset = (input) => {
7560
7979
  collectCharacters(LEADING_SPACES_REGEXP);
7561
7980
 
7562
7981
  // 8.2. Let current descriptor be the empty string.
7563
- currentDescriptor = "";
7982
+ // (Tracked as a start offset, `-1` = empty; sliced once per descriptor.)
7983
+ descriptorStart = -1;
7564
7984
 
7565
7985
  // 8.3. Let state be in descriptor.
7566
7986
  state = "in descriptor";
@@ -7582,9 +8002,9 @@ const parseSrcset = (input) => {
7582
8002
  // descriptors and let current descriptor be the empty string.
7583
8003
  // Set state to after descriptor.
7584
8004
  if (isSpace(charCode)) {
7585
- if (currentDescriptor) {
7586
- descriptors.push(currentDescriptor);
7587
- currentDescriptor = "";
8005
+ if (descriptorStart !== -1) {
8006
+ descriptors.push(input.slice(descriptorStart, position));
8007
+ descriptorStart = -1;
7588
8008
  state = "after descriptor";
7589
8009
  }
7590
8010
  }
@@ -7595,8 +8015,8 @@ const parseSrcset = (input) => {
7595
8015
  else if (charCode === COMMA) {
7596
8016
  position += 1;
7597
8017
 
7598
- if (currentDescriptor) {
7599
- descriptors.push(currentDescriptor);
8018
+ if (descriptorStart !== -1) {
8019
+ descriptors.push(input.slice(descriptorStart, position - 1));
7600
8020
  }
7601
8021
 
7602
8022
  parseDescriptors();
@@ -7606,15 +8026,15 @@ const parseSrcset = (input) => {
7606
8026
  // U+0028 LEFT PARENTHESIS (()
7607
8027
  // Append charCode to current descriptor. Set state to in parens.
7608
8028
  else if (charCode === LEFT_PARENTHESIS) {
7609
- currentDescriptor += input.charAt(position);
8029
+ if (descriptorStart === -1) descriptorStart = position;
7610
8030
  state = "in parens";
7611
8031
  }
7612
8032
  // EOF
7613
8033
  // If current descriptor is not empty, append current descriptor to
7614
8034
  // descriptors. Jump to the step labeled descriptor parser.
7615
8035
  else if (Number.isNaN(charCode)) {
7616
- if (currentDescriptor) {
7617
- descriptors.push(currentDescriptor);
8036
+ if (descriptorStart !== -1) {
8037
+ descriptors.push(input.slice(descriptorStart, position));
7618
8038
  }
7619
8039
 
7620
8040
  parseDescriptors();
@@ -7623,8 +8043,8 @@ const parseSrcset = (input) => {
7623
8043
 
7624
8044
  // Anything else
7625
8045
  // Append charCode to current descriptor.
7626
- } else {
7627
- currentDescriptor += input.charAt(position);
8046
+ } else if (descriptorStart === -1) {
8047
+ descriptorStart = position;
7628
8048
  }
7629
8049
  }
7630
8050
  // In parens
@@ -7632,22 +8052,18 @@ const parseSrcset = (input) => {
7632
8052
  // U+0029 RIGHT PARENTHESIS ())
7633
8053
  // Append charCode to current descriptor. Set state to in descriptor.
7634
8054
  if (charCode === RIGHT_PARENTHESIS) {
7635
- currentDescriptor += input.charAt(position);
7636
8055
  state = "in descriptor";
7637
8056
  }
7638
8057
  // EOF
7639
8058
  // Append current descriptor to descriptors. Jump to the step labeled
7640
8059
  // descriptor parser.
7641
8060
  else if (Number.isNaN(charCode)) {
7642
- descriptors.push(currentDescriptor);
8061
+ descriptors.push(input.slice(descriptorStart, position));
7643
8062
  parseDescriptors();
7644
8063
  return;
7645
8064
  }
7646
8065
  // Anything else
7647
- // Append charCode to current descriptor.
7648
- else {
7649
- currentDescriptor += input.charAt(position);
7650
- }
8066
+ // Append charCode to current descriptor. (Covered by the tracked range.)
7651
8067
  }
7652
8068
  // After descriptor
7653
8069
  else if (state === "after descriptor") {
@@ -7719,6 +8135,270 @@ const parseSrcset = (input) => {
7719
8135
  }
7720
8136
  };
7721
8137
 
8138
+ // Babel's `path.skip()`, children-only: set by `A.skipChildren()` during an
8139
+ // `enter` dispatch, consumed by the walk.
8140
+ let _walkSkip = false;
8141
+ // The walk's current position (`A.node` / `A.parent` read these; module-level
8142
+ // so the accessor methods' defaults avoid self-referential `this` typing).
8143
+ /** @type {HtmlNodeRef} */
8144
+ let _curNode = 0;
8145
+ /** @type {HtmlNodeRef | null} */
8146
+ let _curParent = null;
8147
+
8148
+ /* eslint-disable jsdoc/require-template -- `A` below is the accessor const, not a type parameter */
8149
+ /**
8150
+ * The HTML path (Babel's `path` shape): the AST accessor with the walk's
8151
+ * current position on it — the single argument every visitor receives.
8152
+ * @typedef {typeof A} HtmlPath
8153
+ */
8154
+ /* eslint-enable jsdoc/require-template */
8155
+
8156
+ // AST field-access seam (mirrors the CSS parser's `A`): every AST field a
8157
+ // consumer reads goes through one of these accessors, so the node
8158
+ // representation can change underneath without touching consumers. `n` is an
8159
+ // `HtmlNodeRef`; results are valid until the next `buildHtmlAst` call.
8160
+ const A = {
8161
+ // === path position (rebound by the walk before every visitor call) ===
8162
+ /**
8163
+ * @returns {HtmlNodeRef} current node — only valid during a visitor callback
8164
+ */
8165
+ get node() {
8166
+ return _curNode;
8167
+ },
8168
+ /**
8169
+ * @returns {HtmlNodeRef | null} enclosing node (null = the document root)
8170
+ */
8171
+ get parent() {
8172
+ return _curParent;
8173
+ },
8174
+ /** Stop the walk descending into the current node (enter only). */
8175
+ skipChildren() {
8176
+ _walkSkip = true;
8177
+ },
8178
+ // === field reads — `n` defaults to the current node ===
8179
+ /**
8180
+ * @param {HtmlNodeRef=} n node
8181
+ * @returns {number} `NodeType`
8182
+ */
8183
+ type(n = _curNode) {
8184
+ return _hTy[n];
8185
+ },
8186
+ /**
8187
+ * @param {HtmlNodeRef=} n node
8188
+ * @returns {number} start offset
8189
+ */
8190
+ start(n = _curNode) {
8191
+ return _hSt[n];
8192
+ },
8193
+ /**
8194
+ * @param {HtmlNodeRef=} n node
8195
+ * @returns {number} end offset
8196
+ */
8197
+ end(n = _curNode) {
8198
+ return _hEn[n];
8199
+ },
8200
+ /**
8201
+ * @param {HtmlElement=} n element
8202
+ * @returns {string} lowercased (foreign-content: adjusted) tag name
8203
+ */
8204
+ tagName(n = _curNode) {
8205
+ return _hStr[n];
8206
+ },
8207
+ /**
8208
+ * @param {HtmlElement=} n element
8209
+ * @returns {number} `NS_*` namespace
8210
+ */
8211
+ namespace(n = _curNode) {
8212
+ return _hFl[n] & NS_MASK;
8213
+ },
8214
+ /**
8215
+ * @param {HtmlElement=} n element
8216
+ * @returns {boolean} true for void elements
8217
+ */
8218
+ selfClosing(n = _curNode) {
8219
+ return (_hFl[n] & FLAG_SELF_CLOSING) !== 0;
8220
+ },
8221
+ // materialized attribute list — test/tooling convenience, allocates; the
8222
+ // parser reads attributes through the scalar accessors below
8223
+ /**
8224
+ * @param {HtmlElement=} n element
8225
+ * @returns {HtmlAttribute[]} materialized attributes
8226
+ */
8227
+ attributes(n = _curNode) {
8228
+ const out = [];
8229
+ const start = _hAStart[n];
8230
+ for (let i = start; i < start + _hACount[n]; i++) {
8231
+ out.push({
8232
+ name: _aName[i],
8233
+ value: _aValueOf(i),
8234
+ serializedName: _aSerializedName(i),
8235
+ nameStart: _aNameStart[i],
8236
+ nameEnd: _aNameEnd[i],
8237
+ valueStart: _aValStart[i],
8238
+ valueEnd: _aValEnd[i]
8239
+ });
8240
+ }
8241
+ return out;
8242
+ },
8243
+ /**
8244
+ * @param {HtmlElement=} n element
8245
+ * @returns {number} attribute count
8246
+ */
8247
+ attributeCount(n = _curNode) {
8248
+ return _hACount[n];
8249
+ },
8250
+ /**
8251
+ * The i-th attribute of an element, as an id for the `attribute*` reads.
8252
+ * @param {number} i attribute index
8253
+ * @param {HtmlElement=} n element
8254
+ * @returns {HtmlAttributeRef} attribute ref
8255
+ */
8256
+ attributeAt(i, n = _curNode) {
8257
+ return _hAStart[n] + i;
8258
+ },
8259
+ /**
8260
+ * Linear lookup by (lowercased) name.
8261
+ * @param {string} name attribute name
8262
+ * @param {HtmlElement=} n element
8263
+ * @returns {HtmlAttributeRef} attribute ref (0 = not present)
8264
+ */
8265
+ findAttribute(name, n = _curNode) {
8266
+ return _aFind(_hAStart[n], _hACount[n], name);
8267
+ },
8268
+ /**
8269
+ * @param {HtmlAttributeRef} a attribute ref
8270
+ * @returns {string} lowercased (foreign-content: adjusted) attribute name
8271
+ */
8272
+ attributeName(a) {
8273
+ return _aName[a];
8274
+ },
8275
+ /**
8276
+ * @param {HtmlAttributeRef} a attribute ref
8277
+ * @returns {string} raw (undecoded) attribute value ("" when valueless)
8278
+ */
8279
+ attributeValue(a) {
8280
+ return _aValueOf(a);
8281
+ },
8282
+ /**
8283
+ * @param {HtmlAttributeRef} a attribute ref
8284
+ * @returns {number} name start offset
8285
+ */
8286
+ attributeNameStart(a) {
8287
+ return _aNameStart[a];
8288
+ },
8289
+ /**
8290
+ * @param {HtmlAttributeRef} a attribute ref
8291
+ * @returns {number} name end offset
8292
+ */
8293
+ attributeNameEnd(a) {
8294
+ return _aNameEnd[a];
8295
+ },
8296
+ /**
8297
+ * @param {HtmlAttributeRef} a attribute ref
8298
+ * @returns {number} value start offset (-1 when valueless or on adoption-agency clones)
8299
+ */
8300
+ attributeValueStart(a) {
8301
+ return _aValStart[a];
8302
+ },
8303
+ /**
8304
+ * @param {HtmlAttributeRef} a attribute ref
8305
+ * @returns {number} value end offset
8306
+ */
8307
+ attributeValueEnd(a) {
8308
+ return _aValEnd[a];
8309
+ },
8310
+ /**
8311
+ * @param {HtmlElement=} n element
8312
+ * @returns {number} end offset of the opening tag (after `>`)
8313
+ */
8314
+ tagEnd(n = _curNode) {
8315
+ return _hTagEnd[n];
8316
+ },
8317
+ /**
8318
+ * @param {HtmlElement=} n element
8319
+ * @returns {number} end offset of the tag name
8320
+ */
8321
+ nameEnd(n = _curNode) {
8322
+ return _hNameEnd[n];
8323
+ },
8324
+ /**
8325
+ * @param {HtmlElement=} n element
8326
+ * @returns {number} under `skip.text`, end offset of a raw-text element's body (`tagEnd` when empty)
8327
+ */
8328
+ contentEnd(n = _curNode) {
8329
+ return _hCEnd[n];
8330
+ },
8331
+ /**
8332
+ * @param {HtmlElement=} n element
8333
+ * @returns {HtmlDocumentFragment} `<template>` content fragment (0 = none)
8334
+ */
8335
+ templateContent(n = _curNode) {
8336
+ return _hTc[n];
8337
+ },
8338
+ /**
8339
+ * @param {HtmlText | HtmlComment=} n text / comment node
8340
+ * @returns {string} decoded text / comment data
8341
+ */
8342
+ data(n = _curNode) {
8343
+ return _hStr[n];
8344
+ },
8345
+ /**
8346
+ * @param {HtmlDoctype=} n doctype node
8347
+ * @returns {string} doctype name
8348
+ */
8349
+ doctypeName(n = _curNode) {
8350
+ return _hStr[n];
8351
+ },
8352
+ // The doctype ids are per-parse scalars (a document has at most one
8353
+ // doctype node); the node parameter is accepted for call-shape uniformity.
8354
+ /**
8355
+ * @param {HtmlDoctype=} _n doctype node
8356
+ * @returns {string | null} doctype public id
8357
+ */
8358
+ doctypePublicId(_n) {
8359
+ return _hDocPub;
8360
+ },
8361
+ /**
8362
+ * @param {HtmlDoctype=} _n doctype node
8363
+ * @returns {string | null} doctype system id
8364
+ */
8365
+ doctypeSystemId(_n) {
8366
+ return _hDocSys;
8367
+ },
8368
+ // === tree links (0 = none) ===
8369
+ /**
8370
+ * @param {HtmlNodeRef=} n node
8371
+ * @returns {HtmlNodeRef} first child
8372
+ */
8373
+ firstChild(n = _curNode) {
8374
+ return _hFirst[n];
8375
+ },
8376
+ /**
8377
+ * @param {HtmlNodeRef=} n node
8378
+ * @returns {HtmlNodeRef} next sibling
8379
+ */
8380
+ nextSibling(n = _curNode) {
8381
+ return _hNext[n];
8382
+ },
8383
+ /**
8384
+ * @param {HtmlNodeRef=} n node
8385
+ * @returns {HtmlNodeRef} parent node (a `<template>`'s content links to its fragment)
8386
+ */
8387
+ parentOf(n = _curNode) {
8388
+ return _hParent[n];
8389
+ },
8390
+ /**
8391
+ * @param {HtmlNodeRef=} n node
8392
+ * @returns {HtmlNodeRef[]} materialized child list — test/tooling convenience, allocates
8393
+ */
8394
+ children(n = _curNode) {
8395
+ const out = [];
8396
+ for (let c = _hFirst[n]; c !== 0; c = _hNext[c]) out.push(c);
8397
+ return out;
8398
+ }
8399
+ };
8400
+
8401
+ module.exports.A = A;
7722
8402
  module.exports.NS_HTML = NS_HTML;
7723
8403
  module.exports.NS_MATHML = NS_MATHML;
7724
8404
  module.exports.NS_SVG = NS_SVG;
@@ -7733,6 +8413,5 @@ module.exports.SourceProcessor = SourceProcessor;
7733
8413
  module.exports.buildHtmlAst = buildHtmlAst;
7734
8414
  module.exports.decodeHtmlEntities = decodeHtmlEntities;
7735
8415
  module.exports.decodeHtmlEntitiesWithMap = decodeHtmlEntitiesWithMap;
7736
- module.exports.findAttr = findAttr;
7737
8416
  module.exports.parseSrcset = parseSrcset;
7738
8417
  module.exports.walkHtmlTokens = walkHtmlTokens;