UNPKG

webpack

Version:

Packs ECMAScript/CommonJs/AMD modules for the browser. Allows you to split your codebase into multiple bundles, which can be loaded on demand. Supports loaders to preprocess files, i.e. json, jsx, es7, css, less, ... and your custom stuff.

15,796 lines • 580 kB
/*
	MIT License http://www.opensource.org/licenses/mit-license.php
	Author Raj Aryan (based on SWC parser by Alexander Akait)
*/

"use strict";

const {
	CSS_TYPE,
	HTML_TYPE,
	JAVASCRIPT_TYPE
} = require("../module/ModuleSourceTypeConstants");

const LocConverter = require("../util/LocConverter");
const SourceProcessor = require("../util/SourceProcessor");
const { buildDataURI, readEmbeddedDataURI } = require("../util/dataURL");
const { haltVisitors, resumeVisitors } = require("../util/haltVisitors");

const {
	ADDRESS_DIV_P,
	APPLET_MARQUEE_OBJECT,
	BLOCK_END,
	BLOCK_LEVEL_ELEMENTS,
	BLOCK_START,
	BODY_HTML_BR,
	BODY_START_KEPT_BEFORE,
	BOOLEAN_ATTRIBUTES,
	CAPTION_IGNORED_ENDS,
	CAPTION_TABLE_STARTS,
	CELL_IGNORED_ENDS,
	CLEAR_TABLE,
	CLEAR_TABLE_BODY,
	CLEAR_TABLE_ROW,
	COMMA_LIST_ATTRIBUTES,
	DOM_TOKEN_LIST_ATTRIBUTES,
	EMPTY_ELEMENT_DROPPED,
	EMPTY_METADATA_ELEMENTS,
	EMPTY_REMOVABLE_ATTRIBUTES,
	ENUMERATED_ATTRIBUTE_NAMES,
	ENUMERATED_KEYWORDS,
	EVENT_HANDLER_ATTRIBUTES,
	FONT_BREAKOUT_ATTRS,
	FOREIGN_ATTR_NS,
	FOREIGN_BREAKOUT,
	FORMATTING,
	FOSTER_KEPT,
	HEADING,
	HEAD_BODY_HTML_BR,
	HEAD_ELEMENTS,
	HEAD_VOID_ELEMENTS,
	HTML_SCOPE,
	IGNORED_BODY_TABLE_STARTS,
	IMPLIED,
	IMPLIED_THOROUGH,
	INTEGER_ATTRIBUTES,
	INVALID_VALUE_DEFAULT_DIFFERS,
	IN_HEAD_NOSCRIPT_PASSTHROUGH,
	IN_TABLE_IGNORED_ENDS,
	JAVASCRIPT_SCRIPT_TYPES,
	LEADING_NEWLINE_ELEMENTS,
	LITERAL_TEXT_PARENTS,
	MATHML_SPECIAL,
	MATHML_TEXT_INTEGRATION,
	NOFRAMES_STYLE_NOSCRIPT,
	NO_DECODE_TEXT,
	OPTIONAL_END_TAG_AT_END,
	OPTIONAL_END_TAG_FOLLOWERS,
	OPTIONAL_END_TAG_UNLESS_TRAILING_NODE,
	PARAM_SOURCE_TRACK,
	P_ENDS_ON_PARENT_END_TAG,
	QUIRKY_EXACT,
	QUIRKY_PREFIXES,
	RAW_TEXT_ELEMENTS,
	REDUNDANT_DEFAULT_ATTRIBUTES,
	REDUNDANT_TYPE_ATTRIBUTES,
	RESERVED_ELEMENT_NAMES,
	REWRITABLE_ATTRIBUTES,
	ROW_IGNORED_ENDS,
	ROW_TRIGGER_STARTS,
	SCOPE_ENDED_BY,
	SHADOW_HOSTS,
	SHELL_ELEMENTS,
	SIGNED_INTEGER_ATTRIBUTES,
	SPECIAL,
	SRCSET_ATTRIBUTES,
	STYLE_SCRIPT_TEMPLATE,
	SVG_ATTR_ADJUST,
	SVG_SPECIAL,
	SVG_TAG_ADJUST,
	TABLE_CONTEXT,
	TABLE_SCOPE_STOP,
	TBODY_GROUP,
	TBODY_IGNORED_ENDS,
	TBODY_TRIGGER_STARTS,
	TD_TH,
	TD_TH_TR,
	TEMPLATE_START_TAG_MODES: TEMPLATE_START_TAG_MODE_NAMES,
	TOKEN_LIST_ATTRIBUTES,
	TRANSPARENT_IMPLIED_ELEMENTS,
	URL_ATTRIBUTES,
	VOID,
	VOID_FORMATTING
} = require("./data");
/** @typedef {import("./syntax-printer").CompiledVisitorMap} CompiledVisitorMap */
/** @typedef {import("../util/SourceProcessor").VisitorFn<HtmlPath>} VisitorFn */
/** @typedef {import("./syntax-printer").HtmlTransformOptions} HtmlTransformOptions */
/** @typedef {import("./syntax-printer").HtmlProcessOptions} HtmlProcessOptions */
/** @typedef {import("./syntax-printer").PrintContext} PrintContext */

const { deferredWrite, declineDeferredWrites } = SourceProcessor;

/** @import { OutputHtmlOptions } from "../../declarations/WebpackOptions" */

/**
 * Renders one nested body this document embeds — an inline `<style>`, every
 * `style=""` (handed over as a whole stylesheet), a `<script>` holding JSON or
 * JavaScript, every event handler attribute (`as` says it is a function body
 * rather than a script), an `<svg>` subtree (`as: "foreign-element"`), and the
 * document an `<iframe srcdoc>` holds. Returning the input unchanged declines
 * it. Synchronous: it is called from inside the print walk, so a caller with an
 * async minifier collects the bodies in a walk-only pass first and looks the
 * results up here.
 *
 * What comes back is spliced in as-is, so an `<svg>` renderer must return
 * markup — everything else is literal text. A `<style>` / `<script>` body may
 * therefore carry its own `sourceMappingURL`, which is the only way a map
 * reaches inline content; nothing else here can hold one.
 * @typedef {(source: string, info: { type: string, hostType: string, as?: string }) => string | undefined} EmbeddedSourceRenderer
 */

/**
 * One embedded source recorded for a caller that can only answer
 * asynchronously, and the text to print once it has.
 * @typedef {import("../util/dataURL").DeferredEmbeddedSource} DeferredEmbeddedSource
 */

// Neither a JSON `<script>` body nor an `<svg>` subtree is a module source
// type, so neither has a constant in `ModuleSourceTypeConstants` — they name
// themselves here.
const JSON_TYPE = "json";
const SVG_TYPE = "svg";
// A `style=""` holds a block's contents, not a stylesheet. It is still CSS, so
// it is offered as CSS — with `as` saying which production it is, the same
// vocabulary `module.parser.css.as` uses.
const BLOCK_CONTENTS = "block-contents";
// An `onclick=""` is that shape one language over: JavaScript, but a function
// body — the production the `return` cancelling an event is written in.
const EVENT_HANDLER = "event-handler";
// An `<svg>` subtree is SVG, but an element of the HTML document holding it
// rather than a standalone `.svg`, which may be read without a namespace.
const FOREIGN_ELEMENT = "foreign-element";

// Every language `renderEmbeddedSource` is offered from a document, in the
// order the offer sites below reach them. A new site adds its language here, so
// a caller that has to say up front which it handles reads one list.
const EMBEDDED_LANGUAGES = [
	CSS_TYPE,
	JAVASCRIPT_TYPE,
	JSON_TYPE,
	SVG_TYPE,
	HTML_TYPE
];

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

// #region html entities
// Auto-generated by `tooling/generate-html-data.js` from
// `tooling/html-entities.json` — run `yarn fix:special` to refresh.

// WHATWG named character references, keyed without the leading `&`: the keys
// that do not end in `;` are the legacy names matching unterminated.

// Null-prototype, so `&toString;` cannot reach an inherited `Object.prototype`
// key and read back as a matched 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;":"IJ","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;":"ffi","fflig;":"ff","ffllig;":"ffl","ffr;":"𝔣","filig;":"fi","fjlig;":"fj","flat;":"♭","fllig;":"fl","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;":"ij","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;":"ʼn","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_LEFT_PARENTHESIS = 0x28;
const CC_RIGHT_PARENTHESIS = 0x29;
const CC_ASTERISK = 0x2a;
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_COMMERCIAL_AT = 0x40;
const CC_LEFT_SQUARE_BRACKET = 0x5b;
const CC_REVERSE_SOLIDUS = 0x5c;
const CC_RIGHT_SQUARE_BRACKET = 0x5d;
const CC_LOW_LINE = 0x5f;
const CC_GRAVE_ACCENT = 0x60;
const CC_LEFT_CURLY_BRACKET = 0x7b;
const CC_RIGHT_CURLY_BRACKET = 0x7d;
const CC_NO_BREAK_SPACE = 0xa0;

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

/**
 * How an attribute value is quoted in the source (HTML §13.2.5.36–38).
 * @type {{ None: 0, Double: 1, Single: 2 }}
 */
const Quote = {
	None: QUOTE_NONE,
	Double: QUOTE_DOUBLE,
	Single: QUOTE_SINGLE
};

// The longest WHATWG named entity is 32 chars with its `;`, 31 without. Capping
// the tokenizer's reference run and the decoder's longest-prefix backtrack there
// keeps `&` followed by thousands of alphanumerics linear-time.
const MAX_ENTITY_NAME_LEN = 32;

// ASCII character-class bit flags in one table: a load and a mask in place of
// the comparison chain these per-code-point predicates would otherwise run. No
// code point >= 0x80 is in any class, so callers short-circuit on `cc < 0x80`.
const CCLS_SPACE = 1;
const CCLS_DIGIT = 2;
const CCLS_UPPER = 4;
const CCLS_LOWER = 8;
const CCLS_HEX = 16;
// Terminator sets of the tag-name / attribute-name states' fast-forward scans,
// mirroring those states' arcs; one mask test replaces their compare chains.
const CCLS_TAG_NAME_TERM = 32;
const CCLS_ATTR_NAME_TERM = 64;
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;
	if (
		(f & CCLS_SPACE) !== 0 ||
		i === CC_SOLIDUS ||
		i === CC_GREATER_THAN ||
		i === CC_NULL
	) {
		f |= CCLS_TAG_NAME_TERM | CCLS_ATTR_NAME_TERM;
	}
	if (
		i === CC_EQUALS ||
		i === CC_QUOTATION_MARK ||
		i === CC_APOSTROPHE ||
		i === CC_LESS_THAN
	) {
		f |= CCLS_ATTR_NAME_TERM;
	}
	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 isAsciiWhitespace = (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, dataStart: number, dataEnd: number) => number=} comment
 * @property {(input: string, start: number, end: number, nameStart: number, nameEnd: number, publicStart: number, publicEnd: number, systemStart: number, systemEnd: number, forceQuirks: boolean, subsetStart?: number, subsetEnd?: number) => number=} doctype `subsetStart` / `subsetEnd` delimit an XML internal subset (`tokenizeXml` only; -1 when absent)
 * @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, which is where a `<![CDATA[` opens a CDATA section
 * @property {((name: string) => boolean)=} vetoesContentMode returns true when the tree builder will not run the algorithm that switches the tokenizer for this start tag, so it stays in the data state
 * @property {string=} fragmentContext context element tag name for fragment parsing; seeds the initial content mode
 * @property {boolean=} scripting read a `noscript` as raw text, as a document with scripting on does
 */

/**
 * @param {string} name tag name (lowercase)
 * @param {boolean=} scripting whether `noscript` holds raw text, as it does with scripting on
 * @returns {number} content mode state for this tag, or STATE_DATA
 */
const getContentModeForTag = (name, scripting) => {
	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;
		case "noscript":
			return scripting === true ? STATE_RAWTEXT : STATE_DATA;
		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 rangeAsciiCaseInsensitiveMatch = (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
 * @param {boolean} scripting whether `noscript` holds raw text, as it does with scripting on
 * @returns {number} content mode state, or STATE_DATA
 */
const getContentModeForRange = (input, start, end, scripting) => {
	switch (end - start) {
		case 3:
			if (rangeAsciiCaseInsensitiveMatch(input, start, end, "xmp")) {
				return STATE_RAWTEXT;
			}
			return STATE_DATA;
		case 5:
			if (rangeAsciiCaseInsensitiveMatch(input, start, end, "title")) {
				return STATE_RCDATA;
			}
			if (rangeAsciiCaseInsensitiveMatch(input, start, end, "style")) {
				return STATE_RAWTEXT;
			}
			return STATE_DATA;
		case 6:
			if (rangeAsciiCaseInsensitiveMatch(input, start, end, "script")) {
				return STATE_SCRIPT_DATA;
			}
			if (rangeAsciiCaseInsensitiveMatch(input, start, end, "iframe")) {
				return STATE_RAWTEXT;
			}
			return STATE_DATA;
		case 7:
			if (rangeAsciiCaseInsensitiveMatch(input, start, end, "noembed")) {
				return STATE_RAWTEXT;
			}
			return STATE_DATA;
		case 8:
			if (rangeAsciiCaseInsensitiveMatch(input, start, end, "textarea")) {
				return STATE_RCDATA;
			}
			if (rangeAsciiCaseInsensitiveMatch(input, start, end, "noframes")) {
				return STATE_RAWTEXT;
			}
			if (
				scripting &&
				rangeAsciiCaseInsensitiveMatch(input, start, end, "noscript")
			) {
				return STATE_RAWTEXT;
			}
			return STATE_DATA;
		case 9:
			if (rangeAsciiCaseInsensitiveMatch(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 tokenize = (input, pos = 0, callbacks = {}) => {
	const len = input.length;
	const scripting = callbacks.scripting === true;
	let state = STATE_DATA;
	let returnState = STATE_DATA;

	let textStart = pos;
	let tagStart = pos;
	let tagNameStart = -1;
	let tagNameEnd = -1;
	let attributeNameStart = -1;
	let attributeNameEnd = -1;
	let attributeValueStart = -1;
	let attrQuoteType = QUOTE_NONE;
	let commentStart = pos;
	// Data range of the comment being scanned, tracked rather than re-derived by
	// trimming delimiters: the comment states append characters that were never
	// consumed, such as the `--` an inner `-->` turns into.
	let commentDataStart = pos;
	let commentDataEnd = pos;
	// Sub-ranges of the DOCTYPE token being scanned (-1 = the spec's
	// "missing" value, which a consumer must tell apart from an empty one).
	let doctypeNameStart = -1;
	let doctypeNameEnd = -1;
	let doctypePublicStart = -1;
	let doctypePublicEnd = -1;
	let doctypeSystemStart = -1;
	let doctypeSystemEnd = -1;
	let doctypeForceQuirks = false;
	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;
	// How much of `"script"` the ASCII-alpha run after `<` / `</` has matched,
	// for SCRIPT_DATA_DOUBLE_ESCAPE_{START,END}: 0..6 chars, or 7 for no longer
	// matching. A counter rather than a buffer, which a long run would grow.
	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;
	// Memoized next `<` / `&` / NUL for the text-run fast-forwards, where native
	// `indexOf` beats a per-char loop. `pos` only moves forward, so each memo is
	// refreshed at most once per occurrence; `len` means no further one.
	let nextLt = -1;
	let nextAmp = -1;
	let nextNul = -1;
	let nextHyphen = -1;
	let nextGt = -1;
	let nextBracket = -1;
	// Same memo scheme for the closing quote of quoted attribute values (long
	// values: data: URIs, srcset, inline style). Memoized so a value with many
	// `&` references doesn't re-run the quote scan per reference.
	let nextDQuote = -1;
	let nextSQuote = -1;

	/**
	 * 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 `decodeEntities`.
	 * @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, named at `lastOpenTagStart..End`. In
	// foreign content `isForeign` vetoes the RAWTEXT/RCDATA/script switch, so an
	// SVG `<title>` is markup; `lastOpenTagName` is materialized only there.
	const contentModeAfterOpenTag = () => {
		const m = getContentModeForRange(
			input,
			lastOpenTagStart,
			lastOpenTagEnd,
			scripting
		);
		// Ordinary tags stay in the data state whatever the tree builder did, so
		// they pay for neither the name nor the callback.
		if (m === STATE_DATA) return STATE_DATA;
		lastOpenTagName = input
			.slice(lastOpenTagStart, lastOpenTagEnd)
			.toLowerCase();
		if (
			callbacks.vetoesContentMode !== undefined &&
			callbacks.vetoesContentMode(lastOpenTagName)
		) {
			return STATE_DATA;
		}
		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.vetoesContentMode !== undefined &&
			callbacks.vetoesContentMode(lastOpenTagName)
				? STATE_DATA
				: getContentModeForTag(lastOpenTagName, scripting);
	}

	/**
	 * @param {number} endPos end position
	 */
	const flushText = (endPos) => {
		if (textStart < endPos) {
			if (callbacks.text !== undefined) {
				callbacks.text(input, textStart, endPos);
			}
			// Advancing `textStart` makes a second `flushText` over the same span a
			// no-op instead of a duplicate emit. `emitOpenTag` / `emitCloseTag`
			// overwrite it with their own `nextPos`, so their start is unaffected.
			textStart = endPos;
		}
	};

	/**
	 * @param {number} endPos end position
	 * @returns {number} next position
	 */
	const emitAttribute = (endPos) => {
		// The default `nextPos` advances past the closing quote, if any, so the
		// machine continues without an `attribute` callback. A callback's return
		// value overrides it, and owes the same advance from its `quoteType`.
		let nextPos = attrQuoteType === QUOTE_NONE ? endPos : endPos + 1;
		if (callbacks.attribute !== undefined && attributeNameStart !== -1) {
			nextPos = callbacks.attribute(
				input,
				attributeNameStart,
				attributeNameEnd,
				attributeValueStart,
				attributeValueStart === -1 ? -1 : endPos,
				attrQuoteType
			);
		}
		if (attributeNameStart !== -1) tagHasAttributes = true;
		attributeNameStart = -1;
		attributeValueStart = -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);

		// Every WHATWG tokenizer state is handled. Omitted: the parse errors
		// needing state an offset scanner lacks (duplicate-attribute,
		// cdata-in-html-content, `*-in-input-stream`) and reference substitution.
		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 ordinary text without re-entering the switch.
					// Only the data state's "anything else" arc is batched: the stops
					// are exactly the code points with arcs of their own.
					pos++;
					if (nextLt < pos) {
						nextLt = input.indexOf("<", pos);
						if (nextLt === -1) nextLt = len;
					}
					if (nextAmp < pos) {
						nextAmp = input.indexOf("&", pos);
						if (nextAmp === -1) nextAmp = len;
					}
					if (nextNul < pos) {
						nextNul = input.indexOf("\0", pos);
						if (nextNul === -1) nextNul = len;
					}
					pos = Math.min(nextLt, nextAmp, nextNul);
					// Fused: the memo invariant guarantees `input[nextLt] === "<"`, so
					// take this state's `<` arc without another dispatch.
					if (pos === nextLt && pos < len) {
						tagStart = pos;
						state = STATE_TAG_OPEN;
						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++;
					// Fused: when the next char is alpha, take the end-tag-open state's
					// alpha arc (and its tag-name run scan) without another dispatch.
					if (pos < len && isAsciiAlpha(input.charCodeAt(pos))) {
						flushText(tagStart);
						tagNameStart = pos;
						state = STATE_TAG_NAME;
						pos++;
						while (pos < len) {
							const c2 = input.charCodeAt(pos);
							if (c2 < 0x80 && (CHAR_CLASS[c2] & CCLS_TAG_NAME_TERM) !== 0) {
								break;
							}
							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;
					// Fused reconsume: the first char is alpha, so the tag-name state
					// always lands in its run scan — run it here without a dispatch.
					pos++;
					while (pos < len) {
						const c2 = input.charCodeAt(pos);
						if (c2 < 0x80 && (CHAR_CLASS[c2] & CCLS_TAG_NAME_TERM) !== 0) {
							break;
						}
						pos++;
					}
				} else if (cc === CC_QUESTION_MARK) {
					// `?`: unexpected-question-mark-instead-of-tag-name parse error. Create a
					// comment token with empty data, reconsume in bogus comment.
					reportError(
						"unexpected-question-mark-instead-of-tag-name",
						pos,
						pos + 1,
						"warning"
					);
					flushText(tagStart);
					commentStart = tagStart;
					commentDataStart = pos;
					commentDataEnd = pos;
					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:
				// The spec's ASCII-alpha arc is taken inline by the `<` `/` arc above,
				// the only way into this state, so `cc` is never ASCII alpha here.
				if (cc === CC_GREATER_THAN) {
					// `>`: missing-end-tag-name parse error, switch to data. Emitting no
					// token drops `</>` rather than keeping it in the text run, where
					// re-escaping the text node would resurface it as visible text.
					reportError("missing-end-tag-name", pos, pos + 1, "warning");
					flushText(tagStart);
					state = STATE_DATA;
					pos++;
					textStart = pos;
				} else {
					// Anything else: invalid-first-character-of-tag-name parse error. Create a
					// comment token with empty data, reconsume in bogus comment.
					reportError(
						"invalid-first-character-of-tag-name",
						pos,
						pos + 1,
						"warning"
					);
					flushText(tagStart);
					commentStart = tagStart;
					commentDataStart = pos;
					commentDataEnd = pos;
					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:
				// Whitespace (tab, LF, FF, space)
				// Switch to the before attribute name state.
				if (isAsciiWhitespace(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 < 0x80 && (CHAR_CLASS[c2] & CCLS_TAG_NAME_TERM) !== 0) {
							break;
						}
						pos++;
					}
				}
				break;

			// https://html.spec.whatwg.org/multipage/parsing.html#before-attribute-name-state
			case STATE_BEFORE_ATTRIBUTE_NAME:
				// Whitespace (tab, LF, FF, space)
				// Ignore the character.
				// Reconsume so space is handled in BEFORE_ATTRIBUTE_NAME
				if (isAsciiWhitespace(cc)) {
					pos++;
				} else if (cc === CC_SOLIDUS || cc === CC_GREATER_THAN) {
					// `/` / `>` / EOF: reconsume in after attribute name.
					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"
					);
					attributeNameStart = 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.
					attributeNameStart = pos;
					state = STATE_ATTRIBUTE_NAME;
					// Fused reconsume: `cc` can't be a terminator here (space / `/` /
					// `>` / `=` took earlier arcs), so run the attribute-name state's
					// else arc — its first-char errors and run scan — without a dispatch.
					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"
						);
					}
					pos++;
					while (pos < len) {
						const c2 = input.charCodeAt(pos);
						if (c2 < 0x80 && (CHAR_CLASS[c2] & CCLS_ATTR_NAME_TERM) !== 0) {
							break;
						}
						pos++;
					}
				}
				break;

			// https://html.spec.whatwg.org/multipage/parsing.html#attribute-name-state
			case STATE_ATTRIBUTE_NAME:
				// Whitespace (tab, LF, FF, space) / `/` / `>` / EOF: reconsume in after
				// attribute name.
				if (
					isAsciiWhitespace(cc) ||
					cc === CC_SOLIDUS ||
					cc === CC_GREATER_THAN
				) {
					attributeNameEnd = pos;
					state = STATE_AFTER_ATTRIBUTE_NAME;
					// Reconsume
				} else if (cc === CC_EQUALS) {
					attributeNameEnd = 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 attribute name, stopping on a terminator
					// (space, `/`, `>`, `=`) or on a char needing a per-occurrence parse
					// error (NUL, `"`, `'`, `<`), which the outer switch re-handles.
					pos++;
					while (pos < len) {
						const c2 = input.charCodeAt(pos);
						if (c2 < 0x80 && (CHAR_CLASS[c2] & CCLS_ATTR_NAME_TERM) !== 0) {
							break;
						}
						pos++;
					}
				}
				break;

			// https://html.spec.whatwg.org/multipage/parsing.html#after-attribute-name-state
			case STATE_AFTER_ATTRIBUTE_NAME:
				// Whitespace (tab, LF, FF, space)
				// Ignore the character.
				if (isAsciiWhitespace(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);
					attributeNameStart = pos;
					state = STATE_ATTRIBUTE_NAME;
					// Reconsume
				}
				break;

			// https://html.spec.whatwg.org/multipage/parsing.html#before-attribute-value-state
			case STATE_BEFORE_ATTRIBUTE_VALUE:
				// Whitespace (tab, LF, FF, space)
				// Ignore the character.
				if (isAsciiWhitespace(cc)) {
					pos++;
				} else if (cc === CC_QUOTATION_MARK) {
					// U+0022 QUOTATION MARK (")
					// Switch to the attribute value (double-quoted) state.
					attributeValueStart = pos + 1;
					attrQuoteType = QUOTE_DOUBLE;
					state = STATE_ATTRIBUTE_VALUE_DOUBLE_QUOTED;
					pos++;
					// Fused: run that state's memoized value scan now; a leading `"` /
					// `&` / NUL keeps the min at `pos` for the next dispatch.
					if (nextDQuote < pos) {
						nextDQuote = input.indexOf('"', pos);
						if (nextDQuote === -1) nextDQuote = len;
					}
					if (nextAmp < pos) {
						nextAmp = input.indexOf("&", pos);
						if (nextAmp === -1) nextAmp = len;
					}
					if (nextNul < pos) {
						nextNul = input.indexOf("\0", pos);
						if (nextNul === -1) nextNul = len;
					}
					pos = Math.min(nextDQuote, nextAmp, nextNul);
				} else if (cc === CC_APOSTROPHE) {
					// U+0027 APOSTROPHE (')
					// Switch to the attribute value (single-quoted) state.
					attributeValueStart = pos + 1;
					attrQuoteType = QUOTE_SINGLE;
					state = STATE_ATTRIBUTE_VALUE_SINGLE_QUOTED;
					pos++;
					// Fused: run that state's memoized value scan now; a leading `'` /
					// `&` / NUL keeps the min at `pos` for the next dispatch.
					if (nextSQuote < pos) {
						nextSQuote = input.indexOf("'", pos);
						if (nextSQuote === -1) nextSQuote = len;
					}
					if (nextAmp < pos) {
						nextAmp = input.indexOf("&", pos);
						if (nextAmp === -1) nextAmp = len;
					}
					if (nextNul < pos) {
						nextNul = input.indexOf("\0", pos);
						if (nextNul === -1) nextNul = len;
					}
					pos = Math.min(nextSQuote, nextAmp, nextNul);
				} else if (cc === CC_GREATER_THAN) {
					// `>`: missing-attribute-value parse error, switch to data and emit
					// the tag token. The attribute's empty value range points at the `>`,
					// so the open tag's own range still includes it.
					reportError("missing-attribute-value", pos, pos + 1, "warning");
					attributeValueStart = 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.
					attributeValueStart = 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 with the
					// same memoized native-scan scheme as the data state.
					pos++;
					if (nextDQuote < pos) {
						nextDQuote = input.indexOf('"', pos);
						if (nextDQuote === -1) nextDQuote = len;
					}
					if (nextAmp < pos) {
						nextAmp = input.indexOf("&", pos);
						if (nextAmp === -1) nextAmp = len;
					}
					if (nextNul < pos) {
						nextNul = input.indexOf("\0", pos);
						if (nextNul === -1) nextNul = len;
					}
					pos = Math.min(nextDQuote, nextAmp, nextNul);
				}
				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 with the
					// same memoized native-scan scheme as the data state.
					pos++;
					if (nextSQuote < pos) {
						nextSQuote = input.indexOf("'", pos);
						if (nextSQuote === -1) nextSQuote = len;
					}
					if (nextAmp < pos) {
						nextAmp = input.indexOf("&", pos);
						if (nextAmp === -1) nextAmp = len;
					}
					if (nextNul < pos) {
						nextNul = input.indexOf("\0", pos);
						if (nextNul === -1) nextNul = len;
					}
					pos = Math.min(nextSQuote, nextAmp, nextNul);
				}
				break;

			// https://html.spec.whatwg.org/multipage/parsing.html#attribute-value-(unquoted)-state
			case STATE_ATTRIBUTE_VALUE_UNQUOTED:
				if (isAsciiWhitespace(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:
				// Whitespace (tab, LF, FF, space)
				// Switch to the before attribute name state.
				if (isAsciiWhitespace(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:
				// `>`: Set the self-closing flag of the tag token, switch to data. Emit the
				// 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:
				// `--`: Consume those two characters, create a comment token with empty
				// data, and switch to the comment start state.
				if (
					cc === CC_HYPHEN_MINUS &&
					input.charCodeAt(pos + 1) === CC_HYPHEN_MINUS
				) {
					pos += 2;
					commentStart = tagStart;
					commentDataStart = pos;
					commentDataEnd = pos;
					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;
					doctypeNameStart = -1;
					doctypeNameEnd = -1;
					doctypePublicStart = -1;
					doctypePublicEnd = -1;
					doctypeSystemStart = -1;
					doctypeSystemEnd = -1;
					doctypeForceQuirks = false;
					state = STATE_DOCTYPE;
				} else if (
					// `[CDATA[`: consume it and switch to CDATA section, but only with an
					// adjusted current node outside the HTML namespace. Elsewhere it is a
					// bogus comment, leaving the rest as text.
					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 &&
					callbacks.isForeign !== undefined &&
					callbacks.isForeign()
				) {
					pos += 7;
					commentStart = tagStart;
					commentDataStart = pos;
					commentDataEnd = pos;
					state = STATE_CDATA_SECTION;
				} else {
					// Anything else: incorrectly-opened-comment parse error. Create a comment
					// token with empty data, switch to bogus comment (don't consume anything
					// in the current state).
					reportError("incorrectly-opened-comment", tagStart, pos, "warning");
					commentStart = tagStart;
					commentDataStart = pos;
					commentDataEnd = pos;
					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,
							commentDataStart,
							commentDataEnd
						);
					}
					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,
							commentDataStart,
							commentDataEnd
						);
					}
					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.
					commentDataEnd = pos;
					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++;
					commentDataEnd = 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");
					}
					// Fast-forward over ordinary comment text (same memoized
					// `indexOf` scheme as the data state); stop on `<` / `-` / NUL.
					pos++;
					if (nextLt < pos) {
						nextLt = input.indexOf("<", pos);
						if (nextLt === -1) nextLt = len;
					}
					if (nextHyphen < pos) {
						nextHyphen = input.indexOf("-", pos);
						if (nextHyphen === -1) nextHyphen = len;
					}
					if (nextNul < pos) {
						nextNul = input.indexOf("\0", pos);
						if (nextNul === -1) nextNul = len;
					}
					pos = Math.min(nextLt, nextHyphen, nextNul);
					commentDataEnd = 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 `-` to the comment data and reconsume in
					// comment, where NUL and `<` are handled.
					commentDataEnd = pos;
					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,
							commentDataStart,
							commentDataEnd
						);
					}
					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) {
					// One of the two pending dashes joins the data; the other stays
					// pending, so the range grows by one rather than to `pos`.
					commentDataEnd++;
					pos++;
				} else {
					// Anything else: append two `-` to the comment data and reconsume in
					// comment, where NUL and `<` are handled.
					commentDataEnd = pos;
					state = STATE_COMMENT;
					// Reconsume
				}
				break;

			// https://html.spec.whatwg.org/multipage/parsing.html#comment-end-bang-state
			case STATE_COMMENT_END_BANG:
				// `-`: Append two `-` and a `!` to the comment data, switch to comment end
				// dash.
				if (cc === CC_HYPHEN_MINUS) {
					commentDataEnd = pos;
					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,
							commentDataStart,
							commentDataEnd
						);
					}
					state = STATE_DATA;
					textStart = nextPos;
					pos = nextPos;
				} else {
					// Anything else: append two `-` and a `!` to the comment data and
					// reconsume in comment, where NUL and `<` are handled.
					commentDataEnd = pos;
					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,
							commentDataStart,
							commentDataEnd
						);
					}
					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");
					}
					// Fast-forward over ordinary bogus-comment text (memoized `indexOf`
					// scheme); stop on `>` / NUL.
					pos++;
					if (nextGt < pos) {
						nextGt = input.indexOf(">", pos);
						if (nextGt === -1) nextGt = len;
					}
					if (nextNul < pos) {
						nextNul = input.indexOf("\0", pos);
						if (nextNul === -1) nextNul = len;
					}
					pos = Math.min(nextGt, nextNul);
					commentDataEnd = pos;
				}
				break;

			// https://html.spec.whatwg.org/multipage/parsing.html#comment-less-than-sign-state
			case STATE_COMMENT_LESS_THAN_SIGN:
				// `!`: Append the input character to the comment data, switch to comment
				// less-than sign bang.
				if (cc === CC_EXCLAMATION_MARK) {
					state = STATE_COMMENT_LESS_THAN_SIGN_BANG;
					pos++;
					commentDataEnd = pos;
				} else if (cc === CC_LESS_THAN) {
					// U+003C LESS-THAN SIGN (<)
					// Append the current input character to the comment token's data.
					pos++;
					commentDataEnd = 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:
				// `>` / EOF: reconsume in comment end. Anything else: nested-comment parse
				// error, reconsume in comment end.
				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:
				// Whitespace (tab, LF, FF, space)
				// Switch to the before DOCTYPE name state.
				if (isAsciiWhitespace(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:
				// Whitespace (tab, LF, FF, space)
				// Ignore the character.
				if (isAsciiWhitespace(cc)) {
					pos++;
				} else if (cc === CC_NULL) {
					// NUL: unexpected-null-character parse error. Create a new DOCTYPE token.
					// Set the token's name to a U+FFFD, switch to DOCTYPE name.
					reportError("unexpected-null-character", pos, pos + 1, "warning");
					state = STATE_DOCTYPE_NAME;
					doctypeNameStart = pos;
					pos++;
					doctypeNameEnd = pos;
				} else if (cc === CC_GREATER_THAN) {
					// `>`: missing-doctype-name parse error. Create a new DOCTYPE token, force
					// quirks, switch to data. Emit the token.
					reportError("missing-doctype-name", pos, pos + 1, "warning");
					doctypeForceQuirks = true;
					let nextPos = pos + 1;
					if (callbacks.doctype !== undefined) {
						nextPos = callbacks.doctype(
							input,
							commentStart,
							pos + 1,
							doctypeNameStart,
							doctypeNameEnd,
							doctypePublicStart,
							doctypePublicEnd,
							doctypeSystemStart,
							doctypeSystemEnd,
							doctypeForceQuirks
						);
					}
					state = STATE_DATA;
					textStart = nextPos;
					pos = nextPos;
				} else {
					// ASCII upper alpha, and anything else: create a DOCTYPE token, set
					// its name to the input character — lowercased in the first case —
					// and switch to DOCTYPE name.
					state = STATE_DOCTYPE_NAME;
					doctypeNameStart = pos;
					pos++;
					doctypeNameEnd = pos;
				}
				break;

			// https://html.spec.whatwg.org/multipage/parsing.html#doctype-name-state
			case STATE_DOCTYPE_NAME:
				// Whitespace (tab, LF, FF, space)
				// Switch to the after DOCTYPE name state.
				if (isAsciiWhitespace(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,
							doctypeNameStart,
							doctypeNameEnd,
							doctypePublicStart,
							doctypePublicEnd,
							doctypeSystemStart,
							doctypeSystemEnd,
							doctypeForceQuirks
						);
					}
					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++;
					doctypeNameEnd = pos;
				} else {
					// ASCII upper alpha: Append the lowercased input character to the DOCTYPE
					// token's name. Anything else: Append the input character to the DOCTYPE
					// token's name.
					pos++;
					doctypeNameEnd = 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 (isAsciiWhitespace(cc)) {
					// Whitespace (tab, LF, FF, 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,
							doctypeNameStart,
							doctypeNameEnd,
							doctypePublicStart,
							doctypePublicEnd,
							doctypeSystemStart,
							doctypeSystemEnd,
							doctypeForceQuirks
						);
					}
					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: invalid-character-sequence-after-doctype-name parse
					// error, force quirks, reconsume in bogus DOCTYPE.
					reportError(
						"invalid-character-sequence-after-doctype-name",
						pos,
						pos + 1,
						"warning"
					);
					doctypeForceQuirks = true;
					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 (isAsciiWhitespace(cc)) {
					// Whitespace (tab, LF, FF, space)
					// Switch to the before DOCTYPE public identifier state.
					state = STATE_BEFORE_DOCTYPE_PUBLIC_IDENTIFIER;
					pos++;
				} else if (cc === CC_QUOTATION_MARK) {
					// `"`: missing-whitespace-after-doctype-public-keyword parse error. Set
					// the 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++;
					doctypePublicStart = pos;
					doctypePublicEnd = pos;
				} else if (cc === CC_APOSTROPHE) {
					// `'`: missing-whitespace-after-doctype-public-keyword parse error. Set
					// the 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++;
					doctypePublicStart = pos;
					doctypePublicEnd = pos;
				} else if (cc === CC_GREATER_THAN) {
					// `>`: missing-doctype-public-identifier parse error, force quirks, switch
					// to data. Emit the DOCTYPE token.
					reportError(
						"missing-doctype-public-identifier",
						pos,
						pos + 1,
						"warning"
					);
					doctypeForceQuirks = true;
					let nextPos = pos + 1;
					if (callbacks.doctype !== undefined) {
						nextPos = callbacks.doctype(
							input,
							commentStart,
							pos + 1,
							doctypeNameStart,
							doctypeNameEnd,
							doctypePublicStart,
							doctypePublicEnd,
							doctypeSystemStart,
							doctypeSystemEnd,
							doctypeForceQuirks
						);
					}
					state = STATE_DATA;
					textStart = nextPos;
					pos = nextPos;
				} else {
					// Anything else: missing-quote-before-doctype-public-identifier parse
					// error, force quirks, reconsume in bogus DOCTYPE.
					reportError(
						"missing-quote-before-doctype-public-identifier",
						pos,
						pos + 1,
						"warning"
					);
					doctypeForceQuirks = true;
					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 (isAsciiWhitespace(cc)) {
					// Whitespace (tab, LF, FF, space)
					// Ignore the character.
					pos++;
				} else if (cc === CC_QUOTATION_MARK) {
					// `"`: Set the 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++;
					doctypePublicStart = pos;
					doctypePublicEnd = pos;
				} else if (cc === CC_APOSTROPHE) {
					// `'`: Set the 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++;
					doctypePublicStart = pos;
					doctypePublicEnd = pos;
				} else if (cc === CC_GREATER_THAN) {
					// `>`: missing-doctype-public-identifier parse error, force quirks, switch
					// to data. Emit the DOCTYPE token.
					reportError(
						"missing-doctype-public-identifier",
						pos,
						pos + 1,
						"warning"
					);
					doctypeForceQuirks = true;
					let nextPos = pos + 1;
					if (callbacks.doctype !== undefined) {
						nextPos = callbacks.doctype(
							input,
							commentStart,
							pos + 1,
							doctypeNameStart,
							doctypeNameEnd,
							doctypePublicStart,
							doctypePublicEnd,
							doctypeSystemStart,
							doctypeSystemEnd,
							doctypeForceQuirks
						);
					}
					state = STATE_DATA;
					textStart = nextPos;
					pos = nextPos;
				} else {
					// Anything else: missing-quote-before-doctype-public-identifier parse
					// error, force quirks, reconsume in bogus DOCTYPE.
					reportError(
						"missing-quote-before-doctype-public-identifier",
						pos,
						pos + 1,
						"warning"
					);
					doctypeForceQuirks = true;
					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) {
					// NUL: unexpected-null-character parse error. Append a U+FFFD to the
					// DOCTYPE token's public identifier.
					reportError("unexpected-null-character", pos, pos + 1, "warning");
					pos++;
					doctypePublicEnd = pos;
				} else if (cc === CC_GREATER_THAN) {
					// `>`: abrupt-doctype-public-identifier parse error, force quirks, switch
					// to data. Emit the DOCTYPE token.
					reportError(
						"abrupt-doctype-public-identifier",
						pos,
						pos + 1,
						"warning"
					);
					doctypeForceQuirks = true;
					let nextPos = pos + 1;
					if (callbacks.doctype !== undefined) {
						nextPos = callbacks.doctype(
							input,
							commentStart,
							pos + 1,
							doctypeNameStart,
							doctypeNameEnd,
							doctypePublicStart,
							doctypePublicEnd,
							doctypeSystemStart,
							doctypeSystemEnd,
							doctypeForceQuirks
						);
					}
					state = STATE_DATA;
					textStart = nextPos;
					pos = nextPos;
				} else {
					// Anything else
					// Append the current input character to the current DOCTYPE token's public
					// identifier.
					pos++;
					doctypePublicEnd = 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) {
					// NUL: unexpected-null-character parse error. Append a U+FFFD to the
					// DOCTYPE token's public identifier.
					reportError("unexpected-null-character", pos, pos + 1, "warning");
					pos++;
					doctypePublicEnd = pos;
				} else if (cc === CC_GREATER_THAN) {
					// `>`: abrupt-doctype-public-identifier parse error, force quirks, switch
					// to data. Emit the DOCTYPE token.
					reportError(
						"abrupt-doctype-public-identifier",
						pos,
						pos + 1,
						"warning"
					);
					doctypeForceQuirks = true;
					let nextPos = pos + 1;
					if (callbacks.doctype !== undefined) {
						nextPos = callbacks.doctype(
							input,
							commentStart,
							pos + 1,
							doctypeNameStart,
							doctypeNameEnd,
							doctypePublicStart,
							doctypePublicEnd,
							doctypeSystemStart,
							doctypeSystemEnd,
							doctypeForceQuirks
						);
					}
					state = STATE_DATA;
					textStart = nextPos;
					pos = nextPos;
				} else {
					// Anything else
					// Append the current input character to the current DOCTYPE token's public
					// identifier.
					pos++;
					doctypePublicEnd = 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 (isAsciiWhitespace(cc)) {
					// Whitespace (tab, LF, FF, 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,
							doctypeNameStart,
							doctypeNameEnd,
							doctypePublicStart,
							doctypePublicEnd,
							doctypeSystemStart,
							doctypeSystemEnd,
							doctypeForceQuirks
						);
					}
					state = STATE_DATA;
					textStart = nextPos;
					pos = nextPos;
				} else if (cc === CC_QUOTATION_MARK) {
					// `"`: missing-whitespace-between-doctype-public-and-system-identifiers
					// parse error. Set the system identifier to empty (not missing), then
					// switch to DOCTYPE system identifier (double-quoted).
					reportError(
						"missing-whitespace-between-doctype-public-and-system-identifiers",
						pos,
						pos + 1,
						"warning"
					);
					state = STATE_DOCTYPE_SYSTEM_IDENTIFIER_DOUBLE_QUOTED;
					pos++;
					doctypeSystemStart = pos;
					doctypeSystemEnd = pos;
				} else if (cc === CC_APOSTROPHE) {
					// `'`: missing-whitespace-between-doctype-public-and-system-identifiers
					// parse error. Set the system identifier to empty (not missing), then
					// switch to DOCTYPE system identifier (single-quoted).
					reportError(
						"missing-whitespace-between-doctype-public-and-system-identifiers",
						pos,
						pos + 1,
						"warning"
					);
					state = STATE_DOCTYPE_SYSTEM_IDENTIFIER_SINGLE_QUOTED;
					pos++;
					doctypeSystemStart = pos;
					doctypeSystemEnd = pos;
				} else {
					// Anything else: missing-quote-before-doctype-system-identifier parse
					// error, force quirks, reconsume in bogus DOCTYPE.
					reportError(
						"missing-quote-before-doctype-system-identifier",
						pos,
						pos + 1,
						"warning"
					);
					doctypeForceQuirks = true;
					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 (isAsciiWhitespace(cc)) {
					// Whitespace (tab, LF, FF, 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,
							doctypeNameStart,
							doctypeNameEnd,
							doctypePublicStart,
							doctypePublicEnd,
							doctypeSystemStart,
							doctypeSystemEnd,
							doctypeForceQuirks
						);
					}
					state = STATE_DATA;
					textStart = nextPos;
					pos = nextPos;
				} else if (cc === CC_QUOTATION_MARK) {
					// `"`: Set the 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++;
					doctypeSystemStart = pos;
					doctypeSystemEnd = pos;
				} else if (cc === CC_APOSTROPHE) {
					// `'`: Set the 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++;
					doctypeSystemStart = pos;
					doctypeSystemEnd = pos;
				} else {
					// Anything else: missing-quote-before-doctype-system-identifier parse
					// error, force quirks, reconsume in bogus DOCTYPE.
					reportError(
						"missing-quote-before-doctype-system-identifier",
						pos,
						pos + 1,
						"warning"
					);
					doctypeForceQuirks = true;
					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 (isAsciiWhitespace(cc)) {
					// Whitespace (tab, LF, FF, space)
					// Switch to the before DOCTYPE system identifier state.
					state = STATE_BEFORE_DOCTYPE_SYSTEM_IDENTIFIER;
					pos++;
				} else if (cc === CC_QUOTATION_MARK) {
					// `"`: missing-whitespace-after-doctype-system-keyword parse error. Set
					// the 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++;
					doctypeSystemStart = pos;
					doctypeSystemEnd = pos;
				} else if (cc === CC_APOSTROPHE) {
					// `'`: missing-whitespace-after-doctype-system-keyword parse error. Set
					// the 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++;
					doctypeSystemStart = pos;
					doctypeSystemEnd = pos;
				} else if (cc === CC_GREATER_THAN) {
					// `>`: missing-doctype-system-identifier parse error, force quirks, switch
					// to data. Emit the DOCTYPE token.
					reportError(
						"missing-doctype-system-identifier",
						pos,
						pos + 1,
						"warning"
					);
					doctypeForceQuirks = true;
					let nextPos = pos + 1;
					if (callbacks.doctype !== undefined) {
						nextPos = callbacks.doctype(
							input,
							commentStart,
							pos + 1,
							doctypeNameStart,
							doctypeNameEnd,
							doctypePublicStart,
							doctypePublicEnd,
							doctypeSystemStart,
							doctypeSystemEnd,
							doctypeForceQuirks
						);
					}
					state = STATE_DATA;
					textStart = nextPos;
					pos = nextPos;
				} else {
					// Anything else: missing-quote-before-doctype-system-identifier parse
					// error, force quirks, reconsume in bogus DOCTYPE.
					reportError(
						"missing-quote-before-doctype-system-identifier",
						pos,
						pos + 1,
						"warning"
					);
					doctypeForceQuirks = true;
					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 (isAsciiWhitespace(cc)) {
					// Whitespace (tab, LF, FF, space)
					// Ignore the character.
					pos++;
				} else if (cc === CC_QUOTATION_MARK) {
					// `"`: Set the 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++;
					doctypeSystemStart = pos;
					doctypeSystemEnd = pos;
				} else if (cc === CC_APOSTROPHE) {
					// `'`: Set the 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++;
					doctypeSystemStart = pos;
					doctypeSystemEnd = pos;
				} else if (cc === CC_GREATER_THAN) {
					// `>`: missing-doctype-system-identifier parse error, force quirks, switch
					// to data. Emit the DOCTYPE token.
					reportError(
						"missing-doctype-system-identifier",
						pos,
						pos + 1,
						"warning"
					);
					doctypeForceQuirks = true;
					let nextPos = pos + 1;
					if (callbacks.doctype !== undefined) {
						nextPos = callbacks.doctype(
							input,
							commentStart,
							pos + 1,
							doctypeNameStart,
							doctypeNameEnd,
							doctypePublicStart,
							doctypePublicEnd,
							doctypeSystemStart,
							doctypeSystemEnd,
							doctypeForceQuirks
						);
					}
					state = STATE_DATA;
					textStart = nextPos;
					pos = nextPos;
				} else {
					// Anything else: missing-quote-before-doctype-system-identifier parse
					// error, force quirks, reconsume in bogus DOCTYPE.
					reportError(
						"missing-quote-before-doctype-system-identifier",
						pos,
						pos + 1,
						"warning"
					);
					doctypeForceQuirks = true;
					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) {
					// NUL: unexpected-null-character parse error. Append a U+FFFD to the
					// DOCTYPE token's system identifier.
					reportError("unexpected-null-character", pos, pos + 1, "warning");
					pos++;
					doctypeSystemEnd = pos;
				} else if (cc === CC_GREATER_THAN) {
					// `>`: abrupt-doctype-system-identifier parse error, force quirks, switch
					// to data. Emit the DOCTYPE token.
					reportError(
						"abrupt-doctype-system-identifier",
						pos,
						pos + 1,
						"warning"
					);
					doctypeForceQuirks = true;
					let nextPos = pos + 1;
					if (callbacks.doctype !== undefined) {
						nextPos = callbacks.doctype(
							input,
							commentStart,
							pos + 1,
							doctypeNameStart,
							doctypeNameEnd,
							doctypePublicStart,
							doctypePublicEnd,
							doctypeSystemStart,
							doctypeSystemEnd,
							doctypeForceQuirks
						);
					}
					state = STATE_DATA;
					textStart = nextPos;
					pos = nextPos;
				} else {
					// Anything else
					// Append the current input character to the current DOCTYPE token's system
					// identifier.
					pos++;
					doctypeSystemEnd = 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) {
					// NUL: unexpected-null-character parse error. Append a U+FFFD to the
					// DOCTYPE token's system identifier.
					reportError("unexpected-null-character", pos, pos + 1, "warning");
					pos++;
					doctypeSystemEnd = pos;
				} else if (cc === CC_GREATER_THAN) {
					// `>`: abrupt-doctype-system-identifier parse error, force quirks, switch
					// to data. Emit the DOCTYPE token.
					reportError(
						"abrupt-doctype-system-identifier",
						pos,
						pos + 1,
						"warning"
					);
					doctypeForceQuirks = true;
					let nextPos = pos + 1;
					if (callbacks.doctype !== undefined) {
						nextPos = callbacks.doctype(
							input,
							commentStart,
							pos + 1,
							doctypeNameStart,
							doctypeNameEnd,
							doctypePublicStart,
							doctypePublicEnd,
							doctypeSystemStart,
							doctypeSystemEnd,
							doctypeForceQuirks
						);
					}
					state = STATE_DATA;
					textStart = nextPos;
					pos = nextPos;
				} else {
					// Anything else
					// Append the current input character to the current DOCTYPE token's system
					// identifier.
					pos++;
					doctypeSystemEnd = 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 (isAsciiWhitespace(cc)) {
					// Whitespace (tab, LF, FF, 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,
							doctypeNameStart,
							doctypeNameEnd,
							doctypePublicStart,
							doctypePublicEnd,
							doctypeSystemStart,
							doctypeSystemEnd,
							doctypeForceQuirks
						);
					}
					state = STATE_DATA;
					textStart = nextPos;
					pos = nextPos;
				} else {
					// Anything else: unexpected-character-after-doctype-system-identifier
					// parse error, reconsume in bogus DOCTYPE. (This does not set the 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,
							doctypeNameStart,
							doctypeNameEnd,
							doctypePublicStart,
							doctypePublicEnd,
							doctypeSystemStart,
							doctypeSystemEnd,
							doctypeForceQuirks
						);
					}
					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 if (cc === CC_NULL) {
					// U+0000 NULL is an unexpected-null-character parse error.
					reportError("unexpected-null-character", pos, pos + 1, "warning");
					pos++;
				} else {
					// Anything else: emit the input character. Fast-forwards to the
					// next `]` or NUL, the code points with arcs of their own, in one
					// scan each — both memoized, so neither rescans per character.
					pos++;
					if (nextBracket < pos) {
						nextBracket = input.indexOf("]", pos);
						if (nextBracket === -1) nextBracket = len;
					}
					if (nextNul < pos) {
						nextNul = input.indexOf("\0", pos);
						if (nextNul === -1) nextNul = len;
					}
					pos = Math.min(nextBracket, nextNul);
				}
				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. The `]]` just consumed closes the
					// section rather than joining its content.
					commentDataEnd = pos - 2;
					let nextPos = pos + 1;
					if (callbacks.comment !== undefined) {
						nextPos = callbacks.comment(
							input,
							commentStart,
							pos + 1,
							commentDataStart,
							commentDataEnd
						);
					}
					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) {
					// `&`: Set the return state to the RCDATA state, switch to character
					// reference. (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 (same memoized
					// `indexOf` scheme as the data state) — batches only this
					// state's "anything else" arc; `&` / `<` / NUL keep their own.
					pos++;
					if (nextLt < pos) {
						nextLt = input.indexOf("<", pos);
						if (nextLt === -1) nextLt = len;
					}
					if (nextAmp < pos) {
						nextAmp = input.indexOf("&", pos);
						if (nextAmp === -1) nextAmp = len;
					}
					if (nextNul < pos) {
						nextNul = input.indexOf("\0", pos);
						if (nextNul === -1) nextNul = len;
					}
					pos = Math.min(nextLt, nextAmp, nextNul);
					// Fused: take this state's `<` arc without another dispatch.
					if (pos === nextLt && pos < len) {
						tagStart = pos;
						state = STATE_RCDATA_LESS_THAN_SIGN;
						pos++;
					}
				}
				break;

			// https://html.spec.whatwg.org/multipage/parsing.html#rcdata-less-than-sign-state
			case STATE_RCDATA_LESS_THAN_SIGN:
				// `/`: switch to RCDATA end tag open. (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:
				// ASCII alpha: Create a new end tag token, set its tag name to the empty
				// string, reconsume in RCDATA end tag name.
				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:
				// Whitespace (tab, LF, FF, 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 (isAsciiWhitespace(cc)) {
					tagNameEnd = pos;
					if (
						rangeAsciiCaseInsensitiveMatch(
							input,
							tagNameStart,
							tagNameEnd,
							lastOpenTagName
						)
					) {
						flushText(tagStart);
						state = STATE_BEFORE_ATTRIBUTE_NAME;
						pos++;
					} else {
						state = STATE_RCDATA;
						// Reconsume
					}
				} else if (cc === CC_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 (
						rangeAsciiCaseInsensitiveMatch(
							input,
							tagNameStart,
							tagNameEnd,
							lastOpenTagName
						)
					) {
						flushText(tagStart);
						state = STATE_SELF_CLOSING_START_TAG;
						pos++;
					} else {
						state = STATE_RCDATA;
						// Reconsume
					}
				} else if (cc === CC_GREATER_THAN) {
					// `>`: If the current end tag token is an appropriate end tag token, then
					// switch to the data state and emit the tag token. Otherwise, treat it as
					// per the "anything else" entry below.
					tagNameEnd = pos;
					if (
						rangeAsciiCaseInsensitiveMatch(
							input,
							tagNameStart,
							tagNameEnd,
							lastOpenTagName
						)
					) {
						flushText(tagStart);
						state = STATE_DATA;
						pos = emitCloseTag(pos + 1);
					} else {
						state = STATE_RCDATA;
						// Reconsume
					}
				} else if (isAsciiAlpha(cc)) {
					// ASCII alpha: append it lowercased to the tag name and as written to
					// the temporary buffer. Fused, so the whole run is one dispatch.
					pos++;
					while (pos < len && isAsciiAlpha(input.charCodeAt(pos))) pos++;
				} else {
					// Anything else: emit `<`, `/` and the temporary buffer in order,
					// then reconsume in RCDATA.
					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 (same memoized
					// `indexOf` scheme as the data state) — batches only this
					// state's "anything else" arc; `<` / NUL keep their own.
					pos++;
					if (nextLt < pos) {
						nextLt = input.indexOf("<", pos);
						if (nextLt === -1) nextLt = len;
					}
					if (nextNul < pos) {
						nextNul = input.indexOf("\0", pos);
						if (nextNul === -1) nextNul = len;
					}
					pos = Math.min(nextLt, nextNul);
					// Fused: take this state's `<` arc without another dispatch.
					if (pos === nextLt && pos < len) {
						tagStart = pos;
						state = STATE_RAWTEXT_LESS_THAN_SIGN;
						pos++;
					}
				}
				break;

			// https://html.spec.whatwg.org/multipage/parsing.html#rawtext-less-than-sign-state
			case STATE_RAWTEXT_LESS_THAN_SIGN:
				// `/`: switch to RAWTEXT end tag open. (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:
				// ASCII alpha: Create a new end tag token, set its tag name to the empty
				// string, reconsume in RAWTEXT end tag name.
				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:
				// Whitespace (tab, LF, FF, 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 (isAsciiWhitespace(cc)) {
					tagNameEnd = pos;
					if (
						rangeAsciiCaseInsensitiveMatch(
							input,
							tagNameStart,
							tagNameEnd,
							lastOpenTagName
						)
					) {
						flushText(tagStart);
						state = STATE_BEFORE_ATTRIBUTE_NAME;
						pos++;
					} else {
						state = STATE_RAWTEXT;
					}
				} else if (cc === CC_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 (
						rangeAsciiCaseInsensitiveMatch(
							input,
							tagNameStart,
							tagNameEnd,
							lastOpenTagName
						)
					) {
						flushText(tagStart);
						state = STATE_SELF_CLOSING_START_TAG;
						pos++;
					} else {
						state = STATE_RAWTEXT;
					}
				} else if (cc === CC_GREATER_THAN) {
					// `>`: If the current end tag token is an appropriate end tag token, then
					// switch to the data state and emit the tag token. Otherwise, treat it as
					// per the "anything else" entry below.
					tagNameEnd = pos;
					if (
						rangeAsciiCaseInsensitiveMatch(
							input,
							tagNameStart,
							tagNameEnd,
							lastOpenTagName
						)
					) {
						flushText(tagStart);
						state = STATE_DATA;
						pos = emitCloseTag(pos + 1);
					} else {
						state = STATE_RAWTEXT;
					}
				} else if (isAsciiAlpha(cc)) {
					// ASCII alpha: append it lowercased to the tag name and as written to
					// the temporary buffer. Fused, so the whole run is one dispatch.
					pos++;
					while (pos < len && isAsciiAlpha(input.charCodeAt(pos))) pos++;
				} else {
					// Anything else: emit `<`, `/` and the temporary buffer in order,
					// then reconsume in RAWTEXT.
					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 (same memoized
					// `indexOf` scheme as the data state) — batches only this
					// state's "anything else" arc; `<` / NUL keep their own.
					pos++;
					if (nextLt < pos) {
						nextLt = input.indexOf("<", pos);
						if (nextLt === -1) nextLt = len;
					}
					if (nextNul < pos) {
						nextNul = input.indexOf("\0", pos);
						if (nextNul === -1) nextNul = len;
					}
					pos = Math.min(nextLt, nextNul);
					// Fused: take this state's `<` arc without another dispatch.
					if (pos === nextLt && pos < len) {
						tagStart = pos;
						state = STATE_SCRIPT_DATA_LESS_THAN_SIGN;
						pos++;
					}
				}
				break;

			// https://html.spec.whatwg.org/multipage/parsing.html#script-data-less-than-sign-state
			case STATE_SCRIPT_DATA_LESS_THAN_SIGN:
				// `/`: switch to script data end tag open. (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:
				// ASCII alpha: Create a new end tag token, set its tag name to the empty
				// string, reconsume in script data end tag name.
				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:
				// Whitespace (tab, LF, FF, 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 (isAsciiWhitespace(cc)) {
					tagNameEnd = pos;
					if (
						rangeAsciiCaseInsensitiveMatch(
							input,
							tagNameStart,
							tagNameEnd,
							lastOpenTagName
						)
					) {
						flushText(tagStart);
						state = STATE_BEFORE_ATTRIBUTE_NAME;
						pos++;
					} else {
						state = STATE_SCRIPT_DATA;
					}
				} else if (cc === CC_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 (
						rangeAsciiCaseInsensitiveMatch(
							input,
							tagNameStart,
							tagNameEnd,
							lastOpenTagName
						)
					) {
						flushText(tagStart);
						state = STATE_SELF_CLOSING_START_TAG;
						pos++;
					} else {
						state = STATE_SCRIPT_DATA;
					}
				} else if (cc === CC_GREATER_THAN) {
					// `>`: If the current end tag token is an appropriate end tag token, then
					// switch to the data state and emit the tag token. Otherwise, treat it as
					// per the "anything else" entry below.
					tagNameEnd = pos;
					if (
						rangeAsciiCaseInsensitiveMatch(
							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
					// Fused: consume the whole alpha run in one dispatch.
					pos++;
					while (pos < len && isAsciiAlpha(input.charCodeAt(pos))) pos++;
				} else {
					// Anything else: emit `<`, `/` and the temporary buffer in order,
					// then reconsume in script data.
					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:
				// `-`: switch to script data escape start dash. 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:
				// `-`: switch to script data escaped dash dash. 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:
				// `-`: switch to script data escaped dash. 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");
					}
					// Fast-forward ordinary escaped script text without re-entering
					// the state switch; stop on the significant code points above.
					pos++;
					while (pos < len) {
						const c2 = input.charCodeAt(pos);
						if (
							c2 === CC_HYPHEN_MINUS ||
							c2 === CC_LESS_THAN ||
							c2 === CC_NULL
						) {
							break;
						}
						pos++;
					}
				}
				break;

			// https://html.spec.whatwg.org/multipage/parsing.html#script-data-escaped-dash-state
			case STATE_SCRIPT_DATA_ESCAPED_DASH:
				// `-`: switch to script data escaped dash dash. 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:
				// `/`: switch to script data escaped end tag open. (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 script data double escape
					// start.
					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:
				// ASCII alpha: Create a new end tag token, set its tag name to the empty
				// string, reconsume in script data escaped end tag name.
				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:
				// Whitespace (tab, LF, FF, 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 (isAsciiWhitespace(cc)) {
					tagNameEnd = pos;
					if (
						rangeAsciiCaseInsensitiveMatch(
							input,
							tagNameStart,
							tagNameEnd,
							lastOpenTagName
						)
					) {
						flushText(tagStart);
						state = STATE_BEFORE_ATTRIBUTE_NAME;
						pos++;
					} else {
						state = STATE_SCRIPT_DATA_ESCAPED;
					}
				} else if (cc === CC_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 (
						rangeAsciiCaseInsensitiveMatch(
							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) {
					// `>`: If the current end tag token is an appropriate end tag token, then
					// switch to the data state and emit the tag token. Otherwise, treat it as
					// per the "anything else" entry below.
					tagNameEnd = pos;
					if (
						rangeAsciiCaseInsensitiveMatch(
							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
					// Fused: consume the whole alpha run in one dispatch.
					pos++;
					while (pos < len && isAsciiAlpha(input.charCodeAt(pos))) pos++;
				} else {
					// Anything else: emit `<`, `/` and the temporary buffer in order,
					// then reconsume in script data escaped.
					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:
				// Whitespace, `/` or `>`: switch to script data double escaped where the
				// temporary buffer is "script", otherwise to script data escaped, and
				// emit the input character.
				if (
					isAsciiWhitespace(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 while the
					// lowercased char is the expected one, otherwise snap to the
					// sentinel so a later char cannot revive the match.
					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:
				// `-`: switch to script data double escaped dash. 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");
					}
					// Fast-forward ordinary double-escaped script text without re-entering
					// the state switch; stop on the significant code points above.
					pos++;
					while (pos < len) {
						const c2 = input.charCodeAt(pos);
						if (
							c2 === CC_HYPHEN_MINUS ||
							c2 === CC_LESS_THAN ||
							c2 === CC_NULL
						) {
							break;
						}
						pos++;
					}
				}
				break;

			// https://html.spec.whatwg.org/multipage/parsing.html#script-data-double-escaped-dash-state
			case STATE_SCRIPT_DATA_DOUBLE_ESCAPED_DASH:
				// `-`: switch to script data double escaped dash dash. 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:
				// `/`: Set the temporary buffer to the empty string, switch to script data
				// double escape end. 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:
				// Whitespace, `/` or `>`: switch to script data escaped where the
				// temporary buffer is "script", otherwise to script data double escaped,
				// and emit the input character.
				if (
					isAsciiWhitespace(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 while the
					// lowercased char is the expected one, otherwise snap to the
					// sentinel so a later char cannot revive the match.
					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) in one native scan.
					pos++;
					const nul = input.indexOf("\0", pos);
					pos = nul === -1 ? len : nul;
				}
				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) {
					// `#`: Append the input character to the temporary buffer. Set the
					// character reference code to zero, switch to numeric character reference.
					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 longest run naming a character reference: measure the
				// alphanumeric run, capped at MAX_ENTITY_NAME_LEN - 1 as the `;` is
				// separate, then walk it longest-first for the first prefix in the table.
				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;
				// Try the full run with its trailing `;` first — the overwhelmingly
				// common case (`&amp;`, `&nbsp;`, …) then needs exactly one slice.
				if (hasSemicolon && runLen > 0) {
					const withSemicolon = input.slice(pos, pos + runLen + 1);
					if (HTML_ENTITIES[withSemicolon] !== undefined) {
						namedEntityConsumed = runLen + 1;
						matchedWithSemicolon = true;
					}
				}
				if (namedEntityConsumed === 0) {
					// Slice the candidate run once; prefixes come from this short
					// string instead of re-slicing the 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);
						if (HTML_ENTITIES[bare] !== undefined) {
							namedEntityConsumed = n;
							break;
						}
					}
				}
				if (namedEntityConsumed > 0) {
					// A legacy match without its `;` is a
					// missing-semicolon-after-character-reference parse error, except
					// where the spec's attribute rule silences it.
					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: append the input character to the attribute's
					// value where the reference was consumed in one, otherwise emit it.
					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 / U+0058: Append the input character to the temporary buffer,
					// switch to hexadecimal character reference start.
					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:
				// ASCII hex digit: reconsume in hexadecimal character reference.
				// Anything else: absence-of-digits-in-numeric-character-reference parse
				// error, flush the consumed code points and reconsume in the return.
				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:
				// 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 return.
				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 hex digit: multiply the reference code by 16 and add the
					// input character's value. Accumulation stops past the Unicode range
					// so it cannot overflow, and still flags as outside-range.
					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: missing-semicolon-after-character-reference parse error,
					// reconsume in numeric character reference end.
					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 reference code by 10 and add the input
					// character's value. Accumulation stops past the Unicode range so it
					// cannot overflow, and 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: missing-semicolon-after-character-reference parse error,
					// reconsume in numeric character reference end.
					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++;
		}
	}

	// WHY: EOF in a non-data state, per the WHATWG spec. An in-progress comment,
	// doctype, cdata or tag emits its partial range and an `eof-in-X` parse
	// error, at severity `"error"` because that range is missing its terminator.
	// A data / `<` / `</` / `<!` input emits `eof-before-tag-name` and falls
	// through to flush the pending text, which still holds the lone `<`. Inside a
	// character reference, the end-of-reference processing runs first — the
	// numeric end states never do at EOF — then the return state's branch 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
	) {
		// A deliberate deviation from the spec's per-character emission
		// model — the in-progress tag is emitted as its offset range up to EOF
		// rather than dropped, so the consumer still sees it.
		reportError("eof-in-tag", len, len, "error");
		// If we hit EOF mid-attribute-name, the name runs to EOF. Set
		// attributeNameEnd here so the emitted attribute range is valid.
		if (state === STATE_ATTRIBUTE_NAME && attributeNameStart !== -1) {
			attributeNameEnd = len;
		}
		if (attributeNameStart !== -1) emitAttribute(len);
		// At EOF before the tag-name end was recorded the name runs to EOF, and
		// `tagNameEnd` may carry over from a previous tag — so it is reset
		// whenever missing or stale, below `tagNameStart`.
		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");
			// The bogus comment this becomes has nothing left to consume.
			commentDataStart = len;
			commentDataEnd = len;
		} else if (state !== STATE_BOGUS_COMMENT) {
			reportError("eof-in-comment", len, len, "error");
		}
		if (callbacks.comment !== undefined) {
			pos = callbacks.comment(
				input,
				commentStart,
				len,
				commentDataStart,
				commentDataEnd
			);
		}
	} else if (state >= STATE_CDATA_SECTION && state <= STATE_CDATA_SECTION_END) {
		reportError("eof-in-cdata", len, len, "error");
		if (callbacks.comment !== undefined) {
			// No `]]>` closed it, so everything after `<![CDATA[` is content.
			pos = callbacks.comment(input, commentStart, len, commentDataStart, 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");
			doctypeForceQuirks = true;
		}
		if (callbacks.doctype !== undefined) {
			pos = callbacks.doctype(
				input,
				commentStart,
				len,
				doctypeNameStart,
				doctypeNameEnd,
				doctypePublicStart,
				doctypePublicEnd,
				doctypeSystemStart,
				doctypeSystemEnd,
				doctypeForceQuirks
			);
		}
	} 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: an
			// eof-in-script-html-comment-like-text parse error. The less-than-sign
			// and double-escape-end states reconsume into it at EOF, per spec.
			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;
};

/**
 * Whether a code unit is XML's `S` production (§2.3): space, tab, LF or CR.
 * @param {number} cc code unit
 * @returns {boolean} true for XML whitespace
 */
const isXmlWhitespace = (cc) =>
	cc === CC_SPACE || cc === CC_TAB || cc === CC_LF || cc === CC_CR;

/**
 * Where a name ends: the first code unit XML's `Name` cannot hold and the markup
 * around it uses as a delimiter. Lenient on purpose, as the HTML tokenizer is.
 * @param {string} input source
 * @param {number} pos where the name starts
 * @returns {number} offset past the name
 */
const _xmlNameEnd = (input, pos) => {
	const len = input.length;
	while (pos < len) {
		const cc = input.charCodeAt(pos);
		if (
			isXmlWhitespace(cc) ||
			cc === CC_GREATER_THAN ||
			cc === CC_SOLIDUS ||
			cc === CC_EQUALS ||
			cc === CC_LESS_THAN ||
			cc === CC_QUOTATION_MARK ||
			cc === CC_APOSTROPHE ||
			cc === CC_LEFT_SQUARE_BRACKET ||
			cc === CC_QUESTION_MARK
		) {
			break;
		}
		pos++;
	}
	return pos;
};

/**
 * Whether a code unit may open a name (§2.3 `NameStartChar`), read leniently:
 * anything but ASCII that cannot, so a `<` before a digit or a space is text.
 * @param {number} cc code unit
 * @returns {boolean} true when a tag name can start with it
 */
const _isXmlNameStart = (cc) =>
	cc >= 0x80 ||
	(cc >= 0x41 && cc <= 0x5a) ||
	(cc >= 0x61 && cc <= 0x7a) ||
	cc === CC_LOW_LINE ||
	cc === 0x3a;

/**
 * @param {string} input source
 * @param {number} pos offset
 * @returns {number} offset of the first non-whitespace code unit at or past it
 */
const _skipXmlWhitespace = (input, pos) => {
	const len = input.length;
	while (pos < len && isXmlWhitespace(input.charCodeAt(pos))) pos++;
	return pos;
};

/**
 * Where a DOCTYPE's internal subset ends: its `]`, skipping what a declaration
 * may quote and the comments and processing instructions it may hold.
 * @param {string} input source
 * @param {number} pos offset past the `[`
 * @returns {number} offset of the closing `]`, or the input length when missing
 */
const _xmlInternalSubsetEnd = (input, pos) => {
	const len = input.length;
	while (pos < len) {
		const cc = input.charCodeAt(pos);
		if (cc === CC_RIGHT_SQUARE_BRACKET) return pos;
		if (cc === CC_QUOTATION_MARK || cc === CC_APOSTROPHE) {
			const close = input.indexOf(cc === CC_APOSTROPHE ? "'" : '"', pos + 1);
			pos = close === -1 ? len : close + 1;
		} else if (input.startsWith("<!--", pos)) {
			const close = input.indexOf("-->", pos + 4);
			pos = close === -1 ? len : close + 3;
		} else if (input.startsWith("<?", pos)) {
			const close = input.indexOf("?>", pos + 2);
			pos = close === -1 ? len : close + 2;
		} else {
			pos++;
		}
	}
	return len;
};

/**
 * Read a quoted DOCTYPE identifier.
 * @param {string} input source
 * @param {number} pos offset of the opening quote
 * @returns {number} offset of the closing quote, or -1 when there is no quote at `pos`
 */
const _xmlQuotedEnd = (input, pos) => {
	const cc = input.charCodeAt(pos);
	if (cc !== CC_QUOTATION_MARK && cc !== CC_APOSTROPHE) return -1;
	const close = input.indexOf(cc === CC_APOSTROPHE ? "'" : '"', pos + 1);
	return close === -1 ? input.length : close;
};

/**
 * Tokenize XML 1.0 markup (§2.4–§4.1) into the callbacks `tokenize` drives, so
 * one tree builder serves both. Never throws: a document that is not well-formed
 * reads on the way a lenient processor would, reporting nothing.
 * @param {string} input input string
 * @param {number} pos current position
 * @param {HtmlTokenCallbacks} callbacks callbacks
 * @returns {number} final position
 */
const tokenizeXml = (input, pos = 0, callbacks = {}) => {
	const len = input.length;
	if (callbacks.parseError !== undefined && input.includes("\0", pos)) {
		callbacks.parseError(
			input,
			"unexpected-null-character",
			pos,
			pos,
			"warning"
		);
	}
	let textStart = pos;
	while (pos < len) {
		const lt = input.indexOf("<", pos);
		if (lt === -1) break;
		const next = input.charCodeAt(lt + 1);
		let end = -1;
		if (next === CC_EXCLAMATION_MARK) {
			if (input.startsWith("--", lt + 2)) {
				const close = input.indexOf("-->", lt + 4);
				const dataEnd = close === -1 ? len : close;
				end = close === -1 ? len : close + 3;
				if (textStart < lt && callbacks.text !== undefined) {
					callbacks.text(input, textStart, lt);
				}
				if (callbacks.comment !== undefined) {
					callbacks.comment(input, lt, end, lt + 4, dataEnd);
				}
			} else if (input.startsWith("[CDATA[", lt + 2)) {
				const close = input.indexOf("]]>", lt + 9);
				const dataEnd = close === -1 ? len : close;
				end = close === -1 ? len : close + 3;
				if (textStart < lt && callbacks.text !== undefined) {
					callbacks.text(input, textStart, lt);
				}
				if (callbacks.comment !== undefined) {
					callbacks.comment(input, lt, end, lt + 9, dataEnd);
				}
			} else if (
				rangeAsciiCaseInsensitiveMatch(input, lt + 2, lt + 9, "doctype")
			) {
				end = _tokenizeXmlDoctype(input, lt, callbacks, textStart);
			} else {
				// A declaration outside the DTD is not XML: read it as a comment, the
				// way the HTML tokenizer reads a bogus one.
				const close = input.indexOf(">", lt + 2);
				end = close === -1 ? len : close + 1;
				if (textStart < lt && callbacks.text !== undefined) {
					callbacks.text(input, textStart, lt);
				}
				if (callbacks.comment !== undefined) {
					callbacks.comment(input, lt, end, lt + 2, close === -1 ? len : close);
				}
			}
		} else if (next === CC_QUESTION_MARK) {
			const close = input.indexOf("?>", lt + 2);
			end = close === -1 ? len : close + 2;
			if (textStart < lt && callbacks.text !== undefined) {
				callbacks.text(input, textStart, lt);
			}
			if (close !== -1 && callbacks.comment !== undefined) {
				callbacks.comment(input, lt, end, lt + 2, close);
			}
		} else if (next === CC_SOLIDUS) {
			const nameStart = lt + 2;
			const nameEnd = _xmlNameEnd(input, nameStart);
			if (nameEnd !== nameStart) {
				const close = input.indexOf(">", nameEnd);
				end = close === -1 ? len : close + 1;
				if (textStart < lt && callbacks.text !== undefined) {
					callbacks.text(input, textStart, lt);
				}
				if (close !== -1 && callbacks.closeTag !== undefined) {
					callbacks.closeTag(input, lt, end, nameStart, nameEnd);
				}
			}
		} else if (_isXmlNameStart(next)) {
			const nameStart = lt + 1;
			const nameEnd = _xmlNameEnd(input, nameStart);
			if (nameEnd !== nameStart) {
				if (textStart < lt && callbacks.text !== undefined) {
					callbacks.text(input, textStart, lt);
				}
				end = _tokenizeXmlStartTag(input, lt, nameStart, nameEnd, callbacks);
			}
		}
		if (end === -1) {
			// A `<` that opens nothing stays in the text, as it does in HTML.
			pos = lt + 1;
		} else {
			pos = end;
			textStart = end;
		}
	}
	if (textStart < len && callbacks.text !== undefined) {
		callbacks.text(input, textStart, len);
	}
	return len;
};

/**
 * @param {string} input source
 * @param {number} start offset of the `<`
 * @param {number} nameStart offset of the tag name
 * @param {number} nameEnd offset past the tag name
 * @param {HtmlTokenCallbacks} callbacks callbacks
 * @returns {number} offset past the tag
 */
const _tokenizeXmlStartTag = (input, start, nameStart, nameEnd, callbacks) => {
	const len = input.length;
	let pos = nameEnd;
	while (true) {
		pos = _skipXmlWhitespace(input, pos);
		if (pos >= len) {
			if (callbacks.parseError !== undefined) {
				callbacks.parseError(input, "eof-in-tag", len, len, "error");
			}
			if (callbacks.openTag !== undefined) {
				callbacks.openTag(input, start, len, nameStart, nameEnd, false);
			}
			return len;
		}
		const cc = input.charCodeAt(pos);
		if (cc === CC_GREATER_THAN) {
			if (callbacks.openTag !== undefined) {
				callbacks.openTag(input, start, pos + 1, nameStart, nameEnd, false);
			}
			return pos + 1;
		}
		// A `<` cannot sit in a tag, so the next one starts markup of its own.
		if (cc === CC_LESS_THAN) {
			if (callbacks.openTag !== undefined) {
				callbacks.openTag(input, start, pos, nameStart, nameEnd, false);
			}
			return pos;
		}
		if (cc === CC_SOLIDUS && input.charCodeAt(pos + 1) === CC_GREATER_THAN) {
			if (callbacks.openTag !== undefined) {
				callbacks.openTag(input, start, pos + 2, nameStart, nameEnd, true);
			}
			return pos + 2;
		}
		const attributeStart = pos;
		const attributeEnd = _xmlNameEnd(input, pos);
		// A stray `/`, quote or `=` is skipped so the tag still ends where it should.
		if (attributeEnd === attributeStart) {
			pos++;
			continue;
		}
		pos = _skipXmlWhitespace(input, attributeEnd);
		let valueStart = -1;
		let valueEnd = -1;
		let quoteType = QUOTE_NONE;
		if (input.charCodeAt(pos) === CC_EQUALS) {
			pos = _skipXmlWhitespace(input, pos + 1);
			const quote = input.charCodeAt(pos);
			if (quote === CC_QUOTATION_MARK || quote === CC_APOSTROPHE) {
				quoteType = quote === CC_QUOTATION_MARK ? QUOTE_DOUBLE : QUOTE_SINGLE;
				valueStart = pos + 1;
				const close = input.indexOf(
					quote === CC_QUOTATION_MARK ? '"' : "'",
					valueStart
				);
				valueEnd = close === -1 ? len : close;
				pos = close === -1 ? len : close + 1;
			} else {
				valueStart = pos;
				while (
					pos < len &&
					!isXmlWhitespace(input.charCodeAt(pos)) &&
					input.charCodeAt(pos) !== CC_GREATER_THAN
				) {
					pos++;
				}
				valueEnd = pos;
			}
		}
		if (callbacks.attribute !== undefined) {
			callbacks.attribute(
				input,
				attributeStart,
				attributeEnd,
				valueStart,
				valueEnd,
				quoteType
			);
		}
	}
};

/**
 * @param {string} input source
 * @param {number} start offset of the `<`
 * @param {HtmlTokenCallbacks} callbacks callbacks
 * @param {number} textStart start of the text still to flush before the declaration
 * @returns {number} offset past the declaration
 */
const _tokenizeXmlDoctype = (input, start, callbacks, textStart) => {
	const len = input.length;
	let pos = _skipXmlWhitespace(input, start + 9);
	const nameStart = pos;
	const nameEnd = _xmlNameEnd(input, pos);
	pos = _skipXmlWhitespace(input, nameEnd);
	let publicStart = -1;
	let publicEnd = -1;
	let systemStart = -1;
	let systemEnd = -1;
	if (input.startsWith("PUBLIC", pos)) {
		pos = _skipXmlWhitespace(input, pos + 6);
		const close = _xmlQuotedEnd(input, pos);
		if (close !== -1) {
			publicStart = pos + 1;
			publicEnd = close;
			pos = _skipXmlWhitespace(input, close + 1);
		}
	} else if (input.startsWith("SYSTEM", pos)) {
		pos = _skipXmlWhitespace(input, pos + 6);
	}
	const systemClose = _xmlQuotedEnd(input, pos);
	if (systemClose !== -1) {
		systemStart = pos + 1;
		systemEnd = systemClose;
		pos = _skipXmlWhitespace(input, systemClose + 1);
	}
	let subsetStart = -1;
	let subsetEnd = -1;
	if (input.charCodeAt(pos) === CC_LEFT_SQUARE_BRACKET) {
		subsetStart = pos + 1;
		subsetEnd = _xmlInternalSubsetEnd(input, subsetStart);
		pos = subsetEnd + 1;
	}
	const close = input.indexOf(">", pos);
	const end = close === -1 ? len : close + 1;
	if (textStart < start && callbacks.text !== undefined) {
		callbacks.text(input, textStart, start);
	}
	if (callbacks.doctype !== undefined) {
		callbacks.doctype(
			input,
			start,
			end,
			nameEnd === nameStart ? -1 : nameStart,
			nameEnd === nameStart ? -1 : nameEnd,
			publicStart,
			publicEnd,
			systemStart,
			systemEnd,
			false,
			subsetStart,
			subsetEnd
		);
	}
	return end;
};

// WHATWG numeric-character-reference-end remap for 0x80-0x9F: these C1 control
// code points decode to the matching Windows-1252 glyph, with a parse error,
// rather than to the control character itself.
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) => {
	// WHY: numeric-character-reference-end-state maps 0x00, anything above
	// 0x10FFFF and the surrogates to U+FFFD, 0x80-0x9F through the table above,
	// and everything else — noncharacters and C0 controls included — to itself.
	// Its parse-error classes are not raised here: the scanner has already run.
	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);
};

/**
 * 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
 */
/**
 * Decode a named character reference spanning `[start, end)` of `str` (the
 * `&` at `start`, the optional `;` included). Slices the name once; WHATWG
 * longest-prefix and consumed-as-part-of-an-attribute semantics as before.
 * @param {string} str the raw string
 * @param {number} start offset of the reference's `&`
 * @param {number} end offset just past the reference (past the `;` if present)
 * @param {number} nextCharCode char code following the reference (NaN at the end)
 * @param {boolean=} isAttribute true when the reference came from an attribute value
 * @returns {string | undefined} decoded text, or `undefined` when it stays literal
 */
const decodeNamedReference = (str, start, end, nextCharCode, isAttribute) => {
	// Mirrors the old `match.slice(1)`: the name keeps its trailing `;`.
	const name = str.slice(start + 1, end);
	const matchEndsWithSemi = name.charCodeAt(name.length - 1) === 0x3b;

	// Attribute-context guard: a match not ending in `;` whose next source
	// character is `=` or alphanumeric is flushed literally rather than decoded.
	// The greedy scan absorbed the alphanumerics, so only `=` can reach here.
	if (isAttribute && !matchEndsWithSemi && nextCharCode === 0x3d /* = */) {
		return undefined;
	}

	// Fast path: the scan usually captures exactly one entity (`&amp;`,
	// `&lt;`, `&nbsp;`, ...), so the whole `name` is the match.
	if (name.length <= MAX_ENTITY_NAME_LEN) {
		const full = HTML_ENTITIES[name];
		if (full !== undefined) return full;
	}

	// Capping the longest-prefix search at MAX_ENTITY_NAME_LEN keeps `&` plus
	// thousands of alphanumerics linear-time; nothing past it can match, so it
	// rides along in `name.slice(i)`. The full length was handled just above.
	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.
			if (
				isAttribute &&
				i < name.length &&
				prefix.charCodeAt(prefix.length - 1) !== 0x3b
			) {
				return undefined;
			}
			return HTML_ENTITIES[prefix] + name.slice(i);
		}
	}
	return undefined;
};

/** @typedef {{ text: string, map: number[] | undefined }} DecodedEntitiesWithMap */

/**
 * 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.
 *
 * With `withMap` the result also carries 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`).
 * @overload
 * @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
 */
/**
 * @overload
 * @param {string} str the raw string from the token slice
 * @param {boolean | undefined} isAttribute true if `str` came from an attribute value
 * @param {true} withMap also build the decoded-to-raw boundary map
 * @returns {DecodedEntitiesWithMap} decoded text and offset map
 */
/**
 * @param {string} str the raw string from the token slice
 * @param {boolean=} isAttribute true if `str` came from an attribute value
 * @param {boolean=} withMap also build the decoded-to-raw boundary map
 * @returns {string | DecodedEntitiesWithMap} decoded string (with map when `withMap`)
 */
const decodeEntities = (str, isAttribute, withMap) => {
	// WHY: a hand-rolled scan of the three reference forms — `&#x<hex>+`,
	// `&#<dec>+`, `&<alpha><alnum>*`, kept apart so `&#65b` does not eat the `b`
	// as hex. Entity-dense text then pays no regex machinery and no per-match
	// object, and a string whose references all stay literal is returned as it
	// came. Map bookkeeping is gated on `withMap` at the rare per-reference
	// points, so the dominant no-map mode pays nothing for sharing this code.
	let amp = str.indexOf("&");
	if (amp === -1) return withMap ? { text: str, map: undefined } : str;
	let out = "";
	let last = 0;
	/** @type {number[] | undefined} */
	let map;
	do {
		// `end` stays 0 where the `&` starts no well-formed reference. A numeric
		// reference accumulates during the digit scan, saturating above the
		// code-point range since every overflow decodes alike, so it needs no slice.
		let end = 0;
		let code = -1;
		let cc = str.charCodeAt(amp + 1);
		if (cc === 0x23 /* # */) {
			cc = str.charCodeAt(amp + 2);
			let p;
			if (cc === 0x78 /* x */ || cc === 0x58 /* X */) {
				code = 0;
				p = amp + 3;
				while (isAsciiHexDigit((cc = str.charCodeAt(p)))) {
					code = code * 16 + (cc <= 0x39 ? cc - 0x30 : (cc | 0x20) - 0x57);
					if (code > 0x10ffff) code = 0x110000;
					p++;
				}
				if (p > amp + 3) end = p;
			} else {
				code = 0;
				p = amp + 2;
				while (isAsciiDigit((cc = str.charCodeAt(p)))) {
					code = code * 10 + (cc - 0x30);
					if (code > 0x10ffff) code = 0x110000;
					p++;
				}
				if (p > amp + 2) end = p;
			}
		} else if (isAsciiAlpha(cc)) {
			code = -1;
			let p = amp + 2;
			while (isAsciiAlphanumeric(str.charCodeAt(p))) p++;
			end = p;
		}
		if (end !== 0) {
			if (str.charCodeAt(end) === 0x3b /* ; */) end++;
			// Numeric references always decode; named ones may stay literal.
			const decoded =
				code >= 0
					? decodeNumericReference(code)
					: decodeNamedReference(
							str,
							amp,
							end,
							str.charCodeAt(end),
							isAttribute
						);
			if (decoded !== undefined) {
				if (withMap) map = _mapReference(map, last, amp, decoded.length);
				out += str.slice(last, amp);
				out += decoded;
				last = end;
			}
			amp = str.indexOf("&", end);
		} else {
			amp = str.indexOf("&", amp + 1);
		}
	} while (amp !== -1);
	if (last === 0) return withMap ? { text: str, map: undefined } : str;
	const text = out + str.slice(last);
	return withMap
		? _finishMap(text, /** @type {number[]} */ (map), last, str.length)
		: text;
};

// Map-mode bookkeeping for `decodeEntities`, kept out of the scanner body so
// the dominant no-map mode stays small enough to inline.
/**
 * @param {number[] | undefined} map boundary map so far
 * @param {number} last raw offset the literal run starts at
 * @param {number} amp raw offset of the decoded reference
 * @param {number} decodedLength length of the decoded text
 * @returns {number[]} the map, with the literal run's and reference's boundaries appended
 */
const _mapReference = (map, last, amp, decodedLength) => {
	if (map === undefined) map = [];
	for (let r = last; r < amp; r++) map.push(r);
	for (let i = 0; i < decodedLength; i++) map.push(amp);
	return map;
};

/**
 * @param {string} text decoded text
 * @param {number[]} map boundary map so far
 * @param {number} last raw offset of the trailing literal run
 * @param {number} rawLength raw string length
 * @returns {DecodedEntitiesWithMap} the finished result
 */
const _finishMap = (text, map, last, rawLength) => {
	for (let r = last; r < rawLength; r++) map.push(r);
	map.push(rawLength);
	return { text, map };
};

/**
 * Escape a string per the WHATWG "escape a string" algorithm
 * (https://html.spec.whatwg.org/multipage/parsing.html#escapingString) in
 * attribute mode: `&`, U+00A0 and whichever quote delimits the value \u2014 the
 * other one stands literal, which is what makes `alt='say "hi"'` legal. CR/LF
 * are additionally encoded as numeric references so the result stays
 * single-line (usable in a `data:` URI). Single pass, returns the input
 * unchanged when nothing needs escaping.
 * @param {string} s string to escape
 * @param {number=} delimiter the quote the value is written in, `"` by default
 * @param {boolean=} minimal leave an `&` no reference can start after it bare,
 * and U+00A0 literal, as `_escapeTextContent` does for a text node
 * @returns {string} HTML attribute-safe string
 */
const escapeAttribute = (s, delimiter, minimal) => {
	const quote = delimiter === CC_APOSTROPHE ? CC_APOSTROPHE : CC_QUOTATION_MARK;
	// fast path: native indexOf scans beat any per-character loop
	if (
		!s.includes("&") &&
		!s.includes(quote === CC_APOSTROPHE ? "'" : '"') &&
		(minimal === true || !s.includes("\u00A0")) &&
		!s.includes("\n") &&
		!s.includes("\r")
	) {
		return s;
	}
	let out = "";
	let last = 0;
	for (let i = 0; i < s.length; i++) {
		let rep;
		switch (s.charCodeAt(i)) {
			case CC_AMPERSAND: {
				// §13.2.5.41: a `&` the parser cannot continue into a reference is
				// data — asked of the table, since `&c` names none. The minifier
				// writes it bare; generated markup escapes it.
				if (minimal === true && !_startsCharacterReference(s, i, true)) {
					continue;
				}
				rep = "&amp;";
				break;
			}
			// Only the delimiter is escaped, so the compare costs a quote, not a
			// character: everything ordinary still falls straight through.
			case CC_QUOTATION_MARK:
				if (quote !== CC_QUOTATION_MARK) continue;
				rep = "&quot;";
				break;
			case CC_APOSTROPHE:
				if (quote !== CC_APOSTROPHE) continue;
				rep = "&#39;";
				break;
			case CC_NO_BREAK_SPACE:
				// WHY: the minifier keeps it literal, so that a value's own spelling
				// does not decide the output: a source writing the character and one
				// writing `&nbsp;` are the same attribute and print as one. Generated
				// markup still escapes it, matching `escapeText` against
				// `_escapeTextContent` on the text side. Both spellings are the same
				// DOM, and the literal is the shorter of the two.
				if (minimal === true) continue;
				rep = "&nbsp;";
				break;
			case CC_LF:
				rep = "&#10;";
				break;
			case CC_CR:
				rep = "&#13;";
				break;
			default:
				continue;
		}
		out += s.slice(last, i) + rep;
		last = i + 1;
	}
	return out + s.slice(last);
};

/**
 * Escape a string per the WHATWG "escape a string" algorithm in text mode:
 * `&`, U+00A0, `<` and `>`. CR/LF are additionally encoded as numeric
 * references so the result stays single-line (usable in a `data:` URI).
 * Single pass, returns the input unchanged when nothing needs escaping.
 * @param {string} s string to escape
 * @returns {string} HTML text-content-safe string
 */
const escapeText = (s) => {
	// fast path: native indexOf scans beat any per-character loop
	if (
		!s.includes("&") &&
		!s.includes("<") &&
		!s.includes(">") &&
		!s.includes("\u00A0") &&
		!s.includes("\n") &&
		!s.includes("\r")
	) {
		return s;
	}
	let out = "";
	let last = 0;
	for (let i = 0; i < s.length; i++) {
		let rep;
		switch (s.charCodeAt(i)) {
			case CC_AMPERSAND:
				rep = "&amp;";
				break;
			case CC_LESS_THAN:
				rep = "&lt;";
				break;
			case CC_GREATER_THAN:
				rep = "&gt;";
				break;
			case CC_NO_BREAK_SPACE:
				rep = "&nbsp;";
				break;
			case CC_LF:
				rep = "&#10;";
				break;
			case CC_CR:
				rep = "&#13;";
				break;
			default:
				continue;
		}
		out += s.slice(last, i) + rep;
		last = i + 1;
	}
	return out + s.slice(last);
};

/**
 * @param {string} name meta name
 * @param {string} content meta content
 * @returns {string} meta tag
 */
const metaTag = (name, content) => {
	// og: uses property=; all others (including twitter:) use name=
	const attr = name.startsWith("og:")
		? `property="${escapeAttribute(name)}"`
		: `name="${escapeAttribute(name)}"`;
	return `<meta ${attr} content="${escapeAttribute(content)}">`;
};

/**
 * @param {string | { href: string, target?: string }} base base option
 * @returns {string} base tag
 */
const baseTag = (base) => {
	const href = typeof base === "string" ? base : base.href;
	const targetAttr =
		typeof base === "object" && base.target
			? ` target="${escapeAttribute(base.target)}"`
			: "";
	return `<base href="${escapeAttribute(href)}"${targetAttr}>`;
};

/**
 * Serializes head tags in spec order — charset, base, meta, title — for a page
 * built from scratch; an authored page merges the same options in `parse`.
 * @param {OutputHtmlOptions} opts html options
 * @returns {string} head tags string
 */
const buildHeadTags = (opts) => {
	let out = "";
	const meta = opts.meta;
	if (meta && meta.charset) {
		out += `<meta charset="${escapeAttribute(meta.charset)}">`;
	}
	if (opts.base) out += baseTag(opts.base);
	if (meta) {
		for (const [name, content] of Object.entries(meta)) {
			if (name === "charset") continue;
			out += metaTag(name, content);
		}
	}
	if (opts.title) {
		out += `<title>${escapeText(opts.title)}</title>`;
	}
	return out;
};

// cspell:ignore definitionurl malignmark mglyph selectedcontent megamorphic attributeless rowspan imagesizes novalidate maxlength

// WHATWG HTML tree construction (https://html.spec.whatwg.org/multipage/parsing.html#tree-construction)
// on top of tokenize. 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;
// XML elements do not participate in HTML, MathML or SVG tree construction.
// The fourth value fits in the existing two-bit namespace field.
const NS_XML = 3;

/**
 * An element's namespace (Infra §8): its MathML or SVG `Flag` bit, neither for HTML.
 * @type {{ HTML: 0, MathML: 1, SVG: 2, XML: 3 }}
 */
const Namespace = {
	HTML: NS_HTML,
	MathML: NS_MATHML,
	SVG: NS_SVG,
	XML: NS_XML
};

/**
 * 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.
 * @type {{ Document: 1, DocumentFragment: 2, Element: 3, Text: 4, Comment: 5, Doctype: 6, ProcessingInstruction: 7, Attribute: 8 }}
 */
const NodeType = {
	Document: 1,
	DocumentFragment: 2,
	Element: 3,
	Text: 4,
	Comment: 5,
	Doctype: 6,
	ProcessingInstruction: 7,
	// An element's attribute, as the DOM's `Attr`: read through the path with a
	// negative id (`Field.attributes`), never visited by the walk.
	Attribute: 8
};

/**
 * The parts of a node read with `field`, `fieldCount` and `fieldNamed`, named
 * as the DOM names them. A single-node field reads as a list of at most one.
 * @type {{ attributes: 1, content: 2 }}
 */
const Field = {
	// An element's attributes, a list of `Attribute` nodes.
	attributes: 1,
	// A `<template>`'s content fragment, a single node.
	content: 2
};

/**
 * A contiguous run of attribute ids in the attribute columns — how a start-tag
 * token and an element refer to their attributes. `start` is the first id
 * (`count` 0 = none).
 * @typedef {{ start: number, count: number }} AttributeRun
 */

// Shared frozen empty run for attributeless elements and synthesized tags.
const EMPTY_ATTRS = /** @type {AttributeRun} */ (
	Object.freeze({ start: 0, count: 0 })
);

// WHY: struct-of-arrays AST backend. One node is one integer id
// (`HtmlNodeRef`) into the parallel columns below, so there is no per-node
// object and no per-parent children array: shape lives in the four link
// columns, and the only heap references are the payload and attribute side
// arrays. The columns are module-level and reused across parses — grown, and
// shrunk on release only past `_COLUMN_SHRINK_CAPACITY` — so a steady-state
// parse allocates almost nothing per node, at the price of a tree being valid
// only until the next `parseHtml`. Id 0 is reserved so the links can use it as
// null.
let _nodeCapacity = 0;
let _nodeCount = 0;
// Released columns all share these: growth replaces a column before any write.
const EMPTY_U8 = new Uint8Array(0);
const EMPTY_I32 = new Int32Array(0);
// Highest id `_nodeCount` reached this parse. Streaming rewinds the allocator,
// so `_nodeCount` no longer bounds the ids written, and the release pass has to
// clear up to here or a recycled slot keeps its strings alive.
let _nodeHighWater = 0;

let _nodeStarts = EMPTY_I32;
let _nodeEnds = EMPTY_I32;
/** end offset of an element's opening tag (after `>`) */
let _nodeTagEnds = EMPTY_I32;
/** end offset of an element's tag name */
let _nodeNameEnds = EMPTY_I32;
/**
 * under `skip.text`, a raw-text element's body-end offset (`HtmlAstSkip`); for
 * a `<template>` (`FLAG_HAS_TEMPLATE`) it instead holds the content-fragment ref
 */
let _nodeContentEnds = EMPTY_I32;
let _nodeParents = EMPTY_I32;

let _nodeLastChildren = EMPTY_I32;

/** @type {string[]} tag name / text data / comment data / doctype name */
const _nodeStrings = [];
/** first attribute id of an element's contiguous run */
let _nodeAttrStarts = EMPTY_I32;
/** attribute count of an element's run */
let _nodeAttrCounts = EMPTY_I32;
// The single doctype node's public/system ids (a document inserts at most one
// doctype node — later doctype tokens are ignored — so no column is needed).
/** @type {string | null} */
let _doctypePublicId = null;
/** @type {string | null} */
let _doctypeSystemId = null;
// Where the doctype's identifiers are written, -1 without one.
let _doctypePublicStart = -1;
let _doctypePublicEnd = -1;
let _doctypeSystemStart = -1;
let _doctypeSystemEnd = -1;

const NS_MASK = 3;
const FLAG_SELF_CLOSING = 4;

/**
 * The bits of a node's flags column read with `flag`; an element's namespace is
 * its MathML or SVG bit, neither set for HTML.
 * @type {{ mathml: 1, svg: 2, selfClosing: 4 }}
 */
const Flag = {
	mathml: NS_MATHML,
	svg: NS_SVG,
	selfClosing: FLAG_SELF_CLOSING
};

/**
 * The parts of a node read with `rangeOf` and `textOf`.
 * @type {{ name: 1, value: 2, content: 3, startTag: 4, endTag: 5, publicId: 6, systemId: 7 }}
 */
const Part = {
	// An element's, attribute's, doctype's or processing instruction's name.
	name: 1,
	// An attribute's value, inside its quotes; a text's, comment's or PI's data.
	value: 2,
	// An element's content, between its start and end tags.
	content: 3,
	// An element's start tag, `<` to `>`.
	startTag: 4,
	// An element's end tag, when the source wrote one.
	endTag: 5,
	// A doctype's public identifier, inside its quotes.
	publicId: 6,
	// A doctype's system identifier, inside its quotes.
	systemId: 7
};
// A `<template>` element: `_nodeContentEnds` holds its content-fragment ref
// instead of a raw-text body-end offset (the two never coexist on one node).
const FLAG_HAS_TEMPLATE = 8;
// A `<template shadowrootmode>` kept where it was written, and its host: the
// engine takes it out of the tree, so nothing may move it away from that host.
const FLAG_SHADOW_ROOT = 16;
const FLAG_SHADOW_HOST = 32;
// The element whose content prints from source, and everything still open around
// it. A flag, not a lookup: the walk asks this of every node.
const FLAG_FOSTER_REGION = 128;
// An element that stops a "have an element in scope" walk (§13.2.4.2), decided
// once at creation: an element's namespace and name never change after it.
const FLAG_SCOPE_BOUNDARY = 64;

/**
 * @param {HtmlNodeRef} el an element
 * @returns {boolean} whether a shadow root can be attached to it
 */
const _isShadowHost = (el) => {
	if (_namespaceOf(el) !== NS_HTML) return false;
	const tagName = _tagNameOf(el);
	if (SHADOW_HOSTS.has(tagName)) return true;
	return (
		tagName.includes("-") &&
		tagName.charCodeAt(0) >= 97 &&
		tagName.charCodeAt(0) <= 122 &&
		!RESERVED_ELEMENT_NAMES.has(tagName)
	);
};

/** @type {(el: HtmlNodeRef) => string} */
const _tagNameOf = (el) => _nodeStrings[el];
/** @type {(n: HtmlNodeRef) => string} a text's, comment's or processing instruction's data */
const _dataOf = (n) => _nodeStrings[n];

/**
 * @param {HtmlNodeRef} n node
 * @returns {HtmlNodeRef} its first child (0 = none)
 */
const _firstChild = (n) => _S._nodeFirstChildren[n];

/**
 * The cursor slot for `n`'s children, claimed afresh when it holds another
 * node's or an earlier epoch's reads.
 * @param {HtmlNodeRef} n node
 * @returns {number} the slot
 */
const _cursorSlot = (n) => {
	const slot = n & (_CURSOR_SLOTS - 1);
	if (_cursorEpochs[slot] !== _cursorEpoch || _cursorParents[slot] !== n) {
		_cursorEpochs[slot] = _cursorEpoch;
		_cursorParents[slot] = n;
		_cursorIndexes[slot] = 0;
		_cursorChildren[slot] = _S._nodeFirstChildren[n];
		_cursorCounts[slot] = -1;
	}
	return slot;
};
/**
 * @param {HtmlNodeRef} n node
 * @returns {HtmlNodeRef} its next sibling (0 = none)
 */
const _nextSibling = (n) => _S._nodeNextSiblings[n];
/**
 * @param {HtmlNodeRef} n node
 * @returns {HtmlNodeRef} its parent (0 = none; a `<template>`'s content links to its fragment)
 */
const _parentOf = (n) => _nodeParents[n];
// A `<template>`'s content-fragment ref (in the shared `_nodeContentEnds` slot),
// or 0 when `el` is not a template — the tree builder's spec "template contents".
/** @type {(el: HtmlNodeRef) => HtmlDocumentFragment} */
const _templateContentOf = (el) =>
	(_S._nodeFlags[el] & FLAG_HAS_TEMPLATE) !== 0 ? _nodeContentEnds[el] : 0;
/** @type {(el: HtmlNodeRef) => number} */
const _namespaceOf = (el) => _S._nodeFlags[el] & NS_MASK;

// === Attribute columns ===

// One attribute is one integer id here, and an element holds a contiguous run.
// Values are read from the source by offset and the serializer name from the
// adjusted name, so only the name pointer is retained per attribute.
let _attrCapacity = 0;
let _attrCount = 0;
// Elements a repeated `<html>` / `<body>` start tag merged attributes onto
// (§13.2.6.4.4, §13.2.6.4.7). The merged names are not in the element's source
// open tag, so the printer rebuilds that tag instead of echoing it.
const _mergedAttrNodes = new Set();
// Attribute runs whose start tag dropped a duplicate name and spells one no
// HTML vocabulary does: the printer hands those back whole (see `_isWordSpan`).
/** @type {Set<number>} */
const _foreignSyntaxAttrRuns = new Set();
// A processing instruction's target; its data rides in `_nodeStrings` like a
// comment's. A map, since a second string column per node would cost more.
/** @type {Map<HtmlNodeRef, string>} */
const _piTargets = new Map();
let _attrNameStarts = EMPTY_I32;
let _attrNameEnds = EMPTY_I32;
let _attrValueStarts = EMPTY_I32;
let _attrValueEnds = EMPTY_I32;
/** bit 0: name has a `FOREIGN_ATTR_NS` serializer name (set on foreign adjust) */
let _attrFlags = EMPTY_U8;
/** @type {string[]} lowercased (foreign-content: adjusted) attribute name */
const _attrNames = [];
/** @type {(string | null)[]} value override (null = slice the source by offset) */
const _attrValues = [];
/** source of the current parse, for by-offset attribute values */
let _htmlSource = "";
/** @type {LocConverter | undefined} made by the first `A.loc` of a parse */
let _htmlLocConverter;

/**
 * @param {number} need minimum capacity
 * @param {boolean=} exact allocate `need` directly (initial pre-size) instead of doubling
 */
const _growAttrColumns = (need, exact) => {
	let cap = _attrCapacity || 4096;
	if (exact) {
		if (need > cap) cap = need;
	} else {
		while (cap < need) cap *= 2;
		// A capacity over the shrink threshold is released after the parse, so
		// doubling just past the pre-size costs the next one its columns.
		if (cap > _COLUMN_SHRINK_CAPACITY && need <= _COLUMN_SHRINK_CAPACITY) {
			cap = _COLUMN_SHRINK_CAPACITY;
		}
	}
	const nameStart = new Int32Array(cap);
	nameStart.set(_attrNameStarts);
	_attrNameStarts = nameStart;
	const nameEnd = new Int32Array(cap);
	nameEnd.set(_attrNameEnds);
	_attrNameEnds = nameEnd;
	const valStart = new Int32Array(cap);
	valStart.set(_attrValueStarts);
	_attrValueStarts = valStart;
	const valEnd = new Int32Array(cap);
	valEnd.set(_attrValueEnds);
	_attrValueEnds = valEnd;
	const fl = new Uint8Array(cap);
	fl.set(_attrFlags);
	_attrFlags = fl;
	// Keep the string columns filled to capacity (see `_growNodeColumns`).
	for (let i = _attrNames.length; i < cap; i++) {
		_attrNames.push("");
		_attrValues.push(null);
	}
	_attrCapacity = cap;
};

/** @type {(name: string, value: string | null, nameStart: number, nameEnd: number, valueStart: number, valueEnd: number) => number} */
const _allocAttr = (name, value, nameStart, nameEnd, valueStart, valueEnd) => {
	const i = ++_attrCount;
	if (i >= _attrCapacity) _growAttrColumns(i + 1);
	_attrNameStarts[i] = nameStart;
	_attrNameEnds[i] = nameEnd;
	_attrValueStarts[i] = valueStart;
	_attrValueEnds[i] = valueEnd;
	_attrFlags[i] = 0;
	// Ids are sequential, so these indexed writes append (arrays stay packed).
	_attrNames[i] = name;
	_attrValues[i] = value;
	return i;
};

/**
 * Whether a span reads as HTML vocabulary rather than as another language's
 * text: an attribute name or list token starts with an ASCII letter, `_` or
 * `-`, and no delimiter a template engine wraps a statement in appears in it.
 * @param {string} text the string holding the span
 * @param {number} start first index
 * @param {number} end index after the last
 * @returns {boolean} true when the span is a word
 */
const _isWordSpan = (text, start, end) => {
	const first = text.charCodeAt(start);
	if (
		!(first >= 0x61 && first <= 0x7a) &&
		!(first >= 0x41 && first <= 0x5a) &&
		first !== 0x5f &&
		first !== 0x2d
	) {
		return false;
	}
	for (let i = start + 1; i < end; i++) {
		const c = text.charCodeAt(i);
		if (c === 0x7b || c === 0x7d || c === 0x3c || c === 0x3e) return false;
	}
	return true;
};

/** @type {(i: number) => string} */
const _attrValueOf = (i) => {
	const v = _attrValues[i];
	return v !== null
		? v
		: _htmlSource.slice(_attrValueStarts[i], _attrValueEnds[i]);
};

/**
 * An attribute's value as the element is handed it. The tree construction
 * matches values the tokenizer has already resolved references in, so how one
 * is spelled must not decide which branch a token takes.
 * @param {number} i the attribute
 * @returns {string} the value with its character references resolved
 */
const _attrValueDecoded = (i) =>
	xmlMode ? _xmlAttributeValue(i) : decodeEntities(_attrValueOf(i), true);

/**
 * Where an attribute ends: past its value's closing quote, or its name when
 * it has no value.
 * @param {number} i the attribute
 * @returns {number} its end offset
 */
const _attrEnd = (i) => {
	const valueStart = _attrValueStarts[i];
	if (valueStart === -1) return _attrNameEnds[i];
	const valueEnd = _attrValueEnds[i];
	const quote = _htmlSource.charCodeAt(valueStart - 1);
	return quote === CC_QUOTATION_MARK || quote === CC_APOSTROPHE
		? valueEnd + 1
		: valueEnd;
};

/**
 * Where the end tag the source wrote for an element starts, read back off its
 * range: an element's end spans the end tag naming it, so that tag is the
 * last thing in it.
 * @param {HtmlNodeRef} n element
 * @returns {number} the offset of its end tag's `<`, -1 when the source left it out
 */
const _endTagStart = (n) => {
	const end = _nodeEnds[n];
	if (end === 0 || _htmlSource.charCodeAt(end - 1) !== 62) return -1;
	let i = end - 2;
	for (;;) {
		const c = i >= 0 ? _htmlSource.charCodeAt(i) : 0;
		if (c !== 0x20 && c !== 0x09 && c !== 0x0a && c !== 0x0c && c !== 0x0d) {
			break;
		}
		i--;
	}
	const name = _tagNameOf(n);
	const nameEnd = i + 1;
	const nameStart = nameEnd - name.length;
	return nameStart >= 2 &&
		_htmlSource.charCodeAt(nameStart - 1) === 47 &&
		_htmlSource.charCodeAt(nameStart - 2) === 60 &&
		(xmlMode
			? _htmlSource.slice(nameStart, nameEnd) === name
			: rangeAsciiCaseInsensitiveMatch(_htmlSource, nameStart, nameEnd, name))
		? nameStart - 2
		: -1;
};

/**
 * Whether the source wrote this element's start tag: a parser-inserted one
 * borrows another token's offsets, so its written name does not match.
 * @param {HtmlNodeRef} n element
 * @returns {boolean} whether its start tag is in the source
 */
const _hasSourceTag = (n) => {
	const start = _nodeStarts[n];
	const nameEnd = _nodeNameEnds[n];
	const stored = _nodeStrings[n];
	// Fold the raw range against the stored name in place — no name slice, no
	// `toLowerCase` pair. An adjusted foreign name (`foreignObject`) carries
	// upper case the fold cannot match, so a miss falls back to the old compare.
	return (
		rangeAsciiCaseInsensitiveMatch(_htmlSource, start + 1, nameEnd, stored) ||
		_htmlSource.slice(start + 1, nameEnd).toLowerCase() === stored.toLowerCase()
	);
};

/**
 * @param {HtmlNodeRef} n element
 * @returns {string} its start tag as written, `""` when the parser inserted it
 */
const _openTagOf = (n) =>
	_hasSourceTag(n) ? _htmlSource.slice(_nodeStarts[n], _nodeTagEnds[n]) : "";

/**
 * Generated from the start tag's own name (exact source casing, so foreign
 * camelCase holds), since an omitted optional end tag serializes the same.
 * @param {HtmlNodeRef} n element
 * @returns {string} its end tag, `""` when the parser inserted it
 */
const _closeTagOf = (n) => {
	const nameEnd = _nodeNameEnds[n];
	return nameEnd === 0
		? ""
		: `</${_htmlSource.slice(_nodeStarts[n] + 1, nameEnd)}>`;
};

// Scalar position reads for the parser, printer and plugins, which must not
// allocate the arrays the path's `range`, `loc` and `rangeOf` return.
/** @type {(n: HtmlNodeRef) => number} where a node or attribute starts */
const _startOf = (n) => (n < 0 ? _attrNameStarts[-n] : _nodeStarts[n]);
/** @type {(n: HtmlNodeRef) => number} where a node or attribute ends */
const _endOf = (n) => (n < 0 ? _attrEnd(-n) : _nodeEnds[n]);
/** @type {(n: HtmlNodeRef) => number} where a tag or attribute name ends (0 for a parser-inserted tag) */
const _nameEndOf = (n) => (n < 0 ? _attrNameEnds[-n] : _nodeNameEnds[n]);
/** @type {(n: HtmlNodeRef) => number} where an element's opening tag ends */
const _contentStartOf = (n) => _nodeTagEnds[n];

/** @type {(n: HtmlNodeRef) => number} how many attributes an element has */
const _attributeCountOf = (n) => _nodeAttrCounts[n];

/** @type {(i: number, n: HtmlNodeRef) => number} an element's i-th attribute id */
const _attributeAt = (i, n) => _nodeAttrStarts[n] + i;

/** @type {(name: string, n: HtmlNodeRef) => number} an element's attribute id by name (0 = none) */
const _findAttribute = (name, n) =>
	_findAttr(_nodeAttrStarts[n], _nodeAttrCounts[n], name);

/** @type {(i: number) => string} an attribute's name */
const _attrNameOf = (i) => _attrNames[i];

/**
 * An element's attributes as objects, raw values and offsets included: what
 * the printer reads where it compares or reorders them.
 * @param {HtmlNodeRef} n element
 * @returns {HtmlAttribute[]} its attributes
 */
const _attributeList = (n) => {
	const out = [];
	const start = _nodeAttrStarts[n];
	for (let i = start; i < start + _nodeAttrCounts[n]; i++) {
		out.push({
			name: _attrNames[i],
			value: _attrValueOf(i),
			serializedName: _attrSerializedName(i),
			nameStart: _attrNameStarts[i],
			nameEnd: _attrNameEnds[i],
			valueStart: _attrValueStarts[i],
			valueEnd: _attrValueEnds[i]
		});
	}
	return out;
};

// Linear name lookup in a run — attribute lists are short, a loop beats a Map.
/** @type {(start: number, count: number, name: string) => number} */
const _findAttr = (start, count, name) => {
	for (let i = start; i < start + count; i++) {
		if (_attrNames[i] === name) return i;
	}
	return 0;
};

/** @type {(i: number) => number} exact copy of an attribute into a new id */
const _copyAttr = (i) => {
	const c = _allocAttr(
		_attrNames[i],
		_attrValues[i],
		_attrNameStarts[i],
		_attrNameEnds[i],
		_attrValueStarts[i],
		_attrValueEnds[i]
	);
	_attrFlags[c] = _attrFlags[i];
	return c;
};

// Derived html5lib serializer name: a `FOREIGN_ATTR_NS`-adjusted attribute is
// flagged and keyed by its name, a camelCase-adjusted one carries an uppercase
// letter and serializes as itself, and everything else is the plain name.
/** @type {(i: number) => string | undefined} */
const _attrSerializedName = (i) => {
	if ((_attrFlags[i] & 1) !== 0) return FOREIGN_ATTR_NS[_attrNames[i]];
	const name = _attrNames[i];
	return /[A-Z]/.test(name) ? name : undefined;
};

/**
 * @param {number} need minimum capacity
 * @param {boolean=} exact allocate `need` directly (initial pre-size) instead of doubling
 */
const _growNodeColumns = (need, exact) => {
	let cap = _nodeCapacity || 4096;
	// `exact` (the initial pre-size): allocate the estimate directly. Doubling
	// from 4096 would overshoot it to the next power of two (~2x) and waste that
	// capacity for the process lifetime. Incremental growth still doubles.
	if (exact) {
		if (need > cap) cap = need;
	} else {
		while (cap < need) cap *= 2;
		// A capacity over the shrink threshold is released after the parse, so
		// doubling just past the pre-size costs the next one its columns.
		if (cap > _COLUMN_SHRINK_CAPACITY && need <= _COLUMN_SHRINK_CAPACITY) {
			cap = _COLUMN_SHRINK_CAPACITY;
		}
	}
	const ty = new Uint8Array(cap);
	ty.set(_S._nodeTypes);
	_S._nodeTypes = ty;
	const fl = new Uint8Array(cap);
	fl.set(_S._nodeFlags);
	_S._nodeFlags = fl;
	const st = new Int32Array(cap);
	st.set(_nodeStarts);
	_nodeStarts = st;
	const en = new Int32Array(cap);
	en.set(_nodeEnds);
	_nodeEnds = en;
	const tagEnd = new Int32Array(cap);
	tagEnd.set(_nodeTagEnds);
	_nodeTagEnds = tagEnd;
	const nameEnd = new Int32Array(cap);
	nameEnd.set(_nodeNameEnds);
	_nodeNameEnds = nameEnd;
	const cEnd = new Int32Array(cap);
	cEnd.set(_nodeContentEnds);
	_nodeContentEnds = cEnd;
	const parent = new Int32Array(cap);
	parent.set(_nodeParents);
	_nodeParents = parent;
	const first = new Int32Array(cap);
	first.set(_S._nodeFirstChildren);
	_S._nodeFirstChildren = first;
	const last = new Int32Array(cap);
	last.set(_nodeLastChildren);
	_nodeLastChildren = last;
	const next = new Int32Array(cap);
	next.set(_S._nodeNextSiblings);
	_S._nodeNextSiblings = next;
	const aStart = new Int32Array(cap);
	aStart.set(_nodeAttrStarts);
	_nodeAttrStarts = aStart;
	const aCount = new Int32Array(cap);
	aCount.set(_nodeAttrCounts);
	_nodeAttrCounts = aCount;
	// Keep the string column filled to capacity so per-node writes are in-place
	// packed stores, not length-growing appends re-paying the regrowth cascade.
	for (let i = _nodeStrings.length; i < cap; i++) _nodeStrings.push("");
	_nodeCapacity = cap;
};

/** Start a new parse: invalidate all prior refs, release prior heap refs. */
const _resetAstColumns = () => {
	_xmlAttributeValues = null;
	// Overwrite the used prefix (releases the prior parse's strings) instead of
	// truncating: a `length = 0` would right-size the backing store and make
	// every write of the next parse a length-growing append again.
	const written = _nodeCount > _nodeHighWater ? _nodeCount : _nodeHighWater;
	const usedNodes = Math.min(written, _nodeStrings.length - 1);
	for (let i = 0; i <= usedNodes; i++) _nodeStrings[i] = "";
	const usedAttrs = Math.min(_attrCount, _attrNames.length - 1);
	for (let i = 0; i <= usedAttrs; i++) {
		_attrNames[i] = "";
		_attrValues[i] = null;
	}
	_nodeCount = 0;
	_nodeHighWater = 0;
	_attrCount = 0;
	_mergedAttrNodes.clear();
	_foreignSyntaxAttrRuns.clear();
	_piTargets.clear();
	_doctypePublicId = null;
	_doctypeSystemId = null;
	_doctypePublicStart = -1;
	_doctypePublicEnd = -1;
	_doctypeSystemStart = -1;
	_doctypeSystemEnd = -1;
};

// WHY: the columns grow to the largest document parsed and are shrunk on
// release above this capacity, so one pathological file cannot pin tens of MB
// for the process lifetime. Strictly above: `_growNodeColumns` clamps a doubling
// to exactly this capacity, so releasing at it re-allocates every column again.
const _COLUMN_SHRINK_CAPACITY = 65536;
const _SPARSE_MARKUP_LIMIT = 512;

// Release the side arrays' heap references (strings, attribute names/values)
// once a walk has consumed the tree, so the retained columns don't pin the
// parsed source until the next parse.
const _releaseAstColumns = () => {
	_xmlAttributeValues = null;
	// Kept regime: overwrite the used prefix in place (see `_resetAstColumns`);
	// oversized arrays are dropped entirely, mirroring the typed columns below.
	if (_nodeStrings.length > _COLUMN_SHRINK_CAPACITY) {
		_nodeStrings.length = 0;
	} else {
		const written = _nodeCount > _nodeHighWater ? _nodeCount : _nodeHighWater;
		const usedNodes = Math.min(written, _nodeStrings.length - 1);
		for (let i = 0; i <= usedNodes; i++) _nodeStrings[i] = "";
	}
	if (_attrNames.length > _COLUMN_SHRINK_CAPACITY) {
		_attrNames.length = 0;
		_attrValues.length = 0;
	} else {
		const usedAttrs = Math.min(_attrCount, _attrNames.length - 1);
		for (let i = 0; i <= usedAttrs; i++) {
			_attrNames[i] = "";
			_attrValues[i] = null;
		}
	}
	// The printer's sort buffers grew to the widest tag and longest token list
	// this document held; dropping them keeps one page off the module forever.
	_sortOrderScratch.length = 0;
	_sortKeyScratch.length = 0;
	_tokenSpanStarts.length = 0;
	_tokenSpanEnds.length = 0;
	_tokenSpanKeys.length = 0;
	_nodeCount = 0;
	_nodeHighWater = 0;
	_attrCount = 0;
	_mergedAttrNodes.clear();
	_foreignSyntaxAttrRuns.clear();
	_piTargets.clear();
	_htmlSource = "";
	_htmlLocConverter = undefined;
	_doctypePublicId = null;
	_doctypeSystemId = null;
	_doctypePublicStart = -1;
	_doctypePublicEnd = -1;
	_doctypeSystemStart = -1;
	_doctypeSystemEnd = -1;
	if (_nodeCapacity > _COLUMN_SHRINK_CAPACITY) {
		_nodeCapacity = 0;
		_S._nodeTypes = EMPTY_U8;
		_S._nodeFlags = EMPTY_U8;
		_nodeStarts = EMPTY_I32;
		_nodeEnds = EMPTY_I32;
		_nodeTagEnds = EMPTY_I32;
		_nodeNameEnds = EMPTY_I32;
		_nodeContentEnds = EMPTY_I32;
		_nodeParents = EMPTY_I32;
		_S._nodeFirstChildren = EMPTY_I32;
		_nodeLastChildren = EMPTY_I32;
		_S._nodeNextSiblings = EMPTY_I32;
		_nodeAttrStarts = EMPTY_I32;
		_nodeAttrCounts = EMPTY_I32;
	}
	if (_attrCapacity > _COLUMN_SHRINK_CAPACITY) {
		_attrCapacity = 0;
		_attrNameStarts = EMPTY_I32;
		_attrNameEnds = EMPTY_I32;
		_attrValueStarts = EMPTY_I32;
		_attrValueEnds = EMPTY_I32;
		_attrFlags = EMPTY_U8;
	}
};

/** @type {(type: number, start: number, end: number) => HtmlNodeRef} */
// The element-only columns (tagEnds/nameEnds/contentEnds/attrStarts/
// attrCounts) are left stale here — they are only ever read for Element
// nodes, and every Element writes them in mkEl/cloneSubtree.
const _allocNode = (type, start, end) => {
	const i = ++_nodeCount;
	if (i >= _nodeCapacity) _growNodeColumns(i + 1);
	_S._nodeTypes[i] = type;
	_S._nodeFlags[i] = 0;
	_nodeStarts[i] = start;
	_nodeEnds[i] = end;
	_nodeParents[i] = 0;
	_S._nodeFirstChildren[i] = 0;
	_nodeLastChildren[i] = 0;
	_S._nodeNextSiblings[i] = 0;
	// `_nodeStrings[i]` is written by every caller right after this returns
	// (tag name / text / comment data; "" at the few string-less sites), as a
	// packed in-place store — no placeholder double-write here.
	return i;
};

// Raw child append — no text merging, no `<template>` content redirect (the
// tree builder layers those on top). `node` must be detached (`next` = 0).
/** @type {(parent: HtmlNodeRef, node: HtmlNodeRef) => void} */
const _appendChild = (parent, node) => {
	_nodeParents[node] = parent;
	const last = _nodeLastChildren[parent];
	if (last === 0) _S._nodeFirstChildren[parent] = node;
	else _S._nodeNextSiblings[last] = node;
	_nodeLastChildren[parent] = node;
};

/** @type {(data: string, start: number, end: number) => HtmlNodeRef} */
const _makeTextNode = (data, start, end) => {
	const i = _allocNode(NodeType.Text, start, end);
	_nodeStrings[i] = data;
	return i;
};

/** @type {(data: string, start: number, end: number) => HtmlNodeRef} */
const _makeCommentNode = (data, start, end) => {
	const i = _allocNode(NodeType.Comment, start, end);
	_nodeStrings[i] = data;
	return i;
};

/** @type {(target: string, data: string, start: number, end: number) => HtmlNodeRef} */
const _makeProcessingInstructionNode = (target, data, start, end) => {
	const i = _allocNode(NodeType.ProcessingInstruction, start, end);
	_nodeStrings[i] = data;
	_piTargets.set(i, target);
	return i;
};

// Marker entry in the active-formatting-elements list (never a valid ref).
const AFE_MARKER = -1;

/**
 * Clones an element's attribute run, dropping source offsets so the consumer
 * emits no duplicate dependency for the reopened element's spans.
 * @param {HtmlElement} el element whose attributes are cloned
 * @returns {AttributeRun} the cloned run
 */
const cloneAttrs = (el) => {
	const start = _nodeAttrStarts[el];
	const count = _nodeAttrCounts[el];
	const newStart = _attrCount + 1;
	for (let i = start; i < start + count; i++) {
		const c = _allocAttr(_attrNames[i], _attrValueOf(i), -1, -1, -1, -1);
		_attrFlags[c] = _attrFlags[i];
	}
	return { start: newStart, count };
};

/**
 * Merges a repeated `<html>`/`<body>` tag's attributes into the element, adding
 * only names not already present. Runs are contiguous, so any addition
 * re-allocates the whole run and orphans the old slots.
 * @param {HtmlElement} el element receiving the attributes
 * @param {AttributeRun} run attributes of the repeated tag
 * @returns {void}
 */
const mergeAttrs = (el, run) => {
	const start = _nodeAttrStarts[el];
	const count = _nodeAttrCounts[el];
	let extra = 0;
	for (let i = run.start; i < run.start + run.count; i++) {
		if (_findAttr(start, count, _attrNames[i]) === 0) extra++;
	}
	if (extra === 0) return;
	const newStart = _attrCount + 1;
	for (let i = start; i < start + count; i++) _copyAttr(i);
	for (let i = run.start; i < run.start + run.count; i++) {
		if (_findAttr(start, count, _attrNames[i]) === 0) _copyAttr(i);
	}
	_nodeAttrStarts[el] = newStart;
	_nodeAttrCounts[el] = count + extra;
	_mergedAttrNodes.add(el);
};

/**
 * A materialized attribute as returned by `A.attributes` (tests/tooling) —
 * the parser-facing representation is an id into the attribute columns, read
 * through the scalar `A.attr*` accessors.
 * @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
 */

/**
 * A node reference into the struct-of-arrays AST: an integer id indexing the
 * parallel `_h*` columns. Read fields through the exported accessor `A`. Refs
 * are only valid until the next `parseHtml` call — the columns are reused
 * across parses — so consume a tree fully before parsing again.
 * @typedef {number} HtmlNodeRef
 */

/** @typedef {HtmlNodeRef} HtmlElement ref to an Element node */
/** @typedef {HtmlNodeRef} HtmlText ref to a Text node */
/** @typedef {HtmlNodeRef} HtmlComment ref to a Comment node */
/** @typedef {HtmlNodeRef} HtmlDoctype ref to a Doctype node */
/** @typedef {HtmlNodeRef} HtmlProcessingInstruction ref to a ProcessingInstruction node */
/** @typedef {HtmlNodeRef} HtmlDocument ref to the Document node */
/** @typedef {HtmlNodeRef} HtmlDocumentFragment ref to a DocumentFragment node */
/** @typedef {HtmlNodeRef} HtmlNode */

/**
 * An attribute as a node: a negative id, which the shared reads (`A.name`,
 * `A.value`, `A.start`, …) take. Same validity contract as `HtmlNodeRef`.
 * @typedef {number} HtmlAttributeRef
 */

/** @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;
const TOKEN_PROCESSING_INSTRUCTION = 7;

/** @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), publicStart: number, publicEnd: number, systemStart: number, systemEnd: number, forceQuirks: boolean, start: number, end: number }} DoctypeToken */
/** @typedef {{ type: typeof TOKEN_START_TAG, name: string, attrs: AttributeRun, selfClosing: boolean, pos: TagPos, swallowNewline?: boolean }} StartTagToken */
/** @typedef {{ type: typeof TOKEN_END_TAG, name: string, pos: TagPos }} EndTagToken */
/** @typedef {{ type: typeof TOKEN_EOF }} EofToken */
/** @typedef {{ type: typeof TOKEN_PROCESSING_INSTRUCTION, name: string, data: string, start: number, end: number }} ProcessingInstructionToken the target rides in `name`, as the doctype's does */

/**
 * Every insertion mode handles a processing instruction token on the same arc
 * as a comment token, so the arcs test both.
 * @param {Token} t current token
 * @returns {t is CommentToken | ProcessingInstructionToken} true for a comment or processing instruction token
 */
const isCommentOrProcessingInstruction = (t) =>
	t.type === TOKEN_COMMENT || t.type === TOKEN_PROCESSING_INSTRUCTION;

/**
 * Internal token passed through the tree-construction insertion modes.
 * @typedef {CharToken | CommentToken | DoctypeToken | StartTagToken | EndTagToken | EofToken | ProcessingInstructionToken} 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: AttributeRun, selfClosing: boolean, start: number, end: number, publicId: (string | null), systemId: (string | null), publicStart: number, publicEnd: number, systemStart: number, systemEnd: number, forceQuirks: boolean, swallowNewline: boolean, pos: TagPos }} MutableToken
 */

/** @typedef {{ parent: HtmlNodeRef, beforeNode: HtmlNodeRef }} InsertionPlace `beforeNode` 0 = plain append */

// 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;

// The §13.2.6.4.18 re-dispatch table, bound to this file's numbering. A name the
// binding does not cover would silently dispatch to `undefined`, so it throws.
/** @type {Record<string, number>} */
const MODE_BY_NAME = {
	IN_TABLE: MODE_IN_TABLE,
	IN_COLUMN_GROUP: MODE_IN_COLUMN_GROUP,
	IN_TABLE_BODY: MODE_IN_TABLE_BODY,
	IN_ROW: MODE_IN_ROW
};
const TEMPLATE_START_TAG_MODES = new Map(
	Object.entries(TEMPLATE_START_TAG_MODE_NAMES).map(([tag, name]) => {
		const mode = MODE_BY_NAME[name];
		if (mode === undefined) throw new Error(`Unknown insertion mode ${name}`);
		return [tag, mode];
	})
);

/**
 * MathML/SVG specials are handled via namespace checks rather than one table.
 * @param {HtmlElement} el element to test
 * @returns {boolean} true when the element is "special" (§13.2.4.2)
 */
const isSpecial = (el) => {
	const ns = _namespaceOf(el);
	const tag = _tagNameOf(el);
	if (ns === NS_HTML) return SPECIAL.has(tag);
	if (ns === NS_MATHML) return MATHML_SPECIAL.has(tag);
	if (ns === NS_SVG) return SVG_SPECIAL.has(tag.toLowerCase());
	return false;
};

/**
 * `<font color|face|size>` breaks out of foreign content (§13.2.6.5).
 * @param {AttributeRun} run the tag's attributes
 * @returns {boolean} true when one of the breakout attributes is present
 */
const hasFontBreakoutAttr = (run) => {
	for (let i = run.start; i < run.start + run.count; i++) {
		if (FONT_BREAKOUT_ATTRS.has(_attrNames[i])) return true;
	}
	return false;
};

/**
 * 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 = (Math.imul(h, 31) + c) | 0;
	}
	return h;
};

// Text-run scan classes for the `skip.text` fast path: 2 = stop the fast
// path (& / NUL / CR), 1 = ASCII whitespace, 0 = ordinary text.
const _TEXT_SCAN_CLASS = new Uint8Array(128);
_TEXT_SCAN_CLASS[0x09] = 1;
_TEXT_SCAN_CLASS[0x0a] = 1;
_TEXT_SCAN_CLASS[0x0c] = 1;
_TEXT_SCAN_CLASS[0x20] = 1;
_TEXT_SCAN_CLASS[0x26] = 2;
_TEXT_SCAN_CLASS[0x00] = 2;
_TEXT_SCAN_CLASS[0x0d] = 2;

/**
 * @param {Iterable<string>} names lowercase names to intern
 * @returns {{ mask: number, hashes: Int32Array, values: (string | undefined)[], memo: string[], sourceBacked: number[], noted: Uint8Array }} open-addressed intern table
 */
const buildNameInternTable = (names) => {
	// Open-addressed table (~25% load): the per-name probe is one or two array
	// reads instead of a `Map#get`, and the tables are built once at startup.
	const unique = [...new Set(names)];
	let size = 8;
	while (size < unique.length * 4) size <<= 1;
	const mask = size - 1;
	const hashes = new Int32Array(size);
	/** @type {(string | undefined)[]} */
	const values = Array.from({ length: size });
	for (const name of unique) {
		const h = hashLowerName(name);
		let slot = h & mask;
		// Two names sharing a hash take two slots rather than one bucket, so a
		// slot holds one string and the lookup's load stays monomorphic.
		while (values[slot] !== undefined) slot = (slot + 1) & mask;
		hashes[slot] = h;
		values[slot] = name;
	}
	// `memo` short-circuits the hash walk for a name just seen, with `""` as the
	// empty slot so the load stays monomorphic. `sourceBacked` lists the slots
	// holding a slice of the input — the only ones the next parse drops.
	return {
		mask,
		hashes,
		values,
		memo: Array.from({ length: 512 }).fill(""),
		sourceBacked: [],
		noted: new Uint8Array(512)
	};
};

/** @typedef {ReturnType<typeof buildNameInternTable>} NameInternTable */

/**
 * Drop the memo entries holding a slice of the last parsed input, so no parse
 * retains another's source. Past the memo's own size a full clear is cheaper.
 * @param {NameInternTable} table intern table
 * @returns {void}
 */
const _dropSourceBackedNames = (table) => {
	const slots = table.sourceBacked;
	if (slots.length === 0) return;
	const { memo, noted } = table;
	// A slot a known name has since claimed is dropped with the rest: the entry is
	// safe to keep, but noting that costs a write on the path every name takes.
	if (slots.length >= memo.length) {
		memo.fill("");
		noted.fill(0);
	} else {
		for (const slot of slots) {
			memo[slot] = "";
			noted[slot] = 0;
		}
	}
	slots.length = 0;
};

/**
 * 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 {NameInternTable} 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) => {
	// A document spells the same few names over and over, so a direct-mapped memo
	// keyed on folded first char and length answers most calls in one compare.
	// Every hit re-verifies, so a collision misses rather than lies.
	let c0 = input.charCodeAt(start);
	if (c0 >= 0x41 && c0 <= 0x5a) c0 += 0x20;
	const memo = table.memo;
	const memoSlot = (Math.imul(c0, 31) + (end - start)) & 511;
	const cached = memo[memoSlot];
	if (
		cached.length !== 0 &&
		rangeAsciiCaseInsensitiveMatch(input, start, end, cached)
	) {
		return cached;
	}
	let h = end - start;
	for (let i = start; i < end; i++) {
		let c = input.charCodeAt(i);
		if (c >= 0x41 && c <= 0x5a) c += 0x20;
		h = (Math.imul(h, 31) + c) | 0;
	}
	const { mask, hashes, values } = table;
	let slot = h & mask;
	for (;;) {
		const hit = values[slot];
		// Only an empty slot ends the walk: a name that hashes the same as another
		// sits one slot along, so an equal hash that does not match is not a miss.
		if (hit === undefined) break;
		if (
			hashes[slot] === h &&
			rangeAsciiCaseInsensitiveMatch(input, start, end, hit)
		) {
			return (memo[memoSlot] = hit);
		}
		slot = (slot + 1) & mask;
	}
	// Unknown name (custom element, data-* attribute, non-ASCII, …). Only ASCII
	// folds — a Unicode `toLowerCase` maps U+212A onto `k` and U+0130 onto two
	// characters, neither of which the tokenizer does.
	if (table.noted[memoSlot] === 0) {
		table.noted[memoSlot] = 1;
		table.sourceBacked.push(memoSlot);
	}
	return (memo[memoSlot] = asciiLowercase(input.slice(start, end)));
};

// 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.
 * @param {HtmlElement} e element to test
 * @returns {boolean} true for an HTML `<template>` element
 */
const isHtmlTemplateEl = (e) =>
	_tagNameOf(e) === "template" && _namespaceOf(e) === NS_HTML;

/**
 * Shared empty skip set so the common (no-skip) call allocates nothing.
 * @type {HtmlAstSkip}
 */
const EMPTY_SKIP = Object.freeze({});

/**
 * Optional node kinds a consumer can drop from the AST for speed/memory. Each
 * is a pure output reduction — tree construction (and quirks detection) runs
 * unchanged, so element structure and offsets are identical either way.
 * @typedef {object} HtmlAstSkip
 * @property {boolean=} text drop every `Text` node. Raw-text element bodies (`<script>`/`<style>`/…) aren't emitted either — their content span is recorded as the element's `contentEnd` (see `RAW_TEXT_ELEMENTS`) so a consumer can read `[tagEnd, contentEnd]` by offset. For consumers that read text by offset (e.g. `HtmlParser`), never the html5lib serializer.
 * @property {boolean=} comments drop comment nodes entirely. Not for consumers that read comments (e.g. webpack magic comments).
 * @property {boolean=} doctype drop the doctype node; quirks-mode detection is unaffected.
 */

/**
 * @typedef {object} HtmlParseOptions
 * @property {string=} fragmentContext context element name for fragment parsing (e.g. `td`, `svg path`); omit for a full document
 * @property {boolean=} xml parse as XML instead of applying the HTML tree-construction rules
 * @property {HtmlAstSkip=} skip node kinds to omit from the AST (see `HtmlAstSkip`); omit to build the full tree
 * @property {boolean=} scripting parse as a browser with scripting on does, where a `noscript` holds raw text; off by default, so what it holds is read as markup
 */

/**
 * @param {string} name raw SVG tag name
 * @returns {string} case-adjusted SVG tag name
 */
const adjustSvgTag = (name) => SVG_TAG_ADJUST[name] || name;

/**
 * @param {string} name doctype name
 * @param {string | null} pub public id
 * @param {string | null} sys system id
 * @returns {boolean} whether the doctype forces quirks mode
 */
const isQuirky = (name, pub, 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;
};

/**
 * Whether an element of this name and namespace bounds a scope (§13.2.4.2).
 * @param {string} tagName the element's name
 * @param {number} ns the element's namespace
 * @returns {boolean} true when it stops a "have an element in scope" walk
 */
const _boundsScope = (tagName, ns) => {
	if (ns === NS_HTML) return HTML_SCOPE.has(tagName);
	if (ns === NS_MATHML) return MATHML_SPECIAL.has(tagName);
	if (ns === NS_SVG) return SVG_SPECIAL.has(tagName.toLowerCase());
	return false;
};

/**
 * Allocates an element node.
 * @param {string} tagName tag name
 * @param {number} ns namespace flag (`NS_HTML`, `NS_MATHML`, `NS_SVG`)
 * @param {AttributeRun} attrs the element's attributes
 * @param {TagPos | null | undefined} pos source position, absent for synthesized elements
 * @returns {HtmlElement} the new element
 */
const mkEl = (tagName, ns, attrs, pos) => {
	// Inlined element allocation: every column is written exactly once (the
	// generic _allocNode would zero five columns mkEl immediately overwrites).
	const el = ++_nodeCount;
	if (el >= _nodeCapacity) _growNodeColumns(el + 1);
	_S._nodeTypes[el] = NodeType.Element;
	// Void HTML elements are marked self-closing and never receive children.
	_S._nodeFlags[el] =
		(ns === NS_HTML && VOID.has(tagName) ? ns | FLAG_SELF_CLOSING : ns) |
		(_boundsScope(tagName, ns) ? FLAG_SCOPE_BOUNDARY : 0);
	if (pos) {
		_nodeStarts[el] = pos.start;
		_nodeEnds[el] = pos.end;
		_nodeTagEnds[el] = pos.tagEnd;
		_nodeNameEnds[el] = pos.nameEnd;
		// End of a raw-text element's body under `skip.text` (defaults to the
		// body start, i.e. empty); lets consumers read `<script>`/`<style>`
		// content as [`tagEnd`, `contentEnd`] without a `Text` node.
		_nodeContentEnds[el] = pos.tagEnd;
	} else {
		_nodeStarts[el] = 0;
		_nodeEnds[el] = 0;
		_nodeTagEnds[el] = 0;
		_nodeNameEnds[el] = 0;
		_nodeContentEnds[el] = 0;
	}
	_nodeParents[el] = 0;
	_S._nodeFirstChildren[el] = 0;
	_nodeLastChildren[el] = 0;
	_S._nodeNextSiblings[el] = 0;
	_nodeAttrStarts[el] = attrs.start;
	_nodeAttrCounts[el] = attrs.count;
	_nodeStrings[el] = tagName;
	return el;
};

/**
 * A `<template>`'s children live in its content fragment; inserting into a
 * template really inserts there (the spec's "template contents" redirect).
 * @param {HtmlNodeRef} parent container
 * @returns {HtmlNodeRef} effective container to link children into
 */
const effParent = (parent) => {
	const tc = _templateContentOf(parent);
	return tc !== 0 ? tc : parent;
};

/**
 * Elements that stop a "have an element in scope" walk (§13.2.4.2).
 * @param {HtmlElement} el element to test
 * @returns {boolean} true when the element bounds a scope
 */
const isScopeBoundary = (el) => (_S._nodeFlags[el] & FLAG_SCOPE_BOUNDARY) !== 0;

/**
 * Compares two attribute values, by source range while both are unresolved —
 * `_attrValueOf` would slice a string per side just to throw it away.
 * @param {number} i first attribute id
 * @param {number} j second attribute id
 * @returns {boolean} true when both values are equal
 */
const _attrValueEquals = (i, j) => {
	if (_attrValues[i] === null && _attrValues[j] === null) {
		const start = _attrValueStarts[i];
		const len = _attrValueEnds[i] - start;
		const otherStart = _attrValueStarts[j];
		if (len !== _attrValueEnds[j] - otherStart) return false;
		for (let k = 0; k < len; k++) {
			if (
				_htmlSource.charCodeAt(start + k) !==
				_htmlSource.charCodeAt(otherStart + k)
			) {
				return false;
			}
		}
		return true;
	}
	return _attrValueOf(i) === _attrValueOf(j);
};

/**
 * Compares two elements' attribute runs, as the active-formatting-elements
 * "Noah's Ark" clause does.
 * @param {HtmlElement} a first element
 * @param {HtmlElement} b second element
 * @returns {boolean} true when both carry the same attributes
 */
const sameAttrs = (a, b) => {
	const aStart = _nodeAttrStarts[a];
	const aCount = _nodeAttrCounts[a];
	const bStart = _nodeAttrStarts[b];
	const bCount = _nodeAttrCounts[b];
	if (aCount !== bCount) return false;
	// Names are unique (deduped), counts tiny — a nested scan beats a Map
	// here on this formatting-element hot path.
	for (let i = bStart; i < bStart + bCount; i++) {
		const j = _findAttr(aStart, aCount, _attrNames[i]);
		if (j === 0 || !_attrValueEquals(j, i)) return false;
	}
	return true;
};

/**
 * @param {string} s text to test
 * @returns {boolean} true when the text starts with HTML whitespace
 */
const startsWithAsciiWhitespace = (s) => {
	const c = s.charCodeAt(0);
	return c === 0x09 || c === 0x0a || c === 0x0c || c === 0x0d || c === 0x20;
};

/**
 * @param {string} s text to test
 * @returns {boolean} true when the text is HTML whitespace only
 */
const isAllAsciiWhitespace = (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;
};

// A whitespace run the printer can shorten: two of them in a row, or a lone one
// that is not already a space.
const _COLLAPSIBLE_WHITESPACE_REGEXP = /[\t\n\f\r]|[ ][ ]/;

/**
 * Collapse every run of HTML whitespace to one space — never remove one:
 * `a <b>c</b>` and `a<b>c</b>` render differently.
 * @param {string} s text data
 * @returns {string} the collapsed text
 */
const collapseWhitespaceRuns = (s) => {
	// Most text collapses to itself, and one scan answers that before the walk
	// below reaches its first run.
	if (!_COLLAPSIBLE_WHITESPACE_REGEXP.test(s)) return s;
	let out = "";
	let last = 0;
	let run = -1;
	for (let i = 0; i <= s.length; i++) {
		const c = i === s.length ? 0 : s.charCodeAt(i);
		const isAsciiWhitespace =
			c === 0x09 || c === 0x0a || c === 0x0c || c === 0x0d || c === 0x20;
		if (isAsciiWhitespace) {
			if (run === -1) run = i;
			continue;
		}
		// A one-character run is already a single space unless it is a tab or a
		// newline, which still print shorter as one.
		if (run !== -1 && (i - run > 1 || s.charCodeAt(run) !== 0x20)) {
			out += `${s.slice(last, run)} `;
			last = i;
		}
		run = -1;
	}
	return last === 0 ? s : out + s.slice(last);
};

/**
 * @param {HtmlElement} el element to test
 * @returns {boolean} true for a MathML text integration point (§13.2.6.5)
 */
const mathmlTextIntegrationPoint = (el) =>
	_namespaceOf(el) === NS_MATHML && MATHML_TEXT_INTEGRATION.has(_tagNameOf(el));

/**
 * @param {HtmlElement} el element to test
 * @returns {boolean} true for an HTML integration point (§13.2.6.5)
 */
const htmlIntegrationPoint = (el) => {
	if (_namespaceOf(el) === NS_MATHML && _tagNameOf(el) === "annotation-xml") {
		const enc = _findAttr(_nodeAttrStarts[el], _nodeAttrCounts[el], "encoding");
		if (enc !== 0) {
			const value = _attrValueDecoded(enc).toLowerCase();
			if (value === "text/html" || value === "application/xhtml+xml") {
				return true;
			}
		}
		return false;
	}
	if (
		_namespaceOf(el) === NS_SVG &&
		SVG_SPECIAL.has(_tagNameOf(el).toLowerCase())
	) {
		return true;
	}
	return false;
};

/**
 * Rewrites SVG camelCase names and flags namespaced ones for the serializer.
 * In place: the run is consumed only by this tag's element.
 * @param {AttributeRun} run the tag's attributes
 * @param {number} ns namespace flag of the tag
 * @returns {AttributeRun} the same run
 */
const adjustForeignAttrs = (run, ns) => {
	for (let i = run.start; i < run.start + run.count; i++) {
		const name = _attrNames[i];
		if (ns === NS_SVG && SVG_ATTR_ADJUST[name]) {
			_attrNames[i] = SVG_ATTR_ADJUST[name];
		}
		if (FOREIGN_ATTR_NS[name]) {
			_attrFlags[i] |= 1;
		}
	}
	return run;
};

/**
 * Only `definitionurl` is rewritten (in place — the run is consumed only by
 * this tag's element); the camelCase name serializes as itself.
 * @param {AttributeRun} run the tag's attributes
 * @returns {AttributeRun} the same run
 */
const adjustMathmlAttrs = (run) => {
	for (let i = run.start; i < run.start + run.count; i++) {
		if (_attrNames[i] === "definitionurl") _attrNames[i] = "definitionURL";
	}
	return run;
};

let inputHasNul = true;
let skipText = false;
let skipComments = false;
let skipDoctype = false;
let xmlMode = false;
/** @type {HtmlNodeRef} */
let doc = 0;
let mode = 0;
let originalMode = 0;
/** @type {HtmlElement[]} stack of open elements (bottom .. top) */
const open = [];
/** @type {HtmlNodeRef[]} active formatting elements (AFE_MARKER = marker) */
const afe = [];

/** @type {HtmlElement} 0 = none */
let form = 0;
let framesetOk = true;
let pendAttrStart = 0;
// Whether §13.2.5.33 dropped a repeated name from the tag being read; a tag
// that dropped none needs no word test, keeping it off the per-attribute path.
let pendAttrDropped = false;
let fosterParenting = false;
/** @type {number[]} */
const templateModes = [];
let quirks = false;
// The scripting flag (§13.2.2): set, a `noscript` is a raw text element.
let scripting = false;
/** @type {HtmlElement} fragment context element (0 = document parse) */
let fragment = 0;

/**
 * @returns {boolean} whether the parser is parsing template contents
 */
const parsingTemplateContents = () =>
	open.some(isHtmlTemplateEl) || (fragment !== 0 && isHtmlTemplateEl(fragment));

let tokenEnd = 0;
let swallowNextNewline = false;
// Which of `</html>` / `</head>` / `</body>` the source spelled, per parse: the
// tree records no end tag, and `removeImpliedTags` keeps the ones that were written.
let sourceShellEnds = 0;
const SHELL_END_HTML = 1;
const SHELL_END_HEAD = 2;
const SHELL_END_BODY = 4;
let eofInTag = false;
let sawSelectedContent = false;
const decode = decodeEntities;

const XML_NAMED_REFERENCES = {
	amp: "&",
	apos: "'",
	gt: ">",
	lt: "<",
	quot: '"'
};

/**
 * @param {number} code Unicode code point
 * @returns {boolean} whether XML 1.0 permits the character
 */
const isXmlChar = (code) =>
	code === CC_TAB ||
	code === CC_LF ||
	code === CC_CR ||
	(code >= 0x20 && code <= 0xd7ff) ||
	(code >= 0xe000 && code <= 0xfffd) ||
	(code >= 0x10000 && code <= 0x10ffff);

// A character reference, or an entity reference by any name a lenient reader
// would take; which names resolve is up to the document's DTD.
const XML_REFERENCE_REGEXP =
	/&(?:#(?:x([\da-fA-F]+)|(\d+))|([^\s&;#<>"'=/%]+));/g;
const XML_CHARACTER_REFERENCE_REGEXP = /&#(?:x([\da-fA-F]+)|(\d+));/g;
// WHY: §3.3.3 normalizes each literal whitespace character of an attribute value
// to a space after §2.11 has folded CRLF, so a CRLF is one space; replacement
// text of an entity is already folded, so there each character counts alone.
const XML_ATTRIBUTE_LINE_END_REGEXP = /\r\n?|[\t\n]/g;
const XML_WHITESPACE_REGEXP = /[\t\n\r]/g;
const XML_LINE_END_REGEXP = /\r\n?/g;
// Entities an entity may name before the rest is left as written.
const XML_ENTITY_DEPTH_LIMIT = 16;
// Characters one parse may expand entities into beyond the source itself, so
// a declaration nesting its references ("billion laughs") stays bounded.
const XML_ENTITY_EXPANSION_BUDGET = 1 << 20;

/** @type {Map<string, string> | null} internal general entities the DOCTYPE declared */
let _xmlEntities = null;
/** @type {Map<number, string> | null} normalized XML attribute values, decoded once per parse */
let _xmlAttributeValues = null;
// The source range of the text whose entity reference is being tokenized, -1
// outside one: what the replacement text builds is reported there.
let _xmlEntityRangeStart = -1;
/** @type {Map<string, boolean> | null} which entities expand to markup, as far as asked */
let _xmlMarkupEntities = null;
let _xmlEntityRangeEnd = -1;
/**
 * @typedef {object} XmlAttributeDeclaration
 * @property {boolean} tokenized whether the type is not CDATA, so §3.3.3 also trims and collapses spaces
 * @property {string | null} defaultValue the default as written, null for `#REQUIRED` / `#IMPLIED`
 */
/** @type {Map<string, Map<string, XmlAttributeDeclaration>> | null} attributes the DOCTYPE declared, per element */
let _xmlAttributeDeclarations = null;
/** @type {string[]} entities being expanded, innermost last */
const _xmlEntityStack = [];
let _xmlExpansionBudget = 0;

/**
 * @param {string} match the reference as written
 * @param {string | undefined} hex hexadecimal digits of a character reference
 * @param {string | undefined} decimal decimal digits of a character reference
 * @returns {string} the character, or U+FFFD for one XML does not allow
 */
const _xmlCharacterReference = (match, hex, decimal) => {
	const code = Number.parseInt(
		hex === undefined ? /** @type {string} */ (decimal) : hex,
		hex === undefined ? 10 : 16
	);
	return isXmlChar(code) ? String.fromCodePoint(code) : "\uFFFD";
};

/**
 * Resolve character and entity references (§4.4): the five predefined names,
 * and the internal general entities the document's DTD declared.
 * @param {string} value source text, its line ends already folded
 * @param {boolean} attribute whether `value` is (part of) an attribute value
 * @returns {string} decoded text
 */
const _decodeXmlReferences = (value, attribute) =>
	value.replace(XML_REFERENCE_REGEXP, (match, hex, decimal, name) => {
		if (name === undefined) return _xmlCharacterReference(match, hex, decimal);
		if (Object.prototype.hasOwnProperty.call(XML_NAMED_REFERENCES, name)) {
			return XML_NAMED_REFERENCES[
				/** @type {keyof typeof XML_NAMED_REFERENCES} */ (name)
			];
		}
		const replacement =
			_xmlEntities === null ? undefined : _xmlEntities.get(name);
		// An undeclared or recursive entity, or one past the limits, stays as written.
		if (
			replacement === undefined ||
			_xmlEntityStack.length >= XML_ENTITY_DEPTH_LIMIT ||
			_xmlEntityStack.includes(name) ||
			_xmlExpansionBudget < replacement.length
		) {
			return match;
		}
		_xmlExpansionBudget -= replacement.length;
		_xmlEntityStack.push(name);
		const text = attribute
			? replacement.replace(XML_WHITESPACE_REGEXP, " ")
			: replacement;
		const decoded = text.includes("&")
			? _decodeXmlReferences(text, attribute)
			: text;
		_xmlEntityStack.pop();
		return decoded;
	});

/**
 * Decode the character data of XML content.
 * @param {string} value source text, its line ends already folded
 * @returns {string} decoded text
 */
const decodeXmlEntities = (value) =>
	value.includes("&") ? _decodeXmlReferences(value, false) : value;

/**
 * An attribute value as §3.3.3 normalizes a CDATA one: literal whitespace
 * becomes a space, then references are resolved.
 * @param {string} value the value as written
 * @returns {string} normalized value
 */
const decodeXmlAttribute = (value) => {
	const spaced = value.replace(XML_ATTRIBUTE_LINE_END_REGEXP, " ");
	return spaced.includes("&") ? _decodeXmlReferences(spaced, true) : spaced;
};

/**
 * @param {number} i attribute index
 * @returns {string} its normalized XML value, without spending the expansion budget again
 */
const _xmlAttributeValue = (i) => {
	if (_xmlAttributeValues === null) _xmlAttributeValues = new Map();
	const known = _xmlAttributeValues.get(i);
	if (known !== undefined) return known;
	const value = decodeXmlAttribute(_attrValueOf(i));
	_xmlAttributeValues.set(i, value);
	return value;
};

/**
 * Escape text for XML content (§2.4): `&`, `<` and `>`, and a CR, which §2.11
 * would otherwise read back as a LF. Only a `]]>` needs its `>` escaped, but a
 * bare one costs gzip what it saves raw where the text also spells `&lt;`.
 * @param {string} data decoded text
 * @returns {string} the text as character data
 */
const escapeXmlText = (data) =>
	data
		.replace(/&/g, "&amp;")
		.replace(/</g, "&lt;")
		.replace(/>/g, "&gt;")
		.replace(/\r/g, "&#13;");

/**
 * Escape an attribute value for XML (§3.1): `&`, `<` and the delimiter, plus the
 * whitespace §3.3.3 would otherwise normalize to a space.
 * @param {string} value decoded value
 * @param {string} quote the delimiter it is written between
 * @returns {string} the value as written between `quote`s
 */
const escapeXmlAttribute = (value, quote) => {
	let out = value.replace(/&/g, "&amp;").replace(/</g, "&lt;");
	out =
		quote === '"' ? out.replace(/"/g, "&quot;") : out.replace(/'/g, "&apos;");
	return out
		.replace(/\t/g, "&#9;")
		.replace(/\n/g, "&#10;")
		.replace(/\r/g, "&#13;");
};

/**
 * Read an attribute-list declaration (§3.3) into `_xmlAttributeDeclarations`.
 * @param {string} input source
 * @param {number} pos offset past `<!ATTLIST`
 * @param {number} end offset of the internal subset's `]`
 * @returns {number} offset past the declaration
 */
const _readXmlAttributeListDeclaration = (input, pos, end) => {
	pos = _skipXmlWhitespace(input, pos);
	const elementEnd = _xmlNameEnd(input, pos);
	const element = input.slice(pos, elementEnd);
	pos = elementEnd;
	while (pos < end) {
		pos = _skipXmlWhitespace(input, pos);
		if (pos >= end || input.charCodeAt(pos) === CC_GREATER_THAN) break;
		const nameEnd = _xmlNameEnd(input, pos);
		if (nameEnd === pos) break;
		const name = input.slice(pos, nameEnd);
		pos = _skipXmlWhitespace(input, nameEnd);
		// AttType: a keyword, an enumeration, or NOTATION and an enumeration.
		let tokenized = true;
		if (input.startsWith("CDATA", pos)) {
			tokenized = false;
			pos += 5;
		} else {
			if (input.startsWith("NOTATION", pos)) {
				pos = _skipXmlWhitespace(input, pos + 8);
			}
			if (input.charCodeAt(pos) === 0x28 /* ( */) {
				const close = input.indexOf(")", pos);
				pos = close === -1 || close > end ? end : close + 1;
			} else {
				pos = _xmlNameEnd(input, pos);
			}
		}
		pos = _skipXmlWhitespace(input, pos);
		let defaultValue = null;
		if (input.startsWith("#FIXED", pos)) {
			pos = _skipXmlWhitespace(input, pos + 6);
		} else if (input.charCodeAt(pos) === 0x23 /* # */) {
			pos = _xmlNameEnd(input, pos + 1);
		}
		const quote = input.charCodeAt(pos);
		if (quote === CC_QUOTATION_MARK || quote === CC_APOSTROPHE) {
			const close = input.indexOf(quote === CC_APOSTROPHE ? "'" : '"', pos + 1);
			if (close === -1 || close > end) return end;
			defaultValue = input.slice(pos + 1, close);
			pos = close + 1;
		}
		if (_xmlAttributeDeclarations === null) {
			_xmlAttributeDeclarations = new Map();
		}
		let declarations = _xmlAttributeDeclarations.get(element);
		if (declarations === undefined) {
			declarations = new Map();
			_xmlAttributeDeclarations.set(element, declarations);
		}
		// §3.3: the first declaration of an attribute binds it.
		if (!declarations.has(name)) {
			declarations.set(name, { tokenized, defaultValue });
		}
	}
	const close = input.indexOf(">", pos);
	return close === -1 || close > end ? end : close + 1;
};

/**
 * Apply what the DTD declares to a start tag's attribute run: a tokenized type's
 * value is trimmed and its spaces collapsed, and a missing attribute with a
 * default gets one. Both are stored as a spelling `decodeXmlAttribute` reads.
 * @param {{ start: number, count: number }} attrs the tag's attribute run, updated in place
 * @param {Map<string, XmlAttributeDeclaration>} declarations the element's declared attributes
 * @returns {void}
 */
const _applyXmlAttributeDeclarations = (attrs, declarations) => {
	if (_xmlAttributeValues === null) _xmlAttributeValues = new Map();
	const start = attrs.start;
	const count = attrs.count;
	for (let i = start; i < start + count; i++) {
		const declaration = declarations.get(_attrNames[i]);
		if (declaration === undefined || !declaration.tokenized) continue;
		const value = _xmlAttributeValue(i);
		const collapsed = value.replace(/ +/g, " ").replace(/^ | $/g, "");
		if (collapsed !== value) {
			_attrValues[i] = escapeXmlAttribute(collapsed, '"');
			_xmlAttributeValues.set(i, collapsed);
		}
	}
	for (const [name, declaration] of declarations) {
		if (declaration.defaultValue === null) continue;
		let present = false;
		for (let i = start; i < start + count; i++) {
			if (_attrNames[i] === name) {
				present = true;
				break;
			}
		}
		if (present) continue;
		let value = decodeXmlAttribute(declaration.defaultValue);
		if (declaration.tokenized) {
			value = value.replace(/ +/g, " ").replace(/^ | $/g, "");
		}
		// No source offsets: the attribute is the DTD's, not the tag's.
		const index = _allocAttr(
			name,
			escapeXmlAttribute(value, '"'),
			-1,
			-1,
			-1,
			-1
		);
		_xmlAttributeValues.set(index, value);
		if (attrs.count === 0) attrs.start = index;
		attrs.count++;
	}
};

/**
 * Read the general entities an internal DTD subset declares (§4.2). A literal's
 * character references are expanded as it is declared; its entity references
 * wait until the entity is used.
 * @param {string} input source
 * @param {number} start offset past the `[`
 * @param {number} end offset of the `]`
 * @returns {void}
 */
const _readXmlInternalSubset = (input, start, end) => {
	let pos = start;
	while (pos < end) {
		const cc = input.charCodeAt(pos);
		if (cc === 0x25 /* % */) {
			// §5.1: once a parameter entity went unread, a non-validating processor
			// must not process the declarations after it.
			return;
		}
		if (cc !== CC_LESS_THAN) {
			pos++;
			continue;
		}
		if (input.startsWith("<!--", pos)) {
			const close = input.indexOf("-->", pos + 4);
			pos = close === -1 ? end : close + 3;
			continue;
		}
		if (input.startsWith("<?", pos)) {
			const close = input.indexOf("?>", pos + 2);
			pos = close === -1 ? end : close + 2;
			continue;
		}
		if (input.startsWith("<!ATTLIST", pos)) {
			pos = _readXmlAttributeListDeclaration(input, pos + 9, end);
			continue;
		}
		const isEntity = input.startsWith("<!ENTITY", pos);
		let i = _skipXmlWhitespace(input, pos + (isEntity ? 8 : 2));
		const parameter = isEntity && input.charCodeAt(i) === 0x25;
		if (parameter) i = _skipXmlWhitespace(input, i + 1);
		const nameStart = i;
		const nameEnd = _xmlNameEnd(input, i);
		i = _skipXmlWhitespace(input, nameEnd);
		const quote = input.charCodeAt(i);
		if (
			isEntity &&
			!parameter &&
			nameEnd !== nameStart &&
			(quote === CC_QUOTATION_MARK || quote === CC_APOSTROPHE)
		) {
			const close = input.indexOf(quote === CC_APOSTROPHE ? "'" : '"', i + 1);
			if (close === -1 || close > end) return;
			const name = input.slice(nameStart, nameEnd);
			if (_xmlEntities === null) _xmlEntities = new Map();
			// §4.2: the first declaration of a name binds it.
			if (!_xmlEntities.has(name)) {
				_xmlEntities.set(
					name,
					input
						.slice(i + 1, close)
						.replace(XML_LINE_END_REGEXP, "\n")
						.replace(XML_CHARACTER_REFERENCE_REGEXP, _xmlCharacterReference)
				);
			}
			i = close + 1;
		}
		// The rest of the declaration: skip what it quotes, since a literal may
		// hold a `>`.
		while (i < end) {
			const c = input.charCodeAt(i);
			if (c === CC_GREATER_THAN) break;
			if (c === CC_QUOTATION_MARK || c === CC_APOSTROPHE) {
				const close = input.indexOf(c === CC_APOSTROPHE ? "'" : '"', i + 1);
				i = close === -1 ? end : close + 1;
			} else {
				i++;
			}
		}
		pos = i + 1;
	}
};
/** @type {MutableToken} */
const tok = {
	type: TOKEN_EOF,
	name: "",
	data: "",
	attrs: { start: 0, count: 0 },
	selfClosing: false,
	start: 0,
	end: 0,
	publicId: null,
	systemId: null,
	publicStart: -1,
	publicEnd: -1,
	systemStart: -1,
	systemEnd: -1,
	forceQuirks: false,
	swallowNewline: false,
	pos: { start: 0, end: 0, tagEnd: 0, nameEnd: 0 }
};

const cur = () =>
	/** @type {HtmlElement} */ (open.length > 0 ? open[open.length - 1] : 0);
/**
 * Remove an element from the middle of the open element stack. A removal that
 * leaves still-open descendants above it breaks the streamed walk whether or
 * not the walk entered the element: it drops off the stack without its subtree
 * ending, so those descendants would be entered under a parent the walk has no
 * open-stack evidence for. Removing the top is just a pop and keeps streaming —
 * which is the common `</form>` after implied end tags.
 * @param {number} index position in `open` to remove
 * @returns {void}
 */
const removeOpenAt = (index) => {
	open.splice(index, 1);
	_openSpliced = true;
	_openStackChanged = true;
	if (_streaming && index < open.length) _streamHalted = true;
};

/**
 * Pop the open element stack. Every pop routes through here so the streamed
 * walk has a single completion point — the HTML analog of the CSS parser's
 * `onRule` sink — instead of being polled once per token.
 * @returns {HtmlElement} the popped element
 */
const popOpen = () => {
	const el = /** @type {HtmlElement} */ (open.pop());
	// A parser-inserted element keeps the range it was made with.
	if (_nodeNameEnds[el] !== 0) _endClosedElement(el);
	// Only mark: an element's end offset is assigned after it is popped, so the
	// walk has to wait for the token to finish.
	_openStackChanged = true;
	return el;
};

/**
 * End a popped element where the token closing it starts, never before its
 * own tag or last child; an end tag naming it moves the end past the tag.
 * Kept out of `popOpen` so that stays small enough to inline into `runMode`.
 * @param {HtmlElement} el the element
 */
const _endClosedElement = (el) => {
	// What it holds so far: its start tag, or the end tag a caller just set.
	let end = _nodeEnds[el];
	const at = _currentTokenStart();
	if (at > end) end = at;
	const last = _nodeLastChildren[el];
	if (last !== 0 && _nodeEnds[last] > end) end = _nodeEnds[last];
	_nodeEnds[el] = end;
};

/**
 * @returns {number} where the token being processed starts (the input's end at EOF)
 */
const _currentTokenStart = () => {
	const type = tok.type;
	if (type === TOKEN_START_TAG || type === TOKEN_END_TAG) return tok.pos.start;
	return type === TOKEN_EOF ? _S.source.length : tok.start;
};

const adjustedCurrent = () => {
	if (open.length === 1 && fragment) return fragment;
	return cur();
};

/**
 * Appends a node, merging it into the previous sibling when both are text.
 * @param {HtmlNodeRef} parent container
 * @param {HtmlNodeRef} node node to append
 * @returns {void}
 */
const appendTo = (parent, node) => {
	const p = effParent(parent);
	const last = _nodeLastChildren[p];
	if (
		_S._nodeTypes[node] === NodeType.Text &&
		last !== 0 &&
		_S._nodeTypes[last] === NodeType.Text
	) {
		_nodeStrings[last] += _nodeStrings[node];
		_nodeEnds[last] = _nodeEnds[node];
		return;
	}
	_appendChild(p, 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: 0 };
/**
 * @param {HtmlNodeRef} parent container
 * @param {HtmlNodeRef} beforeNode node to insert before (`0` = append)
 * @returns {InsertionPlace} the shared insertion place
 */
const placeAt = (parent, beforeNode) => {
	sharedPlace.parent = parent;
	sharedPlace.beforeNode = beforeNode;
	return sharedPlace;
};

/**
 * §13.2.6.1 "appropriate place for inserting a node", the foster-parenting
 * branch: where a node goes when the target is a table context — inside the
 * innermost `<template>` when one stands above the innermost `<table>`, else
 * before that table, and at the root when neither is open.
 * @returns {[HtmlNodeRef, HtmlNodeRef]} the parent and the node to insert before (`0` = append)
 */
const _fosterPlace = () => {
	// The innermost of each still open, found in one walk down the stack.
	let lastTemplate = -1;
	let lastTable = -1;
	for (let i = open.length - 1; i >= 0; i--) {
		if (_namespaceOf(open[i]) !== NS_HTML) continue;
		const name = _tagNameOf(open[i]);
		if (lastTemplate === -1 && name === "template") lastTemplate = i;
		else if (lastTable === -1 && name === "table") lastTable = i;
	}
	if (lastTemplate !== -1 && (lastTable === -1 || lastTemplate > lastTable)) {
		return [open[lastTemplate], 0];
	}
	if (lastTable === -1) return [open[0], 0];
	const table = open[lastTable];
	const parent = _nodeParents[table];
	// A table already taken out of the tree fosters before the element that
	// stood under it instead.
	return parent !== 0 ? [parent, table] : [open[lastTable - 1], 0];
};

// "appropriate place for inserting a node"
const appropriatePlace = () => {
	const target = cur();
	if (
		fosterParenting &&
		TABLE_CONTEXT.has(_tagNameOf(target)) &&
		_namespaceOf(target) === NS_HTML
	) {
		const [parent, beforeNode] = _fosterPlace();
		return placeAt(parent, beforeNode);
	}
	return placeAt(target, 0);
};

/**
 * Inserts a node at an insertion place, appending when `beforeNode` is `0`.
 * @param {InsertionPlace} place where the node goes
 * @param {HtmlNodeRef} node node to insert
 * @returns {void}
 */
const insertAtPlace = (place, node) => {
	const before = place.beforeNode;
	if (before !== 0) {
		const p = effParent(place.parent);
		// Find `before`'s previous sibling (insert-before is a rare foster/
		// adoption path, so the sibling scan stays off the hot path).
		let prev = 0;
		let c = _S._nodeFirstChildren[p];
		while (c !== 0 && c !== before) {
			prev = c;
			c = _S._nodeNextSiblings[c];
		}
		if (c === 0) {
			// `before` not under `parent` (not reachable from the spec paths).
			appendTo(place.parent, node);
			return;
		}
		if (
			_S._nodeTypes[node] === NodeType.Text &&
			prev !== 0 &&
			_S._nodeTypes[prev] === NodeType.Text
		) {
			// Before-node merge deliberately does not bump the sibling's `end`.
			_nodeStrings[prev] += _nodeStrings[node];
			return;
		}
		_nodeParents[node] = p;
		_S._nodeNextSiblings[node] = before;
		if (prev === 0) _S._nodeFirstChildren[p] = node;
		else _S._nodeNextSiblings[prev] = node;
		if (_namespaceOf(before) === NS_HTML && _tagNameOf(before) === "table") {
			_recordFostered(before, node);
		}
	} else {
		appendTo(place.parent, node);
	}
};

/**
 * Remember that `node` was foster parented out of `table`. The parent it landed
 * in is not recorded: the adoption agency can move it elsewhere afterwards, so
 * only the tree answers that.
 * @param {HtmlNodeRef} table the table it was written inside
 * @param {HtmlNodeRef} node the fostered node
 * @returns {void}
 */
const _recordFostered = (table, node) => {
	if (_fosteredLog === null) _fosteredLog = [];
	_fosteredLog.push(table, node);
};

/**
 * Inserts a character run at the appropriate place (§13.2.6.1).
 * @param {string} data decoded text
 * @param {number} start source offset of the run
 * @param {number} end source offset past the run
 * @returns {void}
 */
const insertCharacters = (data, start, end) => {
	const place = appropriatePlace();
	if (_S._nodeTypes[place.parent] === NodeType.Document) return; // never insert text into document
	// `skip.text` drops every `Text` node, which construction has already used,
	// so element structure is unaffected. A raw-text element instead records its
	// body end, giving a consumer the span [`tagEnd`, `contentEnd`].
	if (skipText) {
		const p = place.parent;
		if (
			_S._nodeTypes[p] === NodeType.Element &&
			RAW_TEXT_ELEMENTS.has(_tagNameOf(p))
		) {
			_nodeContentEnds[p] = end;
		}
		return;
	}
	// Inlined text insert: a run merging into the adjacent text sibling, common
	// with inline formatting, appends the string alone rather than allocating a
	// throwaway node. Mirrors `insertAtPlace` / `appendTo`, `end` included.
	const p = effParent(place.parent);
	const before = place.beforeNode;
	if (before !== 0) {
		let prev = 0;
		let c = _S._nodeFirstChildren[p];
		while (c !== 0 && c !== before) {
			prev = c;
			c = _S._nodeNextSiblings[c];
		}
		if (c === 0) {
			// `before` not under `parent` (not reachable from the spec paths).
			_appendChild(p, _makeTextNode(data, start, end));
			return;
		}
		if (prev !== 0 && _S._nodeTypes[prev] === NodeType.Text) {
			_nodeStrings[prev] += data;
			return;
		}
		const node = _makeTextNode(data, start, end);
		_nodeParents[node] = p;
		_S._nodeNextSiblings[node] = before;
		if (prev === 0) _S._nodeFirstChildren[p] = node;
		else _S._nodeNextSiblings[prev] = node;
		if (_namespaceOf(before) === NS_HTML && _tagNameOf(before) === "table") {
			_recordFostered(before, node);
		}
	} else {
		const last = _nodeLastChildren[p];
		if (last !== 0 && _S._nodeTypes[last] === NodeType.Text) {
			_nodeStrings[last] += data;
			_nodeEnds[last] = end;
			return;
		}
		_appendChild(p, _makeTextNode(data, start, end));
	}
};

/**
 * Every insertion mode inserts a processing instruction where it inserts a
 * comment (§13.2.6.4). `skip.comments` drops comments alone.
 * @param {CommentToken | ProcessingInstructionToken} t comment or processing instruction token
 * @param {InsertionPlace=} place explicit insertion place
 * @returns {void}
 */
const insertCommentOrProcessingInstruction = (t, place) => {
	const isPi = t.type === TOKEN_PROCESSING_INSTRUCTION;
	if (skipComments && !isPi) return;
	const p = place || appropriatePlace();
	insertAtPlace(
		p,
		isPi
			? _makeProcessingInstructionNode(t.name, t.data, t.start, t.end)
			: _makeCommentNode(t.data, t.start, t.end)
	);
};

/**
 * Inserts an HTML element and pushes it onto the open element stack.
 * @param {string} tagName tag name
 * @param {AttributeRun} attrs the element's attributes
 * @param {TagPos | null} pos source position, `null` for a synthesized element
 * @returns {HtmlElement} the inserted element
 */
const insertHtmlElement = (tagName, attrs, pos) => {
	const el = mkEl(tagName, NS_HTML, attrs, pos);
	const place = appropriatePlace();
	insertAtPlace(place, el);
	open.push(el);
	return el;
};

/**
 * Inserts a MathML/SVG element and pushes it onto the open element stack.
 * @param {string} tagName tag name
 * @param {number} ns namespace flag (`NS_MATHML`, `NS_SVG`)
 * @param {AttributeRun} attrs the element's attributes
 * @param {TagPos | null} pos source position, `null` for a synthesized element
 * @returns {HtmlElement} the inserted element
 */
const insertForeignElement = (tagName, ns, attrs, pos) => {
	const el = mkEl(tagName, ns, attrs, pos);
	const place = appropriatePlace();
	insertAtPlace(place, el);
	open.push(el);
	return el;
};

// ---- scopes ----
// Scope "kind" selects which extra elements act as boundaries, as a small int so
// the checks below — several per body tag — allocate no predicate closure.
const SCOPE_DEFAULT = 0;
const SCOPE_BUTTON = 1;
const SCOPE_LIST_ITEM = 2;
/**
 * @param {HtmlElement} el element to test
 * @param {number} kind scope kind (`SCOPE_DEFAULT`, `SCOPE_BUTTON`, `SCOPE_LIST_ITEM`)
 * @returns {boolean} true when the element bounds that scope
 */
const isBoundaryForKind = (el, kind) => {
	if (isScopeBoundary(el)) return true;
	if (_namespaceOf(el) !== NS_HTML) return false;
	if (kind === SCOPE_BUTTON) return _tagNameOf(el) === "button";
	if (kind === SCOPE_LIST_ITEM) {
		return _tagNameOf(el) === "ol" || _tagNameOf(el) === "ul";
	}
	return false;
};
/**
 * "have an element in scope": walks the open stack from the top until the
 * named HTML element is found (true) or a scope boundary is hit (false).
 * @param {string} tagName tag name to look for
 * @param {number} kind scope kind (`SCOPE_DEFAULT`, `SCOPE_BUTTON`, `SCOPE_LIST_ITEM`)
 * @returns {boolean} true when the element is in that scope
 */
const hasNameInScope = (tagName, kind) => {
	for (let i = open.length - 1; i >= 0; i--) {
		const el = open[i];
		if (_namespaceOf(el) === NS_HTML && _tagNameOf(el) === tagName) {
			return true;
		}
		if (isBoundaryForKind(el, kind)) return false;
	}
	return false;
};
/**
 * @param {string} tagName tag name to look for
 * @returns {boolean} true when the element is in scope
 */
const inScope = (tagName) => hasNameInScope(tagName, SCOPE_DEFAULT);
/**
 * @param {string} tagName tag name to look for
 * @returns {boolean} true when the element is in button scope
 */
const inButtonScope = (tagName) => hasNameInScope(tagName, SCOPE_BUTTON);
/**
 * @param {string} tagName tag name to look for
 * @returns {boolean} true when the element is in list item scope
 */
const inListItemScope = (tagName) => hasNameInScope(tagName, SCOPE_LIST_ITEM);
/**
 * @param {HtmlElement} target element to look for
 * @returns {boolean} true when that element itself is in scope
 */
const inScopeEl = (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;
};
/**
 * @param {string | Set<string>} target a single tag name (the common case) or a set of names
 * @returns {boolean} true when one of them is in table scope
 */
const inTableScope = (target) => {
	const set = typeof target === "string" ? null : target;
	for (let i = open.length - 1; i >= 0; i--) {
		const el = open[i];
		if (_namespaceOf(el) === NS_HTML) {
			if (set ? set.has(_tagNameOf(el)) : _tagNameOf(el) === target) {
				return true;
			}
			if (TABLE_SCOPE_STOP.has(_tagNameOf(el))) return false;
		}
	}
	return false;
};
const generateImpliedEndTags = (except = "") => {
	while (open.length) {
		const el = cur();
		if (
			_namespaceOf(el) === NS_HTML &&
			IMPLIED.has(_tagNameOf(el)) &&
			_tagNameOf(el) !== except
		) {
			popOpen();
		} else {
			break;
		}
	}
};
const generateImpliedEndTagsThorough = () => {
	while (open.length) {
		const el = cur();
		if (_namespaceOf(el) === NS_HTML && IMPLIED_THOROUGH.has(_tagNameOf(el))) {
			popOpen();
		} else {
			break;
		}
	}
};

// ---- active formatting elements ----

/**
 * `splice` allocates an array for the removed element on every call; the list
 * is short, so shifting in place is both cheaper and allocation-free.
 * @param {number} i position in `afe` to remove
 * @returns {void}
 */
const afeRemoveAt = (i) => {
	const last = afe.length - 1;
	for (let k = i; k < last; k++) afe[k] = afe[k + 1];
	afe.pop();
};
/**
 * Pushes a formatting element, applying the "Noah's Ark" clause: a third
 * equal entry since the last marker drops the earliest of them.
 * @param {HtmlElement} el formatting element to push
 * @returns {void}
 */
const pushAfe = (el) => {
	let count = 0;
	for (let i = afe.length - 1; i >= 0; i--) {
		const e = afe[i];
		if (e === AFE_MARKER) break;
		if (
			_tagNameOf(e) === _tagNameOf(el) &&
			_namespaceOf(e) === _namespaceOf(el) &&
			sameAttrs(e, el)
		) {
			count++;
			if (count === 3) {
				afeRemoveAt(i);
				break;
			}
		}
	}
	afe.push(el);
};
const insertMarker = () => afe.push(AFE_MARKER);
const clearAfeToMarker = () => {
	while (afe.length) {
		if (afe.pop() === AFE_MARKER) break;
	}
};
const reconstructAfe = () => {
	if (afe.length === 0) return;
	let i = afe.length - 1;
	if (afe[i] === AFE_MARKER || open.includes(afe[i])) return;
	while (i > 0) {
		i--;
		if (afe[i] === AFE_MARKER || open.includes(afe[i])) {
			i++;
			break;
		}
	}
	for (; i < afe.length; i++) {
		const e = afe[i];
		// §13.2.6.4.7 gives `a` and `nobr` a start-tag rule of their own; every other
		// formatting element re-nests as written.
		_printFromSource = true;
		const el = mkEl(_tagNameOf(e), _namespaceOf(e), cloneAttrs(e), null);
		const place = appropriatePlace();
		insertAtPlace(place, el);
		open.push(el);
		afe[i] = el;
	}
};

// ---- close p ----
const closePElement = () => {
	generateImpliedEndTags("p");
	// pop until a p has been popped
	while (open.length) {
		const el = /** @type {HtmlElement} */ (popOpen());
		if (_namespaceOf(el) === NS_HTML && _tagNameOf(el) === "p") {
			if (tok.type === TOKEN_END_TAG) _nodeEnds[el] = tokenEnd;
			break;
		}
	}
};

/**
 * Pops the open element stack through the named HTML element.
 * @param {string} tagName tag name to pop through
 * @returns {void}
 */
const popUntil = (tagName) => {
	while (open.length) {
		const el = /** @type {HtmlElement} */ (popOpen());
		if (_namespaceOf(el) === NS_HTML && _tagNameOf(el) === tagName) {
			if (tok.type === TOKEN_END_TAG) _nodeEnds[el] = tokenEnd;
			break;
		}
	}
};
/**
 * Pops the open element stack through the first of the named HTML elements.
 * @param {Set<string>} set tag names to pop through
 * @returns {void}
 */
const popUntilOneOf = (set) => {
	while (open.length) {
		const el = /** @type {HtmlElement} */ (popOpen());
		if (_namespaceOf(el) === NS_HTML && set.has(_tagNameOf(el))) {
			if (tok.type === TOKEN_END_TAG) _nodeEnds[el] = tokenEnd;
			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 = _tagNameOf(node);
		if (_namespaceOf(node) === 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 = _S.head ? MODE_AFTER_HEAD : MODE_BEFORE_HEAD;
				return;
			}
		}
		if (last) {
			mode = MODE_IN_BODY;
			return;
		}
	}
};

// ---------- token processing ----------
/**
 * Splits a character token's leading whitespace; per the spec each character
 * is its own token, so a mixed run can straddle a mode change.
 * @param {CharToken} t character token
 * @param {boolean} insert whether to insert the leading whitespace
 * @returns {CharToken | null} the non-whitespace remainder, `null` when the token was all whitespace
 */
const leadingWs = (t, insert) => {
	const m = /^[\t\n\f\r ]+/.exec(t.data);
	const ws = m ? m[0] : "";
	// `t.data` is decoded and the offsets are raw, so counting one in the other
	// lines up only where decoding changed no length. Otherwise the whole span is
	// kept: too wide is harmless, too narrow names source that is not this text.
	const aligned = t.end - t.start === t.data.length;
	if (ws && insert) {
		insertCharacters(ws, t.start, aligned ? t.start + ws.length : t.end);
	}
	if (ws.length === t.data.length) return null;
	return {
		...t,
		data: t.data.slice(ws.length),
		start: aligned ? t.start + ws.length : t.start
	};
};

/**
 * Runs one token through tree construction (§13.2.6).
 * @param {Token} t token to process
 * @returns {void}
 */
const process = (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 &&
		_namespaceOf(ac) !== NS_HTML &&
		ty !== TOKEN_EOF &&
		shouldUseForeignRules(ac, t);
	if (useForeign) {
		foreignContent(t);
		return;
	}
	runMode(t);
};

/**
 * XML has no insertion modes: elements are appended to the current element
 * and an end tag closes the element with the same case-sensitive name.
 * @param {Token} t token to process
 * @returns {void}
 */
const processXml = (t) => {
	const ty = t.type;
	if (_xmlEntityRangeStart !== -1) {
		if (ty === TOKEN_START_TAG || ty === TOKEN_END_TAG) {
			t.pos.start = _xmlEntityRangeStart;
			t.pos.end = _xmlEntityRangeEnd;
			t.pos.tagEnd = _xmlEntityRangeEnd;
			t.pos.nameEnd = 0;
		} else if (ty !== TOKEN_EOF) {
			const located = /** @type {CharToken} */ (t);
			located.start = _xmlEntityRangeStart;
			located.end = _xmlEntityRangeEnd;
		}
	}
	if (ty === TOKEN_START_TAG || ty === TOKEN_END_TAG) tokenEnd = t.pos.end;
	else if (ty !== TOKEN_EOF) tokenEnd = t.end;
	if (ty === TOKEN_CHAR) {
		if (open.length === 0) return;
		insertCharacters(t.data, t.start, t.end);
		return;
	}
	if (isCommentOrProcessingInstruction(t)) {
		insertCommentOrProcessingInstruction(
			t,
			open.length === 0 ? { parent: doc, beforeNode: 0 } : undefined
		);
		return;
	}
	if (ty === TOKEN_DOCTYPE) {
		if (!skipDoctype) {
			const dt = _allocNode(NodeType.Doctype, t.start, t.end);
			_nodeStrings[dt] = t.name;
			_doctypePublicId = t.publicId;
			_doctypeSystemId = t.systemId;
			_doctypePublicStart = t.publicStart;
			_doctypePublicEnd = t.publicEnd;
			_doctypeSystemStart = t.systemStart;
			_doctypeSystemEnd = t.systemEnd;
			_appendChild(doc, dt);
		}
		return;
	}
	if (ty === TOKEN_START_TAG) {
		if (_xmlAttributeDeclarations !== null) {
			const declarations = _xmlAttributeDeclarations.get(t.name);
			if (declarations !== undefined) {
				_applyXmlAttributeDeclarations(t.attrs, declarations);
			}
		}
		for (let i = t.attrs.start; i < t.attrs.start + t.attrs.count; i++) {
			_xmlAttributeValue(i);
		}
		const el = mkEl(t.name, NS_XML, t.attrs, t.pos);
		if (t.selfClosing) _S._nodeFlags[el] |= FLAG_SELF_CLOSING;
		appendTo(open.length === 0 ? doc : cur(), el);
		if (!t.selfClosing) open.push(el);
		return;
	}
	if (ty === TOKEN_END_TAG) {
		// A misnested end tag closes the element it names and the ones opened
		// inside it; one naming no open element is dropped.
		for (let i = open.length - 1; i >= 0; i--) {
			if (_tagNameOf(open[i]) !== t.name) continue;
			while (open.length > i + 1) popOpen();
			_nodeEnds[popOpen()] = t.pos.end;
			return;
		}
	}
};

/**
 * @param {HtmlElement} ac adjusted current node
 * @param {Token} t token to process
 * @returns {boolean} true when the token follows the foreign content rules
 */
const shouldUseForeignRules = (ac, t) => {
	if (_namespaceOf(ac) === NS_HTML) return false;
	if (t.type === TOKEN_START_TAG) {
		if (
			mathmlTextIntegrationPoint(ac) &&
			t.name !== "mglyph" &&
			t.name !== "malignmark"
		) {
			return false;
		}
		if (
			_namespaceOf(ac) === NS_MATHML &&
			_tagNameOf(ac) === "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 (isCommentOrProcessingInstruction(t)) return true;
	return false;
};

/**
 * "in foreign content" rules (§13.2.6.5).
 * @param {Token} t token to process
 * @returns {void}
 */
const foreignContent = (t) => {
	if (t.type === TOKEN_CHAR) {
		const data = inputHasNul ? t.data.replace(/\0/g, "�") : t.data;
		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 (isCommentOrProcessingInstruction(t)) {
		insertCommentOrProcessingInstruction(t);
		return;
	}
	if (t.type === TOKEN_DOCTYPE) return;
	if (t.type === TOKEN_START_TAG) {
		const acn = _namespaceOf(adjustedCurrent());
		if (
			FOREIGN_BREAKOUT.has(t.name) ||
			(t.name === "font" && hasFontBreakoutAttr(t.attrs))
		) {
			// parse error; pop until integration point / html / mathml-text-integration
			while (open.length > 1) {
				const c = cur();
				if (
					_namespaceOf(c) === NS_HTML ||
					mathmlTextIntegrationPoint(c) ||
					htmlIntegrationPoint(c)
				) {
					break;
				}
				popOpen();
			}
			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) {
			popOpen();
		}
		return;
	}
	if (t.type === TOKEN_END_TAG) {
		if (
			t.name === "script" &&
			_tagNameOf(cur()) === "script" &&
			_namespaceOf(cur()) === NS_SVG
		) {
			_nodeEnds[popOpen()] = tokenEnd;
			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 &&
				_namespaceOf(cur()) !== NS_HTML &&
				!mathmlTextIntegrationPoint(cur()) &&
				!htmlIntegrationPoint(cur())
			) {
				popOpen();
			}
			runMode(t);
			return;
		}
		// any other end tag
		let i = open.length - 1;
		let node = open[i];
		if (_tagNameOf(node).toLowerCase() !== t.name) {
			/* parse error */
		}
		while (i >= 0) {
			node = open[i];
			if (i === 0) return;
			if (
				_namespaceOf(node) !== NS_HTML &&
				_tagNameOf(node).toLowerCase() === t.name
			) {
				while (open.length > i) popOpen();
				_nodeEnds[node] = tokenEnd;
				return;
			}
			i--;
			if (open[i] && _namespaceOf(open[i]) === NS_HTML) {
				runMode(t);
				return;
			}
		}
	}
};

// ---------- adoption agency algorithm ----------
/**
 * Adoption agency algorithm (§13.2.6.4.7).
 * @param {string} subject tag name of the end tag being handled
 * @param {TagPos | null} pos source position of that end tag
 * @returns {boolean} false when the caller must fall back to "any other end tag"
 */
const adoptionAgency = (subject, pos) => {
	// step 1
	const c = cur();
	if (
		_namespaceOf(c) === NS_HTML &&
		_tagNameOf(c) === subject &&
		!afe.includes(c)
	) {
		popOpen();
		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] === AFE_MARKER) {
				if (_afeNameBelowMarker(i, subject)) _printFromSource = true;
				break;
			}
			if (_tagNameOf(afe[i]) === subject && _namespaceOf(afe[i]) === NS_HTML) {
				fmtIdx = i;
				break;
			}
		}
		if (fmtIdx === -1) return false; // act as any other end tag
		const fmt = afe[fmtIdx];
		const openIdx = open.indexOf(fmt);
		if (openIdx === -1) {
			afeRemoveAt(fmtIdx);
			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) {
				_nodeEnds[open[open.length - 1]] = tokenEnd;
				popOpen();
			}
			afeRemoveAt(fmtIdx);
			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;
			let nodeAfeIdx = afe.indexOf(node);
			if (inner > 3 && nodeAfeIdx !== -1) {
				afeRemoveAt(nodeAfeIdx);
				if (nodeAfeIdx < bookmark) bookmark--;
				nodeAfeIdx = -1;
			}
			if (nodeAfeIdx === -1) {
				removeOpenAt(nodeIdx);
				continue;
			}
			// create clone
			const clone = mkEl(
				_tagNameOf(node),
				_namespaceOf(node),
				cloneAttrs(node),
				null
			);
			afe[nodeAfeIdx] = 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
		_printFromSource = true;
		const cloneFmt = mkEl(
			_tagNameOf(fmt),
			_namespaceOf(fmt),
			cloneAttrs(fmt),
			null
		);
		// Take the direct children of `furthest`; a template's content fragment
		// stays put. A declarative shadow root is not one of them — the engine took
		// it out of the tree on attaching, so it stays with its host.
		let k = _S._nodeFirstChildren[furthest];
		_S._nodeFirstChildren[furthest] = 0;
		_nodeLastChildren[furthest] = 0;
		while (k !== 0) {
			const next = _S._nodeNextSiblings[k];
			_S._nodeNextSiblings[k] = 0;
			_nodeParents[k] = 0;
			appendTo(
				(_S._nodeFlags[k] & FLAG_SHADOW_ROOT) === 0 ? cloneFmt : furthest,
				k
			);
			k = next;
		}
		appendTo(furthest, cloneFmt);
		// remove fmt from afe, insert clone at bookmark
		const curFmtIdx = afe.indexOf(fmt);
		if (curFmtIdx !== -1) {
			afeRemoveAt(curFmtIdx);
			if (curFmtIdx < bookmark) bookmark--;
		}
		afe.splice(bookmark, 0, cloneFmt);
		// remove fmt from open, insert clone below furthest
		const ofi = open.indexOf(fmt);
		if (ofi !== -1) removeOpenAt(ofi);
		const newFurthestIdx = open.indexOf(furthest);
		open.splice(newFurthestIdx + 1, 0, cloneFmt);
		_openSpliced = true;
		_openStackChanged = true;
	}
	return true;
};

/**
 * @param {HtmlElement} commonAncestor common ancestor node of the adoption agency
 * @returns {InsertionPlace} where the moved node goes
 */
const placeForCommonAncestor = (commonAncestor) => {
	if (
		TABLE_CONTEXT.has(_tagNameOf(commonAncestor)) &&
		_namespaceOf(commonAncestor) === NS_HTML
	) {
		// The same place `appropriatePlace` fosters to, spelled as its own object:
		// the adoption agency holds this one while it moves nodes about, so the
		// shared one would be overwritten under it.
		const [parent, beforeNode] = _fosterPlace();
		return { parent, beforeNode };
	}
	return { parent: commonAncestor, beforeNode: 0 };
};

/**
 * Unlinks a node from its parent so it can be inserted again.
 * @param {HtmlNodeRef} node node to detach
 * @returns {void}
 */
const detach = (node) => {
	const p = _nodeParents[node];
	if (p === 0) return;
	let prev = 0;
	let c = _S._nodeFirstChildren[p];
	while (c !== 0 && c !== node) {
		prev = c;
		c = _S._nodeNextSiblings[c];
	}
	if (c === 0) return;
	if (prev === 0) _S._nodeFirstChildren[p] = _S._nodeNextSiblings[node];
	else _S._nodeNextSiblings[prev] = _S._nodeNextSiblings[node];
	if (_nodeLastChildren[p] === node) _nodeLastChildren[p] = prev;
	_S._nodeNextSiblings[node] = 0;
	_nodeParents[node] = 0;
};

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

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

/**
 * Dispatches the current insertion mode. An integer switch keeps each
 * `modes.x(t)` call site monomorphic, where `modes[mode]` would not.
 * @param {Token} t token to process
 * @returns {void}
 */
const runMode = (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);
	}
};

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 (isCommentOrProcessingInstruction(t)) {
		insertCommentOrProcessingInstruction(t, { parent: doc, beforeNode: 0 });
		return;
	}
	if (t.type === TOKEN_DOCTYPE) {
		// `skip.doctype` drops the node only; quirks detection below is unaffected.
		if (!skipDoctype) {
			const dt = _allocNode(NodeType.Doctype, t.start, t.end);
			_nodeStrings[dt] = t.name;
			// At most one doctype node is ever inserted (later doctype tokens
			// are ignored), so its ids live in two per-parse scalars.
			_doctypePublicId = t.publicId;
			_doctypeSystemId = t.systemId;
			_doctypePublicStart = t.publicStart;
			_doctypePublicEnd = t.publicEnd;
			_doctypeSystemStart = t.systemStart;
			_doctypeSystemEnd = t.systemEnd;
			_appendChild(doc, dt);
		}
		quirks = t.forceQuirks || 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 (isCommentOrProcessingInstruction(t)) {
		insertCommentOrProcessingInstruction(t, { parent: doc, beforeNode: 0 });
		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);
		_appendChild(doc, 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, EMPTY_ATTRS, null);
	_appendChild(doc, 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 (isCommentOrProcessingInstruction(t)) {
		insertCommentOrProcessingInstruction(t);
		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") {
		_S.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;
	}
	_S.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;
		popOpen();
		mode = MODE_AFTER_HEAD;
		process(r);
		return;
	}
	if (isCommentOrProcessingInstruction(t)) {
		insertCommentOrProcessingInstruction(t);
		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);
			popOpen();
			return;
		}
		if (t.name === "title") {
			genericRcdata(t);
			return;
		}
		if (NOFRAMES_STYLE_NOSCRIPT.has(t.name)) {
			if (t.name === "noscript" && !scripting) {
				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();
			const fragment = _allocNode(NodeType.DocumentFragment, 0, 0);
			_nodeStrings[fragment] = "";
			// Parent link so the iterative walk can ascend out of the content.
			_nodeParents[fragment] = el;
			_nodeContentEnds[el] = fragment;
			_S._nodeFlags[el] |= FLAG_HAS_TEMPLATE;
			// The engine attaches this content to the parent as a shadow root and
			// drops the template from the tree; the one thing that leaves behind is
			// that the tree construction can no longer move it away from its host.
			const host = _nodeParents[el];
			const shadowRootMode = _findAttr(
				t.attrs.start,
				t.attrs.count,
				"shadowrootmode"
			);
			if (
				shadowRootMode !== 0 &&
				host !== 0 &&
				host !== open[0] &&
				(_S._nodeFlags[host] & FLAG_SHADOW_HOST) === 0 &&
				_S._nodeTypes[host] === NodeType.Element &&
				_isShadowHost(host)
			) {
				const mode = _attrValueDecoded(shadowRootMode).toLowerCase();
				if (mode === "open" || mode === "closed") {
					_S._nodeFlags[el] |= FLAG_SHADOW_ROOT;
					_S._nodeFlags[host] |= FLAG_SHADOW_HOST;
				}
			}
			return;
		}
		if (t.name === "head") return;
	}
	if (t.type === TOKEN_END_TAG) {
		if (t.name === "head") {
			sourceShellEnds |= SHELL_END_HEAD;
			popOpen();
			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
	popOpen();
	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") {
		popOpen();
		mode = MODE_IN_HEAD;
		return;
	}
	if (t.type === TOKEN_CHAR && isAllAsciiWhitespace(t.data)) {
		return modes.inHead(t);
	}
	if (isCommentOrProcessingInstruction(t)) 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;
	}
	popOpen();
	mode = MODE_IN_HEAD;
	process(t);
};

modes.afterHead = (t) => {
	if (t.type === TOKEN_CHAR) {
		const r = leadingWs(t, true);
		if (!r) return;
		insertHtmlElement("body", EMPTY_ATTRS, null);
		framesetOk = true;
		mode = MODE_IN_BODY;
		process(r);
		return;
	}
	if (isCommentOrProcessingInstruction(t)) {
		insertCommentOrProcessingInstruction(t);
		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 = _S.head;
			open.push(headEl);
			modes.inHead(t);
			const idx = open.indexOf(headEl);
			if (idx !== -1) removeOpenAt(idx);
			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", EMPTY_ATTRS, null);
	framesetOk = true;
	mode = MODE_IN_BODY;
	process(t);
};

modes.inBody = (t) => {
	if (t.type === TOKEN_CHAR) {
		// Deliberately not guarded by `inputHasNul`: this `includes` flattens the
		// decoded rope, and skipping it makes the inserts below allocate more.
		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);
		// After the implicit-body reset in "after head", `framesetOk` only goes
		// true→false, so once false skip this per-text-token whitespace scan.
		if (framesetOk && !isAllAsciiWhitespace(t.data)) framesetOk = false;
		return;
	}
	if (isCommentOrProcessingInstruction(t)) {
		insertCommentOrProcessingInstruction(t);
		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: pops to the matching open element, stopping at
 * the first special element; also the adoption agency's no-element fallback.
 * @param {string} name tag name of the end tag
 * @returns {void}
 */
const anyOtherEndTag = (name) => {
	for (let i = open.length - 1; i >= 0; i--) {
		const node = open[i];
		if (_namespaceOf(node) === NS_HTML && _tagNameOf(node) === name) {
			generateImpliedEndTags(name);
			while (open.length > i) {
				_nodeEnds[open[open.length - 1]] = tokenEnd;
				popOpen();
			}
			return;
		}
		if (isSpecial(node)) return;
	}
};

/**
 * "in body" start tag rules (§13.2.6.4.7).
 * @param {StartTagToken} t start tag token
 * @returns {void}
 */
const startTagInBody = (t) => {
	const name = t.name;
	if (name === "html") {
		if (open.some(isHtmlTemplateEl)) {
			return;
		}
		mergeAttrs(open[0], t.attrs);
		return;
	}
	if (HEAD_ELEMENTS.has(name)) {
		return modes.inHead(t);
	}
	if (name === "body") {
		const second = open[1];
		if (
			!second ||
			_tagNameOf(second) !== "body" ||
			open.some(isHtmlTemplateEl)
		) {
			return;
		}
		framesetOk = false;
		mergeAttrs(second, t.attrs);
		return;
	}
	if (name === "frameset") {
		const second = open[1];
		if (!second || _tagNameOf(second) !== "body") return;
		if (!framesetOk) return;
		detach(second);
		while (open.length > 1) popOpen();
		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 (_namespaceOf(cur()) === NS_HTML && HEADING.has(_tagNameOf(cur()))) {
			popOpen();
		}
		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 && !parsingTemplateContents()) {
			return;
		}
		closeIfPInButtonScope();
		// The end tag this needs generates implied end tags where the source's did
		// not, so anything open between the two forms moves.
		if (_formResetWouldPop()) _printFromSource = true;
		const el = insertHtmlElement("form", t.attrs, t.pos);
		if (!parsingTemplateContents()) {
			form = el;
		}
		return;
	}
	if (name === "li") {
		framesetOk = false;
		for (let i = open.length - 1; i >= 0; i--) {
			const node = open[i];
			if (_namespaceOf(node) === NS_HTML && _tagNameOf(node) === "li") {
				generateImpliedEndTags("li");
				popUntil("li");
				break;
			}
			if (
				isSpecial(node) &&
				!(_namespaceOf(node) === NS_HTML && ADDRESS_DIV_P.has(_tagNameOf(node)))
			) {
				// A list-item-scope boundary (`ul`, `ol`, a table cell) keeps every item
				// below it out of reach of any `</li>`, whatever the print writes or drops.
				if (isBoundaryForKind(node, SCOPE_LIST_ITEM)) break;
				// Inlined rather than a call: this runs for every `<li>` in a list.
				for (let j = i - 1; j >= 0 && !_printFromSource; j--) {
					const below = open[j];
					if (_namespaceOf(below) === NS_HTML && _tagNameOf(below) === "li") {
						_printFromSource = true;
					}
				}
				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 (
				_namespaceOf(node) === NS_HTML &&
				(_tagNameOf(node) === "dd" || _tagNameOf(node) === "dt")
			) {
				generateImpliedEndTags(_tagNameOf(node));
				popUntil(_tagNameOf(node));
				break;
			}
			if (
				isSpecial(node) &&
				!(_namespaceOf(node) === NS_HTML && ADDRESS_DIV_P.has(_tagNameOf(node)))
			) {
				for (let j = i - 1; j >= 0 && !_printFromSource; j--) {
					const below = open[j];
					if (_namespaceOf(below) !== NS_HTML) continue;
					const bn = _tagNameOf(below);
					if (bn === "dd" || bn === "dt") _printFromSource = true;
				}
				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] === AFE_MARKER) {
				if (_afeNameBelowMarker(i, "a")) _printFromSource = true;
				break;
			}
			if (_tagNameOf(afe[i]) === "a") {
				adoptionAgency("a", t.pos);
				const idx = afe.findIndex(
					(e) => e !== AFE_MARKER && _tagNameOf(e) === "a"
				);
				if (idx !== -1) {
					const el = afe[idx];
					afeRemoveAt(idx);
					const oi = open.indexOf(el);
					if (oi !== -1) removeOpenAt(oi);
				}
				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);
		popOpen();
		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 &&
			_namespaceOf(fragment) === NS_HTML &&
			_tagNameOf(fragment) === "select"
		) {
			return;
		}
		reconstructAfe();
		insertHtmlElement("input", t.attrs, t.pos);
		popOpen();
		const ty = _findAttr(t.attrs.start, t.attrs.count, "type");
		if (ty === 0 || _attrValueDecoded(ty).toLowerCase() !== "hidden") {
			framesetOk = false;
		}
		return;
	}
	if (PARAM_SOURCE_TRACK.has(name)) {
		insertHtmlElement(name, t.attrs, t.pos);
		popOpen();
		return;
	}
	if (name === "hr") {
		closeIfPInButtonScope();
		// An `<hr>` is a `<select>`'s own separator, so inside one it leaves what
		// it was written in; outside one it is ordinary content and nests.
		if (inScope("select")) generateImpliedEndTags();
		insertHtmlElement("hr", t.attrs, t.pos);
		popOpen();
		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" || (name === "noscript" && scripting)) {
		genericRawtext(t);
		return;
	}
	if (name === "select") {
		reconstructAfe();
		if (inScope("select")) {
			generateImpliedEndTags();
			popUntil("select");
			resetInsertionMode();
			return;
		}
		insertHtmlElement("select", t.attrs, t.pos);
		framesetOk = false;
		return;
	}
	if (name === "optgroup" || name === "option") {
		// Inside a select these end what is open like a list item does, an
		// `<option>` sparing its `<optgroup>`; outside one only an option gives way.
		if (inScope("select")) {
			generateImpliedEndTags(name === "option" ? "optgroup" : "");
		} else if (
			_namespaceOf(cur()) === NS_HTML &&
			_tagNameOf(cur()) === "option"
		) {
			popOpen();
		}
		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) popOpen();
		return;
	}
	if (name === "svg") {
		reconstructAfe();
		const attrs = adjustForeignAttrs(t.attrs, NS_SVG);
		insertForeignElement("svg", NS_SVG, attrs, t.pos);
		if (t.selfClosing) popOpen();
		return;
	}
	if (IGNORED_BODY_TABLE_STARTS.has(name)) {
		return;
	}
	// any other start tag
	reconstructAfe();
	insertHtmlElement(name, t.attrs, t.pos);
};

/**
 * "in body" end tag rules (§13.2.6.4.7).
 * @param {EndTagToken} t end tag token
 * @returns {void}
 */
const endTagInBody = (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;
		sourceShellEnds |= name === "html" ? SHELL_END_HTML : SHELL_END_BODY;
		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 (!parsingTemplateContents()) {
			const node = form;
			form = 0;
			if (!node || !inScopeEl(node)) return;
			generateImpliedEndTags();
			const idx = open.indexOf(node);
			if (idx !== -1) removeOpenAt(idx);
		} 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)) {
		// With no such element in the list, §13.2.6.4.7 hands the token back rather
		// than swallowing it — which is what Noah's Ark leaves behind: an element
		// still open that the list no longer names.
		if (!adoptionAgency(name, t.pos)) anyOtherEndTag(name);
		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);
		popOpen();
		framesetOk = false;
		return;
	}
	anyOtherEndTag(name);
};

/**
 * Generic RAWTEXT: inserts the element and switches to "text" mode, which
 * appends the characters the tokenizer already emits and pops on the end tag.
 * @param {StartTagToken} t start tag token
 * @returns {void}
 */
const genericRawtext = (t) => {
	insertHtmlElement(t.name, t.attrs, t.pos);
	originalMode = mode;
	mode = MODE_TEXT;
};
/**
 * Generic RCDATA, as `genericRawtext` for the elements whose text is decoded.
 * @param {StartTagToken} t start tag token
 * @param {boolean=} swallow whether a newline right after the tag is dropped
 * @returns {void}
 */
const genericRcdata = (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) popOpen();
		mode = originalMode;
		process(t);
		return;
	}
	if (t.type === TOKEN_END_TAG) {
		// Rawtext rather than an end tag where it cannot close the current
		// element: a name that does not match, or one running to EOF with no
		// delimiter, which the tokenizer still emits as a partial tag.
		if (
			cur() &&
			(t.name !== _tagNameOf(cur()) || t.pos.end === t.pos.nameEnd)
		) {
			insertCharacters(
				_S.source.slice(t.pos.start, t.pos.end),
				t.pos.start,
				t.pos.end
			);
			return;
		}
		// span the close tag, like every other explicit-close pop
		_nodeEnds[popOpen()] = tokenEnd;
		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(_tagNameOf(c)) && _namespaceOf(c) === NS_HTML) {
			pendingTableChars = { list: [], hasNonWs: false };
			originalMode = mode;
			mode = MODE_IN_TABLE_TEXT;
			return process(t);
		}
	}
	if (isCommentOrProcessingInstruction(t)) {
		insertCommentOrProcessingInstruction(t);
		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.start, t.attrs.count, "type");
			if (ty !== 0 && _attrValueDecoded(ty).toLowerCase() === "hidden") {
				insertHtmlElement("input", t.attrs, t.pos);
				popOpen();
				return;
			}
		}
		if (name === "form") {
			const inTemplate = parsingTemplateContents();
			if (form && !inTemplate) {
				return;
			}
			const el = insertHtmlElement("form", t.attrs, t.pos);
			if (!inTemplate) form = el;
			popOpen();
			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 =
			inputHasNul && 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 (!isAllAsciiWhitespace(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 (_namespaceOf(c) === NS_HTML && CLEAR_TABLE.has(_tagNameOf(c))) {
			break;
		}
		popOpen();
	}
};
const clearStackToTableBodyContext = () => {
	while (open.length) {
		const c = cur();
		if (_namespaceOf(c) === NS_HTML && CLEAR_TABLE_BODY.has(_tagNameOf(c))) {
			break;
		}
		popOpen();
	}
};
const clearStackToTableRowContext = () => {
	while (open.length) {
		const c = cur();
		if (_namespaceOf(c) === NS_HTML && CLEAR_TABLE_ROW.has(_tagNameOf(c))) {
			break;
		}
		popOpen();
	}
};

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 (_tagNameOf(cur()) !== "colgroup") return;
		popOpen();
		mode = MODE_IN_TABLE;
		process(r);
		return;
	}
	if (isCommentOrProcessingInstruction(t)) {
		insertCommentOrProcessingInstruction(t);
		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);
		popOpen();
		return;
	}
	if (t.type === TOKEN_END_TAG && t.name === "colgroup") {
		if (_tagNameOf(cur()) !== "colgroup") return;
		popOpen();
		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 (_tagNameOf(cur()) !== "colgroup") return;
	popOpen();
	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();
		popOpen();
		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();
		popOpen();
		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();
		popOpen();
		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();
		popOpen();
		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();
		popOpen();
		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 ||
		isCommentOrProcessingInstruction(t) ||
		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 && isAllAsciiWhitespace(t.data)) {
		return modes.inBody(t);
	}
	if (isCommentOrProcessingInstruction(t)) {
		insertCommentOrProcessingInstruction(t, {
			parent: open[0],
			beforeNode: 0
		});
		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;
		sourceShellEnds |= SHELL_END_HTML;
		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 (isCommentOrProcessingInstruction(t)) {
		insertCommentOrProcessingInstruction(t);
		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 (_tagNameOf(cur()) === "html") return;
		popOpen();
		if (!fragment && _tagNameOf(cur()) !== "frameset") {
			mode = MODE_AFTER_FRAMESET;
		}
		return;
	}
	if (t.type === TOKEN_START_TAG && t.name === "frame") {
		insertHtmlElement("frame", t.attrs, t.pos);
		popOpen();
		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 (isCommentOrProcessingInstruction(t)) {
		insertCommentOrProcessingInstruction(t);
		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 (isCommentOrProcessingInstruction(t)) {
		insertCommentOrProcessingInstruction(t, { parent: doc, beforeNode: 0 });
		return;
	}
	if (t.type === TOKEN_DOCTYPE) return modes.inBody(t);
	if (t.type === TOKEN_CHAR && isAllAsciiWhitespace(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 (isCommentOrProcessingInstruction(t)) {
		insertCommentOrProcessingInstruction(t, { parent: doc, beforeNode: 0 });
		return;
	}
	if (t.type === TOKEN_DOCTYPE) return modes.inBody(t);
	if (t.type === TOKEN_CHAR && isAllAsciiWhitespace(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);
	}
};

const dispatch = () => {
	process(/** @type {Token} */ (/** @type {Token | MutableToken} */ (tok)));
	// One boolean per token: only a stack change can complete a subtree.
	if (_streamEligible && (_openStackChanged || _openSpliced)) {
		_openStackChanged = false;
		if (_streaming) {
			_streamSync();
		} else if (_nodeCount >= _STREAM_MIN_NODES) {
			// Big enough to be worth recycling: start streaming here. The tree so
			// far is unvisited, so the first sync enters the open stack and flushes
			// everything already finished under it.
			_streaming = true;
			_streamSync();
		}
	}
};

// WHY: tokenizer callbacks hoisted to module scope, over the module-scope
// parser state, so `tokenize`'s indirect `callbacks.text(...)` / `openTag` /
// `attribute(...)` sites see one stable function identity across every parse.
// They stay monomorphic and V8 inlines them; a fresh per-parse closure made
// them megamorphic and blocked that. `parseHtml` refreshes only
// `fragmentContext` on the shared object before each tokenize.

/** @type {() => boolean} */
const currentNodeIsForeign = () => {
	const adjustedCurrentNode = adjustedCurrent();
	return (
		open.length > 0 &&
		adjustedCurrentNode !== 0 &&
		_namespaceOf(adjustedCurrentNode) !== NS_HTML
	);
};
/**
 * Whether the tree builder skips the tokenizer switch for this start tag, so
 * its content stays markup: foreign content, and every frameset-mode tag but
 * `noframes`, which those modes ignore rather than hand to the head rules.
 * @param {string} name the start tag's lowercased name
 * @returns {boolean} true when the tokenizer stays in the data state
 */
const vetoesContentMode = (name) => {
	if (currentNodeIsForeign()) return true;
	return (
		name !== "noframes" &&
		(mode === MODE_IN_FRAMESET ||
			mode === MODE_AFTER_FRAMESET ||
			mode === MODE_AFTER_AFTER_FRAMESET)
	);
};
/** @type {(input: string, code: string) => void} */
const handleParseError = (input, code) => {
	if (code === "eof-in-tag") eofInTag = true;
	else if (code === "unexpected-null-character") inputHasNul = true;
};
/**
 * @param {string} input input
 * @param {number} rangeStart start of the range
 * @param {number} rangeEnd end of the range
 * @returns {string} the range with NULLs replaced, per the DOCTYPE states
 */
const doctypePart = (input, rangeStart, rangeEnd) => {
	const raw = input.slice(rangeStart, rangeEnd);
	return inputHasNul ? raw.replace(/\0/g, "\uFFFD") : raw;
};
/** @type {(input: string, start: number, end: number, nameStart: number, nameEnd: number, publicStart: number, publicEnd: number, systemStart: number, systemEnd: number, forceQuirks: boolean) => number} */
const handleDoctypeToken = (
	input,
	start,
	end,
	nameStart,
	nameEnd,
	publicStart,
	publicEnd,
	systemStart,
	systemEnd,
	forceQuirks
) => {
	// Only the token immediately after the start tag may be the swallowed
	// newline; anything else clears the flag (§13.2.6.4.7 "the next token").
	swallowNextNewline = false;
	tok.type = TOKEN_DOCTYPE;
	// A missing name and an empty one are alike to every consumer here, since the
	// spec's name states always append a character. Folding is ASCII-only, as
	// those states lowercase upper alpha and append the rest as written.
	tok.name =
		nameStart === -1
			? ""
			: asciiLowercase(doctypePart(input, nameStart, nameEnd));
	tok.publicId =
		publicStart === -1 ? null : doctypePart(input, publicStart, publicEnd);
	tok.systemId =
		systemStart === -1 ? null : doctypePart(input, systemStart, systemEnd);
	tok.publicStart = publicStart;
	tok.publicEnd = publicEnd;
	tok.systemStart = systemStart;
	tok.systemEnd = systemEnd;
	tok.forceQuirks = forceQuirks;
	tok.start = start;
	tok.end = end;
	dispatch();
	return end;
};
// A `<?…>` bogus comment is a processing instruction when its target is
// well-formed (§13.2.5). The tokenizer reports the source range; the DOM-level
// distinction is drawn here, so `tokenize` keeps reporting one comment token.
const PI_SCAN_COMMENT = 0;
const PI_SCAN_MATCH = 1;
// Cut short by EOF — the spec emits only an end-of-file token, so no node.
const PI_SCAN_DROP = 2;
// Sub-ranges of the instruction last scanned, read only right after a match.
let _piScanTargetEnd = 0;
let _piScanDataStart = 0;
let _piScanDataEnd = 0;

/**
 * @param {string} input source
 * @param {number} start offset of the `<`
 * @param {number} end offset past the token
 * @returns {number} one of the `PI_SCAN_*` outcomes
 */
const _scanProcessingInstruction = (input, start, end) => {
	const targetStart = start + 2;
	// Nothing after `<?`, so EOF landed in the open state and drops the token.
	if (targetStart >= end) return PI_SCAN_DROP;
	const first = input.charCodeAt(targetStart);
	if (!isAsciiAlpha(first) && first !== CC_LOW_LINE) return PI_SCAN_COMMENT;
	let i = targetStart + 1;
	while (i < end) {
		const cc = input.charCodeAt(i);
		if (
			isAsciiWhitespace(cc) ||
			cc === CC_QUESTION_MARK ||
			cc === CC_GREATER_THAN
		) {
			break;
		}
		if (
			!isAsciiAlphanumeric(cc) &&
			cc !== CC_HYPHEN_MINUS &&
			cc !== CC_LOW_LINE
		) {
			return PI_SCAN_COMMENT;
		}
		i++;
	}
	// EOF while still reading the target drops the token, whatever it spells; a
	// target that ends at a delimiter is judged below first.
	if (i === end) return PI_SCAN_DROP;
	// Reserved for XML, and common enough in legacy pages to keep as comments.
	if (
		rangeAsciiCaseInsensitiveMatch(input, targetStart, i, "xml") ||
		rangeAsciiCaseInsensitiveMatch(input, targetStart, i, "xml-stylesheet")
	) {
		return PI_SCAN_COMMENT;
	}
	_piScanTargetEnd = i;
	// The rest ran into EOF instead of the closing `>`, so the token is dropped.
	if (input.charCodeAt(end - 1) !== CC_GREATER_THAN) return PI_SCAN_DROP;
	let contentEnd = end - 1;
	while (i < contentEnd && isAsciiWhitespace(input.charCodeAt(i))) i++;
	// A `?` right before the `>` closes the instruction instead of being data.
	if (contentEnd > i && input.charCodeAt(contentEnd - 1) === CC_QUESTION_MARK) {
		contentEnd--;
	}
	_piScanDataStart = i;
	_piScanDataEnd = contentEnd;
	return PI_SCAN_MATCH;
};

/** @type {(input: string, start: number, end: number, dataStart: number, dataEnd: number) => number} */
const handleCommentToken = (input, start, end, dataStart, dataEnd) => {
	// Only the token immediately after the start tag may be the swallowed
	// newline; anything else clears the flag (§13.2.6.4.7 "the next token").
	swallowNextNewline = false;
	// A CDATA section in foreign content arrives through this callback but is
	// character data, not a comment.
	if (
		input.startsWith("<![CDATA[", start) &&
		open.length > 0 &&
		adjustedCurrent() &&
		_namespaceOf(adjustedCurrent()) !== NS_HTML
	) {
		const data = input.slice(dataStart, dataEnd).replace(/\r\n?/g, "\n");
		if (data !== "") {
			tok.type = TOKEN_CHAR;
			// A NUL stays a NUL: foreign content replaces it without clearing
			// frameset-ok, and an HTML integration point drops it (§13.2.5.69).
			tok.data = data;
			tok.start = start;
			tok.end = end;
			dispatch();
		}
		return end;
	}
	if (input.charCodeAt(start + 1) === CC_QUESTION_MARK) {
		const scan = _scanProcessingInstruction(input, start, end);
		if (scan === PI_SCAN_DROP) return end;
		if (scan === PI_SCAN_MATCH) {
			tok.type = TOKEN_PROCESSING_INSTRUCTION;
			tok.name = input.slice(start + 2, _piScanTargetEnd);
			const piRaw = input.slice(_piScanDataStart, _piScanDataEnd);
			tok.data = inputHasNul ? piRaw.replace(/\0/g, "\uFFFD") : piRaw;
			tok.start = start;
			tok.end = end;
			dispatch();
			return end;
		}
	}
	tok.type = TOKEN_COMMENT;
	const commentRaw = input.slice(dataStart, dataEnd);
	tok.data = inputHasNul ? commentRaw.replace(/\0/g, "�") : commentRaw;
	tok.start = start;
	tok.end = end;
	dispatch();
	return end;
};
/** @type {(input: string, start: number, end: number) => number} */
const handleTextToken = (input, start, end) => {
	// `skip.text` drops the node, so only whitespace-ness matters. Absent `&`,
	// `\0`, `\r` and a pending newline-swallow the decoded value is the raw range,
	// so a canonical marker is dispatched without slicing or decoding.
	if (skipText && !swallowNextNewline) {
		let hasNonWhitespace = false;
		let i = start;
		for (; i < end; i++) {
			const charCode = input.charCodeAt(i);
			// 2 = break (& / NUL / CR), 1 = whitespace, 0 = other.
			const scanClass = charCode < 128 ? _TEXT_SCAN_CLASS[charCode] : 0;
			if (scanClass === 2) break;
			if (scanClass === 0) hasNonWhitespace = true;
		}
		if (i === end) {
			tok.type = TOKEN_CHAR;
			tok.data = hasNonWhitespace ? "x" : " ";
			tok.start = start;
			tok.end = end;
			dispatch();
			return end;
		}
	}
	const raw = input.slice(start, end);
	// CR normalization only when a CR is actually present (common case has none).
	const normalized =
		_S.inputHasCr && raw.includes("\r") ? raw.replace(/\r\n?/g, "\n") : raw;
	const top = adjustedCurrent();
	const rawMode =
		mode === MODE_TEXT ||
		(top && _namespaceOf(top) === NS_HTML && _tagNameOf(top) === "plaintext");
	const noDecode =
		top &&
		_namespaceOf(top) === NS_HTML &&
		(NO_DECODE_TEXT.has(_tagNameOf(top)) ||
			(scripting && _tagNameOf(top) === "noscript")) &&
		(mode === MODE_TEXT || mode === MODE_IN_BODY);
	let data = noDecode ? normalized : decode(normalized, 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 && inputHasNul && 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;
};
/** @type {(input: string, nameStart: number, nameEnd: number, valueStart: number, valueEnd: number, quoteType: number) => number} */
const handleAttributeToken = (
	input,
	nameStart,
	nameEnd,
	valueStart,
	valueEnd,
	quoteType
) => {
	const name = internLowerName(ATTR_NAME_INTERN, input, nameStart, nameEnd);
	if (pendAttrStart === 0) {
		pendAttrStart = _attrCount + 1;
		pendAttrDropped = false;
	}
	// Drop duplicate attribute names (per spec). Plain loop avoids a
	// per-attribute closure allocation on this hot path.
	let dup = false;
	for (let i = pendAttrStart; i <= _attrCount; i++) {
		if (_attrNames[i] === name) {
			dup = true;
			break;
		}
	}
	if (dup) pendAttrDropped = true;
	if (!dup) {
		// The raw value is read from the source by offset on demand, since
		// consumers re-resolve requests from it and the offsets have to stay
		// aligned. Only a valueless attribute stores an override.
		_allocAttr(
			name,
			valueStart !== -1 ? null : "",
			nameStart,
			nameEnd,
			valueStart,
			valueEnd
		);
	}
	if (valueStart === -1) return nameEnd;
	return quoteType !== QUOTE_NONE ? valueEnd + 1 : valueEnd;
};
/** @type {(input: string, start: number, end: number, nameStart: number, nameEnd: number, selfClosing: boolean) => number} */
const handleStartTagToken = (
	input,
	start,
	end,
	nameStart,
	nameEnd,
	selfClosing
) => {
	// Only the token immediately after the start tag may be the swallowed
	// newline; anything else clears the flag (§13.2.6.4.7 "the next token").
	swallowNextNewline = false;
	// 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) {
		// Any attribute slots already allocated for it are orphaned.
		pendAttrStart = 0;
		pendAttrDropped = false;
		return end;
	}
	const name = internLowerName(TAG_NAME_INTERN, input, nameStart, nameEnd);
	if (name === "selectedcontent") sawSelectedContent = true;
	tok.type = TOKEN_START_TAG;
	tok.name = name;
	// The reused token carries the tag's attribute run; every consumer of
	// `t.attrs` runs synchronously within this dispatch.
	tok.attrs.start = pendAttrStart;
	tok.attrs.count = pendAttrStart === 0 ? 0 : _attrCount + 1 - pendAttrStart;
	// Only a name the run kept is tested: a dropped one repeats one of them.
	if (pendAttrDropped) {
		for (let i = pendAttrStart; i <= _attrCount; i++) {
			const attributeName = _attrNames[i];
			if (!_isWordSpan(attributeName, 0, attributeName.length)) {
				_foreignSyntaxAttrRuns.add(pendAttrStart);
				break;
			}
		}
	}
	pendAttrStart = 0;
	pendAttrDropped = false;
	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;
};
/** @type {(input: string, start: number, end: number, nameStart: number, nameEnd: number) => number} */
const handleEndTagToken = (input, start, end, nameStart, nameEnd) => {
	// Only the token immediately after the start tag may be the swallowed
	// newline; anything else clears the flag (§13.2.6.4.7 "the next token").
	swallowNextNewline = false;
	// Dropped like a start tag the input ran out inside: `</p` and `</br` would
	// otherwise build the element a stray one of them inserts (§13.2.6.4.7).
	// Not in raw text, whose text mode reads an unfinished end tag as text.
	if (eofInTag && mode !== MODE_TEXT) {
		pendAttrStart = 0;
		pendAttrDropped = false;
		return end;
	}
	// Most end tags close the current element: compare the raw range against
	// its already-interned tag name — one case-folding walk, no hash/probe.
	let name;
	if (open.length > 0) {
		const topName = _nodeStrings[open[open.length - 1]];
		if (rangeAsciiCaseInsensitiveMatch(input, nameStart, nameEnd, topName)) {
			name = topName;
		}
	}
	if (name === undefined) {
		name = internLowerName(TAG_NAME_INTERN, input, nameStart, nameEnd);
	}
	// End tags drop any parsed attributes (the slots are orphaned).
	pendAttrStart = 0;
	pendAttrDropped = false;
	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;
};
/** @type {HtmlTokenCallbacks} */
const PARSE_CALLBACKS = {
	isForeign: currentNodeIsForeign,
	vetoesContentMode,
	fragmentContext: undefined,
	scripting: false,
	parseError: handleParseError,
	doctype: handleDoctypeToken,
	comment: handleCommentToken,
	text: handleTextToken,
	attribute: handleAttributeToken,
	openTag: handleStartTagToken,
	closeTag: handleEndTagToken
};

const dispatchXml = () => {
	processXml(/** @type {Token} */ (/** @type {Token | MutableToken} */ (tok)));
};

// A reference in the same shape as `XML_REFERENCE_REGEXP`, matched in place.
const XML_REFERENCE_STICKY_REGEXP =
	/&(?:#(?:x[\da-fA-F]+|\d+)|([^\s&;#<>"'=/%]+));/y;

/**
 * @param {string} data decoded character data
 * @param {number} start source offset the token is reported at
 * @param {number} end source offset past it
 * @returns {void}
 */
const _dispatchXmlCharacters = (data, start, end) => {
	if (data === "") return;
	tok.type = TOKEN_CHAR;
	tok.data = data;
	tok.start = start;
	tok.end = end;
	dispatchXml();
};

/**
 * Whether an entity's replacement text is markup once the entities it names are
 * expanded too. Memoized per parse, so nesting costs one visit per name.
 * @param {string} name declared entity name
 * @param {number=} depth entities traversed so far
 * @returns {boolean} true when expanding it can build a node other than text
 */
const _xmlEntityHoldsMarkup = (name, depth = 0) => {
	if (_xmlMarkupEntities === null) _xmlMarkupEntities = new Map();
	const known = _xmlMarkupEntities.get(name);
	if (known !== undefined) return known;
	// Beyond the expansion limit, tokenize conservatively without searching deeper.
	if (depth >= XML_ENTITY_DEPTH_LIMIT) return true;
	// A cycle reads its own name as text-only, which is what it expands to.
	_xmlMarkupEntities.set(name, false);
	const replacement = /** @type {Map<string, string>} */ (_xmlEntities).get(
		name
	);
	let holds = replacement !== undefined && replacement.includes("<");
	if (replacement !== undefined && !holds) {
		for (
			let i = replacement.indexOf("&");
			i !== -1 && !holds;
			i = replacement.indexOf("&", i + 1)
		) {
			XML_REFERENCE_STICKY_REGEXP.lastIndex = i;
			const match = XML_REFERENCE_STICKY_REGEXP.exec(replacement);
			holds =
				match !== null &&
				match[1] !== undefined &&
				_xmlEntityHoldsMarkup(match[1], depth + 1);
		}
	}
	_xmlMarkupEntities.set(name, holds);
	return holds;
};

/**
 * Dispatch XML character data, expanding a reference to an entity whose
 * replacement text is itself markup (§4.4.2 "included") by tokenizing it in
 * place. What it builds is reported at the range of the text that named it.
 * @param {string} text source text, line ends folded
 * @param {number} start source offset of the text
 * @param {number} end source offset past it
 * @returns {void}
 */
const _dispatchXmlText = (text, start, end) => {
	if (_xmlEntities === null || !text.includes("&")) {
		_dispatchXmlCharacters(decodeXmlEntities(text), start, end);
		return;
	}
	let pending = "";
	let last = 0;
	for (let i = text.indexOf("&"); i !== -1; i = text.indexOf("&", i + 1)) {
		XML_REFERENCE_STICKY_REGEXP.lastIndex = i;
		const match = XML_REFERENCE_STICKY_REGEXP.exec(text);
		if (match === null || match[1] === undefined) continue;
		const name = match[1];
		const replacement = _xmlEntities.get(name);
		if (
			replacement === undefined ||
			!_xmlEntityHoldsMarkup(name) ||
			_xmlEntityStack.length >= XML_ENTITY_DEPTH_LIMIT ||
			_xmlEntityStack.includes(name) ||
			_xmlExpansionBudget < replacement.length
		) {
			continue;
		}
		pending += decodeXmlEntities(text.slice(last, i));
		_dispatchXmlCharacters(pending, start, end);
		pending = "";
		last = i + match[0].length;
		i = last - 1;
		_xmlExpansionBudget -= replacement.length;
		// The printer cannot rebuild a reference from what it expanded to, so a
		// document holding one prints as written.
		_S._xmlVerbatim = true;
		const outer = _xmlEntityRangeStart;
		if (outer === -1) {
			_xmlEntityRangeStart = start;
			_xmlEntityRangeEnd = end;
		}
		_xmlEntityStack.push(name);
		// A tag the replacement text ends inside is the entity's to drop, not the document's.
		const outerEofInTag = eofInTag;
		tokenizeXml(replacement, 0, XML_PARSE_CALLBACKS);
		eofInTag = outerEofInTag;
		_xmlEntityStack.pop();
		if (outer === -1) _xmlEntityRangeStart = -1;
	}
	pending += decodeXmlEntities(last === 0 ? text : text.slice(last));
	_dispatchXmlCharacters(pending, start, end);
};

/** @type {(input: string, start: number, end: number, nameStart: number, nameEnd: number, publicStart: number, publicEnd: number, systemStart: number, systemEnd: number, forceQuirks: boolean, subsetStart?: number, subsetEnd?: number) => number} */
const handleXmlDoctypeToken = (
	input,
	start,
	end,
	nameStart,
	nameEnd,
	publicStart,
	publicEnd,
	systemStart,
	systemEnd,
	forceQuirks,
	subsetStart = -1,
	subsetEnd = -1
) => {
	// §2.8: only the first DOCTYPE declares anything.
	if (
		subsetStart !== -1 &&
		_xmlEntities === null &&
		_xmlAttributeDeclarations === null
	) {
		_readXmlInternalSubset(input, subsetStart, subsetEnd);
	}
	tok.type = TOKEN_DOCTYPE;
	tok.name = nameStart === -1 ? "" : doctypePart(input, nameStart, nameEnd);
	tok.publicId =
		publicStart === -1 ? null : doctypePart(input, publicStart, publicEnd);
	tok.systemId =
		systemStart === -1 ? null : doctypePart(input, systemStart, systemEnd);
	tok.publicStart = publicStart;
	tok.publicEnd = publicEnd;
	tok.systemStart = systemStart;
	tok.systemEnd = systemEnd;
	tok.forceQuirks = forceQuirks;
	tok.start = start;
	tok.end = end;
	dispatchXml();
	return end;
};

/** @type {(input: string, start: number, end: number, dataStart: number, dataEnd: number) => number} */
const handleXmlCommentToken = (input, start, end, dataStart, dataEnd) => {
	const cdata = input.startsWith("<![CDATA[", start);
	// Character data needs an element to go in; outside the root it is a comment.
	if (cdata && open.length > 0) {
		const raw = input.slice(dataStart, dataEnd);
		const data =
			_xmlEntityRangeStart === -1
				? raw.replace(XML_LINE_END_REGEXP, "\n")
				: raw;
		if (data !== "") {
			tok.type = TOKEN_CHAR;
			tok.data = inputHasNul ? data.replace(/\0/g, "�") : data;
			tok.start = start;
			tok.end = end;
			dispatchXml();
		}
		return end;
	}
	if (input.charCodeAt(start + 1) === CC_QUESTION_MARK) {
		// §2.6: the target is a name, and the data starts past the whitespace after it.
		const targetEnd = _xmlNameEnd(input, dataStart);
		tok.type = TOKEN_PROCESSING_INSTRUCTION;
		tok.name = input.slice(dataStart, targetEnd);
		const piRaw = input.slice(_skipXmlWhitespace(input, targetEnd), dataEnd);
		tok.data =
			_S.inputHasCr || _xmlEntityRangeStart !== -1
				? piRaw.replace(XML_LINE_END_REGEXP, "\n")
				: piRaw;
	} else {
		tok.type = TOKEN_COMMENT;
		const raw = input.slice(dataStart, dataEnd);
		const commentRaw =
			_S.inputHasCr || _xmlEntityRangeStart !== -1
				? raw.replace(XML_LINE_END_REGEXP, "\n")
				: raw;
		tok.data = inputHasNul ? commentRaw.replace(/\0/g, "�") : commentRaw;
	}
	tok.start = start;
	tok.end = end;
	dispatchXml();
	return end;
};

/** @type {(input: string, start: number, end: number) => number} */
const handleXmlTextToken = (input, start, end) => {
	let text = input.slice(start, end);
	if (_S.inputHasCr && _xmlEntityRangeStart === -1) {
		text = text.replace(XML_LINE_END_REGEXP, "\n");
	}
	if (inputHasNul) text = text.replace(/\0/g, "�");
	_dispatchXmlText(text, start, end);
	return end;
};

/** @type {(input: string, nameStart: number, nameEnd: number, valueStart: number, valueEnd: number, quoteType: number) => number} */
const handleXmlAttributeToken = (
	input,
	nameStart,
	nameEnd,
	valueStart,
	valueEnd,
	quoteType
) => {
	// Names are case-sensitive and never interned: XML has no vocabulary to share.
	const name = input.slice(nameStart, nameEnd);
	if (pendAttrStart === 0) pendAttrStart = _attrCount + 1;
	let duplicate = false;
	for (let i = pendAttrStart; i <= _attrCount; i++) {
		if (_attrNames[i] === name) {
			duplicate = true;
			break;
		}
	}
	if (!duplicate) {
		if (_xmlEntityRangeStart !== -1) {
			// Inside an entity's replacement text, which no offset into the source reaches.
			_allocAttr(
				name,
				valueStart === -1 ? "" : input.slice(valueStart, valueEnd),
				-1,
				-1,
				-1,
				-1
			);
		} else {
			_allocAttr(
				name,
				valueStart !== -1 ? null : "",
				nameStart,
				nameEnd,
				valueStart,
				valueEnd
			);
		}
	}
	if (valueStart === -1) return nameEnd;
	return quoteType !== QUOTE_NONE ? valueEnd + 1 : valueEnd;
};

/** @type {(input: string, start: number, end: number, nameStart: number, nameEnd: number, selfClosing: boolean) => number} */
const handleXmlStartTagToken = (
	input,
	start,
	end,
	nameStart,
	nameEnd,
	selfClosing
) => {
	const attrStart = pendAttrStart;
	pendAttrStart = 0;
	// The input ended inside the tag, which drops it as HTML's eof-in-tag does.
	if (eofInTag) return end;
	tok.type = TOKEN_START_TAG;
	tok.name = input.slice(nameStart, nameEnd);
	tok.attrs.start = attrStart;
	tok.attrs.count = attrStart === 0 ? 0 : _attrCount + 1 - attrStart;
	tok.selfClosing = selfClosing;
	tok.pos.start = start;
	tok.pos.end = end;
	tok.pos.tagEnd = end;
	tok.pos.nameEnd = nameEnd;
	dispatchXml();
	return end;
};

/** @type {(input: string, start: number, end: number, nameStart: number, nameEnd: number) => number} */
const handleXmlEndTagToken = (input, start, end, nameStart, nameEnd) => {
	pendAttrStart = 0;
	tok.type = TOKEN_END_TAG;
	tok.name = input.slice(nameStart, nameEnd);
	tok.pos.start = start;
	tok.pos.end = end;
	tok.pos.tagEnd = end;
	tok.pos.nameEnd = nameEnd;
	dispatchXml();
	return end;
};

/** @type {HtmlTokenCallbacks} */
const XML_PARSE_CALLBACKS = {
	parseError: handleParseError,
	doctype: handleXmlDoctypeToken,
	comment: handleXmlCommentToken,
	text: handleXmlTextToken,
	attribute: handleXmlAttributeToken,
	openTag: handleXmlStartTagToken,
	closeTag: handleXmlEndTagToken
};

/**
 * @param {string} input HTML source
 * @param {number=} pos start byte offset (string input only; default `0`)
 * @param {HtmlParseOptions=} options parse options (fragment context, AST skips)
 * @returns {HtmlDocument} ref to the document node — read through `A`; valid until the next parse
 */
const parseHtml = (input, pos = 0, options = {}) => {
	_S.source = input;
	_S.inputHasCr = input.includes("\r");
	// The tokenizer reports every literal NUL before emitting its token, letting
	// the parse-error callback set this flag without a separate full-input scan.
	inputHasNul = false;
	// Only an unknown name's entry is a slice of the last document; one naming a
	// known element is a shared constant, and stays warm across parses.
	_dropSourceBackedNames(TAG_NAME_INTERN);
	_dropSourceBackedNames(ATTR_NAME_INTERN);
	const { fragmentContext, skip = EMPTY_SKIP } = options;
	scripting = options.scripting === true;
	xmlMode = options.xml === true;
	_S._xmlMode = xmlMode;
	_xmlEntities = null;
	_xmlAttributeDeclarations = null;
	_xmlEntityRangeStart = -1;
	_xmlMarkupEntities = null;
	_S._xmlVerbatim = false;
	_xmlEntityStack.length = 0;
	_xmlExpansionBudget = XML_ENTITY_EXPANSION_BUDGET;
	sourceShellEnds = 0;
	skipText = skip.text === true;
	skipComments = skip.comments === true;
	skipDoctype = skip.doctype === true;
	_resetAstColumns();
	_entered.length = 0;
	_skipFrom = -1;
	_streaming = false;
	_docSkipped = false;
	_openStackChanged = false;
	_openSpliced = false;
	_streamHalted = false;
	// WHY: pre-size the columns from the input length. A document past
	// `_COLUMN_SHRINK_CAPACITY` re-enters with released columns, and the doubling
	// cascade would re-allocate and copy every one of them several times per
	// parse. The densest realistic HTML runs about 12 bytes per node and 50 per
	// attribute, so `len/12` and `len/48` reach the final capacity in one
	// allocation, a sparser document over-allocates scratch the release frees,
	// and doubling stays as the net under a short estimate. `skip.text`, which
	// `HtmlParser` uses, drops roughly half the nodes and so estimates `len/24`;
	// attributes are untouched by it.
	let nodeEstimate = (input.length / (skipText ? 24 : 12)) | 0;
	let attrEstimate = (input.length / 48) | 0;
	// A long inline style/script or comment has very few markup delimiters, so
	// estimating columns from its byte length wastes memory. Probe only until the
	// document proves dense; `indexOf` skips the long raw-text spans natively.
	if (
		nodeEstimate > (_nodeCapacity || 4096) ||
		attrEstimate > (_attrCapacity || 4096)
	) {
		let markupCount = 0;
		let searchFrom = 0;
		while (markupCount <= _SPARSE_MARKUP_LIMIT) {
			const next = input.indexOf("<", searchFrom);
			if (next < 0) break;
			markupCount++;
			searchFrom = next + 1;
		}
		if (markupCount <= _SPARSE_MARKUP_LIMIT) {
			nodeEstimate = Math.min(nodeEstimate, markupCount * 2 + 16);
			attrEstimate = Math.min(attrEstimate, markupCount * 8 + 16);
		}
	}
	if (_streamEligible) {
		// Streaming recycles node ids, so a document past the threshold peaks at
		// the activation point plus a batch of headroom rather than at the whole
		// estimate. Attributes are not recycled, so their estimate stands.
		const cap = _STREAM_MIN_NODES + _FLUSH_BATCH * 2;
		if (nodeEstimate > cap) nodeEstimate = cap;
	}
	if (nodeEstimate > _nodeCapacity) _growNodeColumns(nodeEstimate, true);
	if (attrEstimate > _attrCapacity) _growAttrColumns(attrEstimate, true);
	_htmlSource = _S.source;
	_htmlLocConverter = undefined;
	_cursorEpoch = (_cursorEpoch + 1) | 0;
	doc = _allocNode(NodeType.Document, 0, 0);
	_nodeStrings[doc] = "";
	mode = MODE_INITIAL;
	originalMode = 0;
	open.length = 0;
	afe.length = 0;
	_S.head = 0;
	form = 0;
	framesetOk = true;
	pendAttrStart = 0;
	pendAttrDropped = false;
	fosterParenting = false;
	_fosteredLog = null;
	_printFromSource = false;
	_S._fosteredRuns = null;
	_S._fosteredNodes = null;
	_S._fosterHolders = null;
	// Node slots are reused between parses, so the marks come off with the sets.
	if (_S._fosterVerbatim !== null) {
		for (const n of _S._fosterVerbatim) _S._nodeFlags[n] &= ~FLAG_FOSTER_REGION;
	}
	_S._fosterVerbatim = null;
	_fosterOpenTail = null;
	_fosterSpanEnd = 0;
	_fosterMoves = null;
	templateModes.length = 0;
	quirks = false;
	fragment = 0;
	tokenEnd = 0;
	sharedPlace.parent = doc;
	sharedPlace.beforeNode = 0;
	pendingTableChars = null;
	swallowNextNewline = false;
	eofInTag = false;
	sawSelectedContent = false;

	// ---------- 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, EMPTY_ATTRS, null);
		const htmlEl = mkEl("html", NS_HTML, EMPTY_ATTRS, null);
		_appendChild(doc, 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;
		}
	}

	// Refresh the per-parse fragment context on the shared module-scope
	// callbacks object; its function properties stay identity-stable so the
	// tokenizer's callback call sites remain monomorphic.
	PARSE_CALLBACKS.fragmentContext = fragment ? _tagNameOf(fragment) : undefined;
	PARSE_CALLBACKS.scripting = scripting;
	// A `skipChildren()` on the root means nothing under it is visited, so the
	// walk never starts and only the document's own `exit` is left to fire.
	if (_streamEligible && !_enterNode(doc)) {
		_streamEligible = false;
		_docSkipped = true;
	}
	if (xmlMode) {
		// Everything outside the root element is the document's too, so it spans all.
		_nodeEnds[doc] = input.length;
		tokenizeXml(_S.source, pos, XML_PARSE_CALLBACKS);
	} else {
		tokenize(_S.source, pos, PARSE_CALLBACKS);
		tok.type = TOKEN_EOF;
		dispatch();
	}
	// What the input left open ends with it.
	for (let i = 0; i < open.length; i++) {
		if (_nodeNameEnds[open[i]] !== 0) _nodeEnds[open[i]] = _S.source.length;
	}

	// WHY: not while printing. The mirror is content the engine makes, with no
	// source text behind it, and the printer emits what it walks — so serializing
	// it would write the selected option's subtree into the document, which the
	// engine reading that output would then mirror a second time.
	if (sawSelectedContent && _writer === undefined) {
		mirrorSelectedContent(doc, 0);
	}
	if (_streamEligible) _streamFinish(doc);
	else if (_docSkipped) _exitNode(doc);

	return doc;
};

/**
 * Deep-clone a node into fresh ids, dropping attribute source offsets and the
 * raw-text body span so cloned content does not re-emit dependencies.
 * @param {HtmlNodeRef} node node
 * @returns {HtmlNodeRef} clone
 */
const cloneSubtree = (node) => {
	const ty = _S._nodeTypes[node];
	const clone = _allocNode(ty, _nodeStarts[node], _nodeEnds[node]);
	_nodeStrings[clone] = _nodeStrings[node];
	if (ty !== NodeType.Element) return clone;
	_S._nodeFlags[clone] = _S._nodeFlags[node];
	const attrs = cloneAttrs(node);
	_nodeAttrStarts[clone] = attrs.start;
	_nodeAttrCounts[clone] = attrs.count;
	_nodeTagEnds[clone] = _nodeTagEnds[node];
	_nodeNameEnds[clone] = _nodeNameEnds[node];
	// Empty body span: the clone re-emits no raw-text dependency.
	_nodeContentEnds[clone] = _nodeTagEnds[node];
	for (
		let k = _S._nodeFirstChildren[node];
		k !== 0;
		k = _S._nodeNextSiblings[k]
	) {
		_appendChild(clone, cloneSubtree(k));
	}
	// Clone `<template>` content into the clone's own fragment.
	const tc = _templateContentOf(node);
	if (tc !== 0) {
		const fragment = _allocNode(NodeType.DocumentFragment, 0, 0);
		_nodeStrings[fragment] = "";
		// Parent link so the iterative walk can ascend out of the content.
		_nodeParents[fragment] = clone;
		_nodeContentEnds[clone] = fragment;
		_S._nodeFlags[clone] |= FLAG_HAS_TEMPLATE;
		for (
			let k = _S._nodeFirstChildren[tc];
			k !== 0;
			k = _S._nodeNextSiblings[k]
		) {
			_appendChild(fragment, cloneSubtree(k));
		}
	}
	return clone;
};

/**
 * 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} selected option (0 = none)
 */
const selectedOption = (select) => {
	/** @type {HtmlElement[]} */
	const options = [];
	/** @param {HtmlElement} el element */
	const collect = (el) => {
		for (
			let c = _S._nodeFirstChildren[el];
			c !== 0;
			c = _S._nodeNextSiblings[c]
		) {
			if (
				_S._nodeTypes[c] !== NodeType.Element ||
				_namespaceOf(c) !== NS_HTML
			) {
				continue;
			}
			if (_nodeStrings[c] === "option") options.push(c);
			else if (_nodeStrings[c] === "optgroup") collect(c);
		}
	};
	collect(select);
	if (options.length === 0) return 0;
	for (let i = options.length - 1; i >= 0; i--) {
		const el = options[i];
		if (_findAttr(_nodeAttrStarts[el], _nodeAttrCounts[el], "selected") !== 0) {
			return el;
		}
	}
	return options[0];
};

/**
 * Fill each `<selectedcontent>` with a clone of its `<select>`'s selected
 * option subtree (the customizable-select mirroring behavior). Not while
 * printing: the clone has no source behind it, and the printer writes what it
 * walks (see the call site).
 * @param {HtmlNodeRef} node node
 * @param {HtmlElement} select nearest ancestor select (0 = none)
 */
const mirrorSelectedContent = (node, select) => {
	// A `<template>`'s children live in its content fragment.
	const tc = _templateContentOf(node);
	const container = tc !== 0 ? tc : node;
	for (
		let child = _S._nodeFirstChildren[container];
		child !== 0;
		child = _S._nodeNextSiblings[child]
	) {
		if (_S._nodeTypes[child] !== NodeType.Element) continue;
		if (_namespaceOf(child) === NS_HTML && _nodeStrings[child] === "select") {
			mirrorSelectedContent(child, child);
		} else if (
			select !== 0 &&
			_namespaceOf(child) === NS_HTML &&
			_nodeStrings[child] === "selectedcontent"
		) {
			const option = selectedOption(select);
			if (option !== 0) {
				// Replace the children with clones of the option's subtree.
				_S._nodeFirstChildren[child] = 0;
				_nodeLastChildren[child] = 0;
				for (
					let k = _S._nodeFirstChildren[option];
					k !== 0;
					k = _S._nodeNextSiblings[k]
				) {
					_appendChild(child, cloneSubtree(k));
				}
				// A `<select>` carried into the clone is a select of its own there, so
				// its `<selectedcontent>` mirrors inside the copy as it would anywhere.
				mirrorSelectedContent(child, 0);
			}
		} else if (
			select !== 0 &&
			_namespaceOf(child) === NS_HTML &&
			_nodeStrings[child] === "option"
		) {
			// An option is what a `<selectedcontent>` mirrors, so nothing inside one
			// mirrors: not a `<selectedcontent>` written there, and not a `<select>`
			// nested under it either.
			continue;
		} else {
			mirrorSelectedContent(child, select);
		}
	}
};

/**
 * Whether the `&` at `i` would be consumed as a character reference when the
 * text is read back (§13.2.5.72): only before `#` or a name the table holds.
 * Every other `&` the tokenizer flushes as literal text, so `R&D` needs no
 * escape — and neither does the `&` in a query string.
 * @param {string} s decoded text
 * @param {number} i the `&`'s index
 * @param {boolean=} terminated whether nothing can follow `s` in the same run
 * @returns {boolean} true when it has to be escaped
 */
const _startsCharacterReference = (s, i, terminated) => {
	// Unterminated, the next sibling's first characters could complete the run
	// once a comment between them is dropped (`a &am` + `p;`) — the same reason
	// the source fast path keeps out. An attribute value ends at its delimiter.
	let end = i + 1;
	while (end < s.length) {
		const c = s.charCodeAt(end);
		if (!isAsciiAlphanumeric(c) && c !== 0x23) break;
		end++;
	}
	if (end === s.length && terminated !== true) return true;
	const next = s.charCodeAt(i + 1);
	// `#` opens a numeric reference.
	if (next === 0x23) return true;
	if (!isAsciiAlphanumeric(next)) return false;
	// The named-reference table holds the names with and without their `;`, so
	// any prefix hit is a reference the tokenizer would take.
	const longest = Math.min(s.length, i + 1 + MAX_ENTITY_NAME_LEN);
	for (let j = i + 2; j <= longest; j++) {
		if (HTML_ENTITIES[s.slice(i + 1, j)] !== undefined) return true;
	}
	return false;
};

/**
 * Rebuild an opening tag from the node's name and attributes, for elements with
 * no matching source tag (adoption-agency clones, reconstructed formatting
 * elements, renamed tokens like `<image>`). Attribute values are raw
 * (undecoded), so they re-parse to the original decoded values.
 * @param {HtmlPath} path the accessor positioned on the element
 * @returns {string} serialized opening tag
 */
const _synthesizeOpenTag = (path) => {
	const tagName = _tagNameOf(path.node);
	let out = `<${tagName}`;
	// Looked up once for the tag, as the attribute loop does for a written one.
	const enumeratedOn =
		_minifying && _namespaceOf(path.node) === NS_HTML
			? ENUMERATED_KEYWORDS[tagName]
			: undefined;
	for (const attribute of _attributeList(path.node)) {
		const name =
			attribute.serializedName !== undefined
				? attribute.serializedName
				: attribute.name;
		if (attribute.value === "") {
			out += ` ${name}`;
			continue;
		}
		// Nothing was written here for this tag, so there is no source spelling to
		// beat: the shortest one wins outright.
		if (!_minifying || !_transforms.normalizeAttributeQuotes) {
			out += ` ${name}="${attribute.value.replace(/"/g, "&quot;")}"`;
			continue;
		}
		const value = _rewriteAttributeValue(
			tagName,
			name,
			attribute.value,
			false,
			enumeratedOn
		);
		out += ` ${name}=${_minimalAttributeValue(decodeEntities(value, true))}`;
	}
	return `${out}>`;
};

/**
 * Whether an element's default display puts whitespace at its edge outside every
 * line box, so none of it renders.
 * @param {HtmlPath} path the accessor
 * @param {HtmlNodeRef} node node (0 = none)
 * @returns {boolean} true when it is a block-level HTML element
 */
const _isBlockLevel = (path, node) =>
	node !== 0 &&
	path.type(node) === NodeType.Element &&
	_namespaceOf(node) === NS_HTML &&
	BLOCK_LEVEL_ELEMENTS.has(_tagNameOf(node));

/**
 * Whether a text node's leading whitespace sits outside every line box, which is
 * so when it opens a block container. A block *sibling* in front of it would do
 * the same, but reading backwards costs a scan of the whole child list — so this
 * only keeps a space that renders, never drops one that does not.
 * @param {HtmlPath} path the accessor
 * @param {HtmlNodeRef} node the text node
 * @param {HtmlNodeRef} parent its parent (0 = none)
 * @returns {boolean} true when the leading whitespace renders as nothing
 */
const _startsOutsideLineBox = (path, node, parent) =>
	parent !== 0 &&
	_S._nodeFirstChildren[parent] === node &&
	_isBlockLevel(path, parent);

/**
 * Whether a text node's trailing whitespace sits outside every line box: it
 * closes a block container, or a block element follows it.
 * @param {HtmlPath} path the accessor
 * @param {HtmlNodeRef} node the text node
 * @param {HtmlNodeRef} parent its parent (0 = none)
 * @returns {boolean} true when the trailing whitespace renders as nothing
 */
const _endsOutsideLineBox = (path, node, parent) => {
	const next = _nextSibling(node);
	if (next === 0) return parent !== 0 && _isBlockLevel(path, parent);
	return _isBlockLevel(path, next);
};

/**
 * Whether a node prints nothing at all, so what follows it in the output is its
 * next sibling. The three that can: a comment the inert-comment rule drops, an
 * element `removeEmptyElements` drops, and a whitespace-only text node the
 * active `collapseWhitespace` tier deletes — the same questions the printer asks
 * when it reaches them, asked here so the §13.1.2.4 tests read the output.
 * @param {HtmlPath} path the accessor
 * @param {HtmlNodeRef} node the node
 * @returns {boolean} true when it contributes no output
 */
const _printsNothing = (path, node) => {
	const type = path.type(node);
	if (type === NodeType.Comment) {
		return (
			_minifying &&
			!_keepComment(_dataOf(node), path.source(node), _startOf(node))
		);
	}
	if (type === NodeType.Element) {
		return _isRemovableEmptyElement(path, node);
	}
	if (type !== NodeType.Text) return false;
	const data = _dataOf(node);
	if (!isAllAsciiWhitespace(data)) return false;
	const parent = _parentOf(node);
	if (parent === 0 || _namespaceOf(parent) !== NS_HTML) return false;
	const parentName = _tagNameOf(parent);
	// A literal-text body is data — its whitespace is printed as written.
	if (LITERAL_TEXT_PARENTS.has(parentName)) return false;
	// Outside any block formatting context, so nothing ever renders it — which is
	// a reason to drop it, not a claim that a re-serializing pass already did.
	if (parentName === "head" || parentName === "html") {
		return _minifying && !_headLeftBefore(path, node, parent, parentName);
	}
	if (
		_S._collapseWhitespace === "none" ||
		_S._collapseWhitespace === "conservative"
	) {
		return false;
	}
	for (let ancestor = parent; ancestor !== 0; ancestor = _parentOf(ancestor)) {
		if (
			_namespaceOf(ancestor) === NS_HTML &&
			LEADING_NEWLINE_ELEMENTS.has(_tagNameOf(ancestor))
		) {
			return false;
		}
	}
	// Whitespace-only, so it collapses to one space; either edge going empties it.
	return (
		_S._collapseWhitespace === "all" ||
		_startsOutsideLineBox(path, node, parent) ||
		_endsOutsideLineBox(path, node, parent)
	);
};

// One piece of a `noscript` body as the in-head-noscript mode reads it:
// whitespace, a comment closed where the tokenizer closes one (never an abrupt
// `<!-->` / `<!--->`), or a start tag, whose name is captured.
const HEAD_NOSCRIPT_PIECE_REGEXP =
	/[\t\n\f\r ]+|<!--(?!-?>)[\s\S]*?--!?>|<([a-z][^\t\n\f\r />]*)(?:[\t\n\f\r /][^>]*)?>/giy;

/**
 * Whether a reader with scripting off keeps all of a `noscript` body in the head:
 * whitespace, comments and the void elements its mode passes through, nothing else.
 * @param {string} body the body as written
 * @returns {boolean} true when that reader never leaves the head for it
 */
const _staysInHead = (body) => {
	HEAD_NOSCRIPT_PIECE_REGEXP.lastIndex = 0;
	while (HEAD_NOSCRIPT_PIECE_REGEXP.lastIndex < body.length) {
		const piece = HEAD_NOSCRIPT_PIECE_REGEXP.exec(body);
		if (piece === null) return false;
		const name = piece[1];
		if (name === undefined) continue;
		const lowered = name.toLowerCase();
		if (
			!IN_HEAD_NOSCRIPT_PASSTHROUGH.has(lowered) ||
			!HEAD_VOID_ELEMENTS.has(lowered)
		) {
			return false;
		}
	}
	return true;
};

/**
 * Whether a reader with scripting off has left the head before `node`, at a
 * `noscript` holding what the head cannot, and so reads its whitespace as body text.
 * @param {HtmlPath} path the accessor
 * @param {HtmlNodeRef} node a whitespace-only text node
 * @param {HtmlNodeRef} parent its parent, the `head` or the `html` element
 * @param {string} parentName the parent's tag name
 * @returns {boolean} true when that reader has the whitespace in the body
 */
const _headLeftBefore = (path, node, parent, parentName) => {
	// Under `<html>` the whitespace stands after the whole head, so all of it counts.
	const inHead = parentName === "head";
	for (
		let child = _firstChild(inHead ? parent : _S.head);
		child !== 0 && child !== (inHead ? node : 0);
		child = _nextSibling(child)
	) {
		if (
			path.type(child) === NodeType.Element &&
			_namespaceOf(child) === NS_HTML &&
			_tagNameOf(child) === "noscript"
		) {
			const content = _firstChild(child);
			if (content !== 0 && !_staysInHead(_dataOf(content))) return true;
		}
	}
	return false;
};

/**
 * The next sibling that prints something, skipping the ones that print nothing.
 * @param {HtmlPath} path the accessor
 * @param {HtmlNodeRef} node the node to start after
 * @returns {HtmlNodeRef} that sibling, or 0
 */
const _nextPrintedSibling = (path, node) => {
	let next = _nextSibling(node);
	while (next !== 0 && _printsNothing(path, next)) {
		next = _nextSibling(next);
	}
	return next;
};

// The elements whose start tag closes the one before it only inside a `ruby`.
const RUBY_SEGMENTS = new Set(["rb", "rp", "rt", "rtc"]);

/**
 * Whether a `ruby` is in scope above `node`. A `<rb>` / `<rt>` / `<rp>` start tag
 * only closes the one before it when there is one (§13.2.6.4.7), so without it
 * an omitted end tag nests the next element inside this one instead.
 * @param {HtmlPath} path the accessor
 * @param {HtmlNodeRef} node the element
 * @returns {boolean} true when a `ruby` encloses it
 */
const _hasRubyInScope = (path, node) => {
	for (
		let parent = _parentOf(node);
		parent !== 0 && path.type(parent) === NodeType.Element;
		parent = _parentOf(parent)
	) {
		if (_namespaceOf(parent) !== NS_HTML) return false;
		const name = _tagNameOf(parent);
		if (name === "ruby") return true;
		if (HTML_SCOPE.has(name)) return false;
	}
	return false;
};

/**
 * Whether an element's end tag may be omitted (§13.1.2.4). Reads what will be
 * printed rather than the tree: a comment minifying drops, and whitespace the
 * collapse tier deletes, are not content the tag has to stay for.
 * @param {HtmlPath} path the accessor positioned on the element
 * @param {string} name the element's lowercased tag name
 * @param {HtmlNodeRef} node the element
 * @returns {boolean} true when the end tag can be left out
 */
const _canOmitEndTag = (path, name, node) => {
	if (_namespaceOf(node) !== NS_HTML) return false;
	if (name === "html" || name === "head" || name === "body") {
		return _removeImpliedTags !== "none" && _canOmitShellEnd(path, name, node);
	}
	if (!_transforms.removeOptionalTags) return false;
	if (OPTIONAL_END_TAG_UNLESS_TRAILING_NODE.has(name)) {
		// Whitespace or a comment behind it would move inside once the tag goes;
		// an element closes it through the insertion mode instead.
		const trailing = _nextPrintedSibling(path, node);
		if (trailing === 0) return true;
		if (path.type(trailing) !== NodeType.Element) return false;
		// A `<template>` does not close what it stands behind: §13.2.6.4.12 hands it
		// to the head rules, which insert it where it is.
		return !(
			_namespaceOf(trailing) === NS_HTML && _tagNameOf(trailing) === "template"
		);
	}
	const followers = OPTIONAL_END_TAG_FOLLOWERS.get(name);
	if (followers === undefined) return false;
	const next = _nextPrintedSibling(path, node);
	if (next === 0) {
		if (!OPTIONAL_END_TAG_AT_END.has(name)) return false;
		const parent = _parentOf(node);
		const parentIsHtml = parent !== 0 && _namespaceOf(parent) === NS_HTML;
		// Left open inside a formatting element, this is the furthest block the
		// adoption agency needs — the parent's end tag would then restructure both.
		if (
			parentIsHtml &&
			SPECIAL.has(name) &&
			FORMATTING.has(_tagNameOf(parent))
		) {
			return false;
		}
		if (!parentIsHtml) return false;
		// A `<template>`'s content is a fragment, not an element, and `</template>`
		// closes what is open inside it.
		if (path.type(parent) !== NodeType.Element) return true;
		// "Any other end tag" walks past a node that is not special, so a parent's
		// end tag closes one of those whatever the parent is.
		if (!SPECIAL.has(name)) return true;
		// A special one blocks that walk instead, so only a parent whose end tag
		// generates implied end tags closes it. Under any other — `<canvas>`, a
		// custom element — it stays open and swallows what follows the parent.
		return P_ENDS_ON_PARENT_END_TAG.has(_tagNameOf(parent));
	}
	if (
		path.type(next) !== NodeType.Element ||
		_namespaceOf(next) !== NS_HTML ||
		!followers.has(_tagNameOf(next))
	) {
		return false;
	}
	// A `<table>` start tag closes an open `p` only outside quirks mode — the
	// one follower whose reading the document mode decides.
	if (quirks && name === "p" && _tagNameOf(next) === "table") return false;
	// Outside a `ruby` the follower's start tag closes nothing, so this one would
	// go on to hold it.
	if (RUBY_SEGMENTS.has(name) && !_hasRubyInScope(path, node)) return false;
	// A parser-implied `<tbody>` / `<tr>` is printed transparently, so there is
	// no start tag to close this element — its rows would continue this one.
	return (
		_hasSourceTag(next) ||
		_attributeCountOf(next) !== 0 ||
		!TRANSPARENT_IMPLIED_ELEMENTS.has(_tagNameOf(next))
	);
};

/**
 * The last sibling before `node` that prints something, or 0 — the accessor only
 * walks forward, so this walks the chain. Skipping what prints nothing is what
 * keeps it the mirror of `_nextPrintedSibling`: the two decide whether one of
 * `</thead>` and a `<tbody>` start tag may go, and reading a dropped whitespace
 * node from one side only would let both go and merge the rows into the head.
 * @param {HtmlPath} path the accessor
 * @param {HtmlNodeRef} node the element
 * @returns {HtmlNodeRef} the preceding printed sibling, or 0
 */
const _previousPrintedSibling = (path, node) => {
	let previous = 0;
	for (
		let c = _firstChild(_parentOf(node));
		c !== 0 && c !== node;
		c = _nextSibling(c)
	) {
		if (!_printsNothing(path, c)) previous = c;
	}
	return previous;
};

/**
 * Whether an element is shaped like one the parser would re-imply: no attributes
 * to carry, and `child` opening it. Anything before that child (whitespace, a
 * comment) would land outside the implied element, so it blocks the omission.
 * @param {HtmlPath} path the accessor
 * @param {HtmlNodeRef} node the element
 * @param {string} child the tag name the implied element must start with
 * @returns {boolean} true when the shape allows dropping the start tag
 */
const _impliedStartTagShape = (path, node, child) => {
	if (_attributeCountOf(node) !== 0 || _namespaceOf(node) !== NS_HTML) {
		return false;
	}
	// What will print, not what the tree holds: a comment minifying drops is not
	// content the tag has to stay for, the same way the end tags read it.
	let first = _firstChild(node);
	while (first !== 0 && _printsNothing(path, first)) {
		first = _nextSibling(first);
	}
	return (
		first !== 0 &&
		path.type(first) === NodeType.Element &&
		_namespaceOf(first) === NS_HTML &&
		_tagNameOf(first) === child
	);
};

/**
 * Whether a `<tbody>` start tag may be omitted (§13.1.2.4): the parser re-implies
 * it when a `<tr>` opens a table section, but only if no earlier section ran into
 * it — a preceding `tbody` / `thead` / `tfoot` that also dropped its end tag would
 * swallow these rows instead.
 * @param {HtmlPath} path the accessor positioned on the element
 * @param {HtmlNodeRef} node the element
 * @returns {boolean} true when the start tag can be left out
 */
const _canOmitTbodyStart = (path, node) => {
	if (!_impliedStartTagShape(path, node, "tr")) return false;
	const previous = _previousPrintedSibling(path, node);
	if (previous === 0 || path.type(previous) !== NodeType.Element) return true;
	const previousName = _tagNameOf(previous);
	if (
		previousName !== "tbody" &&
		previousName !== "thead" &&
		previousName !== "tfoot"
	) {
		return true;
	}
	return !_canOmitEndTag(path, previousName, previous);
};

/**
 * Whether a `<colgroup>` start tag may be omitted (§13.1.2.4): a `<col>` in a
 * table implies one, but only when the `<col>` is the first thing inside and no
 * preceding `<colgroup>` dropped its end tag — that one would take these columns.
 * @param {HtmlPath} path the accessor positioned on the element
 * @param {HtmlNodeRef} node the element
 * @returns {boolean} true when the start tag can be left out
 */
const _canOmitColgroupStart = (path, node) => {
	if (!_impliedStartTagShape(path, node, "col")) return false;
	const previous = _previousPrintedSibling(path, node);
	if (
		previous === 0 ||
		path.type(previous) !== NodeType.Element ||
		_tagNameOf(previous) !== "colgroup"
	) {
		return true;
	}
	return !_canOmitEndTag(path, "colgroup", previous);
};

// Every rewrite on, which is what a print with no `transforms` option makes and
// what the walk-only passes hold. Frozen, since it is shared rather than copied.
/** @type {Required<HtmlTransformOptions>} */
const _ALL_TRANSFORMS = Object.freeze({
	collapseBooleanAttributes: true,
	comments: "some",
	normalizeAttributeQuotes: true,
	normalizeEnumeratedAttributes: true,
	normalizeListAttributes: true,
	normalizeNumericAttributes: true,
	normalizeUrlAttributes: true,
	removeOptionalTags: true
});

/**
 * The rewrites this print makes, resolved once: each name is what the options
 * set it to, and what `_ALL_TRANSFORMS` gives it where they set nothing — so no
 * read of the result asks whether the option was given. `comments` keeps
 * whatever it was given, which is still falsy only when it is `false`.
 *
 * Always a copy, and always made the one way: spreading the table and then
 * writing over a name it already has leaves the object's shape alone, so every
 * read of the result — and there is one per token — sees a single hidden class.
 * Handing the frozen table itself back where nothing is set would save this
 * object and cost each of those reads a second shape, `Object.freeze` giving a
 * frozen object a map of its own.
 * @param {HtmlTransformOptions | undefined} options the `transforms` option
 * @returns {Required<HtmlTransformOptions>} every rewrite resolved
 */
const _transformsFrom = (options) => {
	/** @type {Required<HtmlTransformOptions>} */
	const out = { ..._ALL_TRANSFORMS };
	if (options !== undefined) {
		for (const name of Object.keys(_ALL_TRANSFORMS)) {
			const value = options[/** @type {keyof HtmlTransformOptions} */ (name)];
			if (value !== undefined) {
				/** @type {{ [name: string]: HtmlTransformOptions[keyof HtmlTransformOptions] }} */ (
					out
				)[name] = value;
			}
		}
	}
	return out;
};

// The same table, as a copy, for the reason `_transformsFrom` always makes one.
/** @type {Required<HtmlTransformOptions>} */
const _DEFAULT_TRANSFORMS = _transformsFrom(undefined);

/**
 * The `comments` option resolved to what it means per comment: `true` every
 * one, `false` none, `"some"` the ones that carry something, or a predicate
 * over the comment's own text — which is what a pattern compiles to here, so no
 * print re-reads one per comment. A pattern is matched from the start each
 * time, since a `g` flag would otherwise carry an index between comments.
 * @param {Required<HtmlTransformOptions>["comments"]} comments the option
 * @returns {boolean | "some" | ((comment: string, line: number, column: number) => boolean)} what it keeps
 */
const _keptComments = (comments) => {
	if (comments === "all") return true;
	if (typeof comments === "boolean" || comments === "some") return comments;
	if (typeof comments === "function") return comments;
	const pattern =
		typeof comments === "string" ? new RegExp(comments) : comments;
	return (comment) => {
		pattern.lastIndex = 0;
		return pattern.test(comment);
	};
};

// Which rewrites this print makes. One object rather than a flag apiece: it is
// read straight off the options, and a fixed shape keeps each read monomorphic.
/** @type {Required<HtmlTransformOptions>} */
let _transforms = _DEFAULT_TRANSFORMS;

/**
 * The per-transform switches out of a wider options object — what
 * `optimization.minimize.html` names beside the transforms that are off until
 * asked for. Exported so the minify function the minimizer plugin ships to its
 * workers can pick them without repeating the names.
 * @param {HtmlTransformOptions} options an options object that may carry them
 * @returns {HtmlTransformOptions | undefined} the switches, or undefined when none is set
 */
const pickTransforms = (options) => {
	/** @type {HtmlTransformOptions | undefined} */
	let out;
	for (const name of Object.keys(_ALL_TRANSFORMS)) {
		const value = options[/** @type {keyof HtmlTransformOptions} */ (name)];
		if (value === undefined) continue;
		if (out === undefined) out = {};
		/** @type {{ [name: string]: HtmlTransformOptions[keyof HtmlTransformOptions] }} */ (
			out
		)[name] = value;
	}
	return out;
};

// Whether this run is minifying, for the tests that run before `printer` does and
// have to predict what it will drop.
let _minifying = false;

// Whether an `<iframe srcdoc>` is among what is deferred. A caller minifying
// them itself keeps the attribute on the normal path, which re-quotes it to
// whichever delimiter its content makes shorter.
/**
 * One value, written with whichever delimiter it needs fewer escapes inside — a
 * minified document is mostly `"`, so a value holding a `"` is shorter quoted
 * with `'`. Where neither costs anything the source's own delimiter is kept.
 * @param {string} value the value to write
 * @param {string} source the delimiter the source spelled it with, if any
 * @returns {string} the value, delimiters and all
 */
const _quoteDecodedValue = (value, source) => {
	const double = value.includes('"');
	const single = value.includes("'");
	const quote =
		double !== single ? (double ? 39 : 34) : source === "'" ? 39 : 34;
	const delimiter = String.fromCharCode(quote);

	return `${delimiter}${escapeAttribute(value, quote, true)}${delimiter}`;
};

/**
 * One attribute, written with the delimiter `_quoteDecodedValue` picks for its
 * value.
 * @param {string} name the attribute name
 * @param {string} value the value to write
 * @param {string} source the delimiter the source spelled it with, if any
 * @returns {string} the attribute, delimiter and all
 */
const _writeQuotedAttribute = (name, value, source) =>
	` ${name}=${_quoteDecodedValue(value, source)}`;

let _deferSrcdoc = true;

/**
 * Hand one nested body to the caller's renderer. Anything it throws is a
 * renderer that did not answer: a document must not fail to serialize because
 * something inside it did not parse.
 * @param {string} source the nested body
 * @param {string} type its language, e.g. `"css"` / `"javascript"` / `"svg"`
 * @param {string=} as which production of that language it is, for one with more than one
 * @returns {string | null} the rendered body, or null when there is no renderer or it declined
 */
const _renderEmbeddedOrNull = (source, type, as) => {
	if (_S._embeddedAnswers !== undefined) {
		const answer = _S._embeddedAnswers({
			type,
			hostType: HTML_TYPE,
			source,
			...(as === undefined ? undefined : { as })
		});
		return typeof answer === "string" ? answer : null;
	}
	if (_S._renderEmbeddedSource === undefined) return null;
	try {
		const rendered = _S._renderEmbeddedSource(source, {
			type,
			hostType: HTML_TYPE,
			...(as === undefined ? undefined : { as })
		});
		// Anything but text is a renderer that did not answer, not a body to write.
		return typeof rendered === "string" ? rendered : null;
	} catch (_err) {
		return null;
	}
};

/**
 * Record one nested body for a caller that answers asynchronously and print the
 * marker standing in for it. `onDeclined` spells what an untapped run spells,
 * so declining costs nothing the built-ins would have done.
 * @param {string} source the nested body
 * @param {string} type its language, e.g. `"css"` / `"javascript"` / `"svg"`
 * @param {(rendered: string | undefined) => string} build the text to print once the answer is in
 * @param {string=} as which production of that language it is, for one with more than one
 * @param {((rendered: string | undefined) => boolean)=} decides whether an answer overturns a choice this print made without it
 * @returns {string} the marker to print
 */
const _deferEmbedded = (source, type, build, as, decides) => {
	const holes = /** @type {DeferredEmbeddedSource[]} */ (
		_S._deferEmbeddedSource
	);
	const id = holes.length;

	holes.push({
		type,
		hostType: HTML_TYPE,
		source,
		build,
		...(as === undefined ? undefined : { as }),
		...(decides === undefined ? undefined : { decides })
	});

	return deferredWrite(id);
};

/**
 * Whether this element is the outermost `<svg>` of an SVG subtree — the whole
 * of which is offered to the renderer, so a nested one is part of its parent's.
 * @param {HtmlPath} path the accessor positioned on the element
 * @returns {boolean} true for an SVG root
 */
const _isSvgRoot = (path) => {
	if (_tagNameOf(path.node) !== "svg" || _namespaceOf(path.node) !== NS_SVG) {
		return false;
	}
	const parent = _parentOf(path.node);
	return parent === 0 || _namespaceOf(parent) !== NS_SVG;
};

// Whether an empty `class` / `id` / `style` / `dir` may go (see
// `HtmlProcessOptions`), off unless asked for: a presence selector reads it.
let _removeEmptyAttributes = false;

// Whether a childless, attribute-less element may go (see
// `HtmlProcessOptions`), off unless asked for: CSS can give one a size or a
// `::before`, and this cannot see the stylesheet.
let _removeEmptyElements = false;
/**
 * Whether each element ends up empty in the output, memoized: the answer is
 * fixed for a parse, and the walk that finds it is the whole subtree.
 * @type {Map<HtmlNodeRef, boolean>}
 */
const _emptyElements = new Map();

// Which comments this print keeps, with a pattern already compiled to the
// predicate it stands for — `true` every one, `false` none, `"some"` the ones
// that carry something (see `_keepComment`).
/** @type {boolean | "some" | ((comment: string, line: number, column: number) => boolean)} */
let _commentsKept = "some";
// The source a predicate's comment stands in, and the converter placing one,
// built on the first comment asked about.
let _commentInput = "";
/** @type {LocConverter | undefined} */
let _commentLocs;

// The two reorderings (see `HtmlProcessOptions`), off unless asked for: neither
// changes what anything matches, but both change what a script reading
// `element.attributes` or `className` back sees.
let _sortAttributes = false;
let _sortTokenLists = false;
// The quote an XML attribute takes when both cost the same (see `xmlQuote`).
/** @type {"\"" | "'"} */
let _xmlQuote = '"';

// Foster parenting puts a node before the table it was written inside, so tree
// and source order disagree there; it is recorded where it happens. The tree
// then holds what no arrangement of tags reaches.
let _printFromSource = false;
/** @type {HtmlNodeRef[] | null} this parse's fosters, flat: table, then node */
let _fosteredLog = null;

/** @type {Map<HtmlNodeRef, boolean> | null} */
let _fosterMoves = null;

// That holder and everything still open around it: the slice runs to where the
// source stopped, so an end tag none of them was given cannot be invented here.
/** @type {Set<HtmlNodeRef> | null} */
let _fosterOpenTail = null;
/** @type {number} how far the furthest verbatim span reached */
let _fosterSpanEnd = 0;

// WHY: how much of the `<html>` / `<head>` / `<body>` shell §13.1.2.4 lets the
// parser imply may be left out. Every other optional tag goes unconditionally,
// since nothing can observe that, but these six are what a consumer reading the
// page with a regexp rather than a parser looks for. `"smart"` drops the one
// such a reader never matches: the `<html>` start tag, omittable only with no
// attribute at all, so the `<html lang=en>` anyone greps for keeps its tag.
// `</html>` stays with it, since a truncation check reads a page as complete by
// finding one.
/** @type {"none" | "smart" | "all"} */
let _removeImpliedTags = "smart";

/**
 * Whether the `<html>` / `<head>` / `<body>` start tag §13.1.2.4 lets the parser
 * imply may be left out. A comment minification keeps, whitespace, or an element
 * the parser still reads as head content would move above the tag once it goes;
 * a comment it drops is gone from the output, so it is skipped over.
 * @param {HtmlPath} path the accessor
 * @param {string} name the element's lowercased tag name
 * @param {HtmlNodeRef} node the element
 * @returns {boolean} true when the start tag can be dropped
 */
const _canOmitShellStart = (path, name, node) => {
	if (name !== "html" && name !== "head" && name !== "body") return false;
	if (_attributeCountOf(node) !== 0) return false;
	let first = _firstChild(node);
	while (first !== 0 && _printsNothing(path, first)) {
		first = _nextSibling(first);
	}
	if (first === 0) return name === "html" || _removeImpliedTags === "all";
	const type = path.type(first);
	if (type === NodeType.Comment) return false;
	if (name === "html") return true;
	if (_removeImpliedTags !== "all") return false;
	if (name === "head") return type === NodeType.Element;
	// Whitespace `"all"` drops is not whitespace the tag has to stay for; any
	// lesser tier keeps it, and without the tag the parser drops it on the way in.
	if (type === NodeType.Text) {
		return (
			!startsWithAsciiWhitespace(_dataOf(first)) ||
			_S._collapseWhitespace === "all"
		);
	}
	return !BODY_START_KEPT_BEFORE.has(_tagNameOf(first));
};

/**
 * Whether `</html>` / `</head>` / `</body>` can be left out without the parser
 * handing what follows to the element it closed: §4.13 lets an `html` or `body`
 * end tag go when a comment does not follow it, and a `head` one when neither a
 * comment nor whitespace does.
 * @param {HtmlPath} path the accessor
 * @param {string} name the element's lowercased tag name
 * @param {HtmlNodeRef} node the element
 * @returns {boolean} true when nothing behind the tag would move inside
 */
const _shellEndTagOmittable = (path, name, node) => {
	let next = _nextSibling(node);
	while (next !== 0 && _printsNothing(path, next)) {
		next = _nextSibling(next);
	}
	if (next === 0) return true;
	const type = path.type(next);
	if (type === NodeType.Comment) return false;
	return (
		name !== "head" ||
		type !== NodeType.Text ||
		!startsWithAsciiWhitespace(_dataOf(next))
	);
};

/**
 * The same, for the end tag — `"all"` only: `"smart"` keeps every one of them,
 * `</html>` included, because a reader checking a page downloaded whole looks
 * for it. A comment behind the tag would move inside the element it closed,
 * and whitespace behind `</head>` into `<body>`.
 * @param {HtmlPath} path the accessor
 * @param {string} name the element's lowercased tag name
 * @param {HtmlNodeRef} node the element
 * @returns {boolean} true when the end tag can be dropped
 */
const _canOmitShellEnd = (path, name, node) =>
	_removeImpliedTags === "all" && _shellEndTagOmittable(path, name, node);

/**
 * The end tag a transparent implied element still prints: the parser re-implies
 * the start tag behind it, never the end tag, and outside `"all"` a shell tag
 * the source spelled stays — `</html>` is what a truncation check looks for.
 * @param {HtmlPath} path the accessor
 * @param {string} name the element's lowercased tag name
 * @param {HtmlNodeRef} node the element
 * @returns {string} the end tag, or `""` when nothing needs it
 */
const _impliedCloseTag = (path, name, node) => {
	if (!SHELL_ELEMENTS.has(name)) {
		return _canOmitEndTag(path, name, node) ? "" : `</${name}>`;
	}
	const spelled =
		name === "html"
			? SHELL_END_HTML
			: name === "head"
				? SHELL_END_HEAD
				: SHELL_END_BODY;
	return (sourceShellEnds & spelled) === 0 || _removeImpliedTags === "all"
		? _shellEndTagOmittable(path, name, node)
			? ""
			: `</${name}>`
		: `</${name}>`;
};

/**
 * Whether a parser-inserted table group has to print a start tag the source
 * never spelled. With `removeOptionalTags` off its end tag prints, and a
 * `</tbody>` with no `<tbody>` is not the document that was parsed.
 * @param {HtmlPath} path the accessor positioned on the element
 * @param {string} name its lowercased tag name
 * @param {HtmlNodeRef} node the element
 * @returns {boolean} true when the start tag has to be materialized
 */
const _impliedTableGroupStartNeeded = (path, name, node) => {
	if (!_transforms.removeOptionalTags) {
		return name === "tbody" || name === "colgroup";
	}
	// §4.13 lets the start tag go only when the group before it kept its end
	// tag, or that one would take these columns over.
	return name === "colgroup" && !_canOmitColgroupStart(path, node);
};

/**
 * Whether an element's start tag may be left out: one the parser re-implies, or
 * a shell tag the option lets go. A shell element the source never spelled falls
 * through to the transparent-implied path, which already prints nothing for it.
 * @param {HtmlPath} path the accessor positioned on the element
 * @param {string} name its lowercased tag name
 * @param {HtmlNodeRef} node the element
 * @param {string} open what its start tag printed as, `""` when the source
 * spelled none
 * @returns {boolean} true when the start tag can be dropped
 */
const _canOmitStartTag = (path, name, node, open) => {
	if (name === "tbody") {
		return _transforms.removeOptionalTags && _canOmitTbodyStart(path, node);
	}
	if (name === "colgroup") {
		return _transforms.removeOptionalTags && _canOmitColgroupStart(path, node);
	}
	return (
		open !== "" &&
		_removeImpliedTags !== "none" &&
		_canOmitShellStart(path, name, node)
	);
};

// The throwaway rule a `style` attribute's declaration list is minified inside.
// Its selector is only written and read back here, so it is the shortest one
// the printer cannot rewrite, and both halves are named from one place.

/**
 * How one attribute is written out, and whether it leaves the tag ending in an
 * unquoted value — the shortest of the minimal reference spelling, the value
 * bare, and the value quoted. `null` when none of them beats the source.
 * @param {string} attributeName the attribute's name
 * @param {string} value its rewritten value
 * @param {string} rawValue its value as the source spelled it
 * @param {string | null} decoded the value decoded, for one carrying character references, or null
 * @param {boolean} quoted whether the source spelled the value with a delimiter, which is two characters this has to beat
 * @param {string} delimiter the delimiter the source spelled it with, if any
 * @returns {{ text: string, unquotedTail: boolean } | null} what to emit, or null to leave the source alone
 */
const _writeAttribute = (
	attributeName,
	value,
	rawValue,
	decoded,
	quoted,
	delimiter
) => {
	// `normalizeAttributeQuotes` holds back the delimiters, not the spelling
	// inside them, so a frozen value keeps the delimiter the source chose.
	const frozen = !_transforms.normalizeAttributeQuotes;
	const minimal =
		decoded === null
			? null
			: frozen
				? _quoteDecodedValue(decoded, delimiter)
				: _minimalAttributeValue(decoded);
	const unquotable = !frozen && _canUnquoteAttributeValue(value);

	if (
		minimal !== null &&
		minimal.length <
			(unquotable ? value.length : rawValue.length + (quoted ? 2 : 0))
	) {
		const opened = minimal.charCodeAt(0);

		return {
			text: ` ${attributeName}=${minimal}`,
			unquotedTail: opened !== CC_QUOTATION_MARK && opened !== CC_APOSTROPHE
		};
	}

	if (unquotable) {
		return { text: ` ${attributeName}=${value}`, unquotedTail: true };
	}

	if (value !== rawValue) {
		const quotedValue = frozen
			? _quoteDecodedValue(value, delimiter)
			: _quoteAttributeValue(value);

		if (quotedValue !== null) {
			return { text: ` ${attributeName}=${quotedValue}`, unquotedTail: false };
		}
	}

	return null;
};

/**
 * Write a `style` list the CSS minifier changed, at its shortest spelling. It is
 * written even where the source spells shorter, since a lowered list says more.
 * @param {string} attributeName the attribute's name
 * @param {string} list the minified declaration list
 * @param {string} delimiter the delimiter the source spelled it with, if any
 * @returns {{ text: string, unquotedTail: boolean }} what to emit
 */
const _writeStyleList = (attributeName, list, delimiter) => {
	// Read when written, as `_writeAttribute` reads it.
	const value = _transforms.normalizeAttributeQuotes
		? _minimalAttributeValue(list)
		: _quoteDecodedValue(list, delimiter);
	const opened = value.charCodeAt(0);
	return {
		text: ` ${attributeName}=${value}`,
		unquotedTail: opened !== CC_QUOTATION_MARK && opened !== CC_APOSTROPHE
	};
};

/**
 * Offer a `style` attribute's declaration list to the renderer as the
 * block-contents production it is, which gives it the merges a rule's block
 * gets. The deferring path offers it at the attribute instead, where the shape
 * an answer has to land in is known.
 * @param {string} raw raw (undecoded) attribute value
 * @returns {string} the rendered declarations, or the value as written
 */
const _minifyStyleAttribute = (raw) => {
	if (!_rendersNow() || raw.trim() === "") return raw;
	const rendered = _renderEmbeddedOrNull(raw, CSS_TYPE, BLOCK_CONTENTS);
	return rendered === null ? raw : rendered;
};

/**
 * Infra's strip leading and trailing ASCII whitespace: `String#trim` also eats
 * NBSP and the other Unicode spaces, which HTML's grammars read as characters.
 * @param {string} value the value
 * @returns {string} the trimmed value
 */
const stripAsciiWhitespace = (value) => {
	let start = 0;
	let end = value.length;
	while (start < end && isAsciiWhitespace(value.charCodeAt(start))) start++;
	while (end > start && isAsciiWhitespace(value.charCodeAt(end - 1))) end--;
	return start === 0 && end === value.length ? value : value.slice(start, end);
};

/**
 * Normalize a `<meta name=viewport>` content list: its grammar ignores the
 * whitespace around the `,` / `;` / `=` separators, so dropping that keeps every
 * pair intact.
 * @param {string} raw raw (undecoded) attribute value
 * @returns {string} the normalized list
 */
const _normalizeViewport = (raw) =>
	stripAsciiWhitespace(raw)
		.replace(/[\t\n\f\r ]*([,;=])[\t\n\f\r ]*/g, "$1")
		.replace(/[\t\n\f\r ]+/g, " ");

/**
 * Wrap a rewritten attribute value in the shortest quote it admits, or null when
 * it carries both kinds and the source spelling has to stand.
 * @param {string} value the value
 * @returns {string | null} the quoted value, or null
 */
const _quoteAttributeValue = (value) => {
	if (!value.includes('"')) return `"${value}"`;
	if (!value.includes("'")) return `'${value}'`;
	return null;
};

/**
 * The shortest way to write a decoded value: bare where the grammar allows it,
 * else under whichever quote costs less. `<`, `>` and the quote that is not the
 * delimiter carry no meaning inside a quoted value, so `alt="say &quot;hi&quot;"`
 * is `alt='say "hi"'` and `title="&lt;b&gt;"` is `title="<b>"`.
 * @param {string} value the decoded attribute value
 * @returns {string} the value as written, delimiters included
 */
const _minimalAttributeValue = (value) => {
	const underDouble = escapeAttribute(value, CC_QUOTATION_MARK, true);
	const underSingle = escapeAttribute(value, CC_APOSTROPHE, true);
	let best = `"${underDouble}"`;
	if (underSingle.length + 2 < best.length) best = `'${underSingle}'`;
	// A bare value costs no delimiters, so it wins whenever it is legal at all.
	for (const bare of [underDouble, underSingle]) {
		if (bare.length < best.length && _canUnquoteAttributeValue(bare)) {
			best = bare;
		}
	}
	return best;
};

/**
 * Whether an attribute only restates the element's own default — its value
 * naming that default, or naming no keyword at all where the invalid value
 * default is that same state — so dropping it leaves the DOM reading the same
 * value, but leaves an attribute selector, which matches the content attribute,
 * with nothing to match. Both tables are looked up per element by the caller,
 * not per attribute here: the answer is the same for every attribute of one tag.
 * @param {Record<string, string> | undefined} markers the element's marker table
 * @param {Record<string, string> | undefined} defaults the element's spec-default table
 * @param {string} tagName lowercased element name
 * @param {string} name lowercased attribute name
 * @param {string} value the attribute's raw value
 * @returns {boolean} true when the attribute may be dropped
 */
const _isRedundantAttribute = (markers, defaults, tagName, name, value) => {
	// Looked up by what the value says, not how it was spelled — guarded on the
	// `&` so a value carrying no reference pays nothing.
	const lowered = asciiLowercase(
		stripAsciiWhitespace(
			value.includes("&") ? decodeEntities(value, true) : value
		)
	);
	// `<script type>`: only a spelling that names JavaScript, which is what the
	// element already runs. `module` and the empty spelling say something.
	if (tagName === "script" && name === "type") {
		return (
			lowered !== "" &&
			lowered !== "module" &&
			JAVASCRIPT_SCRIPT_TYPES.has(lowered)
		);
	}
	if (markers !== undefined && markers[name] === lowered) return true;
	if (defaults === undefined || defaults[name] === undefined) return false;
	if (defaults[name] === lowered) return true;
	// A value naming no keyword takes the attribute's invalid value default,
	// which is the state a missing attribute names except where
	// `INVALID_VALUE_DEFAULT_DIFFERS` says the two part.
	const enumerated = ENUMERATED_KEYWORDS[tagName];
	const keywords = enumerated === undefined ? undefined : enumerated[name];
	if (keywords === undefined || keywords.has(lowered)) return false;
	return !INVALID_VALUE_DEFAULT_DIFFERS.has(`${tagName} ${name}`);
};

/**
 * Whether an empty attribute reads as its own absence, so `removeEmptyAttributes`
 * may drop it — only on the elements the spec defines it on, never `<x-foo rel="">`.
 * @param {string} elementName lowercased element name
 * @param {string} name lowercased attribute name
 * @param {string} value the attribute's raw value
 * @returns {boolean} true when the attribute may be dropped
 */
const _isRemovableEmptyAttribute = (elementName, name, value) => {
	if (!_removeEmptyAttributes) return false;
	const on = EMPTY_REMOVABLE_ATTRIBUTES.get(name);
	if (on === undefined || (on !== null && !on.has(elementName))) return false;
	// Read decoded: a reference is how a value is spelled, not what it says, and
	// the tokenizer hands the element the same empty either way. Guarded on the
	// `&`, and `stripAsciiWhitespace` returns the input when there is nothing to trim.
	return (
		stripAsciiWhitespace(
			value.includes("&") ? decodeEntities(value, true) : value
		) === ""
	);
};

/**
 * Whether an embedded body's answer is in hand as the print goes, so a decision
 * made before it is printed can read it rather than the marker standing for it.
 * @returns {boolean} true with a synchronous renderer, or answers from a print before
 */
const _rendersNow = () =>
	_S._embeddedAnswers !== undefined ||
	(_S._renderEmbeddedSource !== undefined &&
		_S._deferEmbeddedSource === undefined);

/**
 * Whether a `style` attribute the renderer minifies to nothing goes with the
 * empty ones: the value it prints is empty, and an empty one reads as absent.
 * @param {string} elementName lowercased element name
 * @param {string} value the attribute's raw value
 * @returns {boolean} true when the attribute prints nothing
 */
const _styleRendersEmpty = (elementName, value) =>
	value !== "" &&
	_rendersNow() &&
	_isRemovableEmptyAttribute(elementName, "style", "") &&
	_minifyStyleAttribute(
		value.includes("&") ? decodeEntities(value, true) : value
	) === "";

/**
 * Whether a `<style>` element's body is CSS: the attribute is optional, and the
 * only value that keeps it CSS is `text/css`.
 * @param {HtmlPath} path the accessor
 * @param {HtmlElement} element the `<style>` element
 * @returns {boolean} true when the body may be minified as CSS
 */
const _isCssStyleElement = (path, element) => {
	for (const attribute of _attributeList(element)) {
		if (attribute.name !== "type") continue;
		// Read the way `_scriptType` reads its own: the value the spec matches is
		// the one the parser read, so a reference is decoded first.
		const raw = attribute.value;
		const value = asciiLowercase(
			stripAsciiWhitespace(raw.includes("&") ? decodeEntities(raw, true) : raw)
		);
		return value === "" || value === "text/css";
	}
	return true;
};

/**
 * Whether an attribute reaches the output: one this print drops is not a
 * difference between two elements, nor an attribute that keeps a run apart.
 * @param {string} elementName lowercased element name
 * @param {string} name lowercased attribute name
 * @param {string} value its raw value
 * @returns {boolean} true when it prints
 */
const _attributePrints = (elementName, name, value) => {
	const markers =
		_S._removeRedundantAttributes !== "none"
			? REDUNDANT_TYPE_ATTRIBUTES[elementName]
			: undefined;
	const defaults =
		_S._removeRedundantAttributes === "all"
			? REDUNDANT_DEFAULT_ATTRIBUTES[elementName]
			: undefined;
	if (
		(markers !== undefined || defaults !== undefined) &&
		_isRedundantAttribute(markers, defaults, elementName, name, value)
	) {
		return false;
	}
	if (name === "style" && _styleRendersEmpty(elementName, value)) {
		return false;
	}
	return !_isRemovableEmptyAttribute(elementName, name, value);
};

/**
 * How many of an element's attributes this print keeps.
 * @param {HtmlPath} path the accessor
 * @param {HtmlNodeRef} element the element
 * @returns {number} the count
 */
const _printedAttributeCount = (path, element) => {
	const name = _tagNameOf(element);
	let kept = 0;
	for (const attribute of _attributeList(element)) {
		if (_attributePrints(name, attribute.name, attribute.value)) kept++;
	}
	return kept;
};

/**
 * Whether any of an element's attributes reaches the output, stopping at the
 * first that does.
 * @param {HtmlPath} path the accessor
 * @param {HtmlNodeRef} element the element
 * @returns {boolean} true when one prints
 */
const _printsAnAttribute = (path, element) => {
	const name = _tagNameOf(element);
	for (const attribute of _attributeList(element)) {
		if (_attributePrints(name, attribute.name, attribute.value)) return true;
	}
	return false;
};

// Each `<style>` body the emptiness check rendered, by its text, until the
// printer takes it — so asking whether a sheet prints does not render it twice.
/** @type {Map<string, string> | null} */
let _renderedSheets = null;

/**
 * A `<style>` body as the renderer answers it, or as written where it declines;
 * kept for the printer to take.
 * @param {string} css the body
 * @returns {string} what it prints
 */
const _renderedSheet = (css) => {
	if (_renderedSheets === null) _renderedSheets = new Map();
	let printed = _renderedSheets.get(css);
	if (printed === undefined) {
		const rendered = _renderEmbeddedOrNull(css, CSS_TYPE);
		printed = rendered === null ? css : rendered;
		_renderedSheets.set(css, printed);
	}
	return printed;
};

/**
 * What `_renderedSheet` answers for this body, handed over once: the printer
 * takes it, so the next `<style>` with the same text is asked afresh.
 * @param {string} css the body
 * @returns {string} what it prints
 */
const _takeRenderedSheet = (css) => {
	const printed = _renderedSheet(css);
	/** @type {Map<string, string>} */ (_renderedSheets).delete(css);
	return printed;
};

/**
 * What a `<style>` body's deferred answer would overturn, for `processAsync` to
 * print again with it: the element going empty.
 * @returns {((answer: string | undefined) => boolean) | undefined} the test, or undefined where no choice rests on it
 */
const _sheetDecides = () => (_removeEmptyElements ? _answersEmpty : undefined);

/**
 * @param {string | undefined} answer a renderer's answer
 * @returns {boolean} true when it prints nothing
 */
const _answersEmpty = (answer) => answer === "";

// An at-rule that only applies at the top of a sheet, so a second sheet carrying
// one cannot be appended to a first; an escaped at-rule name may spell one.
const _SHEET_LEADING_AT_RULE_REGEXP =
	/@(?:charset|import|namespace)\b|@[\w-]*\\/i;

// A namespace applies to the whole sheet declaring it, so none is appended to it.
const _SHEET_NAMESPACE_REGEXP = /@namespace\b|@[\w-]*\\/i;

/**
 * A literal-text element's only child when that is one text node — the shape a
 * merge needs, since anything else means the element is empty.
 * @param {HtmlElement} element a `<style>` or `<script>`
 * @returns {HtmlNodeRef} the text node, or 0
 */
const _onlyTextChild = (element) => {
	const first = _S._nodeFirstChildren[element];
	return first !== 0 &&
		_S._nodeTypes[first] === NodeType.Text &&
		_S._nodeNextSiblings[first] === 0
		? first
		: 0;
};

/**
 * Whether two elements carry exactly the same attributes. Values compare raw:
 * two spellings of one value are rare enough not to be worth decoding for, and
 * reading them as different only declines a merge.
 * @param {HtmlPath} path the accessor
 * @param {HtmlElement} one an element
 * @param {HtmlElement} other another
 * @returns {boolean} true when the two agree on every attribute
 */
const _sameAttributes = (path, one, other) => {
	// What each will spell, not what the tree holds: a `type` this print drops
	// as redundant is not a difference between them.
	const elementName = _tagNameOf(one);
	let count = 0;
	for (const attribute of _attributeList(one)) {
		if (!_attributePrints(elementName, attribute.name, attribute.value)) {
			continue;
		}
		count++;
		const match = _findAttribute(attribute.name, other);
		if (match === 0 || _attrValueOf(match) !== attribute.value) {
			return false;
		}
	}
	return count === _printedAttributeCount(path, other);
};

/**
 * The `<style>` that may be folded into `element`: the next sibling, when only
 * whitespace lies between them and the two agree on every attribute. Adjacency
 * is what makes the fold safe — nothing moves past anything, so the cascade is
 * the one the source wrote. Media, `title` and `blocking` all ride on the
 * attribute test, and `type` on both being CSS in the first place.
 * @param {HtmlPath} path the accessor
 * @param {HtmlElement} element a `<style>` element
 * @returns {HtmlElement} the next `<style>` of the run, or 0
 */
const _mergeableStyleAfter = (path, element) => {
	for (
		let next = _S._nodeNextSiblings[element];
		next !== 0;
		next = _S._nodeNextSiblings[next]
	) {
		const type = _S._nodeTypes[next];
		if (type === NodeType.Text) {
			if (!isAllAsciiWhitespace(_nodeStrings[next])) return 0;
			continue;
		}
		// A comment this print drops leaves the two adjacent, so it is not what
		// stands between them; one it keeps still is.
		if (type === NodeType.Comment) {
			if (_printsNothing(path, next)) continue;
			return 0;
		}
		return type === NodeType.Element &&
			_namespaceOf(next) === NS_HTML &&
			_tagNameOf(next) === "style" &&
			_onlyTextChild(next) !== 0 &&
			_isCssStyleElement(path, next) &&
			_sameAttributes(path, element, next)
			? next
			: 0;
	}
	return 0;
};

/**
 * Whether a CSS name starts at `at` (§4.3.9), which makes an `@` before it an
 * at-keyword; a lone `@` is a delimiter opening an ordinary rule's prelude.
 * @param {string} css the sheet's text
 * @param {number} at where the name would start
 * @returns {boolean} true when an identifier sequence starts there
 */
const _startsCssName = (css, at) => {
	/**
	 * @param {number} c a code unit
	 * @returns {boolean} true for a name-start code point
	 */
	const nameStart = (c) =>
		(c | 0x20) >= 0x61 && (c | 0x20) <= 0x7a ? true : c === 0x5f || c >= 0x80;
	/**
	 * @param {number} i where a backslash would be
	 * @returns {boolean} true for a valid escape there
	 */
	const escape = (i) =>
		css.charCodeAt(i) === CC_REVERSE_SOLIDUS &&
		i + 1 < css.length &&
		css.charCodeAt(i + 1) !== CC_LF;
	const first = css.charCodeAt(at);
	if (nameStart(first) || escape(at)) return true;
	if (first !== CC_HYPHEN_MINUS) return false;
	const second = css.charCodeAt(at + 1);
	return nameStart(second) || second === CC_HYPHEN_MINUS || escape(at + 1);
};

/**
 * Where the url token a `(` opens ends (§4.3.6): an unquoted `url(` is one token
 * up to its `)`, a bad one to the first `)` its remnants reach, brackets and all.
 * @param {string} css the sheet's text
 * @param {number} at where the `(` is
 * @returns {number} the `)`'s index; 0 where no url token starts, -1 where the input ends inside one, -2 where it ends on a lone backslash
 */
const _urlTokenEnd = (css, at) => {
	if (at < 3 || !/^url$/i.test(css.slice(at - 3, at))) return 0;
	const before = at > 3 ? css.charCodeAt(at - 4) : 0;
	// Part of a longer name, `image-url(` is a function like any other.
	if (
		before >= 0x80 ||
		before === CC_HYPHEN_MINUS ||
		before === CC_LOW_LINE ||
		before === CC_REVERSE_SOLIDUS ||
		(before < 0x80 && isAsciiAlphanumeric(before))
	) {
		return 0;
	}
	let i = at + 1;
	while (i < css.length && isAsciiWhitespace(css.charCodeAt(i))) i++;
	if (i >= css.length) return -1;
	const first = css.charCodeAt(i);
	if (first === CC_QUOTATION_MARK || first === CC_APOSTROPHE) return 0;
	let bad = false;
	for (; i < css.length; i++) {
		const c = css.charCodeAt(i);
		if (c === CC_RIGHT_PARENTHESIS) return i;
		if (c === CC_REVERSE_SOLIDUS) {
			if (i + 1 >= css.length) return -2;
			// An escape the remnants of a bad url read as one, and a good url only
			// where it is valid; a backslash before a newline makes the url bad.
			if (css.charCodeAt(i + 1) === CC_LF && !bad) bad = true;
			else i++;
			continue;
		}
		if (bad) continue;
		if (isAsciiWhitespace(c)) {
			while (i + 1 < css.length && isAsciiWhitespace(css.charCodeAt(i + 1))) {
				i++;
			}
			if (i + 1 >= css.length) return -1;
			if (css.charCodeAt(i + 1) !== CC_RIGHT_PARENTHESIS) bad = true;
			continue;
		}
		bad =
			c === CC_QUOTATION_MARK ||
			c === CC_APOSTROPHE ||
			c === CC_LEFT_PARENTHESIS ||
			c <= 0x08 ||
			c === 0x0b ||
			(c >= 0x0e && c <= 0x1f) ||
			c === 0x7f;
	}
	return -1;
};

/**
 * Whether a declaration starting at `at` names a custom property, past the
 * whitespace and closed comments before it.
 * @param {string} css the sheet's text
 * @param {number} at where the declaration starts
 * @returns {boolean} true where its name starts with `--`
 */
const _startsCustomProperty = (css, at) => {
	let i = at;
	for (;;) {
		while (i < css.length && isAsciiWhitespace(css.charCodeAt(i))) i++;
		if (!css.startsWith("/*", i)) return css.startsWith("--", i);
		const end = css.indexOf("*/", i + 2);
		if (end === -1) return false;
		i = end + 2;
	}
};

/**
 * What a sheet needs appended to end where a rule does, as §5.4 ends it at the
 * end of input: the quote, comment end and closers it left open, then `{}` or `;`
 * for a statement it did not finish. Null where it ends on a lone backslash,
 * which would escape whatever followed.
 * @param {string} css the sheet's text
 * @returns {string | null} the text to append before another sheet, empty where it already ends at a rule
 */
const _sheetClosing = (css) => {
	/** @type {number[]} */
	const closers = [];
	// Where the declaration being read starts in each `{` block, -1 in a bracket.
	/** @type {number[]} */
	const declarations = [];
	// Whether nothing has been read since a rule ended, and whether the
	// statement being read is an at-rule, which a `;` ends.
	let ended = true;
	let atRule = false;
	let open = "";
	for (let i = 0; i < css.length && open.length === 0; i++) {
		const c = css.charCodeAt(i);
		if (isAsciiWhitespace(c)) continue;
		if (c === CC_SOLIDUS && css.charCodeAt(i + 1) === CC_ASTERISK) {
			const close = css.indexOf("*/", i + 2);
			if (close === -1) open = "*/";
			else i = close + 1;
			continue;
		}
		const top = closers.length === 0;
		if (top && ended) {
			// §5.4.1 skips a CDO or CDC where a rule may start.
			if (css.startsWith("<!--", i) || css.startsWith("-->", i)) {
				i += c === CC_LESS_THAN ? 3 : 2;
				continue;
			}
			atRule = c === CC_COMMERCIAL_AT && _startsCssName(css, i + 1);
		}
		if (top) ended = false;
		if (c === CC_REVERSE_SOLIDUS) {
			if (++i >= css.length) return null;
		} else if (c === CC_QUOTATION_MARK || c === CC_APOSTROPHE) {
			// CSS treats a form feed as a newline too; HTML leaves it as written.
			// Either newline ends a bad string before the next sheet is appended.
			for (i++; i < css.length; i++) {
				const d = css.charCodeAt(i);
				if (d === CC_REVERSE_SOLIDUS) {
					if (++i >= css.length) return null;
				} else if (d === c || d === CC_LF || d === CC_FF) {
					break;
				}
			}
			if (i >= css.length) open = String.fromCharCode(c);
		} else if (c === CC_LEFT_CURLY_BRACKET) {
			closers.push(CC_RIGHT_CURLY_BRACKET);
			declarations.push(i + 1);
		} else if (c === CC_LEFT_PARENTHESIS) {
			const url = _urlTokenEnd(css, i);
			if (url === -2) return null;
			if (url === -1) {
				open = ")";
			} else if (url !== 0) {
				i = url;
			} else {
				closers.push(CC_RIGHT_PARENTHESIS);
				declarations.push(-1);
			}
		} else if (c === CC_LEFT_SQUARE_BRACKET) {
			closers.push(CC_RIGHT_SQUARE_BRACKET);
			declarations.push(-1);
		} else if (!top && c === closers[closers.length - 1]) {
			// §5.4.8 ends a block only on its own closer; any other is a token in it.
			closers.pop();
			declarations.pop();
			if (closers.length === 0 && c === CC_RIGHT_CURLY_BRACKET) ended = true;
		} else if (top && atRule && c === CC_SEMICOLON) {
			ended = true;
		} else if (
			c === CC_SEMICOLON &&
			closers[closers.length - 1] === CC_RIGHT_CURLY_BRACKET
		) {
			declarations[declarations.length - 1] = i + 1;
		}
	}
	// WHY: the engine keeps a custom property's value as written, open where the
	// input ran out in it — and so does the CSS minifier. Closed to append another
	// sheet, it would read back another value, so such a sheet is not joined.
	for (let block = 0; block < closers.length; block++) {
		if (
			closers[block] === CC_RIGHT_CURLY_BRACKET &&
			(open.length !== 0 || block !== closers.length - 1) &&
			_startsCustomProperty(css, declarations[block])
		) {
			return null;
		}
	}
	for (let i = closers.length - 1; i >= 0; i--) {
		open += String.fromCharCode(closers[i]);
	}
	if (closers.length !== 0) ended = closers[0] === CC_RIGHT_CURLY_BRACKET;
	return ended ? open : `${open}${atRule ? ";" : "{}"}`;
};

/**
 * The text a `<style>` run prints as one sheet, minified once: each sheet's
 * source, the one before it closed first as its own end of input closed it.
 * @param {HtmlPath} path the accessor
 * @param {HtmlElement} element the run's first `<style>`
 * @param {string} css its own text
 * @param {boolean} absorb whether to mark the rest of the run as printed here
 * @returns {string} the run's text
 */
const _styleRunText = (path, element, css, absorb) => {
	let next = _mergeableStyleAfter(path, element);
	if (next === 0 || _SHEET_NAMESPACE_REGEXP.test(css)) return css;
	let text = css;
	let previous = css;
	for (; next !== 0; next = _mergeableStyleAfter(path, next)) {
		const source = _nodeStrings[_onlyTextChild(next)];
		if (_SHEET_LEADING_AT_RULE_REGEXP.test(source)) break;
		// Only a sheet with another appended needs closing: the last sheet keeps
		// its own end of input, and can contain most of the document's bytes.
		const closing = _sheetClosing(previous);
		if (closing === null) break;
		if (absorb) {
			if (_S._absorbed === null) _S._absorbed = new Set();
			_S._absorbed.add(next);
		}
		text += closing + source;
		previous = source;
	}
	return text;
};

/**
 * Whether a `<style>` prints nothing because its sheet minifies to nothing —
 * with `mergeStyles`, every sheet of the run it opens, since those print here.
 * @param {HtmlPath} path the accessor
 * @param {HtmlElement} element the `<style>` element
 * @returns {boolean} true when its body prints nothing
 */
const _sheetPrintsNothing = (path, element) => {
	const first = _onlyTextChild(element);
	if (first === 0 || !_rendersNow() || !_isCssStyleElement(path, element)) {
		return false;
	}
	const css = _nodeStrings[first];
	const text = _S._mergeStyles ? _styleRunText(path, element, css, false) : css;
	return text.trim() !== "" && _renderedSheet(text) === "";
};

/**
 * Whether an element carries nothing at all, so `removeEmptyElements` may drop
 * it. Every guard but the name table is a rule: a void element is childless by
 * definition, a `<template>`'s children hang off a content fragment, foreign
 * content is not ours to judge, and an element spelling any attribute was
 * written for a reason — which is what keeps `<script src>`, `<iframe src>` and
 * `<div id=mount>`, each of which html-minifier-terser drops.
 *
 * What no guard can cover is CSS: `.spacer { height: 4px }` or a `::before`
 * makes an empty element visible, and this cannot see the stylesheet. That is
 * the risk the option is off by default for.
 * @param {HtmlPath} path the accessor positioned on the element
 * @param {HtmlElement} element the element
 * @returns {boolean} true when the element may be dropped
 */
// Every test is pure, so the O(1) ones come first: `_childrenPrintNothing`
// walks the subtree, and the printer asks this of every element. Memoized for
// the same reason — without it a nested run costs one walk per ancestor.
const _isRemovableEmptyElement = (path, element) => {
	if (!_removeEmptyElements) return false;
	// Asked before the memo: on component markup nearly every element answers
	// here, and its first attribute nearly always prints. One the print drops
	// is not what keeps the element.
	if (_attributeCountOf(element) !== 0 && _printsAnAttribute(path, element)) {
		return false;
	}
	const memo = _emptyElements.get(element);
	if (memo !== undefined) return memo;
	const answer =
		_templateContentOf(element) === 0 &&
		_namespaceOf(element) === NS_HTML &&
		// Only a name known to render nothing empty: a void element that belongs
		// in the head, with no attributes, states nothing either.
		(EMPTY_ELEMENT_DROPPED.has(_tagNameOf(element)) ||
			EMPTY_METADATA_ELEMENTS.has(_tagNameOf(element))) &&
		(_childrenPrintNothing(path, element) ||
			(_tagNameOf(element) === "style" && _sheetPrintsNothing(path, element)));
	_emptyElements.set(element, answer);
	return answer;
};

/**
 * Whether every child of an element prints nothing, so the element ends up
 * empty in the output whatever the source tree holds. Recursive with
 * `_isRemovableEmptyElement`, which is what lets a run of nested empties go
 * together rather than one layer per build.
 * @param {HtmlPath} path the accessor
 * @param {HtmlElement} element the element
 * @returns {boolean} true when nothing inside it reaches the output
 */
const _childrenPrintNothing = (path, element) => {
	for (
		let child = _S._nodeFirstChildren[element];
		child !== 0;
		child = _nextSibling(child)
	) {
		if (
			path.type(child) === NodeType.Element
				? !_isRemovableEmptyElement(path, child)
				: !_printsNothing(path, child)
		) {
			return false;
		}
	}
	return true;
};

/**
 * Whether an attribute is boolean on this element, so its presence alone is the
 * value. `null` in the table means the attribute is global.
 * @param {string} tagName lowercased element name
 * @param {string} name lowercased attribute name
 * @returns {boolean} true when the value carries nothing
 */
const _isBooleanAttribute = (tagName, name) => {
	const elements = BOOLEAN_ATTRIBUTES.get(name);
	if (elements === undefined) return BOOLEAN_ATTRIBUTES.has(name);
	return elements === null || elements.has(tagName);
};

/**
 * Whether an attribute is an event handler on this element, so its value is
 * JavaScript rather than text. `null` in the table means the attribute is a
 * handler on every element.
 * @param {string} tagName lowercased element name
 * @param {string} name lowercased attribute name
 * @returns {boolean} true when the value is a handler body
 */
const _isEventHandlerAttribute = (tagName, name) => {
	const elements = EVENT_HANDLER_ATTRIBUTES.get(name);
	if (elements === undefined) return false;
	return elements === null || elements.has(tagName);
};

/**
 * The `data:` URL a URL attribute holds, read for the renderer, or null where
 * the attribute is no URL on this element or the URL offers nothing.
 * @param {string} tagName lowercased element name
 * @param {string} name lowercased attribute name
 * @param {string} value the attribute's decoded value
 * @returns {ReturnType<typeof readEmbeddedDataURI>} the payload and its language, or null
 */
const _attributeDataUrl = (tagName, name, value) => {
	const elements = URL_ATTRIBUTES.get(name);
	if (elements === undefined || (elements !== null && !elements.has(tagName))) {
		return null;
	}
	const url = stripAsciiWhitespace(value);
	// The URL parser drops every tab and newline, so a payload holding one is
	// not the text the browser reads.
	if ((url.charCodeAt(0) | 0x20) !== 0x64 || /[\t\n\r]/.test(url)) return null;
	return readEmbeddedDataURI(url);
};

// One list's token spans, reused: ordering a list allocates only its result.
/** @type {number[]} */
const _tokenSpanStarts = [];
/** @type {number[]} */
const _tokenSpanEnds = [];
// A hash of each span's length and edge code units, so two tokens that cannot
// be equal are told apart without reading either.
/** @type {number[]} */
const _tokenSpanKeys = [];

/**
 * Record the token `list` holds between `start` and `end` as span `index`.
 * @param {string} list the list
 * @param {number} index which span this is
 * @param {number} start the token's start
 * @param {number} end the token's end
 * @returns {void}
 */
const _recordTokenSpan = (list, index, start, end) => {
	_tokenSpanStarts[index] = start;
	_tokenSpanEnds[index] = end;
	_tokenSpanKeys[index] =
		(((end - start) & 0x3f) << 24) |
		((list.charCodeAt(start) & 0xfff) << 12) |
		(list.charCodeAt(end - 1) & 0xfff);
};

/**
 * Record every token of a collapsed, trimmed list as a span.
 * @param {string} list a single-space-separated list
 * @returns {number} how many tokens it holds
 */
const _collectTokenSpans = (list) => {
	let count = 0;
	for (let at = 0; ;) {
		const space = list.indexOf(" ", at);
		const end = space === -1 ? list.length : space;
		_recordTokenSpan(list, count++, at, end);
		if (space === -1) return count;
		at = space + 1;
	}
};

/**
 * Whether the `count` recorded spans of `list` name the same token twice. The
 * lists are short, so comparing packed keys pairwise beats building a set.
 * @param {string} list the list the spans were recorded over
 * @param {number} count how many spans are recorded
 * @returns {boolean} true when some token appears more than once
 */
const _repeatsAToken = (list, count) => {
	for (let i = 0; i < count; i++) {
		const key = _tokenSpanKeys[i];
		for (let j = i + 1; j < count; j++) {
			if (_tokenSpanKeys[j] !== key) continue;
			const start = _tokenSpanStarts[i];
			const other = _tokenSpanStarts[j];
			const width = _tokenSpanEnds[i] - start;
			if (_tokenSpanEnds[j] - other !== width) continue;
			let k = 0;
			while (
				k < width &&
				list.charCodeAt(start + k) === list.charCodeAt(other + k)
			) {
				k++;
			}
			if (k >= width) return true;
		}
	}
	return false;
};

/**
 * Whether every token of a collapsed list is a word, which is what makes the
 * list the set the DOM reflects rather than another language's text.
 * @param {string} list a collapsed, trimmed token list
 * @returns {boolean} true when no token holds a foreign delimiter
 */
const _tokensAreWords = (list) => {
	const length = list.length;
	for (let start = 0; start < length;) {
		let end = list.indexOf(" ", start);
		if (end === -1) end = length;
		if (!_isWordSpan(list, start, end)) return false;
		start = end + 1;
	}
	return true;
};

/**
 * Collapse an ASCII-whitespace-separated token list (`class`) to single spaces,
 * and — where every token is a word, so the list is the ordered *set* the DOM
 * reflects — drop a token already in it. Order is kept unless `sort` asks.
 * @param {string} raw raw (undecoded) value
 * @param {boolean} isSet whether the DOM reflects the list as a `DOMTokenList`
 * @param {boolean} sort whether to print the tokens in order (`sortTokenLists`)
 * @returns {string} the collapsed list
 */
const _normalizeTokenList = (raw, isSet, sort) => {
	// One walk answers "already collapsed?" and records the spans the repeat
	// check reads, so the common already-minified list is read once.
	const length = raw.length;
	let dirty = false;
	let count = 0;
	let tokenStart = -1;
	for (let i = 0; i < length; i++) {
		const c = raw.charCodeAt(i);
		if (c === 0x20) {
			if (tokenStart === -1) {
				dirty = true;
				break;
			}
			if (isSet) _recordTokenSpan(raw, count, tokenStart, i);
			count++;
			tokenStart = -1;
		} else if (c >= 0x09 && c <= 0x0d && c !== 0x0b) {
			dirty = true;
			break;
		} else if (tokenStart === -1) {
			tokenStart = i;
		}
	}
	if (!dirty && tokenStart === -1 && length !== 0) dirty = true;
	if (!dirty && count === 0) return raw;
	const collapsed = dirty
		? stripAsciiWhitespace(raw).replace(/[\t\n\f\r ]+/g, " ")
		: raw;
	if (dirty && !collapsed.includes(" ")) return collapsed;
	if (!isSet) return collapsed;
	// Cutting the list into tokens is the allocation this runs often enough to
	// care about, and almost no list repeats one — so the checks read the spans
	// in place and only a list that folds pays for the pieces.
	if (sort) {
		return _tokensAreWords(collapsed) ? _sortedTokenList(collapsed) : collapsed;
	}
	if (dirty) {
		count = _collectTokenSpans(collapsed);
	} else {
		_recordTokenSpan(raw, count++, tokenStart, length);
	}
	if (!_repeatsAToken(collapsed, count) || !_tokensAreWords(collapsed)) {
		return collapsed;
	}
	return [...new Set(collapsed.split(" "))].join(" ");
};

/**
 * Compare two of `list`'s tokens where they sit, in the code unit order
 * `Array#sort` puts strings in — a prefix sorts before what extends it.
 * @param {string} list the collapsed list
 * @param {number} aStart first token's start
 * @param {number} aEnd first token's end
 * @param {number} bStart second token's start
 * @param {number} bEnd second token's end
 * @returns {number} negative, zero or positive
 */
const _compareTokens = (list, aStart, aEnd, bStart, bEnd) => {
	const shared = Math.min(aEnd - aStart, bEnd - bStart);
	for (let i = 0; i < shared; i++) {
		const difference =
			list.charCodeAt(aStart + i) - list.charCodeAt(bStart + i);
		if (difference !== 0) return difference;
	}
	return aEnd - aStart - (bEnd - bStart);
};

/**
 * `collapsed`'s tokens in sorted order, with a repeat dropped. They are ordered
 * where they sit, so the pieces `split` would build are never built.
 * @param {string} collapsed a single-space-separated list
 * @returns {string} the ordered set
 */
const _sortedTokenList = (collapsed) => {
	const starts = _tokenSpanStarts;
	const ends = _tokenSpanEnds;
	let count = 0;
	for (let at = 0; ;) {
		const space = collapsed.indexOf(" ", at);
		starts[count] = at;
		ends[count] = space === -1 ? collapsed.length : space;
		count++;
		if (space === -1) break;
		at = space + 1;
	}
	// A list carries a handful of tokens, so insertion sort beats `Array#sort`'s
	// setup and leaves equal tokens adjacent for the pass below to drop.
	for (let i = 1; i < count; i++) {
		const start = starts[i];
		const end = ends[i];
		let j = i - 1;
		while (
			j >= 0 &&
			_compareTokens(collapsed, starts[j], ends[j], start, end) > 0
		) {
			starts[j + 1] = starts[j];
			ends[j + 1] = ends[j];
			j--;
		}
		starts[j + 1] = start;
		ends[j + 1] = end;
	}
	let out = "";
	for (let i = 0; i < count; i++) {
		if (
			i !== 0 &&
			_compareTokens(
				collapsed,
				starts[i - 1],
				ends[i - 1],
				starts[i],
				ends[i]
			) === 0
		) {
			continue;
		}
		if (out !== "") out += " ";
		out += collapsed.slice(starts[i], ends[i]);
	}
	return out;
};

/**
 * Drop the whitespace a comma-separated list's grammar ignores: "split a string
 * on commas" strips each token's edges and nothing else. Whitespace inside a
 * token stays — it is part of the token `accept` keeps, and a separator of its
 * own in the number list `coords` parses.
 * @param {string} raw raw (undecoded) value
 * @returns {string} the normalized list
 */
const _normalizeCommaList = (raw) =>
	raw
		.split(",")
		.map((item) => stripAsciiWhitespace(item))
		.join(",");

/**
 * Normalize an integer attribute: the parse rules skip leading ASCII whitespace
 * and stop at the first non-digit, so the ends and any leading zeros carry
 * nothing. A value those rules would not read as one integer is left alone —
 * only the signed rules accept a `+` or `-`, so a signed value on a
 * non-negative attribute stays as it is (it does not parse at all).
 * @param {string} raw raw (undecoded) value
 * @param {boolean} signed whether the attribute parses with the signed rules
 * @returns {string} the normalized integer
 */
const _normalizeInteger = (raw, signed) => {
	const value = stripAsciiWhitespace(raw);
	if (!(signed ? /^[+-]?\d+$/ : /^\d+$/).test(value)) return raw;
	const negative = value.charCodeAt(0) === 0x2d;
	const digits = value.replace(/^[+-]/, "").replace(/^0+(?=\d)/, "");
	return negative && digits !== "0" ? `-${digits}` : digits;
};

// The `*` bucket, read on every attribute the element does not scope itself.
const ENUMERATED_GLOBAL_KEYWORDS = ENUMERATED_KEYWORDS["*"];

/**
 * The value rewrites that keep an attribute's parsed meaning: a srcset's
 * whitespace, a `style` declaration list, and the viewport `content` list.
 * @param {string} element lowercased element name
 * @param {string} name lowercased attribute name
 * @param {string} raw raw (undecoded) value
 * @param {boolean} viewport whether the element is `<meta name=viewport>`
 * @param {Record<string, Set<string>> | undefined} enumeratedOn what the element scopes itself, looked up once per element by the caller
 * @returns {string} the rewritten value, or `raw` when nothing applies
 */
const _rewriteAttributeValue = (element, name, raw, viewport, enumeratedOn) => {
	// One question for the whole chain below, and most attributes on a page — a
	// `data-*`, an `id`, an `aria-*` — are done here rather than after a miss in
	// each table in turn.
	if (!REWRITABLE_ATTRIBUTES.has(name)) return raw;
	// An enumerated value matches ASCII-case-insensitively, so folding one the
	// spec names changes nothing the DOM reads.
	if (
		_transforms.normalizeEnumeratedAttributes &&
		ENUMERATED_ATTRIBUTE_NAMES.has(name)
	) {
		const own = enumeratedOn === undefined ? undefined : enumeratedOn[name];
		const enumerated =
			own === undefined ? ENUMERATED_GLOBAL_KEYWORDS[name] : own;
		if (enumerated !== undefined) {
			// Case-insensitively, and nothing else: an enumerated attribute names a
			// keyword, so `type="time "` is not `time` — it is the value no keyword
			// matches, which puts the input in its default state.
			const lowered = asciiLowercase(raw);
			// A value the spec does not enumerate is left exactly as written:
			// `target` names a browsing context, and `type` on a custom element
			// means what it says.
			if (enumerated.has(lowered)) return lowered;
		}
	}
	if (SRCSET_ATTRIBUTES.has(name)) {
		return _transforms.normalizeListAttributes ? _normalizeSrcset(raw) : raw;
	}
	if (name === "style") return _minifyStyleAttribute(raw);
	// A list `sortTokenLists` reorders is read whatever `normalizeListAttributes`
	// says: another order rewrites the separators between the tokens anyway.
	const tokenListOn =
		_transforms.normalizeListAttributes ||
		(_sortTokenLists && DOM_TOKEN_LIST_ATTRIBUTES.has(name))
			? TOKEN_LIST_ATTRIBUTES.get(name)
			: undefined;
	if (
		tokenListOn !== undefined &&
		(tokenListOn === null || tokenListOn.has(element))
	) {
		// Only the lists the DOM reflects as a `DOMTokenList` are sets, so only
		// they may be reordered or have a repeat folded away — `ping` is the order
		// its requests go out in and `accesskey` the order its keys are tried.
		const isSet = DOM_TOKEN_LIST_ATTRIBUTES.has(name);
		return _normalizeTokenList(raw, isSet, _sortTokenLists && isSet);
	}
	if (_transforms.normalizeListAttributes) {
		if (viewport && name === "content") return _normalizeViewport(raw);
		if (COMMA_LIST_ATTRIBUTES.has(name)) return _normalizeCommaList(raw);
	}
	const urlOn = _transforms.normalizeUrlAttributes
		? URL_ATTRIBUTES.get(name)
		: undefined;
	if (urlOn !== undefined && (urlOn === null || urlOn.has(element))) {
		return stripAsciiWhitespace(raw);
	}
	const integerOn = _transforms.normalizeNumericAttributes
		? INTEGER_ATTRIBUTES.get(name)
		: undefined;
	if (
		integerOn !== undefined &&
		(integerOn === null || integerOn.has(element))
	) {
		return _normalizeInteger(raw, SIGNED_INTEGER_ATTRIBUTES.has(name));
	}
	return raw;
};

/**
 * Whether an element is the viewport `<meta>`, whose content list is the only
 * `content` this rewrites.
 * @param {HtmlPath} path the accessor positioned on the `<meta>`
 * @returns {boolean} true for `<meta name=viewport>`
 */
const _isViewportMeta = (path) => {
	for (const attribute of _attributeList(path.node)) {
		if (attribute.name === "name") {
			const value = attribute.value;
			return (
				asciiLowercase(
					stripAsciiWhitespace(
						value.includes("&") ? decodeEntities(value, true) : value
					)
				) === "viewport"
			);
		}
	}
	return false;
};

/**
 * Re-serialize a srcset with its whitespace reduced to what the grammar needs.
 * The candidate list is unchanged — only the bytes between candidates go — and
 * anything the parser rejects is handed back untouched.
 * @param {string} raw raw (undecoded) attribute value
 * @returns {string} the normalized value
 */
const _normalizeSrcset = (raw) => {
	let candidates;
	try {
		candidates = parseSrcset(raw);
	} catch (_err) {
		return raw;
	}
	if (candidates.length === 0) return raw;
	let out = "";
	let previousBare = false;
	for (let i = 0; i < candidates.length; i++) {
		const url = candidates[i][0];
		const end = candidates[i][2];
		const nextStart =
			i + 1 < candidates.length ? candidates[i + 1][1] : raw.length;
		let descriptor = raw.slice(end, nextStart);
		const comma = descriptor.indexOf(",");
		if (comma !== -1) descriptor = descriptor.slice(0, comma);
		descriptor = stripAsciiWhitespace(descriptor).replace(/[\t\n\f\r ]+/g, " ");
		// A descriptor-less URL runs to the next whitespace, so without one it
		// would swallow the separating comma and the URL behind it.
		if (i !== 0) out += previousBare ? ", " : ",";
		out += descriptor === "" ? url : `${url} ${descriptor}`;
		previousBare = descriptor === "";
	}
	return out;
};

/**
 * Infra's ASCII lowercase, which is all HTML folds: `toLowerCase` also maps
 * characters the parser leaves alone (U+212A onto `k`). Returns the input
 * itself when there is nothing to fold.
 * @param {string} value source name or value
 * @returns {string} the value as the parser would match it
 */
const asciiLowercase = (value) => {
	for (let i = 0; i < value.length; i++) {
		const c = value.charCodeAt(i);
		if (c >= 0x41 && c <= 0x5a) {
			return (
				value.slice(0, i) +
				value
					.slice(i)
					.replace(/[A-Z]/g, (u) => String.fromCharCode(u.charCodeAt(0) + 0x20))
			);
		}
	}
	return value;
};

/**
 * Whether a raw attribute value keeps its meaning unquoted: §13.1.2.3 admits a
 * non-empty run with no whitespace, quote, backtick, `=`, `<` or `>`. Character
 * references decode identically either way, so `&` needs no guard.
 * @param {string} raw raw (undecoded) value, without its quotes
 * @returns {boolean} true when the unquoted form re-parses to the same value
 */
const _canUnquoteAttributeValue = (raw) => {
	for (let i = 0; i < raw.length; i++) {
		const c = raw.charCodeAt(i);
		if (
			c === 0x20 ||
			c === 0x22 ||
			c === 0x27 ||
			c === 0x3c ||
			c === 0x3d ||
			c === 0x3e ||
			c === 0x60 ||
			(c >= 0x09 && c <= 0x0d && c !== 0x0b)
		) {
			return false;
		}
	}
	return raw.length !== 0;
};

// How often each attribute name occurs in the document being printed, or null
// until `sortAttributes` asks. Built from the parse's attribute column rather
// than by walking the tree, which already holds every one in a flat run.
/** @type {Map<string, number> | null} */
let _attributeFrequencies = null;

// Reused by `_sortedAttributes`: one element's attribute indices and their
// ranks, so ordering a tag allocates nothing.
/** @type {number[]} */
const _sortOrderScratch = [];
/** @type {number[]} */
const _sortKeyScratch = [];

// The print order is a property of the document, so resolving a name to its
// rank once leaves the per-element sort comparing two integers.
/** @type {Map<string, number> | null} */
let _attributeRanks = null;

/**
 * @returns {Map<string, number>} each attribute name's place in the print order
 */
const _attributeRank = () => {
	if (_attributeRanks !== null) return _attributeRanks;
	const frequency = _attributeFrequency();
	const names = [...frequency.keys()].sort((a, b) => {
		// Commonest names first, so the run two elements share is one run of bytes
		// a compressor pays for once. Name order breaks ties, keeping this total.
		const byFrequency =
			/** @type {number} */ (frequency.get(b)) -
			/** @type {number} */ (frequency.get(a));
		if (byFrequency !== 0) return byFrequency;
		return a < b ? -1 : 1;
	});
	const ranks = new Map();
	for (let i = 0; i < names.length; i++) ranks.set(names[i], i);
	_attributeRanks = ranks;
	return ranks;
};

/**
 * How often each attribute name reaches the output. Counted over the runs the
 * elements own rather than the whole column, because an attribute a transform
 * drops is not one the print order should be ranked on.
 * @returns {Map<string, number>} how often each attribute name occurs
 */
const _attributeFrequency = () => {
	if (_attributeFrequencies !== null) return _attributeFrequencies;
	const frequency = new Map();
	for (let node = 1; node <= _nodeCount; node++) {
		if (_S._nodeTypes[node] !== NodeType.Element) continue;
		const count = _nodeAttrCounts[node];
		if (count === 0) continue;
		const start = _nodeAttrStarts[node];
		// Foreign content keeps every attribute it was written with, so only an
		// HTML element is asked what it drops — the same test the printer makes.
		const namespace = _namespaceOf(node);
		const html = namespace === NS_HTML;
		const elementName = _tagNameOf(node);
		const markers =
			html && _S._removeRedundantAttributes !== "none"
				? REDUNDANT_TYPE_ATTRIBUTES[elementName]
				: undefined;
		const defaults =
			html && _S._removeRedundantAttributes === "all"
				? REDUNDANT_DEFAULT_ATTRIBUTES[elementName]
				: undefined;
		const mayDrop = markers !== undefined || defaults !== undefined;
		for (let i = start; i < start + count; i++) {
			if (namespace === NS_XML && _attrNameStarts[i] === -1) continue;
			const name = _attrNames[i];
			if (html) {
				// Valueless reads as the empty value here as it does there.
				const value = _attrValueOf(i);
				if (
					(mayDrop &&
						_isRedundantAttribute(
							markers,
							defaults,
							elementName,
							name,
							value
						)) ||
					_isRemovableEmptyAttribute(elementName, name, value)
				) {
					continue;
				}
			}
			frequency.set(name, (frequency.get(name) || 0) + 1);
		}
	}
	_attributeFrequencies = frequency;
	return frequency;
};

/**
 * The element's attribute indices in the order `sortAttributes` prints them:
 * the document's commonest names first, ties by name.
 * @param {HtmlPath} path the accessor positioned on the element
 * @param {number} count its attribute count
 * @returns {number[]} the indices, in print order
 */
const _sortedAttributes = (path, count) => {
	const ranks = _attributeRank();
	const order = _sortOrderScratch;
	const keys = _sortKeyScratch;
	for (let i = 0; i < count; i++) {
		order[i] = i;
		// A name with no rank is one every element drops, so it prints nowhere —
		// but as `undefined` it would compare false both ways and stop the sort
		// short, leaving the names after it where a second pass would move them.
		const rank = ranks.get(_attrNameOf(_attributeAt(i, path.node)));
		keys[i] = rank === undefined ? ranks.size : rank;
	}
	// An element carries a handful of attributes, so a stable insertion sort over
	// their ranks beats `Array#sort`'s setup and the comparator it would call.
	for (let i = 1; i < count; i++) {
		const key = keys[i];
		const index = order[i];
		let j = i - 1;
		while (j >= 0 && keys[j] > key) {
			keys[j + 1] = keys[j];
			order[j + 1] = order[j];
			j--;
		}
		keys[j + 1] = key;
		order[j + 1] = index;
	}
	return order;
};

/**
 * Whether the source's own bytes still separate an attribute from the one this
 * print just wrote. A quoted value ends at its quote, an unquoted one only at
 * whitespace, and a bare name at whitespace or `/`.
 * @param {string} out the tag rebuilt so far
 * @param {boolean} unquotedTail whether what it ends with is an unquoted value
 * @param {string} separator the source bytes standing between the two
 * @returns {boolean} true where nothing further is needed
 */
const _separates = (out, unquotedTail, separator) => {
	const last = out.charCodeAt(out.length - 1);
	if (last === CC_QUOTATION_MARK || last === CC_APOSTROPHE) return true;
	if (separator === "") return false;
	const cc = separator.charCodeAt(0);
	return isAsciiWhitespace(cc) || (!unquotedTail && cc === CC_SOLIDUS);
};

/**
 * Whether the run spells a name no HTML vocabulary would: sorting such a tag
 * reorders the statement another language wrote its delimiters into.
 * @param {HtmlPath} path the accessor positioned on the element
 * @param {number} count how many attributes the element has
 * @returns {boolean} true where one of the names is no word
 */
const _holdsForeignSyntax = (path, count) => {
	for (let i = 0; i < count; i++) {
		const name = _attrNameOf(_attributeAt(i, path.node));
		if (!_isWordSpan(name, 0, name.length)) return true;
	}
	return false;
};

/**
 * Remove the one space every attribute's text below opens with, `at` naming it,
 * so the source's own separator stands in its place.
 * @param {string} out the tag rebuilt so far
 * @param {number} at where the space sits, or -1 before the first attribute
 * @returns {string} the text without it
 */
const _dropOpenedSpace = (out, at) =>
	at === -1 ? out : out.slice(0, at) + out.slice(at + 1);

/**
 * Drop a `/` written as a separator that nothing followed, `at` naming it: the
 * attribute it stood before was dropped, so it now separates nothing.
 * @param {string} out the tag rebuilt so far
 * @param {number} at where the `/` sits, or -1 where none is pending
 * @returns {string} the text without it
 */
const _dropDanglingSolidus = (out, at) =>
	at !== -1 && at === out.length - 1 ? out.slice(0, at) : out;

/**
 * A `style` attribute written with its declaration list minified.
 * @param {string} minified the declaration list to write
 * @param {boolean} bare whether an empty list is written as the bare name
 * @param {string} attributeName the attribute's name
 * @param {string} rawValue the value as the source wrote it
 * @param {string | null} decoded the value its references decode to, null without any
 * @param {boolean} quoted whether the source quoted the value
 * @param {string} delimiter the source's quote, "" when unquoted
 * @returns {{ text: string, unquotedTail: boolean } | null} what to emit, the bare name for an empty list as a next pass reads it
 */
const _writeStyle = (
	minified,
	bare,
	attributeName,
	rawValue,
	decoded,
	quoted,
	delimiter
) =>
	minified === "" && bare
		? { text: ` ${attributeName}`, unquotedTail: false }
		: minified !== (decoded !== null ? decoded : rawValue)
			? _writeStyleList(attributeName, minified, delimiter)
			: _writeAttribute(
					attributeName,
					rawValue,
					rawValue,
					decoded,
					quoted,
					delimiter
				);

/**
 * Rewrite an opening tag from its own source spans, collapsing the whitespace
 * between its attributes to one space and dropping the quotes around any value
 * that is legal unquoted (§13.1.2.3). webpack's own late asset passes match the
 * sentinels they left behind with the quote optional, so they still see them
 * (see `INTEGRITY_SENTINEL_REGEXP`); a third-party post-processor that regexes
 * emitted HTML assuming quotes would not, which is why `removeAttributeQuotes`
 * is off by default in html-minifier.
 *
 * Falls back to the source tag when an attribute lies outside it — a repeated
 * `<html>` / `<body>` tag merges its attributes onto the element already open,
 * and those cannot be sliced from here.
 * @param {HtmlPath} path the accessor positioned on the element
 * @param {string} open the opening tag's source text
 * @returns {string} the minified opening tag
 */
const _minifyOpenTag = (path, open) => {
	const node = path.node;
	const tagStart = _nodeStarts[node];
	const tagEnd = _nodeTagEnds[node];
	const count = _nodeAttrCounts[node];
	const attrStart = _nodeAttrStarts[node];
	// HTML parsing ASCII-lowercases names, so folding one changes nothing. Foreign
	// content keeps its source bytes: SVG admits element names no adjustment table
	// would case-correct on the way back in.
	const html = (_S._nodeFlags[node] & NS_MASK) === NS_HTML;
	// A tag another language wrote: what sits between the attributes the spec's
	// duplicate drop kept is carried over from the source rather than collapsed.
	const verbatim =
		count !== 0 &&
		_foreignSyntaxAttrRuns.size !== 0 &&
		_foreignSyntaxAttrRuns.has(attrStart);
	const nameEnd = _nodeNameEnds[node] - tagStart;
	// A bare `<name>` — half of all tags — is what the rest would rebuild byte
	// for byte; decided before the name is sliced, so it allocates nothing.
	if (count === 0 && open.length === nameEnd + 1) {
		let folds = false;
		if (html) {
			for (let i = 1; i < nameEnd; i++) {
				const c = open.charCodeAt(i);
				if (c >= 0x41 && c <= 0x5a) {
					folds = true;
					break;
				}
			}
		}
		if (!folds) return open;
	}
	const sourceName = open.slice(1, nameEnd);
	const elementName = html ? asciiLowercase(sourceName) : sourceName;
	let out = `<${elementName}`;
	const isViewportMeta =
		html && _nodeStrings[node] === "meta" && _isViewportMeta(path);
	let unquotedTail = false;
	// Where the space an attribute's own text opens with sits, so a source
	// separator written in front of it replaces rather than doubles it.
	let openedSpaceAt = -1;
	let solidusAt = -1;
	// Only a foreign element's surviving `/>` reads it, so only there is a
	// deferred answer held to the shape the built-in was written in.
	const tailIsRead = !html && open.charCodeAt(open.length - 2) === 47;
	let consumedEnd = _nodeNameEnds[node];
	// HTML only, which is what keeps `consumedEnd` meaningful: it is read once
	// more below, for a foreign element's `/>` alone. Per element rather than per
	// attribute, these being the printer's hottest lookups.
	const markers =
		html && _S._removeRedundantAttributes !== "none"
			? REDUNDANT_TYPE_ATTRIBUTES[elementName]
			: undefined;
	const defaults =
		html && _S._removeRedundantAttributes === "all"
			? REDUNDANT_DEFAULT_ATTRIBUTES[elementName]
			: undefined;
	const mayDrop = markers !== undefined || defaults !== undefined;
	const enumeratedOn = html ? ENUMERATED_KEYWORDS[elementName] : undefined;
	const order =
		_sortAttributes &&
		html &&
		!verbatim &&
		count > 1 &&
		!_holdsForeignSyntax(path, count)
			? _sortedAttributes(path, count)
			: null;
	for (let k = 0; k < count; k++) {
		const i = order === null ? k : order[k];
		const attribute = attrStart + i;
		const nameStart = _attrNameStarts[attribute];
		const nameEnd = _attrNameEnds[attribute];
		if (nameStart < tagStart || nameEnd > tagEnd) return open;
		out = _dropOpenedSpace(out, openedSpaceAt);
		out = _dropDanglingSolidus(out, solidusAt);
		openedSpaceAt = -1;
		solidusAt = -1;
		if (verbatim) {
			let separator = open.slice(consumedEnd - tagStart, nameStart - tagStart);
			// A space of its own where the source's bytes no longer separate: what
			// this print dropped is what held the value off the template.
			if (!_separates(out, unquotedTail, separator)) {
				separator = ` ${separator}`;
			}
			out += separator;
			openedSpaceAt = out.length;
		} else if (
			order === null &&
			!unquotedTail &&
			nameStart - consumedEnd === 1 &&
			open.charCodeAt(consumedEnd - tagStart) === CC_SOLIDUS
		) {
			// A `/` the tokenizer read as the separator and another language as part
			// of a word, `{{/if}}` among them: it separates either way, at one byte.
			out += "/";
			solidusAt = out.length - 1;
			openedSpaceAt = out.length;
		}
		// Template identifiers are parsed as attribute names too, but their case
		// belongs to the template language. Keep their source spelling like SVG's.
		const attributeName =
			html && !verbatim
				? _attrNames[attribute]
				: open.slice(nameStart - tagStart, nameEnd - tagStart);
		const valueStart = _attrValueStarts[attribute];
		if (valueStart === -1) {
			// Valueless is the empty value, so it goes with the empty ones.
			if (
				html &&
				!verbatim &&
				_isRemovableEmptyAttribute(elementName, attributeName, "")
			) {
				consumedEnd = nameEnd;
				continue;
			}
			out += ` ${attributeName}`;
			unquotedTail = false;
			consumedEnd = nameEnd;
			continue;
		}
		// `attributeValueStart` points past the opening quote, `attributeValueEnd`
		// at the closing one, so a quoted value's span is one wider on each side.
		const quote = open.charCodeAt(valueStart - 1 - tagStart);
		const quoted = quote === 34 || quote === 39;
		const sourceDelimiter = quoted ? open[valueStart - tagStart - 1] : "";
		const rawEnd = _attrValueEnds[attribute];
		const valueEnd = rawEnd + (quoted ? 1 : 0);
		const rawValue = open.slice(valueStart - tagStart, rawEnd - tagStart);
		// Dropped before any rewrite, and whether or not the source quoted it:
		// the value only restates the default, or carries nothing at all — a
		// `style` included whose declarations the renderer minifies to nothing.
		if (
			html &&
			!verbatim &&
			((mayDrop &&
				_isRedundantAttribute(
					markers,
					defaults,
					elementName,
					attributeName,
					rawValue
				)) ||
				_isRemovableEmptyAttribute(elementName, attributeName, rawValue) ||
				(attributeName === "style" &&
					_styleRendersEmpty(elementName, rawValue)))
		) {
			consumedEnd = valueEnd;
			continue;
		}
		// A whole document, so it is offered decoded and written back escaped —
		// the one attribute whose reference-carrying value is still rewritten.
		if (
			html &&
			!verbatim &&
			(_S._renderEmbeddedSource !== undefined ||
				(_S._deferEmbeddedSource !== undefined && _deferSrcdoc)) &&
			attributeName === "srcdoc" &&
			elementName === "iframe" &&
			rawValue !== ""
		) {
			const document = decodeEntities(rawValue, true);
			// Read now: an answer arriving after the print finds the options reset.
			const bare = _transforms.normalizeAttributeQuotes;
			/**
			 * @param {string} rendered the document the renderer answered
			 * @param {string} delimiter the quote the source spelled it with, if any
			 * @returns {string} the attribute — the bare name for an empty one, as a next pass reads it
			 */
			const writeDocument = (rendered, delimiter) =>
				rendered === "" && bare
					? ` ${attributeName}`
					: _writeQuotedAttribute(attributeName, rendered, delimiter);
			// The whole attribute is deferred, not the document inside it: what
			// escaping it needs is decided by what it holds, which is the answer.
			if (_S._deferEmbeddedSource !== undefined && _deferSrcdoc) {
				// Declining leaves the attribute as written, delimiter and all: a bare
				// value has none, and quoting one would only lengthen it.
				const delimiter = sourceDelimiter;
				const asWritten = ` ${attributeName}=${delimiter}${rawValue}${delimiter}`;

				out += _deferEmbedded(document, HTML_TYPE, (rendered) =>
					typeof rendered !== "string" || rendered === document
						? asWritten
						: writeDocument(rendered, delimiter)
				);
				unquotedTail = false;
				consumedEnd = valueEnd;
				continue;
			}

			// The synchronous renderer: `_renderEmbedded` defers where a collector
			// is set, and this attribute is the one kept off that path.
			const rendered = _renderEmbeddedOrNull(document, HTML_TYPE);
			if (rendered !== null && rendered !== document) {
				out += writeDocument(
					rendered,
					quoted ? open[valueStart - tagStart - 1] : ""
				);
				unquotedTail = false;
				consumedEnd = valueEnd;
				continue;
			}
		}
		// Where nothing below can change the value: foreign content rewrites only
		// `style`, and a bare value is already the shortest spelling of itself.
		if (quoted || html || attributeName === "style" || rawValue.includes("&")) {
			const hasReference = rawValue.includes("&");
			// Rewritten from the source text where it carries no reference; one that
			// does is rewritten from what it decodes to, below.
			let value = rawValue;
			if (!hasReference) {
				if (html) {
					value = _rewriteAttributeValue(
						elementName,
						attributeName,
						rawValue,
						isViewportMeta,
						enumeratedOn
					);
				} else if (attributeName === "style") {
					// The one foreign-content attribute HTML's rules still fit: SVG and
					// MathML `style` is a CSS declaration list, same as HTML's. Every
					// other name is adjusted or means something else there.
					value = _minifyStyleAttribute(rawValue);
				}
			}
			// WHY: an empty value is the bare name — the after-attribute-name state
			// reads `x` in `<p x>` and `<p x="">` as one attribute with one empty
			// value, in foreign content too — and a boolean spelled by its own name
			// is the same presence, which the spec does canonicalize. Writing either
			// as the bare name is just the `=""` going, a quoting decision, so it
			// answers to the switch naming the rest of them. The decode is guarded on
			// the reference to stay off the hot path, and tried before unquoting so
			// every reference it writes stays bare.
			const decoded = hasReference ? decodeEntities(rawValue, true) : null;
			const spellable = decoded;
			// The name the presence is spelled with is what it decodes to, so
			// `checked="&#x63;…"` is the same presence as `checked="checked"`.
			const presence = spellable !== null ? spellable : rawValue;
			// The grammars read what the value decodes to, so a referenced one is
			// rewritten from that and spelled back. `style` has its own path below.
			const rewritten =
				spellable !== null && html && attributeName !== "style"
					? _rewriteAttributeValue(
							elementName,
							attributeName,
							spellable,
							isViewportMeta,
							enumeratedOn
						)
					: spellable;
			// What the attribute ends up saying, whichever spelling it arrived in:
			// the bare name answers to that, not to the side the value was written.
			const settled = rewritten !== null ? rewritten : value;
			if (
				_transforms.normalizeAttributeQuotes &&
				(settled === "" ||
					(html &&
						_transforms.collapseBooleanAttributes &&
						(_transforms.collapseBooleanAttributes === "all" ||
							(presence.length === attributeName.length &&
								asciiLowercase(presence) === attributeName)) &&
						_isBooleanAttribute(elementName, attributeName)))
			) {
				out += ` ${attributeName}`;
				unquotedTail = false;
				consumedEnd = valueEnd;
				continue;
			}
			const isStyle = attributeName === "style";
			// A `style=""` carrying a character reference is a declaration list like
			// any other once decoded — `&quot;` is a quote to CSS, not an entity — so
			// it is minified from the decoded text and spelled back from there.
			const declarations =
				isStyle && spellable !== null ? _minifyStyleAttribute(spellable) : null;
			// Read now: an answer arriving after the print finds the options reset.
			const bare = _transforms.normalizeAttributeQuotes;
			const written = isStyle
				? _writeStyle(
						declarations !== null ? declarations : value,
						bare,
						attributeName,
						rawValue,
						spellable,
						quoted,
						sourceDelimiter
					)
				: _writeAttribute(
						attributeName,
						value,
						rawValue,
						rewritten,
						quoted,
						sourceDelimiter
					);
			// The JavaScript an event handler holds, offered decoded — `&amp;&amp;`
			// is an operator to JavaScript rather than an entity. No built-in.
			const handler =
				!isStyle &&
				!verbatim &&
				(_S._renderEmbeddedSource !== undefined ||
					_S._deferEmbeddedSource !== undefined) &&
				_isEventHandlerAttribute(elementName, attributeName)
					? hasReference
						? spellable
						: rawValue
					: null;

			if (handler !== null && stripAsciiWhitespace(handler) !== "") {
				const delimiter = sourceDelimiter;
				// A foreign `/>` reads back whether this value ends unquoted, which a
				// deferred answer is not there to decide: the source's quoting stands.
				const holdQuoted = tailIsRead && _S._deferEmbeddedSource !== undefined;
				// Declining leaves the attribute as an untapped run writes it, which is
				// the source spelling wherever nothing shorter was found.
				const asWritten =
					written === null || holdQuoted
						? ` ${attributeName}=${delimiter}${rawValue}${delimiter}`
						: written.text;
				const declinedTail =
					!holdQuoted && written !== null && written.unquotedTail;
				/**
				 * The answer is only written where it spells shorter than the source
				 * does: a minifier ends a statement it was handed without one, so a
				 * handler as short as `onclick="x"` comes back a byte longer.
				 * @param {string | null | undefined} rendered what the renderer answered
				 * @returns {{ text: string, unquotedTail: boolean } | null} what to emit, or null where nothing it answered is written
				 */
				const writeHandler = (rendered) => {
					if (typeof rendered !== "string") return null;

					// The answer is source text, so it is escaped for the delimiter it
					// lands in — an `&` in it opens a reference where it is not.
					const answered = holdQuoted
						? {
								text: _writeQuotedAttribute(attributeName, rendered, delimiter),
								unquotedTail: false
							}
						: _writeAttribute(
								attributeName,
								escapeAttribute(rendered, CC_QUOTATION_MARK, true),
								rawValue,
								rendered,
								quoted,
								sourceDelimiter
							);

					return answered !== null && answered.text.length < asWritten.length
						? answered
						: null;
				};

				if (_S._deferEmbeddedSource !== undefined) {
					out += _deferEmbedded(
						handler,
						JAVASCRIPT_TYPE,
						(rendered) => {
							const answered = writeHandler(rendered);

							return answered === null ? asWritten : answered.text;
						},
						EVENT_HANDLER
					);
					unquotedTail = declinedTail;
				} else {
					const answered = writeHandler(
						_renderEmbeddedOrNull(handler, JAVASCRIPT_TYPE, EVENT_HANDLER)
					);

					out += answered === null ? asWritten : answered.text;
					unquotedTail =
						answered === null ? declinedTail : answered.unquotedTail;
				}
				consumedEnd = valueEnd;
				continue;
			}

			// A URL attribute's `data:` payload, offered decoded as its media type's
			// language and written back only where the rebuilt URL spells shorter.
			const dataUrl =
				html &&
				!isStyle &&
				!verbatim &&
				(_S._renderEmbeddedSource !== undefined ||
					_S._deferEmbeddedSource !== undefined)
					? _attributeDataUrl(
							elementName,
							attributeName,
							hasReference ? /** @type {string} */ (spellable) : rawValue
						)
					: null;

			if (dataUrl !== null) {
				// HTML only, so no foreign `/>` reads back how this value ends.
				const asWritten =
					written === null
						? ` ${attributeName}=${sourceDelimiter}${rawValue}${sourceDelimiter}`
						: written.text;
				const declinedTail = written !== null && written.unquotedTail;
				/**
				 * @param {string | null | undefined} rendered what the renderer answered
				 * @returns {{ text: string, unquotedTail: boolean } | null} what to emit, or null where nothing it answered is written
				 */
				const writeDataUrl = (rendered) => {
					if (typeof rendered !== "string" || rendered === dataUrl.payload) {
						return null;
					}
					const url = buildDataURI(dataUrl.parsed, rendered);
					const answered = _writeAttribute(
						attributeName,
						escapeAttribute(url, CC_QUOTATION_MARK, true),
						rawValue,
						url,
						quoted,
						sourceDelimiter
					);

					return answered !== null && answered.text.length < asWritten.length
						? answered
						: null;
				};

				if (_S._deferEmbeddedSource !== undefined) {
					out += _deferEmbedded(dataUrl.payload, dataUrl.type, (rendered) => {
						const answered = writeDataUrl(rendered);

						return answered === null ? asWritten : answered.text;
					});
					unquotedTail = declinedTail;
				} else {
					const answered = writeDataUrl(
						_renderEmbeddedOrNull(dataUrl.payload, dataUrl.type)
					);

					out += answered === null ? asWritten : answered.text;
					unquotedTail =
						answered === null ? declinedTail : answered.unquotedTail;
				}
				consumedEnd = valueEnd;
				continue;
			}

			// Offered whatever the built-in made of it: a list already at its
			// shortest is one `_writeAttribute` declines, not one to ask nobody about.
			if (
				isStyle &&
				!verbatim &&
				_S._deferEmbeddedSource !== undefined &&
				_S._embeddedAnswers === undefined
			) {
				// Declining leaves the attribute as written, delimiter and all — an
				// answer nothing beats is the renderer keeping what it was given.
				const delimiter = sourceDelimiter;
				const asWritten = ` ${attributeName}=${delimiter}${rawValue}${delimiter}`;
				// What stands until an answer arrives, and the tag ending it commits
				// to: the built-in's text where it beat the source, else the source.
				const standing = written !== null ? written.text : asWritten;
				const standingTail = written !== null && written.unquotedTail;

				// The list is deferred, not the shape around it: the tag ending is
				// read back before an answer arrives, so one changing it is declined.
				out += _deferEmbedded(
					// Decoded when the source spelled it with references, since that
					// is the list itself rather than one attribute's spelling.
					spellable !== null ? spellable : rawValue,
					CSS_TYPE,
					(rendered) => {
						if (typeof rendered !== "string") return standing;

						const answered = _writeStyle(
							rendered,
							bare,
							attributeName,
							rawValue,
							spellable,
							quoted,
							sourceDelimiter
						);
						// Declining ends the value quoted: this is the branch the
						// source spelled with a delimiter.
						const tail = answered !== null && answered.unquotedTail;

						if (tailIsRead && tail !== standingTail) return standing;

						return answered !== null ? answered.text : asWritten;
					},
					BLOCK_CONTENTS,
					// Written empty, it is the bare name or no attribute at all — and
					// the element holding it may go with it.
					(rendered) => rendered === ""
				);
				unquotedTail = standingTail;
				consumedEnd = valueEnd;
				continue;
			}
			if (written !== null) {
				out += written.text;
				unquotedTail = written.unquotedTail;
				consumedEnd = valueEnd;
				continue;
			}
			// Quoting may be frozen, but a rewrite changed what the value says, so
			// the source's own spelling of it cannot stand.
			if (rewritten !== null && rewritten !== spellable) {
				out += _writeQuotedAttribute(attributeName, rewritten, sourceDelimiter);
				unquotedTail = false;
				consumedEnd = valueEnd;
				continue;
			}
		}
		// A quoted value carries its quotes over; an unquoted one starts at the
		// value itself.
		out += ` ${attributeName}=${open.slice(
			(quoted ? valueStart - 1 : valueStart) - tagStart,
			valueEnd - tagStart
		)}`;
		unquotedTail = !quoted;
		consumedEnd = valueEnd;
	}
	out = _dropOpenedSpace(out, openedSpaceAt);
	out = _dropDanglingSolidus(out, solidusAt);
	if (verbatim) {
		const tail = open.slice(consumedEnd - tagStart, -1);
		// The same fusing rule as between two attributes, over whatever the source
		// wrote before the `>`.
		return tail === "" || _separates(out, unquotedTail, tail)
			? `${out}${tail}>`
			: `${out} ${tail}>`;
	}
	// WHY: a source `/>` self-closes a foreign element and survives there, but
	// only where the `/` is the flag: in `<a href=x/>` it ends an unquoted value,
	// and after one of those it needs a space or it fuses into the value. An HTML
	// element's parser ignores it, so there it goes.
	if (
		_namespaceOf(path.node) !== NS_HTML &&
		open.charCodeAt(open.length - 2) === 47 &&
		tagEnd - 2 >= consumedEnd
	) {
		return `${out}${unquotedTail ? " /" : "/"}>`;
	}
	return `${out}>`;
};

/**
 * Whether a comment must survive minification: downlevel conditional comments
 * (`<!--[if …]>` / `<![endif]-->`) drive IE branching, server-side includes
 * (`<!--#…-->`) are directives, and a `<?…?>` bogus comment is a server-side
 * template tag — all behavior-bearing. Every other comment is inert, so
 * dropping it preserves meaning.
 * @param {string} data comment data (between `<!--` and `-->`)
 * @param {string} source the comment's source text, including its delimiters
 * @param {number} start the comment's start offset (at `<`)
 * @returns {boolean} true to keep the comment
 */
const _keepComment = (data, source, start) => {
	// `<?php … ?>` and `<%… %>` reach the tree as bogus comments (§13.2.5.42),
	// and the forms below drive IE branching or a server-side include. Every one
	// is code rather than a comment, so no level drops it.
	if (source.charCodeAt(1) === 63) return true;
	const trimmed = data.trimStart();
	// `<![` covers the downlevel-revealed forms: `<!--<![endif]-->` closers and
	// standalone `<![if …]>` openers.
	if (
		trimmed.startsWith("[if") ||
		trimmed.startsWith("[endif") ||
		trimmed.startsWith("<![") ||
		trimmed.startsWith("#")
	) {
		return true;
	}
	const kept = _commentsKept;
	// Every other comment is inert, so `"some"` is where the two languages part:
	// nothing left here carries anything a parser reads.
	if (typeof kept === "boolean") return kept;
	if (kept === "some") return false;
	// A pattern or a predicate of the author's, over the comment's own text, with
	// its line and column for a caller placing it.
	if (_commentLocs === undefined) {
		_commentLocs = new LocConverter(_commentInput);
	}
	const loc = _commentLocs.get(start);
	return kept(data, loc.line, loc.column) === true;
};

// WHY: one object rather than one binding each, so the printer module reads
// the same fields the walk writes — every slot is initialized here, so the
// shape never transitions (see AGENTS.md "Keep instance shapes stable").
const _S = {
	// A run of adjacent `<style>` or `<script>` prints as one, so the elements
	// after the first print nothing; null until a document has a run.
	/** @type {Set<HtmlElement> | null} */
	_absorbed: null,
	// The source open tag `_elementOpenTag` last read — its second result, passed
	// out of band so the per-element pair costs no object.
	_elementOpenSource: "",
	// The caller's renderer for source nested in this document, set per print: the
	// only way any of it is minified, `builtinEmbeddedRenderer` being webpack's own.
	/** @type {EmbeddedSourceRenderer | undefined} */
	_renderEmbeddedSource: undefined,
	// Where an asynchronous caller's embedded sources are recorded instead of
	// rendered, set per print; what the print decides from a body reads its source.
	/** @type {DeferredEmbeddedSource[] | undefined} */
	_deferEmbeddedSource: undefined,
	// What a print before this one was answered, set where `processAsync` prints
	// again because an answer overturned a choice made without it.
	/** @type {((offer: Omit<DeferredEmbeddedSource, "build">) => string | undefined) | undefined} */
	_embeddedAnswers: undefined,
	// Whether the body being rendered is written back escaped, set around a
	// foreign subtree's literal-text element: its answer arrives after the walk,
	// so the escaping rides with the deferred write rather than the printer.
	_escapeEmbedded: false,
	/** @type {Map<HtmlNodeRef, HtmlNodeRef[]> | null} */
	_fosteredRuns: null,
	/** bits 0-1 namespace (`NS_*`), bit 2 self-closing (void), bit 3 template content */
	_nodeFlags: EMPTY_U8,
	// Whether a run of adjacent `<style>` elements prints as one (see
	// `HtmlProcessOptions`), off unless asked for: it removes elements, which
	// `document.styleSheets` and a `:nth-child` selector read.
	_mergeStyles: false,
	// Print a run of adjacent bare `<script>` elements as one.
	_mergeScripts: false,
	// How far text whitespace may be collapsed (see `HtmlProcessOptions`) — set per
	// print, like `_minifying`. "none" is off; the rest name how much of the
	// whitespace against a boundary no line box reaches goes with it.
	/** @type {"none" | "conservative" | "smart" | "all"} */
	_collapseWhitespace: "none",
	// Whether the last parse read XML, which the printer then writes back as XML.
	_xmlMode: false,
	// Whether that XML expanded an entity into markup, so it prints as written.
	_xmlVerbatim: false,
	// Whole-document properties, so a substring cannot hold what the input does not,
	// and decoding introduces neither: a numeric reference to 0 yields U+FFFD, and
	// CR normalization runs on the raw slice first. An unset flag costs the old scan.
	inputHasCr: true,
	// ---- HTML tree-construction parser state ----
	// One active parse at a time, its result valid only until the next `parseHtml`:
	// module scope is what defines the helpers below once rather than per parse.
	source: "",
	/** @type {HtmlElement} 0 = none */
	head: 0,
	_nodeFirstChildren: EMPTY_I32,
	/** `NodeType` per node */
	_nodeTypes: EMPTY_U8,
	_nodeNextSiblings: EMPTY_I32,
	// Which attribute defaults may be dropped (see `HtmlProcessOptions`), off
	// unless asked for: whichever tier drops one, the page stops carrying an
	// attribute that `getAttribute` and every attribute selector read.
	/** @type {"none" | "smart" | "all"} */
	_removeRedundantAttributes: "none",
	/** @type {Map<HtmlNodeRef, HtmlNodeRef> | null} */
	_fosteredNodes: null,
	// Tables whose run has to move but cannot be replayed by re-nesting it. Their own
	// source is what produced the tree, so it is what prints.
	/** @type {Set<HtmlNodeRef> | null} */
	_fosterVerbatim: null,
	/** @type {Set<HtmlNodeRef> | null} */
	_fosterHolders: null
};

/**
 * An element's open tag as it stands in the source, before any minifying; also
 * left in `_elementOpenSource`.
 * @param {HtmlPath} path positioned on the element
 * @returns {string} its source open tag
 */
const _openTagSource = (path) => {
	// A repeated `<html>` / `<body>` start tag adds its unseen attributes to the
	// open element and is itself dropped, so that element's source tag no longer
	// spells its attributes — rebuild it rather than echo a stale one.
	const source =
		_mergedAttrNodes.size !== 0 && _mergedAttrNodes.has(path.node)
			? _synthesizeOpenTag(path)
			: _openTagOf(path.node);
	_S._elementOpenSource = source;
	return source;
};

/**
 * Whether an XML element can print anything between its tags: minifying drops
 * the comments a `<a/>` then stands in for.
 * @param {HtmlPath} path positioned on the element
 * @param {boolean} minify whether minifying
 * @returns {boolean} true when a child may print
 */
const _xmlPrintsChildren = (path, minify) => {
	for (let c = _firstChild(path.node); c !== 0; c = _nextSibling(c)) {
		if (!minify || _S._nodeTypes[c] !== NodeType.Comment) return true;
	}
	return false;
};

// The namespaces whose `<style>` and `style=""` hold CSS; in any other one the
// same names are data a stylesheet has nothing to do with.
const XML_STYLED_NAMESPACES = new Set([
	"http://www.w3.org/2000/svg",
	"http://www.w3.org/1999/xhtml"
]);

/**
 * Whether an XML element is in a namespace that styles with CSS, read off the
 * `xmlns` declarations in scope for its prefix (Namespaces in XML §6.2).
 * @param {HtmlElement} node the element
 * @returns {boolean} true for an SVG or XHTML element
 */
const _xmlElementIsStyled = (node) => {
	const name = _tagNameOf(node);
	const colon = name.indexOf(":");
	const declaration = colon === -1 ? "xmlns" : `xmlns:${name.slice(0, colon)}`;
	for (let n = node; n !== 0; n = _nodeParents[n]) {
		if (_S._nodeTypes[n] !== NodeType.Element) break;
		const start = _nodeAttrStarts[n];
		for (let i = start; i < start + _nodeAttrCounts[n]; i++) {
			if (_attrNames[i] === declaration) {
				return XML_STYLED_NAMESPACES.has(_xmlAttributeValue(i));
			}
		}
	}
	return false;
};

/**
 * XML character data in its shortest well-formed spelling: as it stands where
 * nothing in it is markup, else a CDATA section, else escaped.
 * @param {string} text decoded text
 * @returns {string} the text as XML content
 */
const _xmlCharacterData = (text) => {
	if (!text.includes("<") && !text.includes("&")) {
		return text.replace(/]]>/g, "]]&gt;").replace(/\r/g, "&#13;");
	}
	return text.includes("]]>") || text.includes("\r")
		? escapeXmlText(text)
		: `<![CDATA[${text}]]>`;
};

/**
 * Offer a `style` attribute of an SVG or XHTML element to the caller's renderer,
 * as the declaration list it is.
 * @param {string} value the decoded value
 * @returns {string | null} the value written between its quotes, the marker standing for it, or null when nothing renders it
 */
const _xmlStyleAttribute = (value) => {
	/**
	 * @param {string} css what to write
	 * @returns {string} it quoted
	 */
	const quote = (css) => {
		const delimiter = css.includes('"') && !css.includes("'") ? "'" : '"';
		return `${delimiter}${escapeXmlAttribute(css, delimiter)}${delimiter}`;
	};
	if (_S._deferEmbeddedSource !== undefined) {
		return _deferEmbedded(
			value,
			CSS_TYPE,
			(rendered) => quote(typeof rendered === "string" ? rendered : value),
			BLOCK_CONTENTS
		);
	}
	const rendered = _renderEmbeddedOrNull(value, CSS_TYPE, BLOCK_CONTENTS);
	return rendered === null ? null : quote(rendered);
};

/**
 * An XML element's start tag: as written, or when minifying rebuilt with one
 * space between attributes and each value in its shortest well-formed spelling.
 * @param {HtmlPath} path positioned on the element
 * @param {boolean} minify whether minifying
 * @returns {string} the start tag, ending in `>` even for an empty element
 */
const _xmlOpenTag = (path, minify) => {
	const node = path.node;
	if (!minify) {
		const source = _openTagOf(path.node);
		return source.endsWith("/>") ? `${source.slice(0, -2)}>` : source;
	}
	let out = `<${_tagNameOf(node)}`;
	const start = _nodeAttrStarts[node];
	const count = _nodeAttrCounts[node];
	const order =
		_sortAttributes && count > 1 ? _sortedAttributes(path, count) : null;
	const rendering =
		_S._renderEmbeddedSource !== undefined ||
		_S._deferEmbeddedSource !== undefined;
	for (let index = 0; index < count; index++) {
		const i = start + (order === null ? index : order[index]);
		// A default the DTD supplies is the DTD's to keep supplying.
		if (_attrNameStarts[i] === -1) continue;
		const raw = _attrValueOf(i);
		const decoded = _xmlAttributeValue(i);
		if (
			rendering &&
			_attrNames[i] === "style" &&
			decoded.trim() !== "" &&
			_xmlElementIsStyled(node)
		) {
			const styled = _xmlStyleAttribute(decoded);
			if (styled !== null) {
				out += ` style=${styled}`;
				continue;
			}
		}
		const doubles = decoded.split('"').length;
		const singles = decoded.split("'").length;
		const quote =
			doubles === singles ? _xmlQuote : doubles > singles ? "'" : '"';
		let value = escapeXmlAttribute(decoded, quote);
		const valueStart = _attrValueStarts[i];
		// The spelling the source quoted is kept when shorter, an entity reference
		// the DTD declares being the usual reason.
		if (
			_attrValues[i] === null &&
			valueStart > 0 &&
			raw.length < value.length &&
			!raw.includes("<")
		) {
			const written = _htmlSource.charAt(valueStart - 1);
			if (written === '"' || written === "'") {
				out += ` ${_attrNames[i]}=${written}${raw}${written}`;
				continue;
			}
		}
		value = `${quote}${value}${quote}`;
		out += ` ${_attrNames[i]}=${value}`;
	}
	return `${out}>`;
};

/**
 * An element's end tag as the printer would emit it.
 * @param {HtmlPath} path positioned on the element
 * @param {string} name its tag name
 * @param {string} open its source open tag
 * @param {boolean} innerEmpty whether it printed no children
 * @param {boolean} minify whether minifying
 * @returns {string} the end tag, `""` when it carries nothing
 */
const _elementCloseTag = (path, name, open, innerEmpty, minify) => {
	// Everything after `<plaintext>` is text, so an emitted end tag would re-parse
	// as literal text; a source self-closing tag (`<rect/>` in foreign content)
	// with no children needs none either.
	if (
		name === "plaintext" ||
		(innerEmpty && open.endsWith("/>")) ||
		(minify && _canOmitEndTag(path, name, path.node))
	) {
		return "";
	}
	// `</name>` spelled by the source, falling back to the element's own name when
	// there is no source end tag to echo. Read here rather than up front: an
	// omitted end tag throws it away, and that is what minifying mostly does.
	if (minify && _namespaceOf(path.node) === NS_HTML) {
		// The source name folds to the element's own — the common case, answered
		// off the raw range without building the source spelling to cut apart.
		if (
			rangeAsciiCaseInsensitiveMatch(
				_htmlSource,
				_startOf(path.node) + 1,
				_nameEndOf(path.node),
				name
			)
		) {
			return `</${name}>`;
		}
		return `</${asciiLowercase(_closeTagOf(path.node).slice(2, -1)).trim() || name}>`;
	}
	const sourceClose = _closeTagOf(path.node);
	return sourceClose === "" ? `</${name}>` : sourceClose;
};

/**
 * Whether re-parsing an implied `<html>` / `<body>`'s children without its start
 * tag would not hand them back: a start tag `inBody` ignores still closed
 * `<head>` on the way there, so a head element written after one belongs to the
 * body, and §13.2.6.4.6 gives it back to the head the modes had already left. A
 * comment goes further out still, to the element above.
 * @param {HtmlNodeRef} node the implied element
 * @returns {boolean} true when its start tag has to materialize
 */
const _contentOpensOutside = (node) => {
	const first = _S._nodeFirstChildren[node];
	if (first === 0) return false;
	const type = _S._nodeTypes[first];
	if (type === NodeType.Comment) return true;
	return (
		type === NodeType.Element &&
		_namespaceOf(first) === NS_HTML &&
		HEAD_ELEMENTS.has(_tagNameOf(first))
	);
};

// What `_streamTags` decided for the element just entered — passed out of band
// so each streamed element costs no result object.
let _streamOpen = "";
let _streamClose = "";
// The close that goes with a held-back start tag once it prints: an element
// whose tag the parser no longer re-implies answers to `removeImpliedTags`.
let _streamCloseIfOpened = "";
// Which held-back opener `_emitPiece` settled into a start tag, `-1` for none.
let _materializedPendingSlot = -1;
/** @type {string[]} */
const _printCloseIfOpened = [];
let _streamImplied = "";

/**
 * An element printed in pieces prints as `open + children + close`, and these are
 * those two — the printer's `Element` case, minus everything in it that can only
 * be decided once the children's text is in. They are left in `_streamOpen` /
 * `_streamClose` / `_streamImplied`; `false` for an element not of that
 * shape: a leading newline to round-trip, a self-closing tag, a `<template>`'s
 * content fragment, or a source `/>` whose end tag hangs on whether anything
 * printed inside it. `_streamImplied` names an omitted `<html>` / `<body>` whose
 * open tag still materializes if the first thing inside it turns out to be
 * whitespace.
 * @param {HtmlPath} path positioned on the element
 * @param {boolean} minify whether minifying
 * @returns {boolean} whether the element prints in pieces
 */
const _streamTags = (path, minify) => {
	_streamImplied = "";
	if (xmlMode) {
		// Composed instead when it has no children, so it can print as `<a/>`.
		if (_S._xmlVerbatim || !_xmlPrintsChildren(path, minify)) return false;
		_streamOpen = _xmlOpenTag(path, minify);
		_streamClose = `</${_tagNameOf(path.node)}>`;
		return true;
	}
	// Folded into the `<style>` before it, which already printed its text, or
	// carrying nothing at all.
	if (
		minify &&
		((_S._absorbed !== null && _S._absorbed.has(path.node)) ||
			_isRemovableEmptyElement(path, path.node))
	) {
		_streamOpen = "";
		_streamClose = "";
		return true;
	}
	if (
		(_S._nodeFlags[path.node] & FLAG_SELF_CLOSING) !== 0 ||
		_templateContentOf(path.node) !== 0
	) {
		return false;
	}
	// A fostered run that moves prints inside the table it came out of, which needs
	// both of them held rather than emitted as the walk reaches them. One printing
	// from source holds nothing: its content is a slice, so it still streams.
	if (
		_S._fosterHolders !== null &&
		_S._fosterHolders.has(path.node) &&
		((_S._nodeFlags[path.node] & FLAG_FOSTER_REGION) === 0 ||
			!(/** @type {Set<HtmlNodeRef>} */ (_S._fosterVerbatim).has(path.node)))
	) {
		return false;
	}

	const name = _tagNameOf(path.node);
	if (LEADING_NEWLINE_ELEMENTS.has(name)) return false;
	if (name === "form" && _formNeedsPointerReset(path)) return false;
	// An SVG subtree is offered to the renderer whole, so it has to be composed
	// rather than streamed out in pieces that never form one string.
	if (
		(_S._renderEmbeddedSource !== undefined ||
			_S._deferEmbeddedSource !== undefined) &&
		name === "svg" &&
		_isSvgRoot(path)
	) {
		return false;
	}
	// Read before minifying: bailing afterwards would throw the work away and
	// leave `printer` to redo it, minifying every `style=""` on a `/>`-spelled
	// tag twice — and running a renderer twice over it.
	const source = _openTagSource(path);
	if (source.endsWith("/>")) return false;
	const tag = minify && source !== "" ? _minifyOpenTag(path, source) : source;
	if (minify && _canOmitStartTag(path, name, path.node, source)) {
		_streamOpen = "";
		_streamClose = _canOmitEndTag(path, name, path.node) ? "" : `</${name}>`;
		return true;
	}
	if (source === "") {
		if (
			_attributeCountOf(path.node) === 0 &&
			TRANSPARENT_IMPLIED_ELEMENTS.has(name)
		) {
			// The parser re-implies the start tag, but not the end tag the source
			// carried: without it whatever follows moves inside this element.
			_streamClose = _impliedCloseTag(path, name, path.node);
			_streamCloseIfOpened = _canOmitEndTag(path, name, path.node)
				? ""
				: `</${name}>`;
			// Leading whitespace is decided on the first piece emitted; what the modes
			// hand back out is known here, from the node itself.
			const shell = name === "body" || name === "html";
			const opensOutside = shell && _contentOpensOutside(path.node);
			_streamOpen =
				opensOutside || _impliedTableGroupStartNeeded(path, name, path.node)
					? `<${name}>`
					: "";
			_streamImplied = shell && !opensOutside ? name : "";
		} else {
			_streamOpen = _synthesizeOpenTag(path);
			_streamClose =
				minify && _canOmitEndTag(path, name, path.node) ? "" : `</${name}>`;
		}
		return true;
	}
	_streamOpen = tag;
	_streamClose = _elementCloseTag(path, name, source, false, minify);
	return true;
};

/**
 * Whether a `<form>` needs a `</form>` in front of it: one written inside another
 * reaches the tree only because an end tag cleared the form element pointer on
 * the way — §13.2.6.4.7 ignores the start tag while the pointer is set, so
 * without it the element is dropped on re-parsing. A `<template>` between the two
 * suspends the pointer instead, and needs nothing.
 * @param {HtmlPath} path the accessor positioned on the `<form>`
 * @returns {boolean} true when the end tag has to be written first
 */
const _formNeedsPointerReset = (path) => {
	// §13.2.6.5 inserts a foreign `<form>` without consulting the pointer at all.
	if (_namespaceOf(path.node) !== NS_HTML) return false;
	let found = false;
	for (
		let ancestor = _parentOf(path.node);
		ancestor !== 0;
		ancestor = _parentOf(ancestor)
	) {
		// A template's content is a fragment, not an element: reaching it means the
		// walk left a `<template>`, which suspends the pointer.
		if (path.type(ancestor) === NodeType.DocumentFragment) return false;
		if (path.type(ancestor) !== NodeType.Element) return found;
		if (_namespaceOf(ancestor) !== NS_HTML) continue;
		const name = _tagNameOf(ancestor);
		// A template anywhere above suspends the pointer, whichever side of the
		// outer form it is on, so the start tag is never the one that is ignored.
		if (name === "template") return false;
		if (name === "form") found = true;
	}
	return found;
};

/**
 * Whether printing a fostered run back inside its table hands the parser the same
 * tree: only when "in table" fosters every node of it straight back out. What it
 * keeps instead — its own content, and the start tags §13.2.6.4.9 gives a rule of
 * their own — would stay in the table, and a formatting element §13.2.4.3 rebuilt
 * has no source tag to place it with.
 * @param {HtmlPath} path the accessor
 * @param {HtmlNodeRef} node the run's root
 * @param {0 | 1} needed 1 once a start tag in it is known to end something open
 * @returns {0 | 1 | 2 | 3} 0 = stays put, 1 = replays, 2 = replays and is needed,
 * 3 = has to move but cannot replay, so the table prints from its own source
 */
const _fosterRunReplays = (path, node, needed) => {
	let blocked = false;
	for (let n = node; ;) {
		if (path.type(n) === NodeType.Element) {
			const html = _namespaceOf(n) === NS_HTML;
			const name = html ? _tagNameOf(n) : "";
			// Only the root has to be one "in table" fosters: everything under it was
			// inserted at the same current node either way.
			if (n === node && html && TABLE_CONTEXT.has(name)) blocked = true;
			if (
				html &&
				((FORMATTING.has(name) && !_hasSourceTag(n)) ||
					(n === node &&
						(name === "input"
							? _isHiddenInput(path, n)
							: name === "form"
								? _firstChild(n) !== 0
								: FOSTER_KEPT.has(name))))
			) {
				blocked = true;
			}
			if (!html) {
				const child = _firstChild(n);
				if (child !== 0) {
					n = child;
					continue;
				}
			}
			// A scope rule ends the nearest match on the stack, not the parent, so
			// every element still open above it is a candidate.
			let a = html ? _parentOf(n) : 0;
			while (needed === 0 && a !== 0) {
				if (
					path.type(a) === NodeType.Element &&
					_namespaceOf(a) === NS_HTML &&
					_startTagEnds(_tagNameOf(a), name)
				) {
					needed = 1;
				}
				a = _parentOf(a);
			}
		}
		const child = _firstChild(n);
		if (child !== 0) {
			n = child;
			continue;
		}
		for (;;) {
			if (n === node) {
				return needed === 1 ? (blocked ? 3 : 2) : blocked ? 0 : 1;
			}
			const next = _nextSibling(n);
			if (next !== 0) {
				n = next;
				break;
			}
			n = _parentOf(n);
		}
	}
};

/**
 * Whether an `<input>` is the one §13.2.6.4.9 keeps rather than fosters: every
 * other one takes the "anything else" arc out of the table.
 * @param {HtmlPath} path the accessor
 * @param {HtmlNodeRef} node the `<input>`
 * @returns {boolean} true when the table would keep it
 */
/**
 * Whether a `</form>` written in front of the form about to be inserted would
 * take something with it: §13.2.6.4.7 generates implied end tags before removing
 * the form, and the source's own end tag ran with nothing of the sort open.
 * @returns {boolean} true when one is open between here and the enclosing form
 */
const _formResetWouldPop = () => {
	// §13.2.6.4.7 removes the form from the stack rather than popping to it, so the
	// enclosing one is in the tree and not on the stack — the walk is up the tree.
	let popped = false;
	for (
		let n = open.length === 0 ? 0 : open[open.length - 1];
		n !== 0;
		n = _nodeParents[n]
	) {
		if (_S._nodeTypes[n] !== NodeType.Element || _namespaceOf(n) !== NS_HTML) {
			continue;
		}
		const name = _tagNameOf(n);
		if (name === "form") return popped;
		if (IMPLIED.has(name)) popped = true;
	}
	return false;
};

/**
 * Whether an entry of this name sits below the marker a §13.2.4.3 scan stopped
 * at. The marker is what hides it, and the marker is there because an element
 * the source left open put it there — so the tree is a shape only that element
 * staying open reproduces.
 * @param {number} from index the scan stopped at
 * @param {string} name the tag name it was looking for
 * @returns {boolean} true when the marker is hiding one
 */
const _afeNameBelowMarker = (from, name) => {
	for (let i = from - 1; i >= 0; i--) {
		const e = afe[i];
		if (e === AFE_MARKER) continue;
		if (_namespaceOf(e) === NS_HTML && _tagNameOf(e) === name) return true;
	}
	return false;
};

/**
 * Whether an `<input>` is the one §13.2.6.4.9 keeps rather than fosters: every
 * other one takes the "anything else" arc out of the table.
 * @param {HtmlPath} path the accessor
 * @param {HtmlNodeRef} node the `<input>`
 * @returns {boolean} true when the table would keep it
 */
const _isHiddenInput = (path, node) => {
	for (let i = 0, n = _attributeCountOf(node); i < n; i++) {
		const a = _attributeAt(i, node);
		if (_attrNameOf(a) === "type") {
			return asciiLowercase(_attrValueOf(a)) === "hidden";
		}
	}
	return false;
};

/**
 * Whether a start tag ends an open element of `openName` through a scope check —
 * §4.13's followers, a second tag of the same name, one heading behind another,
 * and the set whose members end each other by generating implied end tags. Inside
 * a table none of them reach it, every scope stopping at the table; printed back
 * with no table between them they do.
 * @param {string} openName the open element's lowercased tag name
 * @param {string} name the start tag's lowercased name
 * @returns {boolean} true when the start tag ends it
 */
const _startTagEnds = (openName, name) => {
	if (name === openName) return true;
	const ended = SCOPE_ENDED_BY.get(openName);
	if (ended !== undefined && ended !== null && ended.has(name)) return true;
	const closers = OPTIONAL_END_TAG_FOLLOWERS.get(openName);
	if (closers !== undefined && closers.has(name)) return true;
	// §4.13's tag-omission list is not §13.2.6.4.7's close-a-p list — `center` is in
	// one and not the other, and the parser's is what decides the tree.
	if (openName === "p" && BLOCK_START.has(name)) return true;
	if (HEADING.has(openName)) return HEADING.has(name);
	return IMPLIED.has(openName) && IMPLIED.has(name);
};

/**
 * Whether `table`'s fostered run prints back inside it, memoized so the holder
 * that skips the run and the table that takes it in answer this the same way.
 * Moving it is a repair, not a preference: tree order is left alone unless a
 * start tag in the run would close the holder on the way back in, and the run
 * replays out of the table it is moved into.
 * @param {HtmlPath} path the accessor
 * @param {HtmlNodeRef} table the table the run came out of
 * @returns {boolean} true when the run moves back in
 */
const _fosterRunMoves = (path, table) => {
	const moves = /** @type {Map<HtmlNodeRef, boolean>} */ (_fosterMoves);
	const cached = moves.get(table);
	if (cached !== undefined) return cached;
	moves.set(table, false);
	const run = /** @type {Map<HtmlNodeRef, HtmlNodeRef[]>} */ (
		_S._fosteredRuns
	).get(table);
	if (run === undefined) return false;
	if (_parentOf(table) === 0) return false;
	let closes = false;
	let text = false;
	let blocked = false;
	for (let i = 0; i < run.length; i++) {
		const verdict = _fosterRunReplays(path, run[i], 0);
		if (verdict === 0 || verdict === 3) {
			blocked = true;
			break;
		}
		if (verdict === 2) closes = true;
		if (path.type(run[i]) === NodeType.Text) text = true;
	}
	// Interleaving reads both in source order, which holds only while they do not
	// overlap: one written inside a sibling's span has no place among them.
	if (!blocked) {
		for (let i = 0; i < run.length && !blocked; i++) {
			const at = _startOf(run[i]);
			for (let c = _firstChild(table); c !== 0; c = _nextSibling(c)) {
				if (at > _startOf(c) && at < _fosterRegionEnd(path, c)) {
					blocked = true;
					break;
				}
			}
		}
	}
	// A table keeps whitespace-only text and fosters the rest: moved back in beside
	// each other the two fuse, and one non-whitespace character fosters all of it.
	// That blocks the re-nesting like any other node that cannot replay.
	if (text && !blocked) {
		for (let c = _firstChild(table); c !== 0; c = _nextSibling(c)) {
			if (path.type(c) === NodeType.Text) {
				blocked = true;
				break;
			}
		}
	}
	// The run moves or stays whole, so one node needing to move and another unable
	// to replay is the verbatim case — worth a second look only once blocked.
	if (blocked) {
		closes = false;
		for (let i = 0; i < run.length && !closes; i++) {
			const verdict = _fosterRunReplays(path, run[i], 0);
			if (verdict === 2 || verdict === 3) closes = true;
		}
	}
	if (!closes) return false;
	// Nothing re-nested reproduces this one, so it prints the source it was built
	// from and the run prints nothing: it is already inside that.
	if (blocked) {
		if (_S._fosterVerbatim === null) _S._fosterVerbatim = new Set();
		// The holder, not the table: an earlier run in it prints end tags that leave
		// the list of active formatting elements elsewhere before this one is read.
		const holder = _parentOf(table);
		_S._fosterVerbatim.add(holder);
		// Anything still open around it: the slice runs to where the source stopped,
		// so an end tag none of them was given cannot be invented after it.
		if (_fosterOpenTail === null) _fosterOpenTail = new Set();
		for (let a = holder; a !== 0; a = _parentOf(a)) {
			_fosterOpenTail.add(a);
			_S._nodeFlags[a] |= FLAG_FOSTER_REGION;
		}
		const reach = _fosterRegionEnd(path, table);
		if (reach > _fosterSpanEnd) _fosterSpanEnd = reach;
		moves.set(table, true);
		return true;
	}
	moves.set(table, closes);
	return closes;
};

/**
 * Decide every fostered run before the walk starts and forget the ones that stay
 * put: a holder whose run moves cannot print in pieces, and that costs its whole
 * subtree the streamed path — a price only the runs that actually move should
 * pay. With none left the walk runs exactly as it does for a document that never
 * fostered anything.
 * @param {HtmlPath} path the accessor
 * @returns {void}
 */
const _settleFosteredRuns = (path) => {
	const log = /** @type {HtmlNodeRef[]} */ (_fosteredLog);
	// Nothing is built for a document whose runs all stay put, which is most of
	// them: one pass says whether any node would be moved at all.
	let moves = false;
	for (let i = 1; i < log.length; i += 2) {
		const verdict = _fosterRunReplays(path, log[i], 0);
		if (verdict === 2 || verdict === 3) {
			moves = true;
			break;
		}
	}
	if (!moves) return;
	/** @type {Map<HtmlNodeRef, HtmlNodeRef[]>} */
	const runs = new Map();
	for (let i = 0; i < log.length; i += 2) {
		const run = runs.get(log[i]);
		if (run === undefined) runs.set(log[i], [log[i + 1]]);
		else run.push(log[i + 1]);
	}
	_S._fosteredRuns = runs;
	_fosterMoves = new Map();
	/** @type {Map<HtmlNodeRef, HtmlNodeRef>} */
	const nodes = new Map();
	/** @type {Set<HtmlNodeRef>} */
	const holders = new Set();
	for (let i = 0; i < log.length; i += 2) {
		if (!_fosterRunMoves(path, log[i])) continue;
		// The adoption agency can move a node after it is fostered, so the holder
		// that skips the run is read from the tree, not from where it went in.
		holders.add(_parentOf(log[i + 1]));
		nodes.set(log[i + 1], log[i]);
	}
	for (const table of runs.keys()) {
		if (!_fosterRunMoves(path, table)) runs.delete(table);
	}
	_S._fosteredNodes = nodes;
	_S._fosterHolders = holders;
};

// Stands in for what the print chooses the bytes of: a raw-text body it
// rewrites whole, or an attribute value it respells.
const _DIGEST_OPAQUE = "\u0003";

/**
 * The tree as a string, for the one check the printer cannot make from the tree
 * alone: whether what it printed parses back to what it printed from. Read in
 * the form the print is licensed to leave — comments gone, a run of text
 * collapsed and joined, the elements and attributes the options drop dropped,
 * every value the printer respells standing in for itself — so an equal digest
 * says the print moved nothing, not that it changed nothing. What a repair does
 * move (a node under another parent, an attribute a bad quote swallowed)
 * reshapes the tree, and that is what parts the two.
 * @param {HtmlNodeRef} root the node whose children are read
 * @returns {string} a rendering equal for equal trees
 */
const _digest = (root) => {
	/** @type {string[]} */
	const out = [];
	// A stack rather than recursion: a document nests as deeply as its source says
	// it does, and the tree this reads is the one the printer just walked.
	/** @type {number[]} */
	const stack = [];
	const first = _firstChild(root);
	if (first !== 0) stack.push(first, 0);
	// A run of text siblings reads as one string: dropping the comment or the
	// empty element between two of them is what joins them in the output.
	let pendingText = "";
	let pendingDepth = 0;
	const flushText = () => {
		const text = stripAsciiWhitespace(pendingText);
		pendingText = "";
		if (text !== "") out.push(`${pendingDepth}\u0001t\u0001${text}`);
	};
	while (stack.length !== 0) {
		const depth = /** @type {number} */ (stack.pop());
		const node = /** @type {HtmlNodeRef} */ (stack.pop());
		// The sibling goes on first, so this node's own subtree drains before it.
		const next = _nextSibling(node);
		if (next !== 0) stack.push(next, depth);
		const type = A.type(node);
		// Not flushed: a comment minifying drops leaves the text around it one run.
		if (type === NodeType.Comment) continue;
		if (type === NodeType.Text) {
			if (depth !== pendingDepth) flushText();
			pendingDepth = depth;
			const parent = _parentOf(node);
			pendingText +=
				parent !== 0 && LITERAL_TEXT_PARENTS.has(_tagNameOf(parent))
					? _DIGEST_OPAQUE
					: collapseWhitespaceRuns(_dataOf(node));
			continue;
		}
		if (type === NodeType.Element) {
			// Same as a comment: an element the print drops joins what surrounds it.
			if (
				_isRemovableEmptyElement(A, node) ||
				(_S._absorbed !== null && _S._absorbed.has(node))
			) {
				continue;
			}
			flushText();
			const elementName = _tagNameOf(node);
			const html = _namespaceOf(node) === NS_HTML;
			out.push(
				`${depth}\u0001e\u0001${_namespaceOf(node)}\u0001${elementName}`
			);
			// Per element, as in `_minifyOpenTag`: every attribute of one tag gets the
			// same answer out of these tables.
			const markers =
				html && _S._removeRedundantAttributes !== "none"
					? REDUNDANT_TYPE_ATTRIBUTES[elementName]
					: undefined;
			const defaults =
				html && _S._removeRedundantAttributes === "all"
					? REDUNDANT_DEFAULT_ATTRIBUTES[elementName]
					: undefined;
			const attributes = [];
			for (const a of _attributeList(node)) {
				if (
					html &&
					(((markers !== undefined || defaults !== undefined) &&
						_isRedundantAttribute(
							markers,
							defaults,
							elementName,
							a.name,
							a.value
						)) ||
						_isRemovableEmptyAttribute(elementName, a.name, a.value))
				) {
					continue;
				}
				const name = a.serializedName || a.name;
				// A handler joins the two the printer rewrites wholesale: a renderer's
				// answer is other text for the same behavior, so comparing it says nothing.
				if (
					REWRITABLE_ATTRIBUTES.has(a.name) ||
					a.name === "srcdoc" ||
					_isEventHandlerAttribute(elementName, a.name)
				) {
					attributes.push(`${name}=${_DIGEST_OPAQUE}`);
					continue;
				}
				// The value as the DOM holds it, since another quote and another escape
				// spell the same attribute, with a boolean named by itself as the bare
				// presence the printer writes back.
				attributes.push(
					`${name}=${
						html &&
						a.value.length === a.name.length &&
						_isBooleanAttribute(elementName, a.name) &&
						asciiLowercase(a.value) === a.name
							? ""
							: decodeEntities(a.value, true)
					}`
				);
			}
			attributes.sort();
			out.push(attributes.join("\u0002"));
			const content = _templateContentOf(node);
			const inner = content !== 0 ? _firstChild(content) : _firstChild(node);
			if (inner !== 0) stack.push(inner, depth + 1);
			continue;
		}
		flushText();
		out.push(`${depth}\u0001${type}\u0001${_dataOf(node)}`);
	}
	flushText();
	return out.join("\n");
};

/**
 * Whether this element still owes an end tag once its content printed from
 * source: one the source never wrote is not the printer's to invent, and one it
 * did write is already inside the slice when the span ran past it.
 * @param {HtmlPath} path the accessor positioned on the element
 * @returns {boolean} true when the end tag still has to be printed
 */
const _endTagOutsideSpan = (path) =>
	_endTagStart(path.node) !== -1 && _endOf(path.node) > _fosterSpanEnd;

/**
 * The source an element's content was built from. Nothing re-nested reproduces a
 * run that lands here, and the source did — anchored on the element itself, so no
 * child of it can start earlier and none is left out.
 * @param {HtmlPath} path the accessor
 * @param {HtmlNodeRef} node the element printing from source
 * @returns {string} its raw content
 */
const _fosterSourceRegion = (path, node) => {
	let end = _contentStartOf(node);
	// A fragment, or an element the parser implied, has no opening tag to start
	// from — its content begins wherever the first thing in it was written.
	let from = -1;
	for (let c = _firstChild(node); c !== 0; c = _nextSibling(c)) {
		const reach = _fosterRegionEnd(path, c);
		if (reach > end) end = reach;
		// Offset 0 is a real position for the first node in the source; a node the
		// parser made has no range at all, which is both offsets at zero.
		const at = _startOf(c);
		if ((at !== 0 || _endOf(c) !== 0) && (from === -1 || at < from)) {
			from = at;
		}
	}
	if (from === -1 || from > end) from = _contentStartOf(node);
	return path.source(from, end);
};

/**
 * The last source offset the table's own tokens reach. Its recorded end stops at
 * the start tag when the source never closed it, and the fostered nodes were
 * written inside it, so the whole subtree and the run answer together.
 * @param {HtmlPath} path the accessor
 * @param {HtmlNodeRef} table the table printing from source
 * @returns {number} end offset
 */
const _fosterRegionEnd = (path, table) => {
	let end = _endOf(table);
	const run = /** @type {Map<HtmlNodeRef, HtmlNodeRef[]>} */ (
		_S._fosteredRuns
	).get(table);
	const roots = run === undefined ? [table] : [table, ...run];
	if (path.type(table) !== NodeType.Element) return end;
	for (const root of roots) {
		for (let n = root; ;) {
			if (_endOf(n) > end) end = _endOf(n);
			const child = _firstChild(n);
			if (child !== 0) {
				n = child;
				continue;
			}
			for (;;) {
				if (n === root) break;
				const next = _nextSibling(n);
				if (next !== 0) {
					n = next;
					break;
				}
				n = _parentOf(n);
			}
			if (n === root) break;
		}
	}
	return end;
};

// === Walk ===
// Walk state, hoisted to module scope so the walk helpers keep one function
// identity across parses and the streaming path can drive them mid-parse.
/** @type {CompiledVisitorMap} the between-parses value, so no map is retained */
const EMPTY_VISITORS = [];
/** @type {CompiledVisitorMap} */
let _visitors = EMPTY_VISITORS;
/** @type {PrintContext | undefined} */
let _writer;

// WHY: an iterative depth-first walk over the link columns — `firstChild` and
// `nextSibling` descend, `parent` ascends — so arbitrarily deep markup cannot
// overflow the call stack, where the old recursive walk died at ~10⁵ nesting. A
// `<template>`'s content fragment is visited before the element's children, and
// ascending out of it continues with those: the fragment is never in a sibling
// chain, so its `_nodeNextSiblings` is 0.

/**
 * Fire a node's `enter` visitors; true when the walk may descend.
 * @param {HtmlNodeRef} node node
 * @returns {boolean} false when a visitor called `skipChildren()`
 */
const _enterNode = (node) => {
	// A holder printing from source composes out of the tree, not out of its
	// children's text, so printing them would only be thrown away.
	if (
		(_S._nodeFlags[node] & FLAG_FOSTER_REGION) !== 0 &&
		/** @type {Set<HtmlNodeRef>} */ (_S._fosterVerbatim).has(node)
	) {
		return false;
	}
	const b = _visitors[_S._nodeTypes[node]];
	if (b === undefined || b.enter.length === 0) return true;
	_walkSkip = false;
	_currentNode = node;
	const e = b.enter;
	for (let i = 0; i < e.length; i++) e[i](A);
	const skip = _walkSkip;
	_walkSkip = false;
	if (_walkStopped && _writer === undefined) throw WALK_STOPPED;
	return !skip;
};

// WHY: printing in pieces.
// The printer composes a node from its children's text, so the store would
// hold every node's until the root is taken — several times the output on a
// document of any size. A node whose text is exactly `open + children + close`
// instead emits its opener as it opens and its closer as it exits, with each
// child going straight out as it finishes, so the store only ever holds the
// subtrees that cannot. Indexed by print depth, which is the open path.
/** @type {number} depth of the node the printing walk is on (maintained by `_walkRun`) */
let _printDepth = 0;
/** @type {number[]} 1 while the node at that depth prints in pieces */
const _printOpen = [];
/** @type {string[]} the closer the node at that depth still owes */
const _printClose = [];
// An omitted `<html>` / `<body>` only materializes when the first thing inside it
// is whitespace, which is known once the first piece is emitted — so its opener
// waits in the writer's pending stack until then.
/** @type {string[]} names of the elements whose openers are held back, innermost last */
const _printPendingNames = [];
/** @type {number[]} pending-stack slot of the node at that depth, -1 for none */
const _printPendingSlot = [];

/**
 * Emit one piece of a node printed in pieces. The omitted openers held back above
 * it decide first: an omitted `<html>` / `<body>` materializes exactly when the
 * first thing inside it is whitespace, which the parser would otherwise drop on
 * the way back in. Only the innermost can — once it does, everything outside it
 * starts with a `<`.
 * @param {string} text output text
 */
const _emitPiece = (text) => {
	if (text === "") return;
	const w = /** @type {PrintContext} */ (_writer);
	const held = _printPendingNames.length;
	if (held !== 0) {
		const c = text.charCodeAt(0);
		if (c === 0x09 || c === 0x0a || c === 0x0c || c === 0x0d || c === 0x20) {
			w.setPending(held - 1, `<${_printPendingNames[held - 1]}>`);
			_materializedPendingSlot = held - 1;
		}
		_printPendingNames.length = 0;
		w.flushPending();
	}
	w.emitStreamed(text);
};

/**
 * Decide whether the node just entered prints in pieces, and emit its opener if
 * so. A node can only do that inside something that already is, so the decision
 * walks down from the root and stops at the first that cannot.
 * @param {HtmlNodeRef} node node
 * @param {number} depth its depth in the walk
 * @param {boolean} descending whether the walk may enter its children
 */
const _openStreamed = (node, depth, descending) => {
	const w = /** @type {PrintContext} */ (_writer);
	let streams = 0;
	_printPendingSlot[depth] = -1;
	if (descending && (depth === 0 || _printOpen[depth - 1] === 1)) {
		const ty = _S._nodeTypes[node];
		if (ty === NodeType.Document || ty === NodeType.DocumentFragment) {
			// Nothing of its own to print: its text is its children's, in order.
			streams = 1;
			_printClose[depth] = "";
		} else if (ty === NodeType.Element) {
			_currentNode = node;
			if (_streamTags(A, w.options.mode === "minify")) {
				streams = 1;
				_printClose[depth] =
					(_S._nodeFlags[node] & FLAG_FOSTER_REGION) !== 0 &&
					!_endTagOutsideSpan(A)
						? ""
						: _streamClose;
				if (_streamImplied === "") {
					_emitPiece(_streamOpen);
				} else {
					_printCloseIfOpened[depth] = _streamCloseIfOpened;
					_printPendingSlot[depth] = w.pushPending("");
					_printPendingNames.push(_streamImplied);
				}
			}
		}
	} else if (
		(_S._nodeFlags[node] & FLAG_FOSTER_REGION) !== 0 &&
		(depth === 0 || _printOpen[depth - 1] === 1) &&
		/** @type {Set<HtmlNodeRef>} */ (_S._fosterVerbatim).has(node)
	) {
		// Its children are skipped and its text is `open + that source + close`, so
		// it streams like any other element rather than composing nothing.
		_currentNode = node;
		if (_streamTags(A, w.options.mode === "minify")) {
			streams = 1;
			_printClose[depth] = _endTagOutsideSpan(A) ? _streamClose : "";
			const region = _fosterSourceRegion(A, node);
			if (_streamImplied === "") {
				_emitPiece(_streamOpen + region);
			} else {
				_printPendingSlot[depth] = w.pushPending("");
				_printPendingNames.push(_streamImplied);
				// The tag is still undecided; the content behind it is not, and nothing
				// else will emit it because the children are skipped.
				_emitPiece(region);
			}
		}
	}
	_printOpen[depth] = streams;
};

/**
 * Fire a node's `exit` visitors, then — when printing — its printer (the
 * post-order point: its children are already printed). `_writer` undefined =
 * walk only.
 * @param {HtmlNodeRef} node node
 */
const _exitNode = (node) => {
	const b = _visitors[_S._nodeTypes[node]];
	if (b === undefined && _writer === undefined) return;
	_currentNode = node;
	if (b !== undefined) {
		const x = b.exit;
		for (let i = 0; i < x.length; i++) x[i](A);
		if (_walkStopped && _writer === undefined) throw WALK_STOPPED;
	}
	if (_writer === undefined) return;
	const depth = _printDepth;
	if (_printOpen[depth] === 1) {
		// Printed in pieces: its opener went out as it opened and its children as
		// they finished, so only its closer is left.
		const slot = _printPendingSlot[depth];
		// Its start tag printed after all, so the end tag is no longer the one a
		// transparent element leaves behind.
		_emitPiece(
			slot !== -1 && slot === _materializedPendingSlot
				? _printCloseIfOpened[depth]
				: _printClose[depth]
		);
		// Nothing inside it printed, so its held-back opener never had to.
		if (slot !== -1 && _writer.isPending(slot)) {
			_printPendingNames.pop();
			_writer.dropPending();
		}
		return;
	}
	// Composed at once, but whatever holds it is being printed in pieces, so it
	// goes straight out rather than into the store for a read-back that will never
	// come.
	if (depth !== 0 && _printOpen[depth - 1] === 1) {
		_emitPiece(_writer.printPiece(A));
		return;
	}
	_writer.printNode(node, A);
};

/**
 * First node to visit inside `node` (template content before children).
 * @param {HtmlNodeRef} node node
 * @returns {HtmlNodeRef} first inner node (0 = leaf)
 */
const _firstInner = (node) => {
	const ty = _S._nodeTypes[node];
	if (ty === NodeType.Element) {
		const tc = _templateContentOf(node);
		return tc !== 0 ? tc : _S._nodeFirstChildren[node];
	}
	if (ty === NodeType.Document || ty === NodeType.DocumentFragment) {
		return _S._nodeFirstChildren[node];
	}
	return 0;
};

// WHY: the streaming walk.
// It follows the parser's open element stack: everything under the
// deepest *entered* element is finished markup, so it is visited and its node
// ids recycled, and peak storage is the open subtree rather than the whole
// document — the HTML analog of the CSS grammar's per-top-level-rule recycling.
//
// Streaming starts only once the document passes `_STREAM_MIN_NODES`: below it
// the walk is a single pass at EOF, which is what a small page wants anyway.
//
// Two rules keep it correct and cheap:
//  - An element is entered only when something beneath it has to be flushed
//    (`_FLUSH_BATCH` worth of nodes). Anything that opens and closes in between
//    is never entered and is walked as one completed subtree, so its visitors
//    see a final `end` and live child links.
//  - Nothing is visited unless `_canFlush()` holds. An unsafe stretch (the
//    adoption agency, tables, templates) just buffers, and the next sync sees
//    the final tree.
//
// Only walk-only passes stream: printing holds every node's text until its
// parent prints.
/** @type {boolean} whether a parse is in progress, so re-entry can be refused */
let _parsing = false;
/** @type {boolean} whether this parse may stream its walk (owned by `grammar`) */
let _streamEligible = false;
/** set when the root's `enter` called `skipChildren()`, so only its `exit` fires */
let _docSkipped = false;
/** @type {boolean} whether the walk has started streaming (see `_STREAM_MIN_NODES`) */
let _streaming = false;
/** @type {HtmlElement[]} open elements whose `enter` has already fired */
const _entered = [];
/** `_entered` index whose `enter` called `skipChildren()` (-1 = none) */
let _skipFrom = -1;
/** set when `open` was spliced mid-stack, so the next sync scans it in full */
let _openSpliced = false;
// WHY: set when a mid-stack `open.splice` removes an element the walk already
// entered, such as a `</form>` with content still open inside it. The element
// leaves the stack without its subtree ending, so the stack no longer describes
// what has finished and reconciling against it would close and re-enter live
// elements. Rare enough to stop streaming for the rest of the parse: what is
// visited stays visited, and the remainder is walked at EOF.
let _streamHalted = false;
/** set by `popOpen`: the open stack changed, so the walk has work at token end */
let _openStackChanged = false;
// Nodes that must pile up under the deepest entered element before a flush
// earns its walk setup. Closing an entered element always flushes, so this only
// bounds how much an un-entered subtree holds — a few KB of columns.
const _FLUSH_BATCH = 4096;
// WHY: below this many nodes a document is walked once at EOF instead.
// Recycling only pays once the columns are big enough that not growing them
// beats the per-token sync, and a page never reaching that must not carry the
// cost. Chosen so the streamed cap below stays under
// `_COLUMN_SHRINK_CAPACITY`, keeping the columns reusable by the next parse.
const _STREAM_MIN_NODES = 49152;

/**
 * Whether completed subtrees may be visited and recycled at this point in the
 * parse. An empty active-formatting-elements list is the load-bearing check:
 * the adoption agency only moves elements drawn from `open` / `afe` and the
 * direct children of an element that is still open, and table markers live in
 * `afe` too, so with `afe` empty neither it nor foster parenting can reach a
 * node that was already flushed. The rest keep node ids that outlive the open
 * stack out of the recycled range — template contents, pending table text, the
 * `<selectedcontent>` post-pass, the form pointer — and `MODE_IN_BODY` covers
 * the head, which "after head" reopens to insert into.
 * @returns {boolean} true when flushing is safe
 */
const _canFlush = () =>
	!_streamHalted &&
	afe.length === 0 &&
	templateModes.length === 0 &&
	pendingTableChars === null &&
	!sawSelectedContent &&
	mode === MODE_IN_BODY &&
	open.length !== 0 &&
	(form === 0 || open.includes(form));

/**
 * Depth-first walk of a whole run of `container`'s children, from `first` up to
 * (not including) `stop` (0 = to the end). One loop for the entire run instead
 * of a call per child — sibling-heavy markup flushes hundreds at a time.
 * @param {HtmlNodeRef} container parent the run hangs off
 * @param {HtmlNodeRef} first first child of the run
 * @param {HtmlNodeRef} stop child to stop before (0 = end of the run)
 */
const _walkRun = (container, first, stop) => {
	let node = first;
	descend: for (;;) {
		const descending = _enterNode(node);
		// Printing tracks the open path so a node can emit its own pieces; the
		// streamed walk-only runs never print, so they never pay for it.
		if (_writer !== undefined) _openStreamed(node, _printDepth, descending);
		if (descending) {
			const inner = _firstInner(node);
			if (inner !== 0) {
				node = inner;
				_printDepth++;
				continue;
			}
		}
		for (;;) {
			_exitNode(node);
			const parent = _nodeParents[node];
			// Back at the run's own level: step to the next sibling, or stop.
			if (parent === container && _templateContentOf(container) !== node) {
				const next = _S._nodeNextSiblings[node];
				if (next === 0 || next === stop) return;
				node = next;
				continue descend;
			}
			// Out of a template's content fragment: the element's children follow —
			// they stand in for it, so they share its depth.
			if (_templateContentOf(parent) === node) {
				const inner = _S._nodeFirstChildren[parent];
				if (inner !== 0) {
					node = inner;
					continue descend;
				}
			} else {
				const next = _S._nodeNextSiblings[node];
				if (next !== 0) {
					node = next;
					continue descend;
				}
			}
			node = parent;
			_printDepth--;
		}
	}
};

/**
 * Rewind the node allocator to `to`, never past the deepest open element. Every
 * still-open element is live and its id must survive, so a flush that empties a
 * parent may not reclaim ids a deeper open element already took.
 * @param {number} to id to rewind to
 */
const _streamRewind = (to) => {
	const top = open.length !== 0 ? open[open.length - 1] : 0;
	if (to >= top && to < _nodeCount) {
		if (_nodeCount > _nodeHighWater) _nodeHighWater = _nodeCount;
		_nodeCount = to;
	}
};

/**
 * Visit and recycle `parent`'s completed children — everything before
 * `openChild`, the child still on the open stack (0 = none, i.e. `parent` is
 * the deepest open element). A trailing text node is held back:
 * `appendTo` merges following text into the last child, so flushing it early
 * would split one text node in two.
 * @param {HtmlNodeRef} parent open element to flush under
 * @param {HtmlElement} openChild still-open child to stop before (0 = none)
 * @param {boolean} visit false to recycle without visiting (under `skipChildren`)
 * @param {boolean} hold true while `parent` can still receive text, so a
 * trailing text node stays put; false once it is closing (nothing follows)
 */
const _streamFlush = (parent, openChild, visit, hold) => {
	_cursorEpoch = (_cursorEpoch + 1) | 0;
	const container = effParent(parent);
	const first = _S._nodeFirstChildren[container];
	if (first === 0 || first === openChild) return;
	// Where the run ends: before the still-open child, and before a trailing text
	// node while `parent` can still receive text (`appendTo` merges into the last
	// child, so flushing it early would split one text node in two).
	let child = openChild;
	if (hold && openChild === 0) {
		const lastChild = _nodeLastChildren[container];
		if (lastChild !== 0 && _S._nodeTypes[lastChild] === NodeType.Text) {
			child = lastChild;
		}
	}
	if (first === child) return;
	if (visit) _walkRun(container, first, child);
	_S._nodeFirstChildren[container] = child;
	if (child === 0) _nodeLastChildren[container] = 0;
	// Ids are bump-allocated in tree order, so the visited run and its
	// descendants fill `[parent + 1, _nodeCount]` whenever nothing below
	// `parent` is still open — rewind the allocator over them.
	if (openChild !== 0 || container !== parent) return;
	if (child === 0) {
		_streamRewind(parent);
	}
};

/**
 * Detach a closed element from its parent, releasing its id when it was the
 * parent's last remaining child. Earlier siblings are always flushed before an
 * element is entered, so it is the first child left.
 * @param {HtmlElement} el closed element
 */
const _streamDetach = (el) => {
	const parent = _nodeParents[el];
	const next = _S._nodeNextSiblings[el];
	if (parent !== 0) {
		_S._nodeFirstChildren[parent] = next;
		if (next === 0) _nodeLastChildren[parent] = 0;
	}
	_S._nodeNextSiblings[el] = 0;
	_nodeParents[el] = 0;
	if (next === 0 && _nodeCount === el) _streamRewind(el - 1);
};

/**
 * Bring the streamed walk up to date with the tree built so far: `exit` the
 * elements that closed, `enter` the ones that opened, and flush the completed
 * subtrees in between. Called once per token but does nothing unless
 * `_canFlush()` holds, so an unsafe stretch (incorrectly nested formatting elements,
 * tables, templates) simply buffers and the next catch-up sees the final tree.
 */
/**
 * Index up to which `_entered` still matches the open stack. Pushes and pops
 * only ever diverge in a suffix; a mid-stack `open.splice` sets `_openSpliced`
 * and forces the full scan.
 * @returns {number} length of the common prefix
 */
const _enteredPrefix = () => {
	if (_openSpliced) {
		_openSpliced = false;
		let i = 0;
		while (i < _entered.length && i < open.length && _entered[i] === open[i]) {
			i++;
		}
		return i;
	}
	let i = _entered.length < open.length ? _entered.length : open.length;
	while (i > 0 && _entered[i - 1] !== open[i - 1]) i--;
	return i;
};

const _streamSync = () => {
	// The tree under the walk grows and is recycled from here on.
	_cursorEpoch = (_cursorEpoch + 1) | 0;
	const entered = _entered.length;
	const last = entered !== 0 ? _entered[entered - 1] : 0;
	// Still nothing to do: the entered prefix matches the open stack and too
	// little has piled up under it to be worth a flush.
	if (
		!_openSpliced &&
		open.length >= entered &&
		(entered === 0 || open[entered - 1] === last) &&
		_nodeCount - last < _FLUSH_BATCH
	) {
		return;
	}
	if (!_canFlush()) return;

	let i = _enteredPrefix();
	// Close: everything entered below the divergence has finished. A descendant
	// of a `skipChildren()` element was tracked without being entered, so it does
	// not exit either — only the skipped element itself does.
	for (let k = _entered.length - 1; k >= i; k--) {
		const el = _entered[k];
		const belowSkip = _skipFrom !== -1 && k > _skipFrom;
		_streamFlush(el, 0, _skipFrom === -1 || k < _skipFrom, false);
		if (!belowSkip) _exitNode(el);
		if (_skipFrom === k) _skipFrom = -1;
		_streamDetach(el);
	}
	_entered.length = i;
	// Open: enter the elements now needed, flushing each level's finished
	// children before descending past them.
	for (; i < open.length; i++) {
		const el = open[i];
		const parent = _nodeParents[el];
		const suppressed = _skipFrom !== -1;
		if (parent !== 0) _streamFlush(parent, el, !suppressed, false);
		if (!suppressed && !_enterNode(el)) _skipFrom = i;
		_entered.push(el);
	}
	// Finally the current insertion point's own finished children.
	_streamFlush(open[open.length - 1], 0, _skipFrom === -1, true);
};

/**
 * Finish a streamed walk at EOF: close everything still open (their never-
 * entered descendants are walked as completed children), then the root.
 * @param {HtmlNodeRef} root document / fragment root
 */
const _streamFinish = (root) => {
	for (let k = _entered.length - 1; k >= 0; k--) {
		const el = _entered[k];
		const belowSkip = _skipFrom !== -1 && k > _skipFrom;
		_streamFlush(el, 0, _skipFrom === -1 || k < _skipFrom, false);
		if (!belowSkip) _exitNode(el);
		if (_skipFrom === k) _skipFrom = -1;
		_streamDetach(el);
	}
	_entered.length = 0;
	_streamFlush(root, 0, true, false);
	_exitNode(root);
};

/**
 * Where a document starts once its byte order mark is read as the decoder reads
 * it (HTML §13.2.3.1): as the encoding's mark, not as text.
 * @param {string} input source text
 * @returns {number} 1 past a leading BOM, else 0
 */
const _afterBom = (input) => (input.charCodeAt(0) === 0xfeff ? 1 : 0);

/**
 * 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). When `writer`
 * is given the same walk also prints: each node's printer builds its text into
 * `writer` as the node finishes (post-order), and the root's text is taken as the
 * result — one parse, no re-tokenization.
 * @param {string} input source text
 * @param {CompiledVisitorMap} visitors compiled visitor map
 * @param {PrintContext | undefined} writer the print context to build output into, or undefined (walk only)
 * @param {HtmlProcessOptions} options process options
 */
const grammar = (input, visitors, writer, options) => {
	// WHY: the walk's state — visitors, print context, open path, AST columns —
	// is module-scoped so the tokenizer's call sites stay monomorphic, which means
	// one parse at a time: a visitor starting another would take this one's state
	// over and release its columns on the way out. Nesting the other language is
	// fine, a `<style>`'s CSS having module state of its own, and an
	// `<iframe srcdoc>` is a separate module webpack parses on its own.
	if (_parsing) {
		throw new Error(
			"html syntax: process() is already running; a visitor cannot start another HTML parse"
		);
	}
	_parsing = true;
	_visitors = visitors;
	_writer = writer;
	_walkStopped = false;
	// Set here, not in `printer`: a tag streams out before the first `printer`
	// call, so these must be known from the start of the walk.
	const redundant =
		writer === undefined ? undefined : writer.options.removeRedundantAttributes;
	_S._removeRedundantAttributes =
		redundant === true
			? "smart"
			: redundant === false || redundant === undefined
				? "none"
				: redundant;
	_removeEmptyAttributes =
		writer !== undefined && writer.options.removeEmptyAttributes === true;
	_removeEmptyElements =
		writer !== undefined && writer.options.removeEmptyElements === true;
	_transforms = _transformsFrom(
		writer === undefined ? undefined : writer.options.transforms
	);
	_commentsKept = _keptComments(_transforms.comments);
	_commentInput = input;
	_commentLocs = undefined;
	// With the rest, not in `printer`: a tag streams out before the first
	// `printer` call, and both the optional-end-tag tests and
	// `removeEmptyElements` ask what this tier does with a whitespace node.
	const collapse =
		writer === undefined ? undefined : writer.options.collapseWhitespace;
	_S._collapseWhitespace =
		collapse === true
			? "conservative"
			: collapse === false || collapse === undefined
				? "none"
				: collapse;
	_minifying = writer !== undefined && writer.options.mode === "minify";
	_S._renderEmbeddedSource =
		writer === undefined ? undefined : writer.options.renderEmbeddedSource;
	_S._deferEmbeddedSource =
		writer === undefined ? undefined : writer.options.deferEmbeddedSource;
	_S._embeddedAnswers =
		writer === undefined ? undefined : writer.options.embeddedAnswers;
	_deferSrcdoc = writer === undefined || writer.options.deferSrcdoc !== false;
	_sortAttributes =
		writer !== undefined && writer.options.sortAttributes === true;
	_sortTokenLists =
		writer !== undefined && writer.options.sortTokenLists === true;
	_xmlQuote =
		writer !== undefined && writer.options.xmlQuote === "'" ? "'" : '"';
	_S._mergeStyles = writer !== undefined && writer.options.mergeStyles === true;
	_S._mergeScripts =
		writer !== undefined && writer.options.mergeScripts === true;
	// Node ids are per-parse, so a set kept from the last one would name this
	// one's nodes. Dropped rather than cleared: most documents never fill it.
	_S._absorbed = null;
	// Counted from the parse this call is about to run, so it cannot be kept.
	_attributeFrequencies = null;
	_attributeRanks = null;
	const implied =
		writer === undefined ? false : writer.options.removeImpliedTags;
	_removeImpliedTags =
		implied === undefined || implied === "smart"
			? "smart"
			: implied === false
				? "none"
				: "all";
	// Printing holds every node's text until its parent prints, so only
	// walk-only passes stream; the printing path builds the tree and walks it.
	_streamEligible = writer === undefined;
	_streaming = false;
	_printDepth = 0;
	// A print is read as the browsers it is for read it, scripting on: what a
	// `noscript` holds is then text, kept as written for either reader.
	const parseOptions =
		writer === undefined ? options : { ...options, scripting: true };
	try {
		const start = _afterBom(input);
		// Written back first, since the mark is what names the encoding as UTF-8.
		if (writer !== undefined && start === 1) writer.emitStreamed("\uFEFF");
		const root = parseHtml(input, start, parseOptions);
		if (writer !== undefined) {
			if (_fosteredLog !== null) _settleFosteredRuns(A);
			// A shape no arrangement of tags reaches, or foster parenting — whose repair
			// is checked, not trusted. No real document reaches either.
			const verify = _printFromSource || _fosteredLog !== null;
			_walkRun(_nodeParents[root], root, _S._nodeNextSiblings[root]);
			// A single root: take its text as the output (the printer read every node's
			// source into the store during the walk, so this needs no source). A root
			// that printed in pieces has already emitted all of it.
			if (_printOpen[0] === 1) writer.dropStore();
			else writer.take(root);
			// An entity expanded into markup that no reference could be rebuilt from.
			if (_S._xmlVerbatim) writer.replaceAll(input);
			// Print, then read the output back: the source always parses to its own
			// tree, so it stands in when the printed form does not. Digested after the
			// walk, which is what settles the elements the print left out.
			if (verify) {
				const digest = _digest(root);
				// A deferred body prints as a marker, which reads back as text where
				// its node stood — put it back, or the whole document is handed back.
				const printed =
					_S._deferEmbeddedSource === undefined ||
					_S._deferEmbeddedSource.length === 0
						? writer.result()
						: declineDeferredWrites(writer.result(), _S._deferEmbeddedSource);
				// Both are keyed by node id, which the next parse hands out afresh.
				_emptyElements.clear();
				_S._absorbed = null;
				if (
					_digest(parseHtml(printed, _afterBom(printed), parseOptions)) !==
					digest
				) {
					writer.replaceAll(input);
				}
			}
		}
	} catch (err) {
		if (err !== WALK_STOPPED) throw err;
	} finally {
		_walkStopped = false;
		if (_haltedVisitors !== null) {
			resumeVisitors(_haltedVisitors);
			_haltedVisitors = null;
		}
		// Each held write keeps its offered source — a `<style>`, a `<script>`, a
		// whole `srcdoc` document — and a closure over it, so both go with the parse.
		_S._renderEmbeddedSource = undefined;
		_S._deferEmbeddedSource = undefined;
		_S._embeddedAnswers = undefined;
		_parsing = false;
		_removeImpliedTags = "smart";
		_transforms = _DEFAULT_TRANSFORMS;
		_commentInput = "";
		_commentLocs = undefined;
		_minifying = false;
		// Keyed by this parse's interned names, so it must not outlive it.
		_attributeFrequencies = null;
		_attributeRanks = null;
		_renderedSheets = null;
		_emptyElements.clear();
		_streamEligible = false;
		_streaming = false;
		_docSkipped = false;
		// Held across the walk, so they outlive it the way the visitors do.
		_printOpen.length = 0;
		_printClose.length = 0;
		_printPendingNames.length = 0;
		_printPendingSlot.length = 0;
		_printCloseIfOpened.length = 0;
		_materializedPendingSlot = -1;
		// Hoisting these to module scope outlives the call, so drop the caller's
		// visitors and print context here — they used to fall away with `grammar`.
		_visitors = EMPTY_VISITORS;
		_writer = undefined;
		// The walk consumed the tree: release the side arrays' heap references so
		// the reused columns don't pin this parse's strings until the next parse.
		_releaseAstColumns();
	}
};

/** @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);
// Not `\s`, which matches a non-breaking space. Sticky so `collectCharacters`
// matches at an offset rather than slicing per call, which was quadratic.
// eslint-disable-next-line no-control-regex
const LEADING_SPACES_REGEXP = /[ \t\n\r\u000C]+/y;
// eslint-disable-next-line no-control-regex
const LEADING_COMMAS_OR_SPACES_REGEXP = /[, \t\n\r\u000C]+/y;
// eslint-disable-next-line no-control-regex
const LEADING_NOT_SPACES = /[^ \t\n\r\u000C]+/y;
const TRAILING_COMMAS_REGEXP = /[,]+$/;
const NON_NEGATIVE_INTEGER_REGEXP = /^\d+$/;
// A signed or unsigned integer or decimal, with or without an exponent, holding
// at least one digit, no leading plus, and a digit after any decimal point.
// 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 {number} */
	let descriptorStart;
	/** @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 sticky reg exp to collect characters
	 * @returns {string | undefined} characters
	 */
	function collectCharacters(regExp) {
		regExp.lastIndex = Math.max(0, position);
		const match = regExp.exec(input);

		if (match) {
			const [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 !== undefined || density !== undefined) {
					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 !== undefined ||
					density !== undefined ||
					height !== undefined
				) {
					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 !== undefined || density !== undefined) {
					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 tokenizeDescriptor() {
		// 8.1. Descriptor tokenizer: Skip whitespace
		collectCharacters(LEADING_SPACES_REGEXP);

		// 8.2. Let current descriptor be the empty string.
		// (Tracked as a start offset, `-1` = empty; sliced once per descriptor.)
		descriptorStart = -1;

		// 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: append a non-empty current descriptor to descriptors and empty
				// it, then set state to after descriptor.
				if (isAsciiWhitespace(charCode)) {
					if (descriptorStart !== -1) {
						descriptors.push(input.slice(descriptorStart, position));
						descriptorStart = -1;
						state = "after descriptor";
					}
				}
				// `,`: 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 (descriptorStart !== -1) {
						descriptors.push(input.slice(descriptorStart, position - 1));
					}

					parseDescriptors();

					return;
				}
				// U+0028 LEFT PARENTHESIS (()
				// Append charCode to current descriptor. Set state to in parens.
				else if (charCode === LEFT_PARENTHESIS) {
					if (descriptorStart === -1) descriptorStart = 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 (descriptorStart !== -1) {
						descriptors.push(input.slice(descriptorStart, position));
					}

					parseDescriptors();

					return;

					// Anything else
					// Append charCode to current descriptor.
				} else if (descriptorStart === -1) {
					descriptorStart = 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) {
					state = "in descriptor";
				}
				// EOF
				// Append current descriptor to descriptors. Jump to the step labeled
				// descriptor parser.
				else if (Number.isNaN(charCode)) {
					descriptors.push(input.slice(descriptorStart, position));
					parseDescriptors();
					return;
				}
				// Anything else
				// Append charCode to current descriptor. (Covered by the tracked range.)
			}
			// After descriptor
			else if (state === "after descriptor") {
				// Do the following, depending on the value of charCode:
				if (isAsciiWhitespace(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 {
			tokenizeDescriptor();
		}

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

// The spec's "URL-potentially-surrounded-by-spaces" cleanup: C0 controls,
// DEL..C1 and U+00A0 are stripped from the value after the whitespace trim.
// eslint-disable-next-line no-control-regex
const IGNORE_CHARS_REGEXP = /[\u0000-\u001F\u007F-\u009F\u00A0]/g;

/**
 * Parse a `src`-like URL attribute value: trim ASCII whitespace / U+00A0 from
 * both ends (offsets preserved for source rewriting), then strip ignorable
 * control characters. Throws when nothing remains.
 * @param {string} input attribute value
 * @returns {ParsedSource[]} parsed src
 */
const parseSrc = (input) => {
	const len = input.length;
	if (len === 0) throw new Error("Must be non-empty");

	let start = 0;
	let end = len;

	while (start < end) {
		const code = input.charCodeAt(start);
		if (code > 32 && code !== 160) break;
		start++;
	}

	if (start === end) throw new Error("Must be non-empty");

	while (end > start) {
		const code = input.charCodeAt(end - 1);
		if (code > 32 && code !== 160) break;
		end--;
	}

	let value = input.slice(start, end);

	if (IGNORE_CHARS_REGEXP.test(value)) {
		value = value.replace(IGNORE_CHARS_REGEXP, "");
		if (value.length === 0) throw new Error("Must be non-empty");
	}

	return [[value, start, end]];
};

/**
 * Extracts the `icon-uri` value of an `msapplication-task` meta content
 * (`name=…;action-uri=…;icon-uri=…`) — the other parts are page URLs, not assets.
 * @param {string} input input
 * @returns {ParsedSource[]} parsed icon-uri
 */
const parseMsapplicationTask = (input) => {
	const len = input.length;
	let pos = 0;
	while (pos < len) {
		let sep = input.indexOf(";", pos);
		if (sep === -1) sep = len;
		const eq = input.indexOf("=", pos);
		if (eq !== -1 && eq < sep) {
			const key = input.slice(pos, eq).trim().toLowerCase();
			if (key === "icon-uri") {
				let start = eq + 1;
				let end = sep;
				while (start < end && isAsciiWhitespace(input.charCodeAt(start))) {
					start++;
				}
				while (end > start && isAsciiWhitespace(input.charCodeAt(end - 1))) {
					end--;
				}
				if (start === end) return [];
				return [[input.slice(start, end), start, end]];
			}
		}
		pos = sep + 1;
	}
	return [];
};

// The CSS parser is loaded on first `parseCssUrls` call, not at module load —
// `HtmlGenerator` defers it the same way, and most documents carry no
// URL-bearing SVG presentation attribute.
/** @type {typeof import("../css/syntax-parser") | undefined} */
let _cssParser;

/**
 * Extracts `url(...)` references from a CSS value (an SVG presentation
 * attribute). Reuses webpack's CSS lexer so quoting/escaping match the CSS
 * spec; returns the `parseSrc` shape so the shared emit path maps and rewrites
 * the spans. Unquoted `url(path)` rewrites the content span; quoted
 * `url("path")` rewrites the inner string span (quotes preserved).
 * @param {string} input attribute value (a CSS component-value list)
 * @returns {ParsedSource[]} url references
 */
const parseCssUrls = (input) => {
	const { TokenType, TokenStream, asciiCaseInsensitiveMatch } =
		_cssParser || (_cssParser = require("../css/syntax-parser"));
	const ts = new TokenStream(input);
	/** @type {ParsedSource[]} */
	const result = [];
	for (;;) {
		const t = ts.consume();
		if (t.type === TokenType.EOF) break;
		if (t.type === TokenType.Url) {
			if (t.contentEnd > t.contentStart) {
				result.push([
					input.slice(t.contentStart, t.contentEnd),
					t.contentStart,
					t.contentEnd
				]);
			}
		} else if (
			t.type === TokenType.Function &&
			asciiCaseInsensitiveMatch(input.slice(t.start, t.end - 1), "url")
		) {
			let s = ts.consume();
			while (s.type === TokenType.Whitespace) s = ts.consume();
			if (s.type === TokenType.String) {
				const quote = input.charCodeAt(s.start);
				const innerStart = s.start + 1;
				// Drop the closing quote, unless the string is unterminated at EOF.
				const innerEnd =
					input.charCodeAt(s.end - 1) === quote ? s.end - 1 : s.end;
				if (innerEnd > innerStart) {
					result.push([
						input.slice(innerStart, innerEnd),
						innerStart,
						innerEnd
					]);
				}
			}
		}
	}
	return result;
};

// Children-only skip: set by `A.skipChildren()` during an
// `enter` dispatch, consumed by the walk.
let _walkSkip = false;
// Set by `A.stop()` for the rest of the parse: no visitor fires after it, and a
// walk-only parse ends there, the next parse resetting what it left.
let _walkStopped = false;
// The visitor lists `A.stop()` emptied in place, put back when the parse ends.
/** @type {VisitorFn[][] | null} */
let _haltedVisitors = null;
// Thrown out of the walk to end a stopped walk-only parse; `grammar` catches it.
const WALK_STOPPED = new Error("html syntax: walk stopped");
// The walk's current position, the only thing it writes per visit: `A.parent`
// and `A.index` are read off the link columns when asked.
/** @type {HtmlNodeRef} */
let _currentNode = 0;
// WHY: where the last `A.child` / `A.childCount` read under a parent stopped,
// so a loop over its children takes one step per child even while a nested loop
// reads another parent's: a slot per parent id modulo 64, valid while it carries
// the current epoch, which moves with each parse and streamed sync, the only
// points the tree under a walk grows or recycles ids.
const _CURSOR_SLOTS = 64;
const _cursorParents = new Int32Array(_CURSOR_SLOTS);
const _cursorEpochs = new Int32Array(_CURSOR_SLOTS);
const _cursorIndexes = new Int32Array(_CURSOR_SLOTS);
const _cursorChildren = new Int32Array(_CURSOR_SLOTS);
const _cursorCounts = new Int32Array(_CURSOR_SLOTS);
let _cursorEpoch = 1;

/* eslint-disable jsdoc/require-template -- `A` below is the accessor const, not a type parameter */
/**
 * The HTML path: the AST accessor with the walk's
 * current position on it — the single argument every visitor receives.
 * @typedef {typeof A} HtmlPath
 */
/* eslint-enable jsdoc/require-template */

/**
 * HTML's parts by node type, which type each visitor's path.
 * @typedef {{
 * types: HtmlNodeType,
 * field: typeof Field.attributes | typeof Field.content,
 * flag: typeof Flag.mathml | typeof Flag.svg | typeof Flag.selfClosing,
 * part: 1 | 2 | 3 | 4 | 5 | 6 | 7,
 * text: typeof Part.publicId | typeof Part.systemId,
 * fields: { 1: never, 2: never, 3: typeof Field.attributes | typeof Field.content, 4: never, 5: never, 6: never, 7: never, 8: never },
 * flags: { 1: never, 2: never, 3: typeof Flag.mathml | typeof Flag.svg | typeof Flag.selfClosing, 4: never, 5: never, 6: never, 7: never, 8: never },
 * parts: { 1: never, 2: never, 3: typeof Part.name | typeof Part.content | typeof Part.startTag | typeof Part.endTag, 4: never, 5: never, 6: typeof Part.publicId | typeof Part.systemId, 7: never, 8: typeof Part.name | typeof Part.value },
 * texts: { 1: never, 2: never, 3: never, 4: never, 5: never, 6: typeof Part.publicId | typeof Part.systemId, 7: never, 8: never },
 * items: { [field: number]: number, 1: 8, 2: 2 },
 * }} HtmlParts
 */
/**
 * The path a visitor of node type `T` receives, with the run's state `S`.
 * @template [T=number]
 * @template [S=unknown]
 * @typedef {import("../util/SourceProcessor").TypedNodePath<HtmlPath, HtmlNodeRef, HtmlParts, T, S>} HtmlNodePath
 */
/**
 * A visitor map whose each visitor's path is typed by its node type and run state.
 * @template [S=unknown]
 * @typedef {{
 * 1?: import("../util/SourceProcessor").VisitorBucket<HtmlNodePath<1, S>>,
 * 2?: import("../util/SourceProcessor").VisitorBucket<HtmlNodePath<2, S>>,
 * 3?: import("../util/SourceProcessor").VisitorBucket<HtmlNodePath<3, S>>,
 * 4?: import("../util/SourceProcessor").VisitorBucket<HtmlNodePath<4, S>>,
 * 5?: import("../util/SourceProcessor").VisitorBucket<HtmlNodePath<5, S>>,
 * 6?: import("../util/SourceProcessor").VisitorBucket<HtmlNodePath<6, S>>,
 * 7?: import("../util/SourceProcessor").VisitorBucket<HtmlNodePath<7, S>>,
 * 8?: import("../util/SourceProcessor").VisitorBucket<HtmlNodePath<8, S>>,
 * }} HtmlVisitorMap
 */
/** @typedef {(typeof NodeType)[keyof typeof NodeType]} HtmlNodeType a `NodeType` member */

// AST field-access seam, mirroring the CSS parser's `A`: every field a consumer
// reads goes through an accessor here, so the node representation can change
// underneath. `n` is an `HtmlNodeRef`, valid until the next `parseHtml`.
const A = {
	// === path position (rebound by the walk before every visitor call) ===
	/** @type {unknown} the run's state, set by `SourceProcessor` for the length of a run */
	state: undefined,
	/**
	 * @returns {HtmlNodeRef} current node — only valid during a visitor callback
	 */
	get node() {
		return _currentNode;
	},
	/**
	 * @returns {HtmlNodeRef | null} enclosing node (null = the document root)
	 */
	get parent() {
		const p = _nodeParents[_currentNode];
		return p === 0 ? null : p;
	},
	/**
	 * Counted along the sibling links when asked, so the walk stores nothing for it.
	 * @returns {number} index of the current node among its parent's children (0 for the root or a `<template>`'s content)
	 */
	get index() {
		const node = _currentNode;
		let i = 0;
		let c = _S._nodeFirstChildren[_nodeParents[node]];
		while (c !== node) {
			// Not in its parent's chain: the root, or a `<template>`'s content.
			if (c === 0) return 0;
			c = _S._nodeNextSiblings[c];
			i++;
		}
		return i;
	},
	/** Stop the walk descending into the current node (enter only). */
	skipChildren() {
		_walkSkip = true;
	},
	/** Stop the walk: no visitor fires after the current one returns. */
	stop() {
		if (_walkStopped) return;
		_walkStopped = true;
		_haltedVisitors = haltVisitors(_visitors);
		_visitors = EMPTY_VISITORS;
	},
	/**
	 * Walks the parent links, so a `<template>`'s content ends at its fragment.
	 * @param {number} type a `NodeType`
	 * @returns {boolean} whether an ancestor of the current node has that type
	 */
	within(type) {
		for (let p = _nodeParents[_currentNode]; p !== 0; p = _nodeParents[p]) {
			if (_S._nodeTypes[p] === type) return true;
		}
		return false;
	},
	// === field reads — `n` defaults to the current node ===
	/**
	 * @param {HtmlNodeRef=} n node
	 * @returns {number} `NodeType`
	 */
	type(n = _currentNode) {
		return n < 0 ? NodeType.Attribute : _S._nodeTypes[n];
	},
	/**
	 * @param {HtmlNodeRef=} n node
	 * @returns {[number, number]} start and end offsets in the input (an attribute's end past its closing quote)
	 */
	range(n = _currentNode) {
		return [_startOf(n), _endOf(n)];
	},
	/**
	 * @param {HtmlNodeRef=} n node
	 * @returns {number} where the node starts in the input
	 */
	start(n = _currentNode) {
		return _startOf(n);
	},
	/**
	 * @param {HtmlNodeRef=} n node
	 * @returns {number} where the node ends in the input
	 */
	end(n = _currentNode) {
		return _endOf(n);
	},
	/**
	 * Line / column are converted only when asked, by a converter made on the
	 * first call of a parse. Valid during the walk, as {@link source} is.
	 * @param {HtmlNodeRef=} n node
	 * @returns {[number, number, number, number]} start line and column, end line and column (lines from 1, columns from 0)
	 */
	loc(n = _currentNode) {
		const lc =
			_htmlLocConverter || (_htmlLocConverter = new LocConverter(_htmlSource));
		const s = lc.get(_startOf(n));
		const line = s.line;
		const column = s.column;
		const e = lc.get(_endOf(n));
		return [line, column, e.line, e.column];
	},
	/**
	 * A node's text as written; valid during the walk, before `parseHtml` releases the source.
	 * @param {HtmlNodeRef | number=} n node, or the start offset when `end` is given
	 * @param {number=} end end offset, to read the input between two offsets
	 * @returns {string} the node's source, or the input between the offsets
	 */
	source(n, end) {
		if (end !== undefined) return _htmlSource.slice(n, end);
		const node = n === undefined ? _currentNode : n;
		if (node < 0) {
			return _htmlSource.slice(_attrNameStarts[-node], _attrEnd(-node));
		}
		return _htmlSource.slice(_nodeStarts[node], _nodeEnds[node]);
	},
	/**
	 * An element's or attribute's name (lowercased, adjusted in foreign content),
	 * a doctype's name, a processing instruction's target; "" for other nodes.
	 * @param {HtmlNodeRef=} n node
	 * @returns {string} its name
	 */
	name(n = _currentNode) {
		if (n < 0) return _attrNames[-n];
		const type = _S._nodeTypes[n];
		if (type === NodeType.Element || type === NodeType.Doctype) {
			return _nodeStrings[n];
		}
		return type === NodeType.ProcessingInstruction
			? _piTargets.get(n) || ""
			: "";
	},
	/**
	 * @param {number} field a `Field`
	 * @param {HtmlNodeRef=} n node
	 * @returns {number} number of items in that field (0 when the node has none)
	 */
	fieldCount(field, n = _currentNode) {
		if (field === Field.attributes) {
			return _S._nodeTypes[n] === NodeType.Element ? _nodeAttrCounts[n] : 0;
		}
		return field === Field.content && _templateContentOf(n) !== 0 ? 1 : 0;
	},
	/**
	 * An attribute item is a node the shared reads take.
	 * @param {number} i item index
	 * @param {number} field a `Field`
	 * @param {HtmlNodeRef=} n node
	 * @returns {HtmlNodeRef} the i-th item of that field (0 = none)
	 */
	field(i, field, n = _currentNode) {
		if (field === Field.attributes) {
			return _S._nodeTypes[n] === NodeType.Element &&
				i >= 0 &&
				i < _nodeAttrCounts[n]
				? -(_nodeAttrStarts[n] + i)
				: 0;
		}
		return field === Field.content && i === 0 ? _templateContentOf(n) : 0;
	},
	/**
	 * Attribute names are stored lowercased, so `name` is compared as written.
	 * @param {number} field a `Field`
	 * @param {string} name the name to look for
	 * @param {HtmlNodeRef=} n node
	 * @returns {HtmlNodeRef} the first item of that field with this name (0 = none)
	 */
	fieldNamed(field, name, n = _currentNode) {
		return field === Field.attributes && _S._nodeTypes[n] === NodeType.Element
			? -_findAttr(_nodeAttrStarts[n], _nodeAttrCounts[n], name) | 0
			: 0;
	},
	/**
	 * @param {number} flag a `Flag`
	 * @param {HtmlNodeRef=} n node
	 * @returns {boolean} whether that flag is set
	 */
	flag(flag, n = _currentNode) {
		return n > 0 && (_S._nodeFlags[n] & flag) !== 0;
	},
	/**
	 * A parser-inserted element has no name, tags or content in the source.
	 * @param {number} part a `Part`
	 * @param {HtmlNodeRef=} n node
	 * @returns {[number, number] | null} where that part is written (null without one)
	 */
	rangeOf(part, n = _currentNode) {
		const start = A.startOf(part, n);
		return start === -1 ? null : [start, A.endOf(part, n)];
	},
	/**
	 * @param {number} part a `Part`
	 * @param {HtmlNodeRef=} n node
	 * @returns {number} where that part starts (-1 without one)
	 */
	startOf(part, n = _currentNode) {
		if (n < 0) {
			if (part === Part.name) return _attrNameStarts[-n];
			return part === Part.value ? _attrValueStarts[-n] : -1;
		}
		const type = _S._nodeTypes[n];
		if (type === NodeType.Doctype) {
			if (part === Part.publicId) return _doctypePublicStart;
			return part === Part.systemId ? _doctypeSystemStart : -1;
		}
		if (type !== NodeType.Element || _nodeNameEnds[n] === 0) return -1;
		if (part === Part.name) return _nodeStarts[n] + 1;
		if (part === Part.startTag) {
			return _hasSourceTag(n) ? _nodeStarts[n] : -1;
		}
		if (part === Part.content) return _nodeTagEnds[n];
		return part === Part.endTag ? _endTagStart(n) : -1;
	},
	/**
	 * @param {number} part a `Part`
	 * @param {HtmlNodeRef=} n node
	 * @returns {number} where that part ends (-1 without one)
	 */
	endOf(part, n = _currentNode) {
		if (n < 0) {
			if (part === Part.name) return _attrNameEnds[-n];
			return part === Part.value && _attrValueStarts[-n] !== -1
				? _attrValueEnds[-n]
				: -1;
		}
		const type = _S._nodeTypes[n];
		if (type === NodeType.Doctype) {
			if (part === Part.publicId) return _doctypePublicEnd;
			return part === Part.systemId ? _doctypeSystemEnd : -1;
		}
		if (type !== NodeType.Element || _nodeNameEnds[n] === 0) return -1;
		if (part === Part.name) return _nodeNameEnds[n];
		if (part === Part.startTag) {
			return _hasSourceTag(n) ? _nodeTagEnds[n] : -1;
		}
		if (part !== Part.content && part !== Part.endTag) return -1;
		// Under `skip.text` a raw-text body records its own end, which also holds
		// where the end tag has attributes and so cannot be read back off the range.
		if (
			part === Part.content &&
			skipText &&
			!xmlMode &&
			RAW_TEXT_ELEMENTS.has(_tagNameOf(n))
		) {
			return _nodeContentEnds[n];
		}
		const endTag = _endTagStart(n);
		if (part === Part.endTag) return endTag === -1 ? -1 : _nodeEnds[n];
		return endTag === -1 ? _nodeEnds[n] : endTag;
	},
	/**
	 * Name and value are read with `name` and `value`; this reads a node's
	 * other text, such as a doctype's identifiers, which are stored decoded.
	 * @param {number} part a `Part`
	 * @param {HtmlNodeRef=} n node
	 * @returns {string | null} that part's text (null without one)
	 */
	textOf(part, n = _currentNode) {
		if (n <= 0 || _S._nodeTypes[n] !== NodeType.Doctype) return null;
		if (part === Part.publicId) return _doctypePublicId;
		return part === Part.systemId ? _doctypeSystemId : null;
	},
	/**
	 * A text's, comment's, processing instruction's or attribute's data,
	 * character references decoded; "" for other nodes.
	 * @param {HtmlNodeRef=} n node
	 * @returns {string} its value
	 */
	value(n = _currentNode) {
		if (n < 0) return _attrValueDecoded(-n);
		const type = _S._nodeTypes[n];
		return type === NodeType.Text ||
			type === NodeType.Comment ||
			type === NodeType.ProcessingInstruction
			? _nodeStrings[n]
			: "";
	},
	/**
	 * Counted along the sibling links once per node and visitor call.
	 * @param {HtmlNodeRef=} n node
	 * @returns {number} number of children
	 */
	childCount(n = _currentNode) {
		const slot = _cursorSlot(n);
		const known = _cursorCounts[slot];
		if (known !== -1) return known;
		let count = 0;
		for (
			let c = _S._nodeFirstChildren[n];
			c !== 0;
			c = _S._nodeNextSiblings[c]
		) {
			count++;
		}
		_cursorCounts[slot] = count;
		return count;
	},
	/**
	 * Reached along the sibling links from the last child read under `n`, so a
	 * loop over a node's children takes one step per child.
	 * @param {number} i child index (`0 <= i < childCount(n)`)
	 * @param {HtmlNodeRef=} n node
	 * @returns {HtmlNodeRef} the i-th child (0 = none)
	 */
	child(i, n = _currentNode) {
		const slot = _cursorSlot(n);
		let k = _cursorIndexes[slot];
		let c = _cursorChildren[slot];
		if (i < k) {
			k = 0;
			c = _S._nodeFirstChildren[n];
		}
		for (; k < i && c !== 0; k++) c = _S._nodeNextSiblings[c];
		_cursorIndexes[slot] = k;
		_cursorChildren[slot] = c;
		return c;
	},
	/**
	 * Every child as a new array, for a reader that keeps or searches them.
	 * @param {HtmlNodeRef=} n node
	 * @returns {HtmlNodeRef[]} its children (empty for an attribute or a leaf)
	 */
	children(n = _currentNode) {
		/** @type {HtmlNodeRef[]} */
		const out = [];
		if (n < 0) return out;
		for (
			let c = _S._nodeFirstChildren[n];
			c !== 0;
			c = _S._nodeNextSiblings[c]
		) {
			out.push(c);
		}
		return out;
	}
};

module.exports.A = A;
module.exports.BLOCK_CONTENTS = BLOCK_CONTENTS;
module.exports.EMBEDDED_LANGUAGES = EMBEDDED_LANGUAGES;
module.exports.EVENT_HANDLER = EVENT_HANDLER;
module.exports.FOREIGN_ELEMENT = FOREIGN_ELEMENT;
module.exports.FLAG_FOSTER_REGION = FLAG_FOSTER_REGION;
module.exports.JSON_TYPE = JSON_TYPE;
module.exports.Field = Field;
module.exports.Flag = Flag;
module.exports.Part = Part;
module.exports.Namespace = Namespace;
module.exports.NodeType = NodeType;
module.exports.Quote = Quote;
module.exports.SVG_TYPE = SVG_TYPE;
module.exports._CC_NUMBER_SIGN = CC_NUMBER_SIGN;
module.exports._S = _S;
module.exports._attributeCountOf = _attributeCountOf;
module.exports._attributeList = _attributeList;
module.exports._dataOf = _dataOf;
module.exports._firstChild = _firstChild;
module.exports._tagNameOf = _tagNameOf;
module.exports._namespaceOf = _namespaceOf;
module.exports._nextSibling = _nextSibling;
module.exports._parentOf = _parentOf;
module.exports.asciiLowercase = asciiLowercase;
module.exports.stripAsciiWhitespace = stripAsciiWhitespace;
module.exports._attributePrints = _attributePrints;
module.exports._canOmitEndTag = _canOmitEndTag;
module.exports._canOmitStartTag = _canOmitStartTag;
module.exports._contentOpensOutside = _contentOpensOutside;
module.exports._deferEmbedded = _deferEmbedded;
module.exports._elementCloseTag = _elementCloseTag;
module.exports._endTagOutsideSpan = _endTagOutsideSpan;
module.exports._endsOutsideLineBox = _endsOutsideLineBox;
module.exports._formNeedsPointerReset = _formNeedsPointerReset;
module.exports._fosterRunMoves = _fosterRunMoves;
module.exports._fosterSourceRegion = _fosterSourceRegion;
module.exports._impliedCloseTag = _impliedCloseTag;
module.exports._impliedTableGroupStartNeeded = _impliedTableGroupStartNeeded;
module.exports._isCssStyleElement = _isCssStyleElement;
module.exports._isRedundantAttribute = _isRedundantAttribute;
module.exports._isRemovableEmptyAttribute = _isRemovableEmptyAttribute;
module.exports._isRemovableEmptyElement = _isRemovableEmptyElement;
module.exports._isSvgRoot = _isSvgRoot;
module.exports._keepComment = _keepComment;
module.exports._minifyOpenTag = _minifyOpenTag;
module.exports._nodeStrings = _nodeStrings;
module.exports._openTagSource = _openTagSource;
module.exports._printedAttributeCount = _printedAttributeCount;
module.exports._printsNothing = _printsNothing;
module.exports._headLeftBefore = _headLeftBefore;
module.exports._renderEmbeddedOrNull = _renderEmbeddedOrNull;
module.exports._startsCharacterReference = _startsCharacterReference;
module.exports._startsOutsideLineBox = _startsOutsideLineBox;
module.exports._synthesizeOpenTag = _synthesizeOpenTag;
module.exports._takeRenderedSheet = _takeRenderedSheet;
module.exports._rendersNow = _rendersNow;
module.exports._sheetDecides = _sheetDecides;
module.exports._onlyTextChild = _onlyTextChild;
module.exports._sheetClosing = _sheetClosing;
module.exports._styleRunText = _styleRunText;
module.exports._xmlCharacterData = _xmlCharacterData;
module.exports._xmlElementIsStyled = _xmlElementIsStyled;
module.exports._xmlOpenTag = _xmlOpenTag;
module.exports.baseTag = baseTag;
module.exports.buildHeadTags = buildHeadTags;
module.exports.collapseWhitespaceRuns = collapseWhitespaceRuns;
module.exports.decodeEntities = decodeEntities;
module.exports.decodeXmlAttribute = decodeXmlAttribute;
module.exports.decodeXmlEntities = decodeXmlEntities;
module.exports.escapeAttribute = escapeAttribute;
module.exports.escapeText = escapeText;
module.exports.escapeXmlAttribute = escapeXmlAttribute;
module.exports.escapeXmlText = escapeXmlText;
module.exports.grammar = grammar;
module.exports.isAllAsciiWhitespace = isAllAsciiWhitespace;
module.exports.isAsciiAlphanumeric = isAsciiAlphanumeric;
module.exports.isAsciiWhitespace = isAsciiWhitespace;
module.exports.metaTag = metaTag;
module.exports.parseCssUrls = parseCssUrls;
module.exports.parseHtml = parseHtml;
module.exports.parseMsapplicationTask = parseMsapplicationTask;
module.exports.parseSrc = parseSrc;
module.exports.parseSrcset = parseSrcset;
module.exports.pickTransforms = pickTransforms;
module.exports.startsWithAsciiWhitespace = startsWithAsciiWhitespace;
module.exports.tokenize = tokenize;
module.exports.tokenizeXml = tokenizeXml;