/*
	MIT License http://www.opensource.org/licenses/mit-license.php
	Author Raj Aryan (based on SWC parser by Alexander Akait)
*/

"use strict";

const GenericSourceProcessor = require("../util/SourceProcessor");

// cspell:ignore apos notpre noncharacter noncharacters DFFF FFFE CCLS ALNUM

// #region html entities
// The contents of this region are auto-generated by
// `tooling/generate-html-entities.js` from `tooling/html-entities.json`.
// Do not edit by hand — re-run the generator (via `yarn fix:special`) to refresh.
//
// WHATWG named character references. Keys are entity names WITHOUT the
// leading `&` (some end with `;`, others omit it for legacy entities that
// match without a closing semicolon). Values are the decoded character
// strings (1–2 UTF-16 code units).
// Built on a null prototype so bracket lookups (`HTML_ENTITIES[name]`)
// can't be poisoned by inherited `Object.prototype` keys like `toString`,
// `constructor`, or `__proto__` — without this, `&toString;` would falsely
// look like a matched named character reference.
// prettier-ignore
// cspell:disable-next-line
const HTML_ENTITIES = /** @type {Readonly<Record<string, string>>} */ (Object.freeze(Object.assign(Object.create(null), {"AElig":"Æ","AElig;":"Æ","AMP":"&","AMP;":"&","Aacute":"Á","Aacute;":"Á","Abreve;":"Ă","Acirc":"Â","Acirc;":"Â","Acy;":"А","Afr;":"𝔄","Agrave":"À","Agrave;":"À","Alpha;":"Α","Amacr;":"Ā","And;":"⩓","Aogon;":"Ą","Aopf;":"𝔸","ApplyFunction;":"⁡","Aring":"Å","Aring;":"Å","Ascr;":"𝒜","Assign;":"≔","Atilde":"Ã","Atilde;":"Ã","Auml":"Ä","Auml;":"Ä","Backslash;":"∖","Barv;":"⫧","Barwed;":"⌆","Bcy;":"Б","Because;":"∵","Bernoullis;":"ℬ","Beta;":"Β","Bfr;":"𝔅","Bopf;":"𝔹","Breve;":"˘","Bscr;":"ℬ","Bumpeq;":"≎","CHcy;":"Ч","COPY":"©","COPY;":"©","Cacute;":"Ć","Cap;":"⋒","CapitalDifferentialD;":"ⅅ","Cayleys;":"ℭ","Ccaron;":"Č","Ccedil":"Ç","Ccedil;":"Ç","Ccirc;":"Ĉ","Cconint;":"∰","Cdot;":"Ċ","Cedilla;":"¸","CenterDot;":"·","Cfr;":"ℭ","Chi;":"Χ","CircleDot;":"⊙","CircleMinus;":"⊖","CirclePlus;":"⊕","CircleTimes;":"⊗","ClockwiseContourIntegral;":"∲","CloseCurlyDoubleQuote;":"”","CloseCurlyQuote;":"’","Colon;":"∷","Colone;":"⩴","Congruent;":"≡","Conint;":"∯","ContourIntegral;":"∮","Copf;":"ℂ","Coproduct;":"∐","CounterClockwiseContourIntegral;":"∳","Cross;":"⨯","Cscr;":"𝒞","Cup;":"⋓","CupCap;":"≍","DD;":"ⅅ","DDotrahd;":"⤑","DJcy;":"Ђ","DScy;":"Ѕ","DZcy;":"Џ","Dagger;":"‡","Darr;":"↡","Dashv;":"⫤","Dcaron;":"Ď","Dcy;":"Д","Del;":"∇","Delta;":"Δ","Dfr;":"𝔇","DiacriticalAcute;":"´","DiacriticalDot;":"˙","DiacriticalDoubleAcute;":"˝","DiacriticalGrave;":"`","DiacriticalTilde;":"˜","Diamond;":"⋄","DifferentialD;":"ⅆ","Dopf;":"𝔻","Dot;":"¨","DotDot;":"⃜","DotEqual;":"≐","DoubleContourIntegral;":"∯","DoubleDot;":"¨","DoubleDownArrow;":"⇓","DoubleLeftArrow;":"⇐","DoubleLeftRightArrow;":"⇔","DoubleLeftTee;":"⫤","DoubleLongLeftArrow;":"⟸","DoubleLongLeftRightArrow;":"⟺","DoubleLongRightArrow;":"⟹","DoubleRightArrow;":"⇒","DoubleRightTee;":"⊨","DoubleUpArrow;":"⇑","DoubleUpDownArrow;":"⇕","DoubleVerticalBar;":"∥","DownArrow;":"↓","DownArrowBar;":"⤓","DownArrowUpArrow;":"⇵","DownBreve;":"̑","DownLeftRightVector;":"⥐","DownLeftTeeVector;":"⥞","DownLeftVector;":"↽","DownLeftVectorBar;":"⥖","DownRightTeeVector;":"⥟","DownRightVector;":"⇁","DownRightVectorBar;":"⥗","DownTee;":"⊤","DownTeeArrow;":"↧","Downarrow;":"⇓","Dscr;":"𝒟","Dstrok;":"Đ","ENG;":"Ŋ","ETH":"Ð","ETH;":"Ð","Eacute":"É","Eacute;":"É","Ecaron;":"Ě","Ecirc":"Ê","Ecirc;":"Ê","Ecy;":"Э","Edot;":"Ė","Efr;":"𝔈","Egrave":"È","Egrave;":"È","Element;":"∈","Emacr;":"Ē","EmptySmallSquare;":"◻","EmptyVerySmallSquare;":"▫","Eogon;":"Ę","Eopf;":"𝔼","Epsilon;":"Ε","Equal;":"⩵","EqualTilde;":"≂","Equilibrium;":"⇌","Escr;":"ℰ","Esim;":"⩳","Eta;":"Η","Euml":"Ë","Euml;":"Ë","Exists;":"∃","ExponentialE;":"ⅇ","Fcy;":"Ф","Ffr;":"𝔉","FilledSmallSquare;":"◼","FilledVerySmallSquare;":"▪","Fopf;":"𝔽","ForAll;":"∀","Fouriertrf;":"ℱ","Fscr;":"ℱ","GJcy;":"Ѓ","GT":">","GT;":">","Gamma;":"Γ","Gammad;":"Ϝ","Gbreve;":"Ğ","Gcedil;":"Ģ","Gcirc;":"Ĝ","Gcy;":"Г","Gdot;":"Ġ","Gfr;":"𝔊","Gg;":"⋙","Gopf;":"𝔾","GreaterEqual;":"≥","GreaterEqualLess;":"⋛","GreaterFullEqual;":"≧","GreaterGreater;":"⪢","GreaterLess;":"≷","GreaterSlantEqual;":"⩾","GreaterTilde;":"≳","Gscr;":"𝒢","Gt;":"≫","HARDcy;":"Ъ","Hacek;":"ˇ","Hat;":"^","Hcirc;":"Ĥ","Hfr;":"ℌ","HilbertSpace;":"ℋ","Hopf;":"ℍ","HorizontalLine;":"─","Hscr;":"ℋ","Hstrok;":"Ħ","HumpDownHump;":"≎","HumpEqual;":"≏","IEcy;":"Е","IJlig;":"Ĳ","IOcy;":"Ё","Iacute":"Í","Iacute;":"Í","Icirc":"Î","Icirc;":"Î","Icy;":"И","Idot;":"İ","Ifr;":"ℑ","Igrave":"Ì","Igrave;":"Ì","Im;":"ℑ","Imacr;":"Ī","ImaginaryI;":"ⅈ","Implies;":"⇒","Int;":"∬","Integral;":"∫","Intersection;":"⋂","InvisibleComma;":"⁣","InvisibleTimes;":"⁢","Iogon;":"Į","Iopf;":"𝕀","Iota;":"Ι","Iscr;":"ℐ","Itilde;":"Ĩ","Iukcy;":"І","Iuml":"Ï","Iuml;":"Ï","Jcirc;":"Ĵ","Jcy;":"Й","Jfr;":"𝔍","Jopf;":"𝕁","Jscr;":"𝒥","Jsercy;":"Ј","Jukcy;":"Є","KHcy;":"Х","KJcy;":"Ќ","Kappa;":"Κ","Kcedil;":"Ķ","Kcy;":"К","Kfr;":"𝔎","Kopf;":"𝕂","Kscr;":"𝒦","LJcy;":"Љ","LT":"<","LT;":"<","Lacute;":"Ĺ","Lambda;":"Λ","Lang;":"⟪","Laplacetrf;":"ℒ","Larr;":"↞","Lcaron;":"Ľ","Lcedil;":"Ļ","Lcy;":"Л","LeftAngleBracket;":"⟨","LeftArrow;":"←","LeftArrowBar;":"⇤","LeftArrowRightArrow;":"⇆","LeftCeiling;":"⌈","LeftDoubleBracket;":"⟦","LeftDownTeeVector;":"⥡","LeftDownVector;":"⇃","LeftDownVectorBar;":"⥙","LeftFloor;":"⌊","LeftRightArrow;":"↔","LeftRightVector;":"⥎","LeftTee;":"⊣","LeftTeeArrow;":"↤","LeftTeeVector;":"⥚","LeftTriangle;":"⊲","LeftTriangleBar;":"⧏","LeftTriangleEqual;":"⊴","LeftUpDownVector;":"⥑","LeftUpTeeVector;":"⥠","LeftUpVector;":"↿","LeftUpVectorBar;":"⥘","LeftVector;":"↼","LeftVectorBar;":"⥒","Leftarrow;":"⇐","Leftrightarrow;":"⇔","LessEqualGreater;":"⋚","LessFullEqual;":"≦","LessGreater;":"≶","LessLess;":"⪡","LessSlantEqual;":"⩽","LessTilde;":"≲","Lfr;":"𝔏","Ll;":"⋘","Lleftarrow;":"⇚","Lmidot;":"Ŀ","LongLeftArrow;":"⟵","LongLeftRightArrow;":"⟷","LongRightArrow;":"⟶","Longleftarrow;":"⟸","Longleftrightarrow;":"⟺","Longrightarrow;":"⟹","Lopf;":"𝕃","LowerLeftArrow;":"↙","LowerRightArrow;":"↘","Lscr;":"ℒ","Lsh;":"↰","Lstrok;":"Ł","Lt;":"≪","Map;":"⤅","Mcy;":"М","MediumSpace;":" ","Mellintrf;":"ℳ","Mfr;":"𝔐","MinusPlus;":"∓","Mopf;":"𝕄","Mscr;":"ℳ","Mu;":"Μ","NJcy;":"Њ","Nacute;":"Ń","Ncaron;":"Ň","Ncedil;":"Ņ","Ncy;":"Н","NegativeMediumSpace;":"​","NegativeThickSpace;":"​","NegativeThinSpace;":"​","NegativeVeryThinSpace;":"​","NestedGreaterGreater;":"≫","NestedLessLess;":"≪","NewLine;":"\n","Nfr;":"𝔑","NoBreak;":"⁠","NonBreakingSpace;":" ","Nopf;":"ℕ","Not;":"⫬","NotCongruent;":"≢","NotCupCap;":"≭","NotDoubleVerticalBar;":"∦","NotElement;":"∉","NotEqual;":"≠","NotEqualTilde;":"≂̸","NotExists;":"∄","NotGreater;":"≯","NotGreaterEqual;":"≱","NotGreaterFullEqual;":"≧̸","NotGreaterGreater;":"≫̸","NotGreaterLess;":"≹","NotGreaterSlantEqual;":"⩾̸","NotGreaterTilde;":"≵","NotHumpDownHump;":"≎̸","NotHumpEqual;":"≏̸","NotLeftTriangle;":"⋪","NotLeftTriangleBar;":"⧏̸","NotLeftTriangleEqual;":"⋬","NotLess;":"≮","NotLessEqual;":"≰","NotLessGreater;":"≸","NotLessLess;":"≪̸","NotLessSlantEqual;":"⩽̸","NotLessTilde;":"≴","NotNestedGreaterGreater;":"⪢̸","NotNestedLessLess;":"⪡̸","NotPrecedes;":"⊀","NotPrecedesEqual;":"⪯̸","NotPrecedesSlantEqual;":"⋠","NotReverseElement;":"∌","NotRightTriangle;":"⋫","NotRightTriangleBar;":"⧐̸","NotRightTriangleEqual;":"⋭","NotSquareSubset;":"⊏̸","NotSquareSubsetEqual;":"⋢","NotSquareSuperset;":"⊐̸","NotSquareSupersetEqual;":"⋣","NotSubset;":"⊂⃒","NotSubsetEqual;":"⊈","NotSucceeds;":"⊁","NotSucceedsEqual;":"⪰̸","NotSucceedsSlantEqual;":"⋡","NotSucceedsTilde;":"≿̸","NotSuperset;":"⊃⃒","NotSupersetEqual;":"⊉","NotTilde;":"≁","NotTildeEqual;":"≄","NotTildeFullEqual;":"≇","NotTildeTilde;":"≉","NotVerticalBar;":"∤","Nscr;":"𝒩","Ntilde":"Ñ","Ntilde;":"Ñ","Nu;":"Ν","OElig;":"Œ","Oacute":"Ó","Oacute;":"Ó","Ocirc":"Ô","Ocirc;":"Ô","Ocy;":"О","Odblac;":"Ő","Ofr;":"𝔒","Ograve":"Ò","Ograve;":"Ò","Omacr;":"Ō","Omega;":"Ω","Omicron;":"Ο","Oopf;":"𝕆","OpenCurlyDoubleQuote;":"“","OpenCurlyQuote;":"‘","Or;":"⩔","Oscr;":"𝒪","Oslash":"Ø","Oslash;":"Ø","Otilde":"Õ","Otilde;":"Õ","Otimes;":"⨷","Ouml":"Ö","Ouml;":"Ö","OverBar;":"‾","OverBrace;":"⏞","OverBracket;":"⎴","OverParenthesis;":"⏜","PartialD;":"∂","Pcy;":"П","Pfr;":"𝔓","Phi;":"Φ","Pi;":"Π","PlusMinus;":"±","Poincareplane;":"ℌ","Popf;":"ℙ","Pr;":"⪻","Precedes;":"≺","PrecedesEqual;":"⪯","PrecedesSlantEqual;":"≼","PrecedesTilde;":"≾","Prime;":"″","Product;":"∏","Proportion;":"∷","Proportional;":"∝","Pscr;":"𝒫","Psi;":"Ψ","QUOT":"\"","QUOT;":"\"","Qfr;":"𝔔","Qopf;":"ℚ","Qscr;":"𝒬","RBarr;":"⤐","REG":"®","REG;":"®","Racute;":"Ŕ","Rang;":"⟫","Rarr;":"↠","Rarrtl;":"⤖","Rcaron;":"Ř","Rcedil;":"Ŗ","Rcy;":"Р","Re;":"ℜ","ReverseElement;":"∋","ReverseEquilibrium;":"⇋","ReverseUpEquilibrium;":"⥯","Rfr;":"ℜ","Rho;":"Ρ","RightAngleBracket;":"⟩","RightArrow;":"→","RightArrowBar;":"⇥","RightArrowLeftArrow;":"⇄","RightCeiling;":"⌉","RightDoubleBracket;":"⟧","RightDownTeeVector;":"⥝","RightDownVector;":"⇂","RightDownVectorBar;":"⥕","RightFloor;":"⌋","RightTee;":"⊢","RightTeeArrow;":"↦","RightTeeVector;":"⥛","RightTriangle;":"⊳","RightTriangleBar;":"⧐","RightTriangleEqual;":"⊵","RightUpDownVector;":"⥏","RightUpTeeVector;":"⥜","RightUpVector;":"↾","RightUpVectorBar;":"⥔","RightVector;":"⇀","RightVectorBar;":"⥓","Rightarrow;":"⇒","Ropf;":"ℝ","RoundImplies;":"⥰","Rrightarrow;":"⇛","Rscr;":"ℛ","Rsh;":"↱","RuleDelayed;":"⧴","SHCHcy;":"Щ","SHcy;":"Ш","SOFTcy;":"Ь","Sacute;":"Ś","Sc;":"⪼","Scaron;":"Š","Scedil;":"Ş","Scirc;":"Ŝ","Scy;":"С","Sfr;":"𝔖","ShortDownArrow;":"↓","ShortLeftArrow;":"←","ShortRightArrow;":"→","ShortUpArrow;":"↑","Sigma;":"Σ","SmallCircle;":"∘","Sopf;":"𝕊","Sqrt;":"√","Square;":"□","SquareIntersection;":"⊓","SquareSubset;":"⊏","SquareSubsetEqual;":"⊑","SquareSuperset;":"⊐","SquareSupersetEqual;":"⊒","SquareUnion;":"⊔","Sscr;":"𝒮","Star;":"⋆","Sub;":"⋐","Subset;":"⋐","SubsetEqual;":"⊆","Succeeds;":"≻","SucceedsEqual;":"⪰","SucceedsSlantEqual;":"≽","SucceedsTilde;":"≿","SuchThat;":"∋","Sum;":"∑","Sup;":"⋑","Superset;":"⊃","SupersetEqual;":"⊇","Supset;":"⋑","THORN":"Þ","THORN;":"Þ","TRADE;":"™","TSHcy;":"Ћ","TScy;":"Ц","Tab;":"\t","Tau;":"Τ","Tcaron;":"Ť","Tcedil;":"Ţ","Tcy;":"Т","Tfr;":"𝔗","Therefore;":"∴","Theta;":"Θ","ThickSpace;":"  ","ThinSpace;":" ","Tilde;":"∼","TildeEqual;":"≃","TildeFullEqual;":"≅","TildeTilde;":"≈","Topf;":"𝕋","TripleDot;":"⃛","Tscr;":"𝒯","Tstrok;":"Ŧ","Uacute":"Ú","Uacute;":"Ú","Uarr;":"↟","Uarrocir;":"⥉","Ubrcy;":"Ў","Ubreve;":"Ŭ","Ucirc":"Û","Ucirc;":"Û","Ucy;":"У","Udblac;":"Ű","Ufr;":"𝔘","Ugrave":"Ù","Ugrave;":"Ù","Umacr;":"Ū","UnderBar;":"_","UnderBrace;":"⏟","UnderBracket;":"⎵","UnderParenthesis;":"⏝","Union;":"⋃","UnionPlus;":"⊎","Uogon;":"Ų","Uopf;":"𝕌","UpArrow;":"↑","UpArrowBar;":"⤒","UpArrowDownArrow;":"⇅","UpDownArrow;":"↕","UpEquilibrium;":"⥮","UpTee;":"⊥","UpTeeArrow;":"↥","Uparrow;":"⇑","Updownarrow;":"⇕","UpperLeftArrow;":"↖","UpperRightArrow;":"↗","Upsi;":"ϒ","Upsilon;":"Υ","Uring;":"Ů","Uscr;":"𝒰","Utilde;":"Ũ","Uuml":"Ü","Uuml;":"Ü","VDash;":"⊫","Vbar;":"⫫","Vcy;":"В","Vdash;":"⊩","Vdashl;":"⫦","Vee;":"⋁","Verbar;":"‖","Vert;":"‖","VerticalBar;":"∣","VerticalLine;":"|","VerticalSeparator;":"❘","VerticalTilde;":"≀","VeryThinSpace;":" ","Vfr;":"𝔙","Vopf;":"𝕍","Vscr;":"𝒱","Vvdash;":"⊪","Wcirc;":"Ŵ","Wedge;":"⋀","Wfr;":"𝔚","Wopf;":"𝕎","Wscr;":"𝒲","Xfr;":"𝔛","Xi;":"Ξ","Xopf;":"𝕏","Xscr;":"𝒳","YAcy;":"Я","YIcy;":"Ї","YUcy;":"Ю","Yacute":"Ý","Yacute;":"Ý","Ycirc;":"Ŷ","Ycy;":"Ы","Yfr;":"𝔜","Yopf;":"𝕐","Yscr;":"𝒴","Yuml;":"Ÿ","ZHcy;":"Ж","Zacute;":"Ź","Zcaron;":"Ž","Zcy;":"З","Zdot;":"Ż","ZeroWidthSpace;":"​","Zeta;":"Ζ","Zfr;":"ℨ","Zopf;":"ℤ","Zscr;":"𝒵","aacute":"á","aacute;":"á","abreve;":"ă","ac;":"∾","acE;":"∾̳","acd;":"∿","acirc":"â","acirc;":"â","acute":"´","acute;":"´","acy;":"а","aelig":"æ","aelig;":"æ","af;":"⁡","afr;":"𝔞","agrave":"à","agrave;":"à","alefsym;":"ℵ","aleph;":"ℵ","alpha;":"α","amacr;":"ā","amalg;":"⨿","amp":"&","amp;":"&","and;":"∧","andand;":"⩕","andd;":"⩜","andslope;":"⩘","andv;":"⩚","ang;":"∠","ange;":"⦤","angle;":"∠","angmsd;":"∡","angmsdaa;":"⦨","angmsdab;":"⦩","angmsdac;":"⦪","angmsdad;":"⦫","angmsdae;":"⦬","angmsdaf;":"⦭","angmsdag;":"⦮","angmsdah;":"⦯","angrt;":"∟","angrtvb;":"⊾","angrtvbd;":"⦝","angsph;":"∢","angst;":"Å","angzarr;":"⍼","aogon;":"ą","aopf;":"𝕒","ap;":"≈","apE;":"⩰","apacir;":"⩯","ape;":"≊","apid;":"≋","apos;":"'","approx;":"≈","approxeq;":"≊","aring":"å","aring;":"å","ascr;":"𝒶","ast;":"*","asymp;":"≈","asympeq;":"≍","atilde":"ã","atilde;":"ã","auml":"ä","auml;":"ä","awconint;":"∳","awint;":"⨑","bNot;":"⫭","backcong;":"≌","backepsilon;":"϶","backprime;":"‵","backsim;":"∽","backsimeq;":"⋍","barvee;":"⊽","barwed;":"⌅","barwedge;":"⌅","bbrk;":"⎵","bbrktbrk;":"⎶","bcong;":"≌","bcy;":"б","bdquo;":"„","becaus;":"∵","because;":"∵","bemptyv;":"⦰","bepsi;":"϶","bernou;":"ℬ","beta;":"β","beth;":"ℶ","between;":"≬","bfr;":"𝔟","bigcap;":"⋂","bigcirc;":"◯","bigcup;":"⋃","bigodot;":"⨀","bigoplus;":"⨁","bigotimes;":"⨂","bigsqcup;":"⨆","bigstar;":"★","bigtriangledown;":"▽","bigtriangleup;":"△","biguplus;":"⨄","bigvee;":"⋁","bigwedge;":"⋀","bkarow;":"⤍","blacklozenge;":"⧫","blacksquare;":"▪","blacktriangle;":"▴","blacktriangledown;":"▾","blacktriangleleft;":"◂","blacktriangleright;":"▸","blank;":"␣","blk12;":"▒","blk14;":"░","blk34;":"▓","block;":"█","bne;":"=⃥","bnequiv;":"≡⃥","bnot;":"⌐","bopf;":"𝕓","bot;":"⊥","bottom;":"⊥","bowtie;":"⋈","boxDL;":"╗","boxDR;":"╔","boxDl;":"╖","boxDr;":"╓","boxH;":"═","boxHD;":"╦","boxHU;":"╩","boxHd;":"╤","boxHu;":"╧","boxUL;":"╝","boxUR;":"╚","boxUl;":"╜","boxUr;":"╙","boxV;":"║","boxVH;":"╬","boxVL;":"╣","boxVR;":"╠","boxVh;":"╫","boxVl;":"╢","boxVr;":"╟","boxbox;":"⧉","boxdL;":"╕","boxdR;":"╒","boxdl;":"┐","boxdr;":"┌","boxh;":"─","boxhD;":"╥","boxhU;":"╨","boxhd;":"┬","boxhu;":"┴","boxminus;":"⊟","boxplus;":"⊞","boxtimes;":"⊠","boxuL;":"╛","boxuR;":"╘","boxul;":"┘","boxur;":"└","boxv;":"│","boxvH;":"╪","boxvL;":"╡","boxvR;":"╞","boxvh;":"┼","boxvl;":"┤","boxvr;":"├","bprime;":"‵","breve;":"˘","brvbar":"¦","brvbar;":"¦","bscr;":"𝒷","bsemi;":"⁏","bsim;":"∽","bsime;":"⋍","bsol;":"\\","bsolb;":"⧅","bsolhsub;":"⟈","bull;":"•","bullet;":"•","bump;":"≎","bumpE;":"⪮","bumpe;":"≏","bumpeq;":"≏","cacute;":"ć","cap;":"∩","capand;":"⩄","capbrcup;":"⩉","capcap;":"⩋","capcup;":"⩇","capdot;":"⩀","caps;":"∩︀","caret;":"⁁","caron;":"ˇ","ccaps;":"⩍","ccaron;":"č","ccedil":"ç","ccedil;":"ç","ccirc;":"ĉ","ccups;":"⩌","ccupssm;":"⩐","cdot;":"ċ","cedil":"¸","cedil;":"¸","cemptyv;":"⦲","cent":"¢","cent;":"¢","centerdot;":"·","cfr;":"𝔠","chcy;":"ч","check;":"✓","checkmark;":"✓","chi;":"χ","cir;":"○","cirE;":"⧃","circ;":"ˆ","circeq;":"≗","circlearrowleft;":"↺","circlearrowright;":"↻","circledR;":"®","circledS;":"Ⓢ","circledast;":"⊛","circledcirc;":"⊚","circleddash;":"⊝","cire;":"≗","cirfnint;":"⨐","cirmid;":"⫯","cirscir;":"⧂","clubs;":"♣","clubsuit;":"♣","colon;":":","colone;":"≔","coloneq;":"≔","comma;":",","commat;":"@","comp;":"∁","compfn;":"∘","complement;":"∁","complexes;":"ℂ","cong;":"≅","congdot;":"⩭","conint;":"∮","copf;":"𝕔","coprod;":"∐","copy":"©","copy;":"©","copysr;":"℗","crarr;":"↵","cross;":"✗","cscr;":"𝒸","csub;":"⫏","csube;":"⫑","csup;":"⫐","csupe;":"⫒","ctdot;":"⋯","cudarrl;":"⤸","cudarrr;":"⤵","cuepr;":"⋞","cuesc;":"⋟","cularr;":"↶","cularrp;":"⤽","cup;":"∪","cupbrcap;":"⩈","cupcap;":"⩆","cupcup;":"⩊","cupdot;":"⊍","cupor;":"⩅","cups;":"∪︀","curarr;":"↷","curarrm;":"⤼","curlyeqprec;":"⋞","curlyeqsucc;":"⋟","curlyvee;":"⋎","curlywedge;":"⋏","curren":"¤","curren;":"¤","curvearrowleft;":"↶","curvearrowright;":"↷","cuvee;":"⋎","cuwed;":"⋏","cwconint;":"∲","cwint;":"∱","cylcty;":"⌭","dArr;":"⇓","dHar;":"⥥","dagger;":"†","daleth;":"ℸ","darr;":"↓","dash;":"‐","dashv;":"⊣","dbkarow;":"⤏","dblac;":"˝","dcaron;":"ď","dcy;":"д","dd;":"ⅆ","ddagger;":"‡","ddarr;":"⇊","ddotseq;":"⩷","deg":"°","deg;":"°","delta;":"δ","demptyv;":"⦱","dfisht;":"⥿","dfr;":"𝔡","dharl;":"⇃","dharr;":"⇂","diam;":"⋄","diamond;":"⋄","diamondsuit;":"♦","diams;":"♦","die;":"¨","digamma;":"ϝ","disin;":"⋲","div;":"÷","divide":"÷","divide;":"÷","divideontimes;":"⋇","divonx;":"⋇","djcy;":"ђ","dlcorn;":"⌞","dlcrop;":"⌍","dollar;":"$","dopf;":"𝕕","dot;":"˙","doteq;":"≐","doteqdot;":"≑","dotminus;":"∸","dotplus;":"∔","dotsquare;":"⊡","doublebarwedge;":"⌆","downarrow;":"↓","downdownarrows;":"⇊","downharpoonleft;":"⇃","downharpoonright;":"⇂","drbkarow;":"⤐","drcorn;":"⌟","drcrop;":"⌌","dscr;":"𝒹","dscy;":"ѕ","dsol;":"⧶","dstrok;":"đ","dtdot;":"⋱","dtri;":"▿","dtrif;":"▾","duarr;":"⇵","duhar;":"⥯","dwangle;":"⦦","dzcy;":"џ","dzigrarr;":"⟿","eDDot;":"⩷","eDot;":"≑","eacute":"é","eacute;":"é","easter;":"⩮","ecaron;":"ě","ecir;":"≖","ecirc":"ê","ecirc;":"ê","ecolon;":"≕","ecy;":"э","edot;":"ė","ee;":"ⅇ","efDot;":"≒","efr;":"𝔢","eg;":"⪚","egrave":"è","egrave;":"è","egs;":"⪖","egsdot;":"⪘","el;":"⪙","elinters;":"⏧","ell;":"ℓ","els;":"⪕","elsdot;":"⪗","emacr;":"ē","empty;":"∅","emptyset;":"∅","emptyv;":"∅","emsp13;":" ","emsp14;":" ","emsp;":" ","eng;":"ŋ","ensp;":" ","eogon;":"ę","eopf;":"𝕖","epar;":"⋕","eparsl;":"⧣","eplus;":"⩱","epsi;":"ε","epsilon;":"ε","epsiv;":"ϵ","eqcirc;":"≖","eqcolon;":"≕","eqsim;":"≂","eqslantgtr;":"⪖","eqslantless;":"⪕","equals;":"=","equest;":"≟","equiv;":"≡","equivDD;":"⩸","eqvparsl;":"⧥","erDot;":"≓","erarr;":"⥱","escr;":"ℯ","esdot;":"≐","esim;":"≂","eta;":"η","eth":"ð","eth;":"ð","euml":"ë","euml;":"ë","euro;":"€","excl;":"!","exist;":"∃","expectation;":"ℰ","exponentiale;":"ⅇ","fallingdotseq;":"≒","fcy;":"ф","female;":"♀","ffilig;":"ﬃ","fflig;":"ﬀ","ffllig;":"ﬄ","ffr;":"𝔣","filig;":"ﬁ","fjlig;":"fj","flat;":"♭","fllig;":"ﬂ","fltns;":"▱","fnof;":"ƒ","fopf;":"𝕗","forall;":"∀","fork;":"⋔","forkv;":"⫙","fpartint;":"⨍","frac12":"½","frac12;":"½","frac13;":"⅓","frac14":"¼","frac14;":"¼","frac15;":"⅕","frac16;":"⅙","frac18;":"⅛","frac23;":"⅔","frac25;":"⅖","frac34":"¾","frac34;":"¾","frac35;":"⅗","frac38;":"⅜","frac45;":"⅘","frac56;":"⅚","frac58;":"⅝","frac78;":"⅞","frasl;":"⁄","frown;":"⌢","fscr;":"𝒻","gE;":"≧","gEl;":"⪌","gacute;":"ǵ","gamma;":"γ","gammad;":"ϝ","gap;":"⪆","gbreve;":"ğ","gcirc;":"ĝ","gcy;":"г","gdot;":"ġ","ge;":"≥","gel;":"⋛","geq;":"≥","geqq;":"≧","geqslant;":"⩾","ges;":"⩾","gescc;":"⪩","gesdot;":"⪀","gesdoto;":"⪂","gesdotol;":"⪄","gesl;":"⋛︀","gesles;":"⪔","gfr;":"𝔤","gg;":"≫","ggg;":"⋙","gimel;":"ℷ","gjcy;":"ѓ","gl;":"≷","glE;":"⪒","gla;":"⪥","glj;":"⪤","gnE;":"≩","gnap;":"⪊","gnapprox;":"⪊","gne;":"⪈","gneq;":"⪈","gneqq;":"≩","gnsim;":"⋧","gopf;":"𝕘","grave;":"`","gscr;":"ℊ","gsim;":"≳","gsime;":"⪎","gsiml;":"⪐","gt":">","gt;":">","gtcc;":"⪧","gtcir;":"⩺","gtdot;":"⋗","gtlPar;":"⦕","gtquest;":"⩼","gtrapprox;":"⪆","gtrarr;":"⥸","gtrdot;":"⋗","gtreqless;":"⋛","gtreqqless;":"⪌","gtrless;":"≷","gtrsim;":"≳","gvertneqq;":"≩︀","gvnE;":"≩︀","hArr;":"⇔","hairsp;":" ","half;":"½","hamilt;":"ℋ","hardcy;":"ъ","harr;":"↔","harrcir;":"⥈","harrw;":"↭","hbar;":"ℏ","hcirc;":"ĥ","hearts;":"♥","heartsuit;":"♥","hellip;":"…","hercon;":"⊹","hfr;":"𝔥","hksearow;":"⤥","hkswarow;":"⤦","hoarr;":"⇿","homtht;":"∻","hookleftarrow;":"↩","hookrightarrow;":"↪","hopf;":"𝕙","horbar;":"―","hscr;":"𝒽","hslash;":"ℏ","hstrok;":"ħ","hybull;":"⁃","hyphen;":"‐","iacute":"í","iacute;":"í","ic;":"⁣","icirc":"î","icirc;":"î","icy;":"и","iecy;":"е","iexcl":"¡","iexcl;":"¡","iff;":"⇔","ifr;":"𝔦","igrave":"ì","igrave;":"ì","ii;":"ⅈ","iiiint;":"⨌","iiint;":"∭","iinfin;":"⧜","iiota;":"℩","ijlig;":"ĳ","imacr;":"ī","image;":"ℑ","imagline;":"ℐ","imagpart;":"ℑ","imath;":"ı","imof;":"⊷","imped;":"Ƶ","in;":"∈","incare;":"℅","infin;":"∞","infintie;":"⧝","inodot;":"ı","int;":"∫","intcal;":"⊺","integers;":"ℤ","intercal;":"⊺","intlarhk;":"⨗","intprod;":"⨼","iocy;":"ё","iogon;":"į","iopf;":"𝕚","iota;":"ι","iprod;":"⨼","iquest":"¿","iquest;":"¿","iscr;":"𝒾","isin;":"∈","isinE;":"⋹","isindot;":"⋵","isins;":"⋴","isinsv;":"⋳","isinv;":"∈","it;":"⁢","itilde;":"ĩ","iukcy;":"і","iuml":"ï","iuml;":"ï","jcirc;":"ĵ","jcy;":"й","jfr;":"𝔧","jmath;":"ȷ","jopf;":"𝕛","jscr;":"𝒿","jsercy;":"ј","jukcy;":"є","kappa;":"κ","kappav;":"ϰ","kcedil;":"ķ","kcy;":"к","kfr;":"𝔨","kgreen;":"ĸ","khcy;":"х","kjcy;":"ќ","kopf;":"𝕜","kscr;":"𝓀","lAarr;":"⇚","lArr;":"⇐","lAtail;":"⤛","lBarr;":"⤎","lE;":"≦","lEg;":"⪋","lHar;":"⥢","lacute;":"ĺ","laemptyv;":"⦴","lagran;":"ℒ","lambda;":"λ","lang;":"⟨","langd;":"⦑","langle;":"⟨","lap;":"⪅","laquo":"«","laquo;":"«","larr;":"←","larrb;":"⇤","larrbfs;":"⤟","larrfs;":"⤝","larrhk;":"↩","larrlp;":"↫","larrpl;":"⤹","larrsim;":"⥳","larrtl;":"↢","lat;":"⪫","latail;":"⤙","late;":"⪭","lates;":"⪭︀","lbarr;":"⤌","lbbrk;":"❲","lbrace;":"{","lbrack;":"[","lbrke;":"⦋","lbrksld;":"⦏","lbrkslu;":"⦍","lcaron;":"ľ","lcedil;":"ļ","lceil;":"⌈","lcub;":"{","lcy;":"л","ldca;":"⤶","ldquo;":"“","ldquor;":"„","ldrdhar;":"⥧","ldrushar;":"⥋","ldsh;":"↲","le;":"≤","leftarrow;":"←","leftarrowtail;":"↢","leftharpoondown;":"↽","leftharpoonup;":"↼","leftleftarrows;":"⇇","leftrightarrow;":"↔","leftrightarrows;":"⇆","leftrightharpoons;":"⇋","leftrightsquigarrow;":"↭","leftthreetimes;":"⋋","leg;":"⋚","leq;":"≤","leqq;":"≦","leqslant;":"⩽","les;":"⩽","lescc;":"⪨","lesdot;":"⩿","lesdoto;":"⪁","lesdotor;":"⪃","lesg;":"⋚︀","lesges;":"⪓","lessapprox;":"⪅","lessdot;":"⋖","lesseqgtr;":"⋚","lesseqqgtr;":"⪋","lessgtr;":"≶","lesssim;":"≲","lfisht;":"⥼","lfloor;":"⌊","lfr;":"𝔩","lg;":"≶","lgE;":"⪑","lhard;":"↽","lharu;":"↼","lharul;":"⥪","lhblk;":"▄","ljcy;":"љ","ll;":"≪","llarr;":"⇇","llcorner;":"⌞","llhard;":"⥫","lltri;":"◺","lmidot;":"ŀ","lmoust;":"⎰","lmoustache;":"⎰","lnE;":"≨","lnap;":"⪉","lnapprox;":"⪉","lne;":"⪇","lneq;":"⪇","lneqq;":"≨","lnsim;":"⋦","loang;":"⟬","loarr;":"⇽","lobrk;":"⟦","longleftarrow;":"⟵","longleftrightarrow;":"⟷","longmapsto;":"⟼","longrightarrow;":"⟶","looparrowleft;":"↫","looparrowright;":"↬","lopar;":"⦅","lopf;":"𝕝","loplus;":"⨭","lotimes;":"⨴","lowast;":"∗","lowbar;":"_","loz;":"◊","lozenge;":"◊","lozf;":"⧫","lpar;":"(","lparlt;":"⦓","lrarr;":"⇆","lrcorner;":"⌟","lrhar;":"⇋","lrhard;":"⥭","lrm;":"‎","lrtri;":"⊿","lsaquo;":"‹","lscr;":"𝓁","lsh;":"↰","lsim;":"≲","lsime;":"⪍","lsimg;":"⪏","lsqb;":"[","lsquo;":"‘","lsquor;":"‚","lstrok;":"ł","lt":"<","lt;":"<","ltcc;":"⪦","ltcir;":"⩹","ltdot;":"⋖","lthree;":"⋋","ltimes;":"⋉","ltlarr;":"⥶","ltquest;":"⩻","ltrPar;":"⦖","ltri;":"◃","ltrie;":"⊴","ltrif;":"◂","lurdshar;":"⥊","luruhar;":"⥦","lvertneqq;":"≨︀","lvnE;":"≨︀","mDDot;":"∺","macr":"¯","macr;":"¯","male;":"♂","malt;":"✠","maltese;":"✠","map;":"↦","mapsto;":"↦","mapstodown;":"↧","mapstoleft;":"↤","mapstoup;":"↥","marker;":"▮","mcomma;":"⨩","mcy;":"м","mdash;":"—","measuredangle;":"∡","mfr;":"𝔪","mho;":"℧","micro":"µ","micro;":"µ","mid;":"∣","midast;":"*","midcir;":"⫰","middot":"·","middot;":"·","minus;":"−","minusb;":"⊟","minusd;":"∸","minusdu;":"⨪","mlcp;":"⫛","mldr;":"…","mnplus;":"∓","models;":"⊧","mopf;":"𝕞","mp;":"∓","mscr;":"𝓂","mstpos;":"∾","mu;":"μ","multimap;":"⊸","mumap;":"⊸","nGg;":"⋙̸","nGt;":"≫⃒","nGtv;":"≫̸","nLeftarrow;":"⇍","nLeftrightarrow;":"⇎","nLl;":"⋘̸","nLt;":"≪⃒","nLtv;":"≪̸","nRightarrow;":"⇏","nVDash;":"⊯","nVdash;":"⊮","nabla;":"∇","nacute;":"ń","nang;":"∠⃒","nap;":"≉","napE;":"⩰̸","napid;":"≋̸","napos;":"ŉ","napprox;":"≉","natur;":"♮","natural;":"♮","naturals;":"ℕ","nbsp":" ","nbsp;":" ","nbump;":"≎̸","nbumpe;":"≏̸","ncap;":"⩃","ncaron;":"ň","ncedil;":"ņ","ncong;":"≇","ncongdot;":"⩭̸","ncup;":"⩂","ncy;":"н","ndash;":"–","ne;":"≠","neArr;":"⇗","nearhk;":"⤤","nearr;":"↗","nearrow;":"↗","nedot;":"≐̸","nequiv;":"≢","nesear;":"⤨","nesim;":"≂̸","nexist;":"∄","nexists;":"∄","nfr;":"𝔫","ngE;":"≧̸","nge;":"≱","ngeq;":"≱","ngeqq;":"≧̸","ngeqslant;":"⩾̸","nges;":"⩾̸","ngsim;":"≵","ngt;":"≯","ngtr;":"≯","nhArr;":"⇎","nharr;":"↮","nhpar;":"⫲","ni;":"∋","nis;":"⋼","nisd;":"⋺","niv;":"∋","njcy;":"њ","nlArr;":"⇍","nlE;":"≦̸","nlarr;":"↚","nldr;":"‥","nle;":"≰","nleftarrow;":"↚","nleftrightarrow;":"↮","nleq;":"≰","nleqq;":"≦̸","nleqslant;":"⩽̸","nles;":"⩽̸","nless;":"≮","nlsim;":"≴","nlt;":"≮","nltri;":"⋪","nltrie;":"⋬","nmid;":"∤","nopf;":"𝕟","not":"¬","not;":"¬","notin;":"∉","notinE;":"⋹̸","notindot;":"⋵̸","notinva;":"∉","notinvb;":"⋷","notinvc;":"⋶","notni;":"∌","notniva;":"∌","notnivb;":"⋾","notnivc;":"⋽","npar;":"∦","nparallel;":"∦","nparsl;":"⫽⃥","npart;":"∂̸","npolint;":"⨔","npr;":"⊀","nprcue;":"⋠","npre;":"⪯̸","nprec;":"⊀","npreceq;":"⪯̸","nrArr;":"⇏","nrarr;":"↛","nrarrc;":"⤳̸","nrarrw;":"↝̸","nrightarrow;":"↛","nrtri;":"⋫","nrtrie;":"⋭","nsc;":"⊁","nsccue;":"⋡","nsce;":"⪰̸","nscr;":"𝓃","nshortmid;":"∤","nshortparallel;":"∦","nsim;":"≁","nsime;":"≄","nsimeq;":"≄","nsmid;":"∤","nspar;":"∦","nsqsube;":"⋢","nsqsupe;":"⋣","nsub;":"⊄","nsubE;":"⫅̸","nsube;":"⊈","nsubset;":"⊂⃒","nsubseteq;":"⊈","nsubseteqq;":"⫅̸","nsucc;":"⊁","nsucceq;":"⪰̸","nsup;":"⊅","nsupE;":"⫆̸","nsupe;":"⊉","nsupset;":"⊃⃒","nsupseteq;":"⊉","nsupseteqq;":"⫆̸","ntgl;":"≹","ntilde":"ñ","ntilde;":"ñ","ntlg;":"≸","ntriangleleft;":"⋪","ntrianglelefteq;":"⋬","ntriangleright;":"⋫","ntrianglerighteq;":"⋭","nu;":"ν","num;":"#","numero;":"№","numsp;":" ","nvDash;":"⊭","nvHarr;":"⤄","nvap;":"≍⃒","nvdash;":"⊬","nvge;":"≥⃒","nvgt;":">⃒","nvinfin;":"⧞","nvlArr;":"⤂","nvle;":"≤⃒","nvlt;":"<⃒","nvltrie;":"⊴⃒","nvrArr;":"⤃","nvrtrie;":"⊵⃒","nvsim;":"∼⃒","nwArr;":"⇖","nwarhk;":"⤣","nwarr;":"↖","nwarrow;":"↖","nwnear;":"⤧","oS;":"Ⓢ","oacute":"ó","oacute;":"ó","oast;":"⊛","ocir;":"⊚","ocirc":"ô","ocirc;":"ô","ocy;":"о","odash;":"⊝","odblac;":"ő","odiv;":"⨸","odot;":"⊙","odsold;":"⦼","oelig;":"œ","ofcir;":"⦿","ofr;":"𝔬","ogon;":"˛","ograve":"ò","ograve;":"ò","ogt;":"⧁","ohbar;":"⦵","ohm;":"Ω","oint;":"∮","olarr;":"↺","olcir;":"⦾","olcross;":"⦻","oline;":"‾","olt;":"⧀","omacr;":"ō","omega;":"ω","omicron;":"ο","omid;":"⦶","ominus;":"⊖","oopf;":"𝕠","opar;":"⦷","operp;":"⦹","oplus;":"⊕","or;":"∨","orarr;":"↻","ord;":"⩝","order;":"ℴ","orderof;":"ℴ","ordf":"ª","ordf;":"ª","ordm":"º","ordm;":"º","origof;":"⊶","oror;":"⩖","orslope;":"⩗","orv;":"⩛","oscr;":"ℴ","oslash":"ø","oslash;":"ø","osol;":"⊘","otilde":"õ","otilde;":"õ","otimes;":"⊗","otimesas;":"⨶","ouml":"ö","ouml;":"ö","ovbar;":"⌽","par;":"∥","para":"¶","para;":"¶","parallel;":"∥","parsim;":"⫳","parsl;":"⫽","part;":"∂","pcy;":"п","percnt;":"%","period;":".","permil;":"‰","perp;":"⊥","pertenk;":"‱","pfr;":"𝔭","phi;":"φ","phiv;":"ϕ","phmmat;":"ℳ","phone;":"☎","pi;":"π","pitchfork;":"⋔","piv;":"ϖ","planck;":"ℏ","planckh;":"ℎ","plankv;":"ℏ","plus;":"+","plusacir;":"⨣","plusb;":"⊞","pluscir;":"⨢","plusdo;":"∔","plusdu;":"⨥","pluse;":"⩲","plusmn":"±","plusmn;":"±","plussim;":"⨦","plustwo;":"⨧","pm;":"±","pointint;":"⨕","popf;":"𝕡","pound":"£","pound;":"£","pr;":"≺","prE;":"⪳","prap;":"⪷","prcue;":"≼","pre;":"⪯","prec;":"≺","precapprox;":"⪷","preccurlyeq;":"≼","preceq;":"⪯","precnapprox;":"⪹","precneqq;":"⪵","precnsim;":"⋨","precsim;":"≾","prime;":"′","primes;":"ℙ","prnE;":"⪵","prnap;":"⪹","prnsim;":"⋨","prod;":"∏","profalar;":"⌮","profline;":"⌒","profsurf;":"⌓","prop;":"∝","propto;":"∝","prsim;":"≾","prurel;":"⊰","pscr;":"𝓅","psi;":"ψ","puncsp;":" ","qfr;":"𝔮","qint;":"⨌","qopf;":"𝕢","qprime;":"⁗","qscr;":"𝓆","quaternions;":"ℍ","quatint;":"⨖","quest;":"?","questeq;":"≟","quot":"\"","quot;":"\"","rAarr;":"⇛","rArr;":"⇒","rAtail;":"⤜","rBarr;":"⤏","rHar;":"⥤","race;":"∽̱","racute;":"ŕ","radic;":"√","raemptyv;":"⦳","rang;":"⟩","rangd;":"⦒","range;":"⦥","rangle;":"⟩","raquo":"»","raquo;":"»","rarr;":"→","rarrap;":"⥵","rarrb;":"⇥","rarrbfs;":"⤠","rarrc;":"⤳","rarrfs;":"⤞","rarrhk;":"↪","rarrlp;":"↬","rarrpl;":"⥅","rarrsim;":"⥴","rarrtl;":"↣","rarrw;":"↝","ratail;":"⤚","ratio;":"∶","rationals;":"ℚ","rbarr;":"⤍","rbbrk;":"❳","rbrace;":"}","rbrack;":"]","rbrke;":"⦌","rbrksld;":"⦎","rbrkslu;":"⦐","rcaron;":"ř","rcedil;":"ŗ","rceil;":"⌉","rcub;":"}","rcy;":"р","rdca;":"⤷","rdldhar;":"⥩","rdquo;":"”","rdquor;":"”","rdsh;":"↳","real;":"ℜ","realine;":"ℛ","realpart;":"ℜ","reals;":"ℝ","rect;":"▭","reg":"®","reg;":"®","rfisht;":"⥽","rfloor;":"⌋","rfr;":"𝔯","rhard;":"⇁","rharu;":"⇀","rharul;":"⥬","rho;":"ρ","rhov;":"ϱ","rightarrow;":"→","rightarrowtail;":"↣","rightharpoondown;":"⇁","rightharpoonup;":"⇀","rightleftarrows;":"⇄","rightleftharpoons;":"⇌","rightrightarrows;":"⇉","rightsquigarrow;":"↝","rightthreetimes;":"⋌","ring;":"˚","risingdotseq;":"≓","rlarr;":"⇄","rlhar;":"⇌","rlm;":"‏","rmoust;":"⎱","rmoustache;":"⎱","rnmid;":"⫮","roang;":"⟭","roarr;":"⇾","robrk;":"⟧","ropar;":"⦆","ropf;":"𝕣","roplus;":"⨮","rotimes;":"⨵","rpar;":")","rpargt;":"⦔","rppolint;":"⨒","rrarr;":"⇉","rsaquo;":"›","rscr;":"𝓇","rsh;":"↱","rsqb;":"]","rsquo;":"’","rsquor;":"’","rthree;":"⋌","rtimes;":"⋊","rtri;":"▹","rtrie;":"⊵","rtrif;":"▸","rtriltri;":"⧎","ruluhar;":"⥨","rx;":"℞","sacute;":"ś","sbquo;":"‚","sc;":"≻","scE;":"⪴","scap;":"⪸","scaron;":"š","sccue;":"≽","sce;":"⪰","scedil;":"ş","scirc;":"ŝ","scnE;":"⪶","scnap;":"⪺","scnsim;":"⋩","scpolint;":"⨓","scsim;":"≿","scy;":"с","sdot;":"⋅","sdotb;":"⊡","sdote;":"⩦","seArr;":"⇘","searhk;":"⤥","searr;":"↘","searrow;":"↘","sect":"§","sect;":"§","semi;":";","seswar;":"⤩","setminus;":"∖","setmn;":"∖","sext;":"✶","sfr;":"𝔰","sfrown;":"⌢","sharp;":"♯","shchcy;":"щ","shcy;":"ш","shortmid;":"∣","shortparallel;":"∥","shy":"­","shy;":"­","sigma;":"σ","sigmaf;":"ς","sigmav;":"ς","sim;":"∼","simdot;":"⩪","sime;":"≃","simeq;":"≃","simg;":"⪞","simgE;":"⪠","siml;":"⪝","simlE;":"⪟","simne;":"≆","simplus;":"⨤","simrarr;":"⥲","slarr;":"←","smallsetminus;":"∖","smashp;":"⨳","smeparsl;":"⧤","smid;":"∣","smile;":"⌣","smt;":"⪪","smte;":"⪬","smtes;":"⪬︀","softcy;":"ь","sol;":"/","solb;":"⧄","solbar;":"⌿","sopf;":"𝕤","spades;":"♠","spadesuit;":"♠","spar;":"∥","sqcap;":"⊓","sqcaps;":"⊓︀","sqcup;":"⊔","sqcups;":"⊔︀","sqsub;":"⊏","sqsube;":"⊑","sqsubset;":"⊏","sqsubseteq;":"⊑","sqsup;":"⊐","sqsupe;":"⊒","sqsupset;":"⊐","sqsupseteq;":"⊒","squ;":"□","square;":"□","squarf;":"▪","squf;":"▪","srarr;":"→","sscr;":"𝓈","ssetmn;":"∖","ssmile;":"⌣","sstarf;":"⋆","star;":"☆","starf;":"★","straightepsilon;":"ϵ","straightphi;":"ϕ","strns;":"¯","sub;":"⊂","subE;":"⫅","subdot;":"⪽","sube;":"⊆","subedot;":"⫃","submult;":"⫁","subnE;":"⫋","subne;":"⊊","subplus;":"⪿","subrarr;":"⥹","subset;":"⊂","subseteq;":"⊆","subseteqq;":"⫅","subsetneq;":"⊊","subsetneqq;":"⫋","subsim;":"⫇","subsub;":"⫕","subsup;":"⫓","succ;":"≻","succapprox;":"⪸","succcurlyeq;":"≽","succeq;":"⪰","succnapprox;":"⪺","succneqq;":"⪶","succnsim;":"⋩","succsim;":"≿","sum;":"∑","sung;":"♪","sup1":"¹","sup1;":"¹","sup2":"²","sup2;":"²","sup3":"³","sup3;":"³","sup;":"⊃","supE;":"⫆","supdot;":"⪾","supdsub;":"⫘","supe;":"⊇","supedot;":"⫄","suphsol;":"⟉","suphsub;":"⫗","suplarr;":"⥻","supmult;":"⫂","supnE;":"⫌","supne;":"⊋","supplus;":"⫀","supset;":"⊃","supseteq;":"⊇","supseteqq;":"⫆","supsetneq;":"⊋","supsetneqq;":"⫌","supsim;":"⫈","supsub;":"⫔","supsup;":"⫖","swArr;":"⇙","swarhk;":"⤦","swarr;":"↙","swarrow;":"↙","swnwar;":"⤪","szlig":"ß","szlig;":"ß","target;":"⌖","tau;":"τ","tbrk;":"⎴","tcaron;":"ť","tcedil;":"ţ","tcy;":"т","tdot;":"⃛","telrec;":"⌕","tfr;":"𝔱","there4;":"∴","therefore;":"∴","theta;":"θ","thetasym;":"ϑ","thetav;":"ϑ","thickapprox;":"≈","thicksim;":"∼","thinsp;":" ","thkap;":"≈","thksim;":"∼","thorn":"þ","thorn;":"þ","tilde;":"˜","times":"×","times;":"×","timesb;":"⊠","timesbar;":"⨱","timesd;":"⨰","tint;":"∭","toea;":"⤨","top;":"⊤","topbot;":"⌶","topcir;":"⫱","topf;":"𝕥","topfork;":"⫚","tosa;":"⤩","tprime;":"‴","trade;":"™","triangle;":"▵","triangledown;":"▿","triangleleft;":"◃","trianglelefteq;":"⊴","triangleq;":"≜","triangleright;":"▹","trianglerighteq;":"⊵","tridot;":"◬","trie;":"≜","triminus;":"⨺","triplus;":"⨹","trisb;":"⧍","tritime;":"⨻","trpezium;":"⏢","tscr;":"𝓉","tscy;":"ц","tshcy;":"ћ","tstrok;":"ŧ","twixt;":"≬","twoheadleftarrow;":"↞","twoheadrightarrow;":"↠","uArr;":"⇑","uHar;":"⥣","uacute":"ú","uacute;":"ú","uarr;":"↑","ubrcy;":"ў","ubreve;":"ŭ","ucirc":"û","ucirc;":"û","ucy;":"у","udarr;":"⇅","udblac;":"ű","udhar;":"⥮","ufisht;":"⥾","ufr;":"𝔲","ugrave":"ù","ugrave;":"ù","uharl;":"↿","uharr;":"↾","uhblk;":"▀","ulcorn;":"⌜","ulcorner;":"⌜","ulcrop;":"⌏","ultri;":"◸","umacr;":"ū","uml":"¨","uml;":"¨","uogon;":"ų","uopf;":"𝕦","uparrow;":"↑","updownarrow;":"↕","upharpoonleft;":"↿","upharpoonright;":"↾","uplus;":"⊎","upsi;":"υ","upsih;":"ϒ","upsilon;":"υ","upuparrows;":"⇈","urcorn;":"⌝","urcorner;":"⌝","urcrop;":"⌎","uring;":"ů","urtri;":"◹","uscr;":"𝓊","utdot;":"⋰","utilde;":"ũ","utri;":"▵","utrif;":"▴","uuarr;":"⇈","uuml":"ü","uuml;":"ü","uwangle;":"⦧","vArr;":"⇕","vBar;":"⫨","vBarv;":"⫩","vDash;":"⊨","vangrt;":"⦜","varepsilon;":"ϵ","varkappa;":"ϰ","varnothing;":"∅","varphi;":"ϕ","varpi;":"ϖ","varpropto;":"∝","varr;":"↕","varrho;":"ϱ","varsigma;":"ς","varsubsetneq;":"⊊︀","varsubsetneqq;":"⫋︀","varsupsetneq;":"⊋︀","varsupsetneqq;":"⫌︀","vartheta;":"ϑ","vartriangleleft;":"⊲","vartriangleright;":"⊳","vcy;":"в","vdash;":"⊢","vee;":"∨","veebar;":"⊻","veeeq;":"≚","vellip;":"⋮","verbar;":"|","vert;":"|","vfr;":"𝔳","vltri;":"⊲","vnsub;":"⊂⃒","vnsup;":"⊃⃒","vopf;":"𝕧","vprop;":"∝","vrtri;":"⊳","vscr;":"𝓋","vsubnE;":"⫋︀","vsubne;":"⊊︀","vsupnE;":"⫌︀","vsupne;":"⊋︀","vzigzag;":"⦚","wcirc;":"ŵ","wedbar;":"⩟","wedge;":"∧","wedgeq;":"≙","weierp;":"℘","wfr;":"𝔴","wopf;":"𝕨","wp;":"℘","wr;":"≀","wreath;":"≀","wscr;":"𝓌","xcap;":"⋂","xcirc;":"◯","xcup;":"⋃","xdtri;":"▽","xfr;":"𝔵","xhArr;":"⟺","xharr;":"⟷","xi;":"ξ","xlArr;":"⟸","xlarr;":"⟵","xmap;":"⟼","xnis;":"⋻","xodot;":"⨀","xopf;":"𝕩","xoplus;":"⨁","xotime;":"⨂","xrArr;":"⟹","xrarr;":"⟶","xscr;":"𝓍","xsqcup;":"⨆","xuplus;":"⨄","xutri;":"△","xvee;":"⋁","xwedge;":"⋀","yacute":"ý","yacute;":"ý","yacy;":"я","ycirc;":"ŷ","ycy;":"ы","yen":"¥","yen;":"¥","yfr;":"𝔶","yicy;":"ї","yopf;":"𝕪","yscr;":"𝓎","yucy;":"ю","yuml":"ÿ","yuml;":"ÿ","zacute;":"ź","zcaron;":"ž","zcy;":"з","zdot;":"ż","zeetrf;":"ℨ","zeta;":"ζ","zfr;":"𝔷","zhcy;":"ж","zigrarr;":"⇝","zopf;":"𝕫","zscr;":"𝓏","zwj;":"‍","zwnj;":"‌"})));
// #endregion

const STATE_DATA = 0;
const STATE_TAG_OPEN = 1;
const STATE_END_TAG_OPEN = 2;
const STATE_TAG_NAME = 3;
const STATE_BEFORE_ATTRIBUTE_NAME = 4;
const STATE_ATTRIBUTE_NAME = 5;
const STATE_AFTER_ATTRIBUTE_NAME = 6;
const STATE_BEFORE_ATTRIBUTE_VALUE = 7;
const STATE_ATTRIBUTE_VALUE_DOUBLE_QUOTED = 8;
const STATE_ATTRIBUTE_VALUE_SINGLE_QUOTED = 9;
const STATE_ATTRIBUTE_VALUE_UNQUOTED = 10;
const STATE_AFTER_ATTRIBUTE_VALUE_QUOTED = 11;
const STATE_SELF_CLOSING_START_TAG = 12;

const STATE_MARKUP_DECLARATION_OPEN = 13;
const STATE_COMMENT_START = 14;
const STATE_COMMENT_START_DASH = 15;
const STATE_COMMENT = 16;
const STATE_COMMENT_END_DASH = 17;
const STATE_COMMENT_END = 18;
const STATE_COMMENT_END_BANG = 19;
const STATE_BOGUS_COMMENT = 20;

const STATE_COMMENT_LESS_THAN_SIGN = 21;
const STATE_COMMENT_LESS_THAN_SIGN_BANG = 22;
const STATE_COMMENT_LESS_THAN_SIGN_BANG_DASH = 23;
const STATE_COMMENT_LESS_THAN_SIGN_BANG_DASH_DASH = 24;

const STATE_DOCTYPE = 25;
const STATE_BEFORE_DOCTYPE_NAME = 26;
const STATE_DOCTYPE_NAME = 27;
const STATE_AFTER_DOCTYPE_NAME = 28;
const STATE_AFTER_DOCTYPE_PUBLIC_KEYWORD = 29;
const STATE_BEFORE_DOCTYPE_PUBLIC_IDENTIFIER = 30;
const STATE_DOCTYPE_PUBLIC_IDENTIFIER_DOUBLE_QUOTED = 31;
const STATE_DOCTYPE_PUBLIC_IDENTIFIER_SINGLE_QUOTED = 32;
const STATE_AFTER_DOCTYPE_PUBLIC_IDENTIFIER = 33;
const STATE_BETWEEN_DOCTYPE_PUBLIC_AND_SYSTEM_IDENTIFIERS = 34;
const STATE_AFTER_DOCTYPE_SYSTEM_KEYWORD = 35;
const STATE_BEFORE_DOCTYPE_SYSTEM_IDENTIFIER = 36;
const STATE_DOCTYPE_SYSTEM_IDENTIFIER_DOUBLE_QUOTED = 37;
const STATE_DOCTYPE_SYSTEM_IDENTIFIER_SINGLE_QUOTED = 38;
const STATE_AFTER_DOCTYPE_SYSTEM_IDENTIFIER = 39;
const STATE_BOGUS_DOCTYPE = 40;

const STATE_CDATA_SECTION = 41;
const STATE_CDATA_SECTION_BRACKET = 42;
const STATE_CDATA_SECTION_END = 43;

const STATE_RCDATA = 44;
const STATE_RCDATA_LESS_THAN_SIGN = 45;
const STATE_RCDATA_END_TAG_OPEN = 46;
const STATE_RCDATA_END_TAG_NAME = 47;

const STATE_RAWTEXT = 48;
const STATE_RAWTEXT_LESS_THAN_SIGN = 49;
const STATE_RAWTEXT_END_TAG_OPEN = 50;
const STATE_RAWTEXT_END_TAG_NAME = 51;

const STATE_SCRIPT_DATA = 52;
const STATE_SCRIPT_DATA_LESS_THAN_SIGN = 53;
const STATE_SCRIPT_DATA_END_TAG_OPEN = 54;
const STATE_SCRIPT_DATA_END_TAG_NAME = 55;
const STATE_SCRIPT_DATA_ESCAPE_START = 56;
const STATE_SCRIPT_DATA_ESCAPE_START_DASH = 57;
const STATE_SCRIPT_DATA_ESCAPED = 58;
const STATE_SCRIPT_DATA_ESCAPED_DASH = 59;
const STATE_SCRIPT_DATA_ESCAPED_DASH_DASH = 60;
const STATE_SCRIPT_DATA_ESCAPED_LESS_THAN_SIGN = 61;
const STATE_SCRIPT_DATA_ESCAPED_END_TAG_OPEN = 62;
const STATE_SCRIPT_DATA_ESCAPED_END_TAG_NAME = 63;
const STATE_SCRIPT_DATA_DOUBLE_ESCAPE_START = 64;
const STATE_SCRIPT_DATA_DOUBLE_ESCAPED = 65;
const STATE_SCRIPT_DATA_DOUBLE_ESCAPED_DASH = 66;
const STATE_SCRIPT_DATA_DOUBLE_ESCAPED_DASH_DASH = 67;
const STATE_SCRIPT_DATA_DOUBLE_ESCAPED_LESS_THAN_SIGN = 68;
const STATE_SCRIPT_DATA_DOUBLE_ESCAPE_END = 69;

const STATE_PLAINTEXT = 70;

// https://html.spec.whatwg.org/multipage/parsing.html#character-reference-state
const STATE_CHARACTER_REFERENCE = 71;
// https://html.spec.whatwg.org/multipage/parsing.html#named-character-reference-state
const STATE_NAMED_CHARACTER_REFERENCE = 72;
// https://html.spec.whatwg.org/multipage/parsing.html#ambiguous-ampersand-state
const STATE_AMBIGUOUS_AMPERSAND = 73;
// https://html.spec.whatwg.org/multipage/parsing.html#numeric-character-reference-state
const STATE_NUMERIC_CHARACTER_REFERENCE = 74;
// https://html.spec.whatwg.org/multipage/parsing.html#hexadecimal-character-reference-start-state
const STATE_HEXADECIMAL_CHARACTER_REFERENCE_START = 75;
// https://html.spec.whatwg.org/multipage/parsing.html#decimal-character-reference-start-state
const STATE_DECIMAL_CHARACTER_REFERENCE_START = 76;
// https://html.spec.whatwg.org/multipage/parsing.html#hexadecimal-character-reference-state
const STATE_HEXADECIMAL_CHARACTER_REFERENCE = 77;
// https://html.spec.whatwg.org/multipage/parsing.html#decimal-character-reference-state
const STATE_DECIMAL_CHARACTER_REFERENCE = 78;
// https://html.spec.whatwg.org/multipage/parsing.html#numeric-character-reference-end-state
const STATE_NUMERIC_CHARACTER_REFERENCE_END = 79;

const CC_TAB = 0x09;
const CC_LF = 0x0a;
const CC_FF = 0x0c;
const CC_CR = 0x0d;
const CC_SPACE = 0x20;
const CC_NULL = 0x00;
const CC_EXCLAMATION_MARK = 0x21;
const CC_QUOTATION_MARK = 0x22;
const CC_NUMBER_SIGN = 0x23;
const CC_AMPERSAND = 0x26;
const CC_APOSTROPHE = 0x27;
const CC_HYPHEN_MINUS = 0x2d;
const CC_SOLIDUS = 0x2f;
const CC_SEMICOLON = 0x3b;
const CC_LESS_THAN = 0x3c;
const CC_EQUALS = 0x3d;
const CC_GREATER_THAN = 0x3e;
const CC_QUESTION_MARK = 0x3f;
const CC_LEFT_SQUARE_BRACKET = 0x5b;
const CC_RIGHT_SQUARE_BRACKET = 0x5d;
const CC_GRAVE_ACCENT = 0x60;

const QUOTE_DOUBLE = 1;
const QUOTE_SINGLE = 2;
const QUOTE_NONE = 0;

// Longest WHATWG named entity name *including* the trailing `;` is 32 chars
// (`CounterClockwiseContourIntegral;`); without the trailing `;` it's 31.
// Used to cap both the tokenizer's named-character-reference run length and
// the decoder's longest-prefix backtrack so pathological inputs (e.g. `&`
// followed by thousands of alphanumerics) stay linear-time.
const MAX_ENTITY_NAME_LEN = 32;

// ASCII character-class bit flags packed into one lookup table. The tokenizer
// runs these predicates per code point (tag names, attribute names, character
// references, whitespace skipping), so a single table load + mask replaces the
// per-call comparison chains. Code points >= 0x80 are never in any of these
// classes, so callers short-circuit on `cc < 0x80` before indexing.
const CCLS_SPACE = 1;
const CCLS_DIGIT = 2;
const CCLS_UPPER = 4;
const CCLS_LOWER = 8;
const CCLS_HEX = 16;
const CCLS_ALPHA = CCLS_UPPER | CCLS_LOWER;
const CCLS_ALNUM = CCLS_ALPHA | CCLS_DIGIT;
const CHAR_CLASS = new Uint8Array(128);
for (let i = 0; i < 128; i++) {
	let f = 0;
	if (
		i === CC_TAB ||
		i === CC_LF ||
		i === CC_FF ||
		i === CC_CR ||
		i === CC_SPACE
	) {
		f |= CCLS_SPACE;
	}
	if (i >= 0x30 && i <= 0x39) f |= CCLS_DIGIT | CCLS_HEX;
	if (i >= 0x41 && i <= 0x5a) f |= CCLS_UPPER;
	if (i >= 0x61 && i <= 0x7a) f |= CCLS_LOWER;
	if ((i >= 0x41 && i <= 0x46) || (i >= 0x61 && i <= 0x66)) f |= CCLS_HEX;
	CHAR_CLASS[i] = f;
}

/**
 * @param {number} cc character code
 * @returns {boolean} is ascii alpha
 */
const isAsciiAlpha = (cc) => cc < 0x80 && (CHAR_CLASS[cc] & CCLS_ALPHA) !== 0;

/**
 * @param {number} cc character code
 * @returns {boolean} is ascii alphanumeric
 */
const isAsciiAlphanumeric = (cc) =>
	cc < 0x80 && (CHAR_CLASS[cc] & CCLS_ALNUM) !== 0;

/**
 * @param {number} cc character code
 * @returns {boolean} is ascii digit
 */
const isAsciiDigit = (cc) => cc < 0x80 && (CHAR_CLASS[cc] & CCLS_DIGIT) !== 0;

/**
 * @param {number} cc character code
 * @returns {boolean} is ascii hex digit
 */
const isAsciiHexDigit = (cc) => cc < 0x80 && (CHAR_CLASS[cc] & CCLS_HEX) !== 0;

/**
 * @param {number} cc character code
 * @returns {boolean} is ascii upper alpha
 */
const isAsciiUpperAlpha = (cc) =>
	cc < 0x80 && (CHAR_CLASS[cc] & CCLS_UPPER) !== 0;

/**
 * @param {number} cc character code
 * @returns {boolean} is ascii lower alpha
 */
const isAsciiLowerAlpha = (cc) =>
	cc < 0x80 && (CHAR_CLASS[cc] & CCLS_LOWER) !== 0;

/**
 * Tokenizer whitespace. U+000D CARRIAGE RETURN is included because the spec's
 * input-stream preprocessing converts CR (and CRLF) to LF before tokenizing;
 * this scanner keeps the original offsets, so it treats a raw CR as whitespace
 * to match the post-preprocessing behaviour.
 * @param {number} cc character code
 * @returns {boolean} is space
 */
const isSpace = (cc) => cc < 0x80 && (CHAR_CLASS[cc] & CCLS_SPACE) !== 0;

/**
 * @param {number} code numeric character reference code point
 * @returns {boolean} is a Unicode noncharacter
 */
const isNoncharacter = (code) =>
	(code >= 0xfdd0 && code <= 0xfdef) || (code & 0xfffe) === 0xfffe;

/**
 * @param {number} code numeric character reference code point
 * @returns {boolean} is a C0/C1 control that is not ASCII whitespace
 */
const isControlReference = (code) =>
	code === 0x0d ||
	((code <= 0x1f || (code >= 0x7f && code <= 0x9f)) &&
		code !== CC_TAB &&
		code !== CC_LF &&
		code !== CC_FF &&
		code !== CC_SPACE);

/**
 * Severity of a tokenizer-detected parse error. `"warning"` is recoverable
 * (the tokenizer continued and the emitted token is still well-formed, e.g.
 * missing-attribute-value); `"error"` means the emitted token's offset
 * range is incomplete or does not match what the spec would produce, e.g.
 * eof-in-tag.
 *
 * Token offsets are JS string indices (UTF-16 code-unit offsets into
 * `input`), not byte offsets — relevant for inputs containing non-BMP
 * code points where one code point spans two indices.
 * @typedef {"warning" | "error"} ParseErrorSeverity
 */

/**
 * @typedef {object} HtmlTokenCallbacks
 * @property {(input: string, start: number, end: number, nameStart: number, nameEnd: number, selfClosing: boolean) => number=} openTag
 * @property {(input: string, start: number, end: number, nameStart: number, nameEnd: number) => number=} closeTag
 * @property {(input: string, start: number, end: number) => number=} text
 * @property {(input: string, nameStart: number, nameEnd: number, valueStart: number, valueEnd: number, quoteType: number) => number=} attribute
 * @property {(input: string, start: number, end: number) => number=} comment
 * @property {(input: string, start: number, end: number) => number=} doctype
 * @property {(input: string, code: string, start: number, end: number, severity: ParseErrorSeverity) => void=} parseError
 * @property {(() => boolean)=} isForeign returns true when the adjusted current node is in a foreign (SVG/MathML) namespace, vetoing RAWTEXT/RCDATA/script content-mode switches
 * @property {string=} fragmentContext context element tag name for fragment parsing; seeds the initial content mode
 */

/**
 * @param {string} name tag name (lowercase)
 * @returns {number} content mode state for this tag, or STATE_DATA
 */
const getContentModeForTag = (name) => {
	switch (name) {
		case "textarea":
		case "title":
			return STATE_RCDATA;
		case "style":
		case "xmp":
		case "iframe":
		case "noembed":
		case "noframes":
			return STATE_RAWTEXT;
		case "script":
			return STATE_SCRIPT_DATA;
		case "plaintext":
			return STATE_PLAINTEXT;
		default:
			return STATE_DATA;
	}
};

/**
 * Case-insensitive comparison of `input[start..end)` to a lowercase ASCII
 * literal, without allocating the slice.
 * @param {string} input input
 * @param {number} start range start
 * @param {number} end range end
 * @param {string} lit lowercase ASCII literal
 * @returns {boolean} true if the range equals `lit` ignoring ASCII case
 */
const rangeEqualsLower = (input, start, end, lit) => {
	if (end - start !== lit.length) return false;
	for (let i = 0; i < lit.length; i++) {
		let c = input.charCodeAt(start + i);
		if (c >= 0x41 && c <= 0x5a) c += 0x20;
		if (c !== lit.charCodeAt(i)) return false;
	}
	return true;
};

/**
 * Content mode for the just-opened tag whose name spans `input[start..end)`,
 * matched on the raw range so ordinary tags need neither a slice nor a
 * `toLowerCase`. Mirrors `getContentModeForTag`.
 * @param {string} input input
 * @param {number} start tag-name start
 * @param {number} end tag-name end
 * @returns {number} content mode state, or STATE_DATA
 */
const getContentModeForRange = (input, start, end) => {
	switch (end - start) {
		case 3:
			if (rangeEqualsLower(input, start, end, "xmp")) return STATE_RAWTEXT;
			return STATE_DATA;
		case 5:
			if (rangeEqualsLower(input, start, end, "title")) return STATE_RCDATA;
			if (rangeEqualsLower(input, start, end, "style")) return STATE_RAWTEXT;
			return STATE_DATA;
		case 6:
			if (rangeEqualsLower(input, start, end, "script")) {
				return STATE_SCRIPT_DATA;
			}
			if (rangeEqualsLower(input, start, end, "iframe")) return STATE_RAWTEXT;
			return STATE_DATA;
		case 7:
			if (rangeEqualsLower(input, start, end, "noembed")) return STATE_RAWTEXT;
			return STATE_DATA;
		case 8:
			if (rangeEqualsLower(input, start, end, "textarea")) return STATE_RCDATA;
			if (rangeEqualsLower(input, start, end, "noframes")) return STATE_RAWTEXT;
			return STATE_DATA;
		case 9:
			if (rangeEqualsLower(input, start, end, "plaintext")) {
				return STATE_PLAINTEXT;
			}
			return STATE_DATA;
		default:
			return STATE_DATA;
	}
};

/**
 * @param {string} input input string
 * @param {number} pos current position
 * @param {HtmlTokenCallbacks} callbacks callbacks
 * @returns {number} final position
 */
const walkHtmlTokens = (input, pos = 0, callbacks = {}) => {
	const len = input.length;
	let state = STATE_DATA;
	let returnState = STATE_DATA;

	let textStart = pos;
	let tagStart = pos;
	let tagNameStart = -1;
	let tagNameEnd = -1;
	let attrNameStart = -1;
	let attrNameEnd = -1;
	let attrValueStart = -1;
	let attrQuoteType = QUOTE_NONE;
	let commentStart = pos;
	let lastOpenTagName = "";
	// Tag-name offsets of the last open tag; the lowercased `lastOpenTagName` is
	// derived from these lazily (only for special-content tags).
	let lastOpenTagStart = -1;
	let lastOpenTagEnd = -1;
	// Counter used by SCRIPT_DATA_DOUBLE_ESCAPE_{START,END} to detect whether
	// the ASCII-alpha run after `<` / `</` spells exactly `"script"`. Values
	// 0..6 = number of chars matched so far; 7 = no longer matches (sentinel).
	// Avoids growing a buffer for pathological inputs with long alpha runs.
	let scriptMatch = 0;
	let namedEntityConsumed = 0;
	// Offset of the opening `&` and the running numeric value (clamped past the
	// Unicode range so it can't overflow); used for numeric-reference errors.
	let charRefStart = -1;
	let charRefCode = 0;
	// Tracks whether the current tag has parsed any attributes — used to
	// fire the `end-tag-with-attributes` parse error when an end tag emits.
	let tagHasAttributes = false;

	/**
	 * Reports a tokenizer parse error to the consumer. The offset range and
	 * severity follow the WHATWG spec naming. Severity is `"error"` for
	 * cases where the emitted token is incomplete (EOF inside a tag or
	 * comment); everything else is a `"warning"`. Offsets are JS string
	 * indices (UTF-16 code-unit offsets into `input`).
	 * @param {string} code WHATWG parse-error code (kebab-case)
	 * @param {number} start string offset where the error starts
	 * @param {number} end string offset where the error ends
	 * @param {ParseErrorSeverity} severity error severity
	 */
	const reportError = (code, start, end, severity) => {
		if (callbacks.parseError !== undefined) {
			callbacks.parseError(input, code, start, end, severity);
		}
	};

	/**
	 * Emits the WHATWG numeric-character-reference validation parse error for
	 * the accumulated `charRefCode`, if any. Used both inline (when the
	 * reference is terminated by a real next character) and at EOF (when the
	 * reference runs to the end of input). The scanner only flags the error —
	 * the spec's U+FFFD / Windows-1252 substitution is done by `decodeHtmlEntities`.
	 * @param {number} endPos offset just past the reference
	 */
	const validateNumericReference = (endPos) => {
		if (charRefCode === 0) {
			reportError("null-character-reference", charRefStart, endPos, "warning");
		} else if (charRefCode > 0x10ffff) {
			reportError(
				"character-reference-outside-unicode-range",
				charRefStart,
				endPos,
				"warning"
			);
		} else if (charRefCode >= 0xd800 && charRefCode <= 0xdfff) {
			reportError(
				"surrogate-character-reference",
				charRefStart,
				endPos,
				"warning"
			);
		} else if (isNoncharacter(charRefCode)) {
			reportError(
				"noncharacter-character-reference",
				charRefStart,
				endPos,
				"warning"
			);
		} else if (isControlReference(charRefCode)) {
			reportError(
				"control-character-reference",
				charRefStart,
				endPos,
				"warning"
			);
		}
	};

	// Content mode for the tag just opened (name at `lastOpenTagStart..End`). In
	// foreign content (SVG/MathML) the tree builder vetoes RAWTEXT/RCDATA/script
	// switching via `isForeign`, so e.g. an SVG `<title>`/`<style>` is parsed as
	// normal markup. `lastOpenTagName` (the lowercased name compared by the
	// special end-tag states) is materialized only when a special mode is
	// actually entered — ordinary tags never allocate it.
	const contentModeAfterOpenTag = () => {
		const m = getContentModeForRange(input, lastOpenTagStart, lastOpenTagEnd);
		// Ordinary tags stay in data state regardless of `isForeign` (which only
		// vetoes a switch *into* a special mode), so skip the per-open-tag
		// `isForeign` callback for them.
		if (m === STATE_DATA) return STATE_DATA;
		if (callbacks.isForeign !== undefined && callbacks.isForeign()) {
			return STATE_DATA;
		}
		lastOpenTagName = input
			.slice(lastOpenTagStart, lastOpenTagEnd)
			.toLowerCase();
		return m;
	};

	// HTML fragment parsing: seed the tokenizer with the context element's
	// content mode (e.g. a `textarea`/`style`/`script` context starts in
	// RCDATA/RAWTEXT/script-data rather than data state).
	if (callbacks.fragmentContext !== undefined) {
		lastOpenTagName = callbacks.fragmentContext;
		state =
			callbacks.isForeign !== undefined && callbacks.isForeign()
				? STATE_DATA
				: getContentModeForTag(lastOpenTagName);
	}

	/**
	 * @param {number} endPos end position
	 */
	const flushText = (endPos) => {
		if (textStart < endPos) {
			if (callbacks.text !== undefined) {
				callbacks.text(input, textStart, endPos);
			}
			// Advance `textStart` so a second `flushText` for the same span
			// (e.g. from the EOF handler after a tag-open transition already
			// flushed the pending text) is a no-op rather than a duplicate
			// emit. emitOpenTag / emitCloseTag overwrite `textStart` with
			// their own `nextPos` anyway, so this doesn't shift their start.
			textStart = endPos;
		}
	};

	/**
	 * @param {number} endPos end position
	 * @returns {number} next position
	 */
	const emitAttribute = (endPos) => {
		// Default `nextPos` advances past the closing quote (if any) so the
		// state machine can continue when no `attribute` callback is provided.
		// When a callback IS provided, its return value overrides the default —
		// the callback is expected to do the same advance based on the
		// reported `quoteType`.
		let nextPos = attrQuoteType === QUOTE_NONE ? endPos : endPos + 1;
		if (callbacks.attribute !== undefined && attrNameStart !== -1) {
			nextPos = callbacks.attribute(
				input,
				attrNameStart,
				attrNameEnd,
				attrValueStart,
				attrValueStart === -1 ? -1 : endPos,
				attrQuoteType
			);
		}
		if (attrNameStart !== -1) tagHasAttributes = true;
		attrNameStart = -1;
		attrValueStart = -1;
		attrQuoteType = QUOTE_NONE;
		return nextPos;
	};

	/**
	 * @param {number} endPos end position
	 * @param {boolean} selfClosing is self closing
	 * @returns {number} next position
	 */
	const emitOpenTag = (endPos, selfClosing) => {
		let nextPos = endPos;
		if (callbacks.openTag !== undefined) {
			nextPos = callbacks.openTag(
				input,
				tagStart,
				endPos,
				tagNameStart,
				tagNameEnd,
				selfClosing
			);
		}
		if (!selfClosing) {
			// Record offsets only; `contentModeAfterOpenTag` lowercases lazily.
			lastOpenTagStart = tagNameStart;
			lastOpenTagEnd = tagNameEnd;
		}
		tagHasAttributes = false;
		textStart = nextPos;
		return nextPos;
	};

	/**
	 * @param {number} endPos end position
	 * @returns {number} next position
	 */
	const emitCloseTag = (endPos) => {
		// Per WHATWG: an end tag emitted with attributes is a parse error.
		if (tagHasAttributes) {
			reportError("end-tag-with-attributes", tagStart, endPos, "warning");
		}
		let nextPos = endPos;
		if (callbacks.closeTag !== undefined) {
			nextPos = callbacks.closeTag(
				input,
				tagStart,
				endPos,
				tagNameStart,
				tagNameEnd
			);
		}
		tagHasAttributes = false;
		textStart = nextPos;
		return nextPos;
	};

	while (pos < len) {
		const cc = input.charCodeAt(pos);

		// All WHATWG tokenizer states handled. Deliberately omitted parse errors
		// (need state this offset scanner lacks): duplicate-attribute,
		// cdata-in-html-content, `*-in-input-stream`. Reference substitution is
		// left to `decodeHtmlEntities`.
		switch (state) {
			// https://html.spec.whatwg.org/multipage/parsing.html#data-state
			case STATE_DATA:
				// Consume the next input character:
				// U+003C LESS-THAN SIGN (<)
				// Set the return state to the data state. Switch to the tag open state.
				if (cc === CC_LESS_THAN) {
					tagStart = pos;
					state = STATE_TAG_OPEN;
					pos++;
				} else if (cc === CC_AMPERSAND) {
					// U+0026 AMPERSAND (&)
					// Set the return state to the data state. Switch to the
					// character reference state.
					returnState = STATE_DATA;
					state = STATE_CHARACTER_REFERENCE;
					pos++;
				} else if (cc === CC_NULL) {
					// U+0000 NULL: unexpected-null-character (the data state
					// emits the NULL as-is; the scanner only flags the error).
					reportError("unexpected-null-character", pos, pos + 1, "warning");
					pos++;
				} else {
					// Fast-forward over the run of ordinary text without re-entering
					// the per-state switch; stop on the next significant code point.
					pos++;
					while (pos < len) {
						const c2 = input.charCodeAt(pos);
						if (c2 === CC_LESS_THAN || c2 === CC_AMPERSAND || c2 === CC_NULL) {
							break;
						}
						pos++;
					}
				}
				break;

			// https://html.spec.whatwg.org/multipage/parsing.html#tag-open-state
			case STATE_TAG_OPEN:
				// Consume the next input character:
				// U+002F SOLIDUS (/)
				// Switch to the end tag open state.
				if (cc === CC_SOLIDUS) {
					state = STATE_END_TAG_OPEN;
					pos++;
				} else if (cc === CC_EXCLAMATION_MARK) {
					// U+0021 EXCLAMATION MARK (!)
					// Switch to the markup declaration open state.
					flushText(tagStart);
					commentStart = tagStart;
					state = STATE_MARKUP_DECLARATION_OPEN;
					pos++;
				} else if (isAsciiAlpha(cc)) {
					// ASCII alpha
					// Create a new start tag token, set its tag name to the empty string.
					// Reconsume in the tag name state.
					flushText(tagStart);
					tagNameStart = pos;
					state = STATE_TAG_NAME;
					// Reconsume
				} else if (cc === CC_QUESTION_MARK) {
					// U+003F QUESTION MARK (?)
					// This is an unexpected-question-mark-instead-of-tag-name parse error.
					// Create a comment token whose data is the empty string. Reconsume in the
					// bogus comment state.
					reportError(
						"unexpected-question-mark-instead-of-tag-name",
						pos,
						pos + 1,
						"warning"
					);
					flushText(tagStart);
					commentStart = tagStart;
					state = STATE_BOGUS_COMMENT;
					// Reconsume — let the bogus-comment state consume the `?`
					// itself, matching the spec.
				} else {
					// Anything else
					// This is an invalid-first-character-of-tag-name parse error. Emit a U+003C
					// LESS-THAN SIGN character token. Reconsume in the data state.
					reportError(
						"invalid-first-character-of-tag-name",
						pos,
						pos + 1,
						"warning"
					);
					state = STATE_DATA;
					// Reconsume
				}
				break;

			// https://html.spec.whatwg.org/multipage/parsing.html#end-tag-open-state
			case STATE_END_TAG_OPEN:
				// Consume the next input character:
				// ASCII alpha
				// Create a new end tag token, set its tag name to the empty string.
				// Reconsume in the tag name state.
				if (isAsciiAlpha(cc)) {
					flushText(tagStart);
					tagNameStart = pos;
					state = STATE_TAG_NAME;
					// Reconsume
				} else if (cc === CC_GREATER_THAN) {
					// U+003E GREATER-THAN SIGN (>)
					// This is a missing-end-tag-name parse error. Switch to the data state.
					reportError("missing-end-tag-name", pos, pos + 1, "warning");
					state = STATE_DATA;
					pos++;
				} else {
					// Anything else
					// This is an invalid-first-character-of-tag-name parse error. Create a
					// comment token whose data is the empty string. Reconsume in the bogus
					// comment state.
					reportError(
						"invalid-first-character-of-tag-name",
						pos,
						pos + 1,
						"warning"
					);
					flushText(tagStart);
					commentStart = tagStart;
					state = STATE_BOGUS_COMMENT;
					// Reconsume — let bogus-comment consume this char itself.
				}
				break;

			// https://html.spec.whatwg.org/multipage/parsing.html#tag-name-state
			case STATE_TAG_NAME:
				// Consume the next input character:
				// U+0009 CHARACTER TABULATION (tab)
				// U+000A LINE FEED (LF)
				// U+000C FORM FEED (FF)
				// U+0020 SPACE
				// Switch to the before attribute name state.
				if (isSpace(cc)) {
					tagNameEnd = pos;
					state = STATE_BEFORE_ATTRIBUTE_NAME;
					pos++;
				} else if (cc === CC_SOLIDUS) {
					// U+002F SOLIDUS (/)
					// Switch to the self-closing start tag state.
					tagNameEnd = pos;
					state = STATE_SELF_CLOSING_START_TAG;
					pos++;
				} else if (cc === CC_GREATER_THAN) {
					// U+003E GREATER-THAN SIGN (>)
					// Switch to the data state. Emit the current tag token.
					tagNameEnd = pos;
					if (input.charCodeAt(tagStart + 1) === CC_SOLIDUS) {
						state = STATE_DATA;
						pos = emitCloseTag(pos + 1);
					} else {
						const nextPos = emitOpenTag(pos + 1, false);
						state = nextPos > pos + 1 ? STATE_DATA : contentModeAfterOpenTag();
						pos = nextPos;
					}
				} else {
					// U+0000 NULL: unexpected-null-character (append U+FFFD).
					if (cc === CC_NULL) {
						reportError("unexpected-null-character", pos, pos + 1, "warning");
					}
					// Fast-forward over the ordinary run of the tag name.
					pos++;
					while (pos < len) {
						const c2 = input.charCodeAt(pos);
						if (
							c2 === CC_SOLIDUS ||
							c2 === CC_GREATER_THAN ||
							c2 === CC_NULL ||
							isSpace(c2)
						) {
							break;
						}
						pos++;
					}
				}
				break;

			// https://html.spec.whatwg.org/multipage/parsing.html#before-attribute-name-state
			case STATE_BEFORE_ATTRIBUTE_NAME:
				// Consume the next input character:
				// U+0009 CHARACTER TABULATION (tab)
				// U+000A LINE FEED (LF)
				// U+000C FORM FEED (FF)
				// U+0020 SPACE
				// Ignore the character.
				// Reconsume so space is handled in BEFORE_ATTRIBUTE_NAME
				if (isSpace(cc)) {
					pos++;
				} else if (cc === CC_SOLIDUS || cc === CC_GREATER_THAN) {
					// U+002F SOLIDUS (/)
					// U+003E GREATER-THAN SIGN (>)
					// EOF
					// Reconsume in the after attribute name state.
					state = STATE_AFTER_ATTRIBUTE_NAME;
					// Reconsume
				} else if (cc === CC_EQUALS) {
					// U+003D EQUALS SIGN (=)
					// This is an unexpected-equals-sign-before-attribute-name parse
					// error. Start a new attribute. Switch to the attribute name state.
					reportError(
						"unexpected-equals-sign-before-attribute-name",
						pos,
						pos + 1,
						"warning"
					);
					attrNameStart = pos;
					state = STATE_ATTRIBUTE_NAME;
					pos++;
				} else {
					// Anything else
					// Start a new attribute in the current tag token. Set that attribute name
					// and value to the empty string. Reconsume in the attribute name state.
					attrNameStart = pos;
					state = STATE_ATTRIBUTE_NAME;
					// Reconsume
				}
				break;

			// https://html.spec.whatwg.org/multipage/parsing.html#attribute-name-state
			case STATE_ATTRIBUTE_NAME:
				// Consume the next input character:
				// U+0009 CHARACTER TABULATION (tab)
				// U+000A LINE FEED (LF)
				// U+000C FORM FEED (FF)
				// U+0020 SPACE
				// U+002F SOLIDUS (/)
				// U+003E GREATER-THAN SIGN (>)
				// EOF
				// Reconsume in the after attribute name state.
				if (isSpace(cc) || cc === CC_SOLIDUS || cc === CC_GREATER_THAN) {
					attrNameEnd = pos;
					state = STATE_AFTER_ATTRIBUTE_NAME;
					// Reconsume
				} else if (cc === CC_EQUALS) {
					attrNameEnd = pos;
					state = STATE_BEFORE_ATTRIBUTE_VALUE;
					pos++;
				} else {
					// NULL -> unexpected-null-character; `"` `'` `<` -> unexpected-character-in-attribute-name.
					if (cc === CC_NULL) {
						reportError("unexpected-null-character", pos, pos + 1, "warning");
					} else if (
						cc === CC_QUOTATION_MARK ||
						cc === CC_APOSTROPHE ||
						cc === CC_LESS_THAN
					) {
						reportError(
							"unexpected-character-in-attribute-name",
							pos,
							pos + 1,
							"warning"
						);
					}
					// Fast-forward over the ordinary run of the attribute name; stop on
					// any terminator (space / `/` / `>` / `=`) or a char that needs a
					// per-occurrence parse error (NULL / `"` / `'` / `<`), which the
					// outer switch then re-handles.
					pos++;
					while (pos < len) {
						const c2 = input.charCodeAt(pos);
						if (
							c2 === CC_SOLIDUS ||
							c2 === CC_GREATER_THAN ||
							c2 === CC_EQUALS ||
							c2 === CC_NULL ||
							c2 === CC_QUOTATION_MARK ||
							c2 === CC_APOSTROPHE ||
							c2 === CC_LESS_THAN ||
							isSpace(c2)
						) {
							break;
						}
						pos++;
					}
				}
				break;

			// https://html.spec.whatwg.org/multipage/parsing.html#after-attribute-name-state
			case STATE_AFTER_ATTRIBUTE_NAME:
				// Consume the next input character:
				// U+0009 CHARACTER TABULATION (tab)
				// U+000A LINE FEED (LF)
				// U+000C FORM FEED (FF)
				// U+0020 SPACE
				// Ignore the character.
				if (isSpace(cc)) {
					pos++;
				} else if (cc === CC_SOLIDUS) {
					// U+002F SOLIDUS (/)
					// Switch to the self-closing start tag state.
					emitAttribute(pos);
					state = STATE_SELF_CLOSING_START_TAG;
					pos++;
				} else if (cc === CC_EQUALS) {
					// U+003D EQUALS SIGN (=)
					// Switch to the before attribute value state.
					state = STATE_BEFORE_ATTRIBUTE_VALUE;
					pos++;
				} else if (cc === CC_GREATER_THAN) {
					// U+003E GREATER-THAN SIGN (>)
					// Switch to the data state. Emit the current tag token.
					emitAttribute(pos);
					if (input.charCodeAt(tagStart + 1) === CC_SOLIDUS) {
						state = STATE_DATA;
						pos = emitCloseTag(pos + 1);
					} else {
						const nextPos = emitOpenTag(pos + 1, false);
						state = nextPos > pos + 1 ? STATE_DATA : contentModeAfterOpenTag();
						pos = nextPos;
					}
				} else {
					// Anything else
					// Start a new attribute in the current tag token.
					emitAttribute(pos);
					attrNameStart = pos;
					state = STATE_ATTRIBUTE_NAME;
					// Reconsume
				}
				break;

			// https://html.spec.whatwg.org/multipage/parsing.html#before-attribute-value-state
			case STATE_BEFORE_ATTRIBUTE_VALUE:
				// Consume the next input character:
				// U+0009 CHARACTER TABULATION (tab)
				// U+000A LINE FEED (LF)
				// U+000C FORM FEED (FF)
				// U+0020 SPACE
				// Ignore the character.
				if (isSpace(cc)) {
					pos++;
				} else if (cc === CC_QUOTATION_MARK) {
					// U+0022 QUOTATION MARK (")
					// Switch to the attribute value (double-quoted) state.
					attrValueStart = pos + 1;
					attrQuoteType = QUOTE_DOUBLE;
					state = STATE_ATTRIBUTE_VALUE_DOUBLE_QUOTED;
					pos++;
				} else if (cc === CC_APOSTROPHE) {
					// U+0027 APOSTROPHE (')
					// Switch to the attribute value (single-quoted) state.
					attrValueStart = pos + 1;
					attrQuoteType = QUOTE_SINGLE;
					state = STATE_ATTRIBUTE_VALUE_SINGLE_QUOTED;
					pos++;
				} else if (cc === CC_GREATER_THAN) {
					// U+003E GREATER-THAN SIGN (>)
					// This is a missing-attribute-value parse error. Switch to the data
					// state. Emit the current tag token. The attribute is reported with
					// an empty value range pointing at the `>` so the open-tag offset range
					// still includes the `>`.
					reportError("missing-attribute-value", pos, pos + 1, "warning");
					attrValueStart = pos;
					attrQuoteType = QUOTE_NONE;
					pos = emitAttribute(pos);
					if (input.charCodeAt(tagStart + 1) === CC_SOLIDUS) {
						state = STATE_DATA;
						pos = emitCloseTag(pos + 1);
					} else {
						const nextPos = emitOpenTag(pos + 1, false);
						state = nextPos > pos + 1 ? STATE_DATA : contentModeAfterOpenTag();
						pos = nextPos;
					}
				} else {
					// Anything else
					// Reconsume in the attribute value (unquoted) state.
					attrValueStart = pos;
					attrQuoteType = QUOTE_NONE;
					state = STATE_ATTRIBUTE_VALUE_UNQUOTED;
					// Reconsume
				}
				break;

			// https://html.spec.whatwg.org/multipage/parsing.html#attribute-value-(double-quoted)-state
			case STATE_ATTRIBUTE_VALUE_DOUBLE_QUOTED:
				// Consume the next input character:
				// U+0022 QUOTATION MARK (")
				// Switch to the after attribute value (quoted) state.
				if (cc === CC_QUOTATION_MARK) {
					pos = emitAttribute(pos);
					state = STATE_AFTER_ATTRIBUTE_VALUE_QUOTED;
				} else if (cc === CC_AMPERSAND) {
					// U+0026 AMPERSAND (&)
					// Set the return state to the attribute value (double-quoted)
					// state. Switch to the character reference state.
					returnState = STATE_ATTRIBUTE_VALUE_DOUBLE_QUOTED;
					state = STATE_CHARACTER_REFERENCE;
					pos++;
				} else if (cc === CC_NULL) {
					// U+0000 NULL: unexpected-null-character (append U+FFFD).
					reportError("unexpected-null-character", pos, pos + 1, "warning");
					pos++;
				} else {
					// Fast-forward over the ordinary run of the quoted value.
					pos++;
					while (pos < len) {
						const c2 = input.charCodeAt(pos);
						if (
							c2 === CC_QUOTATION_MARK ||
							c2 === CC_AMPERSAND ||
							c2 === CC_NULL
						) {
							break;
						}
						pos++;
					}
				}
				break;

			// https://html.spec.whatwg.org/multipage/parsing.html#attribute-value-(single-quoted)-state
			case STATE_ATTRIBUTE_VALUE_SINGLE_QUOTED:
				// Consume the next input character:
				// U+0027 APOSTROPHE (')
				// Switch to the after attribute value (quoted) state.
				if (cc === CC_APOSTROPHE) {
					pos = emitAttribute(pos);
					state = STATE_AFTER_ATTRIBUTE_VALUE_QUOTED;
				} else if (cc === CC_AMPERSAND) {
					// U+0026 AMPERSAND (&)
					// Set the return state to the attribute value (single-quoted)
					// state. Switch to the character reference state.
					returnState = STATE_ATTRIBUTE_VALUE_SINGLE_QUOTED;
					state = STATE_CHARACTER_REFERENCE;
					pos++;
				} else if (cc === CC_NULL) {
					// U+0000 NULL: unexpected-null-character (append U+FFFD).
					reportError("unexpected-null-character", pos, pos + 1, "warning");
					pos++;
				} else {
					// Fast-forward over the ordinary run of the quoted value.
					pos++;
					while (pos < len) {
						const c2 = input.charCodeAt(pos);
						if (c2 === CC_APOSTROPHE || c2 === CC_AMPERSAND || c2 === CC_NULL) {
							break;
						}
						pos++;
					}
				}
				break;

			// https://html.spec.whatwg.org/multipage/parsing.html#attribute-value-(unquoted)-state
			case STATE_ATTRIBUTE_VALUE_UNQUOTED:
				if (isSpace(cc)) {
					pos = emitAttribute(pos);
					state = STATE_BEFORE_ATTRIBUTE_NAME;
					// Reconsume so space is handled in BEFORE_ATTRIBUTE_NAME
				} else if (cc === CC_GREATER_THAN) {
					// U+003E GREATER-THAN SIGN (>)
					// This is a missing-attribute-value parse error. Switch to the data state.
					// Emit the current tag token.
					pos = emitAttribute(pos);
					if (input.charCodeAt(tagStart + 1) === CC_SOLIDUS) {
						state = STATE_DATA;
						pos = emitCloseTag(pos + 1);
					} else {
						const nextPos = emitOpenTag(pos + 1, false);
						state = nextPos > pos + 1 ? STATE_DATA : contentModeAfterOpenTag();
						pos = nextPos;
					}
				} else if (cc === CC_AMPERSAND) {
					// U+0026 AMPERSAND (&)
					// Set the return state to the attribute value (unquoted)
					// state. Switch to the character reference state.
					returnState = STATE_ATTRIBUTE_VALUE_UNQUOTED;
					state = STATE_CHARACTER_REFERENCE;
					pos++;
				} else {
					// NULL -> unexpected-null-character; `"` `'` `<` `=` `` ` `` -> unexpected-character-in-unquoted-attribute-value.
					if (cc === CC_NULL) {
						reportError("unexpected-null-character", pos, pos + 1, "warning");
					} else if (
						cc === CC_QUOTATION_MARK ||
						cc === CC_APOSTROPHE ||
						cc === CC_LESS_THAN ||
						cc === CC_EQUALS ||
						cc === CC_GRAVE_ACCENT
					) {
						reportError(
							"unexpected-character-in-unquoted-attribute-value",
							pos,
							pos + 1,
							"warning"
						);
					}
					pos++;
				}
				break;

			// https://html.spec.whatwg.org/multipage/parsing.html#after-attribute-value-(quoted)-state
			case STATE_AFTER_ATTRIBUTE_VALUE_QUOTED:
				// Consume the next input character:
				// U+0009 CHARACTER TABULATION (tab)
				// U+000A LINE FEED (LF)
				// U+000C FORM FEED (FF)
				// U+0020 SPACE
				// Switch to the before attribute name state.
				if (isSpace(cc)) {
					state = STATE_BEFORE_ATTRIBUTE_NAME;
					pos++;
				} else if (cc === CC_SOLIDUS) {
					// U+002F SOLIDUS (/)
					// Switch to the self-closing start tag state.
					state = STATE_SELF_CLOSING_START_TAG;
					pos++;
				} else if (cc === CC_GREATER_THAN) {
					if (input.charCodeAt(tagStart + 1) === CC_SOLIDUS) {
						state = STATE_DATA;
						pos = emitCloseTag(pos + 1);
					} else {
						const nextPos = emitOpenTag(pos + 1, false);
						state = nextPos > pos + 1 ? STATE_DATA : contentModeAfterOpenTag();
						pos = nextPos;
					}
				} else {
					// Anything else
					// This is a missing-whitespace-between-attributes parse error. Reconsume in
					// the before attribute name state.
					reportError(
						"missing-whitespace-between-attributes",
						pos,
						pos + 1,
						"warning"
					);
					state = STATE_BEFORE_ATTRIBUTE_NAME;
					// Reconsume
				}
				break;

			// https://html.spec.whatwg.org/multipage/parsing.html#self-closing-start-tag-state
			case STATE_SELF_CLOSING_START_TAG:
				// Consume the next input character:
				// U+003E GREATER-THAN SIGN (>)
				// Set the self-closing flag of the current tag token. Switch to the data
				// state. Emit the current tag token.
				if (cc === CC_GREATER_THAN) {
					if (input.charCodeAt(tagStart + 1) === CC_SOLIDUS) {
						// An end tag emitted with the self-closing flag set is an
						// end-tag-with-trailing-solidus parse error.
						reportError(
							"end-tag-with-trailing-solidus",
							tagStart,
							pos + 1,
							"warning"
						);
						state = STATE_DATA;
						pos = emitCloseTag(pos + 1);
					} else {
						pos = emitOpenTag(pos + 1, true);
						state = STATE_DATA;
					}
				} else {
					// Anything else
					// This is an unexpected-solidus-in-tag parse error. Reconsume in the before
					// attribute name state.
					reportError("unexpected-solidus-in-tag", pos, pos + 1, "warning");
					state = STATE_BEFORE_ATTRIBUTE_NAME;
					// Reconsume
				}
				break;

			// https://html.spec.whatwg.org/multipage/parsing.html#markup-declaration-open-state
			case STATE_MARKUP_DECLARATION_OPEN:
				// If the next few characters are:
				// Two U+002D HYPHEN-MINUS characters (-)
				// Consume those two characters, create a comment token whose data
				// is the empty string, and switch to the comment start state.
				if (
					cc === CC_HYPHEN_MINUS &&
					input.charCodeAt(pos + 1) === CC_HYPHEN_MINUS
				) {
					pos += 2;
					commentStart = tagStart;
					state = STATE_COMMENT_START;
				} else if (
					// ASCII case-insensitive match for the word "DOCTYPE"
					// Consume those characters and switch to the DOCTYPE state.
					(cc === 0x44 || cc === 0x64) /* D or d */ &&
					(input.charCodeAt(pos + 1) | 0x20) === 0x6f /* o */ &&
					(input.charCodeAt(pos + 2) | 0x20) === 0x63 /* c */ &&
					(input.charCodeAt(pos + 3) | 0x20) === 0x74 /* t */ &&
					(input.charCodeAt(pos + 4) | 0x20) === 0x79 /* y */ &&
					(input.charCodeAt(pos + 5) | 0x20) === 0x70 /* p */ &&
					(input.charCodeAt(pos + 6) | 0x20) === 0x65 /* e */
				) {
					pos += 7;
					commentStart = tagStart;
					state = STATE_DOCTYPE;
				} else if (
					// The string "[CDATA[" (the five uppercase letters "CDATA" with a
					// U+005B LEFT SQUARE BRACKET character before and after)
					// Consume those characters and switch to the CDATA section state.
					cc === CC_LEFT_SQUARE_BRACKET &&
					input.charCodeAt(pos + 1) === 0x43 /* C */ &&
					input.charCodeAt(pos + 2) === 0x44 /* D */ &&
					input.charCodeAt(pos + 3) === 0x41 /* A */ &&
					input.charCodeAt(pos + 4) === 0x54 /* T */ &&
					input.charCodeAt(pos + 5) === 0x41 /* A */ &&
					input.charCodeAt(pos + 6) === CC_LEFT_SQUARE_BRACKET
				) {
					pos += 7;
					commentStart = tagStart;
					state = STATE_CDATA_SECTION;
				} else {
					// Anything else
					// This is an incorrectly-opened-comment parse error. Create a comment token
					// whose data is the empty string. Switch to the bogus comment state (don't
					// consume anything in the current state).
					reportError("incorrectly-opened-comment", tagStart, pos, "warning");
					commentStart = tagStart;
					state = STATE_BOGUS_COMMENT;
					// Reconsume
				}
				break;

			// https://html.spec.whatwg.org/multipage/parsing.html#comment-start-state
			case STATE_COMMENT_START:
				// Consume the next input character:
				// U+002D HYPHEN-MINUS (-)
				// Switch to the comment start dash state.
				if (cc === CC_HYPHEN_MINUS) {
					state = STATE_COMMENT_START_DASH;
					pos++;
				} else if (cc === CC_GREATER_THAN) {
					// U+003E GREATER-THAN SIGN (>)
					// This is an abrupt-closing-of-empty-comment parse error. Switch to the
					// data state. Emit the current comment token.
					reportError(
						"abrupt-closing-of-empty-comment",
						pos,
						pos + 1,
						"warning"
					);
					let nextPos = pos + 1;
					if (callbacks.comment !== undefined) {
						nextPos = callbacks.comment(input, commentStart, pos + 1);
					}
					state = STATE_DATA;
					textStart = nextPos;
					pos = nextPos;
				} else {
					// Anything else
					// Reconsume in the comment state.
					state = STATE_COMMENT;
					// Reconsume
				}
				break;

			// https://html.spec.whatwg.org/multipage/parsing.html#comment-start-dash-state
			case STATE_COMMENT_START_DASH:
				// Consume the next input character:
				// U+002D HYPHEN-MINUS (-)
				// Switch to the comment end state.
				if (cc === CC_HYPHEN_MINUS) {
					state = STATE_COMMENT_END;
					pos++;
				} else if (cc === CC_GREATER_THAN) {
					// U+003E GREATER-THAN SIGN (>)
					// This is an abrupt-closing-of-empty-comment parse error. Switch to the
					// data state. Emit the current comment token.
					reportError(
						"abrupt-closing-of-empty-comment",
						pos,
						pos + 1,
						"warning"
					);
					let nextPos = pos + 1;
					if (callbacks.comment !== undefined) {
						nextPos = callbacks.comment(input, commentStart, pos + 1);
					}
					state = STATE_DATA;
					textStart = nextPos;
					pos = nextPos;
				} else {
					// Anything else
					// Append a U+002D HYPHEN-MINUS character (-) to the comment token's data.
					// Reconsume in the comment state.
					state = STATE_COMMENT;
					// Reconsume
				}
				break;

			// https://html.spec.whatwg.org/multipage/parsing.html#comment-state
			case STATE_COMMENT:
				// Consume the next input character:
				// U+003C LESS-THAN SIGN (<)
				// Append a U+003C LESS-THAN SIGN character to the comment token's data. Switch to the comment less-than sign state.
				if (cc === CC_LESS_THAN) {
					state = STATE_COMMENT_LESS_THAN_SIGN;
					pos++;
				} else if (cc === CC_HYPHEN_MINUS) {
					// Consume the next input character:
					// U+002D HYPHEN-MINUS (-)
					// Switch to the comment end dash state.
					state = STATE_COMMENT_END_DASH;
					pos++;
				} else {
					// U+0000 NULL: unexpected-null-character (append U+FFFD).
					if (cc === CC_NULL) {
						reportError("unexpected-null-character", pos, pos + 1, "warning");
					}
					pos++;
				}
				break;

			// https://html.spec.whatwg.org/multipage/parsing.html#comment-end-dash-state
			case STATE_COMMENT_END_DASH:
				// Consume the next input character:
				// U+002D HYPHEN-MINUS (-)
				// Switch to the comment end state.
				if (cc === CC_HYPHEN_MINUS) {
					state = STATE_COMMENT_END;
					pos++;
				} else {
					// Anything else
					// Append a U+002D HYPHEN-MINUS character (-) to the comment token's data.
					// Reconsume in the comment state (so e.g. NULL and `<` are
					// handled there).
					state = STATE_COMMENT;
					// Reconsume
				}
				break;

			// https://html.spec.whatwg.org/multipage/parsing.html#comment-end-state
			case STATE_COMMENT_END:
				// Consume the next input character:
				// U+003E GREATER-THAN SIGN (>)
				// Switch to the data state. Emit the current comment token.
				if (cc === CC_GREATER_THAN) {
					let nextPos = pos + 1;
					if (callbacks.comment !== undefined) {
						nextPos = callbacks.comment(input, commentStart, pos + 1);
					}
					state = STATE_DATA;
					textStart = nextPos;
					pos = nextPos;
				} else if (cc === CC_EXCLAMATION_MARK) {
					// U+0021 EXCLAMATION MARK (!)
					// Switch to the comment end bang state.
					state = STATE_COMMENT_END_BANG;
					pos++;
				} else if (cc === CC_HYPHEN_MINUS) {
					pos++;
				} else {
					// Anything else
					// Append two U+002D HYPHEN-MINUS characters (-) to the comment token's
					// data. Reconsume in the comment state (so NULL and `<` are
					// handled there).
					state = STATE_COMMENT;
					// Reconsume
				}
				break;

			// https://html.spec.whatwg.org/multipage/parsing.html#comment-end-bang-state
			case STATE_COMMENT_END_BANG:
				// Consume the next input character:
				// U+002D HYPHEN-MINUS (-)
				// Append two U+002D HYPHEN-MINUS characters (-) and a U+0021 EXCLAMATION
				// MARK character (!) to the comment token's data. Switch to the comment end
				// dash state.
				if (cc === CC_HYPHEN_MINUS) {
					state = STATE_COMMENT_END_DASH;
					pos++;
				} else if (cc === CC_GREATER_THAN) {
					// U+003E GREATER-THAN SIGN (>)
					// This is an incorrectly-closed-comment parse error. Switch to the data
					// state. Emit the current comment token.
					reportError("incorrectly-closed-comment", pos, pos + 1, "warning");
					let nextPos = pos + 1;
					if (callbacks.comment !== undefined) {
						nextPos = callbacks.comment(input, commentStart, pos + 1);
					}
					state = STATE_DATA;
					textStart = nextPos;
					pos = nextPos;
				} else {
					// Anything else
					// Append two U+002D HYPHEN-MINUS characters (-) and a U+0021 EXCLAMATION
					// MARK character (!) to the comment token's data. Reconsume in the comment
					// state (so NULL and `<` are handled there).
					state = STATE_COMMENT;
					// Reconsume
				}
				break;

			// https://html.spec.whatwg.org/multipage/parsing.html#bogus-comment-state
			case STATE_BOGUS_COMMENT:
				// Consume the next input character:
				// U+003E GREATER-THAN SIGN (>)
				// Switch to the data state. Emit the current comment token.
				if (cc === CC_GREATER_THAN) {
					let nextPos = pos + 1;
					if (callbacks.comment !== undefined) {
						nextPos = callbacks.comment(input, commentStart, pos + 1);
					}
					state = STATE_DATA;
					textStart = nextPos;
					pos = nextPos;
				} else {
					// U+0000 NULL: unexpected-null-character (append U+FFFD).
					if (cc === CC_NULL) {
						reportError("unexpected-null-character", pos, pos + 1, "warning");
					}
					pos++;
				}
				break;

			// https://html.spec.whatwg.org/multipage/parsing.html#comment-less-than-sign-state
			case STATE_COMMENT_LESS_THAN_SIGN:
				// Consume the next input character:
				// U+0021 EXCLAMATION MARK (!)
				// Append the current input character to the comment token's data. Switch to
				// the comment less-than sign bang state.
				if (cc === CC_EXCLAMATION_MARK) {
					state = STATE_COMMENT_LESS_THAN_SIGN_BANG;
					pos++;
				} else if (cc === CC_LESS_THAN) {
					// U+003C LESS-THAN SIGN (<)
					// Append the current input character to the comment token's data.
					pos++;
				} else {
					// Anything else
					// Reconsume in the comment state.
					state = STATE_COMMENT;
					// Reconsume
				}
				break;

			// https://html.spec.whatwg.org/multipage/parsing.html#comment-less-than-sign-bang-state
			case STATE_COMMENT_LESS_THAN_SIGN_BANG:
				// Consume the next input character:
				// U+002D HYPHEN-MINUS (-)
				// Switch to the comment less-than sign bang dash state.
				if (cc === CC_HYPHEN_MINUS) {
					state = STATE_COMMENT_LESS_THAN_SIGN_BANG_DASH;
					pos++;
				} else {
					// Anything else
					// Reconsume in the comment state.
					state = STATE_COMMENT;
					// Reconsume
				}
				break;

			// https://html.spec.whatwg.org/multipage/parsing.html#comment-less-than-sign-bang-dash-state
			case STATE_COMMENT_LESS_THAN_SIGN_BANG_DASH:
				// Consume the next input character:
				// U+002D HYPHEN-MINUS (-)
				// Switch to the comment less-than sign bang dash dash state.
				if (cc === CC_HYPHEN_MINUS) {
					state = STATE_COMMENT_LESS_THAN_SIGN_BANG_DASH_DASH;
					pos++;
				} else {
					// Anything else
					// Reconsume in the comment end dash state.
					state = STATE_COMMENT_END_DASH;
					// Reconsume
				}
				break;

			// https://html.spec.whatwg.org/multipage/parsing.html#comment-less-than-sign-bang-dash-dash-state
			case STATE_COMMENT_LESS_THAN_SIGN_BANG_DASH_DASH:
				// Consume the next input character:
				// U+003E GREATER-THAN SIGN (>)
				// EOF
				// Reconsume in the comment end state.
				// Anything else
				// This is a nested-comment parse error. Reconsume in the comment end state.
				if (cc !== CC_GREATER_THAN) {
					reportError("nested-comment", pos, pos + 1, "warning");
				}
				state = STATE_COMMENT_END;
				// Reconsume
				break;

			// https://html.spec.whatwg.org/multipage/parsing.html#doctype-state
			case STATE_DOCTYPE:
				// Consume the next input character:
				// U+0009 CHARACTER TABULATION (tab)
				// U+000A LINE FEED (LF)
				// U+000C FORM FEED (FF)
				// U+0020 SPACE
				// Switch to the before DOCTYPE name state.
				if (isSpace(cc)) {
					state = STATE_BEFORE_DOCTYPE_NAME;
					pos++;
				} else if (cc === CC_GREATER_THAN) {
					// U+003E GREATER-THAN SIGN (>)
					// Reconsume in the before DOCTYPE name state.
					state = STATE_BEFORE_DOCTYPE_NAME;
				} else {
					// Anything else
					// This is a missing-whitespace-before-doctype-name parse error. Reconsume
					// in the before DOCTYPE name state.
					reportError(
						"missing-whitespace-before-doctype-name",
						pos,
						pos + 1,
						"warning"
					);
					state = STATE_BEFORE_DOCTYPE_NAME;
				}
				break;

			// https://html.spec.whatwg.org/multipage/parsing.html#before-doctype-name-state
			case STATE_BEFORE_DOCTYPE_NAME:
				// Consume the next input character:
				// U+0009 CHARACTER TABULATION (tab)
				// U+000A LINE FEED (LF)
				// U+000C FORM FEED (FF)
				// U+0020 SPACE
				// Ignore the character.
				if (isSpace(cc)) {
					pos++;
				} else if (cc === CC_NULL) {
					// U+0000 NULL
					// This is an unexpected-null-character parse error. Create a new DOCTYPE
					// token. Set the token's name to a U+FFFD REPLACEMENT CHARACTER character.
					// Switch to the DOCTYPE name state.
					reportError("unexpected-null-character", pos, pos + 1, "warning");
					state = STATE_DOCTYPE_NAME;
					pos++;
				} else if (cc === CC_GREATER_THAN) {
					// U+003E GREATER-THAN SIGN (>)
					// This is a missing-doctype-name parse error. Create a new DOCTYPE token.
					// Set its force-quirks flag to on. Switch to the data state. Emit the
					// current token.
					reportError("missing-doctype-name", pos, pos + 1, "warning");
					let nextPos = pos + 1;
					if (callbacks.doctype !== undefined) {
						nextPos = callbacks.doctype(input, commentStart, pos + 1);
					}
					state = STATE_DATA;
					textStart = nextPos;
					pos = nextPos;
				} else {
					// ASCII upper alpha
					// Create a new DOCTYPE token. Set the token's name to the lowercase version
					// of the current input character (add 0x0020 to the character's code
					// point). Switch to the DOCTYPE name state.
					// Anything else
					// Create a new DOCTYPE token. Set the token's name to the current input
					// character. Switch to the DOCTYPE name state.
					state = STATE_DOCTYPE_NAME;
					pos++;
				}
				break;

			// https://html.spec.whatwg.org/multipage/parsing.html#doctype-name-state
			case STATE_DOCTYPE_NAME:
				// Consume the next input character:
				// U+0009 CHARACTER TABULATION (tab)
				// U+000A LINE FEED (LF)
				// U+000C FORM FEED (FF)
				// U+0020 SPACE
				// Switch to the after DOCTYPE name state.
				if (isSpace(cc)) {
					state = STATE_AFTER_DOCTYPE_NAME;
					pos++;
				} else if (cc === CC_GREATER_THAN) {
					// U+003E GREATER-THAN SIGN (>)
					// Switch to the data state. Emit the current DOCTYPE token.
					let nextPos = pos + 1;
					if (callbacks.doctype !== undefined) {
						nextPos = callbacks.doctype(input, commentStart, pos + 1);
					}
					state = STATE_DATA;
					textStart = nextPos;
					pos = nextPos;
				} else if (cc === CC_NULL) {
					// U+0000 NULL
					// This is an unexpected-null-character parse error. Append a U+FFFD
					// REPLACEMENT CHARACTER character to the current DOCTYPE token's name.
					reportError("unexpected-null-character", pos, pos + 1, "warning");
					pos++;
				} else {
					// ASCII upper alpha
					// Append the lowercase version of the current input character (add 0x0020
					// to the character's code point) to the current DOCTYPE token's name.
					// Anything else
					// Append the current input character to the current DOCTYPE token's name.
					pos++;
				}
				break;

			// https://html.spec.whatwg.org/multipage/parsing.html#after-doctype-name-state
			case STATE_AFTER_DOCTYPE_NAME:
				// Consume the next input character:
				if (isSpace(cc)) {
					// U+0009 CHARACTER TABULATION (tab)
					// U+000A LINE FEED (LF)
					// U+000C FORM FEED (FF)
					// U+0020 SPACE
					// Ignore the character.
					pos++;
				} else if (cc === CC_GREATER_THAN) {
					// U+003E GREATER-THAN SIGN (>)
					// Switch to the data state. Emit the current DOCTYPE token.
					let nextPos = pos + 1;
					if (callbacks.doctype !== undefined) {
						nextPos = callbacks.doctype(input, commentStart, pos + 1);
					}
					state = STATE_DATA;
					textStart = nextPos;
					pos = nextPos;
				} else if (
					pos + 5 < len &&
					(cc === 0x50 || cc === 0x70) /* P or p */ &&
					(input.charCodeAt(pos + 1) | 0x20) === 0x75 /* u */ &&
					(input.charCodeAt(pos + 2) | 0x20) === 0x62 /* b */ &&
					(input.charCodeAt(pos + 3) | 0x20) === 0x6c /* l */ &&
					(input.charCodeAt(pos + 4) | 0x20) === 0x69 /* i */ &&
					(input.charCodeAt(pos + 5) | 0x20) === 0x63 /* c */
				) {
					// ASCII case-insensitive match for the word "PUBLIC"
					pos += 6;
					state = STATE_AFTER_DOCTYPE_PUBLIC_KEYWORD;
				} else if (
					pos + 5 < len &&
					(cc === 0x53 || cc === 0x73) /* S or s */ &&
					(input.charCodeAt(pos + 1) | 0x20) === 0x79 /* y */ &&
					(input.charCodeAt(pos + 2) | 0x20) === 0x73 /* s */ &&
					(input.charCodeAt(pos + 3) | 0x20) === 0x74 /* t */ &&
					(input.charCodeAt(pos + 4) | 0x20) === 0x65 /* e */ &&
					(input.charCodeAt(pos + 5) | 0x20) === 0x6d /* m */
				) {
					// ASCII case-insensitive match for the word "SYSTEM"
					pos += 6;
					state = STATE_AFTER_DOCTYPE_SYSTEM_KEYWORD;
				} else {
					// Anything else
					// This is an invalid-character-sequence-after-doctype-name parse error. Set
					// the current DOCTYPE token's force-quirks flag to on. Reconsume in the
					// bogus DOCTYPE state.
					reportError(
						"invalid-character-sequence-after-doctype-name",
						pos,
						pos + 1,
						"warning"
					);
					state = STATE_BOGUS_DOCTYPE;
				}
				break;

			// https://html.spec.whatwg.org/multipage/parsing.html#after-doctype-public-keyword-state
			case STATE_AFTER_DOCTYPE_PUBLIC_KEYWORD:
				// Consume the next input character:
				if (isSpace(cc)) {
					// U+0009 CHARACTER TABULATION (tab)
					// U+000A LINE FEED (LF)
					// U+000C FORM FEED (FF)
					// U+0020 SPACE
					// Switch to the before DOCTYPE public identifier state.
					state = STATE_BEFORE_DOCTYPE_PUBLIC_IDENTIFIER;
					pos++;
				} else if (cc === CC_QUOTATION_MARK) {
					// U+0022 QUOTATION MARK (")
					// This is a missing-whitespace-after-doctype-public-keyword parse error.
					// Set the current DOCTYPE token's public identifier to the empty string
					// (not missing), then switch to the DOCTYPE public identifier
					// (double-quoted) state.
					reportError(
						"missing-whitespace-after-doctype-public-keyword",
						pos,
						pos + 1,
						"warning"
					);
					state = STATE_DOCTYPE_PUBLIC_IDENTIFIER_DOUBLE_QUOTED;
					pos++;
				} else if (cc === CC_APOSTROPHE) {
					// U+0027 APOSTROPHE (')
					// This is a missing-whitespace-after-doctype-public-keyword parse error.
					// Set the current DOCTYPE token's public identifier to the empty string
					// (not missing), then switch to the DOCTYPE public identifier
					// (single-quoted) state.
					reportError(
						"missing-whitespace-after-doctype-public-keyword",
						pos,
						pos + 1,
						"warning"
					);
					state = STATE_DOCTYPE_PUBLIC_IDENTIFIER_SINGLE_QUOTED;
					pos++;
				} else if (cc === CC_GREATER_THAN) {
					// U+003E GREATER-THAN SIGN (>)
					// This is a missing-doctype-public-identifier parse error. Set the current
					// DOCTYPE token's force-quirks flag to on. Switch to the data state. Emit
					// the current DOCTYPE token.
					reportError(
						"missing-doctype-public-identifier",
						pos,
						pos + 1,
						"warning"
					);
					let nextPos = pos + 1;
					if (callbacks.doctype !== undefined) {
						nextPos = callbacks.doctype(input, commentStart, pos + 1);
					}
					state = STATE_DATA;
					textStart = nextPos;
					pos = nextPos;
				} else {
					// Anything else
					// This is a missing-quote-before-doctype-public-identifier parse error. Set
					// the current DOCTYPE token's force-quirks flag to on. Reconsume in the
					// bogus DOCTYPE state.
					reportError(
						"missing-quote-before-doctype-public-identifier",
						pos,
						pos + 1,
						"warning"
					);
					state = STATE_BOGUS_DOCTYPE;
				}
				break;

			// https://html.spec.whatwg.org/multipage/parsing.html#before-doctype-public-identifier-state
			case STATE_BEFORE_DOCTYPE_PUBLIC_IDENTIFIER:
				// Consume the next input character:
				if (isSpace(cc)) {
					// U+0009 CHARACTER TABULATION (tab)
					// U+000A LINE FEED (LF)
					// U+000C FORM FEED (FF)
					// U+0020 SPACE
					// Ignore the character.
					pos++;
				} else if (cc === CC_QUOTATION_MARK) {
					// U+0022 QUOTATION MARK (")
					// Set the current DOCTYPE token's public identifier to the empty string
					// (not missing), then switch to the DOCTYPE public identifier
					// (double-quoted) state.
					state = STATE_DOCTYPE_PUBLIC_IDENTIFIER_DOUBLE_QUOTED;
					pos++;
				} else if (cc === CC_APOSTROPHE) {
					// U+0027 APOSTROPHE (')
					// Set the current DOCTYPE token's public identifier to the empty string
					// (not missing), then switch to the DOCTYPE public identifier
					// (single-quoted) state.
					state = STATE_DOCTYPE_PUBLIC_IDENTIFIER_SINGLE_QUOTED;
					pos++;
				} else if (cc === CC_GREATER_THAN) {
					// U+003E GREATER-THAN SIGN (>)
					// This is a missing-doctype-public-identifier parse error. Set the current
					// DOCTYPE token's force-quirks flag to on. Switch to the data state. Emit
					// the current DOCTYPE token.
					reportError(
						"missing-doctype-public-identifier",
						pos,
						pos + 1,
						"warning"
					);
					let nextPos = pos + 1;
					if (callbacks.doctype !== undefined) {
						nextPos = callbacks.doctype(input, commentStart, pos + 1);
					}
					state = STATE_DATA;
					textStart = nextPos;
					pos = nextPos;
				} else {
					// Anything else
					// This is a missing-quote-before-doctype-public-identifier parse error. Set
					// the current DOCTYPE token's force-quirks flag to on. Reconsume in the
					// bogus DOCTYPE state.
					reportError(
						"missing-quote-before-doctype-public-identifier",
						pos,
						pos + 1,
						"warning"
					);
					state = STATE_BOGUS_DOCTYPE;
				}
				break;

			// https://html.spec.whatwg.org/multipage/parsing.html#doctype-public-identifier-(double-quoted)-state
			case STATE_DOCTYPE_PUBLIC_IDENTIFIER_DOUBLE_QUOTED:
				// Consume the next input character:
				if (cc === CC_QUOTATION_MARK) {
					// U+0022 QUOTATION MARK (")
					// Switch to the after DOCTYPE public identifier state.
					state = STATE_AFTER_DOCTYPE_PUBLIC_IDENTIFIER;
					pos++;
				} else if (cc === CC_NULL) {
					// U+0000 NULL
					// This is an unexpected-null-character parse error. Append a U+FFFD
					// REPLACEMENT CHARACTER character to the current DOCTYPE token's public
					// identifier.
					reportError("unexpected-null-character", pos, pos + 1, "warning");
					pos++;
				} else if (cc === CC_GREATER_THAN) {
					// U+003E GREATER-THAN SIGN (>)
					// This is an abrupt-doctype-public-identifier parse error. Set the current
					// DOCTYPE token's force-quirks flag to on. Switch to the data state. Emit
					// the current DOCTYPE token.
					reportError(
						"abrupt-doctype-public-identifier",
						pos,
						pos + 1,
						"warning"
					);
					let nextPos = pos + 1;
					if (callbacks.doctype !== undefined) {
						nextPos = callbacks.doctype(input, commentStart, pos + 1);
					}
					state = STATE_DATA;
					textStart = nextPos;
					pos = nextPos;
				} else {
					// Anything else
					// Append the current input character to the current DOCTYPE token's public
					// identifier.
					pos++;
				}
				break;

			// https://html.spec.whatwg.org/multipage/parsing.html#doctype-public-identifier-(single-quoted)-state
			case STATE_DOCTYPE_PUBLIC_IDENTIFIER_SINGLE_QUOTED:
				// Consume the next input character:
				if (cc === CC_APOSTROPHE) {
					// U+0027 APOSTROPHE (')
					// Switch to the after DOCTYPE public identifier state.
					state = STATE_AFTER_DOCTYPE_PUBLIC_IDENTIFIER;
					pos++;
				} else if (cc === CC_NULL) {
					// U+0000 NULL
					// This is an unexpected-null-character parse error. Append a U+FFFD
					// REPLACEMENT CHARACTER character to the current DOCTYPE token's public
					// identifier.
					reportError("unexpected-null-character", pos, pos + 1, "warning");
					pos++;
				} else if (cc === CC_GREATER_THAN) {
					// U+003E GREATER-THAN SIGN (>)
					// This is an abrupt-doctype-public-identifier parse error. Set the current
					// DOCTYPE token's force-quirks flag to on. Switch to the data state. Emit
					// the current DOCTYPE token.
					reportError(
						"abrupt-doctype-public-identifier",
						pos,
						pos + 1,
						"warning"
					);
					let nextPos = pos + 1;
					if (callbacks.doctype !== undefined) {
						nextPos = callbacks.doctype(input, commentStart, pos + 1);
					}
					state = STATE_DATA;
					textStart = nextPos;
					pos = nextPos;
				} else {
					// Anything else
					// Append the current input character to the current DOCTYPE token's public
					// identifier.
					pos++;
				}
				break;

			// https://html.spec.whatwg.org/multipage/parsing.html#after-doctype-public-identifier-state
			case STATE_AFTER_DOCTYPE_PUBLIC_IDENTIFIER:
				// Consume the next input character:
				if (isSpace(cc)) {
					// U+0009 CHARACTER TABULATION (tab)
					// U+000A LINE FEED (LF)
					// U+000C FORM FEED (FF)
					// U+0020 SPACE
					// Switch to the between DOCTYPE public and system identifiers state.
					state = STATE_BETWEEN_DOCTYPE_PUBLIC_AND_SYSTEM_IDENTIFIERS;
					pos++;
				} else if (cc === CC_GREATER_THAN) {
					// U+003E GREATER-THAN SIGN (>)
					// Switch to the data state. Emit the current DOCTYPE token.
					let nextPos = pos + 1;
					if (callbacks.doctype !== undefined) {
						nextPos = callbacks.doctype(input, commentStart, pos + 1);
					}
					state = STATE_DATA;
					textStart = nextPos;
					pos = nextPos;
				} else if (cc === CC_QUOTATION_MARK) {
					// U+0022 QUOTATION MARK (")
					// This is a missing-whitespace-between-doctype-public-and-system-identifiers
					// parse error. Set the current DOCTYPE token's system
					// identifier to the empty string (not missing), then switch
					// to the DOCTYPE system identifier (double-quoted) state.
					reportError(
						"missing-whitespace-between-doctype-public-and-system-identifiers",
						pos,
						pos + 1,
						"warning"
					);
					state = STATE_DOCTYPE_SYSTEM_IDENTIFIER_DOUBLE_QUOTED;
					pos++;
				} else if (cc === CC_APOSTROPHE) {
					// U+0027 APOSTROPHE (')
					// This is a missing-whitespace-between-doctype-public-and-system-identifiers
					// parse error. Set the current DOCTYPE token's system
					// identifier to the empty string (not missing), then switch
					// to the DOCTYPE system identifier (single-quoted) state.
					reportError(
						"missing-whitespace-between-doctype-public-and-system-identifiers",
						pos,
						pos + 1,
						"warning"
					);
					state = STATE_DOCTYPE_SYSTEM_IDENTIFIER_SINGLE_QUOTED;
					pos++;
				} else {
					// Anything else
					// This is a missing-quote-before-doctype-system-identifier parse error. Set
					// the current DOCTYPE token's force-quirks flag to on. Reconsume in the
					// bogus DOCTYPE state.
					reportError(
						"missing-quote-before-doctype-system-identifier",
						pos,
						pos + 1,
						"warning"
					);
					state = STATE_BOGUS_DOCTYPE;
				}
				break;

			// https://html.spec.whatwg.org/multipage/parsing.html#between-doctype-public-and-system-identifiers-state
			case STATE_BETWEEN_DOCTYPE_PUBLIC_AND_SYSTEM_IDENTIFIERS:
				// Consume the next input character:
				if (isSpace(cc)) {
					// U+0009 CHARACTER TABULATION (tab)
					// U+000A LINE FEED (LF)
					// U+000C FORM FEED (FF)
					// U+0020 SPACE
					// Ignore the character.
					pos++;
				} else if (cc === CC_GREATER_THAN) {
					// U+003E GREATER-THAN SIGN (>)
					// Switch to the data state. Emit the current DOCTYPE token.
					let nextPos = pos + 1;
					if (callbacks.doctype !== undefined) {
						nextPos = callbacks.doctype(input, commentStart, pos + 1);
					}
					state = STATE_DATA;
					textStart = nextPos;
					pos = nextPos;
				} else if (cc === CC_QUOTATION_MARK) {
					// U+0022 QUOTATION MARK (")
					// Set the current DOCTYPE token's system identifier to the empty string
					// (not missing), then switch to the DOCTYPE system identifier
					// (double-quoted) state.
					state = STATE_DOCTYPE_SYSTEM_IDENTIFIER_DOUBLE_QUOTED;
					pos++;
				} else if (cc === CC_APOSTROPHE) {
					// U+0027 APOSTROPHE (')
					// Set the current DOCTYPE token's system identifier to the empty string
					// (not missing), then switch to the DOCTYPE system identifier
					// (single-quoted) state.
					state = STATE_DOCTYPE_SYSTEM_IDENTIFIER_SINGLE_QUOTED;
					pos++;
				} else {
					// Anything else
					// This is a missing-quote-before-doctype-system-identifier parse error. Set
					// the current DOCTYPE token's force-quirks flag to on. Reconsume in the
					// bogus DOCTYPE state.
					reportError(
						"missing-quote-before-doctype-system-identifier",
						pos,
						pos + 1,
						"warning"
					);
					state = STATE_BOGUS_DOCTYPE;
				}
				break;

			// https://html.spec.whatwg.org/multipage/parsing.html#after-doctype-system-keyword-state
			case STATE_AFTER_DOCTYPE_SYSTEM_KEYWORD:
				// Consume the next input character:
				if (isSpace(cc)) {
					// U+0009 CHARACTER TABULATION (tab)
					// U+000A LINE FEED (LF)
					// U+000C FORM FEED (FF)
					// U+0020 SPACE
					// Switch to the before DOCTYPE system identifier state.
					state = STATE_BEFORE_DOCTYPE_SYSTEM_IDENTIFIER;
					pos++;
				} else if (cc === CC_QUOTATION_MARK) {
					// U+0022 QUOTATION MARK (")
					// This is a missing-whitespace-after-doctype-system-keyword parse error.
					// Set the current DOCTYPE token's system identifier to the empty string
					// (not missing), then switch to the DOCTYPE system identifier
					// (double-quoted) state.
					reportError(
						"missing-whitespace-after-doctype-system-keyword",
						pos,
						pos + 1,
						"warning"
					);
					state = STATE_DOCTYPE_SYSTEM_IDENTIFIER_DOUBLE_QUOTED;
					pos++;
				} else if (cc === CC_APOSTROPHE) {
					// U+0027 APOSTROPHE (')
					// This is a missing-whitespace-after-doctype-system-keyword parse error.
					// Set the current DOCTYPE token's system identifier to the empty string
					// (not missing), then switch to the DOCTYPE system identifier
					// (single-quoted) state.
					reportError(
						"missing-whitespace-after-doctype-system-keyword",
						pos,
						pos + 1,
						"warning"
					);
					state = STATE_DOCTYPE_SYSTEM_IDENTIFIER_SINGLE_QUOTED;
					pos++;
				} else if (cc === CC_GREATER_THAN) {
					// U+003E GREATER-THAN SIGN (>)
					// This is a missing-doctype-system-identifier parse error. Set the current
					// DOCTYPE token's force-quirks flag to on. Switch to the data state. Emit
					// the current DOCTYPE token.
					reportError(
						"missing-doctype-system-identifier",
						pos,
						pos + 1,
						"warning"
					);
					let nextPos = pos + 1;
					if (callbacks.doctype !== undefined) {
						nextPos = callbacks.doctype(input, commentStart, pos + 1);
					}
					state = STATE_DATA;
					textStart = nextPos;
					pos = nextPos;
				} else {
					// Anything else
					// This is a missing-quote-before-doctype-system-identifier parse error. Set
					// the current DOCTYPE token's force-quirks flag to on. Reconsume in the
					// bogus DOCTYPE state.
					reportError(
						"missing-quote-before-doctype-system-identifier",
						pos,
						pos + 1,
						"warning"
					);
					state = STATE_BOGUS_DOCTYPE;
				}
				break;

			// https://html.spec.whatwg.org/multipage/parsing.html#before-doctype-system-identifier-state
			case STATE_BEFORE_DOCTYPE_SYSTEM_IDENTIFIER:
				// Consume the next input character:
				if (isSpace(cc)) {
					// U+0009 CHARACTER TABULATION (tab)
					// U+000A LINE FEED (LF)
					// U+000C FORM FEED (FF)
					// U+0020 SPACE
					// Ignore the character.
					pos++;
				} else if (cc === CC_QUOTATION_MARK) {
					// U+0022 QUOTATION MARK (")
					// Set the current DOCTYPE token's system identifier to the empty string
					// (not missing), then switch to the DOCTYPE system identifier
					// (double-quoted) state.
					state = STATE_DOCTYPE_SYSTEM_IDENTIFIER_DOUBLE_QUOTED;
					pos++;
				} else if (cc === CC_APOSTROPHE) {
					// U+0027 APOSTROPHE (')
					// Set the current DOCTYPE token's system identifier to the empty string
					// (not missing), then switch to the DOCTYPE system identifier
					// (single-quoted) state.
					state = STATE_DOCTYPE_SYSTEM_IDENTIFIER_SINGLE_QUOTED;
					pos++;
				} else if (cc === CC_GREATER_THAN) {
					// U+003E GREATER-THAN SIGN (>)
					// This is a missing-doctype-system-identifier parse error. Set the current
					// DOCTYPE token's force-quirks flag to on. Switch to the data state. Emit
					// the current DOCTYPE token.
					reportError(
						"missing-doctype-system-identifier",
						pos,
						pos + 1,
						"warning"
					);
					let nextPos = pos + 1;
					if (callbacks.doctype !== undefined) {
						nextPos = callbacks.doctype(input, commentStart, pos + 1);
					}
					state = STATE_DATA;
					textStart = nextPos;
					pos = nextPos;
				} else {
					// Anything else
					// This is a missing-quote-before-doctype-system-identifier parse error. Set
					// the current DOCTYPE token's force-quirks flag to on. Reconsume in the
					// bogus DOCTYPE state.
					reportError(
						"missing-quote-before-doctype-system-identifier",
						pos,
						pos + 1,
						"warning"
					);
					state = STATE_BOGUS_DOCTYPE;
				}
				break;

			// https://html.spec.whatwg.org/multipage/parsing.html#doctype-system-identifier-(double-quoted)-state
			case STATE_DOCTYPE_SYSTEM_IDENTIFIER_DOUBLE_QUOTED:
				// Consume the next input character:
				if (cc === CC_QUOTATION_MARK) {
					// U+0022 QUOTATION MARK (")
					// Switch to the after DOCTYPE system identifier state.
					state = STATE_AFTER_DOCTYPE_SYSTEM_IDENTIFIER;
					pos++;
				} else if (cc === CC_NULL) {
					// U+0000 NULL
					// This is an unexpected-null-character parse error. Append a U+FFFD
					// REPLACEMENT CHARACTER character to the current DOCTYPE token's system
					// identifier.
					reportError("unexpected-null-character", pos, pos + 1, "warning");
					pos++;
				} else if (cc === CC_GREATER_THAN) {
					// U+003E GREATER-THAN SIGN (>)
					// This is an abrupt-doctype-system-identifier parse error. Set the current
					// DOCTYPE token's force-quirks flag to on. Switch to the data state. Emit
					// the current DOCTYPE token.
					reportError(
						"abrupt-doctype-system-identifier",
						pos,
						pos + 1,
						"warning"
					);
					let nextPos = pos + 1;
					if (callbacks.doctype !== undefined) {
						nextPos = callbacks.doctype(input, commentStart, pos + 1);
					}
					state = STATE_DATA;
					textStart = nextPos;
					pos = nextPos;
				} else {
					// Anything else
					// Append the current input character to the current DOCTYPE token's system
					// identifier.
					pos++;
				}
				break;

			// https://html.spec.whatwg.org/multipage/parsing.html#doctype-system-identifier-(single-quoted)-state
			case STATE_DOCTYPE_SYSTEM_IDENTIFIER_SINGLE_QUOTED:
				// Consume the next input character:
				if (cc === CC_APOSTROPHE) {
					// U+0027 APOSTROPHE (')
					// Switch to the after DOCTYPE system identifier state.
					state = STATE_AFTER_DOCTYPE_SYSTEM_IDENTIFIER;
					pos++;
				} else if (cc === CC_NULL) {
					// U+0000 NULL
					// This is an unexpected-null-character parse error. Append a U+FFFD
					// REPLACEMENT CHARACTER character to the current DOCTYPE token's system
					// identifier.
					reportError("unexpected-null-character", pos, pos + 1, "warning");
					pos++;
				} else if (cc === CC_GREATER_THAN) {
					// U+003E GREATER-THAN SIGN (>)
					// This is an abrupt-doctype-system-identifier parse error. Set the current
					// DOCTYPE token's force-quirks flag to on. Switch to the data state. Emit
					// the current DOCTYPE token.
					reportError(
						"abrupt-doctype-system-identifier",
						pos,
						pos + 1,
						"warning"
					);
					let nextPos = pos + 1;
					if (callbacks.doctype !== undefined) {
						nextPos = callbacks.doctype(input, commentStart, pos + 1);
					}
					state = STATE_DATA;
					textStart = nextPos;
					pos = nextPos;
				} else {
					// Anything else
					// Append the current input character to the current DOCTYPE token's system
					// identifier.
					pos++;
				}
				break;

			// https://html.spec.whatwg.org/multipage/parsing.html#after-doctype-system-identifier-state
			case STATE_AFTER_DOCTYPE_SYSTEM_IDENTIFIER:
				// Consume the next input character:
				if (isSpace(cc)) {
					// U+0009 CHARACTER TABULATION (tab)
					// U+000A LINE FEED (LF)
					// U+000C FORM FEED (FF)
					// U+0020 SPACE
					// Ignore the character.
					pos++;
				} else if (cc === CC_GREATER_THAN) {
					// U+003E GREATER-THAN SIGN (>)
					// Switch to the data state. Emit the current DOCTYPE token.
					let nextPos = pos + 1;
					if (callbacks.doctype !== undefined) {
						nextPos = callbacks.doctype(input, commentStart, pos + 1);
					}
					state = STATE_DATA;
					textStart = nextPos;
					pos = nextPos;
				} else {
					// Anything else
					// This is an unexpected-character-after-doctype-system-identifier parse
					// error. Reconsume in the bogus DOCTYPE state. (This does not set the
					// current DOCTYPE token's force-quirks flag to on.)
					reportError(
						"unexpected-character-after-doctype-system-identifier",
						pos,
						pos + 1,
						"warning"
					);
					state = STATE_BOGUS_DOCTYPE;
				}
				break;

			// https://html.spec.whatwg.org/multipage/parsing.html#bogus-doctype-state
			case STATE_BOGUS_DOCTYPE:
				// Consume the next input character:
				if (cc === CC_GREATER_THAN) {
					// U+003E GREATER-THAN SIGN (>)
					// Switch to the data state. Emit the DOCTYPE token.
					let nextPos = pos + 1;
					if (callbacks.doctype !== undefined) {
						nextPos = callbacks.doctype(input, commentStart, pos + 1);
					}
					state = STATE_DATA;
					textStart = nextPos;
					pos = nextPos;
				} else if (cc === CC_NULL) {
					// U+0000 NULL
					// This is an unexpected-null-character parse error. Ignore the character.
					reportError("unexpected-null-character", pos, pos + 1, "warning");
					pos++;
				} else {
					// Anything else
					// Ignore the character.
					pos++;
				}
				break;

			// https://html.spec.whatwg.org/multipage/parsing.html#cdata-section-state
			case STATE_CDATA_SECTION:
				// Consume the next input character:
				// U+005D RIGHT SQUARE BRACKET (])
				// Switch to the CDATA section bracket state.
				if (cc === CC_RIGHT_SQUARE_BRACKET) {
					state = STATE_CDATA_SECTION_BRACKET;
					pos++;
				} else {
					// Anything else
					// Emit the current input character as a character token.
					pos++;
				}
				break;

			// https://html.spec.whatwg.org/multipage/parsing.html#cdata-section-bracket-state
			case STATE_CDATA_SECTION_BRACKET:
				// Consume the next input character:
				// U+005D RIGHT SQUARE BRACKET (])
				// Switch to the CDATA section end state.
				if (cc === CC_RIGHT_SQUARE_BRACKET) {
					state = STATE_CDATA_SECTION_END;
					pos++;
				} else {
					// Anything else
					// Emit a U+005D RIGHT SQUARE BRACKET character token. Reconsume in the
					// CDATA section state.
					state = STATE_CDATA_SECTION;
					// Reconsume
				}
				break;

			// https://html.spec.whatwg.org/multipage/parsing.html#cdata-section-end-state
			case STATE_CDATA_SECTION_END:
				// Consume the next input character:
				// U+005D RIGHT SQUARE BRACKET (])
				// Emit a U+005D RIGHT SQUARE BRACKET character token.
				if (cc === CC_RIGHT_SQUARE_BRACKET) {
					pos++;
				} else if (cc === CC_GREATER_THAN) {
					// U+003E GREATER-THAN SIGN (>)
					// Switch to the data state.
					let nextPos = pos + 1;
					if (callbacks.comment !== undefined) {
						nextPos = callbacks.comment(input, commentStart, pos + 1);
					}
					state = STATE_DATA;
					textStart = nextPos;
					pos = nextPos;
				} else {
					// Anything else
					// Emit two U+005D RIGHT SQUARE BRACKET character tokens. Reconsume in the
					// CDATA section state.
					state = STATE_CDATA_SECTION;
					// Reconsume
				}
				break;

			// https://html.spec.whatwg.org/multipage/parsing.html#rcdata-state
			case STATE_RCDATA:
				// Consume the next input character:
				if (cc === CC_AMPERSAND) {
					// U+0026 AMPERSAND (&)
					// Set the return state to the RCDATA state. Switch to the
					// character reference state. (RCDATA processes references;
					// RAWTEXT/script/PLAINTEXT do not.)
					returnState = STATE_RCDATA;
					state = STATE_CHARACTER_REFERENCE;
					pos++;
				} else if (cc === CC_LESS_THAN) {
					// U+003C LESS-THAN SIGN (<)
					// Switch to the RCDATA less-than sign state.
					tagStart = pos;
					state = STATE_RCDATA_LESS_THAN_SIGN;
					pos++;
				} else if (cc === CC_NULL) {
					// U+0000 NULL is an unexpected-null-character parse error.
					reportError("unexpected-null-character", pos, pos + 1, "warning");
					pos++;
				} else {
					// Fast-forward over ordinary RCDATA text.
					pos++;
					while (pos < len) {
						const c2 = input.charCodeAt(pos);
						if (c2 === CC_AMPERSAND || c2 === CC_LESS_THAN || c2 === CC_NULL) {
							break;
						}
						pos++;
					}
				}
				break;

			// https://html.spec.whatwg.org/multipage/parsing.html#rcdata-less-than-sign-state
			case STATE_RCDATA_LESS_THAN_SIGN:
				// Consume the next input character:
				// U+002F SOLIDUS (/)
				// Switch to the RCDATA end tag open state. (Spec sets a
				// temporary buffer here; we track the would-be content via
				// offset ranges instead.)
				if (cc === CC_SOLIDUS) {
					state = STATE_RCDATA_END_TAG_OPEN;
					pos++;
				} else {
					// Anything else
					// Emit a U+003C LESS-THAN SIGN character token. Reconsume in the RCDATA
					// state.
					state = STATE_RCDATA;
					// Reconsume
				}
				break;

			// https://html.spec.whatwg.org/multipage/parsing.html#rcdata-end-tag-open-state
			case STATE_RCDATA_END_TAG_OPEN:
				// Consume the next input character:
				// ASCII alpha
				// Create a new end tag token, set its tag name to the empty string.
				// Reconsume in the RCDATA end tag name state.
				if (isAsciiAlpha(cc)) {
					tagNameStart = pos;
					state = STATE_RCDATA_END_TAG_NAME;
					// Reconsume
				} else {
					// Anything else
					// Emit a U+003C LESS-THAN SIGN character token and a U+002F SOLIDUS
					// character token. Reconsume in the RCDATA state.
					state = STATE_RCDATA;
					// Reconsume
				}
				break;

			// https://html.spec.whatwg.org/multipage/parsing.html#rcdata-end-tag-name-state
			case STATE_RCDATA_END_TAG_NAME:
				// Consume the next input character:
				// U+0009 CHARACTER TABULATION (tab)
				// U+000A LINE FEED (LF)
				// U+000C FORM FEED (FF)
				// U+0020 SPACE
				// If the current end tag token is an appropriate end tag token, then switch
				// to the before attribute name state. Otherwise, treat it as per the
				// "anything else" entry below.
				if (isSpace(cc)) {
					tagNameEnd = pos;
					if (
						rangeEqualsLower(input, tagNameStart, tagNameEnd, lastOpenTagName)
					) {
						flushText(tagStart);
						state = STATE_BEFORE_ATTRIBUTE_NAME;
						pos++;
					} else {
						state = STATE_RCDATA;
						// Reconsume
					}
				} else if (cc === CC_SOLIDUS) {
					// U+002F SOLIDUS (/)
					// If the current end tag token is an appropriate end tag token, then switch
					// to the self-closing start tag state. Otherwise, treat it as per the
					// "anything else" entry below.
					tagNameEnd = pos;
					if (
						rangeEqualsLower(input, tagNameStart, tagNameEnd, lastOpenTagName)
					) {
						flushText(tagStart);
						state = STATE_SELF_CLOSING_START_TAG;
						pos++;
					} else {
						state = STATE_RCDATA;
						// Reconsume
					}
				} else if (cc === CC_GREATER_THAN) {
					// U+003E GREATER-THAN SIGN (>)
					// If the current end tag token is an appropriate end tag token, then switch
					// to the data state and emit the current tag token. Otherwise, treat it as
					// per the "anything else" entry below.
					tagNameEnd = pos;
					if (
						rangeEqualsLower(input, tagNameStart, tagNameEnd, lastOpenTagName)
					) {
						flushText(tagStart);
						state = STATE_DATA;
						pos = emitCloseTag(pos + 1);
					} else {
						state = STATE_RCDATA;
						// Reconsume
					}
				} else if (isAsciiAlpha(cc)) {
					// ASCII upper alpha / ASCII lower alpha
					// Append the lowercase version of the current input character to the
					// current tag token's tag name. Append the current input character to
					// the temporary buffer.
					pos++;
				} else {
					// Anything else
					// Emit a U+003C LESS-THAN SIGN character token, a U+002F SOLIDUS character
					// token, and a character token for each of the characters in the temporary
					// buffer (in the order they were added to the buffer). Reconsume in the
					// RCDATA state.
					state = STATE_RCDATA;
					// Reconsume
				}
				break;

			// https://html.spec.whatwg.org/multipage/parsing.html#rawtext-state
			case STATE_RAWTEXT:
				// Consume the next input character:
				// U+003C LESS-THAN SIGN (<)
				// Switch to the RAWTEXT less-than sign state.
				if (cc === CC_LESS_THAN) {
					tagStart = pos;
					state = STATE_RAWTEXT_LESS_THAN_SIGN;
					pos++;
				} else if (cc === CC_NULL) {
					// U+0000 NULL is an unexpected-null-character parse error.
					reportError("unexpected-null-character", pos, pos + 1, "warning");
					pos++;
				} else {
					// Fast-forward over ordinary RAWTEXT text.
					pos++;
					while (pos < len) {
						const c2 = input.charCodeAt(pos);
						if (c2 === CC_LESS_THAN || c2 === CC_NULL) break;
						pos++;
					}
				}
				break;

			// https://html.spec.whatwg.org/multipage/parsing.html#rawtext-less-than-sign-state
			case STATE_RAWTEXT_LESS_THAN_SIGN:
				// Consume the next input character:
				// U+002F SOLIDUS (/)
				// Switch to the RAWTEXT end tag open state. (Spec sets a
				// temporary buffer here; we track via offset ranges instead.)
				if (cc === CC_SOLIDUS) {
					state = STATE_RAWTEXT_END_TAG_OPEN;
					pos++;
				} else {
					// Anything else
					// Emit a U+003C LESS-THAN SIGN character token. Reconsume in the RAWTEXT
					// state.
					state = STATE_RAWTEXT;
					// Reconsume
				}
				break;

			// https://html.spec.whatwg.org/multipage/parsing.html#rawtext-end-tag-open-state
			case STATE_RAWTEXT_END_TAG_OPEN:
				// Consume the next input character:
				// ASCII alpha
				// Create a new end tag token, set its tag name to the empty string.
				// Reconsume in the RAWTEXT end tag name state.
				if (isAsciiAlpha(cc)) {
					tagNameStart = pos;
					state = STATE_RAWTEXT_END_TAG_NAME;
					// Reconsume
				} else {
					// Anything else
					// Emit a U+003C LESS-THAN SIGN character token and a U+002F SOLIDUS
					// character token. Reconsume in the RAWTEXT state.
					state = STATE_RAWTEXT;
					// Reconsume
				}
				break;

			// https://html.spec.whatwg.org/multipage/parsing.html#rawtext-end-tag-name-state
			case STATE_RAWTEXT_END_TAG_NAME:
				// Consume the next input character:
				// U+0009 CHARACTER TABULATION (tab)
				// U+000A LINE FEED (LF)
				// U+000C FORM FEED (FF)
				// U+0020 SPACE
				// If the current end tag token is an appropriate end tag token, then switch
				// to the before attribute name state. Otherwise, treat it as per the
				// "anything else" entry below.
				if (isSpace(cc)) {
					tagNameEnd = pos;
					if (
						rangeEqualsLower(input, tagNameStart, tagNameEnd, lastOpenTagName)
					) {
						flushText(tagStart);
						state = STATE_BEFORE_ATTRIBUTE_NAME;
						pos++;
					} else {
						state = STATE_RAWTEXT;
					}
				} else if (cc === CC_SOLIDUS) {
					// U+002F SOLIDUS (/)
					// If the current end tag token is an appropriate end tag token, then switch
					// to the self-closing start tag state. Otherwise, treat it as per the
					// "anything else" entry below.
					tagNameEnd = pos;
					if (
						rangeEqualsLower(input, tagNameStart, tagNameEnd, lastOpenTagName)
					) {
						flushText(tagStart);
						state = STATE_SELF_CLOSING_START_TAG;
						pos++;
					} else {
						state = STATE_RAWTEXT;
					}
				} else if (cc === CC_GREATER_THAN) {
					// U+003E GREATER-THAN SIGN (>)
					// If the current end tag token is an appropriate end tag token, then switch
					// to the data state and emit the current tag token. Otherwise, treat it as
					// per the "anything else" entry below.
					tagNameEnd = pos;
					if (
						rangeEqualsLower(input, tagNameStart, tagNameEnd, lastOpenTagName)
					) {
						flushText(tagStart);
						state = STATE_DATA;
						pos = emitCloseTag(pos + 1);
					} else {
						state = STATE_RAWTEXT;
					}
				} else if (isAsciiAlpha(cc)) {
					// ASCII upper alpha / ASCII lower alpha
					// Append the lowercase version of the current input character to the
					// current tag token's tag name. Append the current input character to
					// the temporary buffer.
					pos++;
				} else {
					// Anything else
					// Emit a U+003C LESS-THAN SIGN character token, a U+002F SOLIDUS character
					// token, and a character token for each of the characters in the temporary
					// buffer (in the order they were added to the buffer). Reconsume in the
					// RAWTEXT state.
					state = STATE_RAWTEXT;
					// Reconsume
				}
				break;

			// https://html.spec.whatwg.org/multipage/parsing.html#script-data-state
			case STATE_SCRIPT_DATA:
				// Consume the next input character:
				// U+003C LESS-THAN SIGN (<)
				// Switch to the script data less-than sign state.
				if (cc === CC_LESS_THAN) {
					tagStart = pos;
					state = STATE_SCRIPT_DATA_LESS_THAN_SIGN;
					pos++;
				} else if (cc === CC_NULL) {
					// U+0000 NULL is an unexpected-null-character parse error.
					reportError("unexpected-null-character", pos, pos + 1, "warning");
					pos++;
				} else {
					// Fast-forward over ordinary script-data text.
					pos++;
					while (pos < len) {
						const c2 = input.charCodeAt(pos);
						if (c2 === CC_LESS_THAN || c2 === CC_NULL) break;
						pos++;
					}
				}
				break;

			// https://html.spec.whatwg.org/multipage/parsing.html#script-data-less-than-sign-state
			case STATE_SCRIPT_DATA_LESS_THAN_SIGN:
				// Consume the next input character:
				// U+002F SOLIDUS (/)
				// Switch to the script data end tag open state. (Spec sets a
				// temporary buffer here; we track via offset ranges instead.)
				if (cc === CC_SOLIDUS) {
					state = STATE_SCRIPT_DATA_END_TAG_OPEN;
					pos++;
				} else if (cc === CC_EXCLAMATION_MARK) {
					// U+0021 EXCLAMATION MARK (!)
					// Switch to the script data escape start state. Emit a U+003C LESS-THAN
					// SIGN character token and a U+0021 EXCLAMATION MARK character token.
					state = STATE_SCRIPT_DATA_ESCAPE_START;
					pos++;
				} else {
					// Anything else
					// Emit a U+003C LESS-THAN SIGN character token. Reconsume in the script
					// data state.
					state = STATE_SCRIPT_DATA;
					// Reconsume
				}
				break;

			// https://html.spec.whatwg.org/multipage/parsing.html#script-data-end-tag-open-state
			case STATE_SCRIPT_DATA_END_TAG_OPEN:
				// Consume the next input character:
				// ASCII alpha
				// Create a new end tag token, set its tag name to the empty string.
				// Reconsume in the script data end tag name state.
				if (isAsciiAlpha(cc)) {
					tagNameStart = pos;
					state = STATE_SCRIPT_DATA_END_TAG_NAME;
					// Reconsume
				} else {
					// Anything else
					// Emit a U+003C LESS-THAN SIGN character token and a U+002F SOLIDUS
					// character token. Reconsume in the script data state.
					state = STATE_SCRIPT_DATA;
					// Reconsume
				}
				break;

			// https://html.spec.whatwg.org/multipage/parsing.html#script-data-end-tag-name-state
			case STATE_SCRIPT_DATA_END_TAG_NAME:
				// Consume the next input character:
				// U+0009 CHARACTER TABULATION (tab)
				// U+000A LINE FEED (LF)
				// U+000C FORM FEED (FF)
				// U+0020 SPACE
				// If the current end tag token is an appropriate end tag token, then switch
				// to the before attribute name state. Otherwise, treat it as per the
				// "anything else" entry below.
				if (isSpace(cc)) {
					tagNameEnd = pos;
					if (
						rangeEqualsLower(input, tagNameStart, tagNameEnd, lastOpenTagName)
					) {
						flushText(tagStart);
						state = STATE_BEFORE_ATTRIBUTE_NAME;
						pos++;
					} else {
						state = STATE_SCRIPT_DATA;
					}
				} else if (cc === CC_SOLIDUS) {
					// U+002F SOLIDUS (/)
					// If the current end tag token is an appropriate end tag token, then switch
					// to the self-closing start tag state. Otherwise, treat it as per the
					// "anything else" entry below.
					tagNameEnd = pos;
					if (
						rangeEqualsLower(input, tagNameStart, tagNameEnd, lastOpenTagName)
					) {
						flushText(tagStart);
						state = STATE_SELF_CLOSING_START_TAG;
						pos++;
					} else {
						state = STATE_SCRIPT_DATA;
					}
				} else if (cc === CC_GREATER_THAN) {
					// U+003E GREATER-THAN SIGN (>)
					// If the current end tag token is an appropriate end tag token, then switch
					// to the data state and emit the current tag token. Otherwise, treat it as
					// per the "anything else" entry below.
					tagNameEnd = pos;
					if (
						rangeEqualsLower(input, tagNameStart, tagNameEnd, lastOpenTagName)
					) {
						flushText(tagStart);
						state = STATE_DATA;
						pos = emitCloseTag(pos + 1);
					} else {
						state = STATE_SCRIPT_DATA;
					}
				} else if (isAsciiAlpha(cc)) {
					// ASCII upper alpha / ASCII lower alpha
					pos++;
				} else {
					// Anything else
					// Emit a U+003C LESS-THAN SIGN character token, a U+002F SOLIDUS character
					// token, and a character token for each of the characters in the temporary
					// buffer (in the order they were added to the buffer). Reconsume in the
					// script data state.
					state = STATE_SCRIPT_DATA;
					// Reconsume
				}
				break;

			// https://html.spec.whatwg.org/multipage/parsing.html#script-data-escape-start-state
			case STATE_SCRIPT_DATA_ESCAPE_START:
				// Consume the next input character:
				// U+002D HYPHEN-MINUS (-)
				// Switch to the script data escape start dash state. Emit a U+002D
				// HYPHEN-MINUS character token.
				if (cc === CC_HYPHEN_MINUS) {
					state = STATE_SCRIPT_DATA_ESCAPE_START_DASH;
					pos++;
				} else {
					// Anything else
					// Reconsume in the script data state.
					state = STATE_SCRIPT_DATA;
					// Reconsume
				}
				break;

			// https://html.spec.whatwg.org/multipage/parsing.html#script-data-escape-start-dash-state
			case STATE_SCRIPT_DATA_ESCAPE_START_DASH:
				// Consume the next input character:
				// U+002D HYPHEN-MINUS (-)
				// Switch to the script data escaped dash dash state. Emit a U+002D
				// HYPHEN-MINUS character token.
				if (cc === CC_HYPHEN_MINUS) {
					state = STATE_SCRIPT_DATA_ESCAPED_DASH_DASH;
					pos++;
				} else {
					// Anything else
					// Reconsume in the script data state.
					state = STATE_SCRIPT_DATA;
					// Reconsume
				}
				break;

			// https://html.spec.whatwg.org/multipage/parsing.html#script-data-escaped-state
			case STATE_SCRIPT_DATA_ESCAPED:
				// Consume the next input character:
				// U+002D HYPHEN-MINUS (-)
				// Switch to the script data escaped dash state. Emit a U+002D HYPHEN-MINUS
				// character token.
				if (cc === CC_HYPHEN_MINUS) {
					state = STATE_SCRIPT_DATA_ESCAPED_DASH;
					pos++;
				} else if (cc === CC_LESS_THAN) {
					// U+003C LESS-THAN SIGN (<)
					// Switch to the script data escaped less-than sign state.
					tagStart = pos;
					state = STATE_SCRIPT_DATA_ESCAPED_LESS_THAN_SIGN;
					pos++;
				} else {
					// Anything else
					// Emit the current input character as a character token.
					// U+0000 NULL is an unexpected-null-character parse error.
					if (cc === CC_NULL) {
						reportError("unexpected-null-character", pos, pos + 1, "warning");
					}
					pos++;
				}
				break;

			// https://html.spec.whatwg.org/multipage/parsing.html#script-data-escaped-dash-state
			case STATE_SCRIPT_DATA_ESCAPED_DASH:
				// Consume the next input character:
				// U+002D HYPHEN-MINUS (-)
				// Switch to the script data escaped dash dash state. Emit a U+002D
				// HYPHEN-MINUS character token.
				if (cc === CC_HYPHEN_MINUS) {
					state = STATE_SCRIPT_DATA_ESCAPED_DASH_DASH;
					pos++;
				} else if (cc === CC_LESS_THAN) {
					// U+003C LESS-THAN SIGN (<)
					// Switch to the script data escaped less-than sign state.
					tagStart = pos;
					state = STATE_SCRIPT_DATA_ESCAPED_LESS_THAN_SIGN;
					pos++;
				} else {
					// Anything else
					// Switch to the script data escaped state. Emit the current input character
					// as a character token. U+0000 NULL is an unexpected-null-character error.
					if (cc === CC_NULL) {
						reportError("unexpected-null-character", pos, pos + 1, "warning");
					}
					state = STATE_SCRIPT_DATA_ESCAPED;
					pos++;
				}
				break;

			// https://html.spec.whatwg.org/multipage/parsing.html#script-data-escaped-dash-dash-state
			case STATE_SCRIPT_DATA_ESCAPED_DASH_DASH:
				// Consume the next input character:
				// U+002D HYPHEN-MINUS (-)
				// Emit a U+002D HYPHEN-MINUS character token.
				if (cc === CC_HYPHEN_MINUS) {
					pos++;
				} else if (cc === CC_LESS_THAN) {
					// U+003C LESS-THAN SIGN (<)
					// Switch to the script data escaped less-than sign state.
					tagStart = pos;
					state = STATE_SCRIPT_DATA_ESCAPED_LESS_THAN_SIGN;
					pos++;
				} else if (cc === CC_GREATER_THAN) {
					// U+003E GREATER-THAN SIGN (>)
					// Switch to the script data state. Emit a U+003E GREATER-THAN SIGN
					// character token.
					state = STATE_SCRIPT_DATA;
					pos++;
				} else {
					// Anything else
					// Switch to the script data escaped state. Emit the current input character
					// as a character token. U+0000 NULL is an unexpected-null-character error.
					if (cc === CC_NULL) {
						reportError("unexpected-null-character", pos, pos + 1, "warning");
					}
					state = STATE_SCRIPT_DATA_ESCAPED;
					pos++;
				}
				break;

			// https://html.spec.whatwg.org/multipage/parsing.html#script-data-escaped-less-than-sign-state
			case STATE_SCRIPT_DATA_ESCAPED_LESS_THAN_SIGN:
				// Consume the next input character:
				// U+002F SOLIDUS (/)
				// Switch to the script data escaped end tag open state.
				// (Spec sets a temporary buffer; we track via offset ranges.)
				if (cc === CC_SOLIDUS) {
					state = STATE_SCRIPT_DATA_ESCAPED_END_TAG_OPEN;
					pos++;
				} else if (isAsciiAlpha(cc)) {
					// ASCII alpha
					// Set the temporary buffer to the empty string. Emit a U+003C LESS-THAN
					// SIGN character token. Reconsume in the script data double escape start
					// state.
					scriptMatch = 0;
					state = STATE_SCRIPT_DATA_DOUBLE_ESCAPE_START;
					// Reconsume
				} else {
					// Anything else
					// Emit a U+003C LESS-THAN SIGN character token. Reconsume in the script
					// data escaped state.
					state = STATE_SCRIPT_DATA_ESCAPED;
					// Reconsume
				}
				break;

			// https://html.spec.whatwg.org/multipage/parsing.html#script-data-escaped-end-tag-open-state
			case STATE_SCRIPT_DATA_ESCAPED_END_TAG_OPEN:
				// Consume the next input character:
				// ASCII alpha
				// Create a new end tag token, set its tag name to the empty string.
				// Reconsume in the script data escaped end tag name state.
				if (isAsciiAlpha(cc)) {
					tagNameStart = pos;
					state = STATE_SCRIPT_DATA_ESCAPED_END_TAG_NAME;
					// Reconsume
				} else {
					// Anything else
					// Emit a U+003C LESS-THAN SIGN character token and a U+002F SOLIDUS
					// character token. Reconsume in the script data escaped state.
					state = STATE_SCRIPT_DATA_ESCAPED;
					// Reconsume
				}
				break;

			// https://html.spec.whatwg.org/multipage/parsing.html#script-data-escaped-end-tag-name-state
			case STATE_SCRIPT_DATA_ESCAPED_END_TAG_NAME:
				// Consume the next input character:
				// U+0009 CHARACTER TABULATION (tab)
				// U+000A LINE FEED (LF)
				// U+000C FORM FEED (FF)
				// U+0020 SPACE
				// If the current end tag token is an appropriate end tag token, then switch
				// to the before attribute name state. Otherwise, treat it as per the
				// "anything else" entry below.
				if (isSpace(cc)) {
					tagNameEnd = pos;
					if (
						rangeEqualsLower(input, tagNameStart, tagNameEnd, lastOpenTagName)
					) {
						flushText(tagStart);
						state = STATE_BEFORE_ATTRIBUTE_NAME;
						pos++;
					} else {
						state = STATE_SCRIPT_DATA_ESCAPED;
					}
				} else if (cc === CC_SOLIDUS) {
					// U+002F SOLIDUS (/)
					// If the current end tag token is an appropriate end tag token, then switch
					// to the self-closing start tag state. Otherwise, treat it as per the
					// "anything else" entry below.
					tagNameEnd = pos;
					if (
						rangeEqualsLower(input, tagNameStart, tagNameEnd, lastOpenTagName)
					) {
						flushText(tagStart);
						state = STATE_SELF_CLOSING_START_TAG;
						pos++;
					} else {
						state = STATE_SCRIPT_DATA_ESCAPED;
					}
				} else if (cc === CC_GREATER_THAN) {
					// U+003E GREATER-THAN SIGN (>)
					// If the current end tag token is an appropriate end tag token, then switch
					// to the data state and emit the current tag token. Otherwise, treat it as
					// per the "anything else" entry below.
					tagNameEnd = pos;
					if (
						rangeEqualsLower(input, tagNameStart, tagNameEnd, lastOpenTagName)
					) {
						flushText(tagStart);
						state = STATE_DATA;
						pos = emitCloseTag(pos + 1);
					} else {
						state = STATE_SCRIPT_DATA_ESCAPED;
					}
				} else if (isAsciiAlpha(cc)) {
					// ASCII upper alpha / ASCII lower alpha
					pos++;
				} else {
					// Anything else
					// Emit a U+003C LESS-THAN SIGN character token, a U+002F SOLIDUS character
					// token, and a character token for each of the characters in the temporary
					// buffer (in the order they were added to the buffer). Reconsume in the
					// script data escaped state.
					state = STATE_SCRIPT_DATA_ESCAPED;
					// Reconsume
				}
				break;

			// https://html.spec.whatwg.org/multipage/parsing.html#script-data-double-escape-start-state
			case STATE_SCRIPT_DATA_DOUBLE_ESCAPE_START:
				// Consume the next input character:
				// U+0009 CHARACTER TABULATION (tab)
				// U+000A LINE FEED (LF)
				// U+000C FORM FEED (FF)
				// U+0020 SPACE
				// U+002F SOLIDUS (/)
				// U+003E GREATER-THAN SIGN (>)
				// If the temporary buffer is the string "script", then switch to the script
				// data double escaped state. Otherwise, switch to the script data escaped
				// state. Emit the current input character as a character token.
				if (isSpace(cc) || cc === CC_SOLIDUS || cc === CC_GREATER_THAN) {
					state =
						scriptMatch === 6
							? STATE_SCRIPT_DATA_DOUBLE_ESCAPED
							: STATE_SCRIPT_DATA_ESCAPED;
					pos++;
				} else if (isAsciiUpperAlpha(cc) || isAsciiLowerAlpha(cc)) {
					// ASCII alpha — advance the `"script"` match counter if the
					// lowercase form matches the next expected char, otherwise
					// snap to the sentinel so further chars can't revive a
					// match. No buffer allocation.
					const lower = isAsciiUpperAlpha(cc) ? cc + 0x20 : cc;
					if (scriptMatch < 6 && lower === "script".charCodeAt(scriptMatch)) {
						scriptMatch++;
					} else {
						scriptMatch = 7;
					}
					pos++;
				} else {
					// Anything else
					// Reconsume in the script data escaped state.
					state = STATE_SCRIPT_DATA_ESCAPED;
					// Reconsume
				}
				break;

			// https://html.spec.whatwg.org/multipage/parsing.html#script-data-double-escaped-state
			case STATE_SCRIPT_DATA_DOUBLE_ESCAPED:
				// Consume the next input character:
				// U+002D HYPHEN-MINUS (-)
				// Switch to the script data double escaped dash state. Emit a U+002D
				// HYPHEN-MINUS character token.
				if (cc === CC_HYPHEN_MINUS) {
					state = STATE_SCRIPT_DATA_DOUBLE_ESCAPED_DASH;
					pos++;
				} else if (cc === CC_LESS_THAN) {
					// U+003C LESS-THAN SIGN (<)
					// Switch to the script data double escaped less-than sign state. Emit a
					// U+003C LESS-THAN SIGN character token.
					state = STATE_SCRIPT_DATA_DOUBLE_ESCAPED_LESS_THAN_SIGN;
					pos++;
				} else {
					// Anything else
					// Emit the current input character as a character token.
					// U+0000 NULL is an unexpected-null-character parse error.
					if (cc === CC_NULL) {
						reportError("unexpected-null-character", pos, pos + 1, "warning");
					}
					pos++;
				}
				break;

			// https://html.spec.whatwg.org/multipage/parsing.html#script-data-double-escaped-dash-state
			case STATE_SCRIPT_DATA_DOUBLE_ESCAPED_DASH:
				// Consume the next input character:
				// U+002D HYPHEN-MINUS (-)
				// Switch to the script data double escaped dash dash state. Emit a U+002D
				// HYPHEN-MINUS character token.
				if (cc === CC_HYPHEN_MINUS) {
					state = STATE_SCRIPT_DATA_DOUBLE_ESCAPED_DASH_DASH;
					pos++;
				} else if (cc === CC_LESS_THAN) {
					// U+003C LESS-THAN SIGN (<)
					// Switch to the script data double escaped less-than sign state. Emit a
					// U+003C LESS-THAN SIGN character token.
					state = STATE_SCRIPT_DATA_DOUBLE_ESCAPED_LESS_THAN_SIGN;
					pos++;
				} else {
					// Anything else
					// Switch to the script data double escaped state. Emit the current input
					// character as a character token. NULL is unexpected-null-character.
					if (cc === CC_NULL) {
						reportError("unexpected-null-character", pos, pos + 1, "warning");
					}
					state = STATE_SCRIPT_DATA_DOUBLE_ESCAPED;
					pos++;
				}
				break;

			// https://html.spec.whatwg.org/multipage/parsing.html#script-data-double-escaped-dash-dash-state
			case STATE_SCRIPT_DATA_DOUBLE_ESCAPED_DASH_DASH:
				// Consume the next input character:
				// U+002D HYPHEN-MINUS (-)
				// Emit a U+002D HYPHEN-MINUS character token.
				if (cc === CC_HYPHEN_MINUS) {
					pos++;
				} else if (cc === CC_LESS_THAN) {
					// U+003C LESS-THAN SIGN (<)
					// Switch to the script data double escaped less-than sign state. Emit a
					// U+003C LESS-THAN SIGN character token.
					state = STATE_SCRIPT_DATA_DOUBLE_ESCAPED_LESS_THAN_SIGN;
					pos++;
				} else if (cc === CC_GREATER_THAN) {
					// U+003E GREATER-THAN SIGN (>)
					// Switch to the script data state. Emit a U+003E GREATER-THAN SIGN
					// character token.
					state = STATE_SCRIPT_DATA;
					pos++;
				} else {
					// Anything else
					// Switch to the script data double escaped state. Emit the current input
					// character as a character token. NULL is unexpected-null-character.
					if (cc === CC_NULL) {
						reportError("unexpected-null-character", pos, pos + 1, "warning");
					}
					state = STATE_SCRIPT_DATA_DOUBLE_ESCAPED;
					pos++;
				}
				break;

			// https://html.spec.whatwg.org/multipage/parsing.html#script-data-double-escaped-less-than-sign-state
			case STATE_SCRIPT_DATA_DOUBLE_ESCAPED_LESS_THAN_SIGN:
				// Consume the next input character:
				// U+002F SOLIDUS (/)
				// Set the temporary buffer to the empty string. Switch to the script data
				// double escape end state. Emit a U+002F SOLIDUS character token.
				if (cc === CC_SOLIDUS) {
					scriptMatch = 0;
					state = STATE_SCRIPT_DATA_DOUBLE_ESCAPE_END;
					pos++;
				} else {
					// Anything else
					// Reconsume in the script data double escaped state.
					state = STATE_SCRIPT_DATA_DOUBLE_ESCAPED;
					// Reconsume
				}
				break;

			// https://html.spec.whatwg.org/multipage/parsing.html#script-data-double-escape-end-state
			case STATE_SCRIPT_DATA_DOUBLE_ESCAPE_END:
				// Consume the next input character:
				// U+0009 CHARACTER TABULATION (tab)
				// U+000A LINE FEED (LF)
				// U+000C FORM FEED (FF)
				// U+0020 SPACE
				// U+002F SOLIDUS (/)
				// U+003E GREATER-THAN SIGN (>)
				// If the temporary buffer is the string "script", then switch to the script
				// data escaped state. Otherwise, switch to the script data double escaped
				// state. Emit the current input character as a character token.
				if (isSpace(cc) || cc === CC_SOLIDUS || cc === CC_GREATER_THAN) {
					state =
						scriptMatch === 6
							? STATE_SCRIPT_DATA_ESCAPED
							: STATE_SCRIPT_DATA_DOUBLE_ESCAPED;
					pos++;
				} else if (isAsciiUpperAlpha(cc) || isAsciiLowerAlpha(cc)) {
					// ASCII alpha — advance the `"script"` match counter if the
					// lowercase form matches the next expected char, otherwise
					// snap to the sentinel so further chars can't revive a
					// match. No buffer allocation.
					const lower = isAsciiUpperAlpha(cc) ? cc + 0x20 : cc;
					if (scriptMatch < 6 && lower === "script".charCodeAt(scriptMatch)) {
						scriptMatch++;
					} else {
						scriptMatch = 7;
					}
					pos++;
				} else {
					// Anything else
					// Reconsume in the script data double escaped state.
					state = STATE_SCRIPT_DATA_DOUBLE_ESCAPED;
					// Reconsume
				}
				break;

			// https://html.spec.whatwg.org/multipage/parsing.html#plaintext-state
			case STATE_PLAINTEXT:
				// Consume the next input character:
				// U+0000 NULL is an unexpected-null-character parse error.
				// Anything else: emit the current input character.
				if (cc === CC_NULL) {
					reportError("unexpected-null-character", pos, pos + 1, "warning");
					pos++;
				} else {
					// Fast-forward to the next NULL (or EOF).
					pos++;
					while (pos < len && input.charCodeAt(pos) !== CC_NULL) pos++;
				}
				break;

			// https://html.spec.whatwg.org/multipage/parsing.html#character-reference-state
			case STATE_CHARACTER_REFERENCE:
				// Set the temporary buffer to the empty string. Append a U+0026
				// AMPERSAND (&) character to the temporary buffer.
				// `charRefStart` points at that `&` (one before the current pos).
				charRefStart = pos - 1;
				// Consume the next input character:
				if (isAsciiAlphanumeric(cc)) {
					// ASCII alphanumeric
					// Reconsume in the named character reference state.
					state = STATE_NAMED_CHARACTER_REFERENCE;
					// Reconsume
				} else if (cc === CC_NUMBER_SIGN) {
					// U+0023 NUMBER SIGN (#)
					// Append the current input character to the temporary buffer.
					// Set the character reference code to zero. Switch to the
					// numeric character reference state.
					charRefCode = 0;
					state = STATE_NUMERIC_CHARACTER_REFERENCE;
					pos++;
				} else {
					// Anything else
					// Flush code points consumed as a character reference.
					// Reconsume in the return state.
					state = returnState;
					// Reconsume
				}
				break;

			// https://html.spec.whatwg.org/multipage/parsing.html#named-character-reference-state
			case STATE_NAMED_CHARACTER_REFERENCE: {
				// Consume the maximum number of characters possible where the
				// consumed characters are one of the identifiers in the first
				// column of the named character references table.
				//
				// We measure the longest run of ASCII alphanumeric characters
				// (capped at MAX_ENTITY_NAME_LEN - 1 since the optional `;` is
				// handled separately), then walk that run from longest to
				// shortest looking for the first prefix that exists in the
				// entity table (with a trailing `;` if present, otherwise the
				// legacy bare form).
				let runLen = 0;
				while (
					pos + runLen < len &&
					isAsciiAlphanumeric(input.charCodeAt(pos + runLen)) &&
					runLen < MAX_ENTITY_NAME_LEN - 1
				) {
					runLen++;
				}
				const hasSemicolon =
					pos + runLen < len && input.charCodeAt(pos + runLen) === CC_SEMICOLON;
				namedEntityConsumed = 0;
				let matchedWithSemicolon = false;
				// Slice the candidate run from `input` once; prefixes are taken from
				// this short string instead of re-slicing the (potentially huge)
				// input per length.
				const run = input.slice(pos, pos + runLen);
				for (let n = runLen; n > 0; n--) {
					const bare = n === runLen ? run : run.slice(0, n);
					// Try with trailing `;` first if one is present after the run.
					if (
						n === runLen &&
						hasSemicolon &&
						HTML_ENTITIES[`${bare};`] !== undefined
					) {
						namedEntityConsumed = n + 1;
						matchedWithSemicolon = true;
						break;
					}
					if (HTML_ENTITIES[bare] !== undefined) {
						namedEntityConsumed = n;
						break;
					}
				}
				if (namedEntityConsumed > 0) {
					// A legacy match without a trailing `;` is a
					// missing-semicolon-after-character-reference parse error,
					// except for the spec's historical attribute rule: when
					// consumed in an attribute value and the next char is `=` or
					// ASCII alphanumeric, the reference is left undecoded silently.
					if (!matchedWithSemicolon) {
						const next = input.charCodeAt(pos + namedEntityConsumed);
						const inAttribute =
							returnState === STATE_ATTRIBUTE_VALUE_DOUBLE_QUOTED ||
							returnState === STATE_ATTRIBUTE_VALUE_SINGLE_QUOTED ||
							returnState === STATE_ATTRIBUTE_VALUE_UNQUOTED;
						if (
							!(
								inAttribute &&
								(next === CC_EQUALS || isAsciiAlphanumeric(next))
							)
						) {
							reportError(
								"missing-semicolon-after-character-reference",
								pos + namedEntityConsumed,
								pos + namedEntityConsumed + 1,
								"warning"
							);
						}
					}
					pos += namedEntityConsumed;
					state = returnState;
				} else {
					// No match — flush code points consumed as a character
					// reference. Switch to the ambiguous ampersand state.
					state = STATE_AMBIGUOUS_AMPERSAND;
				}
				break;
			}

			// https://html.spec.whatwg.org/multipage/parsing.html#ambiguous-ampersand-state
			case STATE_AMBIGUOUS_AMPERSAND:
				// Consume the next input character:
				if (isAsciiAlphanumeric(cc)) {
					// ASCII alphanumeric
					// If the character reference was consumed as part of an
					// attribute, then append the current input character to the
					// current attribute's value. Otherwise, emit the current
					// input character as a character token.
					pos++;
				} else if (cc === CC_SEMICOLON) {
					// U+003B SEMICOLON (;)
					// This is an unknown-named-character-reference parse error.
					// Reconsume in the return state.
					reportError(
						"unknown-named-character-reference",
						pos,
						pos + 1,
						"warning"
					);
					state = returnState;
					// Reconsume
				} else {
					// Anything else
					// Reconsume in the return state.
					state = returnState;
					// Reconsume
				}
				break;

			// https://html.spec.whatwg.org/multipage/parsing.html#numeric-character-reference-state
			case STATE_NUMERIC_CHARACTER_REFERENCE:
				// Set the character reference code to zero (0).
				// Consume the next input character:
				if (cc === 0x78 || cc === 0x58) {
					// U+0078 LATIN SMALL LETTER X
					// U+0058 LATIN CAPITAL LETTER X
					// Append the current input character to the temporary
					// buffer. Switch to the hexadecimal character reference
					// start state.
					state = STATE_HEXADECIMAL_CHARACTER_REFERENCE_START;
					pos++;
				} else {
					// Anything else
					// Reconsume in the decimal character reference start state.
					state = STATE_DECIMAL_CHARACTER_REFERENCE_START;
					// Reconsume
				}
				break;

			// https://html.spec.whatwg.org/multipage/parsing.html#hexadecimal-character-reference-start-state
			case STATE_HEXADECIMAL_CHARACTER_REFERENCE_START:
				// Consume the next input character:
				// ASCII hex digit: reconsume in the hexadecimal character reference state.
				// Anything else: absence-of-digits-in-numeric-character-reference parse
				// error. Flush code points consumed as a character reference. Reconsume
				// in the return state.
				if (isAsciiHexDigit(cc)) {
					state = STATE_HEXADECIMAL_CHARACTER_REFERENCE;
				} else {
					reportError(
						"absence-of-digits-in-numeric-character-reference",
						pos,
						pos + 1,
						"warning"
					);
					state = returnState;
				}
				// Reconsume
				break;

			// https://html.spec.whatwg.org/multipage/parsing.html#decimal-character-reference-start-state
			case STATE_DECIMAL_CHARACTER_REFERENCE_START:
				// Consume the next input character:
				// ASCII digit: reconsume in the decimal character reference state.
				// Anything else: absence-of-digits-in-numeric-character-reference parse
				// error. Flush code points consumed as a character reference. Reconsume
				// in the return state.
				if (isAsciiDigit(cc)) {
					state = STATE_DECIMAL_CHARACTER_REFERENCE;
				} else {
					reportError(
						"absence-of-digits-in-numeric-character-reference",
						pos,
						pos + 1,
						"warning"
					);
					state = returnState;
				}
				// Reconsume
				break;

			// https://html.spec.whatwg.org/multipage/parsing.html#hexadecimal-character-reference-state
			case STATE_HEXADECIMAL_CHARACTER_REFERENCE:
				// Consume the next input character:
				if (isAsciiHexDigit(cc)) {
					// ASCII digit / upper hex / lower hex
					// Multiply the character reference code by 16. Add a numeric
					// version of the current input character to the character
					// reference code. Stop accumulating once past the Unicode
					// range so the value can't overflow (still flags as
					// outside-range at the end).
					if (charRefCode < 0x110000) {
						const v = cc <= 0x39 ? cc - 0x30 : (cc | 0x20) - 0x61 + 10;
						charRefCode = charRefCode * 16 + v;
					}
					pos++;
				} else if (cc === CC_SEMICOLON) {
					// U+003B SEMICOLON
					// Switch to the numeric character reference end state.
					state = STATE_NUMERIC_CHARACTER_REFERENCE_END;
					pos++;
				} else {
					// Anything else
					// This is a missing-semicolon-after-character-reference
					// parse error. Reconsume in the numeric character reference
					// end state.
					reportError(
						"missing-semicolon-after-character-reference",
						pos,
						pos + 1,
						"warning"
					);
					state = STATE_NUMERIC_CHARACTER_REFERENCE_END;
					// Reconsume
				}
				break;

			// https://html.spec.whatwg.org/multipage/parsing.html#decimal-character-reference-state
			case STATE_DECIMAL_CHARACTER_REFERENCE:
				// Consume the next input character:
				if (isAsciiDigit(cc)) {
					// ASCII digit
					// Multiply the character reference code by 10. Add a numeric
					// version of the current input character (subtract 0x0030
					// from the character's code point) to the character reference
					// code. Stop accumulating once past the Unicode range so the
					// value can't overflow (still flags as outside-range).
					if (charRefCode < 0x110000) {
						charRefCode = charRefCode * 10 + (cc - 0x30);
					}
					pos++;
				} else if (cc === CC_SEMICOLON) {
					// U+003B SEMICOLON
					// Switch to the numeric character reference end state.
					state = STATE_NUMERIC_CHARACTER_REFERENCE_END;
					pos++;
				} else {
					// Anything else
					// This is a missing-semicolon-after-character-reference
					// parse error. Reconsume in the numeric character reference
					// end state.
					reportError(
						"missing-semicolon-after-character-reference",
						pos,
						pos + 1,
						"warning"
					);
					state = STATE_NUMERIC_CHARACTER_REFERENCE_END;
					// Reconsume
				}
				break;

			// https://html.spec.whatwg.org/multipage/parsing.html#numeric-character-reference-end-state
			case STATE_NUMERIC_CHARACTER_REFERENCE_END:
				// Check the character reference code and report the matching
				// WHATWG validation parse error.
				validateNumericReference(pos);
				// Flush code points consumed as a character reference.
				// Switch to the return state.
				state = returnState;
				// Reconsume
				break;

			/* istanbul ignore next -- @preserve: defensive fallback, all states are explicit above */
			default:
				pos++;
		}
	}

	// Handle EOF in non-data states per the WHATWG spec.
	//
	// Each in-progress comment / doctype / cdata / tag emits its partial
	// token range plus a corresponding `eof-in-X` parse error. Severity is
	// `"error"` because the emitted token offset range is incomplete (missing
	// trailing `-->`, `>`, `]]>`, etc.). For data / `<` / `</` / `<!`-only
	// inputs we emit `eof-before-tag-name` and fall through to flush the
	// pending text span (which still contains the lone `<`).
	// EOF inside a character-reference state: run the end-of-reference
	// processing (numeric end states never run when the reference ends at
	// EOF), then resume in the return state for the branches below.
	if (
		state >= STATE_CHARACTER_REFERENCE &&
		state <= STATE_NUMERIC_CHARACTER_REFERENCE_END
	) {
		if (
			state === STATE_NUMERIC_CHARACTER_REFERENCE ||
			state === STATE_HEXADECIMAL_CHARACTER_REFERENCE_START ||
			state === STATE_DECIMAL_CHARACTER_REFERENCE_START
		) {
			// No digits before EOF.
			reportError(
				"absence-of-digits-in-numeric-character-reference",
				len,
				len,
				"warning"
			);
		} else if (
			state === STATE_HEXADECIMAL_CHARACTER_REFERENCE ||
			state === STATE_DECIMAL_CHARACTER_REFERENCE
		) {
			// Digits but no closing `;` before EOF.
			reportError(
				"missing-semicolon-after-character-reference",
				len,
				len,
				"warning"
			);
			validateNumericReference(len);
		} else if (state === STATE_NUMERIC_CHARACTER_REFERENCE_END) {
			validateNumericReference(len);
		}
		state = returnState;
	}

	if (
		(state >= STATE_TAG_NAME && state <= STATE_SELF_CLOSING_START_TAG) ||
		state === STATE_RCDATA_END_TAG_NAME ||
		state === STATE_RAWTEXT_END_TAG_NAME ||
		state === STATE_SCRIPT_DATA_END_TAG_NAME ||
		state === STATE_SCRIPT_DATA_ESCAPED_END_TAG_NAME
	) {
		// EOF mid-tag — emit the partial open/close tag at EOF so the
		// consumer still sees the tag. This is a deliberate deviation
		// from the spec's per-character emission model: rather than
		// dropping the in-progress tag, we emit its offset range up to EOF.
		reportError("eof-in-tag", len, len, "error");
		// If we hit EOF mid-attribute-name, the name runs to EOF. Set
		// attrNameEnd here so the emitted attribute range is valid.
		if (state === STATE_ATTRIBUTE_NAME && attrNameStart !== -1) {
			attrNameEnd = len;
		}
		if (attrNameStart !== -1) emitAttribute(len);
		// If we hit EOF before the tag-name end was recorded, the name runs
		// to EOF. `tagNameEnd` may carry over from a previously emitted tag,
		// so reset it whenever it's missing or stale (less than `tagNameStart`)
		// — covers `<div` open-tag EOFs as well as `<title>x</tit` and other
		// content-mode end-tag-name EOFs.
		if (tagNameStart !== -1 && tagNameEnd < tagNameStart) {
			tagNameEnd = len;
		}
		flushText(tagStart);
		pos =
			input.charCodeAt(tagStart + 1) === CC_SOLIDUS
				? emitCloseTag(len)
				: emitOpenTag(len, false);
	} else if (
		(state >= STATE_COMMENT_START && state <= STATE_BOGUS_COMMENT) ||
		(state >= STATE_COMMENT_LESS_THAN_SIGN &&
			state <= STATE_COMMENT_LESS_THAN_SIGN_BANG_DASH_DASH) ||
		state === STATE_MARKUP_DECLARATION_OPEN
	) {
		// EOF in markup-declaration-open takes the spec's "anything else"
		// branch: incorrectly-opened-comment, then a bogus comment (which has
		// no EOF error of its own). Bogus comments at EOF are likewise normal.
		if (state === STATE_MARKUP_DECLARATION_OPEN) {
			reportError("incorrectly-opened-comment", commentStart, len, "warning");
		} else if (state !== STATE_BOGUS_COMMENT) {
			reportError("eof-in-comment", len, len, "error");
		}
		if (callbacks.comment !== undefined) {
			pos = callbacks.comment(input, commentStart, len);
		}
	} else if (state >= STATE_CDATA_SECTION && state <= STATE_CDATA_SECTION_END) {
		reportError("eof-in-cdata", len, len, "error");
		if (callbacks.comment !== undefined) {
			pos = callbacks.comment(input, commentStart, len);
		}
	} else if (state >= STATE_DOCTYPE && state <= STATE_BOGUS_DOCTYPE) {
		// EOF in bogus DOCTYPE emits the token with no parse error (spec).
		if (state !== STATE_BOGUS_DOCTYPE) {
			reportError("eof-in-doctype", len, len, "error");
		}
		if (callbacks.doctype !== undefined) {
			pos = callbacks.doctype(input, commentStart, len);
		}
	} else {
		if (
			state === STATE_SCRIPT_DATA_ESCAPED ||
			state === STATE_SCRIPT_DATA_ESCAPED_DASH ||
			state === STATE_SCRIPT_DATA_ESCAPED_DASH_DASH ||
			state === STATE_SCRIPT_DATA_ESCAPED_LESS_THAN_SIGN ||
			state === STATE_SCRIPT_DATA_DOUBLE_ESCAPED ||
			state === STATE_SCRIPT_DATA_DOUBLE_ESCAPED_DASH ||
			state === STATE_SCRIPT_DATA_DOUBLE_ESCAPED_DASH_DASH ||
			state === STATE_SCRIPT_DATA_DOUBLE_ESCAPED_LESS_THAN_SIGN ||
			state === STATE_SCRIPT_DATA_DOUBLE_ESCAPE_END
		) {
			// Inside `<script><!-- … ` at EOF — spec calls this an
			// eof-in-script-html-comment-like-text parse error. The
			// less-than-sign and double-escape-end states reconsume back
			// into the (double-)escaped state on EOF per spec, which then
			// hits this same error.
			reportError("eof-in-script-html-comment-like-text", len, len, "error");
		} else if (state === STATE_TAG_OPEN || state === STATE_END_TAG_OPEN) {
			// `<` or `</` with nothing after; spec calls this
			// eof-before-tag-name. The lone `<` / `</` is preserved in the
			// pending text span which is flushed below.
			reportError("eof-before-tag-name", len, len, "warning");
		}
		if (textStart < len && callbacks.text !== undefined) {
			callbacks.text(input, textStart, len);
		}
	}

	return pos;
};

// WHATWG numeric-character-reference-end Windows-1252 remap table for the
// 0x80-0x9F range. Per spec these C1 control code points decode to the
// corresponding Windows-1252 glyph (with a parse error) rather than to the
// raw C1 control character.
const NUMERIC_C1_REMAP = {
	0x80: "€",
	0x82: "‚",
	0x83: "ƒ",
	0x84: "„",
	0x85: "…",
	0x86: "†",
	0x87: "‡",
	0x88: "ˆ",
	0x89: "‰",
	0x8a: "Š",
	0x8b: "‹",
	0x8c: "Œ",
	0x8e: "Ž",
	0x91: "‘",
	0x92: "’",
	0x93: "“",
	0x94: "”",
	0x95: "•",
	0x96: "–",
	0x97: "—",
	0x98: "˜",
	0x99: "™",
	0x9a: "š",
	0x9b: "›",
	0x9c: "œ",
	0x9e: "ž",
	0x9f: "Ÿ"
};

/**
 * @param {number} code numeric character reference code point
 * @returns {string} decoded character per WHATWG remap rules
 */
const decodeNumericReference = (code) => {
	// Per WHATWG numeric-character-reference-end-state:
	//   - 0x00, > 0x10FFFF, or surrogate (0xD800-0xDFFF) -> U+FFFD.
	//   - 0x80-0x9F -> Windows-1252 remap (above).
	//   - Anything else (including noncharacters and C0 controls) -> the
	//     code point itself; we don't surface the spec's parse-error
	//     classes here since decoding is happening after the scanner ran.
	if (code === 0 || code > 0x10ffff || (code >= 0xd800 && code <= 0xdfff)) {
		return "�";
	}
	if (code >= 0x80 && code <= 0x9f) {
		const remapped = /** @type {Record<number, string>} */ (NUMERIC_C1_REMAP)[
			code
		];
		if (remapped !== undefined) return remapped;
	}
	return String.fromCodePoint(code);
};

// Match one of three forms (each with an optional trailing `;`):
//   `&#x<hex>` - hex numeric reference (requires the `x`/`X`).
//   `&#<dec>`  - decimal numeric reference (digits only).
//   `&<name>`  - named reference (letter followed by alphanumerics).
// The three alternatives are kept separate so a decimal reference like
// `&#65b` doesn't greedily eat the trailing `b` as if it were hex.
const CHARACTER_REFERENCE_REGEXP =
	/&(?:#[xX][0-9a-fA-F]+|#[0-9]+|[a-zA-Z][a-zA-Z0-9]*);?/g;

/**
 * Decode a single matched character reference.
 * @param {string} match the matched reference text
 * @param {number} nextCharCode char code following the match in the source (NaN at the end)
 * @param {boolean=} isAttribute true when the match came from an attribute value
 * @returns {string} decoded text, or `match` itself when it stays literal
 */
const decodeOneReference = (match, nextCharCode, isAttribute) => {
	// Numeric reference: &#65; or &#x41;
	if (match.charCodeAt(1) === 0x23 /* # */) {
		const lastChar = match.charAt(match.length - 1);
		const isHex = match.charCodeAt(2) === 0x78 || match.charCodeAt(2) === 0x58;
		const body = isHex
			? lastChar === ";"
				? match.slice(3, -1)
				: match.slice(3)
			: lastChar === ";"
				? match.slice(2, -1)
				: match.slice(2);
		// The regex above guarantees at least one digit in `body`,
		// so `parseInt` always returns a finite number here.
		return decodeNumericReference(Number.parseInt(body, isHex ? 16 : 10));
	}

	// Named reference. Try the full captured name first, then
	// progressively shorter prefixes - this handles direct matches
	// like `&amp;` as well as WHATWG longest-prefix semantics where
	// e.g. `&notpre;` decodes as `&not` (a legacy bare entity)
	// followed by `pre;` as literal text.
	const name = match.slice(1);
	const matchEndsWithSemi = name.charCodeAt(name.length - 1) === 0x3b;

	// Attribute-context guard: if the entity match didn't end with `;`
	// and the next character in the source is `=` or ASCII
	// alphanumeric, the WHATWG spec says to flush the literal text
	// rather than decode. The greedy regex already absorbed any
	// trailing alphanumerics, so the only candidate "next char" here
	// is `=` (or any non-alphanumeric).
	if (isAttribute && !matchEndsWithSemi && nextCharCode === 0x3d /* = */) {
		return match;
	}

	// Fast path: the regex usually captures exactly one entity (`&amp;`,
	// `&lt;`, `&nbsp;`, …), so the whole `name` is the match — avoid the
	// full-length `name.slice(0, name.length)` the loop's first iteration
	// would allocate. No leftover, so the attribute guard never applies.
	if (name.length <= MAX_ENTITY_NAME_LEN) {
		const full = HTML_ENTITIES[name];
		if (full !== undefined) return full;
	}

	// Cap the longest-prefix search at MAX_ENTITY_NAME_LEN so pathological
	// inputs like `&` + thousands of alphanumerics stay linear-time.
	// Anything past that cap can't possibly match and is appended
	// verbatim as part of `name.slice(i)`. The full-length case was just
	// handled above, so start one shorter when it's the cap.
	const searchLen =
		name.length > MAX_ENTITY_NAME_LEN ? MAX_ENTITY_NAME_LEN : name.length - 1;
	for (let i = searchLen; i > 0; i--) {
		const prefix = name.slice(0, i);
		if (HTML_ENTITIES[prefix] !== undefined) {
			// Attribute-context longest-prefix guard: if the matched
			// prefix doesn't end with `;` and the leftover starts with
			// an alphanumeric character, leave literal per WHATWG.
			// (The regex greedy-consumes alphanumerics, so any leftover
			// within `name` is itself alphanumeric — we only need to
			// check non-empty leftover here; the `=` case is handled
			// above against the source character after the match.)
			if (
				isAttribute &&
				i < name.length &&
				prefix.charCodeAt(prefix.length - 1) !== 0x3b
			) {
				return match;
			}
			return HTML_ENTITIES[prefix] + name.slice(i);
		}
	}
	return match;
};

/**
 * Decode HTML character references in a string. Handles all numeric
 * references (with WHATWG remap of 0x00, surrogates, out-of-range, and the
 * C1 Windows-1252 table) and the full WHATWG named character references
 * table. Unknown or malformed references are left as literal text.
 *
 * When `isAttribute` is `true`, applies the WHATWG
 * "consumed-as-part-of-an-attribute" rule: a named reference without a
 * trailing `;` whose next character is `=` or ASCII alphanumeric is left
 * undecoded, so e.g. `&amp=foo` stays literal in an attribute value but
 * decodes to `&=foo` in text.
 * @param {string} str the raw string from the token slice
 * @param {boolean=} isAttribute true if `str` came from an attribute value
 * @returns {string} decoded string
 */
const decodeHtmlEntities = (str, isAttribute) => {
	if (!str.includes("&")) return str;

	return str.replace(CHARACTER_REFERENCE_REGEXP, (match, offset, source) =>
		decodeOneReference(
			match,
			source.charCodeAt(offset + match.length),
			isAttribute
		)
	);
};

/**
 * Like `decodeHtmlEntities`, but also returns a boundary map from the
 * decoded string back to raw offsets, so spans computed on the decoded
 * text (e.g. srcset candidate URLs) can be translated to source ranges.
 * `map[i]` is the raw offset of decoded boundary `i` (`0..text.length`);
 * boundaries inside a reference's decoded text map to the reference start.
 * `map` is `undefined` when nothing was decoded (then `text === str`).
 * @param {string} str the raw string from the token slice
 * @param {boolean=} isAttribute true if `str` came from an attribute value
 * @returns {{ text: string, map: number[] | undefined }} decoded text and offset map
 */
const decodeHtmlEntitiesWithMap = (str, isAttribute) => {
	/** @type {number[] | undefined} */
	let map;
	if (str.includes("&")) {
		CHARACTER_REFERENCE_REGEXP.lastIndex = 0;
		let text = "";
		let last = 0;
		/** @type {RegExpExecArray | null} */
		let m;
		while ((m = CHARACTER_REFERENCE_REGEXP.exec(str)) !== null) {
			const match = m[0];
			const decoded = decodeOneReference(
				match,
				str.charCodeAt(m.index + match.length),
				isAttribute
			);
			if (decoded === match) continue;
			if (map === undefined) map = [];
			for (let r = last; r < m.index; r++) map.push(r);
			text += str.slice(last, m.index);
			for (let i = 0; i < decoded.length; i++) map.push(m.index);
			text += decoded;
			last = m.index + match.length;
		}
		if (map !== undefined) {
			for (let r = last; r < str.length; r++) map.push(r);
			map.push(str.length);
			return { text: text + str.slice(last), map };
		}
	}
	return { text: str, map };
};

// cspell:ignore advasoft altglyph altglyphdef altglyphitem animatecolor animatemotion animatetransform arcrole aswedit attributename attributetype basefrequency baseprofile bgsound calcmode clippathunits definitionurl diffuseconstant fedropshadow filterunits glyphref gradienttransform gradientunits hotjava hotmetal kernelmatrix kernelunitlength keypoints keysplines keytimes limitingconeangle malignmark markerheight markerwidth maskcontentunits maskunits metrius mglyph mtext numoctaves pathlength patterncontentunits patterntransform patternunits pointsatx pointsaty pointsatz preservealpha primitiveunits refx refy repeatcount repeatdur requiredextensions requiredfeatures selectedcontent silmaril softquad specularconstant specularexponent startoffset stddeviation stitchtiles surfacescale systemlanguage tablevalues targetx targety textlength viewbox viewtarget webtechs xchannelselector ychannelselector megamorphic attributeless rowspan imagesizes novalidate maxlength

// WHATWG HTML tree construction (https://html.spec.whatwg.org/multipage/parsing.html#tree-construction)
// on top of walkHtmlTokens. Scripting is always disabled (webpack is a build tool).

// Namespaces (mirrors swc_html_ast::Namespace)
const NS_HTML = 0;
const NS_MATHML = 1;
const NS_SVG = 2;

/**
 * @typedef {object} HtmlAttribute
 * @property {string} name lowercased (and, in foreign content, adjusted) attribute name
 * @property {string} value
 * @property {string=} serializedName name used by the html5lib tree serializer (foreign-namespaced)
 * @property {number} nameStart source offset, or -1 on adoption-agency clones
 * @property {number} nameEnd
 * @property {number} valueStart source offset, or -1 when valueless / on clones
 * @property {number} valueEnd
 */

/**
 * @typedef {object} HtmlElement
 * @property {typeof NodeType.Element} type
 * @property {string} tagName
 * @property {number} namespace
 * @property {HtmlAttribute[]} attributes
 * @property {HtmlNode[]} children
 * @property {boolean} selfClosing
 * @property {number} start
 * @property {number} end
 * @property {number} tagEnd end offset of the opening tag (after `>`)
 * @property {number} nameEnd end offset of the tag name
 * @property {HtmlDocumentFragment=} templateContent `<template>` content fragment
 */

/**
 * @typedef {object} HtmlText
 * @property {typeof NodeType.Text} type
 * @property {string} data
 * @property {number} start
 * @property {number} end
 */

/**
 * @typedef {object} HtmlComment
 * @property {typeof NodeType.Comment} type
 * @property {string} data
 * @property {number} start
 * @property {number} end
 */

/**
 * @typedef {object} HtmlDoctype
 * @property {typeof NodeType.Doctype} type
 * @property {string} name
 * @property {string | null} publicId
 * @property {string | null} systemId
 * @property {number} start
 * @property {number} end
 */

/**
 * @typedef {object} HtmlDocument
 * @property {typeof NodeType.Document} type
 * @property {HtmlNode[]} children
 */

/**
 * @typedef {object} HtmlDocumentFragment
 * @property {typeof NodeType.DocumentFragment} type
 * @property {HtmlNode[]} children
 */

/** @typedef {HtmlElement | HtmlText | HtmlComment | HtmlDoctype} HtmlNode */

/** @typedef {{ start: number, end: number, tagEnd: number, nameEnd: number }} TagPos */

// Tree-construction token `type` discriminators. Numeric for the same reason
// as `NodeType` / the CSS `TT_*` constants: the insertion modes dispatch on
// `t.type` per token, and integer `===` beats string comparison there.
const TOKEN_CHAR = 1;
const TOKEN_COMMENT = 2;
const TOKEN_DOCTYPE = 3;
const TOKEN_START_TAG = 4;
const TOKEN_END_TAG = 5;
const TOKEN_EOF = 6;

/** @typedef {{ type: typeof TOKEN_CHAR, data: string, start: number, end: number }} CharToken */
/** @typedef {{ type: typeof TOKEN_COMMENT, data: string, start: number, end: number }} CommentToken */
/** @typedef {{ type: typeof TOKEN_DOCTYPE, name: string, publicId: (string | null), systemId: (string | null), start: number, end: number }} DoctypeToken */
/** @typedef {{ type: typeof TOKEN_START_TAG, name: string, attrs: HtmlAttribute[], selfClosing: boolean, pos: TagPos, swallowNewline?: boolean }} StartTagToken */
/** @typedef {{ type: typeof TOKEN_END_TAG, name: string, pos: TagPos }} EndTagToken */
/** @typedef {{ type: typeof TOKEN_EOF }} EofToken */

/**
 * Internal token passed through the tree-construction insertion modes.
 * @typedef {CharToken | CommentToken | DoctypeToken | StartTagToken | EndTagToken | EofToken} Token
 */

/**
 * The tree builder reuses a single mutable token (with a reused `pos`) instead
 * of allocating one object per tokenizer callback. All fields are always
 * present so the shape never changes — keeping the `process`/insertion-mode
 * `t.*` reads monomorphic — and fields irrelevant to the current `type` carry
 * stale values that those handlers never read. Tokens that must outlive the
 * current callback (buffered table characters, synthesized re-dispatches) are
 * copied into fresh plain objects instead.
 * @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
 */

/** @typedef {{ parent: HtmlElement | HtmlDocument | HtmlDocumentFragment, beforeNode: (HtmlNode | null) }} InsertionPlace */

// Insertion modes (§13.2.4.1). Numeric for the same reason as the token and
// `NodeType` enums: `runMode` dispatches on `mode` once per token.
const MODE_INITIAL = 1;
const MODE_BEFORE_HTML = 2;
const MODE_BEFORE_HEAD = 3;
const MODE_IN_HEAD = 4;
const MODE_IN_HEAD_NOSCRIPT = 5;
const MODE_AFTER_HEAD = 6;
const MODE_IN_BODY = 7;
const MODE_TEXT = 8;
const MODE_IN_TABLE = 9;
const MODE_IN_TABLE_TEXT = 10;
const MODE_IN_CAPTION = 11;
const MODE_IN_COLUMN_GROUP = 12;
const MODE_IN_TABLE_BODY = 13;
const MODE_IN_ROW = 14;
const MODE_IN_CELL = 15;
const MODE_IN_TEMPLATE = 16;
const MODE_AFTER_BODY = 17;
const MODE_IN_FRAMESET = 18;
const MODE_AFTER_FRAMESET = 19;
const MODE_AFTER_AFTER_BODY = 20;
const MODE_AFTER_AFTER_FRAMESET = 21;

// "in template" start-tag re-dispatch targets (§13.2.6.4.18).
const TEMPLATE_START_TAG_MODES = new Map([
	["caption", MODE_IN_TABLE],
	["colgroup", MODE_IN_TABLE],
	["tbody", MODE_IN_TABLE],
	["tfoot", MODE_IN_TABLE],
	["thead", MODE_IN_TABLE],
	["col", MODE_IN_COLUMN_GROUP],
	["tr", MODE_IN_TABLE_BODY],
	["td", MODE_IN_ROW],
	["th", MODE_IN_ROW]
]);

const VOID = new Set([
	"area",
	"base",
	"br",
	"col",
	"embed",
	"hr",
	"img",
	"input",
	"keygen",
	"link",
	"meta",
	"param",
	"source",
	"track",
	"wbr"
]);

const SPECIAL = new Set([
	"address",
	"applet",
	"area",
	"article",
	"aside",
	"base",
	"basefont",
	"bgsound",
	"blockquote",
	"body",
	"br",
	"button",
	"caption",
	"center",
	"col",
	"colgroup",
	"dd",
	"details",
	"dir",
	"div",
	"dl",
	"dt",
	"embed",
	"fieldset",
	"figcaption",
	"figure",
	"footer",
	"form",
	"frame",
	"frameset",
	"h1",
	"h2",
	"h3",
	"h4",
	"h5",
	"h6",
	"head",
	"header",
	"hgroup",
	"hr",
	"html",
	"iframe",
	"img",
	"input",
	"keygen",
	"li",
	"link",
	"listing",
	"main",
	"marquee",
	"menu",
	"meta",
	"nav",
	"noembed",
	"noframes",
	"noscript",
	"object",
	"ol",
	"p",
	"param",
	"plaintext",
	"pre",
	"script",
	"search",
	"section",
	"select",
	"source",
	"style",
	"summary",
	"table",
	"tbody",
	"td",
	"template",
	"textarea",
	"tfoot",
	"th",
	"thead",
	"title",
	"tr",
	"track",
	"ul",
	"wbr",
	"xmp"
]);
// MathML/SVG specials handled via namespace checks below.

const FORMATTING = new Set([
	"a",
	"b",
	"big",
	"code",
	"em",
	"font",
	"i",
	"nobr",
	"s",
	"small",
	"strike",
	"strong",
	"tt",
	"u"
]);

const HEADING = new Set(["h1", "h2", "h3", "h4", "h5", "h6"]);

const isSpecial = (/** @type {HtmlElement} */ el) => {
	if (el.namespace === NS_HTML) return SPECIAL.has(el.tagName);
	if (el.namespace === NS_MATHML) return MATHML_SPECIAL.has(el.tagName);
	if (el.namespace === NS_SVG) return SVG_SPECIAL.has(el.tagName.toLowerCase());
	return false;
};

// SVG tag name case adjustments (null prototype: looked up with markup-controlled
// names, a plain object would resolve `constructor` etc. to inherited values)
const SVG_TAG_ADJUST = Object.assign(Object.create(null), {
	altglyph: "altGlyph",
	altglyphdef: "altGlyphDef",
	altglyphitem: "altGlyphItem",
	animatecolor: "animateColor",
	animatemotion: "animateMotion",
	animatetransform: "animateTransform",
	clippath: "clipPath",
	feblend: "feBlend",
	fecolormatrix: "feColorMatrix",
	fecomponenttransfer: "feComponentTransfer",
	fecomposite: "feComposite",
	feconvolvematrix: "feConvolveMatrix",
	fediffuselighting: "feDiffuseLighting",
	fedisplacementmap: "feDisplacementMap",
	fedistantlight: "feDistantLight",
	fedropshadow: "feDropShadow",
	feflood: "feFlood",
	fefunca: "feFuncA",
	fefuncb: "feFuncB",
	fefuncg: "feFuncG",
	fefuncr: "feFuncR",
	fegaussianblur: "feGaussianBlur",
	feimage: "feImage",
	femerge: "feMerge",
	femergenode: "feMergeNode",
	femorphology: "feMorphology",
	feoffset: "feOffset",
	fepointlight: "fePointLight",
	fespecularlighting: "feSpecularLighting",
	fespotlight: "feSpotLight",
	fetile: "feTile",
	feturbulence: "feTurbulence",
	foreignobject: "foreignObject",
	glyphref: "glyphRef",
	lineargradient: "linearGradient",
	radialgradient: "radialGradient",
	textpath: "textPath"
});

// SVG attribute case adjustments (lowercase -> camelCase). Subset commonly tested.
// (null prototype, see SVG_TAG_ADJUST)
const SVG_ATTR_ADJUST = Object.assign(Object.create(null), {
	attributename: "attributeName",
	attributetype: "attributeType",
	basefrequency: "baseFrequency",
	baseprofile: "baseProfile",
	calcmode: "calcMode",
	clippathunits: "clipPathUnits",
	diffuseconstant: "diffuseConstant",
	edgemode: "edgeMode",
	filterunits: "filterUnits",
	glyphref: "glyphRef",
	gradienttransform: "gradientTransform",
	gradientunits: "gradientUnits",
	kernelmatrix: "kernelMatrix",
	kernelunitlength: "kernelUnitLength",
	keypoints: "keyPoints",
	keysplines: "keySplines",
	keytimes: "keyTimes",
	lengthadjust: "lengthAdjust",
	limitingconeangle: "limitingConeAngle",
	markerheight: "markerHeight",
	markerunits: "markerUnits",
	markerwidth: "markerWidth",
	maskcontentunits: "maskContentUnits",
	maskunits: "maskUnits",
	numoctaves: "numOctaves",
	pathlength: "pathLength",
	patterncontentunits: "patternContentUnits",
	patterntransform: "patternTransform",
	patternunits: "patternUnits",
	pointsatx: "pointsAtX",
	pointsaty: "pointsAtY",
	pointsatz: "pointsAtZ",
	preservealpha: "preserveAlpha",
	preserveaspectratio: "preserveAspectRatio",
	primitiveunits: "primitiveUnits",
	refx: "refX",
	refy: "refY",
	repeatcount: "repeatCount",
	repeatdur: "repeatDur",
	requiredextensions: "requiredExtensions",
	requiredfeatures: "requiredFeatures",
	specularconstant: "specularConstant",
	specularexponent: "specularExponent",
	spreadmethod: "spreadMethod",
	startoffset: "startOffset",
	stddeviation: "stdDeviation",
	stitchtiles: "stitchTiles",
	surfacescale: "surfaceScale",
	systemlanguage: "systemLanguage",
	tablevalues: "tableValues",
	targetx: "targetX",
	targety: "targetY",
	textlength: "textLength",
	viewbox: "viewBox",
	viewtarget: "viewTarget",
	xchannelselector: "xChannelSelector",
	ychannelselector: "yChannelSelector",
	zoomandpan: "zoomAndPan"
});

// Foreign attributes that get a prefix in the serialization (namespaced).
// (null prototype, see SVG_TAG_ADJUST)
const FOREIGN_ATTR_NS = Object.assign(Object.create(null), {
	"xlink:actuate": "xlink actuate",
	"xlink:arcrole": "xlink arcrole",
	"xlink:href": "xlink href",
	"xlink:role": "xlink role",
	"xlink:show": "xlink show",
	"xlink:title": "xlink title",
	"xlink:type": "xlink type",
	"xml:lang": "xml lang",
	"xml:space": "xml space",
	xmlns: "xmlns",
	"xmlns:xlink": "xmlns xlink"
});

const MATHML_TEXT_INTEGRATION = new Set(["mi", "mo", "mn", "ms", "mtext"]);

// Tag-name groups consulted by the insertion modes, as module-level Sets. The
// tree builder runs these membership tests per token on hot paths; a shared
// `Set.has` avoids allocating a fresh array and running `Array#includes` each
// time. Grouped by where they are used.
const MATHML_SPECIAL = new Set([
	"mi",
	"mo",
	"mn",
	"ms",
	"mtext",
	"annotation-xml"
]);
const SVG_SPECIAL = new Set(["foreignobject", "desc", "title"]);
const HTML_SCOPE = new Set([
	"applet",
	"caption",
	"html",
	"table",
	"td",
	"th",
	"marquee",
	"object",
	"template"
]);
const IMPLIED = new Set([
	"dd",
	"dt",
	"li",
	"optgroup",
	"option",
	"p",
	"rb",
	"rp",
	"rt",
	"rtc"
]);
const IMPLIED_THOROUGH = new Set([
	"caption",
	"colgroup",
	"dd",
	"dt",
	"li",
	"optgroup",
	"option",
	"p",
	"rb",
	"rp",
	"rt",
	"rtc",
	"tbody",
	"td",
	"tfoot",
	"th",
	"thead",
	"tr"
]);
const CLEAR_TABLE = new Set(["table", "template", "html"]);
const CLEAR_TABLE_BODY = new Set([
	"tbody",
	"tfoot",
	"thead",
	"template",
	"html"
]);
const CLEAR_TABLE_ROW = new Set(["tr", "template", "html"]);
const TABLE_SCOPE_STOP = new Set(["html", "table", "template"]);
const TABLE_CONTEXT = new Set(["table", "tbody", "tfoot", "thead", "tr"]);
const TBODY_GROUP = new Set(["tbody", "tfoot", "thead"]);
const TD_TH = new Set(["td", "th"]);
const TD_TH_TR = new Set(["td", "th", "tr"]);
const STYLE_SCRIPT_TEMPLATE = new Set(["style", "script", "template"]);
const HEAD_BODY_HTML_BR = new Set(["head", "body", "html", "br"]);
const BODY_HTML_BR = new Set(["body", "html", "br"]);
// <base>/<basefont>/<bgsound>/<link>/<meta>: void elements inserted then
// immediately popped in the "in head" mode.
const HEAD_VOID_ELEMENTS = new Set([
	"base",
	"basefont",
	"bgsound",
	"link",
	"meta"
]);
const NOFRAMES_STYLE_NOSCRIPT = new Set(["noframes", "style", "noscript"]);
// "in head noscript" start tags that are handled by reprocessing in "in head".
const IN_HEAD_NOSCRIPT_PASSTHROUGH = new Set([
	"basefont",
	"bgsound",
	"link",
	"meta",
	"noframes",
	"style"
]);
const HEAD_ELEMENTS = new Set([
	"base",
	"basefont",
	"bgsound",
	"link",
	"meta",
	"noframes",
	"script",
	"style",
	"template",
	"title"
]);
const BLOCK_START = new Set([
	"address",
	"article",
	"aside",
	"blockquote",
	"center",
	"details",
	"dialog",
	"dir",
	"div",
	"dl",
	"fieldset",
	"figcaption",
	"figure",
	"footer",
	"header",
	"hgroup",
	"main",
	"menu",
	"nav",
	"ol",
	"p",
	"search",
	"section",
	"summary",
	"ul"
]);
const BLOCK_END = new Set([
	"address",
	"article",
	"aside",
	"blockquote",
	"button",
	"center",
	"details",
	"dialog",
	"dir",
	"div",
	"dl",
	"fieldset",
	"figcaption",
	"figure",
	"footer",
	"header",
	"hgroup",
	"listing",
	"main",
	"menu",
	"nav",
	"ol",
	"pre",
	"search",
	"section",
	"summary",
	"ul"
]);
const ADDRESS_DIV_P = new Set(["address", "div", "p"]);
const APPLET_MARQUEE_OBJECT = new Set(["applet", "marquee", "object"]);
const VOID_FORMATTING = new Set([
	"area",
	"br",
	"embed",
	"img",
	"keygen",
	"wbr"
]);
const PARAM_SOURCE_TRACK = new Set(["param", "source", "track"]);
const IGNORED_BODY_TABLE_STARTS = new Set([
	"caption",
	"col",
	"colgroup",
	"frame",
	"head",
	"tbody",
	"td",
	"tfoot",
	"th",
	"thead",
	"tr"
]);
const IN_TABLE_IGNORED_ENDS = new Set([
	"body",
	"caption",
	"col",
	"colgroup",
	"html",
	"tbody",
	"td",
	"tfoot",
	"th",
	"thead",
	"tr"
]);
const CAPTION_TABLE_STARTS = new Set([
	"caption",
	"col",
	"colgroup",
	"tbody",
	"td",
	"tfoot",
	"th",
	"thead",
	"tr"
]);
const CAPTION_IGNORED_ENDS = new Set([
	"body",
	"col",
	"colgroup",
	"html",
	"tbody",
	"td",
	"tfoot",
	"th",
	"thead",
	"tr"
]);
const TBODY_TRIGGER_STARTS = new Set([
	"caption",
	"col",
	"colgroup",
	"tbody",
	"tfoot",
	"thead"
]);
const TBODY_IGNORED_ENDS = new Set([
	"body",
	"caption",
	"col",
	"colgroup",
	"html",
	"td",
	"th",
	"tr"
]);
const ROW_TRIGGER_STARTS = new Set([
	"caption",
	"col",
	"colgroup",
	"tbody",
	"tfoot",
	"thead",
	"tr"
]);
const ROW_IGNORED_ENDS = new Set([
	"body",
	"caption",
	"col",
	"colgroup",
	"html",
	"td",
	"th"
]);
const CELL_IGNORED_ENDS = new Set([
	"body",
	"caption",
	"col",
	"colgroup",
	"html"
]);
const NO_DECODE_TEXT = new Set([
	"script",
	"style",
	"xmp",
	"iframe",
	"noembed",
	"noframes",
	"plaintext"
]);
// HTML start tags that break out of foreign (SVG/MathML) content.
const FOREIGN_BREAKOUT = new Set([
	"b",
	"big",
	"blockquote",
	"body",
	"br",
	"center",
	"code",
	"dd",
	"div",
	"dl",
	"dt",
	"em",
	"embed",
	"h1",
	"h2",
	"h3",
	"h4",
	"h5",
	"h6",
	"head",
	"hr",
	"i",
	"img",
	"li",
	"listing",
	"menu",
	"meta",
	"nobr",
	"ol",
	"p",
	"pre",
	"ruby",
	"s",
	"small",
	"span",
	"strong",
	"strike",
	"sub",
	"sup",
	"table",
	"tt",
	"u",
	"ul",
	"var"
]);
const FONT_BREAKOUT_ATTRS = new Set(["color", "face", "size"]);

// Shared frozen empty attribute list for attributeless elements (the common
// case). Only `<html>`/`<body>` ever receive merged attributes and are always
// built with their own mutable array, so sharing this everywhere else is safe
// and avoids an empty array per element. Frozen so an accidental mutation
// throws instead of corrupting other elements.
const EMPTY_ATTRS = /** @type {HtmlAttribute[]} */ (
	/** @type {unknown} */ (Object.freeze([]))
);

/**
 * Hash of the ASCII-lowercased `name` for the intern tables below; must stay
 * in sync with the range hash in `internLowerName`.
 * @param {string} name lowercase name
 * @returns {number} hash
 */
const hashLowerName = (name) => {
	let h = name.length;
	for (let i = 0; i < name.length; i++) {
		let c = name.charCodeAt(i);
		if (c >= 0x41 && c <= 0x5a) c += 0x20;
		h = ((h << 5) - h + c) | 0;
	}
	return h;
};

/**
 * @param {Iterable<string>} names lowercase names to intern
 * @returns {Map<number, string | string[]>} hash → name(s)
 */
const buildNameInternTable = (names) => {
	/** @type {Map<number, string | string[]>} */
	const table = new Map();
	for (const name of names) {
		const h = hashLowerName(name);
		const cur = table.get(h);
		if (cur === undefined) {
			table.set(h, name);
		} else if (typeof cur === "string") {
			if (cur !== name) table.set(h, [cur, name]);
		} else if (!cur.includes(name)) {
			cur.push(name);
		}
	}
	return table;
};

/**
 * The lowercased name for `input[start..end)`, returning the shared interned
 * string for known names — skipping the per-tag `slice().toLowerCase()`
 * allocation, and making the tree builder's many Set/Map lookups and `===`
 * comparisons on the name hit one string instance with a cached hash.
 * @param {Map<number, string | string[]>} table intern table
 * @param {string} input source text
 * @param {number} start name start
 * @param {number} end name end
 * @returns {string} lowercased name
 */
const internLowerName = (table, input, start, end) => {
	let h = end - start;
	for (let i = start; i < end; i++) {
		let c = input.charCodeAt(i);
		if (c >= 0x41 && c <= 0x5a) c += 0x20;
		h = ((h << 5) - h + c) | 0;
	}
	const hit = table.get(h);
	if (hit !== undefined) {
		if (typeof hit === "string") {
			if (rangeEqualsLower(input, start, end, hit)) return hit;
		} else {
			for (let i = 0; i < hit.length; i++) {
				if (rangeEqualsLower(input, start, end, hit[i])) return hit[i];
			}
		}
	}
	// Unknown name (custom element, data-* attribute, non-ASCII, …).
	return input.slice(start, end).toLowerCase();
};

// Every tag name the tree builder compares against (the sets above already
// cover most of the spec), plus the remaining standard/foreign names so
// ordinary documents intern every tag.
const TAG_NAME_INTERN = buildNameInternTable([
	...VOID,
	...SPECIAL,
	...FORMATTING,
	...HEADING,
	...MATHML_TEXT_INTEGRATION,
	...MATHML_SPECIAL,
	...SVG_SPECIAL,
	...HTML_SCOPE,
	...TABLE_CONTEXT,
	...VOID_FORMATTING,
	...NO_DECODE_TEXT,
	...FOREIGN_BREAKOUT,
	...HEAD_ELEMENTS,
	...Object.keys(SVG_TAG_ADJUST),
	..."a abbr audio bdi bdo canvas cite data datalist del dfn dialog ins kbd label legend map mark math menuitem meter optgroup option output picture progress q rb rp rt rtc ruby samp selectedcontent slot span sub sup svg time u var video".split(
		" "
	)
]);

// Common attribute names (unknown ones — data-*, ARIA, events — fall back).
const ATTR_NAME_INTERN = buildNameInternTable(
	"href src srcset sizes alt title class id style name type value content charset rel media target action method placeholder disabled checked selected multiple readonly required hidden tabindex role lang dir width height loading decoding async defer integrity crossorigin referrerpolicy nonce as for colspan rowspan span label max min step pattern autocomplete autofocus autoplay controls loop muted poster preload download ping imagesrcset imagesizes slot part is property http-equiv accept enctype novalidate maxlength minlength size cols rows wrap open scope headers datetime cite usemap ismap shape coords start reversed face color encoding xmlns".split(
		" "
	)
);

// Hoisted so the many `open.some(...)` "is there an open HTML <template>?"
// checks reuse one predicate instead of allocating an arrow per call.
const isHtmlTemplateEl = (/** @type {HtmlElement} */ e) =>
	e.tagName === "template" && e.namespace === NS_HTML;

// Linear attribute lookup by name — avoids the per-call arrow `Array#find`
// would allocate (attribute lists are short, so a plain loop is faster too).
const findAttr = (
	/** @type {HtmlAttribute[]} */ attrs,
	/** @type {string} */ name
) => {
	for (let i = 0; i < attrs.length; i++) {
		if (attrs[i].name === name) return attrs[i];
	}
	return undefined;
};

/**
 * @param {string} source HTML source
 * @param {string=} fragmentContext context element name for fragment parsing (e.g. `td`, `svg path`); omit for a full document
 * @returns {HtmlDocument} document AST
 */
const buildHtmlAst = (source, fragmentContext) => {
	/** @type {HtmlDocument} */
	const doc = { type: NodeType.Document, children: [] };
	let mode = MODE_INITIAL;
	// Mode to return to after MODE_TEXT / MODE_IN_TABLE_TEXT (0 = unset).
	let originalMode = 0;
	/** @type {HtmlElement[]} stack of open elements (bottom .. top) */
	const open = [];
	/** @type {{ element?: HtmlElement, marker?: boolean }[]} active formatting elements */
	const afe = [];
	const afeEl = (/** @type {{ element?: HtmlElement }} */ e) =>
		/** @type {HtmlElement} */ (e.element);
	/** @type {HtmlElement | null} */
	let head = null;
	/** @type {HtmlElement | null} */
	let form = null;
	let framesetOk = true;
	/** @type {HtmlAttribute[]} */
	let pendingAttrs = [];
	let fosterParenting = false;
	/** @type {number[]} */
	const templateModes = [];
	let quirks = false;
	/** @type {HtmlElement | null} */
	let fragment = null;
	// End offset of the token currently being processed (for element `.end`).
	let tokenEnd = 0;

	const cur = () => /** @type {HtmlElement} */ (open[open.length - 1]);
	const adjustedCurrent = () => {
		if (open.length === 1 && fragment) return fragment;
		return cur();
	};

	const mkEl = (
		/** @type {string} */ tagName,
		/** @type {number} */ ns,
		/** @type {HtmlAttribute[] | undefined} */ attributes,
		/** @type {TagPos | null | undefined} */ pos
	) =>
		/** @type {HtmlElement} */ ({
			type: NodeType.Element,
			tagName,
			namespace: ns,
			attributes: attributes || EMPTY_ATTRS,
			children: [],
			selfClosing: ns === NS_HTML && VOID.has(tagName),
			start: pos ? pos.start : 0,
			end: pos ? pos.end : 0,
			tagEnd: pos ? pos.tagEnd : 0,
			nameEnd: pos ? pos.nameEnd : 0,
			// Present from creation (only `<template>` fills it) so every element
			// keeps one monomorphic hidden class for the open-stack/scope loops.
			templateContent: undefined
		});

	// Clone of a formatting element's attributes: keep name/value (and the
	// serializer name) but drop source offsets so the consumer doesn't emit a
	// duplicate dependency for the reopened element's spans.
	const cloneAttrs = (/** @type {HtmlAttribute[]} */ attributes) =>
		attributes.map((a) => ({
			name: a.name,
			value: a.value,
			serializedName: a.serializedName,
			nameStart: -1,
			nameEnd: -1,
			valueStart: -1,
			valueEnd: -1
		}));

	/**
	 * @param {HtmlElement | HtmlDocument | HtmlDocumentFragment} parent container
	 * @returns {HtmlNode[]} children array to insert into
	 */
	const childrenOf = (parent) =>
		parent.type === NodeType.Element && parent.templateContent
			? parent.templateContent.children
			: parent.children;

	const appendTo = (
		/** @type {HtmlElement | HtmlDocument | HtmlDocumentFragment} */ parent,
		/** @type {HtmlNode} */ node
	) => {
		const arr = childrenOf(parent);
		const last = arr[arr.length - 1];
		if (node.type === NodeType.Text && last && last.type === NodeType.Text) {
			last.data += node.data;
			if (node.end !== undefined) last.end = node.end;
			return;
		}
		arr.push(node);
	};

	// Reused result of `appropriatePlace` — consumed synchronously by
	// `insertAtPlace` and never retained, so one shared object avoids an
	// allocation per inserted node.
	/** @type {InsertionPlace} */
	const sharedPlace = { parent: doc, beforeNode: null };
	const placeAt = (
		/** @type {HtmlElement | HtmlDocument | HtmlDocumentFragment} */ parent,
		/** @type {HtmlNode | null} */ beforeNode
	) => {
		sharedPlace.parent = parent;
		sharedPlace.beforeNode = beforeNode;
		return sharedPlace;
	};

	// "appropriate place for inserting a node"
	const appropriatePlace = () => {
		const target = cur();
		if (
			fosterParenting &&
			TABLE_CONTEXT.has(target.tagName) &&
			target.namespace === NS_HTML
		) {
			// find last template / last table
			let lastTemplate = -1;
			let lastTable = -1;
			for (let i = open.length - 1; i >= 0; i--) {
				if (
					open[i].tagName === "template" &&
					open[i].namespace === NS_HTML &&
					lastTemplate === -1
				) {
					lastTemplate = i;
				}
				if (
					open[i].tagName === "table" &&
					open[i].namespace === NS_HTML &&
					lastTable === -1
				) {
					lastTable = i;
				}
			}
			if (
				lastTemplate !== -1 &&
				(lastTable === -1 || lastTemplate > lastTable)
			) {
				return placeAt(open[lastTemplate], null);
			}
			if (lastTable === -1) {
				return placeAt(open[0], null);
			}
			const table = open[lastTable];
			// find table's parent in tree
			const tp = findParent(table);
			if (tp) return placeAt(tp, table);
			return placeAt(open[lastTable - 1], null);
		}
		return placeAt(target, null);
	};

	const findParent = (/** @type {HtmlNode} */ node) => {
		// search the whole document tree for the parent of node
		/** @type {(HtmlElement | HtmlDocument | HtmlDocumentFragment)[]} */
		const stack = [doc];
		while (stack.length) {
			const x =
				/** @type {HtmlElement | HtmlDocument | HtmlDocumentFragment} */ (
					stack.pop()
				);
			for (const k of childrenOf(x)) {
				if (k === node) return x;
				if (k.type === NodeType.Element) stack.push(k);
			}
		}
		return null;
	};

	const insertAtPlace = (
		/** @type {InsertionPlace} */ place,
		/** @type {HtmlNode} */ node
	) => {
		if (place.beforeNode) {
			const arr = childrenOf(place.parent);
			const idx = arr.indexOf(place.beforeNode);
			if (
				node.type === NodeType.Text &&
				idx > 0 &&
				arr[idx - 1].type === NodeType.Text
			) {
				/** @type {HtmlText} */ (arr[idx - 1]).data += node.data;
				return;
			}
			arr.splice(idx, 0, node);
		} else {
			appendTo(place.parent, node);
		}
	};

	const insertCharacters = (
		/** @type {string} */ data,
		/** @type {number} */ start,
		/** @type {number} */ end
	) => {
		const place = appropriatePlace();
		if (place.parent.type === NodeType.Document) return; // never insert text into document
		// Inlined text insert: when the run merges into the adjacent text sibling
		// (common with inline formatting) only the string is appended — no
		// throwaway text node is allocated. Mirrors `insertAtPlace`/`appendTo`,
		// including that the before-node merge does not bump `end`.
		const arr = childrenOf(place.parent);
		if (place.beforeNode) {
			const idx = arr.indexOf(place.beforeNode);
			const prev = idx > 0 ? arr[idx - 1] : undefined;
			if (prev && prev.type === NodeType.Text) {
				prev.data += data;
				return;
			}
			arr.splice(idx, 0, { type: NodeType.Text, data, start, end });
		} else {
			const last = arr[arr.length - 1];
			if (last && last.type === NodeType.Text) {
				last.data += data;
				last.end = end;
				return;
			}
			arr.push({ type: NodeType.Text, data, start, end });
		}
	};

	/**
	 * @param {string} data comment data
	 * @param {number} start start offset
	 * @param {number} end end offset
	 * @param {InsertionPlace=} place explicit insertion place
	 */
	const insertComment = (data, start, end, place) => {
		const p = place || appropriatePlace();
		insertAtPlace(p, { type: NodeType.Comment, data, start, end });
	};

	const insertHtmlElement = (
		/** @type {string} */ tagName,
		/** @type {HtmlAttribute[]} */ attributes,
		/** @type {TagPos | null} */ pos
	) => {
		const el = mkEl(tagName, NS_HTML, attributes, pos);
		const place = appropriatePlace();
		insertAtPlace(place, el);
		open.push(el);
		return el;
	};

	const insertForeignElement = (
		/** @type {string} */ tagName,
		/** @type {number} */ ns,
		/** @type {HtmlAttribute[]} */ attributes,
		/** @type {TagPos | null} */ pos
	) => {
		const el = mkEl(tagName, ns, attributes, pos);
		const place = appropriatePlace();
		insertAtPlace(place, el);
		open.push(el);
		return el;
	};

	// ---- scopes ----
	const isScopeBoundary = (/** @type {HtmlElement} */ el) => {
		if (el.namespace === NS_HTML) return HTML_SCOPE.has(el.tagName);
		if (el.namespace === NS_MATHML) return MATHML_SPECIAL.has(el.tagName);
		if (el.namespace === NS_SVG) {
			return SVG_SPECIAL.has(el.tagName.toLowerCase());
		}
		return false;
	};
	// Scope "kind" selects which extra elements act as boundaries. Passed as a
	// small int so the scope checks below allocate no per-call predicate closure
	// (these run several times per body tag).
	const SCOPE_DEFAULT = 0;
	const SCOPE_BUTTON = 1;
	const SCOPE_LIST_ITEM = 2;
	const isBoundaryForKind = (
		/** @type {HtmlElement} */ el,
		/** @type {number} */ kind
	) => {
		if (isScopeBoundary(el)) return true;
		if (el.namespace !== NS_HTML) return false;
		if (kind === SCOPE_BUTTON) return el.tagName === "button";
		if (kind === SCOPE_LIST_ITEM) {
			return el.tagName === "ol" || el.tagName === "ul";
		}
		return false;
	};
	// "have an element in scope": walk the open stack from the top until the
	// named HTML element is found (true) or a scope boundary is hit (false).
	const hasNameInScope = (
		/** @type {string} */ tagName,
		/** @type {number} */ kind
	) => {
		for (let i = open.length - 1; i >= 0; i--) {
			const el = open[i];
			if (el.namespace === NS_HTML && el.tagName === tagName) return true;
			if (isBoundaryForKind(el, kind)) return false;
		}
		return false;
	};
	const inScope = (/** @type {string} */ tagName) =>
		hasNameInScope(tagName, SCOPE_DEFAULT);
	const inButtonScope = (/** @type {string} */ tagName) =>
		hasNameInScope(tagName, SCOPE_BUTTON);
	const inListItemScope = (/** @type {string} */ tagName) =>
		hasNameInScope(tagName, SCOPE_LIST_ITEM);
	const inScopeEl = (/** @type {HtmlElement} */ target) => {
		for (let i = open.length - 1; i >= 0; i--) {
			const el = open[i];
			if (el === target) return true;
			if (isScopeBoundary(el)) return false;
		}
		return false;
	};
	// `target` is a single tag name (the common case) or a Set of names.
	const inTableScope = (/** @type {string | Set<string>} */ target) => {
		const set = typeof target === "string" ? null : target;
		for (let i = open.length - 1; i >= 0; i--) {
			const el = open[i];
			if (el.namespace === NS_HTML) {
				if (set ? set.has(el.tagName) : el.tagName === target) return true;
				if (TABLE_SCOPE_STOP.has(el.tagName)) return false;
			}
		}
		return false;
	};
	const generateImpliedEndTags = (except = "") => {
		while (open.length) {
			const el = cur();
			if (
				el.namespace === NS_HTML &&
				IMPLIED.has(el.tagName) &&
				el.tagName !== except
			) {
				open.pop();
			} else {
				break;
			}
		}
	};
	const generateImpliedEndTagsThorough = () => {
		while (open.length) {
			const el = cur();
			if (el.namespace === NS_HTML && IMPLIED_THOROUGH.has(el.tagName)) {
				open.pop();
			} else {
				break;
			}
		}
	};

	// ---- active formatting elements ----
	const pushAfe = (/** @type {HtmlElement} */ el) => {
		let count = 0;
		for (let i = afe.length - 1; i >= 0; i--) {
			const e = afe[i];
			if (e.marker) break;
			if (
				afeEl(e).tagName === el.tagName &&
				afeEl(e).namespace === el.namespace &&
				sameAttrs(afeEl(e), el)
			) {
				count++;
				if (count === 3) {
					afe.splice(i, 1);
					break;
				}
			}
		}
		afe.push({ element: el });
	};
	const sameAttrs = (
		/** @type {HtmlElement} */ a,
		/** @type {HtmlElement} */ b
	) => {
		const aa = a.attributes;
		const ba = b.attributes;
		if (aa.length !== ba.length) return false;
		// Names are unique (deduped), counts tiny — a nested scan beats a Map
		// here and allocates nothing on this formatting-element hot path.
		for (let i = 0; i < ba.length; i++) {
			const x = ba[i];
			let ok = false;
			for (let j = 0; j < aa.length; j++) {
				if (aa[j].name === x.name) {
					ok = aa[j].value === x.value;
					break;
				}
			}
			if (!ok) return false;
		}
		return true;
	};
	const insertMarker = () => afe.push({ marker: true });
	const clearAfeToMarker = () => {
		while (afe.length) {
			const e = /** @type {{ marker?: boolean }} */ (afe.pop());
			if (e.marker) break;
		}
	};
	const reconstructAfe = () => {
		if (afe.length === 0) return;
		let i = afe.length - 1;
		if (afe[i].marker || open.includes(afeEl(afe[i]))) return;
		while (i > 0) {
			i--;
			if (afe[i].marker || open.includes(afeEl(afe[i]))) {
				i++;
				break;
			}
		}
		for (; i < afe.length; i++) {
			const e = afe[i];
			const el = mkEl(
				afeEl(e).tagName,
				afeEl(e).namespace,
				cloneAttrs(afeEl(e).attributes),
				null
			);
			const place = appropriatePlace();
			insertAtPlace(place, el);
			open.push(el);
			afe[i] = { element: el };
		}
	};

	// ---- close p ----
	const closePElement = () => {
		generateImpliedEndTags("p");
		// pop until a p has been popped
		while (open.length) {
			const el = /** @type {HtmlElement} */ (open.pop());
			el.end = tokenEnd;
			if (el.namespace === NS_HTML && el.tagName === "p") break;
		}
	};

	const popUntil = (/** @type {string} */ tagName) => {
		while (open.length) {
			const el = /** @type {HtmlElement} */ (open.pop());
			el.end = tokenEnd;
			if (el.namespace === NS_HTML && el.tagName === tagName) break;
		}
	};
	const popUntilOneOf = (/** @type {Set<string>} */ set) => {
		while (open.length) {
			const el = /** @type {HtmlElement} */ (open.pop());
			el.end = tokenEnd;
			if (el.namespace === NS_HTML && set.has(el.tagName)) break;
		}
	};

	// ---- reset insertion mode appropriately ----
	const resetInsertionMode = () => {
		let last = false;
		for (let i = open.length - 1; i >= 0; i--) {
			let node = open[i];
			if (i === 0) {
				last = true;
				if (fragment) node = fragment;
			}
			const tn = node.tagName;
			if (node.namespace === NS_HTML) {
				if ((tn === "td" || tn === "th") && !last) {
					mode = MODE_IN_CELL;
					return;
				}
				if (tn === "tr") {
					mode = MODE_IN_ROW;
					return;
				}
				if (TBODY_GROUP.has(tn)) {
					mode = MODE_IN_TABLE_BODY;
					return;
				}
				if (tn === "caption") {
					mode = MODE_IN_CAPTION;
					return;
				}
				if (tn === "colgroup") {
					mode = MODE_IN_COLUMN_GROUP;
					return;
				}
				if (tn === "table") {
					mode = MODE_IN_TABLE;
					return;
				}
				if (tn === "template") {
					mode = templateModes[templateModes.length - 1];
					return;
				}
				if (tn === "head" && !last) {
					mode = MODE_IN_HEAD;
					return;
				}
				if (tn === "body") {
					mode = MODE_IN_BODY;
					return;
				}
				if (tn === "frameset") {
					mode = MODE_IN_FRAMESET;
					return;
				}
				if (tn === "html") {
					mode = head ? MODE_AFTER_HEAD : MODE_BEFORE_HEAD;
					return;
				}
			}
			if (last) {
				mode = MODE_IN_BODY;
				return;
			}
		}
	};

	// ---------- token processing ----------
	// Split a character token's leading whitespace; per the spec each character
	// is its own token, so a mixed run can straddle a mode change. Inserts the
	// leading whitespace when `insert`, returns the non-whitespace remainder
	// token (or null when the token was entirely whitespace).
	const leadingWs = (
		/** @type {CharToken} */ t,
		/** @type {boolean} */ insert
	) => {
		const m = /^[\t\n\f\r ]+/.exec(t.data);
		const ws = m ? m[0] : "";
		if (ws && insert) insertCharacters(ws, t.start, t.start + ws.length);
		if (ws.length === t.data.length) return null;
		return { ...t, data: t.data.slice(ws.length), start: t.start + ws.length };
	};

	const isAllWs = (/** @type {string} */ s) => {
		for (let i = 0; i < s.length; i++) {
			const c = s.charCodeAt(i);
			// HTML whitespace: tab / LF / FF / CR / space. A charCodeAt loop avoids
			// the `for…of` code-point iterator + per-char string + Set lookup.
			if (c !== 0x09 && c !== 0x0a && c !== 0x0c && c !== 0x0d && c !== 0x20) {
				return false;
			}
		}
		return true;
	};

	const process = (/** @type {Token} */ t) => {
		// Track the current token's end so explicit closes can set element `.end`.
		// Dispatch on `type` instead of the `in` operator, which goes megamorphic
		// across the token union and shows up on the per-token hot path.
		const ty = t.type;
		if (ty === TOKEN_START_TAG || ty === TOKEN_END_TAG) tokenEnd = t.pos.end;
		else if (ty !== TOKEN_EOF) tokenEnd = t.end;
		// foreign content dispatch
		const ac = adjustedCurrent();
		const useForeign =
			open.length > 0 &&
			ac &&
			ac.namespace !== NS_HTML &&
			ty !== TOKEN_EOF &&
			shouldUseForeignRules(ac, t);
		if (useForeign) {
			foreignContent(t);
			return;
		}
		runMode(t);
	};

	const shouldUseForeignRules = (
		/** @type {HtmlElement} */ ac,
		/** @type {Token} */ t
	) => {
		if (ac.namespace === NS_HTML) return false;
		if (t.type === TOKEN_START_TAG) {
			if (
				mathmlTextIntegrationPoint(ac) &&
				t.name !== "mglyph" &&
				t.name !== "malignmark"
			) {
				return false;
			}
			if (
				ac.namespace === NS_MATHML &&
				ac.tagName === "annotation-xml" &&
				t.name === "svg"
			) {
				return false;
			}
			if (htmlIntegrationPoint(ac)) return false;
			return true;
		}
		if (t.type === TOKEN_CHAR) {
			if (mathmlTextIntegrationPoint(ac)) return false;
			if (htmlIntegrationPoint(ac)) return false;
			return true;
		}
		if (t.type === TOKEN_END_TAG) return true;
		if (t.type === TOKEN_COMMENT) return true;
		return false;
	};

	const mathmlTextIntegrationPoint = (/** @type {HtmlElement} */ el) =>
		el.namespace === NS_MATHML && MATHML_TEXT_INTEGRATION.has(el.tagName);
	const htmlIntegrationPoint = (/** @type {HtmlElement} */ el) => {
		if (el.namespace === NS_MATHML && el.tagName === "annotation-xml") {
			const enc = findAttr(el.attributes, "encoding");
			if (
				enc &&
				(enc.value.toLowerCase() === "text/html" ||
					enc.value.toLowerCase() === "application/xhtml+xml")
			) {
				return true;
			}
			return false;
		}
		if (el.namespace === NS_SVG && SVG_SPECIAL.has(el.tagName.toLowerCase())) {
			return true;
		}
		return false;
	};

	const adjustSvgTag = (/** @type {string} */ name) =>
		/** @type {Record<string, string>} */ (SVG_TAG_ADJUST)[name] || name;
	const adjustForeignAttrs = (
		/** @type {HtmlAttribute[]} */ attrs,
		/** @type {number} */ ns
	) => {
		// Reuse the original array/objects until an attribute actually needs
		// adjusting; unchanged attributes keep serializedName === undefined and
		// the serializer falls back to `name`. Avoids a new array + object per
		// attribute on every foreign element.
		let out = attrs;
		for (let i = 0; i < attrs.length; i++) {
			const a = attrs[i];
			let name = a.name;
			/** @type {string | undefined} */
			let serializedName;
			if (
				ns === NS_SVG &&
				/** @type {Record<string, string>} */ (SVG_ATTR_ADJUST)[name]
			) {
				name = /** @type {Record<string, string>} */ (SVG_ATTR_ADJUST)[name];
				serializedName = name;
			}
			if (/** @type {Record<string, string>} */ (FOREIGN_ATTR_NS)[a.name]) {
				serializedName = /** @type {Record<string, string>} */ (
					FOREIGN_ATTR_NS
				)[a.name];
			}
			if (name === a.name && serializedName === undefined) continue;
			if (out === attrs) out = [...attrs];
			out[i] = { ...a, name, serializedName };
		}
		return out;
	};

	const foreignContent = (/** @type {Token} */ t) => {
		if (t.type === TOKEN_CHAR) {
			const data = t.data.replace(/\0/g, "�");
			insertCharacters(data, t.start, t.end);
			// eslint-disable-next-line no-control-regex
			if (/[^\t\n\f\r \u0000]/.test(t.data)) framesetOk = false;
			return;
		}
		if (t.type === TOKEN_COMMENT) {
			insertComment(t.data, t.start, t.end);
			return;
		}
		if (t.type === TOKEN_DOCTYPE) return;
		if (t.type === TOKEN_START_TAG) {
			const acn = adjustedCurrent().namespace;
			if (
				FOREIGN_BREAKOUT.has(t.name) ||
				(t.name === "font" &&
					t.attrs.some((a) => FONT_BREAKOUT_ATTRS.has(a.name)))
			) {
				// parse error; pop until integration point / html / mathml-text-integration
				while (open.length > 1) {
					const c = cur();
					if (
						c.namespace === NS_HTML ||
						mathmlTextIntegrationPoint(c) ||
						htmlIntegrationPoint(c)
					) {
						break;
					}
					open.pop();
				}
				runMode(t);
				return;
			}
			const ns = acn;
			let name = t.name;
			let attrs = t.attrs;
			if (ns === NS_SVG) {
				name = adjustSvgTag(name);
			}
			if (ns === NS_MATHML) {
				attrs = adjustMathmlAttrs(attrs);
			}
			attrs = adjustForeignAttrs(attrs, ns);
			insertForeignElement(name, ns, attrs, t.pos);
			if (t.selfClosing) {
				open.pop();
			}
			return;
		}
		if (t.type === TOKEN_END_TAG) {
			if (
				t.name === "script" &&
				cur().tagName === "script" &&
				cur().namespace === NS_SVG
			) {
				open.pop();
				return;
			}
			// `</p>` and `</br>` break out: pop foreign elements up to the
			// nearest HTML element or integration point, then process in HTML.
			if (t.name === "p" || t.name === "br") {
				while (
					open.length > 1 &&
					cur().namespace !== NS_HTML &&
					!mathmlTextIntegrationPoint(cur()) &&
					!htmlIntegrationPoint(cur())
				) {
					open.pop();
				}
				runMode(t);
				return;
			}
			// any other end tag
			let i = open.length - 1;
			let node = open[i];
			if (node.tagName.toLowerCase() !== t.name) {
				/* parse error */
			}
			while (i >= 0) {
				node = open[i];
				if (i === 0) return;
				if (
					node.namespace !== NS_HTML &&
					node.tagName.toLowerCase() === t.name
				) {
					while (open.length > i) open.pop();
					return;
				}
				i--;
				if (open[i] && open[i].namespace === NS_HTML) {
					runMode(t);
					return;
				}
			}
		}
	};

	// ---------- adoption agency algorithm ----------
	const adoptionAgency = (
		/** @type {string} */ subject,
		/** @type {TagPos | null} */ pos
	) => {
		// step 1
		const c = cur();
		if (
			c.namespace === NS_HTML &&
			c.tagName === subject &&
			!afe.some((e) => !e.marker && e.element === c)
		) {
			open.pop();
			return true;
		}
		let outer = 0;
		while (outer < 8) {
			outer++;
			// find formatting element
			let fmtIdx = -1;
			for (let i = afe.length - 1; i >= 0; i--) {
				if (afe[i].marker) break;
				if (
					afeEl(afe[i]).tagName === subject &&
					afeEl(afe[i]).namespace === NS_HTML
				) {
					fmtIdx = i;
					break;
				}
			}
			if (fmtIdx === -1) return false; // act as any other end tag
			const fmt = afeEl(afe[fmtIdx]);
			const openIdx = open.indexOf(fmt);
			if (openIdx === -1) {
				afe.splice(fmtIdx, 1);
				return true;
			}
			if (!inScopeEl(fmt)) return true; // parse error, ignore
			// step: furthest block
			let furthestIdx = -1;
			for (let i = openIdx + 1; i < open.length; i++) {
				if (isSpecial(open[i])) {
					furthestIdx = i;
					break;
				}
			}
			if (furthestIdx === -1) {
				while (open.length > openIdx) open.pop();
				afe.splice(fmtIdx, 1);
				return true;
			}
			const furthest = open[furthestIdx];
			const commonAncestor = open[openIdx - 1];
			let bookmark = fmtIdx;
			let node = furthest;
			let lastNode = furthest;
			let nodeIdx = furthestIdx;
			let inner = 0;
			while (true) {
				inner++;
				nodeIdx--;
				node = open[nodeIdx];
				if (node === fmt) break;
				const nodeRef = node;
				let nodeAfeIdx = afe.findIndex(
					(e) => !e.marker && e.element === nodeRef
				);
				if (inner > 3 && nodeAfeIdx !== -1) {
					afe.splice(nodeAfeIdx, 1);
					if (nodeAfeIdx < bookmark) bookmark--;
					nodeAfeIdx = -1;
				}
				if (nodeAfeIdx === -1) {
					open.splice(nodeIdx, 1);
					continue;
				}
				// create clone
				const clone = mkEl(
					node.tagName,
					node.namespace,
					cloneAttrs(node.attributes),
					null
				);
				afe[nodeAfeIdx] = { element: clone };
				open[nodeIdx] = clone;
				node = clone;
				if (lastNode === furthest) bookmark = nodeAfeIdx + 1;
				// append lastNode to node
				detach(lastNode);
				appendTo(node, lastNode);
				lastNode = node;
			}
			// insert lastNode into common ancestor (with foster parenting)
			detach(lastNode);
			const place = placeForCommonAncestor(commonAncestor);
			insertAtPlace(place, lastNode);
			// create element for fmt token, take children of furthest
			const cloneFmt = mkEl(
				fmt.tagName,
				fmt.namespace,
				cloneAttrs(fmt.attributes),
				null
			);
			const kids = [...furthest.children];
			furthest.children.length = 0;
			for (const k of kids) appendTo(cloneFmt, k);
			appendTo(furthest, cloneFmt);
			// remove fmt from afe, insert clone at bookmark
			const curFmtIdx = afe.findIndex((e) => !e.marker && e.element === fmt);
			if (curFmtIdx !== -1) {
				afe.splice(curFmtIdx, 1);
				if (curFmtIdx < bookmark) bookmark--;
			}
			afe.splice(bookmark, 0, { element: cloneFmt });
			// remove fmt from open, insert clone below furthest
			const ofi = open.indexOf(fmt);
			if (ofi !== -1) open.splice(ofi, 1);
			const newFurthestIdx = open.indexOf(furthest);
			open.splice(newFurthestIdx + 1, 0, cloneFmt);
		}
		return true;
	};

	const placeForCommonAncestor = (
		/** @type {HtmlElement} */ commonAncestor
	) => {
		if (
			TABLE_CONTEXT.has(commonAncestor.tagName) &&
			commonAncestor.namespace === NS_HTML
		) {
			// foster
			// reuse appropriatePlace logic but rooted differently: emulate
			let lastTemplate = -1;
			let lastTable = -1;
			for (let i = open.length - 1; i >= 0; i--) {
				if (
					open[i].tagName === "template" &&
					open[i].namespace === NS_HTML &&
					lastTemplate === -1
				) {
					lastTemplate = i;
				}
				if (
					open[i].tagName === "table" &&
					open[i].namespace === NS_HTML &&
					lastTable === -1
				) {
					lastTable = i;
				}
			}
			if (
				lastTemplate !== -1 &&
				(lastTable === -1 || lastTemplate > lastTable)
			) {
				return { parent: open[lastTemplate], beforeNode: null };
			}
			if (lastTable === -1) return { parent: open[0], beforeNode: null };
			const table = open[lastTable];
			const tp = findParent(table);
			if (tp) return { parent: tp, beforeNode: table };
			return { parent: open[lastTable - 1], beforeNode: null };
		}
		return { parent: commonAncestor, beforeNode: null };
	};

	const detach = (/** @type {HtmlNode} */ node) => {
		const p = findParent(node);
		if (!p) return;
		const arr = childrenOf(p);
		const idx = arr.indexOf(node);
		if (idx !== -1) arr.splice(idx, 1);
	};

	// ---------- insertion modes ----------

	/** @type {Record<string, (t: Token) => void>} */
	const modes = {};

	// Dispatch the current insertion mode. An integer switch (cases ordered by
	// frequency) keeps each `modes.x(t)` call site monomorphic, where a keyed
	// `modes[mode]` load + indirect call would defeat inlining on the per-token
	// hot path.
	const runMode = (/** @type {Token} */ t) => {
		switch (mode) {
			case MODE_IN_BODY:
				return modes.inBody(t);
			case MODE_TEXT:
				return modes.text(t);
			case MODE_IN_CELL:
				return modes.inCell(t);
			case MODE_IN_ROW:
				return modes.inRow(t);
			case MODE_IN_TABLE_BODY:
				return modes.inTableBody(t);
			case MODE_IN_TABLE:
				return modes.inTable(t);
			case MODE_IN_TABLE_TEXT:
				return modes.inTableText(t);
			case MODE_IN_CAPTION:
				return modes.inCaption(t);
			case MODE_IN_COLUMN_GROUP:
				return modes.inColumnGroup(t);
			case MODE_IN_TEMPLATE:
				return modes.inTemplate(t);
			case MODE_IN_HEAD:
				return modes.inHead(t);
			case MODE_IN_HEAD_NOSCRIPT:
				return modes.inHeadNoscript(t);
			case MODE_AFTER_HEAD:
				return modes.afterHead(t);
			case MODE_BEFORE_HEAD:
				return modes.beforeHead(t);
			case MODE_BEFORE_HTML:
				return modes.beforeHtml(t);
			case MODE_INITIAL:
				return modes.initial(t);
			case MODE_AFTER_BODY:
				return modes.afterBody(t);
			case MODE_AFTER_AFTER_BODY:
				return modes.afterAfterBody(t);
			case MODE_IN_FRAMESET:
				return modes.inFrameset(t);
			case MODE_AFTER_FRAMESET:
				return modes.afterFrameset(t);
			// MODE_AFTER_AFTER_FRAMESET — every mode is enumerated, so the last
			// one is the `default` (also satisfies exhaustiveness linting).
			default:
				return modes.afterAfterFrameset(t);
		}
	};

	const QUIRKY_PREFIXES = [
		"+//silmaril//dtd html pro v0r11 19970101//",
		"-//as//dtd html 3.0 aswedit + extensions//",
		"-//advasoft ltd//dtd html 3.0 aswedit + extensions//",
		"-//ietf//dtd html 2.0 level 1//",
		"-//ietf//dtd html 2.0 level 2//",
		"-//ietf//dtd html 2.0 strict level 1//",
		"-//ietf//dtd html 2.0 strict level 2//",
		"-//ietf//dtd html 2.0 strict//",
		"-//ietf//dtd html 2.0//",
		"-//ietf//dtd html 2.1e//",
		"-//ietf//dtd html 3.0//",
		"-//ietf//dtd html 3.2 final//",
		"-//ietf//dtd html 3.2//",
		"-//ietf//dtd html 3//",
		"-//ietf//dtd html level 0//",
		"-//ietf//dtd html level 1//",
		"-//ietf//dtd html level 2//",
		"-//ietf//dtd html level 3//",
		"-//ietf//dtd html strict level 0//",
		"-//ietf//dtd html strict level 1//",
		"-//ietf//dtd html strict level 2//",
		"-//ietf//dtd html strict level 3//",
		"-//ietf//dtd html strict//",
		"-//ietf//dtd html//",
		"-//metrius//dtd metrius presentational//",
		"-//microsoft//dtd internet explorer 2.0 html strict//",
		"-//microsoft//dtd internet explorer 2.0 html//",
		"-//microsoft//dtd internet explorer 2.0 tables//",
		"-//microsoft//dtd internet explorer 3.0 html strict//",
		"-//microsoft//dtd internet explorer 3.0 html//",
		"-//microsoft//dtd internet explorer 3.0 tables//",
		"-//netscape comm. corp.//dtd html//",
		"-//netscape comm. corp.//dtd strict html//",
		"-//o'reilly and associates//dtd html 2.0//",
		"-//o'reilly and associates//dtd html extended 1.0//",
		"-//o'reilly and associates//dtd html extended relaxed 1.0//",
		"-//sq//dtd html 2.0 hotmetal + extensions//",
		"-//softquad software//dtd hotmetal pro 6.0::19990601::extensions to html 4.0//",
		"-//softquad//dtd hotmetal pro 4.0::19971010::extensions to html 4.0//",
		"-//spyglass//dtd html 2.0 extended//",
		"-//sun microsystems corp.//dtd hotjava html//",
		"-//sun microsystems corp.//dtd hotjava strict html//",
		"-//w3c//dtd html 3 1995-03-24//",
		"-//w3c//dtd html 3.2 draft//",
		"-//w3c//dtd html 3.2 final//",
		"-//w3c//dtd html 3.2//",
		"-//w3c//dtd html 3.2s draft//",
		"-//w3c//dtd html 4.0 frameset//",
		"-//w3c//dtd html 4.0 transitional//",
		"-//w3c//dtd html experimental 19960712//",
		"-//w3c//dtd html experimental 970421//",
		"-//w3c//dtd w3 html//",
		"-//w3o//dtd w3 html 3.0//",
		"-//webtechs//dtd mozilla html 2.0//",
		"-//webtechs//dtd mozilla html//"
	];
	const QUIRKY_EXACT = new Set([
		"-//w3o//dtd w3 html strict 3.0//en//",
		"-/w3c/dtd html 4.0 transitional/en",
		"html"
	]);
	const isQuirky = (
		/** @type {string} */ name,
		/** @type {string | null} */ pub,
		/** @type {string | null} */ sys
	) => {
		if (name !== "html") return true;
		const p = pub ? pub.toLowerCase() : null;
		const sl = sys ? sys.toLowerCase() : null;
		if (p !== null) {
			if (QUIRKY_EXACT.has(p)) return true;
			for (const pre of QUIRKY_PREFIXES) if (p.startsWith(pre)) return true;
			if (
				sl === null &&
				(p.startsWith("-//w3c//dtd html 4.01 frameset//") ||
					p.startsWith("-//w3c//dtd html 4.01 transitional//"))
			) {
				return true;
			}
		}
		if (sl === "http://www.ibm.com/data/dtd/v11/ibmxhtml1-transitional.dtd") {
			return true;
		}
		return false;
	};

	modes.initial = (t) => {
		if (t.type === TOKEN_CHAR) {
			const r = leadingWs(t, false);
			if (!r) return;
			quirks = true;
			mode = MODE_BEFORE_HTML;
			process(r);
			return;
		}
		if (t.type === TOKEN_COMMENT) {
			insertComment(t.data, t.start, t.end, { parent: doc, beforeNode: null });
			return;
		}
		if (t.type === TOKEN_DOCTYPE) {
			doc.children.push({
				type: NodeType.Doctype,
				name: t.name,
				publicId: t.publicId,
				systemId: t.systemId,
				start: t.start,
				end: t.end
			});
			quirks = isQuirky(t.name, t.publicId, t.systemId);
			mode = MODE_BEFORE_HTML;
			return;
		}
		quirks = true;
		mode = MODE_BEFORE_HTML;
		process(t);
	};

	modes.beforeHtml = (t) => {
		if (t.type === TOKEN_DOCTYPE) return;
		if (t.type === TOKEN_COMMENT) {
			insertComment(t.data, t.start, t.end, { parent: doc, beforeNode: null });
			return;
		}
		if (t.type === TOKEN_CHAR) {
			const r = leadingWs(t, false);
			if (!r) return;
			t = r;
		}
		if (t.type === TOKEN_START_TAG && t.name === "html") {
			const el = mkEl("html", NS_HTML, t.attrs, t.pos);
			doc.children.push(el);
			open.push(el);
			mode = MODE_BEFORE_HEAD;
			return;
		}
		if (t.type === TOKEN_END_TAG && !HEAD_BODY_HTML_BR.has(t.name)) {
			return;
		}
		const el = mkEl("html", NS_HTML, [], null);
		doc.children.push(el);
		open.push(el);
		mode = MODE_BEFORE_HEAD;
		process(t);
	};

	modes.beforeHead = (t) => {
		if (t.type === TOKEN_CHAR) {
			const r = leadingWs(t, false);
			if (!r) return;
			t = r;
		}
		if (t.type === TOKEN_COMMENT) {
			insertComment(t.data, t.start, t.end);
			return;
		}
		if (t.type === TOKEN_DOCTYPE) return;
		if (t.type === TOKEN_START_TAG && t.name === "html") return modes.inBody(t);
		if (t.type === TOKEN_START_TAG && t.name === "head") {
			head = insertHtmlElement("head", t.attrs, t.pos);
			mode = MODE_IN_HEAD;
			return;
		}
		if (t.type === TOKEN_END_TAG && !HEAD_BODY_HTML_BR.has(t.name)) {
			return;
		}
		head = insertHtmlElement("head", EMPTY_ATTRS, null);
		mode = MODE_IN_HEAD;
		process(t);
	};

	modes.inHead = (t) => {
		if (t.type === TOKEN_CHAR) {
			const r = leadingWs(t, true);
			if (!r) return;
			open.pop();
			mode = MODE_AFTER_HEAD;
			process(r);
			return;
		}
		if (t.type === TOKEN_COMMENT) {
			insertComment(t.data, t.start, t.end);
			return;
		}
		if (t.type === TOKEN_DOCTYPE) return;
		if (t.type === TOKEN_START_TAG) {
			if (t.name === "html") return modes.inBody(t);
			if (HEAD_VOID_ELEMENTS.has(t.name)) {
				insertHtmlElement(t.name, t.attrs, t.pos);
				open.pop();
				return;
			}
			if (t.name === "title") {
				genericRcdata(t);
				return;
			}
			if (NOFRAMES_STYLE_NOSCRIPT.has(t.name)) {
				if (t.name === "noscript") {
					insertHtmlElement("noscript", t.attrs, t.pos);
					mode = MODE_IN_HEAD_NOSCRIPT;
					return;
				}
				genericRawtext(t);
				return;
			}
			if (t.name === "script") {
				genericRawtext(t);
				return;
			}
			if (t.name === "template") {
				insertHtmlElement("template", t.attrs, t.pos);
				insertMarker();
				framesetOk = false;
				mode = MODE_IN_TEMPLATE;
				templateModes.push(MODE_IN_TEMPLATE);
				const el = cur();
				el.templateContent = { type: NodeType.DocumentFragment, children: [] };
				return;
			}
			if (t.name === "head") return;
		}
		if (t.type === TOKEN_END_TAG) {
			if (t.name === "head") {
				open.pop();
				mode = MODE_AFTER_HEAD;
				return;
			}
			if (BODY_HTML_BR.has(t.name)) {
				/* fallthrough */
			} else if (t.name === "template") {
				if (!open.some(isHtmlTemplateEl)) {
					return;
				}
				generateImpliedEndTagsThorough();
				popUntil("template");
				clearAfeToMarker();
				templateModes.pop();
				resetInsertionMode();
				return;
			} else {
				return;
			}
		}
		// anything else
		open.pop();
		mode = MODE_AFTER_HEAD;
		process(t);
	};

	modes.inHeadNoscript = (t) => {
		if (t.type === TOKEN_DOCTYPE) return;
		if (t.type === TOKEN_START_TAG && t.name === "html") return modes.inBody(t);
		if (t.type === TOKEN_END_TAG && t.name === "noscript") {
			open.pop();
			mode = MODE_IN_HEAD;
			return;
		}
		if (t.type === TOKEN_CHAR && isAllWs(t.data)) return modes.inHead(t);
		if (t.type === TOKEN_COMMENT) return modes.inHead(t);
		if (
			t.type === TOKEN_START_TAG &&
			IN_HEAD_NOSCRIPT_PASSTHROUGH.has(t.name)
		) {
			return modes.inHead(t);
		}
		// A stray end tag other than </br>/</noscript> is ignored (the comment
		// or content stays inside <noscript>); only </br> and other content fall
		// back to popping <noscript>.
		if (t.type === TOKEN_END_TAG && t.name !== "br") return;
		if (
			t.type === TOKEN_START_TAG &&
			(t.name === "head" || t.name === "noscript")
		) {
			return;
		}
		open.pop();
		mode = MODE_IN_HEAD;
		process(t);
	};

	modes.afterHead = (t) => {
		if (t.type === TOKEN_CHAR) {
			const r = leadingWs(t, true);
			if (!r) return;
			insertHtmlElement("body", [], null);
			mode = MODE_IN_BODY;
			process(r);
			return;
		}
		if (t.type === TOKEN_COMMENT) {
			insertComment(t.data, t.start, t.end);
			return;
		}
		if (t.type === TOKEN_DOCTYPE) return;
		if (t.type === TOKEN_START_TAG) {
			if (t.name === "html") return modes.inBody(t);
			if (t.name === "body") {
				insertHtmlElement("body", t.attrs, t.pos);
				framesetOk = false;
				mode = MODE_IN_BODY;
				return;
			}
			if (t.name === "frameset") {
				insertHtmlElement("frameset", t.attrs, t.pos);
				mode = MODE_IN_FRAMESET;
				return;
			}
			if (HEAD_ELEMENTS.has(t.name)) {
				const headEl = /** @type {HtmlElement} */ (head);
				open.push(headEl);
				modes.inHead(t);
				const idx = open.indexOf(headEl);
				if (idx !== -1) open.splice(idx, 1);
				return;
			}
			if (t.name === "head") return;
		}
		if (t.type === TOKEN_END_TAG) {
			if (t.name === "template") return modes.inHead(t);
			if (!BODY_HTML_BR.has(t.name)) return;
		}
		insertHtmlElement("body", [], null);
		mode = MODE_IN_BODY;
		process(t);
	};

	modes.inBody = (t) => {
		if (t.type === TOKEN_CHAR) {
			if (t.data.includes("\0")) t = { ...t, data: t.data.replace(/\0/g, "") };
			if (t.data === "") return;
			reconstructAfe();
			insertCharacters(t.data, t.start, t.end);
			// `framesetOk` only ever goes true→false, so once it's false skip the
			// per-text-token whitespace scan entirely (it flips false very early in
			// real documents).
			if (framesetOk && !isAllWs(t.data)) framesetOk = false;
			return;
		}
		if (t.type === TOKEN_COMMENT) {
			insertComment(t.data, t.start, t.end);
			return;
		}
		if (t.type === TOKEN_DOCTYPE) return;
		if (t.type === TOKEN_START_TAG) return startTagInBody(t);
		if (t.type === TOKEN_END_TAG) return endTagInBody(t);
		if (t.type === TOKEN_EOF && templateModes.length) {
			return modes.inTemplate(t);
		}
	};

	const closeIfPInButtonScope = () => {
		if (inButtonScope("p")) closePElement();
	};

	// "any other end tag" in body: pop to the matching open element, stopping at
	// the first special element; also the adoption agency's no-element fallback.
	const anyOtherEndTag = (/** @type {string} */ name) => {
		for (let i = open.length - 1; i >= 0; i--) {
			const node = open[i];
			if (node.namespace === NS_HTML && node.tagName === name) {
				generateImpliedEndTags(name);
				while (open.length > i) {
					open[open.length - 1].end = tokenEnd;
					open.pop();
				}
				return;
			}
			if (isSpecial(node)) return;
		}
	};

	const startTagInBody = (/** @type {StartTagToken} */ t) => {
		const name = t.name;
		if (name === "html") {
			if (open.some(isHtmlTemplateEl)) {
				return;
			}
			const htmlEl = open[0];
			for (const a of t.attrs) {
				if (!htmlEl.attributes.some((x) => x.name === a.name)) {
					htmlEl.attributes.push(a);
				}
			}
			return;
		}
		if (HEAD_ELEMENTS.has(name)) {
			return modes.inHead(t);
		}
		if (name === "body") {
			const second = open[1];
			if (!second || second.tagName !== "body" || open.some(isHtmlTemplateEl)) {
				return;
			}
			framesetOk = false;
			for (const a of t.attrs) {
				if (!second.attributes.some((x) => x.name === a.name)) {
					second.attributes.push(a);
				}
			}
			return;
		}
		if (name === "frameset") {
			const second = open[1];
			if (!second || second.tagName !== "body") return;
			if (!framesetOk) return;
			detach(second);
			while (open.length > 1) open.pop();
			insertHtmlElement("frameset", t.attrs, t.pos);
			mode = MODE_IN_FRAMESET;
			return;
		}
		if (BLOCK_START.has(name)) {
			closeIfPInButtonScope();
			insertHtmlElement(name, t.attrs, t.pos);
			return;
		}
		if (HEADING.has(name)) {
			closeIfPInButtonScope();
			if (cur().namespace === NS_HTML && HEADING.has(cur().tagName)) open.pop();
			insertHtmlElement(name, t.attrs, t.pos);
			return;
		}
		if (name === "pre" || name === "listing") {
			closeIfPInButtonScope();
			insertHtmlElement(name, t.attrs, t.pos);
			t.swallowNewline = true;
			framesetOk = false;
			return;
		}
		if (name === "form") {
			if (form && !open.some(isHtmlTemplateEl)) {
				return;
			}
			closeIfPInButtonScope();
			const el = insertHtmlElement("form", t.attrs, t.pos);
			if (!open.some(isHtmlTemplateEl)) {
				form = el;
			}
			return;
		}
		if (name === "li") {
			framesetOk = false;
			for (let i = open.length - 1; i >= 0; i--) {
				const node = open[i];
				if (node.namespace === NS_HTML && node.tagName === "li") {
					generateImpliedEndTags("li");
					popUntil("li");
					break;
				}
				if (
					isSpecial(node) &&
					!(node.namespace === NS_HTML && ADDRESS_DIV_P.has(node.tagName))
				) {
					break;
				}
			}
			closeIfPInButtonScope();
			insertHtmlElement("li", t.attrs, t.pos);
			return;
		}
		if (name === "dd" || name === "dt") {
			framesetOk = false;
			for (let i = open.length - 1; i >= 0; i--) {
				const node = open[i];
				if (
					node.namespace === NS_HTML &&
					(node.tagName === "dd" || node.tagName === "dt")
				) {
					generateImpliedEndTags(node.tagName);
					popUntil(node.tagName);
					break;
				}
				if (
					isSpecial(node) &&
					!(node.namespace === NS_HTML && ADDRESS_DIV_P.has(node.tagName))
				) {
					break;
				}
			}
			closeIfPInButtonScope();
			insertHtmlElement(name, t.attrs, t.pos);
			return;
		}
		if (name === "plaintext") {
			closeIfPInButtonScope();
			insertHtmlElement("plaintext", t.attrs, t.pos);
			return;
		}
		if (name === "button") {
			if (inScope("button")) {
				generateImpliedEndTags();
				popUntil("button");
			}
			reconstructAfe();
			insertHtmlElement("button", t.attrs, t.pos);
			framesetOk = false;
			return;
		}
		if (name === "a") {
			// if there's an <a> in afe after last marker
			for (let i = afe.length - 1; i >= 0; i--) {
				if (afe[i].marker) break;
				if (afeEl(afe[i]).tagName === "a") {
					adoptionAgency("a", t.pos);
					const idx = afe.findIndex(
						(e) => !e.marker && afeEl(e).tagName === "a"
					);
					if (idx !== -1) {
						const el = afeEl(afe[idx]);
						afe.splice(idx, 1);
						const oi = open.indexOf(el);
						if (oi !== -1) open.splice(oi, 1);
					}
					break;
				}
			}
			reconstructAfe();
			const el = insertHtmlElement("a", t.attrs, t.pos);
			pushAfe(el);
			return;
		}
		if (FORMATTING.has(name) && name !== "a" && name !== "nobr") {
			reconstructAfe();
			const el = insertHtmlElement(name, t.attrs, t.pos);
			pushAfe(el);
			return;
		}
		if (name === "nobr") {
			reconstructAfe();
			if (inScope("nobr")) {
				// The adoption agency returns false when a marker shields the nobr
				// from the active formatting list; then act as "any other end tag".
				if (!adoptionAgency("nobr", t.pos)) anyOtherEndTag("nobr");
				reconstructAfe();
			}
			const el = insertHtmlElement("nobr", t.attrs, t.pos);
			pushAfe(el);
			return;
		}
		if (APPLET_MARQUEE_OBJECT.has(name)) {
			reconstructAfe();
			insertHtmlElement(name, t.attrs, t.pos);
			insertMarker();
			framesetOk = false;
			return;
		}
		if (name === "table") {
			if (!quirks) closeIfPInButtonScope();
			insertHtmlElement("table", t.attrs, t.pos);
			framesetOk = false;
			mode = MODE_IN_TABLE;
			return;
		}
		if (VOID_FORMATTING.has(name)) {
			reconstructAfe();
			insertHtmlElement(name, t.attrs, t.pos);
			open.pop();
			framesetOk = false;
			return;
		}
		if (name === "input") {
			// `<input>` inside a select is dropped; if a select is open it is
			// closed first (keygen/textarea no longer behave this way).
			if (inScope("select")) {
				popUntil("select");
				resetInsertionMode();
			} else if (
				fragment &&
				fragment.namespace === NS_HTML &&
				fragment.tagName === "select"
			) {
				return;
			}
			reconstructAfe();
			insertHtmlElement("input", t.attrs, t.pos);
			open.pop();
			const ty = findAttr(t.attrs, "type");
			if (!ty || ty.value.toLowerCase() !== "hidden") framesetOk = false;
			return;
		}
		if (PARAM_SOURCE_TRACK.has(name)) {
			insertHtmlElement(name, t.attrs, t.pos);
			open.pop();
			return;
		}
		if (name === "hr") {
			if (cur().namespace === NS_HTML && cur().tagName === "option") open.pop();
			if (cur().namespace === NS_HTML && cur().tagName === "optgroup") {
				open.pop();
			}
			closeIfPInButtonScope();
			insertHtmlElement("hr", t.attrs, t.pos);
			open.pop();
			framesetOk = false;
			return;
		}
		if (name === "image") {
			return startTagInBody({ ...t, name: "img" });
		}
		if (name === "textarea") {
			genericRcdata(t, true);
			framesetOk = false;
			return;
		}
		if (name === "xmp") {
			closeIfPInButtonScope();
			reconstructAfe();
			framesetOk = false;
			genericRawtext(t);
			return;
		}
		if (name === "iframe") {
			framesetOk = false;
			genericRawtext(t);
			return;
		}
		if (name === "noembed") {
			genericRawtext(t);
			return;
		}
		if (name === "select") {
			reconstructAfe();
			if (inScope("select")) {
				generateImpliedEndTags();
				popUntil("select");
				resetInsertionMode();
				return;
			}
			insertHtmlElement("select", t.attrs, t.pos);
			// Marker so a stray formatting end tag (e.g. `</font>`) can't adopt
			// across the select boundary now that select has no own insertion mode.
			insertMarker();
			framesetOk = false;
			return;
		}
		if (name === "optgroup" || name === "option") {
			if (cur().namespace === NS_HTML && cur().tagName === "option") open.pop();
			if (
				name === "optgroup" &&
				cur().namespace === NS_HTML &&
				cur().tagName === "optgroup"
			) {
				open.pop();
			}
			reconstructAfe();
			insertHtmlElement(name, t.attrs, t.pos);
			return;
		}
		if (name === "rb" || name === "rtc") {
			if (inScope("ruby")) generateImpliedEndTags();
			insertHtmlElement(name, t.attrs, t.pos);
			return;
		}
		if (name === "rp" || name === "rt") {
			if (inScope("ruby")) generateImpliedEndTags("rtc");
			insertHtmlElement(name, t.attrs, t.pos);
			return;
		}
		if (name === "math") {
			reconstructAfe();
			const attrs = adjustForeignAttrs(adjustMathmlAttrs(t.attrs), NS_MATHML);
			insertForeignElement("math", NS_MATHML, attrs, t.pos);
			if (t.selfClosing) open.pop();
			return;
		}
		if (name === "svg") {
			reconstructAfe();
			const attrs = adjustForeignAttrs(t.attrs, NS_SVG);
			insertForeignElement("svg", NS_SVG, attrs, t.pos);
			if (t.selfClosing) open.pop();
			return;
		}
		if (IGNORED_BODY_TABLE_STARTS.has(name)) {
			return;
		}
		// any other start tag
		reconstructAfe();
		insertHtmlElement(name, t.attrs, t.pos);
	};

	const adjustMathmlAttrs = (/** @type {HtmlAttribute[]} */ attrs) => {
		// Only `definitionurl` is rewritten; reuse the original array otherwise.
		let out = attrs;
		for (let i = 0; i < attrs.length; i++) {
			if (attrs[i].name === "definitionurl") {
				if (out === attrs) out = [...attrs];
				out[i] = {
					...attrs[i],
					name: "definitionURL",
					serializedName: "definitionURL"
				};
			}
		}
		return out;
	};

	const endTagInBody = (/** @type {EndTagToken} */ t) => {
		const name = t.name;
		if (name === "template") return modes.inHead(t);
		if (name === "select") {
			if (!inScope("select")) return;
			generateImpliedEndTags();
			popUntil("select");
			return;
		}
		if (name === "body" || name === "html") {
			if (!inScope("body")) return;
			mode = MODE_AFTER_BODY;
			if (name === "html") process(t);
			return;
		}
		if (BLOCK_END.has(name)) {
			if (!inScope(name)) return;
			generateImpliedEndTags();
			popUntil(name);
			return;
		}
		if (name === "form") {
			if (!open.some(isHtmlTemplateEl)) {
				const node = form;
				form = null;
				if (!node || !inScopeEl(node)) return;
				generateImpliedEndTags();
				const idx = open.indexOf(node);
				if (idx !== -1) open.splice(idx, 1);
			} else {
				if (!inScope("form")) return;
				generateImpliedEndTags();
				popUntil("form");
			}
			return;
		}
		if (name === "p") {
			if (!inButtonScope("p")) insertHtmlElement("p", EMPTY_ATTRS, t.pos);
			closePElement();
			return;
		}
		if (name === "li") {
			if (!inListItemScope("li")) return;
			generateImpliedEndTags("li");
			popUntil("li");
			return;
		}
		if (name === "dd" || name === "dt") {
			if (!inScope(name)) return;
			generateImpliedEndTags(name);
			popUntil(name);
			return;
		}
		if (HEADING.has(name)) {
			let anyHeadingInScope = false;
			for (const h of HEADING) {
				if (inScope(h)) {
					anyHeadingInScope = true;
					break;
				}
			}
			if (!anyHeadingInScope) return;
			generateImpliedEndTags();
			popUntilOneOf(HEADING);
			return;
		}
		if (name === "sarcasm") {
			/* take a deep breath */
		}
		if (FORMATTING.has(name)) {
			adoptionAgency(name, t.pos);
			return;
		}
		if (APPLET_MARQUEE_OBJECT.has(name)) {
			if (!inScope(name)) return;
			generateImpliedEndTags();
			popUntil(name);
			clearAfeToMarker();
			return;
		}
		if (name === "br") {
			reconstructAfe();
			insertHtmlElement("br", EMPTY_ATTRS, t.pos);
			open.pop();
			framesetOk = false;
			return;
		}
		anyOtherEndTag(name);
	};

	// generic RCDATA/RAWTEXT: tokenizer already emits the text + end tag, so we
	// just insert the element and switch to "text" mode; text mode appends chars
	// and the matching end tag pops.
	const genericRawtext = (/** @type {StartTagToken} */ t) => {
		insertHtmlElement(t.name, t.attrs, t.pos);
		originalMode = mode;
		mode = MODE_TEXT;
	};
	const genericRcdata = (/** @type {StartTagToken} */ t, swallow = false) => {
		insertHtmlElement(t.name, t.attrs, t.pos);
		if (swallow) t.swallowNewline = true;
		originalMode = mode;
		mode = MODE_TEXT;
	};

	modes.text = (t) => {
		if (t.type === TOKEN_CHAR) {
			insertCharacters(t.data, t.start, t.end);
			return;
		}
		if (t.type === TOKEN_EOF) {
			if (open.length) open.pop();
			mode = originalMode;
			process(t);
			return;
		}
		if (t.type === TOKEN_END_TAG) {
			// Treat as rawtext rather than an end tag when it can't close the
			// current element: a non-matching name (e.g. a fragment context), or
			// a name that ran straight to EOF with no delimiter (`</script` at
			// EOF — the tokenizer still emits a partial tag there).
			if (cur() && (t.name !== cur().tagName || t.pos.end === t.pos.nameEnd)) {
				insertCharacters(
					source.slice(t.pos.start, t.pos.end),
					t.pos.start,
					t.pos.end
				);
				return;
			}
			open.pop();
			mode = originalMode;
		}
	};

	// ---------- table modes ----------
	/** @type {{ list: CharToken[], hasNonWs: boolean } | null} */
	let pendingTableChars = null;

	modes.inTable = (t) => {
		if (t.type === TOKEN_CHAR) {
			const c = cur();
			if (TABLE_CONTEXT.has(c.tagName) && c.namespace === NS_HTML) {
				pendingTableChars = { list: [], hasNonWs: false };
				originalMode = mode;
				mode = MODE_IN_TABLE_TEXT;
				return process(t);
			}
		}
		if (t.type === TOKEN_COMMENT) {
			insertComment(t.data, t.start, t.end);
			return;
		}
		if (t.type === TOKEN_DOCTYPE) return;
		if (t.type === TOKEN_START_TAG) {
			const name = t.name;
			if (name === "caption") {
				clearStackToTableContext();
				insertMarker();
				insertHtmlElement("caption", t.attrs, t.pos);
				mode = MODE_IN_CAPTION;
				return;
			}
			if (name === "colgroup") {
				clearStackToTableContext();
				insertHtmlElement("colgroup", t.attrs, t.pos);
				mode = MODE_IN_COLUMN_GROUP;
				return;
			}
			if (name === "col") {
				clearStackToTableContext();
				insertHtmlElement("colgroup", EMPTY_ATTRS, t.pos);
				mode = MODE_IN_COLUMN_GROUP;
				return process(t);
			}
			if (TBODY_GROUP.has(name)) {
				clearStackToTableContext();
				insertHtmlElement(name, t.attrs, t.pos);
				mode = MODE_IN_TABLE_BODY;
				return;
			}
			if (TD_TH_TR.has(name)) {
				clearStackToTableContext();
				insertHtmlElement("tbody", EMPTY_ATTRS, t.pos);
				mode = MODE_IN_TABLE_BODY;
				return process(t);
			}
			if (name === "table") {
				if (!inTableScope("table")) return;
				popUntil("table");
				resetInsertionMode();
				return process(t);
			}
			if (STYLE_SCRIPT_TEMPLATE.has(name)) {
				return modes.inHead(t);
			}
			if (name === "input") {
				const ty = findAttr(t.attrs, "type");
				if (ty && ty.value.toLowerCase() === "hidden") {
					insertHtmlElement("input", t.attrs, t.pos);
					open.pop();
					return;
				}
			}
			if (name === "form") {
				if (form || open.some(isHtmlTemplateEl)) {
					return;
				}
				form = insertHtmlElement("form", t.attrs, t.pos);
				open.pop();
				return;
			}
		}
		if (t.type === TOKEN_END_TAG) {
			if (t.name === "table") {
				if (!inTableScope("table")) return;
				popUntil("table");
				resetInsertionMode();
				return;
			}
			if (IN_TABLE_IGNORED_ENDS.has(t.name)) {
				return;
			}
			if (t.name === "template") return modes.inHead(t);
		}
		if (t.type === TOKEN_EOF) return modes.inBody(t);
		// anything else: foster parenting
		fosterParenting = true;
		modes.inBody(t);
		fosterParenting = false;
	};

	modes.inTableText = (t) => {
		if (t.type === TOKEN_CHAR) {
			const data = t.data.includes("\0") ? t.data.replace(/\0/g, "") : t.data;
			if (data === "") return;
			// Snapshot into a fresh token: these are buffered and replayed after
			// later tokens arrive, so they must not alias the reused token.
			/** @type {CharToken} */
			const tc = { type: TOKEN_CHAR, data, start: t.start, end: t.end };
			const pending = /** @type {{ list: CharToken[], hasNonWs: boolean }} */ (
				pendingTableChars
			);
			pending.list.push(tc);
			if (!isAllWs(tc.data)) pending.hasNonWs = true;
			return;
		}
		// flush
		const chars = /** @type {{ list: CharToken[], hasNonWs: boolean }} */ (
			pendingTableChars
		);
		pendingTableChars = null;
		mode = originalMode;
		for (const ct of chars.list) {
			if (chars.hasNonWs) {
				fosterParenting = true;
				modes.inBody(ct);
				fosterParenting = false;
			} else {
				insertCharacters(ct.data, ct.start, ct.end);
			}
		}
		process(t);
	};

	const clearStackToTableContext = () => {
		while (open.length) {
			const c = cur();
			if (c.namespace === NS_HTML && CLEAR_TABLE.has(c.tagName)) {
				break;
			}
			open.pop();
		}
	};
	const clearStackToTableBodyContext = () => {
		while (open.length) {
			const c = cur();
			if (c.namespace === NS_HTML && CLEAR_TABLE_BODY.has(c.tagName)) {
				break;
			}
			open.pop();
		}
	};
	const clearStackToTableRowContext = () => {
		while (open.length) {
			const c = cur();
			if (c.namespace === NS_HTML && CLEAR_TABLE_ROW.has(c.tagName)) {
				break;
			}
			open.pop();
		}
	};

	modes.inCaption = (t) => {
		if (
			(t.type === TOKEN_END_TAG && t.name === "caption") ||
			(t.type === TOKEN_START_TAG && CAPTION_TABLE_STARTS.has(t.name)) ||
			(t.type === TOKEN_END_TAG && t.name === "table")
		) {
			if (!inTableScope("caption")) return;
			generateImpliedEndTags();
			popUntil("caption");
			clearAfeToMarker();
			mode = MODE_IN_TABLE;
			if (!(t.type === TOKEN_END_TAG && t.name === "caption")) {
				return process(t);
			}
			return;
		}
		if (t.type === TOKEN_END_TAG && CAPTION_IGNORED_ENDS.has(t.name)) {
			return;
		}
		return modes.inBody(t);
	};

	modes.inColumnGroup = (t) => {
		if (t.type === TOKEN_CHAR) {
			const r = leadingWs(t, true);
			if (!r) return;
			if (cur().tagName !== "colgroup") return;
			open.pop();
			mode = MODE_IN_TABLE;
			process(r);
			return;
		}
		if (t.type === TOKEN_COMMENT) {
			insertComment(t.data, t.start, t.end);
			return;
		}
		if (t.type === TOKEN_DOCTYPE) return;
		if (t.type === TOKEN_START_TAG && t.name === "html") return modes.inBody(t);
		if (t.type === TOKEN_START_TAG && t.name === "col") {
			insertHtmlElement("col", t.attrs, t.pos);
			open.pop();
			return;
		}
		if (t.type === TOKEN_END_TAG && t.name === "colgroup") {
			if (cur().tagName !== "colgroup") return;
			open.pop();
			mode = MODE_IN_TABLE;
			return;
		}
		if (t.type === TOKEN_END_TAG && t.name === "col") return;
		if (
			(t.type === TOKEN_START_TAG || t.type === TOKEN_END_TAG) &&
			t.name === "template"
		) {
			return modes.inHead(t);
		}
		if (t.type === TOKEN_EOF) return modes.inBody(t);
		if (cur().tagName !== "colgroup") return;
		open.pop();
		mode = MODE_IN_TABLE;
		process(t);
	};

	modes.inTableBody = (t) => {
		if (t.type === TOKEN_START_TAG && t.name === "tr") {
			clearStackToTableBodyContext();
			insertHtmlElement("tr", t.attrs, t.pos);
			mode = MODE_IN_ROW;
			return;
		}
		if (t.type === TOKEN_START_TAG && (t.name === "th" || t.name === "td")) {
			clearStackToTableBodyContext();
			insertHtmlElement("tr", EMPTY_ATTRS, t.pos);
			mode = MODE_IN_ROW;
			return process(t);
		}
		if (t.type === TOKEN_END_TAG && TBODY_GROUP.has(t.name)) {
			if (!inTableScope(t.name)) return;
			clearStackToTableBodyContext();
			open.pop();
			mode = MODE_IN_TABLE;
			return;
		}
		if (
			(t.type === TOKEN_START_TAG && TBODY_TRIGGER_STARTS.has(t.name)) ||
			(t.type === TOKEN_END_TAG && t.name === "table")
		) {
			if (!inTableScope(TBODY_GROUP)) return;
			clearStackToTableBodyContext();
			open.pop();
			mode = MODE_IN_TABLE;
			return process(t);
		}
		if (t.type === TOKEN_END_TAG && TBODY_IGNORED_ENDS.has(t.name)) {
			return;
		}
		return modes.inTable(t);
	};

	modes.inRow = (t) => {
		if (t.type === TOKEN_START_TAG && (t.name === "th" || t.name === "td")) {
			clearStackToTableRowContext();
			insertHtmlElement(t.name, t.attrs, t.pos);
			mode = MODE_IN_CELL;
			insertMarker();
			return;
		}
		if (t.type === TOKEN_END_TAG && t.name === "tr") {
			if (!inTableScope("tr")) return;
			clearStackToTableRowContext();
			open.pop();
			mode = MODE_IN_TABLE_BODY;
			return;
		}
		if (
			(t.type === TOKEN_START_TAG && ROW_TRIGGER_STARTS.has(t.name)) ||
			(t.type === TOKEN_END_TAG && t.name === "table")
		) {
			if (!inTableScope("tr")) return;
			clearStackToTableRowContext();
			open.pop();
			mode = MODE_IN_TABLE_BODY;
			return process(t);
		}
		if (t.type === TOKEN_END_TAG && TBODY_GROUP.has(t.name)) {
			if (!inTableScope(t.name)) return;
			if (!inTableScope("tr")) return;
			clearStackToTableRowContext();
			open.pop();
			mode = MODE_IN_TABLE_BODY;
			return process(t);
		}
		if (t.type === TOKEN_END_TAG && ROW_IGNORED_ENDS.has(t.name)) {
			return;
		}
		return modes.inTable(t);
	};

	modes.inCell = (t) => {
		if (t.type === TOKEN_END_TAG && (t.name === "td" || t.name === "th")) {
			if (!inTableScope(t.name)) return;
			generateImpliedEndTags();
			popUntil(t.name);
			clearAfeToMarker();
			mode = MODE_IN_ROW;
			return;
		}
		if (t.type === TOKEN_START_TAG && CAPTION_TABLE_STARTS.has(t.name)) {
			if (!inTableScope("td") && !inTableScope("th")) return;
			closeCell();
			return process(t);
		}
		if (t.type === TOKEN_END_TAG && TABLE_CONTEXT.has(t.name)) {
			if (!inTableScope(t.name)) return;
			closeCell();
			return process(t);
		}
		if (t.type === TOKEN_END_TAG && CELL_IGNORED_ENDS.has(t.name)) {
			return;
		}
		return modes.inBody(t);
	};
	const closeCell = () => {
		generateImpliedEndTags();
		popUntilOneOf(TD_TH);
		clearAfeToMarker();
		mode = MODE_IN_ROW;
	};

	modes.inTemplate = (t) => {
		if (
			t.type === TOKEN_CHAR ||
			t.type === TOKEN_COMMENT ||
			t.type === TOKEN_DOCTYPE
		) {
			return modes.inBody(t);
		}
		if (t.type === TOKEN_START_TAG) {
			if (HEAD_ELEMENTS.has(t.name)) {
				return modes.inHead(t);
			}
			const target = TEMPLATE_START_TAG_MODES.get(t.name) || MODE_IN_BODY;
			templateModes[templateModes.length - 1] = target;
			mode = target;
			return process(t);
		}
		if (t.type === TOKEN_END_TAG) {
			if (t.name === "template") return modes.inHead(t);
			return;
		}
		if (t.type === TOKEN_EOF) {
			if (!open.some(isHtmlTemplateEl)) {
				return;
			}
			popUntil("template");
			clearAfeToMarker();
			templateModes.pop();
			resetInsertionMode();
			return process(t);
		}
	};

	modes.afterBody = (t) => {
		if (t.type === TOKEN_CHAR && isAllWs(t.data)) return modes.inBody(t);
		if (t.type === TOKEN_COMMENT) {
			insertComment(t.data, t.start, t.end, {
				parent: open[0],
				beforeNode: null
			});
			return;
		}
		if (t.type === TOKEN_DOCTYPE) return;
		if (t.type === TOKEN_START_TAG && t.name === "html") return modes.inBody(t);
		if (t.type === TOKEN_END_TAG && t.name === "html") {
			if (fragment) return;
			mode = MODE_AFTER_AFTER_BODY;
			return;
		}
		if (t.type === TOKEN_EOF) return;
		mode = MODE_IN_BODY;
		process(t);
	};

	modes.inFrameset = (t) => {
		if (t.type === TOKEN_CHAR) {
			const ws = t.data.replace(/[^\t\n\f\r ]/g, "");
			if (ws) insertCharacters(ws, t.start, t.end);
			return;
		}
		if (t.type === TOKEN_COMMENT) {
			insertComment(t.data, t.start, t.end);
			return;
		}
		if (t.type === TOKEN_DOCTYPE) return;
		if (t.type === TOKEN_START_TAG && t.name === "html") return modes.inBody(t);
		if (t.type === TOKEN_START_TAG && t.name === "frameset") {
			insertHtmlElement("frameset", t.attrs, t.pos);
			return;
		}
		if (t.type === TOKEN_END_TAG && t.name === "frameset") {
			if (cur().tagName === "html") return;
			open.pop();
			if (!fragment && cur().tagName !== "frameset") mode = MODE_AFTER_FRAMESET;
			return;
		}
		if (t.type === TOKEN_START_TAG && t.name === "frame") {
			insertHtmlElement("frame", t.attrs, t.pos);
			open.pop();
			return;
		}
		if (t.type === TOKEN_START_TAG && t.name === "noframes") {
			return modes.inHead(t);
		}
	};

	modes.afterFrameset = (t) => {
		if (t.type === TOKEN_CHAR) {
			const ws = t.data.replace(/[^\t\n\f\r ]/g, "");
			if (ws) insertCharacters(ws, t.start, t.end);
			return;
		}
		if (t.type === TOKEN_COMMENT) {
			insertComment(t.data, t.start, t.end);
			return;
		}
		if (t.type === TOKEN_DOCTYPE) return;
		if (t.type === TOKEN_START_TAG && t.name === "html") return modes.inBody(t);
		if (t.type === TOKEN_END_TAG && t.name === "html") {
			mode = MODE_AFTER_AFTER_FRAMESET;
			return;
		}
		if (t.type === TOKEN_START_TAG && t.name === "noframes") {
			return modes.inHead(t);
		}
	};

	modes.afterAfterBody = (t) => {
		if (t.type === TOKEN_COMMENT) {
			insertComment(t.data, t.start, t.end, { parent: doc, beforeNode: null });
			return;
		}
		if (t.type === TOKEN_DOCTYPE) return modes.inBody(t);
		if (t.type === TOKEN_CHAR && isAllWs(t.data)) return modes.inBody(t);
		if (t.type === TOKEN_START_TAG && t.name === "html") return modes.inBody(t);
		if (t.type === TOKEN_EOF) return;
		mode = MODE_IN_BODY;
		process(t);
	};

	modes.afterAfterFrameset = (t) => {
		if (t.type === TOKEN_COMMENT) {
			insertComment(t.data, t.start, t.end, { parent: doc, beforeNode: null });
			return;
		}
		if (t.type === TOKEN_DOCTYPE) return modes.inBody(t);
		if (t.type === TOKEN_CHAR && isAllWs(t.data)) return modes.inBody(t);
		if (t.type === TOKEN_START_TAG && t.name === "html") return modes.inBody(t);
		if (t.type === TOKEN_START_TAG && t.name === "noframes") {
			return modes.inHead(t);
		}
	};

	// ---------- fragment setup ----------
	if (fragmentContext) {
		let ctxName = fragmentContext.toLowerCase();
		let ctxNs = NS_HTML;
		if (ctxName.startsWith("svg ")) {
			ctxNs = NS_SVG;
			ctxName = ctxName.slice(4);
		} else if (ctxName.startsWith("math ")) {
			ctxNs = NS_MATHML;
			ctxName = ctxName.slice(5);
		}
		fragment = mkEl(ctxName, ctxNs, [], null);
		const htmlEl = mkEl("html", NS_HTML, [], null);
		doc.children.push(htmlEl);
		open.push(htmlEl);
		if (ctxNs !== NS_HTML) {
			mode = MODE_IN_BODY;
		} else if (["title", "textarea"].includes(ctxName)) {
			originalMode = MODE_IN_BODY;
			mode = MODE_TEXT;
		} else if (
			["style", "xmp", "iframe", "noembed", "noframes", "script"].includes(
				ctxName
			)
		) {
			originalMode = MODE_IN_BODY;
			mode = MODE_TEXT;
		} else if (ctxName === "noscript" || ctxName === "plaintext") {
			mode = MODE_IN_BODY;
		} else {
			resetInsertionMode();
		}
		if (ctxName === "template") {
			templateModes.push(MODE_IN_TEMPLATE);
			mode = MODE_IN_TEMPLATE;
		}
	}

	// ---------- tokenizer callbacks ----------
	const decode = decodeHtmlEntities;

	let swallowNextNewline = false;
	// Set by the tokenizer when it hits EOF mid-tag; such a partial tag is
	// dropped (matching the spec's eof-in-tag handling).
	let eofInTag = false;
	// Set when a `<selectedcontent>` is seen, gating the post-parse mirror pass.
	let sawSelectedContent = false;

	// Single reused token (see `MutableToken`). The tokenizer callbacks below
	// fill it and call `dispatch`; processing is synchronous, so the next
	// callback only runs after the current token is fully consumed.
	/** @type {MutableToken} */
	const tok = {
		type: TOKEN_EOF,
		name: "",
		data: "",
		attrs: [],
		selfClosing: false,
		start: 0,
		end: 0,
		publicId: null,
		systemId: null,
		swallowNewline: false,
		pos: { start: 0, end: 0, tagEnd: 0, nameEnd: 0 }
	};
	const dispatch = () =>
		process(/** @type {Token} */ (/** @type {unknown} */ (tok)));

	walkHtmlTokens(source, 0, {
		isForeign: () => {
			const ac = adjustedCurrent();
			return open.length > 0 && ac && ac.namespace !== NS_HTML;
		},
		fragmentContext: fragment ? fragment.tagName : undefined,
		parseError: (input, code) => {
			if (code === "eof-in-tag") eofInTag = true;
		},
		doctype: (input, start, end) => {
			// parse doctype name + ids from the raw token (simplified)
			const { name, publicId, systemId } = parseDoctype(
				input.slice(start, end)
			);
			tok.type = TOKEN_DOCTYPE;
			tok.name = name;
			tok.publicId = publicId;
			tok.systemId = systemId;
			tok.start = start;
			tok.end = end;
			dispatch();
			return end;
		},
		comment: (input, start, end) => {
			// The tokenizer emits CDATA sections through this callback too.
			if (input.startsWith("<![CDATA[", start)) {
				const ac = adjustedCurrent();
				if (open.length > 0 && ac && ac.namespace !== NS_HTML) {
					const innerEnd = input.endsWith("]]>", end) ? end - 3 : end;
					const data = input.slice(start + 9, innerEnd).replace(/\r\n?/g, "\n");
					if (data !== "") {
						tok.type = TOKEN_CHAR;
						tok.data = data;
						tok.start = start;
						tok.end = end;
						dispatch();
					}
					return end;
				}
				tok.type = TOKEN_COMMENT;
				tok.data = input.slice(
					start + 2,
					input.charCodeAt(end - 1) === 0x3e ? end - 1 : end
				);
				tok.start = start;
				tok.end = end;
				dispatch();
				return end;
			}
			let s = start;
			let e = end;
			if (input.startsWith("<!--", start)) s = start + 4;
			else if (input.startsWith("<!", start)) s = start + 2;
			else if (input.startsWith("</", start)) s = start + 2;
			else if (input.charCodeAt(start) === 0x3c) s = start + 1;
			if (input.endsWith("-->", end)) e = end - 3;
			else if (input.endsWith("--!>", end)) e = end - 4;
			else if (input.charCodeAt(end - 1) === 0x3e) e = end - 1;
			else if (input.endsWith("--", end)) e = end - 2;
			else if (input.charCodeAt(end - 1) === 0x2d) e = end - 1;
			if (e < s) e = s;
			tok.type = TOKEN_COMMENT;
			tok.data = input.slice(s, e).replace(/\0/g, "�");
			tok.start = start;
			tok.end = end;
			dispatch();
			return end;
		},
		text: (input, start, end) => {
			const raw = input.slice(start, end);
			// CR normalization only when a CR is actually present (common case has none).
			const s = !raw.includes("\r") ? raw : raw.replace(/\r\n?/g, "\n");
			const top = adjustedCurrent();
			const rawMode =
				mode === MODE_TEXT ||
				(top && top.namespace === NS_HTML && top.tagName === "plaintext");
			const noDecode =
				top &&
				top.namespace === NS_HTML &&
				NO_DECODE_TEXT.has(top.tagName) &&
				(mode === MODE_TEXT || mode === MODE_IN_BODY);
			let data = noDecode ? s : decode(s, false);
			// In RAWTEXT/RCDATA/script/PLAINTEXT, NULL becomes U+FFFD (tokenizer
			// rule); in data state NULLs pass through to be dropped in "in body".
			if (rawMode && data.includes("\0")) data = data.replace(/\0/g, "�");
			// pre/listing/textarea swallow a leading newline (post entity decode).
			if (swallowNextNewline) {
				swallowNextNewline = false;
				if (data[0] === "\n") data = data.slice(1);
			}
			if (data === "") return end;
			tok.type = TOKEN_CHAR;
			tok.data = data;
			tok.start = start;
			tok.end = end;
			dispatch();
			return end;
		},
		attribute: (input, nameStart, nameEnd, valueStart, valueEnd, quoteType) => {
			const name = internLowerName(ATTR_NAME_INTERN, input, nameStart, nameEnd);
			// Raw (undecoded) value: consumers re-resolve requests from it and the
			// offsets must stay aligned with the source. Entity decoding for the
			// html5lib serializer happens there, not here.
			const value = valueStart !== -1 ? input.slice(valueStart, valueEnd) : "";
			// Drop duplicate attribute names (per spec). Plain loop avoids a
			// per-attribute closure allocation on this hot path.
			let dup = false;
			for (let i = 0; i < pendingAttrs.length; i++) {
				if (pendingAttrs[i].name === name) {
					dup = true;
					break;
				}
			}
			if (!dup) {
				// Field order/shape matches `cloneAttrs` / `adjustForeignAttrs` output
				// (serializedName present from creation) so all attribute objects
				// share one hidden class.
				pendingAttrs.push({
					name,
					value,
					serializedName: undefined,
					nameStart,
					nameEnd,
					valueStart,
					valueEnd
				});
			}
			if (valueStart === -1) return nameEnd;
			return quoteType !== QUOTE_NONE ? valueEnd + 1 : valueEnd;
		},
		openTag: (input, start, end, nameStart, nameEnd, selfClosing) => {
			// A start tag the tokenizer only emitted because it hit EOF mid-tag
			// is dropped, matching the spec's eof-in-tag handling.
			if (eofInTag) {
				if (pendingAttrs.length !== 0) pendingAttrs = [];
				return end;
			}
			const name = internLowerName(TAG_NAME_INTERN, input, nameStart, nameEnd);
			if (name === "selectedcontent") sawSelectedContent = true;
			// Attributeless elements share the frozen EMPTY_ATTRS (no allocation);
			// the empty pendingAttrs buffer is reused for the next tag. `<html>`
			// and `<body>` keep their own mutable array since later duplicate tags
			// merge attributes into it.
			let attrs;
			if (pendingAttrs.length === 0 && name !== "html" && name !== "body") {
				attrs = EMPTY_ATTRS;
			} else {
				attrs = pendingAttrs;
				pendingAttrs = [];
			}
			tok.type = TOKEN_START_TAG;
			tok.name = name;
			tok.attrs = attrs;
			tok.selfClosing = selfClosing;
			tok.swallowNewline = false;
			tok.pos.start = start;
			tok.pos.end = end;
			tok.pos.tagEnd = end;
			tok.pos.nameEnd = nameEnd;
			dispatch();
			if (tok.swallowNewline) swallowNextNewline = true;
			return end;
		},
		closeTag: (input, start, end, nameStart, nameEnd) => {
			const name = internLowerName(TAG_NAME_INTERN, input, nameStart, nameEnd);
			// End tags drop any parsed attributes; only reallocate if non-empty.
			if (pendingAttrs.length !== 0) pendingAttrs = [];
			tok.type = TOKEN_END_TAG;
			tok.name = name;
			tok.pos.start = start;
			tok.pos.end = end;
			tok.pos.tagEnd = end;
			tok.pos.nameEnd = nameEnd;
			dispatch();
			return end;
		}
	});
	tok.type = TOKEN_EOF;
	dispatch();

	if (sawSelectedContent) mirrorSelectedContent(doc, null);

	return doc;
};

/**
 * Deep-clone a node, dropping source offsets so cloned content does not
 * re-emit dependencies.
 * @param {HtmlNode} node node
 * @returns {HtmlNode} clone
 */
const cloneSubtree = (node) => {
	if (node.type !== NodeType.Element) return { ...node };
	return {
		type: NodeType.Element,
		tagName: node.tagName,
		namespace: node.namespace,
		attributes: node.attributes.map((a) => ({
			name: a.name,
			value: a.value,
			serializedName: a.serializedName,
			nameStart: -1,
			nameEnd: -1,
			valueStart: -1,
			valueEnd: -1
		})),
		children: node.children.map(cloneSubtree),
		selfClosing: node.selfClosing,
		start: node.start,
		end: node.end,
		tagEnd: node.tagEnd,
		nameEnd: node.nameEnd
	};
};

/**
 * The selected option of a select: the last `<option selected>`, else the
 * first option (scanning direct children and `<optgroup>` children).
 * @param {HtmlElement} select select element
 * @returns {HtmlElement | null} selected option
 */
const selectedOption = (select) => {
	/** @type {HtmlElement[]} */
	const options = [];
	/** @param {HtmlElement} el element */
	const collect = (el) => {
		for (const c of el.children) {
			if (c.type !== NodeType.Element || c.namespace !== NS_HTML) continue;
			if (c.tagName === "option") options.push(c);
			else if (c.tagName === "optgroup") collect(c);
		}
	};
	collect(select);
	if (options.length === 0) return null;
	for (let i = options.length - 1; i >= 0; i--) {
		if (options[i].attributes.some((a) => a.name === "selected")) {
			return options[i];
		}
	}
	return options[0];
};

/**
 * Fill each `<selectedcontent>` with a clone of its `<select>`'s selected
 * option subtree (the customizable-select mirroring behavior).
 * @param {HtmlElement | HtmlDocument | HtmlDocumentFragment} node node
 * @param {HtmlElement | null} select nearest ancestor select
 */
const mirrorSelectedContent = (node, select) => {
	const children =
		node.type === NodeType.Element && node.templateContent
			? node.templateContent.children
			: node.children;
	for (const child of children) {
		if (child.type !== NodeType.Element) continue;
		if (child.namespace === NS_HTML && child.tagName === "select") {
			mirrorSelectedContent(child, child);
		} else if (
			select &&
			child.namespace === NS_HTML &&
			child.tagName === "selectedcontent"
		) {
			const option = selectedOption(select);
			if (option) child.children = option.children.map(cloneSubtree);
		} else {
			mirrorSelectedContent(child, select);
		}
	}
};

const parseDoctype = (/** @type {string} */ raw) => {
	// raw like <!DOCTYPE html ...>
	let s = raw.replace(/^<!/i, "").replace(/>$/, "");
	s = s.replace(/^doctype/i, "");
	s = s.trim();
	if (s === "") return { name: "", publicId: null, systemId: null };
	const m = /^([^\s]+)/.exec(s);
	const name = m ? m[1].toLowerCase() : "";
	let publicId = null;
	let systemId = null;
	const pub = /public\s*("([^"]*)"|'([^']*)')(\s*("([^"]*)"|'([^']*)'))?/i.exec(
		s
	);
	if (pub) {
		publicId = pub[2] !== undefined ? pub[2] : pub[3] || "";
		if (pub[5] !== undefined) {
			systemId = pub[6] !== undefined ? pub[6] : pub[7] || "";
		}
	}
	const sys = /system\s*("([^"]*)"|'([^']*)')/i.exec(s);
	if (sys && publicId === null) {
		systemId = sys[2] !== undefined ? sys[2] : sys[3] || "";
	}
	return { name, publicId, systemId };
};

/**
 * AST node `type` discriminators. Numeric for the same reason as the CSS
 * `NodeType`: compact integer `===` dispatch on the tree-construction and
 * visitor-walk hot paths. Members are literal-typed so `node.type ===
 * NodeType.X` still narrows the `HtmlNode` union.
 * @type {{ Document: 1, DocumentFragment: 2, Element: 3, Text: 4, Comment: 5, Doctype: 6 }}
 */
const NodeType = {
	Document: 1,
	DocumentFragment: 2,
	Element: 3,
	Text: 4,
	Comment: 5,
	Doctype: 6
};

/** @typedef {HtmlNode | HtmlDocument | HtmlDocumentFragment} HtmlVisitableNode */

// HTML-typed views over the generic visitor machinery (`util/SourceProcessor`).
/**
 * @typedef {import("../util/SourceProcessor").VisitorContext} VisitorContext
 * @typedef {import("../util/SourceProcessor").VisitorFn<HtmlVisitableNode>} VisitorFn
 * @typedef {import("../util/SourceProcessor").VisitorBucket<HtmlVisitableNode>} VisitorBucket
 * @typedef {import("../util/SourceProcessor").VisitorMap<HtmlVisitableNode>} VisitorMap
 * @typedef {import("../util/SourceProcessor").CompiledVisitorMap<HtmlVisitableNode>} CompiledVisitorMap
 */

/**
 * @typedef {object} HtmlProcessOptions
 * @property {string=} fragmentContext context element tag name for fragment parsing (see `buildHtmlAst`)
 * @property {(HtmlDocument | HtmlDocumentFragment)=} ast walk this pre-built AST instead of parsing the input
 */

/**
 * The HTML `SourceProcessor` grammar: build the document AST (WHATWG tree
 * construction) and walk it, firing `enter` / `exit` in source order. The root
 * document / fragment node is visited too (with a `null` parent).
 * @param {string} input source text
 * @param {CompiledVisitorMap} visitors compiled visitor map
 * @param {HtmlProcessOptions} options process options
 */
const grammar = (input, visitors, options) => {
	// Babel's `path.skip()`, children-only. Reset per `enter` dispatch.
	let skipFlag = false;
	const visitorCtx = {
		skipChildren() {
			skipFlag = true;
		}
	};

	/**
	 * Fetches the node's visitor bucket once (reused for enter + exit) and uses
	 * index loops — `for…of` would allocate an iterator per node on this hot path.
	 * @param {HtmlVisitableNode} node subtree root
	 * @param {HtmlVisitableNode | null} parent enclosing node
	 */
	const walk = (node, parent) => {
		const b = visitors[node.type];
		let skip = false;
		if (b !== undefined && b.enter.length !== 0) {
			skipFlag = false;
			const e = b.enter;
			for (let i = 0; i < e.length; i++) e[i](node, parent, visitorCtx);
			skip = skipFlag;
			skipFlag = false;
		}
		if (!skip) {
			if (node.type === NodeType.Element) {
				// `<template>` children live in a separate content fragment, not in
				// `children` — walk it first so template content is not skipped.
				const tc = node.templateContent;
				if (tc !== undefined) walk(tc, node);
				const c = node.children;
				for (let i = 0; i < c.length; i++) walk(c[i], node);
			} else if (
				node.type === NodeType.Document ||
				node.type === NodeType.DocumentFragment
			) {
				const c = node.children;
				for (let i = 0; i < c.length; i++) walk(c[i], node);
			}
			// All other types are leaves.
		}
		if (b !== undefined) {
			const x = b.exit;
			for (let i = 0; i < x.length; i++) x[i](node, parent, visitorCtx);
		}
	};

	walk(options.ast || buildHtmlAst(input, options.fragmentContext), null);
};

/**
 * The generic visitor coordinator (`util/SourceProcessor`) bound to the HTML
 * `grammar`. Babel-style usage:
 *
 * ```
 * processor.use({ [NodeType.Element]: (el) => {}, [NodeType.Comment]: { enter, exit } });
 * processor.process(source);
 * ```
 * @extends {GenericSourceProcessor<HtmlVisitableNode, HtmlProcessOptions>}
 */
class SourceProcessor extends GenericSourceProcessor {
	constructor() {
		super(grammar);
	}
}

/** @typedef {[string, number, number]} ParsedSource */

// `parseSrcset` is a direct implementation of the WHATWG "parse a srcset
// attribute" algorithm; it lives here with the other spec-level HTML parsing
// so it can move into the WASM parser alongside the tokenizer in the future.
const COMMA = ",".charCodeAt(0);
const LEFT_PARENTHESIS = "(".charCodeAt(0);
const RIGHT_PARENTHESIS = ")".charCodeAt(0);
const SMALL_LETTER_W = "w".charCodeAt(0);
const SMALL_LETTER_X = "x".charCodeAt(0);
const SMALL_LETTER_H = "h".charCodeAt(0);
// (Don't use \s, to avoid matching non-breaking space)
// eslint-disable-next-line no-control-regex
const LEADING_SPACES_REGEXP = /^[ \t\n\r\u000C]+/;
// eslint-disable-next-line no-control-regex
const LEADING_COMMAS_OR_SPACES_REGEXP = /^[, \t\n\r\u000C]+/;
// eslint-disable-next-line no-control-regex
const LEADING_NOT_SPACES = /^[^ \t\n\r\u000C]+/;
const TRAILING_COMMAS_REGEXP = /[,]+$/;
const NON_NEGATIVE_INTEGER_REGEXP = /^\d+$/;
// ( Positive or negative or unsigned integers or decimals, without or without exponents.
// Must include at least one digit.
// According to spec tests any decimal point must be followed by a digit.
// No leading plus sign is allowed.)
// https://html.spec.whatwg.org/multipage/infrastructure.html#valid-floating-point-number
const FLOATING_POINT_REGEXP =
	/^-?(?:[0-9]+|[0-9]*\.[0-9]+)(?:[eE][+-]?[0-9]+)?$/;

/**
 * @param {string} input input
 * @returns {ParsedSource[]} parsed srcset
 */
const parseSrcset = (input) => {
	// 1. Let input be the value passed to this algorithm.
	const inputLength = input.length;

	/** @type {string | undefined} */
	let url;
	/** @type {string[]} */
	let descriptors;
	/** @type {string} */
	let currentDescriptor;
	/** @type {string} */
	let state;
	/** @type {number} */
	let charCode;
	/** @type {number} */
	let position = 0;
	/** @type {number} */
	let start;

	/** @type {[string, number, number][]} */
	const candidates = [];

	/**
	 * @param {RegExp} regExp reg exp to collect characters
	 * @returns {string | undefined} characters
	 */
	function collectCharacters(regExp) {
		/** @type {string} */
		let chars;
		const match = regExp.exec(input.slice(Math.max(0, position)));

		if (match) {
			[chars] = match;
			position += chars.length;

			return chars;
		}
	}

	/**
	 * @returns {void}
	 */
	function parseDescriptors() {
		// 9. Descriptor parser: Let error be no.
		let pError = false;

		// 10. Let width be absent.
		// 11. Let density be absent.
		// 12. Let future-compat-h be absent. (We're implementing it now as h)
		/** @type {number | undefined} */
		let width;
		/** @type {number | undefined} */
		let density;
		/** @type {number | undefined} */
		let height;
		/** @type {string | undefined} */
		let desc;

		// 13. For each descriptor in descriptors, run the appropriate set of steps
		// from the following list:
		for (let i = 0; i < descriptors.length; i++) {
			desc = descriptors[i];

			const lastChar = desc[desc.length - 1].charCodeAt(0);
			const value = desc.slice(0, Math.max(0, desc.length - 1));

			// If the descriptor consists of a valid non-negative integer followed by
			// a U+0077 LATIN SMALL LETTER W character
			if (
				NON_NEGATIVE_INTEGER_REGEXP.test(value) &&
				lastChar === SMALL_LETTER_W
			) {
				// If width and density are not both absent, then let error be yes.
				if (width || density) {
					pError = true;
				}

				const intVal = Number.parseInt(value, 10);

				// Apply the rules for parsing non-negative integers to the descriptor.
				// If the result is zero, let error be yes.
				// Otherwise, let width be the result.
				if (intVal === 0) {
					pError = true;
				} else {
					width = intVal;
				}
			}
			// If the descriptor consists of a valid floating-point number followed by
			// a U+0078 LATIN SMALL LETTER X character
			else if (
				FLOATING_POINT_REGEXP.test(value) &&
				lastChar === SMALL_LETTER_X
			) {
				// If width, density and future-compat-h are not all absent, then let error
				// be yes.
				if (width || density || height) {
					pError = true;
				}

				const floatVal = Number.parseFloat(value);

				// Apply the rules for parsing floating-point number values to the descriptor.
				// If the result is less than zero, let error be yes. Otherwise, let density
				// be the result.
				if (floatVal < 0) {
					pError = true;
				} else {
					density = floatVal;
				}
			}
			// If the descriptor consists of a valid non-negative integer followed by
			// a U+0068 LATIN SMALL LETTER H character
			else if (
				NON_NEGATIVE_INTEGER_REGEXP.test(value) &&
				lastChar === SMALL_LETTER_H
			) {
				// If height and density are not both absent, then let error be yes.
				if (height || density) {
					pError = true;
				}

				const intVal = Number.parseInt(value, 10);

				// Apply the rules for parsing non-negative integers to the descriptor.
				// If the result is zero, let error be yes. Otherwise, let future-compat-h
				// be the result.
				if (intVal === 0) {
					pError = true;
				} else {
					height = intVal;
				}

				// Anything else, Let error be yes.
			} else {
				pError = true;
			}
		}

		// 15. If error is still no, then append a new image source to candidates whose
		// URL is url, associated with a width width if not absent and a pixel
		// density density if not absent. Otherwise, there is a parse error.
		if (!pError) {
			candidates.push([
				/** @type {string} */ (url),
				start,
				start + /** @type {string} */ (url).length
			]);
		} else {
			throw new Error(
				`Invalid srcset descriptor found in '${input}' at '${desc}'`
			);
		}
	}

	/**
	 * @returns {void}
	 */
	function tokenize() {
		// 8.1. Descriptor tokenizer: Skip whitespace
		collectCharacters(LEADING_SPACES_REGEXP);

		// 8.2. Let current descriptor be the empty string.
		currentDescriptor = "";

		// 8.3. Let state be in descriptor.
		state = "in descriptor";

		while (true) {
			// 8.4. Let charCode be the character at position.
			charCode = input.charCodeAt(position);

			//  Do the following depending on the value of state.
			//  For the purpose of this step, "EOF" is a special character representing
			//  that position is past the end of input.

			// In descriptor
			if (state === "in descriptor") {
				// Do the following, depending on the value of charCode:

				// Space character
				// If current descriptor is not empty, append current descriptor to
				// descriptors and let current descriptor be the empty string.
				// Set state to after descriptor.
				if (isSpace(charCode)) {
					if (currentDescriptor) {
						descriptors.push(currentDescriptor);
						currentDescriptor = "";
						state = "after descriptor";
					}
				}
				// U+002C COMMA (,)
				// Advance position to the next character in input. If current descriptor
				// is not empty, append current descriptor to descriptors. Jump to the step
				// labeled descriptor parser.
				else if (charCode === COMMA) {
					position += 1;

					if (currentDescriptor) {
						descriptors.push(currentDescriptor);
					}

					parseDescriptors();

					return;
				}
				// U+0028 LEFT PARENTHESIS (()
				// Append charCode to current descriptor. Set state to in parens.
				else if (charCode === LEFT_PARENTHESIS) {
					currentDescriptor += input.charAt(position);
					state = "in parens";
				}
				// EOF
				// If current descriptor is not empty, append current descriptor to
				// descriptors. Jump to the step labeled descriptor parser.
				else if (Number.isNaN(charCode)) {
					if (currentDescriptor) {
						descriptors.push(currentDescriptor);
					}

					parseDescriptors();

					return;

					// Anything else
					// Append charCode to current descriptor.
				} else {
					currentDescriptor += input.charAt(position);
				}
			}
			// In parens
			else if (state === "in parens") {
				// U+0029 RIGHT PARENTHESIS ())
				// Append charCode to current descriptor. Set state to in descriptor.
				if (charCode === RIGHT_PARENTHESIS) {
					currentDescriptor += input.charAt(position);
					state = "in descriptor";
				}
				// EOF
				// Append current descriptor to descriptors. Jump to the step labeled
				// descriptor parser.
				else if (Number.isNaN(charCode)) {
					descriptors.push(currentDescriptor);
					parseDescriptors();
					return;
				}
				// Anything else
				// Append charCode to current descriptor.
				else {
					currentDescriptor += input.charAt(position);
				}
			}
			// After descriptor
			else if (state === "after descriptor") {
				// Do the following, depending on the value of charCode:
				if (isSpace(charCode)) {
					// Space character: Stay in this state.
				}
				// EOF: Jump to the step labeled descriptor parser.
				else if (Number.isNaN(charCode)) {
					parseDescriptors();
					return;
				}
				// Anything else
				// Set state to in descriptor. Set position to the previous character in input.
				else {
					state = "in descriptor";
					position -= 1;
				}
			}

			// Advance position to the next character in input.
			position += 1;
		}
	}

	// 3. Let candidates be an initially empty source set.
	// const candidates = []; // Moved to top

	// 4. Splitting loop: Collect a sequence of characters that are space
	//    characters or U+002C COMMA characters. If any U+002C COMMA characters
	//    were collected, that is a parse error.

	while (true) {
		collectCharacters(LEADING_COMMAS_OR_SPACES_REGEXP);

		// 5. If position is past the end of input, return candidates and abort these steps.
		if (position >= inputLength) {
			if (candidates.length === 0) {
				throw new Error("Must contain one or more image candidate strings");
			}

			// (we're done, this is the sole return path)
			return candidates;
		}

		// 6. Collect a sequence of characters that are not space characters,
		//    and let that be url.
		start = position;
		url = collectCharacters(LEADING_NOT_SPACES);

		// 7. Let descriptors be a new empty list.
		descriptors = [];

		// 8. If url ends with a U+002C COMMA character (,), follow these sub steps:
		//		(1). Remove all trailing U+002C COMMA characters from url. If this removed
		//         more than one character, that is a parse error.
		if (url && url.charCodeAt(url.length - 1) === COMMA) {
			url = url.replace(TRAILING_COMMAS_REGEXP, "");

			// (Jump ahead to step 9 to skip tokenization and just push the candidate).
			parseDescriptors();
		}
		//	Otherwise, follow these sub steps:
		else {
			tokenize();
		}

		// 16. Return to the step labeled splitting loop.
	}
};

module.exports.NS_HTML = NS_HTML;
module.exports.NS_MATHML = NS_MATHML;
module.exports.NS_SVG = NS_SVG;
module.exports.NodeType = NodeType;
module.exports.QUOTE_DOUBLE = QUOTE_DOUBLE;
module.exports.QUOTE_NONE = QUOTE_NONE;
module.exports.QUOTE_SINGLE = QUOTE_SINGLE;
module.exports.SVG_TAG_ADJUST = SVG_TAG_ADJUST;
module.exports.SourceProcessor = SourceProcessor;
// Exposed so HtmlParser can map user-configured (lowercased) tag names to
// the adjusted camelCase names the AST carries for foreign content.
module.exports.buildHtmlAst = buildHtmlAst;
module.exports.decodeHtmlEntities = decodeHtmlEntities;
module.exports.decodeHtmlEntitiesWithMap = decodeHtmlEntitiesWithMap;
module.exports.findAttr = findAttr;
module.exports.parseSrcset = parseSrcset;
module.exports.walkHtmlTokens = walkHtmlTokens;
