/
githubmirror
/
webpack
Обзор
Документация
Войти
/
githubmirror
/
webpack
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
v5.108.4
lib/html/syntax.js
8 417 строк
296 KB
Alexander Akait
perf: reduce CPU and memory overhead of the HTML and CSS pipelines (#21332)
03 июл 2026, 22:42
Не верифицирован
03 июл 2026, 22:42
ae28c54
Код
Авторство
О чём код?
/* MIT License http://www.opensource.org/licenses/mit-license.php Author Raj Aryan (based on SWC parser by Alexander Akait) */ "use strict"; const GenericSourceProcessor = require("../util/SourceProcessor"); // cspell:ignore apos notpre noncharacter noncharacters DFFF FFFE CCLS ALNUM // #region html entities // The contents of this region are auto-generated by // `tooling/generate-html-entities.js` from `tooling/html-entities.json`. // Do not edit by hand — re-run the generator (via `yarn fix:special`) to refresh. // // WHATWG named character references. Keys are entity names WITHOUT the // leading `&` (some end with `;`, others omit it for legacy entities that // match without a closing semicolon). Values are the decoded character // strings (1–2 UTF-16 code units). // Built on a null prototype so bracket lookups (`HTML_ENTITIES[name]`) // can't be poisoned by inherited `Object.prototype` keys like `toString`, // `constructor`, or `__proto__` — without this, `&toString;` would falsely // look like a matched named character reference. // prettier-ignore // cspell:disable-next-line const HTML_ENTITIES = /** @type {Readonly<Record<string, string>>} */ (Object.freeze(Object.assign(Object.create(null), {"AElig":"Æ","AElig;":"Æ","AMP":"&","AMP;":"&","Aacute":"Á","Aacute;":"Á","Abrev … [Строка слишком длинная. Вы можете скачать файл] // #endregion const STATE_DATA = 0; const STATE_TAG_OPEN = 1; const STATE_END_TAG_OPEN = 2; const STATE_TAG_NAME = 3; const STATE_BEFORE_ATTRIBUTE_NAME = 4; const STATE_ATTRIBUTE_NAME = 5; const STATE_AFTER_ATTRIBUTE_NAME = 6; const STATE_BEFORE_ATTRIBUTE_VALUE = 7; const STATE_ATTRIBUTE_VALUE_DOUBLE_QUOTED = 8; const STATE_ATTRIBUTE_VALUE_SINGLE_QUOTED = 9; const STATE_ATTRIBUTE_VALUE_UNQUOTED = 10; const STATE_AFTER_ATTRIBUTE_VALUE_QUOTED = 11; const STATE_SELF_CLOSING_START_TAG = 12; const STATE_MARKUP_DECLARATION_OPEN = 13; const STATE_COMMENT_START = 14; const STATE_COMMENT_START_DASH = 15; const STATE_COMMENT = 16; const STATE_COMMENT_END_DASH = 17; const STATE_COMMENT_END = 18; const STATE_COMMENT_END_BANG = 19; const STATE_BOGUS_COMMENT = 20; const STATE_COMMENT_LESS_THAN_SIGN = 21; const STATE_COMMENT_LESS_THAN_SIGN_BANG = 22; const STATE_COMMENT_LESS_THAN_SIGN_BANG_DASH = 23; const STATE_COMMENT_LESS_THAN_SIGN_BANG_DASH_DASH = 24; const STATE_DOCTYPE = 25; const STATE_BEFORE_DOCTYPE_NAME = 26; const STATE_DOCTYPE_NAME = 27; const STATE_AFTER_DOCTYPE_NAME = 28; const STATE_AFTER_DOCTYPE_PUBLIC_KEYWORD = 29; const STATE_BEFORE_DOCTYPE_PUBLIC_IDENTIFIER = 30; const STATE_DOCTYPE_PUBLIC_IDENTIFIER_DOUBLE_QUOTED = 31; const STATE_DOCTYPE_PUBLIC_IDENTIFIER_SINGLE_QUOTED = 32; const STATE_AFTER_DOCTYPE_PUBLIC_IDENTIFIER = 33; const STATE_BETWEEN_DOCTYPE_PUBLIC_AND_SYSTEM_IDENTIFIERS = 34; const STATE_AFTER_DOCTYPE_SYSTEM_KEYWORD = 35; const STATE_BEFORE_DOCTYPE_SYSTEM_IDENTIFIER = 36; const STATE_DOCTYPE_SYSTEM_IDENTIFIER_DOUBLE_QUOTED = 37; const STATE_DOCTYPE_SYSTEM_IDENTIFIER_SINGLE_QUOTED = 38; const STATE_AFTER_DOCTYPE_SYSTEM_IDENTIFIER = 39; const STATE_BOGUS_DOCTYPE = 40; const STATE_CDATA_SECTION = 41; const STATE_CDATA_SECTION_BRACKET = 42; const STATE_CDATA_SECTION_END = 43; const STATE_RCDATA = 44; const STATE_RCDATA_LESS_THAN_SIGN = 45; const STATE_RCDATA_END_TAG_OPEN = 46; const STATE_RCDATA_END_TAG_NAME = 47; const STATE_RAWTEXT = 48; const STATE_RAWTEXT_LESS_THAN_SIGN = 49; const STATE_RAWTEXT_END_TAG_OPEN = 50; const STATE_RAWTEXT_END_TAG_NAME = 51; const STATE_SCRIPT_DATA = 52; const STATE_SCRIPT_DATA_LESS_THAN_SIGN = 53; const STATE_SCRIPT_DATA_END_TAG_OPEN = 54; const STATE_SCRIPT_DATA_END_TAG_NAME = 55; const STATE_SCRIPT_DATA_ESCAPE_START = 56; const STATE_SCRIPT_DATA_ESCAPE_START_DASH = 57; const STATE_SCRIPT_DATA_ESCAPED = 58; const STATE_SCRIPT_DATA_ESCAPED_DASH = 59; const STATE_SCRIPT_DATA_ESCAPED_DASH_DASH = 60; const STATE_SCRIPT_DATA_ESCAPED_LESS_THAN_SIGN = 61; const STATE_SCRIPT_DATA_ESCAPED_END_TAG_OPEN = 62; const STATE_SCRIPT_DATA_ESCAPED_END_TAG_NAME = 63; const STATE_SCRIPT_DATA_DOUBLE_ESCAPE_START = 64; const STATE_SCRIPT_DATA_DOUBLE_ESCAPED = 65; const STATE_SCRIPT_DATA_DOUBLE_ESCAPED_DASH = 66; const STATE_SCRIPT_DATA_DOUBLE_ESCAPED_DASH_DASH = 67; const STATE_SCRIPT_DATA_DOUBLE_ESCAPED_LESS_THAN_SIGN = 68; const STATE_SCRIPT_DATA_DOUBLE_ESCAPE_END = 69; const STATE_PLAINTEXT = 70; // https://html.spec.whatwg.org/multipage/parsing.html#character-reference-state const STATE_CHARACTER_REFERENCE = 71; // https://html.spec.whatwg.org/multipage/parsing.html#named-character-reference-state const STATE_NAMED_CHARACTER_REFERENCE = 72; // https://html.spec.whatwg.org/multipage/parsing.html#ambiguous-ampersand-state const STATE_AMBIGUOUS_AMPERSAND = 73; // https://html.spec.whatwg.org/multipage/parsing.html#numeric-character-reference-state const STATE_NUMERIC_CHARACTER_REFERENCE = 74; // https://html.spec.whatwg.org/multipage/parsing.html#hexadecimal-character-reference-start-state const STATE_HEXADECIMAL_CHARACTER_REFERENCE_START = 75; // https://html.spec.whatwg.org/multipage/parsing.html#decimal-character-reference-start-state const STATE_DECIMAL_CHARACTER_REFERENCE_START = 76; // https://html.spec.whatwg.org/multipage/parsing.html#hexadecimal-character-reference-state const STATE_HEXADECIMAL_CHARACTER_REFERENCE = 77; // https://html.spec.whatwg.org/multipage/parsing.html#decimal-character-reference-state const STATE_DECIMAL_CHARACTER_REFERENCE = 78; // https://html.spec.whatwg.org/multipage/parsing.html#numeric-character-reference-end-state const STATE_NUMERIC_CHARACTER_REFERENCE_END = 79; const CC_TAB = 0x09; const CC_LF = 0x0a; const CC_FF = 0x0c; const CC_CR = 0x0d; const CC_SPACE = 0x20; const CC_NULL = 0x00; const CC_EXCLAMATION_MARK = 0x21; const CC_QUOTATION_MARK = 0x22; const CC_NUMBER_SIGN = 0x23; const CC_AMPERSAND = 0x26; const CC_APOSTROPHE = 0x27; const CC_HYPHEN_MINUS = 0x2d; const CC_SOLIDUS = 0x2f; const CC_SEMICOLON = 0x3b; const CC_LESS_THAN = 0x3c; const CC_EQUALS = 0x3d; const CC_GREATER_THAN = 0x3e; const CC_QUESTION_MARK = 0x3f; const CC_LEFT_SQUARE_BRACKET = 0x5b; const CC_RIGHT_SQUARE_BRACKET = 0x5d; const CC_GRAVE_ACCENT = 0x60; const QUOTE_DOUBLE = 1; const QUOTE_SINGLE = 2; const QUOTE_NONE = 0; // Longest WHATWG named entity name *including* the trailing `;` is 32 chars // (`CounterClockwiseContourIntegral;`); without the trailing `;` it's 31. // Used to cap both the tokenizer's named-character-reference run length and // the decoder's longest-prefix backtrack so pathological inputs (e.g. `&` // followed by thousands of alphanumerics) stay linear-time. const MAX_ENTITY_NAME_LEN = 32; // ASCII character-class bit flags packed into one lookup table. The tokenizer // runs these predicates per code point (tag names, attribute names, character // references, whitespace skipping), so a single table load + mask replaces the // per-call comparison chains. Code points >= 0x80 are never in any of these // classes, so callers short-circuit on `cc < 0x80` before indexing. const CCLS_SPACE = 1; const CCLS_DIGIT = 2; const CCLS_UPPER = 4; const CCLS_LOWER = 8; const CCLS_HEX = 16; const CCLS_ALPHA = CCLS_UPPER | CCLS_LOWER; const CCLS_ALNUM = CCLS_ALPHA | CCLS_DIGIT; const CHAR_CLASS = new Uint8Array(128); for (let i = 0; i < 128; i++) { let f = 0; if ( i === CC_TAB || i === CC_LF || i === CC_FF || i === CC_CR || i === CC_SPACE ) { f |= CCLS_SPACE; } if (i >= 0x30 && i <= 0x39) f |= CCLS_DIGIT | CCLS_HEX; if (i >= 0x41 && i <= 0x5a) f |= CCLS_UPPER; if (i >= 0x61 && i <= 0x7a) f |= CCLS_LOWER; if ((i >= 0x41 && i <= 0x46) || (i >= 0x61 && i <= 0x66)) f |= CCLS_HEX; CHAR_CLASS[i] = f; } /** * @param {number} cc character code * @returns {boolean} is ascii alpha */ const isAsciiAlpha = (cc) => cc < 0x80 && (CHAR_CLASS[cc] & CCLS_ALPHA) !== 0; /** * @param {number} cc character code * @returns {boolean} is ascii alphanumeric */ const isAsciiAlphanumeric = (cc) => cc < 0x80 && (CHAR_CLASS[cc] & CCLS_ALNUM) !== 0; /** * @param {number} cc character code * @returns {boolean} is ascii digit */ const isAsciiDigit = (cc) => cc < 0x80 && (CHAR_CLASS[cc] & CCLS_DIGIT) !== 0; /** * @param {number} cc character code * @returns {boolean} is ascii hex digit */ const isAsciiHexDigit = (cc) => cc < 0x80 && (CHAR_CLASS[cc] & CCLS_HEX) !== 0; /** * @param {number} cc character code * @returns {boolean} is ascii upper alpha */ const isAsciiUpperAlpha = (cc) => cc < 0x80 && (CHAR_CLASS[cc] & CCLS_UPPER) !== 0; /** * @param {number} cc character code * @returns {boolean} is ascii lower alpha */ const isAsciiLowerAlpha = (cc) => cc < 0x80 && (CHAR_CLASS[cc] & CCLS_LOWER) !== 0; /** * Tokenizer whitespace. U+000D CARRIAGE RETURN is included because the spec's * input-stream preprocessing converts CR (and CRLF) to LF before tokenizing; * this scanner keeps the original offsets, so it treats a raw CR as whitespace * to match the post-preprocessing behaviour. * @param {number} cc character code * @returns {boolean} is space */ const isSpace = (cc) => cc < 0x80 && (CHAR_CLASS[cc] & CCLS_SPACE) !== 0; /** * @param {number} code numeric character reference code point * @returns {boolean} is a Unicode noncharacter */ const isNoncharacter = (code) => (code >= 0xfdd0 && code <= 0xfdef) || (code & 0xfffe) === 0xfffe; /** * @param {number} code numeric character reference code point * @returns {boolean} is a C0/C1 control that is not ASCII whitespace */ const isControlReference = (code) => code === 0x0d || ((code <= 0x1f || (code >= 0x7f && code <= 0x9f)) && code !== CC_TAB && code !== CC_LF && code !== CC_FF && code !== CC_SPACE); /** * Severity of a tokenizer-detected parse error. `"warning"` is recoverable * (the tokenizer continued and the emitted token is still well-formed, e.g. * missing-attribute-value); `"error"` means the emitted token's offset * range is incomplete or does not match what the spec would produce, e.g. * eof-in-tag. * * Token offsets are JS string indices (UTF-16 code-unit offsets into * `input`), not byte offsets — relevant for inputs containing non-BMP * code points where one code point spans two indices. * @typedef {"warning" | "error"} ParseErrorSeverity */ /** * @typedef {object} HtmlTokenCallbacks * @property {(input: string, start: number, end: number, nameStart: number, nameEnd: number, selfClosing: boolean) => number=} openTag * @property {(input: string, start: number, end: number, nameStart: number, nameEnd: number) => number=} closeTag * @property {(input: string, start: number, end: number) => number=} text * @property {(input: string, nameStart: number, nameEnd: number, valueStart: number, valueEnd: number, quoteType: number) => number=} attribute * @property {(input: string, start: number, end: number) => number=} comment * @property {(input: string, start: number, end: number) => number=} doctype * @property {(input: string, code: string, start: number, end: number, severity: ParseErrorSeverity) => void=} parseError * @property {(() => boolean)=} isForeign returns true when the adjusted current node is in a foreign (SVG/MathML) namespace, vetoing RAWTEXT/RCDATA/script content-mode switches * @property {string=} fragmentContext context element tag name for fragment parsing; seeds the initial content mode */ /** * @param {string} name tag name (lowercase) * @returns {number} content mode state for this tag, or STATE_DATA */ const getContentModeForTag = (name) => { switch (name) { case "textarea": case "title": return STATE_RCDATA; case "style": case "xmp": case "iframe": case "noembed": case "noframes": return STATE_RAWTEXT; case "script": return STATE_SCRIPT_DATA; case "plaintext": return STATE_PLAINTEXT; default: return STATE_DATA; } }; /** * Case-insensitive comparison of `input[start..end)` to a lowercase ASCII * literal, without allocating the slice. * @param {string} input input * @param {number} start range start * @param {number} end range end * @param {string} lit lowercase ASCII literal * @returns {boolean} true if the range equals `lit` ignoring ASCII case */ const rangeEqualsLower = (input, start, end, lit) => { if (end - start !== lit.length) return false; for (let i = 0; i < lit.length; i++) { let c = input.charCodeAt(start + i); if (c >= 0x41 && c <= 0x5a) c += 0x20; if (c !== lit.charCodeAt(i)) return false; } return true; }; /** * Content mode for the just-opened tag whose name spans `input[start..end)`, * matched on the raw range so ordinary tags need neither a slice nor a * `toLowerCase`. Mirrors `getContentModeForTag`. * @param {string} input input * @param {number} start tag-name start * @param {number} end tag-name end * @returns {number} content mode state, or STATE_DATA */ const getContentModeForRange = (input, start, end) => { switch (end - start) { case 3: if (rangeEqualsLower(input, start, end, "xmp")) return STATE_RAWTEXT; return STATE_DATA; case 5: if (rangeEqualsLower(input, start, end, "title")) return STATE_RCDATA; if (rangeEqualsLower(input, start, end, "style")) return STATE_RAWTEXT; return STATE_DATA; case 6: if (rangeEqualsLower(input, start, end, "script")) { return STATE_SCRIPT_DATA; } if (rangeEqualsLower(input, start, end, "iframe")) return STATE_RAWTEXT; return STATE_DATA; case 7: if (rangeEqualsLower(input, start, end, "noembed")) return STATE_RAWTEXT; return STATE_DATA; case 8: if (rangeEqualsLower(input, start, end, "textarea")) return STATE_RCDATA; if (rangeEqualsLower(input, start, end, "noframes")) return STATE_RAWTEXT; return STATE_DATA; case 9: if (rangeEqualsLower(input, start, end, "plaintext")) { return STATE_PLAINTEXT; } return STATE_DATA; default: return STATE_DATA; } }; /** * @param {string} input input string * @param {number} pos current position * @param {HtmlTokenCallbacks} callbacks callbacks * @returns {number} final position */ const walkHtmlTokens = (input, pos = 0, callbacks = {}) => { const len = input.length; let state = STATE_DATA; let returnState = STATE_DATA; let textStart = pos; let tagStart = pos; let tagNameStart = -1; let tagNameEnd = -1; let attributeNameStart = -1; let attributeNameEnd = -1; let attributeValueStart = -1; let attrQuoteType = QUOTE_NONE; let commentStart = pos; let lastOpenTagName = ""; // Tag-name offsets of the last open tag; the lowercased `lastOpenTagName` is // derived from these lazily (only for special-content tags). let lastOpenTagStart = -1; let lastOpenTagEnd = -1; // Counter used by SCRIPT_DATA_DOUBLE_ESCAPE_{START,END} to detect whether // the ASCII-alpha run after `<` / `</` spells exactly `"script"`. Values // 0..6 = number of chars matched so far; 7 = no longer matches (sentinel). // Avoids growing a buffer for pathological inputs with long alpha runs. let scriptMatch = 0; let namedEntityConsumed = 0; // Offset of the opening `&` and the running numeric value (clamped past the // Unicode range so it can't overflow); used for numeric-reference errors. let charRefStart = -1; let charRefCode = 0; // Tracks whether the current tag has parsed any attributes — used to // fire the `end-tag-with-attributes` parse error when an end tag emits. let tagHasAttributes = false; /** * Reports a tokenizer parse error to the consumer. The offset range and * severity follow the WHATWG spec naming. Severity is `"error"` for * cases where the emitted token is incomplete (EOF inside a tag or * comment); everything else is a `"warning"`. Offsets are JS string * indices (UTF-16 code-unit offsets into `input`). * @param {string} code WHATWG parse-error code (kebab-case) * @param {number} start string offset where the error starts * @param {number} end string offset where the error ends * @param {ParseErrorSeverity} severity error severity */ const reportError = (code, start, end, severity) => { if (callbacks.parseError !== undefined) { callbacks.parseError(input, code, start, end, severity); } }; /** * Emits the WHATWG numeric-character-reference validation parse error for * the accumulated `charRefCode`, if any. Used both inline (when the * reference is terminated by a real next character) and at EOF (when the * reference runs to the end of input). The scanner only flags the error — * the spec's U+FFFD / Windows-1252 substitution is done by `decodeHtmlEntities`. * @param {number} endPos offset just past the reference */ const validateNumericReference = (endPos) => { if (charRefCode === 0) { reportError("null-character-reference", charRefStart, endPos, "warning"); } else if (charRefCode > 0x10ffff) { reportError( "character-reference-outside-unicode-range", charRefStart, endPos, "warning" ); } else if (charRefCode >= 0xd800 && charRefCode <= 0xdfff) { reportError( "surrogate-character-reference", charRefStart, endPos, "warning" ); } else if (isNoncharacter(charRefCode)) { reportError( "noncharacter-character-reference", charRefStart, endPos, "warning" ); } else if (isControlReference(charRefCode)) { reportError( "control-character-reference", charRefStart, endPos, "warning" ); } }; // Content mode for the tag just opened (name at `lastOpenTagStart..End`). In // foreign content (SVG/MathML) the tree builder vetoes RAWTEXT/RCDATA/script // switching via `isForeign`, so e.g. an SVG `<title>`/`<style>` is parsed as // normal markup. `lastOpenTagName` (the lowercased name compared by the // special end-tag states) is materialized only when a special mode is // actually entered — ordinary tags never allocate it. const contentModeAfterOpenTag = () => { const m = getContentModeForRange(input, lastOpenTagStart, lastOpenTagEnd); // Ordinary tags stay in data state regardless of `isForeign` (which only // vetoes a switch *into* a special mode), so skip the per-open-tag // `isForeign` callback for them. if (m === STATE_DATA) return STATE_DATA; if (callbacks.isForeign !== undefined && callbacks.isForeign()) { return STATE_DATA; } lastOpenTagName = input .slice(lastOpenTagStart, lastOpenTagEnd) .toLowerCase(); return m; }; // HTML fragment parsing: seed the tokenizer with the context element's // content mode (e.g. a `textarea`/`style`/`script` context starts in // RCDATA/RAWTEXT/script-data rather than data state). if (callbacks.fragmentContext !== undefined) { lastOpenTagName = callbacks.fragmentContext; state = callbacks.isForeign !== undefined && callbacks.isForeign() ? STATE_DATA : getContentModeForTag(lastOpenTagName); } /** * @param {number} endPos end position */ const flushText = (endPos) => { if (textStart < endPos) { if (callbacks.text !== undefined) { callbacks.text(input, textStart, endPos); } // Advance `textStart` so a second `flushText` for the same span // (e.g. from the EOF handler after a tag-open transition already // flushed the pending text) is a no-op rather than a duplicate // emit. emitOpenTag / emitCloseTag overwrite `textStart` with // their own `nextPos` anyway, so this doesn't shift their start. textStart = endPos; } }; /** * @param {number} endPos end position * @returns {number} next position */ const emitAttribute = (endPos) => { // Default `nextPos` advances past the closing quote (if any) so the // state machine can continue when no `attribute` callback is provided. // When a callback IS provided, its return value overrides the default — // the callback is expected to do the same advance based on the // reported `quoteType`. let nextPos = attrQuoteType === QUOTE_NONE ? endPos : endPos + 1; if (callbacks.attribute !== undefined && 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); // All WHATWG tokenizer states handled. Deliberately omitted parse errors // (need state this offset scanner lacks): duplicate-attribute, // cdata-in-html-content, `*-in-input-stream`. Reference substitution is // left to `decodeHtmlEntities`. switch (state) { // https://html.spec.whatwg.org/multipage/parsing.html#data-state case STATE_DATA: // Consume the next input character: // U+003C LESS-THAN SIGN (<) // Set the return state to the data state. Switch to the tag open state. if (cc === CC_LESS_THAN) { tagStart = pos; state = STATE_TAG_OPEN; pos++; } else if (cc === CC_AMPERSAND) { // U+0026 AMPERSAND (&) // Set the return state to the data state. Switch to the // character reference state. returnState = STATE_DATA; state = STATE_CHARACTER_REFERENCE; pos++; } else if (cc === CC_NULL) { // U+0000 NULL: unexpected-null-character (the data state // emits the NULL as-is; the scanner only flags the error). reportError("unexpected-null-character", pos, pos + 1, "warning"); pos++; } else { // Fast-forward over the run of ordinary text without re-entering // the per-state switch; stop on the next significant code point. pos++; while (pos < len) { const c2 = input.charCodeAt(pos); if (c2 === CC_LESS_THAN || c2 === CC_AMPERSAND || c2 === CC_NULL) { break; } pos++; } } break; // https://html.spec.whatwg.org/multipage/parsing.html#tag-open-state case STATE_TAG_OPEN: // Consume the next input character: // U+002F SOLIDUS (/) // Switch to the end tag open state. if (cc === CC_SOLIDUS) { state = STATE_END_TAG_OPEN; pos++; } else if (cc === CC_EXCLAMATION_MARK) { // U+0021 EXCLAMATION MARK (!) // Switch to the markup declaration open state. flushText(tagStart); commentStart = tagStart; state = STATE_MARKUP_DECLARATION_OPEN; pos++; } else if (isAsciiAlpha(cc)) { // ASCII alpha // Create a new start tag token, set its tag name to the empty string. // Reconsume in the tag name state. flushText(tagStart); tagNameStart = pos; state = STATE_TAG_NAME; // Reconsume } else if (cc === CC_QUESTION_MARK) { // U+003F QUESTION MARK (?) // This is an unexpected-question-mark-instead-of-tag-name parse error. // Create a comment token whose data is the empty string. Reconsume in the // bogus comment state. reportError( "unexpected-question-mark-instead-of-tag-name", pos, pos + 1, "warning" ); flushText(tagStart); commentStart = tagStart; state = STATE_BOGUS_COMMENT; // Reconsume — let the bogus-comment state consume the `?` // itself, matching the spec. } else { // Anything else // This is an invalid-first-character-of-tag-name parse error. Emit a U+003C // LESS-THAN SIGN character token. Reconsume in the data state. reportError( "invalid-first-character-of-tag-name", pos, pos + 1, "warning" ); state = STATE_DATA; // Reconsume } break; // https://html.spec.whatwg.org/multipage/parsing.html#end-tag-open-state case STATE_END_TAG_OPEN: // Consume the next input character: // ASCII alpha // Create a new end tag token, set its tag name to the empty string. // Reconsume in the tag name state. if (isAsciiAlpha(cc)) { flushText(tagStart); tagNameStart = pos; state = STATE_TAG_NAME; // Reconsume } else if (cc === CC_GREATER_THAN) { // U+003E GREATER-THAN SIGN (>) // This is a missing-end-tag-name parse error. Switch to the data state. reportError("missing-end-tag-name", pos, pos + 1, "warning"); state = STATE_DATA; pos++; } else { // Anything else // This is an invalid-first-character-of-tag-name parse error. Create a // comment token whose data is the empty string. Reconsume in the bogus // comment state. reportError( "invalid-first-character-of-tag-name", pos, pos + 1, "warning" ); flushText(tagStart); commentStart = tagStart; state = STATE_BOGUS_COMMENT; // Reconsume — let bogus-comment consume this char itself. } break; // https://html.spec.whatwg.org/multipage/parsing.html#tag-name-state case STATE_TAG_NAME: // Consume the next input character: // U+0009 CHARACTER TABULATION (tab) // U+000A LINE FEED (LF) // U+000C FORM FEED (FF) // U+0020 SPACE // Switch to the before attribute name state. if (isSpace(cc)) { tagNameEnd = pos; state = STATE_BEFORE_ATTRIBUTE_NAME; pos++; } else if (cc === CC_SOLIDUS) { // U+002F SOLIDUS (/) // Switch to the self-closing start tag state. tagNameEnd = pos; state = STATE_SELF_CLOSING_START_TAG; pos++; } else if (cc === CC_GREATER_THAN) { // U+003E GREATER-THAN SIGN (>) // Switch to the data state. Emit the current tag token. tagNameEnd = pos; if (input.charCodeAt(tagStart + 1) === CC_SOLIDUS) { state = STATE_DATA; pos = emitCloseTag(pos + 1); } else { const nextPos = emitOpenTag(pos + 1, false); state = nextPos > pos + 1 ? STATE_DATA : contentModeAfterOpenTag(); pos = nextPos; } } else { // U+0000 NULL: unexpected-null-character (append U+FFFD). if (cc === CC_NULL) { reportError("unexpected-null-character", pos, pos + 1, "warning"); } // Fast-forward over the ordinary run of the tag name. pos++; while (pos < len) { const c2 = input.charCodeAt(pos); if ( c2 === CC_SOLIDUS || c2 === CC_GREATER_THAN || c2 === CC_NULL || isSpace(c2) ) { break; } pos++; } } break; // https://html.spec.whatwg.org/multipage/parsing.html#before-attribute-name-state case STATE_BEFORE_ATTRIBUTE_NAME: // Consume the next input character: // U+0009 CHARACTER TABULATION (tab) // U+000A LINE FEED (LF) // U+000C FORM FEED (FF) // U+0020 SPACE // Ignore the character. // Reconsume so space is handled in BEFORE_ATTRIBUTE_NAME if (isSpace(cc)) { pos++; } else if (cc === CC_SOLIDUS || cc === CC_GREATER_THAN) { // U+002F SOLIDUS (/) // U+003E GREATER-THAN SIGN (>) // EOF // Reconsume in the after attribute name state. state = STATE_AFTER_ATTRIBUTE_NAME; // Reconsume } else if (cc === CC_EQUALS) { // U+003D EQUALS SIGN (=) // This is an unexpected-equals-sign-before-attribute-name parse // error. Start a new attribute. Switch to the attribute name state. reportError( "unexpected-equals-sign-before-attribute-name", pos, pos + 1, "warning" ); 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; // Reconsume } break; // https://html.spec.whatwg.org/multipage/parsing.html#attribute-name-state case STATE_ATTRIBUTE_NAME: // Consume the next input character: // U+0009 CHARACTER TABULATION (tab) // U+000A LINE FEED (LF) // U+000C FORM FEED (FF) // U+0020 SPACE // U+002F SOLIDUS (/) // U+003E GREATER-THAN SIGN (>) // EOF // Reconsume in the after attribute name state. if (isSpace(cc) || cc === CC_SOLIDUS || cc === CC_GREATER_THAN) { 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 ordinary run of the attribute name; stop on // any terminator (space / `/` / `>` / `=`) or a char that needs a // per-occurrence parse error (NULL / `"` / `'` / `<`), which the // outer switch then re-handles. pos++; while (pos < len) { const c2 = input.charCodeAt(pos); if ( c2 === CC_SOLIDUS || c2 === CC_GREATER_THAN || c2 === CC_EQUALS || c2 === CC_NULL || c2 === CC_QUOTATION_MARK || c2 === CC_APOSTROPHE || c2 === CC_LESS_THAN || isSpace(c2) ) { break; } pos++; } } break; // https://html.spec.whatwg.org/multipage/parsing.html#after-attribute-name-state case STATE_AFTER_ATTRIBUTE_NAME: // Consume the next input character: // U+0009 CHARACTER TABULATION (tab) // U+000A LINE FEED (LF) // U+000C FORM FEED (FF) // U+0020 SPACE // Ignore the character. if (isSpace(cc)) { pos++; } else if (cc === CC_SOLIDUS) { // U+002F SOLIDUS (/) // Switch to the self-closing start tag state. emitAttribute(pos); state = STATE_SELF_CLOSING_START_TAG; pos++; } else if (cc === CC_EQUALS) { // U+003D EQUALS SIGN (=) // Switch to the before attribute value state. state = STATE_BEFORE_ATTRIBUTE_VALUE; pos++; } else if (cc === CC_GREATER_THAN) { // U+003E GREATER-THAN SIGN (>) // Switch to the data state. Emit the current tag token. emitAttribute(pos); if (input.charCodeAt(tagStart + 1) === CC_SOLIDUS) { state = STATE_DATA; pos = emitCloseTag(pos + 1); } else { const nextPos = emitOpenTag(pos + 1, false); state = nextPos > pos + 1 ? STATE_DATA : contentModeAfterOpenTag(); pos = nextPos; } } else { // Anything else // Start a new attribute in the current tag token. emitAttribute(pos); 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: // Consume the next input character: // U+0009 CHARACTER TABULATION (tab) // U+000A LINE FEED (LF) // U+000C FORM FEED (FF) // U+0020 SPACE // Ignore the character. if (isSpace(cc)) { pos++; } else if (cc === CC_QUOTATION_MARK) { // U+0022 QUOTATION MARK (") // Switch to the attribute value (double-quoted) state. attributeValueStart = pos + 1; attrQuoteType = QUOTE_DOUBLE; state = STATE_ATTRIBUTE_VALUE_DOUBLE_QUOTED; pos++; } else if (cc === CC_APOSTROPHE) { // U+0027 APOSTROPHE (') // Switch to the attribute value (single-quoted) state. attributeValueStart = pos + 1; attrQuoteType = QUOTE_SINGLE; state = STATE_ATTRIBUTE_VALUE_SINGLE_QUOTED; pos++; } else if (cc === CC_GREATER_THAN) { // U+003E GREATER-THAN SIGN (>) // This is a missing-attribute-value parse error. Switch to the data // state. Emit the current tag token. The attribute is reported with // an empty value range pointing at the `>` so the open-tag offset range // still includes the `>`. reportError("missing-attribute-value", pos, pos + 1, "warning"); 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. pos++; while (pos < len) { const c2 = input.charCodeAt(pos); if ( c2 === CC_QUOTATION_MARK || c2 === CC_AMPERSAND || c2 === CC_NULL ) { break; } pos++; } } break; // https://html.spec.whatwg.org/multipage/parsing.html#attribute-value-(single-quoted)-state case STATE_ATTRIBUTE_VALUE_SINGLE_QUOTED: // Consume the next input character: // U+0027 APOSTROPHE (') // Switch to the after attribute value (quoted) state. if (cc === CC_APOSTROPHE) { pos = emitAttribute(pos); state = STATE_AFTER_ATTRIBUTE_VALUE_QUOTED; } else if (cc === CC_AMPERSAND) { // U+0026 AMPERSAND (&) // Set the return state to the attribute value (single-quoted) // state. Switch to the character reference state. returnState = STATE_ATTRIBUTE_VALUE_SINGLE_QUOTED; state = STATE_CHARACTER_REFERENCE; pos++; } else if (cc === CC_NULL) { // U+0000 NULL: unexpected-null-character (append U+FFFD). reportError("unexpected-null-character", pos, pos + 1, "warning"); pos++; } else { // Fast-forward over the ordinary run of the quoted value. pos++; while (pos < len) { const c2 = input.charCodeAt(pos); if (c2 === CC_APOSTROPHE || c2 === CC_AMPERSAND || c2 === CC_NULL) { break; } pos++; } } break; // https://html.spec.whatwg.org/multipage/parsing.html#attribute-value-(unquoted)-state case STATE_ATTRIBUTE_VALUE_UNQUOTED: if (isSpace(cc)) { pos = emitAttribute(pos); state = STATE_BEFORE_ATTRIBUTE_NAME; // Reconsume so space is handled in BEFORE_ATTRIBUTE_NAME } else if (cc === CC_GREATER_THAN) { // U+003E GREATER-THAN SIGN (>) // This is a missing-attribute-value parse error. Switch to the data state. // Emit the current tag token. pos = emitAttribute(pos); if (input.charCodeAt(tagStart + 1) === CC_SOLIDUS) { state = STATE_DATA; pos = emitCloseTag(pos + 1); } else { const nextPos = emitOpenTag(pos + 1, false); state = nextPos > pos + 1 ? STATE_DATA : contentModeAfterOpenTag(); pos = nextPos; } } else if (cc === CC_AMPERSAND) { // U+0026 AMPERSAND (&) // Set the return state to the attribute value (unquoted) // state. Switch to the character reference state. returnState = STATE_ATTRIBUTE_VALUE_UNQUOTED; state = STATE_CHARACTER_REFERENCE; pos++; } else { // NULL -> unexpected-null-character; `"` `'` `<` `=` `` ` `` -> unexpected-character-in-unquoted-attribute-value. if (cc === CC_NULL) { reportError("unexpected-null-character", pos, pos + 1, "warning"); } else if ( cc === CC_QUOTATION_MARK || cc === CC_APOSTROPHE || cc === CC_LESS_THAN || cc === CC_EQUALS || cc === CC_GRAVE_ACCENT ) { reportError( "unexpected-character-in-unquoted-attribute-value", pos, pos + 1, "warning" ); } pos++; } break; // https://html.spec.whatwg.org/multipage/parsing.html#after-attribute-value-(quoted)-state case STATE_AFTER_ATTRIBUTE_VALUE_QUOTED: // Consume the next input character: // U+0009 CHARACTER TABULATION (tab) // U+000A LINE FEED (LF) // U+000C FORM FEED (FF) // U+0020 SPACE // Switch to the before attribute name state. if (isSpace(cc)) { state = STATE_BEFORE_ATTRIBUTE_NAME; pos++; } else if (cc === CC_SOLIDUS) { // U+002F SOLIDUS (/) // Switch to the self-closing start tag state. state = STATE_SELF_CLOSING_START_TAG; pos++; } else if (cc === CC_GREATER_THAN) { if (input.charCodeAt(tagStart + 1) === CC_SOLIDUS) { state = STATE_DATA; pos = emitCloseTag(pos + 1); } else { const nextPos = emitOpenTag(pos + 1, false); state = nextPos > pos + 1 ? STATE_DATA : contentModeAfterOpenTag(); pos = nextPos; } } else { // Anything else // This is a missing-whitespace-between-attributes parse error. Reconsume in // the before attribute name state. reportError( "missing-whitespace-between-attributes", pos, pos + 1, "warning" ); state = STATE_BEFORE_ATTRIBUTE_NAME; // Reconsume } break; // https://html.spec.whatwg.org/multipage/parsing.html#self-closing-start-tag-state case STATE_SELF_CLOSING_START_TAG: // Consume the next input character: // U+003E GREATER-THAN SIGN (>) // Set the self-closing flag of the current tag token. Switch to the data // state. Emit the current tag token. if (cc === CC_GREATER_THAN) { if (input.charCodeAt(tagStart + 1) === CC_SOLIDUS) { // An end tag emitted with the self-closing flag set is an // end-tag-with-trailing-solidus parse error. reportError( "end-tag-with-trailing-solidus", tagStart, pos + 1, "warning" ); state = STATE_DATA; pos = emitCloseTag(pos + 1); } else { pos = emitOpenTag(pos + 1, true); state = STATE_DATA; } } else { // Anything else // This is an unexpected-solidus-in-tag parse error. Reconsume in the before // attribute name state. reportError("unexpected-solidus-in-tag", pos, pos + 1, "warning"); state = STATE_BEFORE_ATTRIBUTE_NAME; // Reconsume } break; // https://html.spec.whatwg.org/multipage/parsing.html#markup-declaration-open-state case STATE_MARKUP_DECLARATION_OPEN: // If the next few characters are: // Two U+002D HYPHEN-MINUS characters (-) // Consume those two characters, create a comment token whose data // is the empty string, and switch to the comment start state. if ( cc === CC_HYPHEN_MINUS && input.charCodeAt(pos + 1) === CC_HYPHEN_MINUS ) { pos += 2; commentStart = tagStart; state = STATE_COMMENT_START; } else if ( // ASCII case-insensitive match for the word "DOCTYPE" // Consume those characters and switch to the DOCTYPE state. (cc === 0x44 || cc === 0x64) /* D or d */ && (input.charCodeAt(pos + 1) | 0x20) === 0x6f /* o */ && (input.charCodeAt(pos + 2) | 0x20) === 0x63 /* c */ && (input.charCodeAt(pos + 3) | 0x20) === 0x74 /* t */ && (input.charCodeAt(pos + 4) | 0x20) === 0x79 /* y */ && (input.charCodeAt(pos + 5) | 0x20) === 0x70 /* p */ && (input.charCodeAt(pos + 6) | 0x20) === 0x65 /* e */ ) { pos += 7; commentStart = tagStart; state = STATE_DOCTYPE; } else if ( // The string "[CDATA[" (the five uppercase letters "CDATA" with a // U+005B LEFT SQUARE BRACKET character before and after) // Consume those characters and switch to the CDATA section state. cc === CC_LEFT_SQUARE_BRACKET && input.charCodeAt(pos + 1) === 0x43 /* C */ && input.charCodeAt(pos + 2) === 0x44 /* D */ && input.charCodeAt(pos + 3) === 0x41 /* A */ && input.charCodeAt(pos + 4) === 0x54 /* T */ && input.charCodeAt(pos + 5) === 0x41 /* A */ && input.charCodeAt(pos + 6) === CC_LEFT_SQUARE_BRACKET ) { pos += 7; commentStart = tagStart; state = STATE_CDATA_SECTION; } else { // Anything else // This is an incorrectly-opened-comment parse error. Create a comment token // whose data is the empty string. Switch to the bogus comment state (don't // consume anything in the current state). reportError("incorrectly-opened-comment", tagStart, pos, "warning"); commentStart = tagStart; state = STATE_BOGUS_COMMENT; // Reconsume } break; // https://html.spec.whatwg.org/multipage/parsing.html#comment-start-state case STATE_COMMENT_START: // Consume the next input character: // U+002D HYPHEN-MINUS (-) // Switch to the comment start dash state. if (cc === CC_HYPHEN_MINUS) { state = STATE_COMMENT_START_DASH; pos++; } else if (cc === CC_GREATER_THAN) { // U+003E GREATER-THAN SIGN (>) // This is an abrupt-closing-of-empty-comment parse error. Switch to the // data state. Emit the current comment token. reportError( "abrupt-closing-of-empty-comment", pos, pos + 1, "warning" ); let nextPos = pos + 1; if (callbacks.comment !== undefined) { nextPos = callbacks.comment(input, commentStart, pos + 1); } state = STATE_DATA; textStart = nextPos; pos = nextPos; } else { // Anything else // Reconsume in the comment state. state = STATE_COMMENT; // Reconsume } break; // https://html.spec.whatwg.org/multipage/parsing.html#comment-start-dash-state case STATE_COMMENT_START_DASH: // Consume the next input character: // U+002D HYPHEN-MINUS (-) // Switch to the comment end state. if (cc === CC_HYPHEN_MINUS) { state = STATE_COMMENT_END; pos++; } else if (cc === CC_GREATER_THAN) { // U+003E GREATER-THAN SIGN (>) // This is an abrupt-closing-of-empty-comment parse error. Switch to the // data state. Emit the current comment token. reportError( "abrupt-closing-of-empty-comment", pos, pos + 1, "warning" ); let nextPos = pos + 1; if (callbacks.comment !== undefined) { nextPos = callbacks.comment(input, commentStart, pos + 1); } state = STATE_DATA; textStart = nextPos; pos = nextPos; } else { // Anything else // Append a U+002D HYPHEN-MINUS character (-) to the comment token's data. // Reconsume in the comment state. state = STATE_COMMENT; // Reconsume } break; // https://html.spec.whatwg.org/multipage/parsing.html#comment-state case STATE_COMMENT: // Consume the next input character: // U+003C LESS-THAN SIGN (<) // Append a U+003C LESS-THAN SIGN character to the comment token's data. Switch to the comment less-than sign state. if (cc === CC_LESS_THAN) { state = STATE_COMMENT_LESS_THAN_SIGN; pos++; } else if (cc === CC_HYPHEN_MINUS) { // Consume the next input character: // U+002D HYPHEN-MINUS (-) // Switch to the comment end dash state. state = STATE_COMMENT_END_DASH; pos++; } else { // U+0000 NULL: unexpected-null-character (append U+FFFD). if (cc === CC_NULL) { reportError("unexpected-null-character", pos, pos + 1, "warning"); } // Fast-forward ordinary comment 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_LESS_THAN || c2 === CC_HYPHEN_MINUS || c2 === CC_NULL ) { break; } pos++; } } break; // https://html.spec.whatwg.org/multipage/parsing.html#comment-end-dash-state case STATE_COMMENT_END_DASH: // Consume the next input character: // U+002D HYPHEN-MINUS (-) // Switch to the comment end state. if (cc === CC_HYPHEN_MINUS) { state = STATE_COMMENT_END; pos++; } else { // Anything else // Append a U+002D HYPHEN-MINUS character (-) to the comment token's data. // Reconsume in the comment state (so e.g. NULL and `<` are // handled there). state = STATE_COMMENT; // Reconsume } break; // https://html.spec.whatwg.org/multipage/parsing.html#comment-end-state case STATE_COMMENT_END: // Consume the next input character: // U+003E GREATER-THAN SIGN (>) // Switch to the data state. Emit the current comment token. if (cc === CC_GREATER_THAN) { let nextPos = pos + 1; if (callbacks.comment !== undefined) { nextPos = callbacks.comment(input, commentStart, pos + 1); } state = STATE_DATA; textStart = nextPos; pos = nextPos; } else if (cc === CC_EXCLAMATION_MARK) { // U+0021 EXCLAMATION MARK (!) // Switch to the comment end bang state. state = STATE_COMMENT_END_BANG; pos++; } else if (cc === CC_HYPHEN_MINUS) { pos++; } else { // Anything else // Append two U+002D HYPHEN-MINUS characters (-) to the comment token's // data. Reconsume in the comment state (so NULL and `<` are // handled there). state = STATE_COMMENT; // Reconsume } break; // https://html.spec.whatwg.org/multipage/parsing.html#comment-end-bang-state case STATE_COMMENT_END_BANG: // Consume the next input character: // U+002D HYPHEN-MINUS (-) // Append two U+002D HYPHEN-MINUS characters (-) and a U+0021 EXCLAMATION // MARK character (!) to the comment token's data. Switch to the comment end // dash state. if (cc === CC_HYPHEN_MINUS) { state = STATE_COMMENT_END_DASH; pos++; } else if (cc === CC_GREATER_THAN) { // U+003E GREATER-THAN SIGN (>) // This is an incorrectly-closed-comment parse error. Switch to the data // state. Emit the current comment token. reportError("incorrectly-closed-comment", pos, pos + 1, "warning"); let nextPos = pos + 1; if (callbacks.comment !== undefined) { nextPos = callbacks.comment(input, commentStart, pos + 1); } state = STATE_DATA; textStart = nextPos; pos = nextPos; } else { // Anything else // Append two U+002D HYPHEN-MINUS characters (-) and a U+0021 EXCLAMATION // MARK character (!) to the comment token's data. Reconsume in the comment // state (so NULL and `<` are handled there). state = STATE_COMMENT; // Reconsume } break; // https://html.spec.whatwg.org/multipage/parsing.html#bogus-comment-state case STATE_BOGUS_COMMENT: // Consume the next input character: // U+003E GREATER-THAN SIGN (>) // Switch to the data state. Emit the current comment token. if (cc === CC_GREATER_THAN) { let nextPos = pos + 1; if (callbacks.comment !== undefined) { nextPos = callbacks.comment(input, commentStart, pos + 1); } state = STATE_DATA; textStart = nextPos; pos = nextPos; } else { // U+0000 NULL: unexpected-null-character (append U+FFFD). if (cc === CC_NULL) { reportError("unexpected-null-character", pos, pos + 1, "warning"); } pos++; } break; // https://html.spec.whatwg.org/multipage/parsing.html#comment-less-than-sign-state case STATE_COMMENT_LESS_THAN_SIGN: // Consume the next input character: // U+0021 EXCLAMATION MARK (!) // Append the current input character to the comment token's data. Switch to // the comment less-than sign bang state. if (cc === CC_EXCLAMATION_MARK) { state = STATE_COMMENT_LESS_THAN_SIGN_BANG; pos++; } else if (cc === CC_LESS_THAN) { // U+003C LESS-THAN SIGN (<) // Append the current input character to the comment token's data. pos++; } else { // Anything else // Reconsume in the comment state. state = STATE_COMMENT; // Reconsume } break; // https://html.spec.whatwg.org/multipage/parsing.html#comment-less-than-sign-bang-state case STATE_COMMENT_LESS_THAN_SIGN_BANG: // Consume the next input character: // U+002D HYPHEN-MINUS (-) // Switch to the comment less-than sign bang dash state. if (cc === CC_HYPHEN_MINUS) { state = STATE_COMMENT_LESS_THAN_SIGN_BANG_DASH; pos++; } else { // Anything else // Reconsume in the comment state. state = STATE_COMMENT; // Reconsume } break; // https://html.spec.whatwg.org/multipage/parsing.html#comment-less-than-sign-bang-dash-state case STATE_COMMENT_LESS_THAN_SIGN_BANG_DASH: // Consume the next input character: // U+002D HYPHEN-MINUS (-) // Switch to the comment less-than sign bang dash dash state. if (cc === CC_HYPHEN_MINUS) { state = STATE_COMMENT_LESS_THAN_SIGN_BANG_DASH_DASH; pos++; } else { // Anything else // Reconsume in the comment end dash state. state = STATE_COMMENT_END_DASH; // Reconsume } break; // https://html.spec.whatwg.org/multipage/parsing.html#comment-less-than-sign-bang-dash-dash-state case STATE_COMMENT_LESS_THAN_SIGN_BANG_DASH_DASH: // Consume the next input character: // U+003E GREATER-THAN SIGN (>) // EOF // Reconsume in the comment end state. // Anything else // This is a nested-comment parse error. Reconsume in the comment end state. if (cc !== CC_GREATER_THAN) { reportError("nested-comment", pos, pos + 1, "warning"); } state = STATE_COMMENT_END; // Reconsume break; // https://html.spec.whatwg.org/multipage/parsing.html#doctype-state case STATE_DOCTYPE: // Consume the next input character: // U+0009 CHARACTER TABULATION (tab) // U+000A LINE FEED (LF) // U+000C FORM FEED (FF) // U+0020 SPACE // Switch to the before DOCTYPE name state. if (isSpace(cc)) { state = STATE_BEFORE_DOCTYPE_NAME; pos++; } else if (cc === CC_GREATER_THAN) { // U+003E GREATER-THAN SIGN (>) // Reconsume in the before DOCTYPE name state. state = STATE_BEFORE_DOCTYPE_NAME; } else { // Anything else // This is a missing-whitespace-before-doctype-name parse error. Reconsume // in the before DOCTYPE name state. reportError( "missing-whitespace-before-doctype-name", pos, pos + 1, "warning" ); state = STATE_BEFORE_DOCTYPE_NAME; } break; // https://html.spec.whatwg.org/multipage/parsing.html#before-doctype-name-state case STATE_BEFORE_DOCTYPE_NAME: // Consume the next input character: // U+0009 CHARACTER TABULATION (tab) // U+000A LINE FEED (LF) // U+000C FORM FEED (FF) // U+0020 SPACE // Ignore the character. if (isSpace(cc)) { pos++; } else if (cc === CC_NULL) { // U+0000 NULL // This is an unexpected-null-character parse error. Create a new DOCTYPE // token. Set the token's name to a U+FFFD REPLACEMENT CHARACTER character. // Switch to the DOCTYPE name state. reportError("unexpected-null-character", pos, pos + 1, "warning"); state = STATE_DOCTYPE_NAME; pos++; } else if (cc === CC_GREATER_THAN) { // U+003E GREATER-THAN SIGN (>) // This is a missing-doctype-name parse error. Create a new DOCTYPE token. // Set its force-quirks flag to on. Switch to the data state. Emit the // current token. reportError("missing-doctype-name", pos, pos + 1, "warning"); let nextPos = pos + 1; if (callbacks.doctype !== undefined) { nextPos = callbacks.doctype(input, commentStart, pos + 1); } state = STATE_DATA; textStart = nextPos; pos = nextPos; } else { // ASCII upper alpha // Create a new DOCTYPE token. Set the token's name to the lowercase version // of the current input character (add 0x0020 to the character's code // point). Switch to the DOCTYPE name state. // Anything else // Create a new DOCTYPE token. Set the token's name to the current input // character. Switch to the DOCTYPE name state. state = STATE_DOCTYPE_NAME; pos++; } break; // https://html.spec.whatwg.org/multipage/parsing.html#doctype-name-state case STATE_DOCTYPE_NAME: // Consume the next input character: // U+0009 CHARACTER TABULATION (tab) // U+000A LINE FEED (LF) // U+000C FORM FEED (FF) // U+0020 SPACE // Switch to the after DOCTYPE name state. if (isSpace(cc)) { state = STATE_AFTER_DOCTYPE_NAME; pos++; } else if (cc === CC_GREATER_THAN) { // U+003E GREATER-THAN SIGN (>) // Switch to the data state. Emit the current DOCTYPE token. let nextPos = pos + 1; if (callbacks.doctype !== undefined) { nextPos = callbacks.doctype(input, commentStart, pos + 1); } state = STATE_DATA; textStart = nextPos; pos = nextPos; } else if (cc === CC_NULL) { // U+0000 NULL // This is an unexpected-null-character parse error. Append a U+FFFD // REPLACEMENT CHARACTER character to the current DOCTYPE token's name. reportError("unexpected-null-character", pos, pos + 1, "warning"); pos++; } else { // ASCII upper alpha // Append the lowercase version of the current input character (add 0x0020 // to the character's code point) to the current DOCTYPE token's name. // Anything else // Append the current input character to the current DOCTYPE token's name. pos++; } break; // https://html.spec.whatwg.org/multipage/parsing.html#after-doctype-name-state case STATE_AFTER_DOCTYPE_NAME: // Consume the next input character: if (isSpace(cc)) { // U+0009 CHARACTER TABULATION (tab) // U+000A LINE FEED (LF) // U+000C FORM FEED (FF) // U+0020 SPACE // Ignore the character. pos++; } else if (cc === CC_GREATER_THAN) { // U+003E GREATER-THAN SIGN (>) // Switch to the data state. Emit the current DOCTYPE token. let nextPos = pos + 1; if (callbacks.doctype !== undefined) { nextPos = callbacks.doctype(input, commentStart, pos + 1); } state = STATE_DATA; textStart = nextPos; pos = nextPos; } else if ( pos + 5 < len && (cc === 0x50 || cc === 0x70) /* P or p */ && (input.charCodeAt(pos + 1) | 0x20) === 0x75 /* u */ && (input.charCodeAt(pos + 2) | 0x20) === 0x62 /* b */ && (input.charCodeAt(pos + 3) | 0x20) === 0x6c /* l */ && (input.charCodeAt(pos + 4) | 0x20) === 0x69 /* i */ && (input.charCodeAt(pos + 5) | 0x20) === 0x63 /* c */ ) { // ASCII case-insensitive match for the word "PUBLIC" pos += 6; state = STATE_AFTER_DOCTYPE_PUBLIC_KEYWORD; } else if ( pos + 5 < len && (cc === 0x53 || cc === 0x73) /* S or s */ && (input.charCodeAt(pos + 1) | 0x20) === 0x79 /* y */ && (input.charCodeAt(pos + 2) | 0x20) === 0x73 /* s */ && (input.charCodeAt(pos + 3) | 0x20) === 0x74 /* t */ && (input.charCodeAt(pos + 4) | 0x20) === 0x65 /* e */ && (input.charCodeAt(pos + 5) | 0x20) === 0x6d /* m */ ) { // ASCII case-insensitive match for the word "SYSTEM" pos += 6; state = STATE_AFTER_DOCTYPE_SYSTEM_KEYWORD; } else { // Anything else // This is an invalid-character-sequence-after-doctype-name parse error. Set // the current DOCTYPE token's force-quirks flag to on. Reconsume in the // bogus DOCTYPE state. reportError( "invalid-character-sequence-after-doctype-name", pos, pos + 1, "warning" ); state = STATE_BOGUS_DOCTYPE; } break; // https://html.spec.whatwg.org/multipage/parsing.html#after-doctype-public-keyword-state case STATE_AFTER_DOCTYPE_PUBLIC_KEYWORD: // Consume the next input character: if (isSpace(cc)) { // U+0009 CHARACTER TABULATION (tab) // U+000A LINE FEED (LF) // U+000C FORM FEED (FF) // U+0020 SPACE // Switch to the before DOCTYPE public identifier state. state = STATE_BEFORE_DOCTYPE_PUBLIC_IDENTIFIER; pos++; } else if (cc === CC_QUOTATION_MARK) { // U+0022 QUOTATION MARK (") // This is a missing-whitespace-after-doctype-public-keyword parse error. // Set the current DOCTYPE token's public identifier to the empty string // (not missing), then switch to the DOCTYPE public identifier // (double-quoted) state. reportError( "missing-whitespace-after-doctype-public-keyword", pos, pos + 1, "warning" ); state = STATE_DOCTYPE_PUBLIC_IDENTIFIER_DOUBLE_QUOTED; pos++; } else if (cc === CC_APOSTROPHE) { // U+0027 APOSTROPHE (') // This is a missing-whitespace-after-doctype-public-keyword parse error. // Set the current DOCTYPE token's public identifier to the empty string // (not missing), then switch to the DOCTYPE public identifier // (single-quoted) state. reportError( "missing-whitespace-after-doctype-public-keyword", pos, pos + 1, "warning" ); state = STATE_DOCTYPE_PUBLIC_IDENTIFIER_SINGLE_QUOTED; pos++; } else if (cc === CC_GREATER_THAN) { // U+003E GREATER-THAN SIGN (>) // This is a missing-doctype-public-identifier parse error. Set the current // DOCTYPE token's force-quirks flag to on. Switch to the data state. Emit // the current DOCTYPE token. reportError( "missing-doctype-public-identifier", pos, pos + 1, "warning" ); let nextPos = pos + 1; if (callbacks.doctype !== undefined) { nextPos = callbacks.doctype(input, commentStart, pos + 1); } state = STATE_DATA; textStart = nextPos; pos = nextPos; } else { // Anything else // This is a missing-quote-before-doctype-public-identifier parse error. Set // the current DOCTYPE token's force-quirks flag to on. Reconsume in the // bogus DOCTYPE state. reportError( "missing-quote-before-doctype-public-identifier", pos, pos + 1, "warning" ); state = STATE_BOGUS_DOCTYPE; } break; // https://html.spec.whatwg.org/multipage/parsing.html#before-doctype-public-identifier-state case STATE_BEFORE_DOCTYPE_PUBLIC_IDENTIFIER: // Consume the next input character: if (isSpace(cc)) { // U+0009 CHARACTER TABULATION (tab) // U+000A LINE FEED (LF) // U+000C FORM FEED (FF) // U+0020 SPACE // Ignore the character. pos++; } else if (cc === CC_QUOTATION_MARK) { // U+0022 QUOTATION MARK (") // Set the current DOCTYPE token's public identifier to the empty string // (not missing), then switch to the DOCTYPE public identifier // (double-quoted) state. state = STATE_DOCTYPE_PUBLIC_IDENTIFIER_DOUBLE_QUOTED; pos++; } else if (cc === CC_APOSTROPHE) { // U+0027 APOSTROPHE (') // Set the current DOCTYPE token's public identifier to the empty string // (not missing), then switch to the DOCTYPE public identifier // (single-quoted) state. state = STATE_DOCTYPE_PUBLIC_IDENTIFIER_SINGLE_QUOTED; pos++; } else if (cc === CC_GREATER_THAN) { // U+003E GREATER-THAN SIGN (>) // This is a missing-doctype-public-identifier parse error. Set the current // DOCTYPE token's force-quirks flag to on. Switch to the data state. Emit // the current DOCTYPE token. reportError( "missing-doctype-public-identifier", pos, pos + 1, "warning" ); let nextPos = pos + 1; if (callbacks.doctype !== undefined) { nextPos = callbacks.doctype(input, commentStart, pos + 1); } state = STATE_DATA; textStart = nextPos; pos = nextPos; } else { // Anything else // This is a missing-quote-before-doctype-public-identifier parse error. Set // the current DOCTYPE token's force-quirks flag to on. Reconsume in the // bogus DOCTYPE state. reportError( "missing-quote-before-doctype-public-identifier", pos, pos + 1, "warning" ); state = STATE_BOGUS_DOCTYPE; } break; // https://html.spec.whatwg.org/multipage/parsing.html#doctype-public-identifier-(double-quoted)-state case STATE_DOCTYPE_PUBLIC_IDENTIFIER_DOUBLE_QUOTED: // Consume the next input character: if (cc === CC_QUOTATION_MARK) { // U+0022 QUOTATION MARK (") // Switch to the after DOCTYPE public identifier state. state = STATE_AFTER_DOCTYPE_PUBLIC_IDENTIFIER; pos++; } else if (cc === CC_NULL) { // U+0000 NULL // This is an unexpected-null-character parse error. Append a U+FFFD // REPLACEMENT CHARACTER character to the current DOCTYPE token's public // identifier. reportError("unexpected-null-character", pos, pos + 1, "warning"); pos++; } else if (cc === CC_GREATER_THAN) { // U+003E GREATER-THAN SIGN (>) // This is an abrupt-doctype-public-identifier parse error. Set the current // DOCTYPE token's force-quirks flag to on. Switch to the data state. Emit // the current DOCTYPE token. reportError( "abrupt-doctype-public-identifier", pos, pos + 1, "warning" ); let nextPos = pos + 1; if (callbacks.doctype !== undefined) { nextPos = callbacks.doctype(input, commentStart, pos + 1); } state = STATE_DATA; textStart = nextPos; pos = nextPos; } else { // Anything else // Append the current input character to the current DOCTYPE token's public // identifier. pos++; } break; // https://html.spec.whatwg.org/multipage/parsing.html#doctype-public-identifier-(single-quoted)-state case STATE_DOCTYPE_PUBLIC_IDENTIFIER_SINGLE_QUOTED: // Consume the next input character: if (cc === CC_APOSTROPHE) { // U+0027 APOSTROPHE (') // Switch to the after DOCTYPE public identifier state. state = STATE_AFTER_DOCTYPE_PUBLIC_IDENTIFIER; pos++; } else if (cc === CC_NULL) { // U+0000 NULL // This is an unexpected-null-character parse error. Append a U+FFFD // REPLACEMENT CHARACTER character to the current DOCTYPE token's public // identifier. reportError("unexpected-null-character", pos, pos + 1, "warning"); pos++; } else if (cc === CC_GREATER_THAN) { // U+003E GREATER-THAN SIGN (>) // This is an abrupt-doctype-public-identifier parse error. Set the current // DOCTYPE token's force-quirks flag to on. Switch to the data state. Emit // the current DOCTYPE token. reportError( "abrupt-doctype-public-identifier", pos, pos + 1, "warning" ); let nextPos = pos + 1; if (callbacks.doctype !== undefined) { nextPos = callbacks.doctype(input, commentStart, pos + 1); } state = STATE_DATA; textStart = nextPos; pos = nextPos; } else { // Anything else // Append the current input character to the current DOCTYPE token's public // identifier. pos++; } break; // https://html.spec.whatwg.org/multipage/parsing.html#after-doctype-public-identifier-state case STATE_AFTER_DOCTYPE_PUBLIC_IDENTIFIER: // Consume the next input character: if (isSpace(cc)) { // U+0009 CHARACTER TABULATION (tab) // U+000A LINE FEED (LF) // U+000C FORM FEED (FF) // U+0020 SPACE // Switch to the between DOCTYPE public and system identifiers state. state = STATE_BETWEEN_DOCTYPE_PUBLIC_AND_SYSTEM_IDENTIFIERS; pos++; } else if (cc === CC_GREATER_THAN) { // U+003E GREATER-THAN SIGN (>) // Switch to the data state. Emit the current DOCTYPE token. let nextPos = pos + 1; if (callbacks.doctype !== undefined) { nextPos = callbacks.doctype(input, commentStart, pos + 1); } state = STATE_DATA; textStart = nextPos; pos = nextPos; } else if (cc === CC_QUOTATION_MARK) { // U+0022 QUOTATION MARK (") // This is a missing-whitespace-between-doctype-public-and-system-identifiers // parse error. Set the current DOCTYPE token's system // identifier to the empty string (not missing), then switch // to the DOCTYPE system identifier (double-quoted) state. reportError( "missing-whitespace-between-doctype-public-and-system-identifiers", pos, pos + 1, "warning" ); state = STATE_DOCTYPE_SYSTEM_IDENTIFIER_DOUBLE_QUOTED; pos++; } else if (cc === CC_APOSTROPHE) { // U+0027 APOSTROPHE (') // This is a missing-whitespace-between-doctype-public-and-system-identifiers // parse error. Set the current DOCTYPE token's system // identifier to the empty string (not missing), then switch // to the DOCTYPE system identifier (single-quoted) state. reportError( "missing-whitespace-between-doctype-public-and-system-identifiers", pos, pos + 1, "warning" ); state = STATE_DOCTYPE_SYSTEM_IDENTIFIER_SINGLE_QUOTED; pos++; } else { // Anything else // This is a missing-quote-before-doctype-system-identifier parse error. Set // the current DOCTYPE token's force-quirks flag to on. Reconsume in the // bogus DOCTYPE state. reportError( "missing-quote-before-doctype-system-identifier", pos, pos + 1, "warning" ); state = STATE_BOGUS_DOCTYPE; } break; // https://html.spec.whatwg.org/multipage/parsing.html#between-doctype-public-and-system-identifiers-state case STATE_BETWEEN_DOCTYPE_PUBLIC_AND_SYSTEM_IDENTIFIERS: // Consume the next input character: if (isSpace(cc)) { // U+0009 CHARACTER TABULATION (tab) // U+000A LINE FEED (LF) // U+000C FORM FEED (FF) // U+0020 SPACE // Ignore the character. pos++; } else if (cc === CC_GREATER_THAN) { // U+003E GREATER-THAN SIGN (>) // Switch to the data state. Emit the current DOCTYPE token. let nextPos = pos + 1; if (callbacks.doctype !== undefined) { nextPos = callbacks.doctype(input, commentStart, pos + 1); } state = STATE_DATA; textStart = nextPos; pos = nextPos; } else if (cc === CC_QUOTATION_MARK) { // U+0022 QUOTATION MARK (") // Set the current DOCTYPE token's system identifier to the empty string // (not missing), then switch to the DOCTYPE system identifier // (double-quoted) state. state = STATE_DOCTYPE_SYSTEM_IDENTIFIER_DOUBLE_QUOTED; pos++; } else if (cc === CC_APOSTROPHE) { // U+0027 APOSTROPHE (') // Set the current DOCTYPE token's system identifier to the empty string // (not missing), then switch to the DOCTYPE system identifier // (single-quoted) state. state = STATE_DOCTYPE_SYSTEM_IDENTIFIER_SINGLE_QUOTED; pos++; } else { // Anything else // This is a missing-quote-before-doctype-system-identifier parse error. Set // the current DOCTYPE token's force-quirks flag to on. Reconsume in the // bogus DOCTYPE state. reportError( "missing-quote-before-doctype-system-identifier", pos, pos + 1, "warning" ); state = STATE_BOGUS_DOCTYPE; } break; // https://html.spec.whatwg.org/multipage/parsing.html#after-doctype-system-keyword-state case STATE_AFTER_DOCTYPE_SYSTEM_KEYWORD: // Consume the next input character: if (isSpace(cc)) { // U+0009 CHARACTER TABULATION (tab) // U+000A LINE FEED (LF) // U+000C FORM FEED (FF) // U+0020 SPACE // Switch to the before DOCTYPE system identifier state. state = STATE_BEFORE_DOCTYPE_SYSTEM_IDENTIFIER; pos++; } else if (cc === CC_QUOTATION_MARK) { // U+0022 QUOTATION MARK (") // This is a missing-whitespace-after-doctype-system-keyword parse error. // Set the current DOCTYPE token's system identifier to the empty string // (not missing), then switch to the DOCTYPE system identifier // (double-quoted) state. reportError( "missing-whitespace-after-doctype-system-keyword", pos, pos + 1, "warning" ); state = STATE_DOCTYPE_SYSTEM_IDENTIFIER_DOUBLE_QUOTED; pos++; } else if (cc === CC_APOSTROPHE) { // U+0027 APOSTROPHE (') // This is a missing-whitespace-after-doctype-system-keyword parse error. // Set the current DOCTYPE token's system identifier to the empty string // (not missing), then switch to the DOCTYPE system identifier // (single-quoted) state. reportError( "missing-whitespace-after-doctype-system-keyword", pos, pos + 1, "warning" ); state = STATE_DOCTYPE_SYSTEM_IDENTIFIER_SINGLE_QUOTED; pos++; } else if (cc === CC_GREATER_THAN) { // U+003E GREATER-THAN SIGN (>) // This is a missing-doctype-system-identifier parse error. Set the current // DOCTYPE token's force-quirks flag to on. Switch to the data state. Emit // the current DOCTYPE token. reportError( "missing-doctype-system-identifier", pos, pos + 1, "warning" ); let nextPos = pos + 1; if (callbacks.doctype !== undefined) { nextPos = callbacks.doctype(input, commentStart, pos + 1); } state = STATE_DATA; textStart = nextPos; pos = nextPos; } else { // Anything else // This is a missing-quote-before-doctype-system-identifier parse error. Set // the current DOCTYPE token's force-quirks flag to on. Reconsume in the // bogus DOCTYPE state. reportError( "missing-quote-before-doctype-system-identifier", pos, pos + 1, "warning" ); state = STATE_BOGUS_DOCTYPE; } break; // https://html.spec.whatwg.org/multipage/parsing.html#before-doctype-system-identifier-state case STATE_BEFORE_DOCTYPE_SYSTEM_IDENTIFIER: // Consume the next input character: if (isSpace(cc)) { // U+0009 CHARACTER TABULATION (tab) // U+000A LINE FEED (LF) // U+000C FORM FEED (FF) // U+0020 SPACE // Ignore the character. pos++; } else if (cc === CC_QUOTATION_MARK) { // U+0022 QUOTATION MARK (") // Set the current DOCTYPE token's system identifier to the empty string // (not missing), then switch to the DOCTYPE system identifier // (double-quoted) state. state = STATE_DOCTYPE_SYSTEM_IDENTIFIER_DOUBLE_QUOTED; pos++; } else if (cc === CC_APOSTROPHE) { // U+0027 APOSTROPHE (') // Set the current DOCTYPE token's system identifier to the empty string // (not missing), then switch to the DOCTYPE system identifier // (single-quoted) state. state = STATE_DOCTYPE_SYSTEM_IDENTIFIER_SINGLE_QUOTED; pos++; } else if (cc === CC_GREATER_THAN) { // U+003E GREATER-THAN SIGN (>) // This is a missing-doctype-system-identifier parse error. Set the current // DOCTYPE token's force-quirks flag to on. Switch to the data state. Emit // the current DOCTYPE token. reportError( "missing-doctype-system-identifier", pos, pos + 1, "warning" ); let nextPos = pos + 1; if (callbacks.doctype !== undefined) { nextPos = callbacks.doctype(input, commentStart, pos + 1); } state = STATE_DATA; textStart = nextPos; pos = nextPos; } else { // Anything else // This is a missing-quote-before-doctype-system-identifier parse error. Set // the current DOCTYPE token's force-quirks flag to on. Reconsume in the // bogus DOCTYPE state. reportError( "missing-quote-before-doctype-system-identifier", pos, pos + 1, "warning" ); state = STATE_BOGUS_DOCTYPE; } break; // https://html.spec.whatwg.org/multipage/parsing.html#doctype-system-identifier-(double-quoted)-state case STATE_DOCTYPE_SYSTEM_IDENTIFIER_DOUBLE_QUOTED: // Consume the next input character: if (cc === CC_QUOTATION_MARK) { // U+0022 QUOTATION MARK (") // Switch to the after DOCTYPE system identifier state. state = STATE_AFTER_DOCTYPE_SYSTEM_IDENTIFIER; pos++; } else if (cc === CC_NULL) { // U+0000 NULL // This is an unexpected-null-character parse error. Append a U+FFFD // REPLACEMENT CHARACTER character to the current DOCTYPE token's system // identifier. reportError("unexpected-null-character", pos, pos + 1, "warning"); pos++; } else if (cc === CC_GREATER_THAN) { // U+003E GREATER-THAN SIGN (>) // This is an abrupt-doctype-system-identifier parse error. Set the current // DOCTYPE token's force-quirks flag to on. Switch to the data state. Emit // the current DOCTYPE token. reportError( "abrupt-doctype-system-identifier", pos, pos + 1, "warning" ); let nextPos = pos + 1; if (callbacks.doctype !== undefined) { nextPos = callbacks.doctype(input, commentStart, pos + 1); } state = STATE_DATA; textStart = nextPos; pos = nextPos; } else { // Anything else // Append the current input character to the current DOCTYPE token's system // identifier. pos++; } break; // https://html.spec.whatwg.org/multipage/parsing.html#doctype-system-identifier-(single-quoted)-state case STATE_DOCTYPE_SYSTEM_IDENTIFIER_SINGLE_QUOTED: // Consume the next input character: if (cc === CC_APOSTROPHE) { // U+0027 APOSTROPHE (') // Switch to the after DOCTYPE system identifier state. state = STATE_AFTER_DOCTYPE_SYSTEM_IDENTIFIER; pos++; } else if (cc === CC_NULL) { // U+0000 NULL // This is an unexpected-null-character parse error. Append a U+FFFD // REPLACEMENT CHARACTER character to the current DOCTYPE token's system // identifier. reportError("unexpected-null-character", pos, pos + 1, "warning"); pos++; } else if (cc === CC_GREATER_THAN) { // U+003E GREATER-THAN SIGN (>) // This is an abrupt-doctype-system-identifier parse error. Set the current // DOCTYPE token's force-quirks flag to on. Switch to the data state. Emit // the current DOCTYPE token. reportError( "abrupt-doctype-system-identifier", pos, pos + 1, "warning" ); let nextPos = pos + 1; if (callbacks.doctype !== undefined) { nextPos = callbacks.doctype(input, commentStart, pos + 1); } state = STATE_DATA; textStart = nextPos; pos = nextPos; } else { // Anything else // Append the current input character to the current DOCTYPE token's system // identifier. pos++; } break; // https://html.spec.whatwg.org/multipage/parsing.html#after-doctype-system-identifier-state case STATE_AFTER_DOCTYPE_SYSTEM_IDENTIFIER: // Consume the next input character: if (isSpace(cc)) { // U+0009 CHARACTER TABULATION (tab) // U+000A LINE FEED (LF) // U+000C FORM FEED (FF) // U+0020 SPACE // Ignore the character. pos++; } else if (cc === CC_GREATER_THAN) { // U+003E GREATER-THAN SIGN (>) // Switch to the data state. Emit the current DOCTYPE token. let nextPos = pos + 1; if (callbacks.doctype !== undefined) { nextPos = callbacks.doctype(input, commentStart, pos + 1); } state = STATE_DATA; textStart = nextPos; pos = nextPos; } else { // Anything else // This is an unexpected-character-after-doctype-system-identifier parse // error. Reconsume in the bogus DOCTYPE state. (This does not set the // current DOCTYPE token's force-quirks flag to on.) reportError( "unexpected-character-after-doctype-system-identifier", pos, pos + 1, "warning" ); state = STATE_BOGUS_DOCTYPE; } break; // https://html.spec.whatwg.org/multipage/parsing.html#bogus-doctype-state case STATE_BOGUS_DOCTYPE: // Consume the next input character: if (cc === CC_GREATER_THAN) { // U+003E GREATER-THAN SIGN (>) // Switch to the data state. Emit the DOCTYPE token. let nextPos = pos + 1; if (callbacks.doctype !== undefined) { nextPos = callbacks.doctype(input, commentStart, pos + 1); } state = STATE_DATA; textStart = nextPos; pos = nextPos; } else if (cc === CC_NULL) { // U+0000 NULL // This is an unexpected-null-character parse error. Ignore the character. reportError("unexpected-null-character", pos, pos + 1, "warning"); pos++; } else { // Anything else // Ignore the character. pos++; } break; // https://html.spec.whatwg.org/multipage/parsing.html#cdata-section-state case STATE_CDATA_SECTION: // Consume the next input character: // U+005D RIGHT SQUARE BRACKET (]) // Switch to the CDATA section bracket state. if (cc === CC_RIGHT_SQUARE_BRACKET) { state = STATE_CDATA_SECTION_BRACKET; pos++; } else { // Anything else // Emit the current input character as a character token. pos++; } break; // https://html.spec.whatwg.org/multipage/parsing.html#cdata-section-bracket-state case STATE_CDATA_SECTION_BRACKET: // Consume the next input character: // U+005D RIGHT SQUARE BRACKET (]) // Switch to the CDATA section end state. if (cc === CC_RIGHT_SQUARE_BRACKET) { state = STATE_CDATA_SECTION_END; pos++; } else { // Anything else // Emit a U+005D RIGHT SQUARE BRACKET character token. Reconsume in the // CDATA section state. state = STATE_CDATA_SECTION; // Reconsume } break; // https://html.spec.whatwg.org/multipage/parsing.html#cdata-section-end-state case STATE_CDATA_SECTION_END: // Consume the next input character: // U+005D RIGHT SQUARE BRACKET (]) // Emit a U+005D RIGHT SQUARE BRACKET character token. if (cc === CC_RIGHT_SQUARE_BRACKET) { pos++; } else if (cc === CC_GREATER_THAN) { // U+003E GREATER-THAN SIGN (>) // Switch to the data state. let nextPos = pos + 1; if (callbacks.comment !== undefined) { nextPos = callbacks.comment(input, commentStart, pos + 1); } state = STATE_DATA; textStart = nextPos; pos = nextPos; } else { // Anything else // Emit two U+005D RIGHT SQUARE BRACKET character tokens. Reconsume in the // CDATA section state. state = STATE_CDATA_SECTION; // Reconsume } break; // https://html.spec.whatwg.org/multipage/parsing.html#rcdata-state case STATE_RCDATA: // Consume the next input character: if (cc === CC_AMPERSAND) { // U+0026 AMPERSAND (&) // Set the return state to the RCDATA state. Switch to the // character reference state. (RCDATA processes references; // RAWTEXT/script/PLAINTEXT do not.) returnState = STATE_RCDATA; state = STATE_CHARACTER_REFERENCE; pos++; } else if (cc === CC_LESS_THAN) { // U+003C LESS-THAN SIGN (<) // Switch to the RCDATA less-than sign state. tagStart = pos; state = STATE_RCDATA_LESS_THAN_SIGN; pos++; } else if (cc === CC_NULL) { // U+0000 NULL is an unexpected-null-character parse error. reportError("unexpected-null-character", pos, pos + 1, "warning"); pos++; } else { // Fast-forward over ordinary RCDATA text. pos++; while (pos < len) { const c2 = input.charCodeAt(pos); if (c2 === CC_AMPERSAND || c2 === CC_LESS_THAN || c2 === CC_NULL) { break; } pos++; } } break; // https://html.spec.whatwg.org/multipage/parsing.html#rcdata-less-than-sign-state case STATE_RCDATA_LESS_THAN_SIGN: // Consume the next input character: // U+002F SOLIDUS (/) // Switch to the RCDATA end tag open state. (Spec sets a // temporary buffer here; we track the would-be content via // offset ranges instead.) if (cc === CC_SOLIDUS) { state = STATE_RCDATA_END_TAG_OPEN; pos++; } else { // Anything else // Emit a U+003C LESS-THAN SIGN character token. Reconsume in the RCDATA // state. state = STATE_RCDATA; // Reconsume } break; // https://html.spec.whatwg.org/multipage/parsing.html#rcdata-end-tag-open-state case STATE_RCDATA_END_TAG_OPEN: // Consume the next input character: // ASCII alpha // Create a new end tag token, set its tag name to the empty string. // Reconsume in the RCDATA end tag name state. if (isAsciiAlpha(cc)) { tagNameStart = pos; state = STATE_RCDATA_END_TAG_NAME; // Reconsume } else { // Anything else // Emit a U+003C LESS-THAN SIGN character token and a U+002F SOLIDUS // character token. Reconsume in the RCDATA state. state = STATE_RCDATA; // Reconsume } break; // https://html.spec.whatwg.org/multipage/parsing.html#rcdata-end-tag-name-state case STATE_RCDATA_END_TAG_NAME: // Consume the next input character: // U+0009 CHARACTER TABULATION (tab) // U+000A LINE FEED (LF) // U+000C FORM FEED (FF) // U+0020 SPACE // If the current end tag token is an appropriate end tag token, then switch // to the before attribute name state. Otherwise, treat it as per the // "anything else" entry below. if (isSpace(cc)) { tagNameEnd = pos; if ( rangeEqualsLower(input, tagNameStart, tagNameEnd, lastOpenTagName) ) { flushText(tagStart); state = STATE_BEFORE_ATTRIBUTE_NAME; pos++; } else { state = STATE_RCDATA; // Reconsume } } else if (cc === CC_SOLIDUS) { // U+002F SOLIDUS (/) // If the current end tag token is an appropriate end tag token, then switch // to the self-closing start tag state. Otherwise, treat it as per the // "anything else" entry below. tagNameEnd = pos; if ( rangeEqualsLower(input, tagNameStart, tagNameEnd, lastOpenTagName) ) { flushText(tagStart); state = STATE_SELF_CLOSING_START_TAG; pos++; } else { state = STATE_RCDATA; // Reconsume } } else if (cc === CC_GREATER_THAN) { // U+003E GREATER-THAN SIGN (>) // If the current end tag token is an appropriate end tag token, then switch // to the data state and emit the current tag token. Otherwise, treat it as // per the "anything else" entry below. tagNameEnd = pos; if ( rangeEqualsLower(input, tagNameStart, tagNameEnd, lastOpenTagName) ) { flushText(tagStart); state = STATE_DATA; pos = emitCloseTag(pos + 1); } else { state = STATE_RCDATA; // Reconsume } } else if (isAsciiAlpha(cc)) { // ASCII upper alpha / ASCII lower alpha // Append the lowercase version of the current input character to the // current tag token's tag name. Append the current input character to // the temporary buffer. pos++; } else { // Anything else // Emit a U+003C LESS-THAN SIGN character token, a U+002F SOLIDUS character // token, and a character token for each of the characters in the temporary // buffer (in the order they were added to the buffer). Reconsume in the // RCDATA state. state = STATE_RCDATA; // Reconsume } break; // https://html.spec.whatwg.org/multipage/parsing.html#rawtext-state case STATE_RAWTEXT: // Consume the next input character: // U+003C LESS-THAN SIGN (<) // Switch to the RAWTEXT less-than sign state. if (cc === CC_LESS_THAN) { tagStart = pos; state = STATE_RAWTEXT_LESS_THAN_SIGN; pos++; } else if (cc === CC_NULL) { // U+0000 NULL is an unexpected-null-character parse error. reportError("unexpected-null-character", pos, pos + 1, "warning"); pos++; } else { // Fast-forward over ordinary RAWTEXT text. pos++; while (pos < len) { const c2 = input.charCodeAt(pos); if (c2 === CC_LESS_THAN || c2 === CC_NULL) break; pos++; } } break; // https://html.spec.whatwg.org/multipage/parsing.html#rawtext-less-than-sign-state case STATE_RAWTEXT_LESS_THAN_SIGN: // Consume the next input character: // U+002F SOLIDUS (/) // Switch to the RAWTEXT end tag open state. (Spec sets a // temporary buffer here; we track via offset ranges instead.) if (cc === CC_SOLIDUS) { state = STATE_RAWTEXT_END_TAG_OPEN; pos++; } else { // Anything else // Emit a U+003C LESS-THAN SIGN character token. Reconsume in the RAWTEXT // state. state = STATE_RAWTEXT; // Reconsume } break; // https://html.spec.whatwg.org/multipage/parsing.html#rawtext-end-tag-open-state case STATE_RAWTEXT_END_TAG_OPEN: // Consume the next input character: // ASCII alpha // Create a new end tag token, set its tag name to the empty string. // Reconsume in the RAWTEXT end tag name state. if (isAsciiAlpha(cc)) { tagNameStart = pos; state = STATE_RAWTEXT_END_TAG_NAME; // Reconsume } else { // Anything else // Emit a U+003C LESS-THAN SIGN character token and a U+002F SOLIDUS // character token. Reconsume in the RAWTEXT state. state = STATE_RAWTEXT; // Reconsume } break; // https://html.spec.whatwg.org/multipage/parsing.html#rawtext-end-tag-name-state case STATE_RAWTEXT_END_TAG_NAME: // Consume the next input character: // U+0009 CHARACTER TABULATION (tab) // U+000A LINE FEED (LF) // U+000C FORM FEED (FF) // U+0020 SPACE // If the current end tag token is an appropriate end tag token, then switch // to the before attribute name state. Otherwise, treat it as per the // "anything else" entry below. if (isSpace(cc)) { tagNameEnd = pos; if ( rangeEqualsLower(input, tagNameStart, tagNameEnd, lastOpenTagName) ) { flushText(tagStart); state = STATE_BEFORE_ATTRIBUTE_NAME; pos++; } else { state = STATE_RAWTEXT; } } else if (cc === CC_SOLIDUS) { // U+002F SOLIDUS (/) // If the current end tag token is an appropriate end tag token, then switch // to the self-closing start tag state. Otherwise, treat it as per the // "anything else" entry below. tagNameEnd = pos; if ( rangeEqualsLower(input, tagNameStart, tagNameEnd, lastOpenTagName) ) { flushText(tagStart); state = STATE_SELF_CLOSING_START_TAG; pos++; } else { state = STATE_RAWTEXT; } } else if (cc === CC_GREATER_THAN) { // U+003E GREATER-THAN SIGN (>) // If the current end tag token is an appropriate end tag token, then switch // to the data state and emit the current tag token. Otherwise, treat it as // per the "anything else" entry below. tagNameEnd = pos; if ( rangeEqualsLower(input, tagNameStart, tagNameEnd, lastOpenTagName) ) { flushText(tagStart); state = STATE_DATA; pos = emitCloseTag(pos + 1); } else { state = STATE_RAWTEXT; } } else if (isAsciiAlpha(cc)) { // ASCII upper alpha / ASCII lower alpha // Append the lowercase version of the current input character to the // current tag token's tag name. Append the current input character to // the temporary buffer. pos++; } else { // Anything else // Emit a U+003C LESS-THAN SIGN character token, a U+002F SOLIDUS character // token, and a character token for each of the characters in the temporary // buffer (in the order they were added to the buffer). Reconsume in the // RAWTEXT state. state = STATE_RAWTEXT; // Reconsume } break; // https://html.spec.whatwg.org/multipage/parsing.html#script-data-state case STATE_SCRIPT_DATA: // Consume the next input character: // U+003C LESS-THAN SIGN (<) // Switch to the script data less-than sign state. if (cc === CC_LESS_THAN) { tagStart = pos; state = STATE_SCRIPT_DATA_LESS_THAN_SIGN; pos++; } else if (cc === CC_NULL) { // U+0000 NULL is an unexpected-null-character parse error. reportError("unexpected-null-character", pos, pos + 1, "warning"); pos++; } else { // Fast-forward over ordinary script-data text. pos++; while (pos < len) { const c2 = input.charCodeAt(pos); if (c2 === CC_LESS_THAN || c2 === CC_NULL) break; pos++; } } break; // https://html.spec.whatwg.org/multipage/parsing.html#script-data-less-than-sign-state case STATE_SCRIPT_DATA_LESS_THAN_SIGN: // Consume the next input character: // U+002F SOLIDUS (/) // Switch to the script data end tag open state. (Spec sets a // temporary buffer here; we track via offset ranges instead.) if (cc === CC_SOLIDUS) { state = STATE_SCRIPT_DATA_END_TAG_OPEN; pos++; } else if (cc === CC_EXCLAMATION_MARK) { // U+0021 EXCLAMATION MARK (!) // Switch to the script data escape start state. Emit a U+003C LESS-THAN // SIGN character token and a U+0021 EXCLAMATION MARK character token. state = STATE_SCRIPT_DATA_ESCAPE_START; pos++; } else { // Anything else // Emit a U+003C LESS-THAN SIGN character token. Reconsume in the script // data state. state = STATE_SCRIPT_DATA; // Reconsume } break; // https://html.spec.whatwg.org/multipage/parsing.html#script-data-end-tag-open-state case STATE_SCRIPT_DATA_END_TAG_OPEN: // Consume the next input character: // ASCII alpha // Create a new end tag token, set its tag name to the empty string. // Reconsume in the script data end tag name state. if (isAsciiAlpha(cc)) { tagNameStart = pos; state = STATE_SCRIPT_DATA_END_TAG_NAME; // Reconsume } else { // Anything else // Emit a U+003C LESS-THAN SIGN character token and a U+002F SOLIDUS // character token. Reconsume in the script data state. state = STATE_SCRIPT_DATA; // Reconsume } break; // https://html.spec.whatwg.org/multipage/parsing.html#script-data-end-tag-name-state case STATE_SCRIPT_DATA_END_TAG_NAME: // Consume the next input character: // U+0009 CHARACTER TABULATION (tab) // U+000A LINE FEED (LF) // U+000C FORM FEED (FF) // U+0020 SPACE // If the current end tag token is an appropriate end tag token, then switch // to the before attribute name state. Otherwise, treat it as per the // "anything else" entry below. if (isSpace(cc)) { tagNameEnd = pos; if ( rangeEqualsLower(input, tagNameStart, tagNameEnd, lastOpenTagName) ) { flushText(tagStart); state = STATE_BEFORE_ATTRIBUTE_NAME; pos++; } else { state = STATE_SCRIPT_DATA; } } else if (cc === CC_SOLIDUS) { // U+002F SOLIDUS (/) // If the current end tag token is an appropriate end tag token, then switch // to the self-closing start tag state. Otherwise, treat it as per the // "anything else" entry below. tagNameEnd = pos; if ( rangeEqualsLower(input, tagNameStart, tagNameEnd, lastOpenTagName) ) { flushText(tagStart); state = STATE_SELF_CLOSING_START_TAG; pos++; } else { state = STATE_SCRIPT_DATA; } } else if (cc === CC_GREATER_THAN) { // U+003E GREATER-THAN SIGN (>) // If the current end tag token is an appropriate end tag token, then switch // to the data state and emit the current tag token. Otherwise, treat it as // per the "anything else" entry below. tagNameEnd = pos; if ( rangeEqualsLower(input, tagNameStart, tagNameEnd, lastOpenTagName) ) { flushText(tagStart); state = STATE_DATA; pos = emitCloseTag(pos + 1); } else { state = STATE_SCRIPT_DATA; } } else if (isAsciiAlpha(cc)) { // ASCII upper alpha / ASCII lower alpha pos++; } else { // Anything else // Emit a U+003C LESS-THAN SIGN character token, a U+002F SOLIDUS character // token, and a character token for each of the characters in the temporary // buffer (in the order they were added to the buffer). Reconsume in the // script data state. state = STATE_SCRIPT_DATA; // Reconsume } break; // https://html.spec.whatwg.org/multipage/parsing.html#script-data-escape-start-state case STATE_SCRIPT_DATA_ESCAPE_START: // Consume the next input character: // U+002D HYPHEN-MINUS (-) // Switch to the script data escape start dash state. Emit a U+002D // HYPHEN-MINUS character token. if (cc === CC_HYPHEN_MINUS) { state = STATE_SCRIPT_DATA_ESCAPE_START_DASH; pos++; } else { // Anything else // Reconsume in the script data state. state = STATE_SCRIPT_DATA; // Reconsume } break; // https://html.spec.whatwg.org/multipage/parsing.html#script-data-escape-start-dash-state case STATE_SCRIPT_DATA_ESCAPE_START_DASH: // Consume the next input character: // U+002D HYPHEN-MINUS (-) // Switch to the script data escaped dash dash state. Emit a U+002D // HYPHEN-MINUS character token. if (cc === CC_HYPHEN_MINUS) { state = STATE_SCRIPT_DATA_ESCAPED_DASH_DASH; pos++; } else { // Anything else // Reconsume in the script data state. state = STATE_SCRIPT_DATA; // Reconsume } break; // https://html.spec.whatwg.org/multipage/parsing.html#script-data-escaped-state case STATE_SCRIPT_DATA_ESCAPED: // Consume the next input character: // U+002D HYPHEN-MINUS (-) // Switch to the script data escaped dash state. Emit a U+002D HYPHEN-MINUS // character token. if (cc === CC_HYPHEN_MINUS) { state = STATE_SCRIPT_DATA_ESCAPED_DASH; pos++; } else if (cc === CC_LESS_THAN) { // U+003C LESS-THAN SIGN (<) // Switch to the script data escaped less-than sign state. tagStart = pos; state = STATE_SCRIPT_DATA_ESCAPED_LESS_THAN_SIGN; pos++; } else { // Anything else // Emit the current input character as a character token. // U+0000 NULL is an unexpected-null-character parse error. if (cc === CC_NULL) { reportError("unexpected-null-character", pos, pos + 1, "warning"); } pos++; } break; // https://html.spec.whatwg.org/multipage/parsing.html#script-data-escaped-dash-state case STATE_SCRIPT_DATA_ESCAPED_DASH: // Consume the next input character: // U+002D HYPHEN-MINUS (-) // Switch to the script data escaped dash dash state. Emit a U+002D // HYPHEN-MINUS character token. if (cc === CC_HYPHEN_MINUS) { state = STATE_SCRIPT_DATA_ESCAPED_DASH_DASH; pos++; } else if (cc === CC_LESS_THAN) { // U+003C LESS-THAN SIGN (<) // Switch to the script data escaped less-than sign state. tagStart = pos; state = STATE_SCRIPT_DATA_ESCAPED_LESS_THAN_SIGN; pos++; } else { // Anything else // Switch to the script data escaped state. Emit the current input character // as a character token. U+0000 NULL is an unexpected-null-character error. if (cc === CC_NULL) { reportError("unexpected-null-character", pos, pos + 1, "warning"); } state = STATE_SCRIPT_DATA_ESCAPED; pos++; } break; // https://html.spec.whatwg.org/multipage/parsing.html#script-data-escaped-dash-dash-state case STATE_SCRIPT_DATA_ESCAPED_DASH_DASH: // Consume the next input character: // U+002D HYPHEN-MINUS (-) // Emit a U+002D HYPHEN-MINUS character token. if (cc === CC_HYPHEN_MINUS) { pos++; } else if (cc === CC_LESS_THAN) { // U+003C LESS-THAN SIGN (<) // Switch to the script data escaped less-than sign state. tagStart = pos; state = STATE_SCRIPT_DATA_ESCAPED_LESS_THAN_SIGN; pos++; } else if (cc === CC_GREATER_THAN) { // U+003E GREATER-THAN SIGN (>) // Switch to the script data state. Emit a U+003E GREATER-THAN SIGN // character token. state = STATE_SCRIPT_DATA; pos++; } else { // Anything else // Switch to the script data escaped state. Emit the current input character // as a character token. U+0000 NULL is an unexpected-null-character error. if (cc === CC_NULL) { reportError("unexpected-null-character", pos, pos + 1, "warning"); } state = STATE_SCRIPT_DATA_ESCAPED; pos++; } break; // https://html.spec.whatwg.org/multipage/parsing.html#script-data-escaped-less-than-sign-state case STATE_SCRIPT_DATA_ESCAPED_LESS_THAN_SIGN: // Consume the next input character: // U+002F SOLIDUS (/) // Switch to the script data escaped end tag open state. // (Spec sets a temporary buffer; we track via offset ranges.) if (cc === CC_SOLIDUS) { state = STATE_SCRIPT_DATA_ESCAPED_END_TAG_OPEN; pos++; } else if (isAsciiAlpha(cc)) { // ASCII alpha // Set the temporary buffer to the empty string. Emit a U+003C LESS-THAN // SIGN character token. Reconsume in the script data double escape start // state. scriptMatch = 0; state = STATE_SCRIPT_DATA_DOUBLE_ESCAPE_START; // Reconsume } else { // Anything else // Emit a U+003C LESS-THAN SIGN character token. Reconsume in the script // data escaped state. state = STATE_SCRIPT_DATA_ESCAPED; // Reconsume } break; // https://html.spec.whatwg.org/multipage/parsing.html#script-data-escaped-end-tag-open-state case STATE_SCRIPT_DATA_ESCAPED_END_TAG_OPEN: // Consume the next input character: // ASCII alpha // Create a new end tag token, set its tag name to the empty string. // Reconsume in the script data escaped end tag name state. if (isAsciiAlpha(cc)) { tagNameStart = pos; state = STATE_SCRIPT_DATA_ESCAPED_END_TAG_NAME; // Reconsume } else { // Anything else // Emit a U+003C LESS-THAN SIGN character token and a U+002F SOLIDUS // character token. Reconsume in the script data escaped state. state = STATE_SCRIPT_DATA_ESCAPED; // Reconsume } break; // https://html.spec.whatwg.org/multipage/parsing.html#script-data-escaped-end-tag-name-state case STATE_SCRIPT_DATA_ESCAPED_END_TAG_NAME: // Consume the next input character: // U+0009 CHARACTER TABULATION (tab) // U+000A LINE FEED (LF) // U+000C FORM FEED (FF) // U+0020 SPACE // If the current end tag token is an appropriate end tag token, then switch // to the before attribute name state. Otherwise, treat it as per the // "anything else" entry below. if (isSpace(cc)) { tagNameEnd = pos; if ( rangeEqualsLower(input, tagNameStart, tagNameEnd, lastOpenTagName) ) { flushText(tagStart); state = STATE_BEFORE_ATTRIBUTE_NAME; pos++; } else { state = STATE_SCRIPT_DATA_ESCAPED; } } else if (cc === CC_SOLIDUS) { // U+002F SOLIDUS (/) // If the current end tag token is an appropriate end tag token, then switch // to the self-closing start tag state. Otherwise, treat it as per the // "anything else" entry below. tagNameEnd = pos; if ( rangeEqualsLower(input, tagNameStart, tagNameEnd, lastOpenTagName) ) { flushText(tagStart); state = STATE_SELF_CLOSING_START_TAG; pos++; } else { state = STATE_SCRIPT_DATA_ESCAPED; } } else if (cc === CC_GREATER_THAN) { // U+003E GREATER-THAN SIGN (>) // If the current end tag token is an appropriate end tag token, then switch // to the data state and emit the current tag token. Otherwise, treat it as // per the "anything else" entry below. tagNameEnd = pos; if ( rangeEqualsLower(input, tagNameStart, tagNameEnd, lastOpenTagName) ) { flushText(tagStart); state = STATE_DATA; pos = emitCloseTag(pos + 1); } else { state = STATE_SCRIPT_DATA_ESCAPED; } } else if (isAsciiAlpha(cc)) { // ASCII upper alpha / ASCII lower alpha pos++; } else { // Anything else // Emit a U+003C LESS-THAN SIGN character token, a U+002F SOLIDUS character // token, and a character token for each of the characters in the temporary // buffer (in the order they were added to the buffer). Reconsume in the // script data escaped state. state = STATE_SCRIPT_DATA_ESCAPED; // Reconsume } break; // https://html.spec.whatwg.org/multipage/parsing.html#script-data-double-escape-start-state case STATE_SCRIPT_DATA_DOUBLE_ESCAPE_START: // Consume the next input character: // U+0009 CHARACTER TABULATION (tab) // U+000A LINE FEED (LF) // U+000C FORM FEED (FF) // U+0020 SPACE // U+002F SOLIDUS (/) // U+003E GREATER-THAN SIGN (>) // If the temporary buffer is the string "script", then switch to the script // data double escaped state. Otherwise, switch to the script data escaped // state. Emit the current input character as a character token. if (isSpace(cc) || cc === CC_SOLIDUS || cc === CC_GREATER_THAN) { state = scriptMatch === 6 ? STATE_SCRIPT_DATA_DOUBLE_ESCAPED : STATE_SCRIPT_DATA_ESCAPED; pos++; } else if (isAsciiUpperAlpha(cc) || isAsciiLowerAlpha(cc)) { // ASCII alpha — advance the `"script"` match counter if the // lowercase form matches the next expected char, otherwise // snap to the sentinel so further chars can't revive a // match. No buffer allocation. const lower = isAsciiUpperAlpha(cc) ? cc + 0x20 : cc; if (scriptMatch < 6 && lower === "script".charCodeAt(scriptMatch)) { scriptMatch++; } else { scriptMatch = 7; } pos++; } else { // Anything else // Reconsume in the script data escaped state. state = STATE_SCRIPT_DATA_ESCAPED; // Reconsume } break; // https://html.spec.whatwg.org/multipage/parsing.html#script-data-double-escaped-state case STATE_SCRIPT_DATA_DOUBLE_ESCAPED: // Consume the next input character: // U+002D HYPHEN-MINUS (-) // Switch to the script data double escaped dash state. Emit a U+002D // HYPHEN-MINUS character token. if (cc === CC_HYPHEN_MINUS) { state = STATE_SCRIPT_DATA_DOUBLE_ESCAPED_DASH; pos++; } else if (cc === CC_LESS_THAN) { // U+003C LESS-THAN SIGN (<) // Switch to the script data double escaped less-than sign state. Emit a // U+003C LESS-THAN SIGN character token. state = STATE_SCRIPT_DATA_DOUBLE_ESCAPED_LESS_THAN_SIGN; pos++; } else { // Anything else // Emit the current input character as a character token. // U+0000 NULL is an unexpected-null-character parse error. if (cc === CC_NULL) { reportError("unexpected-null-character", pos, pos + 1, "warning"); } pos++; } break; // https://html.spec.whatwg.org/multipage/parsing.html#script-data-double-escaped-dash-state case STATE_SCRIPT_DATA_DOUBLE_ESCAPED_DASH: // Consume the next input character: // U+002D HYPHEN-MINUS (-) // Switch to the script data double escaped dash dash state. Emit a U+002D // HYPHEN-MINUS character token. if (cc === CC_HYPHEN_MINUS) { state = STATE_SCRIPT_DATA_DOUBLE_ESCAPED_DASH_DASH; pos++; } else if (cc === CC_LESS_THAN) { // U+003C LESS-THAN SIGN (<) // Switch to the script data double escaped less-than sign state. Emit a // U+003C LESS-THAN SIGN character token. state = STATE_SCRIPT_DATA_DOUBLE_ESCAPED_LESS_THAN_SIGN; pos++; } else { // Anything else // Switch to the script data double escaped state. Emit the current input // character as a character token. NULL is unexpected-null-character. if (cc === CC_NULL) { reportError("unexpected-null-character", pos, pos + 1, "warning"); } state = STATE_SCRIPT_DATA_DOUBLE_ESCAPED; pos++; } break; // https://html.spec.whatwg.org/multipage/parsing.html#script-data-double-escaped-dash-dash-state case STATE_SCRIPT_DATA_DOUBLE_ESCAPED_DASH_DASH: // Consume the next input character: // U+002D HYPHEN-MINUS (-) // Emit a U+002D HYPHEN-MINUS character token. if (cc === CC_HYPHEN_MINUS) { pos++; } else if (cc === CC_LESS_THAN) { // U+003C LESS-THAN SIGN (<) // Switch to the script data double escaped less-than sign state. Emit a // U+003C LESS-THAN SIGN character token. state = STATE_SCRIPT_DATA_DOUBLE_ESCAPED_LESS_THAN_SIGN; pos++; } else if (cc === CC_GREATER_THAN) { // U+003E GREATER-THAN SIGN (>) // Switch to the script data state. Emit a U+003E GREATER-THAN SIGN // character token. state = STATE_SCRIPT_DATA; pos++; } else { // Anything else // Switch to the script data double escaped state. Emit the current input // character as a character token. NULL is unexpected-null-character. if (cc === CC_NULL) { reportError("unexpected-null-character", pos, pos + 1, "warning"); } state = STATE_SCRIPT_DATA_DOUBLE_ESCAPED; pos++; } break; // https://html.spec.whatwg.org/multipage/parsing.html#script-data-double-escaped-less-than-sign-state case STATE_SCRIPT_DATA_DOUBLE_ESCAPED_LESS_THAN_SIGN: // Consume the next input character: // U+002F SOLIDUS (/) // Set the temporary buffer to the empty string. Switch to the script data // double escape end state. Emit a U+002F SOLIDUS character token. if (cc === CC_SOLIDUS) { scriptMatch = 0; state = STATE_SCRIPT_DATA_DOUBLE_ESCAPE_END; pos++; } else { // Anything else // Reconsume in the script data double escaped state. state = STATE_SCRIPT_DATA_DOUBLE_ESCAPED; // Reconsume } break; // https://html.spec.whatwg.org/multipage/parsing.html#script-data-double-escape-end-state case STATE_SCRIPT_DATA_DOUBLE_ESCAPE_END: // Consume the next input character: // U+0009 CHARACTER TABULATION (tab) // U+000A LINE FEED (LF) // U+000C FORM FEED (FF) // U+0020 SPACE // U+002F SOLIDUS (/) // U+003E GREATER-THAN SIGN (>) // If the temporary buffer is the string "script", then switch to the script // data escaped state. Otherwise, switch to the script data double escaped // state. Emit the current input character as a character token. if (isSpace(cc) || cc === CC_SOLIDUS || cc === CC_GREATER_THAN) { state = scriptMatch === 6 ? STATE_SCRIPT_DATA_ESCAPED : STATE_SCRIPT_DATA_DOUBLE_ESCAPED; pos++; } else if (isAsciiUpperAlpha(cc) || isAsciiLowerAlpha(cc)) { // ASCII alpha — advance the `"script"` match counter if the // lowercase form matches the next expected char, otherwise // snap to the sentinel so further chars can't revive a // match. No buffer allocation. const lower = isAsciiUpperAlpha(cc) ? cc + 0x20 : cc; if (scriptMatch < 6 && lower === "script".charCodeAt(scriptMatch)) { scriptMatch++; } else { scriptMatch = 7; } pos++; } else { // Anything else // Reconsume in the script data double escaped state. state = STATE_SCRIPT_DATA_DOUBLE_ESCAPED; // Reconsume } break; // https://html.spec.whatwg.org/multipage/parsing.html#plaintext-state case STATE_PLAINTEXT: // Consume the next input character: // U+0000 NULL is an unexpected-null-character parse error. // Anything else: emit the current input character. if (cc === CC_NULL) { reportError("unexpected-null-character", pos, pos + 1, "warning"); pos++; } else { // Fast-forward to the next NULL (or EOF). pos++; while (pos < len && input.charCodeAt(pos) !== CC_NULL) pos++; } break; // https://html.spec.whatwg.org/multipage/parsing.html#character-reference-state case STATE_CHARACTER_REFERENCE: // Set the temporary buffer to the empty string. Append a U+0026 // AMPERSAND (&) character to the temporary buffer. // `charRefStart` points at that `&` (one before the current pos). charRefStart = pos - 1; // Consume the next input character: if (isAsciiAlphanumeric(cc)) { // ASCII alphanumeric // Reconsume in the named character reference state. state = STATE_NAMED_CHARACTER_REFERENCE; // Reconsume } else if (cc === CC_NUMBER_SIGN) { // U+0023 NUMBER SIGN (#) // Append the current input character to the temporary buffer. // Set the character reference code to zero. Switch to the // numeric character reference state. charRefCode = 0; state = STATE_NUMERIC_CHARACTER_REFERENCE; pos++; } else { // Anything else // Flush code points consumed as a character reference. // Reconsume in the return state. state = returnState; // Reconsume } break; // https://html.spec.whatwg.org/multipage/parsing.html#named-character-reference-state case STATE_NAMED_CHARACTER_REFERENCE: { // Consume the maximum number of characters possible where the // consumed characters are one of the identifiers in the first // column of the named character references table. // // We measure the longest run of ASCII alphanumeric characters // (capped at MAX_ENTITY_NAME_LEN - 1 since the optional `;` is // handled separately), then walk that run from longest to // shortest looking for the first prefix that exists in the // entity table (with a trailing `;` if present, otherwise the // legacy bare form). let runLen = 0; while ( pos + runLen < len && isAsciiAlphanumeric(input.charCodeAt(pos + runLen)) && runLen < MAX_ENTITY_NAME_LEN - 1 ) { runLen++; } const hasSemicolon = pos + runLen < len && input.charCodeAt(pos + runLen) === CC_SEMICOLON; namedEntityConsumed = 0; let matchedWithSemicolon = false; // Try the full run with its trailing `;` first — the overwhelmingly // common case (`&`, ` `, …) 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 a trailing `;` is a // missing-semicolon-after-character-reference parse error, // except for the spec's historical attribute rule: when // consumed in an attribute value and the next char is `=` or // ASCII alphanumeric, the reference is left undecoded silently. if (!matchedWithSemicolon) { const next = input.charCodeAt(pos + namedEntityConsumed); const inAttribute = returnState === STATE_ATTRIBUTE_VALUE_DOUBLE_QUOTED || returnState === STATE_ATTRIBUTE_VALUE_SINGLE_QUOTED || returnState === STATE_ATTRIBUTE_VALUE_UNQUOTED; if (!( inAttribute && (next === CC_EQUALS || isAsciiAlphanumeric(next)) )) { reportError( "missing-semicolon-after-character-reference", pos + namedEntityConsumed, pos + namedEntityConsumed + 1, "warning" ); } } pos += namedEntityConsumed; state = returnState; } else { // No match — flush code points consumed as a character // reference. Switch to the ambiguous ampersand state. state = STATE_AMBIGUOUS_AMPERSAND; } break; } // https://html.spec.whatwg.org/multipage/parsing.html#ambiguous-ampersand-state case STATE_AMBIGUOUS_AMPERSAND: // Consume the next input character: if (isAsciiAlphanumeric(cc)) { // ASCII alphanumeric // If the character reference was consumed as part of an // attribute, then append the current input character to the // current attribute's value. Otherwise, emit the current // input character as a character token. pos++; } else if (cc === CC_SEMICOLON) { // U+003B SEMICOLON (;) // This is an unknown-named-character-reference parse error. // Reconsume in the return state. reportError( "unknown-named-character-reference", pos, pos + 1, "warning" ); state = returnState; // Reconsume } else { // Anything else // Reconsume in the return state. state = returnState; // Reconsume } break; // https://html.spec.whatwg.org/multipage/parsing.html#numeric-character-reference-state case STATE_NUMERIC_CHARACTER_REFERENCE: // Set the character reference code to zero (0). // Consume the next input character: if (cc === 0x78 || cc === 0x58) { // U+0078 LATIN SMALL LETTER X // U+0058 LATIN CAPITAL LETTER X // Append the current input character to the temporary // buffer. Switch to the hexadecimal character reference // start state. state = STATE_HEXADECIMAL_CHARACTER_REFERENCE_START; pos++; } else { // Anything else // Reconsume in the decimal character reference start state. state = STATE_DECIMAL_CHARACTER_REFERENCE_START; // Reconsume } break; // https://html.spec.whatwg.org/multipage/parsing.html#hexadecimal-character-reference-start-state case STATE_HEXADECIMAL_CHARACTER_REFERENCE_START: // Consume the next input character: // ASCII hex digit: reconsume in the hexadecimal character reference state. // Anything else: absence-of-digits-in-numeric-character-reference parse // error. Flush code points consumed as a character reference. Reconsume // in the return state. if (isAsciiHexDigit(cc)) { state = STATE_HEXADECIMAL_CHARACTER_REFERENCE; } else { reportError( "absence-of-digits-in-numeric-character-reference", pos, pos + 1, "warning" ); state = returnState; } // Reconsume break; // https://html.spec.whatwg.org/multipage/parsing.html#decimal-character-reference-start-state case STATE_DECIMAL_CHARACTER_REFERENCE_START: // Consume the next input character: // ASCII digit: reconsume in the decimal character reference state. // Anything else: absence-of-digits-in-numeric-character-reference parse // error. Flush code points consumed as a character reference. Reconsume // in the return state. if (isAsciiDigit(cc)) { state = STATE_DECIMAL_CHARACTER_REFERENCE; } else { reportError( "absence-of-digits-in-numeric-character-reference", pos, pos + 1, "warning" ); state = returnState; } // Reconsume break; // https://html.spec.whatwg.org/multipage/parsing.html#hexadecimal-character-reference-state case STATE_HEXADECIMAL_CHARACTER_REFERENCE: // Consume the next input character: if (isAsciiHexDigit(cc)) { // ASCII digit / upper hex / lower hex // Multiply the character reference code by 16. Add a numeric // version of the current input character to the character // reference code. Stop accumulating once past the Unicode // range so the value can't overflow (still flags as // outside-range at the end). if (charRefCode < 0x110000) { const v = cc <= 0x39 ? cc - 0x30 : (cc | 0x20) - 0x61 + 10; charRefCode = charRefCode * 16 + v; } pos++; } else if (cc === CC_SEMICOLON) { // U+003B SEMICOLON // Switch to the numeric character reference end state. state = STATE_NUMERIC_CHARACTER_REFERENCE_END; pos++; } else { // Anything else // This is a missing-semicolon-after-character-reference // parse error. Reconsume in the numeric character reference // end state. reportError( "missing-semicolon-after-character-reference", pos, pos + 1, "warning" ); state = STATE_NUMERIC_CHARACTER_REFERENCE_END; // Reconsume } break; // https://html.spec.whatwg.org/multipage/parsing.html#decimal-character-reference-state case STATE_DECIMAL_CHARACTER_REFERENCE: // Consume the next input character: if (isAsciiDigit(cc)) { // ASCII digit // Multiply the character reference code by 10. Add a numeric // version of the current input character (subtract 0x0030 // from the character's code point) to the character reference // code. Stop accumulating once past the Unicode range so the // value can't overflow (still flags as outside-range). if (charRefCode < 0x110000) { charRefCode = charRefCode * 10 + (cc - 0x30); } pos++; } else if (cc === CC_SEMICOLON) { // U+003B SEMICOLON // Switch to the numeric character reference end state. state = STATE_NUMERIC_CHARACTER_REFERENCE_END; pos++; } else { // Anything else // This is a missing-semicolon-after-character-reference // parse error. Reconsume in the numeric character reference // end state. reportError( "missing-semicolon-after-character-reference", pos, pos + 1, "warning" ); state = STATE_NUMERIC_CHARACTER_REFERENCE_END; // Reconsume } break; // https://html.spec.whatwg.org/multipage/parsing.html#numeric-character-reference-end-state case STATE_NUMERIC_CHARACTER_REFERENCE_END: // Check the character reference code and report the matching // WHATWG validation parse error. validateNumericReference(pos); // Flush code points consumed as a character reference. // Switch to the return state. state = returnState; // Reconsume break; /* istanbul ignore next -- @preserve: defensive fallback, all states are explicit above */ default: pos++; } } // Handle EOF in non-data states per the WHATWG spec. // // Each in-progress comment / doctype / cdata / tag emits its partial // token range plus a corresponding `eof-in-X` parse error. Severity is // `"error"` because the emitted token offset range is incomplete (missing // trailing `-->`, `>`, `]]>`, etc.). For data / `<` / `</` / `<!`-only // inputs we emit `eof-before-tag-name` and fall through to flush the // pending text span (which still contains the lone `<`). // EOF inside a character-reference state: run the end-of-reference // processing (numeric end states never run when the reference ends at // EOF), then resume in the return state for the branches below. if ( state >= STATE_CHARACTER_REFERENCE && state <= STATE_NUMERIC_CHARACTER_REFERENCE_END ) { if ( state === STATE_NUMERIC_CHARACTER_REFERENCE || state === STATE_HEXADECIMAL_CHARACTER_REFERENCE_START || state === STATE_DECIMAL_CHARACTER_REFERENCE_START ) { // No digits before EOF. reportError( "absence-of-digits-in-numeric-character-reference", len, len, "warning" ); } else if ( state === STATE_HEXADECIMAL_CHARACTER_REFERENCE || state === STATE_DECIMAL_CHARACTER_REFERENCE ) { // Digits but no closing `;` before EOF. reportError( "missing-semicolon-after-character-reference", len, len, "warning" ); validateNumericReference(len); } else if (state === STATE_NUMERIC_CHARACTER_REFERENCE_END) { validateNumericReference(len); } state = returnState; } if ( (state >= STATE_TAG_NAME && state <= STATE_SELF_CLOSING_START_TAG) || state === STATE_RCDATA_END_TAG_NAME || state === STATE_RAWTEXT_END_TAG_NAME || state === STATE_SCRIPT_DATA_END_TAG_NAME || state === STATE_SCRIPT_DATA_ESCAPED_END_TAG_NAME ) { // EOF mid-tag — emit the partial open/close tag at EOF so the // consumer still sees the tag. This is a deliberate deviation // from the spec's per-character emission model: rather than // dropping the in-progress tag, we emit its offset range up to EOF. reportError("eof-in-tag", len, len, "error"); // If we hit EOF mid-attribute-name, the name runs to EOF. Set // attributeNameEnd here so the emitted attribute range is valid. if (state === STATE_ATTRIBUTE_NAME && attributeNameStart !== -1) { attributeNameEnd = len; } if (attributeNameStart !== -1) emitAttribute(len); // If we hit EOF before the tag-name end was recorded, the name runs // to EOF. `tagNameEnd` may carry over from a previously emitted tag, // so reset it whenever it's missing or stale (less than `tagNameStart`) // — covers `<div` open-tag EOFs as well as `<title>x</tit` and other // content-mode end-tag-name EOFs. if (tagNameStart !== -1 && tagNameEnd < tagNameStart) { tagNameEnd = len; } flushText(tagStart); pos = input.charCodeAt(tagStart + 1) === CC_SOLIDUS ? emitCloseTag(len) : emitOpenTag(len, false); } else if ( (state >= STATE_COMMENT_START && state <= STATE_BOGUS_COMMENT) || (state >= STATE_COMMENT_LESS_THAN_SIGN && state <= STATE_COMMENT_LESS_THAN_SIGN_BANG_DASH_DASH) || state === STATE_MARKUP_DECLARATION_OPEN ) { // EOF in markup-declaration-open takes the spec's "anything else" // branch: incorrectly-opened-comment, then a bogus comment (which has // no EOF error of its own). Bogus comments at EOF are likewise normal. if (state === STATE_MARKUP_DECLARATION_OPEN) { reportError("incorrectly-opened-comment", commentStart, len, "warning"); } else if (state !== STATE_BOGUS_COMMENT) { reportError("eof-in-comment", len, len, "error"); } if (callbacks.comment !== undefined) { pos = callbacks.comment(input, commentStart, len); } } else if (state >= STATE_CDATA_SECTION && state <= STATE_CDATA_SECTION_END) { reportError("eof-in-cdata", len, len, "error"); if (callbacks.comment !== undefined) { pos = callbacks.comment(input, commentStart, len); } } else if (state >= STATE_DOCTYPE && state <= STATE_BOGUS_DOCTYPE) { // EOF in bogus DOCTYPE emits the token with no parse error (spec). if (state !== STATE_BOGUS_DOCTYPE) { reportError("eof-in-doctype", len, len, "error"); } if (callbacks.doctype !== undefined) { pos = callbacks.doctype(input, commentStart, len); } } else { if ( state === STATE_SCRIPT_DATA_ESCAPED || state === STATE_SCRIPT_DATA_ESCAPED_DASH || state === STATE_SCRIPT_DATA_ESCAPED_DASH_DASH || state === STATE_SCRIPT_DATA_ESCAPED_LESS_THAN_SIGN || state === STATE_SCRIPT_DATA_DOUBLE_ESCAPED || state === STATE_SCRIPT_DATA_DOUBLE_ESCAPED_DASH || state === STATE_SCRIPT_DATA_DOUBLE_ESCAPED_DASH_DASH || state === STATE_SCRIPT_DATA_DOUBLE_ESCAPED_LESS_THAN_SIGN || state === STATE_SCRIPT_DATA_DOUBLE_ESCAPE_END ) { // Inside `<script><!-- … ` at EOF — spec calls this an // eof-in-script-html-comment-like-text parse error. The // less-than-sign and double-escape-end states reconsume back // into the (double-)escaped state on EOF per spec, which then // hits this same error. reportError("eof-in-script-html-comment-like-text", len, len, "error"); } else if (state === STATE_TAG_OPEN || state === STATE_END_TAG_OPEN) { // `<` or `</` with nothing after; spec calls this // eof-before-tag-name. The lone `<` / `</` is preserved in the // pending text span which is flushed below. reportError("eof-before-tag-name", len, len, "warning"); } if (textStart < len && callbacks.text !== undefined) { callbacks.text(input, textStart, len); } } return pos; }; // WHATWG numeric-character-reference-end Windows-1252 remap table for the // 0x80-0x9F range. Per spec these C1 control code points decode to the // corresponding Windows-1252 glyph (with a parse error) rather than to the // raw C1 control character. const NUMERIC_C1_REMAP = { 0x80: "€", 0x82: "‚", 0x83: "ƒ", 0x84: "„", 0x85: "…", 0x86: "†", 0x87: "‡", 0x88: "ˆ", 0x89: "‰", 0x8a: "Š", 0x8b: "‹", 0x8c: "Œ", 0x8e: "Ž", 0x91: "‘", 0x92: "’", 0x93: "“", 0x94: "”", 0x95: "•", 0x96: "–", 0x97: "—", 0x98: "˜", 0x99: "™", 0x9a: "š", 0x9b: "›", 0x9c: "œ", 0x9e: "ž", 0x9f: "Ÿ" }; /** * @param {number} code numeric character reference code point * @returns {string} decoded character per WHATWG remap rules */ const decodeNumericReference = (code) => { // Per WHATWG numeric-character-reference-end-state: // - 0x00, > 0x10FFFF, or surrogate (0xD800-0xDFFF) -> U+FFFD. // - 0x80-0x9F -> Windows-1252 remap (above). // - Anything else (including noncharacters and C0 controls) -> the // code point itself; we don't surface the spec's parse-error // classes here since decoding is happening after the scanner ran. if (code === 0 || code > 0x10ffff || (code >= 0xd800 && code <= 0xdfff)) { return "�"; } if (code >= 0x80 && code <= 0x9f) { const remapped = /** @type {Record<number, string>} */ (NUMERIC_C1_REMAP)[ code ]; if (remapped !== undefined) return remapped; } return String.fromCodePoint(code); }; // Match one of three forms (each with an optional trailing `;`): // `&#x<hex>` - hex numeric reference (requires the `x`/`X`). // `&#<dec>` - decimal numeric reference (digits only). // `&<name>` - named reference (letter followed by alphanumerics). // The three alternatives are kept separate so a decimal reference like // `Ab` doesn't greedily eat the trailing `b` as if it were hex. const CHARACTER_REFERENCE_REGEXP = /&(?:#[xX][0-9a-fA-F]+|#[0-9]+|[a-zA-Z][a-zA-Z0-9]*);?/g; /** * Decode a single matched character reference. * @param {string} match the matched reference text * @param {number} nextCharCode char code following the match in the source (NaN at the end) * @param {boolean=} isAttribute true when the match came from an attribute value * @returns {string} decoded text, or `match` itself when it stays literal */ const decodeOneReference = (match, nextCharCode, isAttribute) => { // Numeric reference: A or A if (match.charCodeAt(1) === 0x23 /* # */) { const lastChar = match.charAt(match.length - 1); const isHex = match.charCodeAt(2) === 0x78 || match.charCodeAt(2) === 0x58; const body = isHex ? lastChar === ";" ? match.slice(3, -1) : match.slice(3) : lastChar === ";" ? match.slice(2, -1) : match.slice(2); // The regex above guarantees at least one digit in `body`, // so `parseInt` always returns a finite number here. return decodeNumericReference(Number.parseInt(body, isHex ? 16 : 10)); } // Named reference. Try the full captured name first, then // progressively shorter prefixes - this handles direct matches // like `&` as well as WHATWG longest-prefix semantics where // e.g. `¬pre;` decodes as `¬` (a legacy bare entity) // followed by `pre;` as literal text. const name = match.slice(1); const matchEndsWithSemi = name.charCodeAt(name.length - 1) === 0x3b; // Attribute-context guard: if the entity match didn't end with `;` // and the next character in the source is `=` or ASCII // alphanumeric, the WHATWG spec says to flush the literal text // rather than decode. The greedy regex already absorbed any // trailing alphanumerics, so the only candidate "next char" here // is `=` (or any non-alphanumeric). if (isAttribute && !matchEndsWithSemi && nextCharCode === 0x3d /* = */) { return match; } // Fast path: the regex usually captures exactly one entity (`&`, // `<`, ` `, …), so the whole `name` is the match — avoid the // full-length `name.slice(0, name.length)` the loop's first iteration // would allocate. No leftover, so the attribute guard never applies. if (name.length <= MAX_ENTITY_NAME_LEN) { const full = HTML_ENTITIES[name]; if (full !== undefined) return full; } // Cap the longest-prefix search at MAX_ENTITY_NAME_LEN so pathological // inputs like `&` + thousands of alphanumerics stay linear-time. // Anything past that cap can't possibly match and is appended // verbatim as part of `name.slice(i)`. The full-length case was just // handled above, so start one shorter when it's the cap. const searchLen = name.length > MAX_ENTITY_NAME_LEN ? MAX_ENTITY_NAME_LEN : name.length - 1; for (let i = searchLen; i > 0; i--) { const prefix = name.slice(0, i); if (HTML_ENTITIES[prefix] !== undefined) { // Attribute-context longest-prefix guard: if the matched // prefix doesn't end with `;` and the leftover starts with // an alphanumeric character, leave literal per WHATWG. // (The regex greedy-consumes alphanumerics, so any leftover // within `name` is itself alphanumeric — we only need to // check non-empty leftover here; the `=` case is handled // above against the source character after the match.) if ( isAttribute && i < name.length && prefix.charCodeAt(prefix.length - 1) !== 0x3b ) { return match; } return HTML_ENTITIES[prefix] + name.slice(i); } } return match; }; // Hoisted `replace` callbacks (one per `isAttribute` mode) — no closure // per decode call, and the callback stays monomorphic. /** @type {(match: string, offset: number, source: string) => string} */ const _decodeReferenceInText = (match, offset, source) => decodeOneReference(match, source.charCodeAt(offset + match.length), false); /** @type {(match: string, offset: number, source: string) => string} */ const _decodeReferenceInAttribute = (match, offset, source) => decodeOneReference(match, source.charCodeAt(offset + match.length), true); /** * 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. `&=foo` stays literal in an attribute value but * decodes to `&=foo` in text. * @param {string} str the raw string from the token slice * @param {boolean=} isAttribute true if `str` came from an attribute value * @returns {string} decoded string */ const decodeHtmlEntities = (str, isAttribute) => { if (!str.includes("&")) return str; return str.replace( CHARACTER_REFERENCE_REGEXP, isAttribute ? _decodeReferenceInAttribute : _decodeReferenceInText ); }; /** * Like `decodeHtmlEntities`, but also returns a boundary map from the * decoded string back to raw offsets, so spans computed on the decoded * text (e.g. srcset candidate URLs) can be translated to source ranges. * `map[i]` is the raw offset of decoded boundary `i` (`0..text.length`); * boundaries inside a reference's decoded text map to the reference start. * `map` is `undefined` when nothing was decoded (then `text === str`). * @param {string} str the raw string from the token slice * @param {boolean=} isAttribute true if `str` came from an attribute value * @returns {{ text: string, map: number[] | undefined }} decoded text and offset map */ const decodeHtmlEntitiesWithMap = (str, isAttribute) => { /** @type {number[] | undefined} */ let map; if (str.includes("&")) { CHARACTER_REFERENCE_REGEXP.lastIndex = 0; let text = ""; let last = 0; /** @type {RegExpExecArray | null} */ let m; while ((m = CHARACTER_REFERENCE_REGEXP.exec(str)) !== null) { const match = m[0]; const decoded = decodeOneReference( match, str.charCodeAt(m.index + match.length), isAttribute ); if (decoded === match) continue; if (map === undefined) map = []; for (let r = last; r < m.index; r++) map.push(r); text += str.slice(last, m.index); for (let i = 0; i < decoded.length; i++) map.push(m.index); text += decoded; last = m.index + match.length; } if (map !== undefined) { for (let r = last; r < str.length; r++) map.push(r); map.push(str.length); return { text: text + str.slice(last), map }; } } return { text: str, map }; }; // cspell:ignore advasoft altglyph altglyphdef altglyphitem animatecolor animatemotion animatetransform arcrole aswedit attributename attributetype basefrequency baseprofile bgsound calcmode clippathunits definitionurl diffuseconstant fedropshadow filterunits glyphref gradienttransform gradientunits hotjava hotmetal kernelmatrix kernelunitlength keypoints keysplines keytimes limitingconeangle malignmark markerheight markerwidth maskcontentunits maskunits metrius mglyph mtext numoctaves pathlength patterncontentunits patterntransform patternunits pointsatx pointsaty pointsatz preservealpha primitiveunits refx refy repeatcount repeatdur requiredextensions requiredfeatures selectedcontent silmaril softquad specularconstant specularexponent startoffset stddeviation stitchtiles surfacescale systemlanguage tablevalues targetx targety textlength viewbox viewtarget webtechs xchannelselector ychannelselector megamorphic attributeless rowspan imagesizes novalidate maxlength // WHATWG HTML tree construction (https://html.spec.whatwg.org/multipage/parsing.html#tree-construction) // on top of walkHtmlTokens. Scripting is always disabled (webpack is a build tool). // Namespaces (mirrors swc_html_ast::Namespace) const NS_HTML = 0; const NS_MATHML = 1; const NS_SVG = 2; /** * 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 }} */ const NodeType = { Document: 1, DocumentFragment: 2, Element: 3, Text: 4, Comment: 5, Doctype: 6 }; /** * 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 }) ); // === Struct-of-arrays AST backend === // One AST node = one integer id (`HtmlNodeRef`) indexing the parallel columns // below — no per-node object and no per-parent children array. Tree shape // lives in the four link columns (parent / firstChild / lastChild / // nextSibling); the only heap references are the string payload / attribute // name-and-value side arrays. Columns are module-level and reused // across parses (grown, never shrunk — the CSS parser's `_soa*` strategy), so // a steady-state parse allocates almost nothing per node; consumers must fully // read a tree before the next `buildHtmlAst` call. Id 0 is reserved as // "no node" so the link columns can use 0 as null. let _hCap = 0; let _hN = 0; /** `NodeType` per node */ let _hTy = new Uint8Array(0); /** bits 0-1 namespace (`NS_*`), bit 2 self-closing (void element) */ let _hFl = new Uint8Array(0); let _hSt = new Int32Array(0); let _hEn = new Int32Array(0); /** end offset of an element's opening tag (after `>`) */ let _hTagEnd = new Int32Array(0); /** end offset of an element's tag name */ let _hNameEnd = new Int32Array(0); /** under `skip.text`, end offset of a raw-text element's body (`HtmlAstSkip`) */ let _hCEnd = new Int32Array(0); /** a `<template>`'s content DocumentFragment (0 = none) */ let _hTc = new Int32Array(0); let _hParent = new Int32Array(0); let _hFirst = new Int32Array(0); let _hLast = new Int32Array(0); let _hNext = new Int32Array(0); /** @type {string[]} tag name / text data / comment data / doctype name */ const _hStr = []; /** first attribute id of an element's contiguous run */ let _hAStart = new Int32Array(0); /** attribute count of an element's run */ let _hACount = new Int32Array(0); // 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 _hDocPub = null; /** @type {string | null} */ let _hDocSys = null; const NS_MASK = 3; const FLAG_SELF_CLOSING = 4; /** @type {(el: HtmlNodeRef) => string} */ const _tag = (el) => _hStr[el]; /** @type {(el: HtmlNodeRef) => number} */ const _ns = (el) => _hFl[el] & NS_MASK; // === Attribute columns === // One attribute = one integer id into these columns; an element (and a // start-tag token) holds a contiguous run. The value string is derived from // the source by offset on read — `_aVal` carries an override only for // valueless attributes (`""`) and offset-less adoption-agency clones — and the // html5lib serializer name is derived from the adjusted name plus one flag // bit, so per attribute only the interned name pointer is retained. let _aCap = 0; let _aN = 0; let _aNameStart = new Int32Array(0); let _aNameEnd = new Int32Array(0); let _aValStart = new Int32Array(0); let _aValEnd = new Int32Array(0); /** bit 0: name has a `FOREIGN_ATTR_NS` serializer name (set on foreign adjust) */ let _aFl = new Uint8Array(0); /** @type {string[]} lowercased (foreign-content: adjusted) attribute name */ const _aName = []; /** @type {(string | null)[]} value override (null = slice the source by offset) */ const _aVal = []; /** source of the current parse, for by-offset attribute values */ let _hSrc = ""; /** @param {number} need minimum capacity */ const _aGrow = (need) => { let cap = _aCap || 4096; while (cap < need) cap *= 2; const nameStart = new Int32Array(cap); nameStart.set(_aNameStart); _aNameStart = nameStart; const nameEnd = new Int32Array(cap); nameEnd.set(_aNameEnd); _aNameEnd = nameEnd; const valStart = new Int32Array(cap); valStart.set(_aValStart); _aValStart = valStart; const valEnd = new Int32Array(cap); valEnd.set(_aValEnd); _aValEnd = valEnd; const fl = new Uint8Array(cap); fl.set(_aFl); _aFl = fl; _aCap = cap; }; /** @type {(name: string, value: string | null, nameStart: number, nameEnd: number, valueStart: number, valueEnd: number) => number} */ const _aAlloc = (name, value, nameStart, nameEnd, valueStart, valueEnd) => { const i = ++_aN; if (i >= _aCap) _aGrow(i + 1); _aNameStart[i] = nameStart; _aNameEnd[i] = nameEnd; _aValStart[i] = valueStart; _aValEnd[i] = valueEnd; _aFl[i] = 0; // Ids are sequential, so these indexed writes append (arrays stay packed). _aName[i] = name; _aVal[i] = value; return i; }; /** @type {(i: number) => string} */ const _aValueOf = (i) => { const v = _aVal[i]; return v !== null ? v : _hSrc.slice(_aValStart[i], _aValEnd[i]); }; // Linear name lookup in a run — attribute lists are short, a loop beats a Map. /** @type {(start: number, count: number, name: string) => number} */ const _aFind = (start, count, name) => { for (let i = start; i < start + count; i++) { if (_aName[i] === name) return i; } return 0; }; /** @type {(i: number) => number} exact copy of an attribute into a new id */ const _aCopy = (i) => { const c = _aAlloc( _aName[i], _aVal[i], _aNameStart[i], _aNameEnd[i], _aValStart[i], _aValEnd[i] ); _aFl[c] = _aFl[i]; return c; }; // html5lib serializer name, derived: a `FOREIGN_ATTR_NS`-adjusted attribute is // flagged (its name is the table key), and a camelCase-adjusted name contains // an uppercase letter (unadjusted names are always lowercased), serializing as // itself. Everything else serializes as the plain name (undefined here). /** @type {(i: number) => string | undefined} */ const _aSerializedName = (i) => { if ((_aFl[i] & 1) !== 0) return FOREIGN_ATTR_NS[_aName[i]]; const name = _aName[i]; return /[A-Z]/.test(name) ? name : undefined; }; /** @param {number} need minimum capacity */ const _hGrow = (need) => { let cap = _hCap || 4096; while (cap < need) cap *= 2; const ty = new Uint8Array(cap); ty.set(_hTy); _hTy = ty; const fl = new Uint8Array(cap); fl.set(_hFl); _hFl = fl; const st = new Int32Array(cap); st.set(_hSt); _hSt = st; const en = new Int32Array(cap); en.set(_hEn); _hEn = en; const tagEnd = new Int32Array(cap); tagEnd.set(_hTagEnd); _hTagEnd = tagEnd; const nameEnd = new Int32Array(cap); nameEnd.set(_hNameEnd); _hNameEnd = nameEnd; const cEnd = new Int32Array(cap); cEnd.set(_hCEnd); _hCEnd = cEnd; const tc = new Int32Array(cap); tc.set(_hTc); _hTc = tc; const parent = new Int32Array(cap); parent.set(_hParent); _hParent = parent; const first = new Int32Array(cap); first.set(_hFirst); _hFirst = first; const last = new Int32Array(cap); last.set(_hLast); _hLast = last; const next = new Int32Array(cap); next.set(_hNext); _hNext = next; const aStart = new Int32Array(cap); aStart.set(_hAStart); _hAStart = aStart; const aCount = new Int32Array(cap); aCount.set(_hACount); _hACount = aCount; _hCap = cap; }; /** Start a new parse: invalidate all prior refs, release prior heap refs. */ const _hReset = () => { _hN = 0; _aN = 0; _hStr.length = 0; _aName.length = 0; _aVal.length = 0; // Keep id 0 ("no node" / "no attribute") occupied so writes stay packed. _hStr.push(""); _aName.push(""); _aVal.push(null); _hDocPub = null; _hDocSys = null; }; // 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 _hRelease = () => { _hN = 0; _aN = 0; _hStr.length = 0; _aName.length = 0; _aVal.length = 0; _hSrc = ""; _hDocPub = null; _hDocSys = null; }; /** @type {(type: number, start: number, end: number) => HtmlNodeRef} */ const _hAlloc = (type, start, end) => { const i = ++_hN; if (i >= _hCap) _hGrow(i + 1); _hTy[i] = type; _hFl[i] = 0; _hSt[i] = start; _hEn[i] = end; _hTagEnd[i] = 0; _hNameEnd[i] = 0; _hCEnd[i] = 0; _hTc[i] = 0; _hParent[i] = 0; _hFirst[i] = 0; _hLast[i] = 0; _hNext[i] = 0; _hAStart[i] = 0; _hACount[i] = 0; // Ids are sequential, so this indexed write appends (array stays packed). _hStr[i] = ""; 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 _hAppend = (parent, node) => { _hParent[node] = parent; const last = _hLast[parent]; if (last === 0) _hFirst[parent] = node; else _hNext[last] = node; _hLast[parent] = node; }; /** @type {(data: string, start: number, end: number) => HtmlNodeRef} */ const _mkText = (data, start, end) => { const i = _hAlloc(NodeType.Text, start, end); _hStr[i] = data; return i; }; /** @type {(data: string, start: number, end: number) => HtmlNodeRef} */ const _mkComment = (data, start, end) => { const i = _hAlloc(NodeType.Comment, start, end); _hStr[i] = data; return i; }; // Marker entry in the active-formatting-elements list (never a valid ref). const AFE_MARKER = -1; // Clone of an element's attribute run: keep name/value (and the serializer // name flag) but drop source offsets so the consumer doesn't emit a duplicate // dependency for the reopened element's spans. Values are materialized since // the offsets are gone. const cloneAttrs = (/** @type {HtmlElement} */ el) => { const start = _hAStart[el]; const count = _hACount[el]; const newStart = _aN + 1; for (let i = start; i < start + count; i++) { const c = _aAlloc(_aName[i], _aValueOf(i), -1, -1, -1, -1); _aFl[c] = _aFl[i]; } return { start: newStart, count }; }; // Merge a repeated `<html>`/`<body>` tag's attributes into the element: only // names not already present are added (in source order after the existing // ones). Runs are contiguous, so any addition re-allocates the whole run; the // old slots are orphaned (at most once per repeated tag, rare). const mergeAttrs = ( /** @type {HtmlElement} */ el, /** @type {AttributeRun} */ run ) => { const start = _hAStart[el]; const count = _hACount[el]; let extra = 0; for (let i = run.start; i < run.start + run.count; i++) { if (_aFind(start, count, _aName[i]) === 0) extra++; } if (extra === 0) return; const newStart = _aN + 1; for (let i = start; i < start + count; i++) _aCopy(i); for (let i = run.start; i < run.start + run.count; i++) { if (_aFind(start, count, _aName[i]) === 0) _aCopy(i); } _hAStart[el] = newStart; _hACount[el] = count + extra; }; /** * 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 `buildHtmlAst` 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} HtmlDocument ref to the Document node */ /** @typedef {HtmlNodeRef} HtmlDocumentFragment ref to a DocumentFragment node */ /** @typedef {HtmlNodeRef} HtmlNode */ /** * An attribute reference: an integer id into the attribute columns, read * through the `A.attr*` accessors. 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; /** @typedef {{ type: typeof TOKEN_CHAR, data: string, start: number, end: number }} CharToken */ /** @typedef {{ type: typeof TOKEN_COMMENT, data: string, start: number, end: number }} CommentToken */ /** @typedef {{ type: typeof TOKEN_DOCTYPE, name: string, publicId: (string | null), systemId: (string | null), start: number, end: number }} DoctypeToken */ /** @typedef {{ type: typeof TOKEN_START_TAG, name: string, attrs: 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 */ /** * Internal token passed through the tree-construction insertion modes. * @typedef {CharToken | CommentToken | DoctypeToken | StartTagToken | EndTagToken | EofToken} Token */ /** * The tree builder reuses a single mutable token (with a reused `pos`) instead * of allocating one object per tokenizer callback. All fields are always * present so the shape never changes — keeping the `process`/insertion-mode * `t.*` reads monomorphic — and fields irrelevant to the current `type` carry * stale values that those handlers never read. Tokens that must outlive the * current callback (buffered table characters, synthesized re-dispatches) are * copied into fresh plain objects instead. * @typedef {{ type: number, name: string, data: string, attrs: AttributeRun, selfClosing: boolean, start: number, end: number, publicId: (string | null), systemId: (string | null), 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; // "in template" start-tag re-dispatch targets (§13.2.6.4.18). const TEMPLATE_START_TAG_MODES = new Map([ ["caption", MODE_IN_TABLE], ["colgroup", MODE_IN_TABLE], ["tbody", MODE_IN_TABLE], ["tfoot", MODE_IN_TABLE], ["thead", MODE_IN_TABLE], ["col", MODE_IN_COLUMN_GROUP], ["tr", MODE_IN_TABLE_BODY], ["td", MODE_IN_ROW], ["th", MODE_IN_ROW] ]); const VOID = new Set([ "area", "base", "br", "col", "embed", "hr", "img", "input", "keygen", "link", "meta", "param", "source", "track", "wbr" ]); const SPECIAL = new Set([ "address", "applet", "area", "article", "aside", "base", "basefont", "bgsound", "blockquote", "body", "br", "button", "caption", "center", "col", "colgroup", "dd", "details", "dir", "div", "dl", "dt", "embed", "fieldset", "figcaption", "figure", "footer", "form", "frame", "frameset", "h1", "h2", "h3", "h4", "h5", "h6", "head", "header", "hgroup", "hr", "html", "iframe", "img", "input", "keygen", "li", "link", "listing", "main", "marquee", "menu", "meta", "nav", "noembed", "noframes", "noscript", "object", "ol", "p", "param", "plaintext", "pre", "script", "search", "section", "select", "source", "style", "summary", "table", "tbody", "td", "template", "textarea", "tfoot", "th", "thead", "title", "tr", "track", "ul", "wbr", "xmp" ]); // MathML/SVG specials handled via namespace checks below. const FORMATTING = new Set([ "a", "b", "big", "code", "em", "font", "i", "nobr", "s", "small", "strike", "strong", "tt", "u" ]); const HEADING = new Set(["h1", "h2", "h3", "h4", "h5", "h6"]); const isSpecial = (/** @type {HtmlElement} */ el) => { const ns = _ns(el); const tag = _tag(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; }; // SVG tag name case adjustments (null prototype: looked up with markup-controlled // names, a plain object would resolve `constructor` etc. to inherited values) const SVG_TAG_ADJUST = Object.assign(Object.create(null), { altglyph: "altGlyph", altglyphdef: "altGlyphDef", altglyphitem: "altGlyphItem", animatecolor: "animateColor", animatemotion: "animateMotion", animatetransform: "animateTransform", clippath: "clipPath", feblend: "feBlend", fecolormatrix: "feColorMatrix", fecomponenttransfer: "feComponentTransfer", fecomposite: "feComposite", feconvolvematrix: "feConvolveMatrix", fediffuselighting: "feDiffuseLighting", fedisplacementmap: "feDisplacementMap", fedistantlight: "feDistantLight", fedropshadow: "feDropShadow", feflood: "feFlood", fefunca: "feFuncA", fefuncb: "feFuncB", fefuncg: "feFuncG", fefuncr: "feFuncR", fegaussianblur: "feGaussianBlur", feimage: "feImage", femerge: "feMerge", femergenode: "feMergeNode", femorphology: "feMorphology", feoffset: "feOffset", fepointlight: "fePointLight", fespecularlighting: "feSpecularLighting", fespotlight: "feSpotLight", fetile: "feTile", feturbulence: "feTurbulence", foreignobject: "foreignObject", glyphref: "glyphRef", lineargradient: "linearGradient", radialgradient: "radialGradient", textpath: "textPath" }); // SVG attribute case adjustments (lowercase -> camelCase). Subset commonly tested. // (null prototype, see SVG_TAG_ADJUST) const SVG_ATTR_ADJUST = Object.assign(Object.create(null), { attributename: "attributeName", attributetype: "attributeType", basefrequency: "baseFrequency", baseprofile: "baseProfile", calcmode: "calcMode", clippathunits: "clipPathUnits", diffuseconstant: "diffuseConstant", edgemode: "edgeMode", filterunits: "filterUnits", glyphref: "glyphRef", gradienttransform: "gradientTransform", gradientunits: "gradientUnits", kernelmatrix: "kernelMatrix", kernelunitlength: "kernelUnitLength", keypoints: "keyPoints", keysplines: "keySplines", keytimes: "keyTimes", lengthadjust: "lengthAdjust", limitingconeangle: "limitingConeAngle", markerheight: "markerHeight", markerunits: "markerUnits", markerwidth: "markerWidth", maskcontentunits: "maskContentUnits", maskunits: "maskUnits", numoctaves: "numOctaves", pathlength: "pathLength", patterncontentunits: "patternContentUnits", patterntransform: "patternTransform", patternunits: "patternUnits", pointsatx: "pointsAtX", pointsaty: "pointsAtY", pointsatz: "pointsAtZ", preservealpha: "preserveAlpha", preserveaspectratio: "preserveAspectRatio", primitiveunits: "primitiveUnits", refx: "refX", refy: "refY", repeatcount: "repeatCount", repeatdur: "repeatDur", requiredextensions: "requiredExtensions", requiredfeatures: "requiredFeatures", specularconstant: "specularConstant", specularexponent: "specularExponent", spreadmethod: "spreadMethod", startoffset: "startOffset", stddeviation: "stdDeviation", stitchtiles: "stitchTiles", surfacescale: "surfaceScale", systemlanguage: "systemLanguage", tablevalues: "tableValues", targetx: "targetX", targety: "targetY", textlength: "textLength", viewbox: "viewBox", viewtarget: "viewTarget", xchannelselector: "xChannelSelector", ychannelselector: "yChannelSelector", zoomandpan: "zoomAndPan" }); // Foreign attributes that get a prefix in the serialization (namespaced). // (null prototype, see SVG_TAG_ADJUST) const FOREIGN_ATTR_NS = Object.assign(Object.create(null), { "xlink:actuate": "xlink actuate", "xlink:arcrole": "xlink arcrole", "xlink:href": "xlink href", "xlink:role": "xlink role", "xlink:show": "xlink show", "xlink:title": "xlink title", "xlink:type": "xlink type", "xml:lang": "xml lang", "xml:space": "xml space", xmlns: "xmlns", "xmlns:xlink": "xmlns xlink" }); const MATHML_TEXT_INTEGRATION = new Set(["mi", "mo", "mn", "ms", "mtext"]); // Tag-name groups consulted by the insertion modes, as module-level Sets. The // tree builder runs these membership tests per token on hot paths; a shared // `Set.has` avoids allocating a fresh array and running `Array#includes` each // time. Grouped by where they are used. const MATHML_SPECIAL = new Set([ "mi", "mo", "mn", "ms", "mtext", "annotation-xml" ]); const SVG_SPECIAL = new Set(["foreignobject", "desc", "title"]); const HTML_SCOPE = new Set([ "applet", "caption", "html", "table", "td", "th", "marquee", "object", "template" ]); const IMPLIED = new Set([ "dd", "dt", "li", "optgroup", "option", "p", "rb", "rp", "rt", "rtc" ]); const IMPLIED_THOROUGH = new Set([ "caption", "colgroup", "dd", "dt", "li", "optgroup", "option", "p", "rb", "rp", "rt", "rtc", "tbody", "td", "tfoot", "th", "thead", "tr" ]); const CLEAR_TABLE = new Set(["table", "template", "html"]); const CLEAR_TABLE_BODY = new Set([ "tbody", "tfoot", "thead", "template", "html" ]); const CLEAR_TABLE_ROW = new Set(["tr", "template", "html"]); const TABLE_SCOPE_STOP = new Set(["html", "table", "template"]); const TABLE_CONTEXT = new Set(["table", "tbody", "tfoot", "thead", "tr"]); const TBODY_GROUP = new Set(["tbody", "tfoot", "thead"]); const TD_TH = new Set(["td", "th"]); const TD_TH_TR = new Set(["td", "th", "tr"]); const STYLE_SCRIPT_TEMPLATE = new Set(["style", "script", "template"]); const HEAD_BODY_HTML_BR = new Set(["head", "body", "html", "br"]); const BODY_HTML_BR = new Set(["body", "html", "br"]); // <base>/<basefont>/<bgsound>/<link>/<meta>: void elements inserted then // immediately popped in the "in head" mode. const HEAD_VOID_ELEMENTS = new Set([ "base", "basefont", "bgsound", "link", "meta" ]); const NOFRAMES_STYLE_NOSCRIPT = new Set(["noframes", "style", "noscript"]); // "in head noscript" start tags that are handled by reprocessing in "in head". const IN_HEAD_NOSCRIPT_PASSTHROUGH = new Set([ "basefont", "bgsound", "link", "meta", "noframes", "style" ]); const HEAD_ELEMENTS = new Set([ "base", "basefont", "bgsound", "link", "meta", "noframes", "script", "style", "template", "title" ]); const BLOCK_START = new Set([ "address", "article", "aside", "blockquote", "center", "details", "dialog", "dir", "div", "dl", "fieldset", "figcaption", "figure", "footer", "header", "hgroup", "main", "menu", "nav", "ol", "p", "search", "section", "summary", "ul" ]); const BLOCK_END = new Set([ "address", "article", "aside", "blockquote", "button", "center", "details", "dialog", "dir", "div", "dl", "fieldset", "figcaption", "figure", "footer", "header", "hgroup", "listing", "main", "menu", "nav", "ol", "pre", "search", "section", "summary", "ul" ]); const ADDRESS_DIV_P = new Set(["address", "div", "p"]); const APPLET_MARQUEE_OBJECT = new Set(["applet", "marquee", "object"]); const VOID_FORMATTING = new Set([ "area", "br", "embed", "img", "keygen", "wbr" ]); const PARAM_SOURCE_TRACK = new Set(["param", "source", "track"]); const IGNORED_BODY_TABLE_STARTS = new Set([ "caption", "col", "colgroup", "frame", "head", "tbody", "td", "tfoot", "th", "thead", "tr" ]); const IN_TABLE_IGNORED_ENDS = new Set([ "body", "caption", "col", "colgroup", "html", "tbody", "td", "tfoot", "th", "thead", "tr" ]); const CAPTION_TABLE_STARTS = new Set([ "caption", "col", "colgroup", "tbody", "td", "tfoot", "th", "thead", "tr" ]); const CAPTION_IGNORED_ENDS = new Set([ "body", "col", "colgroup", "html", "tbody", "td", "tfoot", "th", "thead", "tr" ]); const TBODY_TRIGGER_STARTS = new Set([ "caption", "col", "colgroup", "tbody", "tfoot", "thead" ]); const TBODY_IGNORED_ENDS = new Set([ "body", "caption", "col", "colgroup", "html", "td", "th", "tr" ]); const ROW_TRIGGER_STARTS = new Set([ "caption", "col", "colgroup", "tbody", "tfoot", "thead", "tr" ]); const ROW_IGNORED_ENDS = new Set([ "body", "caption", "col", "colgroup", "html", "td", "th" ]); const CELL_IGNORED_ENDS = new Set([ "body", "caption", "col", "colgroup", "html" ]); const NO_DECODE_TEXT = new Set([ "script", "style", "xmp", "iframe", "noembed", "noframes", "plaintext" ]); // HTML start tags that break out of foreign (SVG/MathML) content. const FOREIGN_BREAKOUT = new Set([ "b", "big", "blockquote", "body", "br", "center", "code", "dd", "div", "dl", "dt", "em", "embed", "h1", "h2", "h3", "h4", "h5", "h6", "head", "hr", "i", "img", "li", "listing", "menu", "meta", "nobr", "ol", "p", "pre", "ruby", "s", "small", "span", "strong", "strike", "sub", "sup", "table", "tt", "u", "ul", "var" ]); const FONT_BREAKOUT_ATTRS = new Set(["color", "face", "size"]); // `<font color|face|size>` breaks out of foreign content (§13.2.6.5). const hasFontBreakoutAttr = (/** @type {AttributeRun} */ run) => { for (let i = run.start; i < run.start + run.count; i++) { if (FONT_BREAKOUT_ATTRS.has(_aName[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 = ((h << 5) - h + 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 | string[] | undefined)[] }} 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 | string[] | undefined)[]} */ const values = Array.from({ length: size }); for (const name of unique) { const h = hashLowerName(name); let slot = h & mask; while (values[slot] !== undefined && hashes[slot] !== h) { slot = (slot + 1) & mask; } const cur = values[slot]; if (cur === undefined) { hashes[slot] = h; values[slot] = name; } else if (typeof cur === "string") { values[slot] = [cur, name]; } else { cur.push(name); } } return { mask, hashes, values }; }; /** @typedef {ReturnType<typeof buildNameInternTable>} NameInternTable */ /** * 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) => { let h = end - start; for (let i = start; i < end; i++) { let c = input.charCodeAt(i); if (c >= 0x41 && c <= 0x5a) c += 0x20; h = ((h << 5) - h + c) | 0; } const { mask, hashes, values } = table; let slot = h & mask; for (;;) { const hit = values[slot]; if (hit === undefined) break; if (hashes[slot] === h) { if (typeof hit === "string") { if (rangeEqualsLower(input, start, end, hit)) return hit; } else { for (let i = 0; i < hit.length; i++) { if (rangeEqualsLower(input, start, end, hit[i])) return hit[i]; } } break; } slot = (slot + 1) & mask; } // Unknown name (custom element, data-* attribute, non-ASCII, …). return input.slice(start, end).toLowerCase(); }; // Every tag name the tree builder compares against (the sets above already // cover most of the spec), plus the remaining standard/foreign names so // ordinary documents intern every tag. const TAG_NAME_INTERN = buildNameInternTable([ ...VOID, ...SPECIAL, ...FORMATTING, ...HEADING, ...MATHML_TEXT_INTEGRATION, ...MATHML_SPECIAL, ...SVG_SPECIAL, ...HTML_SCOPE, ...TABLE_CONTEXT, ...VOID_FORMATTING, ...NO_DECODE_TEXT, ...FOREIGN_BREAKOUT, ...HEAD_ELEMENTS, ...Object.keys(SVG_TAG_ADJUST), ..."a abbr audio bdi bdo canvas cite data datalist del dfn dialog ins kbd label legend map mark math menuitem meter optgroup option output picture progress q rb rp rt rtc ruby samp selectedcontent slot span sub sup svg time u var video".split( " " ) ]); // Common attribute names (unknown ones — data-*, ARIA, events — fall back). const ATTR_NAME_INTERN = buildNameInternTable( "href src srcset sizes alt title class id style name type value content charset rel media target action method placeholder disabled checked selected multiple readonly required hidden tabindex role lang dir width height loading decoding async defer integrity crossorigin referrerpolicy nonce as for colspan rowspan span label max min step pattern autocomplete autofocus autoplay controls loop muted poster preload download ping imagesrcset imagesizes slot part is property http-equiv accept enctype novalidate maxlength minlength size cols rows wrap open scope headers datetime cite usemap ismap shape coords start reversed face color encoding xmlns".split( " " ) ); // Hoisted so the many `open.some(...)` "is there an open HTML <template>?" // checks reuse one predicate instead of allocating an arrow per call. const isHtmlTemplateEl = (/** @type {HtmlElement} */ e) => _tag(e) === "template" && _ns(e) === NS_HTML; // Raw text / escapable raw text elements (WHATWG §13.1.2) plus the other // RAWTEXT/RCDATA/PLAINTEXT-tokenized elements. Under `skip.text` their body's // end offset is recorded on the element (see `insertCharacters`) so a consumer // can read the raw content span without a `Text` node. const RAW_TEXT_ELEMENTS = new Set([ "script", "style", "textarea", "title", "xmp", "iframe", "noembed", "noframes", "noscript", "plaintext" ]); /** Shared empty skip set so the common (no-skip) call allocates nothing. */ 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. */ /** * @param {string} source HTML source * @param {string=} fragmentContext context element name for fragment parsing (e.g. `td`, `svg path`); omit for a full document * @param {HtmlAstSkip=} skip node kinds to omit from the AST (see `HtmlAstSkip`); omit to build the full tree * @returns {HtmlDocument} ref to the document node — read through `A`; valid until the next parse */ const buildHtmlAst = (source, fragmentContext, skip = EMPTY_SKIP) => { const skipText = skip.text === true; const skipComments = skip.comments === true; const skipDoctype = skip.doctype === true; _hReset(); _hSrc = source; const doc = _hAlloc(NodeType.Document, 0, 0); let mode = MODE_INITIAL; // Mode to return to after MODE_TEXT / MODE_IN_TABLE_TEXT (0 = unset). let originalMode = 0; /** @type {HtmlElement[]} stack of open elements (bottom .. top) */ const open = []; /** @type {HtmlNodeRef[]} active formatting elements (AFE_MARKER = marker) */ const afe = []; /** @type {HtmlElement} 0 = none */ let head = 0; /** @type {HtmlElement} 0 = none */ let form = 0; let framesetOk = true; // First attribute id of the tag currently being tokenized (0 = none). let pendAttrStart = 0; let fosterParenting = false; /** @type {number[]} */ const templateModes = []; let quirks = false; /** @type {HtmlElement} fragment context element (0 = document parse) */ let fragment = 0; // End offset of the token currently being processed (for element `.end`). let tokenEnd = 0; const cur = () => /** @type {HtmlElement} */ (open[open.length - 1]); const adjustedCurrent = () => { if (open.length === 1 && fragment) return fragment; return cur(); }; const mkEl = ( /** @type {string} */ tagName, /** @type {number} */ ns, /** @type {AttributeRun} */ attrs, /** @type {TagPos | null | undefined} */ pos ) => { const el = _hAlloc( NodeType.Element, pos ? pos.start : 0, pos ? pos.end : 0 ); // Void HTML elements are marked self-closing and never receive children. _hFl[el] = ns === NS_HTML && VOID.has(tagName) ? ns | FLAG_SELF_CLOSING : ns; _hStr[el] = tagName; _hAStart[el] = attrs.start; _hACount[el] = attrs.count; if (pos) { _hTagEnd[el] = pos.tagEnd; _hNameEnd[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. _hCEnd[el] = pos.tagEnd; } 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 = _hTc[parent]; return tc !== 0 ? tc : parent; }; const appendTo = ( /** @type {HtmlNodeRef} */ parent, /** @type {HtmlNodeRef} */ node ) => { const p = effParent(parent); const last = _hLast[p]; if ( _hTy[node] === NodeType.Text && last !== 0 && _hTy[last] === NodeType.Text ) { _hStr[last] += _hStr[node]; _hEn[last] = _hEn[node]; return; } _hAppend(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 }; const placeAt = ( /** @type {HtmlNodeRef} */ parent, /** @type {HtmlNodeRef} */ beforeNode ) => { sharedPlace.parent = parent; sharedPlace.beforeNode = beforeNode; return sharedPlace; }; // "appropriate place for inserting a node" const appropriatePlace = () => { const target = cur(); if ( fosterParenting && TABLE_CONTEXT.has(_tag(target)) && _ns(target) === NS_HTML ) { // find last template / last table let lastTemplate = -1; let lastTable = -1; for (let i = open.length - 1; i >= 0; i--) { if ( _tag(open[i]) === "template" && _ns(open[i]) === NS_HTML && lastTemplate === -1 ) { lastTemplate = i; } if ( _tag(open[i]) === "table" && _ns(open[i]) === NS_HTML && lastTable === -1 ) { lastTable = i; } } if ( lastTemplate !== -1 && (lastTable === -1 || lastTemplate > lastTable) ) { return placeAt(open[lastTemplate], 0); } if (lastTable === -1) { return placeAt(open[0], 0); } const table = open[lastTable]; const tp = _hParent[table]; if (tp !== 0) return placeAt(tp, table); return placeAt(open[lastTable - 1], 0); } return placeAt(target, 0); }; const insertAtPlace = ( /** @type {InsertionPlace} */ place, /** @type {HtmlNodeRef} */ 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 = _hFirst[p]; while (c !== 0 && c !== before) { prev = c; c = _hNext[c]; } if (c === 0) { // `before` not under `parent` (not reachable from the spec paths). appendTo(place.parent, node); return; } if ( _hTy[node] === NodeType.Text && prev !== 0 && _hTy[prev] === NodeType.Text ) { // Before-node merge deliberately does not bump the sibling's `end`. _hStr[prev] += _hStr[node]; return; } _hParent[node] = p; _hNext[node] = before; if (prev === 0) _hFirst[p] = node; else _hNext[prev] = node; } else { appendTo(place.parent, node); } }; const insertCharacters = ( /** @type {string} */ data, /** @type {number} */ start, /** @type {number} */ end ) => { const place = appropriatePlace(); if (_hTy[place.parent] === NodeType.Document) return; // never insert text into document // `skip.text`: drop every `Text` node — construction already used the // decoded token, so removing the node never affects element structure. // For a raw-text element record the body end so a consumer reads the span // [`tagEnd`, `contentEnd`] without a `Text` node (see `HtmlParser`). // Namespace-agnostic: `HtmlParser` extracts `<script>`/`<style>` bodies in // foreign content (e.g. SVG `<style>`) too. if (skipText) { const p = place.parent; if (_hTy[p] === NodeType.Element && RAW_TEXT_ELEMENTS.has(_tag(p))) { _hCEnd[p] = end; } return; } // Inlined text insert: when the run merges into the adjacent text sibling // (common with inline formatting) only the string is appended — no // throwaway text node is allocated. Mirrors `insertAtPlace`/`appendTo`, // including that the before-node merge does not bump `end`. const p = effParent(place.parent); const before = place.beforeNode; if (before !== 0) { let prev = 0; let c = _hFirst[p]; while (c !== 0 && c !== before) { prev = c; c = _hNext[c]; } if (c === 0) { // `before` not under `parent` (not reachable from the spec paths). _hAppend(p, _mkText(data, start, end)); return; } if (prev !== 0 && _hTy[prev] === NodeType.Text) { _hStr[prev] += data; return; } const node = _mkText(data, start, end); _hParent[node] = p; _hNext[node] = before; if (prev === 0) _hFirst[p] = node; else _hNext[prev] = node; } else { const last = _hLast[p]; if (last !== 0 && _hTy[last] === NodeType.Text) { _hStr[last] += data; _hEn[last] = end; return; } _hAppend(p, _mkText(data, start, end)); } }; /** * @param {string} data comment data * @param {number} start start offset * @param {number} end end offset * @param {InsertionPlace=} place explicit insertion place */ const insertComment = (data, start, end, place) => { if (skipComments) return; const p = place || appropriatePlace(); insertAtPlace(p, _mkComment(data, start, end)); }; const insertHtmlElement = ( /** @type {string} */ tagName, /** @type {AttributeRun} */ attrs, /** @type {TagPos | null} */ pos ) => { const el = mkEl(tagName, NS_HTML, attrs, pos); const place = appropriatePlace(); insertAtPlace(place, el); open.push(el); return el; }; const insertForeignElement = ( /** @type {string} */ tagName, /** @type {number} */ ns, /** @type {AttributeRun} */ attrs, /** @type {TagPos | null} */ pos ) => { const el = mkEl(tagName, ns, attrs, pos); const place = appropriatePlace(); insertAtPlace(place, el); open.push(el); return el; }; // ---- scopes ---- const isScopeBoundary = (/** @type {HtmlElement} */ el) => { if (_ns(el) === NS_HTML) return HTML_SCOPE.has(_tag(el)); if (_ns(el) === NS_MATHML) return MATHML_SPECIAL.has(_tag(el)); if (_ns(el) === NS_SVG) { return SVG_SPECIAL.has(_tag(el).toLowerCase()); } return false; }; // Scope "kind" selects which extra elements act as boundaries. Passed as a // small int so the scope checks below allocate no per-call predicate closure // (these run several times per body tag). const SCOPE_DEFAULT = 0; const SCOPE_BUTTON = 1; const SCOPE_LIST_ITEM = 2; const isBoundaryForKind = ( /** @type {HtmlElement} */ el, /** @type {number} */ kind ) => { if (isScopeBoundary(el)) return true; if (_ns(el) !== NS_HTML) return false; if (kind === SCOPE_BUTTON) return _tag(el) === "button"; if (kind === SCOPE_LIST_ITEM) { return _tag(el) === "ol" || _tag(el) === "ul"; } return false; }; // "have an element in scope": walk the open stack from the top until the // named HTML element is found (true) or a scope boundary is hit (false). const hasNameInScope = ( /** @type {string} */ tagName, /** @type {number} */ kind ) => { for (let i = open.length - 1; i >= 0; i--) { const el = open[i]; if (_ns(el) === NS_HTML && _tag(el) === tagName) return true; if (isBoundaryForKind(el, kind)) return false; } return false; }; const inScope = (/** @type {string} */ tagName) => hasNameInScope(tagName, SCOPE_DEFAULT); const inButtonScope = (/** @type {string} */ tagName) => hasNameInScope(tagName, SCOPE_BUTTON); const inListItemScope = (/** @type {string} */ tagName) => hasNameInScope(tagName, SCOPE_LIST_ITEM); const inScopeEl = (/** @type {HtmlElement} */ target) => { for (let i = open.length - 1; i >= 0; i--) { const el = open[i]; if (el === target) return true; if (isScopeBoundary(el)) return false; } return false; }; // `target` is a single tag name (the common case) or a Set of names. const inTableScope = (/** @type {string | Set<string>} */ target) => { const set = typeof target === "string" ? null : target; for (let i = open.length - 1; i >= 0; i--) { const el = open[i]; if (_ns(el) === NS_HTML) { if (set ? set.has(_tag(el)) : _tag(el) === target) return true; if (TABLE_SCOPE_STOP.has(_tag(el))) return false; } } return false; }; const generateImpliedEndTags = (except = "") => { while (open.length) { const el = cur(); if (_ns(el) === NS_HTML && IMPLIED.has(_tag(el)) && _tag(el) !== except) { open.pop(); } else { break; } } }; const generateImpliedEndTagsThorough = () => { while (open.length) { const el = cur(); if (_ns(el) === NS_HTML && IMPLIED_THOROUGH.has(_tag(el))) { open.pop(); } else { break; } } }; // ---- active formatting elements ---- const pushAfe = (/** @type {HtmlElement} */ el) => { let count = 0; for (let i = afe.length - 1; i >= 0; i--) { const e = afe[i]; if (e === AFE_MARKER) break; if (_tag(e) === _tag(el) && _ns(e) === _ns(el) && sameAttrs(e, el)) { count++; if (count === 3) { afe.splice(i, 1); break; } } } afe.push(el); }; const sameAttrs = ( /** @type {HtmlElement} */ a, /** @type {HtmlElement} */ b ) => { const aStart = _hAStart[a]; const aCount = _hACount[a]; const bStart = _hAStart[b]; const bCount = _hACount[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 = _aFind(aStart, aCount, _aName[i]); if (j === 0 || _aValueOf(j) !== _aValueOf(i)) return false; } return true; }; 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]; const el = mkEl(_tag(e), _ns(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} */ (open.pop()); _hEn[el] = tokenEnd; if (_ns(el) === NS_HTML && _tag(el) === "p") break; } }; const popUntil = (/** @type {string} */ tagName) => { while (open.length) { const el = /** @type {HtmlElement} */ (open.pop()); _hEn[el] = tokenEnd; if (_ns(el) === NS_HTML && _tag(el) === tagName) break; } }; const popUntilOneOf = (/** @type {Set<string>} */ set) => { while (open.length) { const el = /** @type {HtmlElement} */ (open.pop()); _hEn[el] = tokenEnd; if (_ns(el) === NS_HTML && set.has(_tag(el))) 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 = _tag(node); if (_ns(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 = head ? MODE_AFTER_HEAD : MODE_BEFORE_HEAD; return; } } if (last) { mode = MODE_IN_BODY; return; } } }; // ---------- token processing ---------- // Split a character token's leading whitespace; per the spec each character // is its own token, so a mixed run can straddle a mode change. Inserts the // leading whitespace when `insert`, returns the non-whitespace remainder // token (or null when the token was entirely whitespace). const leadingWs = ( /** @type {CharToken} */ t, /** @type {boolean} */ insert ) => { const m = /^[\t\n\f\r ]+/.exec(t.data); const ws = m ? m[0] : ""; if (ws && insert) insertCharacters(ws, t.start, t.start + ws.length); if (ws.length === t.data.length) return null; return { ...t, data: t.data.slice(ws.length), start: t.start + ws.length }; }; const isAllWs = (/** @type {string} */ s) => { for (let i = 0; i < s.length; i++) { const c = s.charCodeAt(i); // HTML whitespace: tab / LF / FF / CR / space. A charCodeAt loop avoids // the `for…of` code-point iterator + per-char string + Set lookup. if (c !== 0x09 && c !== 0x0a && c !== 0x0c && c !== 0x0d && c !== 0x20) { return false; } } return true; }; const process = (/** @type {Token} */ t) => { // Track the current token's end so explicit closes can set element `.end`. // Dispatch on `type` instead of the `in` operator, which goes megamorphic // across the token union and shows up on the per-token hot path. const ty = t.type; if (ty === TOKEN_START_TAG || ty === TOKEN_END_TAG) tokenEnd = t.pos.end; else if (ty !== TOKEN_EOF) tokenEnd = t.end; // foreign content dispatch const ac = adjustedCurrent(); const useForeign = open.length > 0 && ac && _ns(ac) !== NS_HTML && ty !== TOKEN_EOF && shouldUseForeignRules(ac, t); if (useForeign) { foreignContent(t); return; } runMode(t); }; const shouldUseForeignRules = ( /** @type {HtmlElement} */ ac, /** @type {Token} */ t ) => { if (_ns(ac) === NS_HTML) return false; if (t.type === TOKEN_START_TAG) { if ( mathmlTextIntegrationPoint(ac) && t.name !== "mglyph" && t.name !== "malignmark" ) { return false; } if ( _ns(ac) === NS_MATHML && _tag(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 (t.type === TOKEN_COMMENT) return true; return false; }; const mathmlTextIntegrationPoint = (/** @type {HtmlElement} */ el) => _ns(el) === NS_MATHML && MATHML_TEXT_INTEGRATION.has(_tag(el)); const htmlIntegrationPoint = (/** @type {HtmlElement} */ el) => { if (_ns(el) === NS_MATHML && _tag(el) === "annotation-xml") { const enc = _aFind(_hAStart[el], _hACount[el], "encoding"); if (enc !== 0) { const value = _aValueOf(enc).toLowerCase(); if (value === "text/html" || value === "application/xhtml+xml") { return true; } } return false; } if (_ns(el) === NS_SVG && SVG_SPECIAL.has(_tag(el).toLowerCase())) { return true; } return false; }; const adjustSvgTag = (/** @type {string} */ name) => /** @type {Record<string, string>} */ (SVG_TAG_ADJUST)[name] || name; // Adjust a foreign start tag's attribute run in place (the run is consumed // only by this tag's element): SVG camelCase names are rewritten, and // namespaced names get the serializer-name flag (see `_aSerializedName`). const adjustForeignAttrs = ( /** @type {AttributeRun} */ run, /** @type {number} */ ns ) => { for (let i = run.start; i < run.start + run.count; i++) { const name = _aName[i]; if ( ns === NS_SVG && /** @type {Record<string, string>} */ (SVG_ATTR_ADJUST)[name] ) { _aName[i] = /** @type {Record<string, string>} */ (SVG_ATTR_ADJUST)[ name ]; } if (/** @type {Record<string, string>} */ (FOREIGN_ATTR_NS)[name]) { _aFl[i] |= 1; } } return run; }; const foreignContent = (/** @type {Token} */ t) => { if (t.type === TOKEN_CHAR) { const data = t.data.replace(/\0/g, "�"); insertCharacters(data, t.start, t.end); // eslint-disable-next-line no-control-regex if (/[^\t\n\f\r \u0000]/.test(t.data)) framesetOk = false; return; } if (t.type === TOKEN_COMMENT) { insertComment(t.data, t.start, t.end); return; } if (t.type === TOKEN_DOCTYPE) return; if (t.type === TOKEN_START_TAG) { const acn = _ns(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 ( _ns(c) === NS_HTML || mathmlTextIntegrationPoint(c) || htmlIntegrationPoint(c) ) { break; } open.pop(); } runMode(t); return; } const ns = acn; let name = t.name; let attrs = t.attrs; if (ns === NS_SVG) { name = adjustSvgTag(name); } if (ns === NS_MATHML) { attrs = adjustMathmlAttrs(attrs); } attrs = adjustForeignAttrs(attrs, ns); insertForeignElement(name, ns, attrs, t.pos); if (t.selfClosing) { open.pop(); } return; } if (t.type === TOKEN_END_TAG) { if ( t.name === "script" && _tag(cur()) === "script" && _ns(cur()) === NS_SVG ) { open.pop(); return; } // `</p>` and `</br>` break out: pop foreign elements up to the // nearest HTML element or integration point, then process in HTML. if (t.name === "p" || t.name === "br") { while ( open.length > 1 && _ns(cur()) !== NS_HTML && !mathmlTextIntegrationPoint(cur()) && !htmlIntegrationPoint(cur()) ) { open.pop(); } runMode(t); return; } // any other end tag let i = open.length - 1; let node = open[i]; if (_tag(node).toLowerCase() !== t.name) { /* parse error */ } while (i >= 0) { node = open[i]; if (i === 0) return; if (_ns(node) !== NS_HTML && _tag(node).toLowerCase() === t.name) { while (open.length > i) open.pop(); return; } i--; if (open[i] && _ns(open[i]) === NS_HTML) { runMode(t); return; } } } }; // ---------- adoption agency algorithm ---------- const adoptionAgency = ( /** @type {string} */ subject, /** @type {TagPos | null} */ pos ) => { // step 1 const c = cur(); if (_ns(c) === NS_HTML && _tag(c) === subject && !afe.includes(c)) { open.pop(); return true; } let outer = 0; while (outer < 8) { outer++; // find formatting element let fmtIdx = -1; for (let i = afe.length - 1; i >= 0; i--) { if (afe[i] === AFE_MARKER) break; if (_tag(afe[i]) === subject && _ns(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) { afe.splice(fmtIdx, 1); return true; } if (!inScopeEl(fmt)) return true; // parse error, ignore // step: furthest block let furthestIdx = -1; for (let i = openIdx + 1; i < open.length; i++) { if (isSpecial(open[i])) { furthestIdx = i; break; } } if (furthestIdx === -1) { while (open.length > openIdx) open.pop(); afe.splice(fmtIdx, 1); return true; } const furthest = open[furthestIdx]; const commonAncestor = open[openIdx - 1]; let bookmark = fmtIdx; let node = furthest; let lastNode = furthest; let nodeIdx = furthestIdx; let inner = 0; while (true) { inner++; nodeIdx--; node = open[nodeIdx]; if (node === fmt) break; let nodeAfeIdx = afe.indexOf(node); if (inner > 3 && nodeAfeIdx !== -1) { afe.splice(nodeAfeIdx, 1); if (nodeAfeIdx < bookmark) bookmark--; nodeAfeIdx = -1; } if (nodeAfeIdx === -1) { open.splice(nodeIdx, 1); continue; } // create clone const clone = mkEl(_tag(node), _ns(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 const cloneFmt = mkEl(_tag(fmt), _ns(fmt), cloneAttrs(fmt), null); // Take all direct children of `furthest` (a template's content fragment // deliberately stays put — mirrors childrenOf-less spec behavior here). let k = _hFirst[furthest]; _hFirst[furthest] = 0; _hLast[furthest] = 0; while (k !== 0) { const next = _hNext[k]; _hNext[k] = 0; _hParent[k] = 0; appendTo(cloneFmt, k); k = next; } appendTo(furthest, cloneFmt); // remove fmt from afe, insert clone at bookmark const curFmtIdx = afe.indexOf(fmt); if (curFmtIdx !== -1) { afe.splice(curFmtIdx, 1); 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) open.splice(ofi, 1); const newFurthestIdx = open.indexOf(furthest); open.splice(newFurthestIdx + 1, 0, cloneFmt); } return true; }; const placeForCommonAncestor = ( /** @type {HtmlElement} */ commonAncestor ) => { if ( TABLE_CONTEXT.has(_tag(commonAncestor)) && _ns(commonAncestor) === NS_HTML ) { // foster // reuse appropriatePlace logic but rooted differently: emulate let lastTemplate = -1; let lastTable = -1; for (let i = open.length - 1; i >= 0; i--) { if ( _tag(open[i]) === "template" && _ns(open[i]) === NS_HTML && lastTemplate === -1 ) { lastTemplate = i; } if ( _tag(open[i]) === "table" && _ns(open[i]) === NS_HTML && lastTable === -1 ) { lastTable = i; } } if ( lastTemplate !== -1 && (lastTable === -1 || lastTemplate > lastTable) ) { return { parent: open[lastTemplate], beforeNode: 0 }; } if (lastTable === -1) return { parent: open[0], beforeNode: 0 }; const table = open[lastTable]; const tp = _hParent[table]; if (tp !== 0) return { parent: tp, beforeNode: table }; return { parent: open[lastTable - 1], beforeNode: 0 }; } return { parent: commonAncestor, beforeNode: 0 }; }; const detach = (/** @type {HtmlNodeRef} */ node) => { const p = _hParent[node]; if (p === 0) return; let prev = 0; let c = _hFirst[p]; while (c !== 0 && c !== node) { prev = c; c = _hNext[c]; } if (c === 0) return; if (prev === 0) _hFirst[p] = _hNext[node]; else _hNext[prev] = _hNext[node]; if (_hLast[p] === node) _hLast[p] = prev; _hNext[node] = 0; _hParent[node] = 0; }; // ---------- insertion modes ---------- /** @type {Record<string, (t: Token) => void>} */ const modes = {}; // Dispatch the current insertion mode. An integer switch (cases ordered by // frequency) keeps each `modes.x(t)` call site monomorphic, where a keyed // `modes[mode]` load + indirect call would defeat inlining on the per-token // hot path. const runMode = (/** @type {Token} */ t) => { switch (mode) { case MODE_IN_BODY: return modes.inBody(t); case MODE_TEXT: return modes.text(t); case MODE_IN_CELL: return modes.inCell(t); case MODE_IN_ROW: return modes.inRow(t); case MODE_IN_TABLE_BODY: return modes.inTableBody(t); case MODE_IN_TABLE: return modes.inTable(t); case MODE_IN_TABLE_TEXT: return modes.inTableText(t); case MODE_IN_CAPTION: return modes.inCaption(t); case MODE_IN_COLUMN_GROUP: return modes.inColumnGroup(t); case MODE_IN_TEMPLATE: return modes.inTemplate(t); case MODE_IN_HEAD: return modes.inHead(t); case MODE_IN_HEAD_NOSCRIPT: return modes.inHeadNoscript(t); case MODE_AFTER_HEAD: return modes.afterHead(t); case MODE_BEFORE_HEAD: return modes.beforeHead(t); case MODE_BEFORE_HTML: return modes.beforeHtml(t); case MODE_INITIAL: return modes.initial(t); case MODE_AFTER_BODY: return modes.afterBody(t); case MODE_AFTER_AFTER_BODY: return modes.afterAfterBody(t); case MODE_IN_FRAMESET: return modes.inFrameset(t); case MODE_AFTER_FRAMESET: return modes.afterFrameset(t); // MODE_AFTER_AFTER_FRAMESET — every mode is enumerated, so the last // one is the `default` (also satisfies exhaustiveness linting). default: return modes.afterAfterFrameset(t); } }; const QUIRKY_PREFIXES = [ "+//silmaril//dtd html pro v0r11 19970101//", "-//as//dtd html 3.0 aswedit + extensions//", "-//advasoft ltd//dtd html 3.0 aswedit + extensions//", "-//ietf//dtd html 2.0 level 1//", "-//ietf//dtd html 2.0 level 2//", "-//ietf//dtd html 2.0 strict level 1//", "-//ietf//dtd html 2.0 strict level 2//", "-//ietf//dtd html 2.0 strict//", "-//ietf//dtd html 2.0//", "-//ietf//dtd html 2.1e//", "-//ietf//dtd html 3.0//", "-//ietf//dtd html 3.2 final//", "-//ietf//dtd html 3.2//", "-//ietf//dtd html 3//", "-//ietf//dtd html level 0//", "-//ietf//dtd html level 1//", "-//ietf//dtd html level 2//", "-//ietf//dtd html level 3//", "-//ietf//dtd html strict level 0//", "-//ietf//dtd html strict level 1//", "-//ietf//dtd html strict level 2//", "-//ietf//dtd html strict level 3//", "-//ietf//dtd html strict//", "-//ietf//dtd html//", "-//metrius//dtd metrius presentational//", "-//microsoft//dtd internet explorer 2.0 html strict//", "-//microsoft//dtd internet explorer 2.0 html//", "-//microsoft//dtd internet explorer 2.0 tables//", "-//microsoft//dtd internet explorer 3.0 html strict//", "-//microsoft//dtd internet explorer 3.0 html//", "-//microsoft//dtd internet explorer 3.0 tables//", "-//netscape comm. corp.//dtd html//", "-//netscape comm. corp.//dtd strict html//", "-//o'reilly and associates//dtd html 2.0//", "-//o'reilly and associates//dtd html extended 1.0//", "-//o'reilly and associates//dtd html extended relaxed 1.0//", "-//sq//dtd html 2.0 hotmetal + extensions//", "-//softquad software//dtd hotmetal pro 6.0::19990601::extensions to html 4.0//", "-//softquad//dtd hotmetal pro 4.0::19971010::extensions to html 4.0//", "-//spyglass//dtd html 2.0 extended//", "-//sun microsystems corp.//dtd hotjava html//", "-//sun microsystems corp.//dtd hotjava strict html//", "-//w3c//dtd html 3 1995-03-24//", "-//w3c//dtd html 3.2 draft//", "-//w3c//dtd html 3.2 final//", "-//w3c//dtd html 3.2//", "-//w3c//dtd html 3.2s draft//", "-//w3c//dtd html 4.0 frameset//", "-//w3c//dtd html 4.0 transitional//", "-//w3c//dtd html experimental 19960712//", "-//w3c//dtd html experimental 970421//", "-//w3c//dtd w3 html//", "-//w3o//dtd w3 html 3.0//", "-//webtechs//dtd mozilla html 2.0//", "-//webtechs//dtd mozilla html//" ]; const QUIRKY_EXACT = new Set([ "-//w3o//dtd w3 html strict 3.0//en//", "-/w3c/dtd html 4.0 transitional/en", "html" ]); const isQuirky = ( /** @type {string} */ name, /** @type {string | null} */ pub, /** @type {string | null} */ sys ) => { if (name !== "html") return true; const p = pub ? pub.toLowerCase() : null; const sl = sys ? sys.toLowerCase() : null; if (p !== null) { if (QUIRKY_EXACT.has(p)) return true; for (const pre of QUIRKY_PREFIXES) if (p.startsWith(pre)) return true; if ( sl === null && (p.startsWith("-//w3c//dtd html 4.01 frameset//") || p.startsWith("-//w3c//dtd html 4.01 transitional//")) ) { return true; } } if (sl === "http://www.ibm.com/data/dtd/v11/ibmxhtml1-transitional.dtd") { return true; } return false; }; modes.initial = (t) => { if (t.type === TOKEN_CHAR) { const r = leadingWs(t, false); if (!r) return; quirks = true; mode = MODE_BEFORE_HTML; process(r); return; } if (t.type === TOKEN_COMMENT) { insertComment(t.data, t.start, t.end, { parent: doc, beforeNode: 0 }); return; } if (t.type === TOKEN_DOCTYPE) { // `skip.doctype` drops the node only; quirks detection below is unaffected. if (!skipDoctype) { const dt = _hAlloc(NodeType.Doctype, t.start, t.end); _hStr[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. _hDocPub = t.publicId; _hDocSys = t.systemId; _hAppend(doc, dt); } quirks = isQuirky(t.name, t.publicId, t.systemId); mode = MODE_BEFORE_HTML; return; } quirks = true; mode = MODE_BEFORE_HTML; process(t); }; modes.beforeHtml = (t) => { if (t.type === TOKEN_DOCTYPE) return; if (t.type === TOKEN_COMMENT) { insertComment(t.data, t.start, t.end, { parent: doc, beforeNode: 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); _hAppend(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); _hAppend(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 (t.type === TOKEN_COMMENT) { insertComment(t.data, t.start, t.end); return; } if (t.type === TOKEN_DOCTYPE) return; if (t.type === TOKEN_START_TAG && t.name === "html") return modes.inBody(t); if (t.type === TOKEN_START_TAG && t.name === "head") { head = insertHtmlElement("head", t.attrs, t.pos); mode = MODE_IN_HEAD; return; } if (t.type === TOKEN_END_TAG && !HEAD_BODY_HTML_BR.has(t.name)) { return; } head = insertHtmlElement("head", EMPTY_ATTRS, null); mode = MODE_IN_HEAD; process(t); }; modes.inHead = (t) => { if (t.type === TOKEN_CHAR) { const r = leadingWs(t, true); if (!r) return; open.pop(); mode = MODE_AFTER_HEAD; process(r); return; } if (t.type === TOKEN_COMMENT) { insertComment(t.data, t.start, t.end); return; } if (t.type === TOKEN_DOCTYPE) return; if (t.type === TOKEN_START_TAG) { if (t.name === "html") return modes.inBody(t); if (HEAD_VOID_ELEMENTS.has(t.name)) { insertHtmlElement(t.name, t.attrs, t.pos); open.pop(); return; } if (t.name === "title") { genericRcdata(t); return; } if (NOFRAMES_STYLE_NOSCRIPT.has(t.name)) { if (t.name === "noscript") { insertHtmlElement("noscript", t.attrs, t.pos); mode = MODE_IN_HEAD_NOSCRIPT; return; } genericRawtext(t); return; } if (t.name === "script") { genericRawtext(t); return; } if (t.name === "template") { insertHtmlElement("template", t.attrs, t.pos); insertMarker(); framesetOk = false; mode = MODE_IN_TEMPLATE; templateModes.push(MODE_IN_TEMPLATE); const el = cur(); const fragment = _hAlloc(NodeType.DocumentFragment, 0, 0); // Parent link so the iterative walk can ascend out of the content. _hParent[fragment] = el; _hTc[el] = fragment; return; } if (t.name === "head") return; } if (t.type === TOKEN_END_TAG) { if (t.name === "head") { open.pop(); mode = MODE_AFTER_HEAD; return; } if (BODY_HTML_BR.has(t.name)) { /* fallthrough */ } else if (t.name === "template") { if (!open.some(isHtmlTemplateEl)) { return; } generateImpliedEndTagsThorough(); popUntil("template"); clearAfeToMarker(); templateModes.pop(); resetInsertionMode(); return; } else { return; } } // anything else open.pop(); mode = MODE_AFTER_HEAD; process(t); }; modes.inHeadNoscript = (t) => { if (t.type === TOKEN_DOCTYPE) return; if (t.type === TOKEN_START_TAG && t.name === "html") return modes.inBody(t); if (t.type === TOKEN_END_TAG && t.name === "noscript") { open.pop(); mode = MODE_IN_HEAD; return; } if (t.type === TOKEN_CHAR && isAllWs(t.data)) return modes.inHead(t); if (t.type === TOKEN_COMMENT) return modes.inHead(t); if ( t.type === TOKEN_START_TAG && IN_HEAD_NOSCRIPT_PASSTHROUGH.has(t.name) ) { return modes.inHead(t); } // A stray end tag other than </br>/</noscript> is ignored (the comment // or content stays inside <noscript>); only </br> and other content fall // back to popping <noscript>. if (t.type === TOKEN_END_TAG && t.name !== "br") return; if ( t.type === TOKEN_START_TAG && (t.name === "head" || t.name === "noscript") ) { return; } open.pop(); mode = MODE_IN_HEAD; process(t); }; modes.afterHead = (t) => { if (t.type === TOKEN_CHAR) { const r = leadingWs(t, true); if (!r) return; insertHtmlElement("body", EMPTY_ATTRS, null); mode = MODE_IN_BODY; process(r); return; } if (t.type === TOKEN_COMMENT) { insertComment(t.data, t.start, t.end); return; } if (t.type === TOKEN_DOCTYPE) return; if (t.type === TOKEN_START_TAG) { if (t.name === "html") return modes.inBody(t); if (t.name === "body") { insertHtmlElement("body", t.attrs, t.pos); framesetOk = false; mode = MODE_IN_BODY; return; } if (t.name === "frameset") { insertHtmlElement("frameset", t.attrs, t.pos); mode = MODE_IN_FRAMESET; return; } if (HEAD_ELEMENTS.has(t.name)) { const headEl = /** @type {HtmlElement} */ (head); open.push(headEl); modes.inHead(t); const idx = open.indexOf(headEl); if (idx !== -1) open.splice(idx, 1); return; } if (t.name === "head") return; } if (t.type === TOKEN_END_TAG) { if (t.name === "template") return modes.inHead(t); if (!BODY_HTML_BR.has(t.name)) return; } insertHtmlElement("body", EMPTY_ATTRS, null); mode = MODE_IN_BODY; process(t); }; modes.inBody = (t) => { if (t.type === TOKEN_CHAR) { if (t.data.includes("\0")) t = { ...t, data: t.data.replace(/\0/g, "") }; if (t.data === "") return; reconstructAfe(); insertCharacters(t.data, t.start, t.end); // `framesetOk` only ever goes true→false, so once it's false skip the // per-text-token whitespace scan entirely (it flips false very early in // real documents). if (framesetOk && !isAllWs(t.data)) framesetOk = false; return; } if (t.type === TOKEN_COMMENT) { insertComment(t.data, t.start, t.end); return; } if (t.type === TOKEN_DOCTYPE) return; if (t.type === TOKEN_START_TAG) return startTagInBody(t); if (t.type === TOKEN_END_TAG) return endTagInBody(t); if (t.type === TOKEN_EOF && templateModes.length) { return modes.inTemplate(t); } }; const closeIfPInButtonScope = () => { if (inButtonScope("p")) closePElement(); }; // "any other end tag" in body: pop to the matching open element, stopping at // the first special element; also the adoption agency's no-element fallback. const anyOtherEndTag = (/** @type {string} */ name) => { for (let i = open.length - 1; i >= 0; i--) { const node = open[i]; if (_ns(node) === NS_HTML && _tag(node) === name) { generateImpliedEndTags(name); while (open.length > i) { _hEn[open[open.length - 1]] = tokenEnd; open.pop(); } return; } if (isSpecial(node)) return; } }; const startTagInBody = (/** @type {StartTagToken} */ t) => { const name = t.name; if (name === "html") { if (open.some(isHtmlTemplateEl)) { return; } mergeAttrs(open[0], t.attrs); return; } if (HEAD_ELEMENTS.has(name)) { return modes.inHead(t); } if (name === "body") { const second = open[1]; if (!second || _tag(second) !== "body" || open.some(isHtmlTemplateEl)) { return; } framesetOk = false; mergeAttrs(second, t.attrs); return; } if (name === "frameset") { const second = open[1]; if (!second || _tag(second) !== "body") return; if (!framesetOk) return; detach(second); while (open.length > 1) open.pop(); insertHtmlElement("frameset", t.attrs, t.pos); mode = MODE_IN_FRAMESET; return; } if (BLOCK_START.has(name)) { closeIfPInButtonScope(); insertHtmlElement(name, t.attrs, t.pos); return; } if (HEADING.has(name)) { closeIfPInButtonScope(); if (_ns(cur()) === NS_HTML && HEADING.has(_tag(cur()))) open.pop(); insertHtmlElement(name, t.attrs, t.pos); return; } if (name === "pre" || name === "listing") { closeIfPInButtonScope(); insertHtmlElement(name, t.attrs, t.pos); t.swallowNewline = true; framesetOk = false; return; } if (name === "form") { if (form && !open.some(isHtmlTemplateEl)) { return; } closeIfPInButtonScope(); const el = insertHtmlElement("form", t.attrs, t.pos); if (!open.some(isHtmlTemplateEl)) { form = el; } return; } if (name === "li") { framesetOk = false; for (let i = open.length - 1; i >= 0; i--) { const node = open[i]; if (_ns(node) === NS_HTML && _tag(node) === "li") { generateImpliedEndTags("li"); popUntil("li"); break; } if ( isSpecial(node) && !(_ns(node) === NS_HTML && ADDRESS_DIV_P.has(_tag(node))) ) { 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 ( _ns(node) === NS_HTML && (_tag(node) === "dd" || _tag(node) === "dt") ) { generateImpliedEndTags(_tag(node)); popUntil(_tag(node)); break; } if ( isSpecial(node) && !(_ns(node) === NS_HTML && ADDRESS_DIV_P.has(_tag(node))) ) { 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) break; if (_tag(afe[i]) === "a") { adoptionAgency("a", t.pos); const idx = afe.findIndex((e) => e !== AFE_MARKER && _tag(e) === "a"); if (idx !== -1) { const el = afe[idx]; afe.splice(idx, 1); const oi = open.indexOf(el); if (oi !== -1) open.splice(oi, 1); } break; } } reconstructAfe(); const el = insertHtmlElement("a", t.attrs, t.pos); pushAfe(el); return; } if (FORMATTING.has(name) && name !== "a" && name !== "nobr") { reconstructAfe(); const el = insertHtmlElement(name, t.attrs, t.pos); pushAfe(el); return; } if (name === "nobr") { reconstructAfe(); if (inScope("nobr")) { // The adoption agency returns false when a marker shields the nobr // from the active formatting list; then act as "any other end tag". if (!adoptionAgency("nobr", t.pos)) anyOtherEndTag("nobr"); reconstructAfe(); } const el = insertHtmlElement("nobr", t.attrs, t.pos); pushAfe(el); return; } if (APPLET_MARQUEE_OBJECT.has(name)) { reconstructAfe(); insertHtmlElement(name, t.attrs, t.pos); insertMarker(); framesetOk = false; return; } if (name === "table") { if (!quirks) closeIfPInButtonScope(); insertHtmlElement("table", t.attrs, t.pos); framesetOk = false; mode = MODE_IN_TABLE; return; } if (VOID_FORMATTING.has(name)) { reconstructAfe(); insertHtmlElement(name, t.attrs, t.pos); open.pop(); framesetOk = false; return; } if (name === "input") { // `<input>` inside a select is dropped; if a select is open it is // closed first (keygen/textarea no longer behave this way). if (inScope("select")) { popUntil("select"); resetInsertionMode(); } else if ( fragment && _ns(fragment) === NS_HTML && _tag(fragment) === "select" ) { return; } reconstructAfe(); insertHtmlElement("input", t.attrs, t.pos); open.pop(); const ty = _aFind(t.attrs.start, t.attrs.count, "type"); if (ty === 0 || _aValueOf(ty).toLowerCase() !== "hidden") { framesetOk = false; } return; } if (PARAM_SOURCE_TRACK.has(name)) { insertHtmlElement(name, t.attrs, t.pos); open.pop(); return; } if (name === "hr") { if (_ns(cur()) === NS_HTML && _tag(cur()) === "option") open.pop(); if (_ns(cur()) === NS_HTML && _tag(cur()) === "optgroup") { open.pop(); } closeIfPInButtonScope(); insertHtmlElement("hr", t.attrs, t.pos); open.pop(); framesetOk = false; return; } if (name === "image") { return startTagInBody({ ...t, name: "img" }); } if (name === "textarea") { genericRcdata(t, true); framesetOk = false; return; } if (name === "xmp") { closeIfPInButtonScope(); reconstructAfe(); framesetOk = false; genericRawtext(t); return; } if (name === "iframe") { framesetOk = false; genericRawtext(t); return; } if (name === "noembed") { genericRawtext(t); return; } if (name === "select") { reconstructAfe(); if (inScope("select")) { generateImpliedEndTags(); popUntil("select"); resetInsertionMode(); return; } insertHtmlElement("select", t.attrs, t.pos); // Marker so a stray formatting end tag (e.g. `</font>`) can't adopt // across the select boundary now that select has no own insertion mode. insertMarker(); framesetOk = false; return; } if (name === "optgroup" || name === "option") { if (_ns(cur()) === NS_HTML && _tag(cur()) === "option") open.pop(); if ( name === "optgroup" && _ns(cur()) === NS_HTML && _tag(cur()) === "optgroup" ) { open.pop(); } reconstructAfe(); insertHtmlElement(name, t.attrs, t.pos); return; } if (name === "rb" || name === "rtc") { if (inScope("ruby")) generateImpliedEndTags(); insertHtmlElement(name, t.attrs, t.pos); return; } if (name === "rp" || name === "rt") { if (inScope("ruby")) generateImpliedEndTags("rtc"); insertHtmlElement(name, t.attrs, t.pos); return; } if (name === "math") { reconstructAfe(); const attrs = adjustForeignAttrs(adjustMathmlAttrs(t.attrs), NS_MATHML); insertForeignElement("math", NS_MATHML, attrs, t.pos); if (t.selfClosing) open.pop(); return; } if (name === "svg") { reconstructAfe(); const attrs = adjustForeignAttrs(t.attrs, NS_SVG); insertForeignElement("svg", NS_SVG, attrs, t.pos); if (t.selfClosing) open.pop(); return; } if (IGNORED_BODY_TABLE_STARTS.has(name)) { return; } // any other start tag reconstructAfe(); insertHtmlElement(name, t.attrs, t.pos); }; // Only `definitionurl` is rewritten (in place — the run is consumed only by // this tag's element); the camelCase name serializes as itself. const adjustMathmlAttrs = (/** @type {AttributeRun} */ run) => { for (let i = run.start; i < run.start + run.count; i++) { if (_aName[i] === "definitionurl") _aName[i] = "definitionURL"; } return run; }; const endTagInBody = (/** @type {EndTagToken} */ t) => { const name = t.name; if (name === "template") return modes.inHead(t); if (name === "select") { if (!inScope("select")) return; generateImpliedEndTags(); popUntil("select"); return; } if (name === "body" || name === "html") { if (!inScope("body")) return; mode = MODE_AFTER_BODY; if (name === "html") process(t); return; } if (BLOCK_END.has(name)) { if (!inScope(name)) return; generateImpliedEndTags(); popUntil(name); return; } if (name === "form") { if (!open.some(isHtmlTemplateEl)) { const node = form; form = 0; if (!node || !inScopeEl(node)) return; generateImpliedEndTags(); const idx = open.indexOf(node); if (idx !== -1) open.splice(idx, 1); } else { if (!inScope("form")) return; generateImpliedEndTags(); popUntil("form"); } return; } if (name === "p") { if (!inButtonScope("p")) insertHtmlElement("p", EMPTY_ATTRS, t.pos); closePElement(); return; } if (name === "li") { if (!inListItemScope("li")) return; generateImpliedEndTags("li"); popUntil("li"); return; } if (name === "dd" || name === "dt") { if (!inScope(name)) return; generateImpliedEndTags(name); popUntil(name); return; } if (HEADING.has(name)) { let anyHeadingInScope = false; for (const h of HEADING) { if (inScope(h)) { anyHeadingInScope = true; break; } } if (!anyHeadingInScope) return; generateImpliedEndTags(); popUntilOneOf(HEADING); return; } if (name === "sarcasm") { /* take a deep breath */ } if (FORMATTING.has(name)) { adoptionAgency(name, t.pos); return; } if (APPLET_MARQUEE_OBJECT.has(name)) { if (!inScope(name)) return; generateImpliedEndTags(); popUntil(name); clearAfeToMarker(); return; } if (name === "br") { reconstructAfe(); insertHtmlElement("br", EMPTY_ATTRS, t.pos); open.pop(); framesetOk = false; return; } anyOtherEndTag(name); }; // generic RCDATA/RAWTEXT: tokenizer already emits the text + end tag, so we // just insert the element and switch to "text" mode; text mode appends chars // and the matching end tag pops. const genericRawtext = (/** @type {StartTagToken} */ t) => { insertHtmlElement(t.name, t.attrs, t.pos); originalMode = mode; mode = MODE_TEXT; }; const genericRcdata = (/** @type {StartTagToken} */ t, swallow = false) => { insertHtmlElement(t.name, t.attrs, t.pos); if (swallow) t.swallowNewline = true; originalMode = mode; mode = MODE_TEXT; }; modes.text = (t) => { if (t.type === TOKEN_CHAR) { insertCharacters(t.data, t.start, t.end); return; } if (t.type === TOKEN_EOF) { if (open.length) open.pop(); mode = originalMode; process(t); return; } if (t.type === TOKEN_END_TAG) { // Treat as rawtext rather than an end tag when it can't close the // current element: a non-matching name (e.g. a fragment context), or // a name that ran straight to EOF with no delimiter (`</script` at // EOF — the tokenizer still emits a partial tag there). if (cur() && (t.name !== _tag(cur()) || t.pos.end === t.pos.nameEnd)) { insertCharacters( source.slice(t.pos.start, t.pos.end), t.pos.start, t.pos.end ); return; } open.pop(); mode = originalMode; } }; // ---------- table modes ---------- /** @type {{ list: CharToken[], hasNonWs: boolean } | null} */ let pendingTableChars = null; modes.inTable = (t) => { if (t.type === TOKEN_CHAR) { const c = cur(); if (TABLE_CONTEXT.has(_tag(c)) && _ns(c) === NS_HTML) { pendingTableChars = { list: [], hasNonWs: false }; originalMode = mode; mode = MODE_IN_TABLE_TEXT; return process(t); } } if (t.type === TOKEN_COMMENT) { insertComment(t.data, t.start, t.end); return; } if (t.type === TOKEN_DOCTYPE) return; if (t.type === TOKEN_START_TAG) { const name = t.name; if (name === "caption") { clearStackToTableContext(); insertMarker(); insertHtmlElement("caption", t.attrs, t.pos); mode = MODE_IN_CAPTION; return; } if (name === "colgroup") { clearStackToTableContext(); insertHtmlElement("colgroup", t.attrs, t.pos); mode = MODE_IN_COLUMN_GROUP; return; } if (name === "col") { clearStackToTableContext(); insertHtmlElement("colgroup", EMPTY_ATTRS, t.pos); mode = MODE_IN_COLUMN_GROUP; return process(t); } if (TBODY_GROUP.has(name)) { clearStackToTableContext(); insertHtmlElement(name, t.attrs, t.pos); mode = MODE_IN_TABLE_BODY; return; } if (TD_TH_TR.has(name)) { clearStackToTableContext(); insertHtmlElement("tbody", EMPTY_ATTRS, t.pos); mode = MODE_IN_TABLE_BODY; return process(t); } if (name === "table") { if (!inTableScope("table")) return; popUntil("table"); resetInsertionMode(); return process(t); } if (STYLE_SCRIPT_TEMPLATE.has(name)) { return modes.inHead(t); } if (name === "input") { const ty = _aFind(t.attrs.start, t.attrs.count, "type"); if (ty !== 0 && _aValueOf(ty).toLowerCase() === "hidden") { insertHtmlElement("input", t.attrs, t.pos); open.pop(); return; } } if (name === "form") { if (form || open.some(isHtmlTemplateEl)) { return; } form = insertHtmlElement("form", t.attrs, t.pos); open.pop(); return; } } if (t.type === TOKEN_END_TAG) { if (t.name === "table") { if (!inTableScope("table")) return; popUntil("table"); resetInsertionMode(); return; } if (IN_TABLE_IGNORED_ENDS.has(t.name)) { return; } if (t.name === "template") return modes.inHead(t); } if (t.type === TOKEN_EOF) return modes.inBody(t); // anything else: foster parenting fosterParenting = true; modes.inBody(t); fosterParenting = false; }; modes.inTableText = (t) => { if (t.type === TOKEN_CHAR) { const data = t.data.includes("\0") ? t.data.replace(/\0/g, "") : t.data; if (data === "") return; // Snapshot into a fresh token: these are buffered and replayed after // later tokens arrive, so they must not alias the reused token. /** @type {CharToken} */ const tc = { type: TOKEN_CHAR, data, start: t.start, end: t.end }; const pending = /** @type {{ list: CharToken[], hasNonWs: boolean }} */ ( pendingTableChars ); pending.list.push(tc); if (!isAllWs(tc.data)) pending.hasNonWs = true; return; } // flush const chars = /** @type {{ list: CharToken[], hasNonWs: boolean }} */ ( pendingTableChars ); pendingTableChars = null; mode = originalMode; for (const ct of chars.list) { if (chars.hasNonWs) { fosterParenting = true; modes.inBody(ct); fosterParenting = false; } else { insertCharacters(ct.data, ct.start, ct.end); } } process(t); }; const clearStackToTableContext = () => { while (open.length) { const c = cur(); if (_ns(c) === NS_HTML && CLEAR_TABLE.has(_tag(c))) { break; } open.pop(); } }; const clearStackToTableBodyContext = () => { while (open.length) { const c = cur(); if (_ns(c) === NS_HTML && CLEAR_TABLE_BODY.has(_tag(c))) { break; } open.pop(); } }; const clearStackToTableRowContext = () => { while (open.length) { const c = cur(); if (_ns(c) === NS_HTML && CLEAR_TABLE_ROW.has(_tag(c))) { break; } open.pop(); } }; modes.inCaption = (t) => { if ( (t.type === TOKEN_END_TAG && t.name === "caption") || (t.type === TOKEN_START_TAG && CAPTION_TABLE_STARTS.has(t.name)) || (t.type === TOKEN_END_TAG && t.name === "table") ) { if (!inTableScope("caption")) return; generateImpliedEndTags(); popUntil("caption"); clearAfeToMarker(); mode = MODE_IN_TABLE; if (!(t.type === TOKEN_END_TAG && t.name === "caption")) { return process(t); } return; } if (t.type === TOKEN_END_TAG && CAPTION_IGNORED_ENDS.has(t.name)) { return; } return modes.inBody(t); }; modes.inColumnGroup = (t) => { if (t.type === TOKEN_CHAR) { const r = leadingWs(t, true); if (!r) return; if (_tag(cur()) !== "colgroup") return; open.pop(); mode = MODE_IN_TABLE; process(r); return; } if (t.type === TOKEN_COMMENT) { insertComment(t.data, t.start, t.end); return; } if (t.type === TOKEN_DOCTYPE) return; if (t.type === TOKEN_START_TAG && t.name === "html") return modes.inBody(t); if (t.type === TOKEN_START_TAG && t.name === "col") { insertHtmlElement("col", t.attrs, t.pos); open.pop(); return; } if (t.type === TOKEN_END_TAG && t.name === "colgroup") { if (_tag(cur()) !== "colgroup") return; open.pop(); mode = MODE_IN_TABLE; return; } if (t.type === TOKEN_END_TAG && t.name === "col") return; if ( (t.type === TOKEN_START_TAG || t.type === TOKEN_END_TAG) && t.name === "template" ) { return modes.inHead(t); } if (t.type === TOKEN_EOF) return modes.inBody(t); if (_tag(cur()) !== "colgroup") return; open.pop(); mode = MODE_IN_TABLE; process(t); }; modes.inTableBody = (t) => { if (t.type === TOKEN_START_TAG && t.name === "tr") { clearStackToTableBodyContext(); insertHtmlElement("tr", t.attrs, t.pos); mode = MODE_IN_ROW; return; } if (t.type === TOKEN_START_TAG && (t.name === "th" || t.name === "td")) { clearStackToTableBodyContext(); insertHtmlElement("tr", EMPTY_ATTRS, t.pos); mode = MODE_IN_ROW; return process(t); } if (t.type === TOKEN_END_TAG && TBODY_GROUP.has(t.name)) { if (!inTableScope(t.name)) return; clearStackToTableBodyContext(); open.pop(); mode = MODE_IN_TABLE; return; } if ( (t.type === TOKEN_START_TAG && TBODY_TRIGGER_STARTS.has(t.name)) || (t.type === TOKEN_END_TAG && t.name === "table") ) { if (!inTableScope(TBODY_GROUP)) return; clearStackToTableBodyContext(); open.pop(); mode = MODE_IN_TABLE; return process(t); } if (t.type === TOKEN_END_TAG && TBODY_IGNORED_ENDS.has(t.name)) { return; } return modes.inTable(t); }; modes.inRow = (t) => { if (t.type === TOKEN_START_TAG && (t.name === "th" || t.name === "td")) { clearStackToTableRowContext(); insertHtmlElement(t.name, t.attrs, t.pos); mode = MODE_IN_CELL; insertMarker(); return; } if (t.type === TOKEN_END_TAG && t.name === "tr") { if (!inTableScope("tr")) return; clearStackToTableRowContext(); open.pop(); mode = MODE_IN_TABLE_BODY; return; } if ( (t.type === TOKEN_START_TAG && ROW_TRIGGER_STARTS.has(t.name)) || (t.type === TOKEN_END_TAG && t.name === "table") ) { if (!inTableScope("tr")) return; clearStackToTableRowContext(); open.pop(); mode = MODE_IN_TABLE_BODY; return process(t); } if (t.type === TOKEN_END_TAG && TBODY_GROUP.has(t.name)) { if (!inTableScope(t.name)) return; if (!inTableScope("tr")) return; clearStackToTableRowContext(); open.pop(); mode = MODE_IN_TABLE_BODY; return process(t); } if (t.type === TOKEN_END_TAG && ROW_IGNORED_ENDS.has(t.name)) { return; } return modes.inTable(t); }; modes.inCell = (t) => { if (t.type === TOKEN_END_TAG && (t.name === "td" || t.name === "th")) { if (!inTableScope(t.name)) return; generateImpliedEndTags(); popUntil(t.name); clearAfeToMarker(); mode = MODE_IN_ROW; return; } if (t.type === TOKEN_START_TAG && CAPTION_TABLE_STARTS.has(t.name)) { if (!inTableScope("td") && !inTableScope("th")) return; closeCell(); return process(t); } if (t.type === TOKEN_END_TAG && TABLE_CONTEXT.has(t.name)) { if (!inTableScope(t.name)) return; closeCell(); return process(t); } if (t.type === TOKEN_END_TAG && CELL_IGNORED_ENDS.has(t.name)) { return; } return modes.inBody(t); }; const closeCell = () => { generateImpliedEndTags(); popUntilOneOf(TD_TH); clearAfeToMarker(); mode = MODE_IN_ROW; }; modes.inTemplate = (t) => { if ( t.type === TOKEN_CHAR || t.type === TOKEN_COMMENT || t.type === TOKEN_DOCTYPE ) { return modes.inBody(t); } if (t.type === TOKEN_START_TAG) { if (HEAD_ELEMENTS.has(t.name)) { return modes.inHead(t); } const target = TEMPLATE_START_TAG_MODES.get(t.name) || MODE_IN_BODY; templateModes[templateModes.length - 1] = target; mode = target; return process(t); } if (t.type === TOKEN_END_TAG) { if (t.name === "template") return modes.inHead(t); return; } if (t.type === TOKEN_EOF) { if (!open.some(isHtmlTemplateEl)) { return; } popUntil("template"); clearAfeToMarker(); templateModes.pop(); resetInsertionMode(); return process(t); } }; modes.afterBody = (t) => { if (t.type === TOKEN_CHAR && isAllWs(t.data)) return modes.inBody(t); if (t.type === TOKEN_COMMENT) { insertComment(t.data, t.start, t.end, { parent: open[0], beforeNode: 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; mode = MODE_AFTER_AFTER_BODY; return; } if (t.type === TOKEN_EOF) return; mode = MODE_IN_BODY; process(t); }; modes.inFrameset = (t) => { if (t.type === TOKEN_CHAR) { const ws = t.data.replace(/[^\t\n\f\r ]/g, ""); if (ws) insertCharacters(ws, t.start, t.end); return; } if (t.type === TOKEN_COMMENT) { insertComment(t.data, t.start, t.end); return; } if (t.type === TOKEN_DOCTYPE) return; if (t.type === TOKEN_START_TAG && t.name === "html") return modes.inBody(t); if (t.type === TOKEN_START_TAG && t.name === "frameset") { insertHtmlElement("frameset", t.attrs, t.pos); return; } if (t.type === TOKEN_END_TAG && t.name === "frameset") { if (_tag(cur()) === "html") return; open.pop(); if (!fragment && _tag(cur()) !== "frameset") mode = MODE_AFTER_FRAMESET; return; } if (t.type === TOKEN_START_TAG && t.name === "frame") { insertHtmlElement("frame", t.attrs, t.pos); open.pop(); return; } if (t.type === TOKEN_START_TAG && t.name === "noframes") { return modes.inHead(t); } }; modes.afterFrameset = (t) => { if (t.type === TOKEN_CHAR) { const ws = t.data.replace(/[^\t\n\f\r ]/g, ""); if (ws) insertCharacters(ws, t.start, t.end); return; } if (t.type === TOKEN_COMMENT) { insertComment(t.data, t.start, t.end); return; } if (t.type === TOKEN_DOCTYPE) return; if (t.type === TOKEN_START_TAG && t.name === "html") return modes.inBody(t); if (t.type === TOKEN_END_TAG && t.name === "html") { mode = MODE_AFTER_AFTER_FRAMESET; return; } if (t.type === TOKEN_START_TAG && t.name === "noframes") { return modes.inHead(t); } }; modes.afterAfterBody = (t) => { if (t.type === TOKEN_COMMENT) { insertComment(t.data, t.start, t.end, { parent: doc, beforeNode: 0 }); return; } if (t.type === TOKEN_DOCTYPE) return modes.inBody(t); if (t.type === TOKEN_CHAR && isAllWs(t.data)) return modes.inBody(t); if (t.type === TOKEN_START_TAG && t.name === "html") return modes.inBody(t); if (t.type === TOKEN_EOF) return; mode = MODE_IN_BODY; process(t); }; modes.afterAfterFrameset = (t) => { if (t.type === TOKEN_COMMENT) { insertComment(t.data, t.start, t.end, { parent: doc, beforeNode: 0 }); return; } if (t.type === TOKEN_DOCTYPE) return modes.inBody(t); if (t.type === TOKEN_CHAR && isAllWs(t.data)) return modes.inBody(t); if (t.type === TOKEN_START_TAG && t.name === "html") return modes.inBody(t); if (t.type === TOKEN_START_TAG && t.name === "noframes") { return modes.inHead(t); } }; // ---------- fragment setup ---------- if (fragmentContext) { let ctxName = fragmentContext.toLowerCase(); let ctxNs = NS_HTML; if (ctxName.startsWith("svg ")) { ctxNs = NS_SVG; ctxName = ctxName.slice(4); } else if (ctxName.startsWith("math ")) { ctxNs = NS_MATHML; ctxName = ctxName.slice(5); } fragment = mkEl(ctxName, ctxNs, EMPTY_ATTRS, null); const htmlEl = mkEl("html", NS_HTML, EMPTY_ATTRS, null); _hAppend(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; } } // ---------- tokenizer callbacks ---------- const decode = decodeHtmlEntities; let swallowNextNewline = false; // Set by the tokenizer when it hits EOF mid-tag; such a partial tag is // dropped (matching the spec's eof-in-tag handling). let eofInTag = false; // Set when a `<selectedcontent>` is seen, gating the post-parse mirror pass. let sawSelectedContent = false; // Single reused token (see `MutableToken`). The tokenizer callbacks below // fill it and call `dispatch`; processing is synchronous, so the next // callback only runs after the current token is fully consumed. /** @type {MutableToken} */ const tok = { type: TOKEN_EOF, name: "", data: "", attrs: { start: 0, count: 0 }, selfClosing: false, start: 0, end: 0, publicId: null, systemId: null, swallowNewline: false, pos: { start: 0, end: 0, tagEnd: 0, nameEnd: 0 } }; const dispatch = () => process(/** @type {Token} */ (/** @type {unknown} */ (tok))); walkHtmlTokens(source, 0, { isForeign: () => { const ac = adjustedCurrent(); return open.length > 0 && ac !== 0 && _ns(ac) !== NS_HTML; }, fragmentContext: fragment ? _tag(fragment) : undefined, parseError: (input, code) => { if (code === "eof-in-tag") eofInTag = true; }, doctype: (input, start, end) => { // parse doctype name + ids from the raw token (simplified) const { name, publicId, systemId } = parseDoctype( input.slice(start, end) ); tok.type = TOKEN_DOCTYPE; tok.name = name; tok.publicId = publicId; tok.systemId = systemId; tok.start = start; tok.end = end; dispatch(); return end; }, comment: (input, start, end) => { // The tokenizer emits CDATA sections through this callback too. if (input.startsWith("<![CDATA[", start)) { const ac = adjustedCurrent(); if (open.length > 0 && ac && _ns(ac) !== NS_HTML) { const innerEnd = input.endsWith("]]>", end) ? end - 3 : end; const data = input.slice(start + 9, innerEnd).replace(/\r\n?/g, "\n"); if (data !== "") { tok.type = TOKEN_CHAR; tok.data = data; tok.start = start; tok.end = end; dispatch(); } return end; } tok.type = TOKEN_COMMENT; tok.data = input.slice( start + 2, input.charCodeAt(end - 1) === 0x3e ? end - 1 : end ); tok.start = start; tok.end = end; dispatch(); return end; } let s = start; let e = end; if (input.startsWith("<!--", start)) s = start + 4; else if (input.startsWith("<!", start)) s = start + 2; else if (input.startsWith("</", start)) s = start + 2; else if (input.charCodeAt(start) === 0x3c) s = start + 1; if (input.endsWith("-->", end)) e = end - 3; else if (input.endsWith("--!>", end)) e = end - 4; else if (input.charCodeAt(end - 1) === 0x3e) e = end - 1; else if (input.endsWith("--", end)) e = end - 2; else if (input.charCodeAt(end - 1) === 0x2d) e = end - 1; if (e < s) e = s; tok.type = TOKEN_COMMENT; tok.data = input.slice(s, e).replace(/\0/g, "�"); tok.start = start; tok.end = end; dispatch(); return end; }, text: (input, start, end) => { // `skip.text` fast path: node dropped, so only whitespace-ness matters. // With no `&`/`\0`/`\r` (and no pending newline-swallow) the decoded value // equals the raw range — dispatch a canonical marker (`" "`/`"x"`) without // the slice + entity decode; anything trickier falls through. if (skipText && !swallowNextNewline) { let hasNonWs = false; let i = start; for (; i < end; i++) { const c = input.charCodeAt(i); // 2 = break (& / NUL / CR), 1 = whitespace, 0 = other. const cls = c < 128 ? _TEXT_SCAN_CLASS[c] : 0; if (cls === 2) break; if (cls === 0) hasNonWs = true; } if (i === end) { tok.type = TOKEN_CHAR; tok.data = hasNonWs ? "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 s = !raw.includes("\r") ? raw : raw.replace(/\r\n?/g, "\n"); const top = adjustedCurrent(); const rawMode = mode === MODE_TEXT || (top && _ns(top) === NS_HTML && _tag(top) === "plaintext"); const noDecode = top && _ns(top) === NS_HTML && NO_DECODE_TEXT.has(_tag(top)) && (mode === MODE_TEXT || mode === MODE_IN_BODY); let data = noDecode ? s : decode(s, false); // In RAWTEXT/RCDATA/script/PLAINTEXT, NULL becomes U+FFFD (tokenizer // rule); in data state NULLs pass through to be dropped in "in body". if (rawMode && data.includes("\0")) data = data.replace(/\0/g, "�"); // pre/listing/textarea swallow a leading newline (post entity decode). if (swallowNextNewline) { swallowNextNewline = false; if (data[0] === "\n") data = data.slice(1); } if (data === "") return end; tok.type = TOKEN_CHAR; tok.data = data; tok.start = start; tok.end = end; dispatch(); return end; }, attribute: (input, nameStart, nameEnd, valueStart, valueEnd, quoteType) => { const name = internLowerName(ATTR_NAME_INTERN, input, nameStart, nameEnd); if (pendAttrStart === 0) pendAttrStart = _aN + 1; // 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 <= _aN; i++) { if (_aName[i] === name) { dup = true; break; } } if (!dup) { // The raw (undecoded) value is read from the source by offset on // demand: consumers re-resolve requests from it and the offsets must // stay aligned with the source. Only a valueless attribute stores an // override (""). _aAlloc( name, valueStart !== -1 ? null : "", nameStart, nameEnd, valueStart, valueEnd ); } if (valueStart === -1) return nameEnd; return quoteType !== QUOTE_NONE ? valueEnd + 1 : valueEnd; }, openTag: (input, start, end, nameStart, nameEnd, selfClosing) => { // A start tag the tokenizer only emitted because it hit EOF mid-tag // is dropped, matching the spec's eof-in-tag handling. if (eofInTag) { // Any attribute slots already allocated for it are orphaned. pendAttrStart = 0; 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 : _aN + 1 - pendAttrStart; pendAttrStart = 0; tok.selfClosing = selfClosing; tok.swallowNewline = false; tok.pos.start = start; tok.pos.end = end; tok.pos.tagEnd = end; tok.pos.nameEnd = nameEnd; dispatch(); if (tok.swallowNewline) swallowNextNewline = true; return end; }, closeTag: (input, start, end, nameStart, nameEnd) => { const name = internLowerName(TAG_NAME_INTERN, input, nameStart, nameEnd); // End tags drop any parsed attributes (the slots are orphaned). pendAttrStart = 0; tok.type = TOKEN_END_TAG; tok.name = name; tok.pos.start = start; tok.pos.end = end; tok.pos.tagEnd = end; tok.pos.nameEnd = nameEnd; dispatch(); return end; } }); tok.type = TOKEN_EOF; dispatch(); if (sawSelectedContent) mirrorSelectedContent(doc, 0); 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 = _hTy[node]; const clone = _hAlloc(ty, _hSt[node], _hEn[node]); _hStr[clone] = _hStr[node]; if (ty !== NodeType.Element) return clone; _hFl[clone] = _hFl[node]; const attrs = cloneAttrs(node); _hAStart[clone] = attrs.start; _hACount[clone] = attrs.count; _hTagEnd[clone] = _hTagEnd[node]; _hNameEnd[clone] = _hNameEnd[node]; // Empty body span: the clone re-emits no raw-text dependency. _hCEnd[clone] = _hTagEnd[node]; for (let k = _hFirst[node]; k !== 0; k = _hNext[k]) { _hAppend(clone, cloneSubtree(k)); } // Clone `<template>` content into the clone's own fragment. const tc = _hTc[node]; if (tc !== 0) { const fragment = _hAlloc(NodeType.DocumentFragment, 0, 0); // Parent link so the iterative walk can ascend out of the content. _hParent[fragment] = clone; _hTc[clone] = fragment; for (let k = _hFirst[tc]; k !== 0; k = _hNext[k]) { _hAppend(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 = _hFirst[el]; c !== 0; c = _hNext[c]) { if (_hTy[c] !== NodeType.Element || _ns(c) !== NS_HTML) continue; if (_hStr[c] === "option") options.push(c); else if (_hStr[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 (_aFind(_hAStart[el], _hACount[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). * @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 = _hTc[node]; const container = tc !== 0 ? tc : node; for (let child = _hFirst[container]; child !== 0; child = _hNext[child]) { if (_hTy[child] !== NodeType.Element) continue; if (_ns(child) === NS_HTML && _hStr[child] === "select") { mirrorSelectedContent(child, child); } else if ( select !== 0 && _ns(child) === NS_HTML && _hStr[child] === "selectedcontent" ) { const option = selectedOption(select); if (option !== 0) { // Replace the children with clones of the option's subtree. _hFirst[child] = 0; _hLast[child] = 0; for (let k = _hFirst[option]; k !== 0; k = _hNext[k]) { _hAppend(child, cloneSubtree(k)); } } } else { mirrorSelectedContent(child, select); } } }; const parseDoctype = (/** @type {string} */ raw) => { // raw like <!DOCTYPE html ...> let s = raw.replace(/^<!/i, "").replace(/>$/, ""); s = s.replace(/^doctype/i, ""); s = s.trim(); if (s === "") return { name: "", publicId: null, systemId: null }; const m = /^([^\s]+)/.exec(s); const name = m ? m[1].toLowerCase() : ""; let publicId = null; let systemId = null; const pub = /public\s*("([^"]*)"|'([^']*)')(\s*("([^"]*)"|'([^']*)'))?/i.exec( s ); if (pub) { publicId = pub[2] !== undefined ? pub[2] : pub[3] || ""; if (pub[5] !== undefined) { systemId = pub[6] !== undefined ? pub[6] : pub[7] || ""; } } const sys = /system\s*("([^"]*)"|'([^']*)')/i.exec(s); if (sys && publicId === null) { systemId = sys[2] !== undefined ? sys[2] : sys[3] || ""; } return { name, publicId, systemId }; }; /** @typedef {HtmlNode | HtmlDocument | HtmlDocumentFragment} HtmlVisitableNode */ // HTML-typed views over the generic visitor machinery (`util/SourceProcessor`). /** * @typedef {import("../util/SourceProcessor").VisitorFn<HtmlPath>} VisitorFn * @typedef {import("../util/SourceProcessor").VisitorBucket<HtmlPath>} VisitorBucket * @typedef {import("../util/SourceProcessor").VisitorMap<HtmlPath>} VisitorMap * @typedef {import("../util/SourceProcessor").CompiledVisitorMap<HtmlPath>} CompiledVisitorMap */ /** * @typedef {object} HtmlProcessOptions * @property {string=} fragmentContext context element tag name for fragment parsing (see `buildHtmlAst`); the HTML analog of the CSS parser's `as` parse-mode option * @property {HtmlAstSkip=} skip node kinds to omit from the AST for speed/memory (see `HtmlAstSkip`) */ /** * The HTML `SourceProcessor` grammar: build the document AST (WHATWG tree * construction) and walk it, firing `enter` / `exit` in source order. The root * document / fragment node is visited too (with a `null` parent). * @param {string} input source text * @param {CompiledVisitorMap} visitors compiled visitor map * @param {HtmlProcessOptions} options process options */ const grammar = (input, visitors, options) => { const root = buildHtmlAst(input, options.fragmentContext, options.skip); // Iterative depth-first walk over the link columns: `firstChild` // / `nextSibling` descend, the `parent` column ascends, so arbitrarily deep // markup can't overflow the call stack (the old recursive walk died at // ~10⁵ nesting). A `<template>`'s content fragment is visited before the // element's children; ascending out of it continues with those children // (the fragment is never in a sibling chain, `_hNext` = 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 enter = (node) => { const b = visitors[_hTy[node]]; if (b === undefined || b.enter.length === 0) return true; _walkSkip = false; _curNode = node; const p = _hParent[node]; _curParent = p === 0 ? null : p; const e = b.enter; for (let i = 0; i < e.length; i++) e[i](A); const skip = _walkSkip; _walkSkip = false; return !skip; }; /** * Fire a node's `exit` visitors. * @param {HtmlNodeRef} node node */ const exit = (node) => { const b = visitors[_hTy[node]]; if (b === undefined) return; _curNode = node; const p = _hParent[node]; _curParent = p === 0 ? null : p; const x = b.exit; for (let i = 0; i < x.length; i++) x[i](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 = _hTy[node]; if (ty === NodeType.Element) { const tc = _hTc[node]; return tc !== 0 ? tc : _hFirst[node]; } if (ty === NodeType.Document || ty === NodeType.DocumentFragment) { return _hFirst[node]; } return 0; }; let node = root; descend: for (;;) { if (enter(node)) { const inner = firstInner(node); if (inner !== 0) { node = inner; continue; } } // Leaf (or skipped): exit and move sideways / upwards. for (;;) { exit(node); if (node === root) break descend; const parent = _hParent[node]; // Out of a template's content fragment: the element's children follow. if (_hTc[parent] === node) { const first = _hFirst[parent]; if (first !== 0) { node = first; continue descend; } } else { const sibling = _hNext[node]; if (sibling !== 0) { node = sibling; continue descend; } } node = parent; } } // 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. _hRelease(); }; /** * The generic visitor coordinator (`util/SourceProcessor`) bound to the HTML * `grammar`. Babel-style usage: * * ``` * processor.use({ [NodeType.Element]: (path) => {}, [NodeType.Comment]: { enter, exit } }); * processor.process(source); * ``` * @extends {GenericSourceProcessor<HtmlPath, HtmlProcessOptions>} */ class SourceProcessor extends GenericSourceProcessor { constructor() { super(grammar); } } /** @typedef {[string, number, number]} ParsedSource */ // `parseSrcset` is a direct implementation of the WHATWG "parse a srcset // attribute" algorithm; it lives here with the other spec-level HTML parsing // so it can move into the WASM parser alongside the tokenizer in the future. const COMMA = ",".charCodeAt(0); const LEFT_PARENTHESIS = "(".charCodeAt(0); const RIGHT_PARENTHESIS = ")".charCodeAt(0); const SMALL_LETTER_W = "w".charCodeAt(0); const SMALL_LETTER_X = "x".charCodeAt(0); const SMALL_LETTER_H = "h".charCodeAt(0); // (Don't use \s, to avoid matching non-breaking space) // Sticky so `collectCharacters` matches at an offset without slicing the // whole remaining input per call (which made srcset parsing 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+$/; // ( Positive or negative or unsigned integers or decimals, without or without exponents. // Must include at least one digit. // According to spec tests any decimal point must be followed by a digit. // No leading plus sign is allowed.) // https://html.spec.whatwg.org/multipage/infrastructure.html#valid-floating-point-number const FLOATING_POINT_REGEXP = /^-?(?:[0-9]+|[0-9]*\.[0-9]+)(?:[eE][+-]?[0-9]+)?$/; /** * @param {string} input input * @returns {ParsedSource[]} parsed srcset */ const parseSrcset = (input) => { // 1. Let input be the value passed to this algorithm. const inputLength = input.length; /** @type {string | undefined} */ let url; /** @type {string[]} */ let descriptors; /** @type {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 || density) { pError = true; } const intVal = Number.parseInt(value, 10); // Apply the rules for parsing non-negative integers to the descriptor. // If the result is zero, let error be yes. // Otherwise, let width be the result. if (intVal === 0) { pError = true; } else { width = intVal; } } // If the descriptor consists of a valid floating-point number followed by // a U+0078 LATIN SMALL LETTER X character else if ( FLOATING_POINT_REGEXP.test(value) && lastChar === SMALL_LETTER_X ) { // If width, density and future-compat-h are not all absent, then let error // be yes. if (width || density || height) { pError = true; } const floatVal = Number.parseFloat(value); // Apply the rules for parsing floating-point number values to the descriptor. // If the result is less than zero, let error be yes. Otherwise, let density // be the result. if (floatVal < 0) { pError = true; } else { density = floatVal; } } // If the descriptor consists of a valid non-negative integer followed by // a U+0068 LATIN SMALL LETTER H character else if ( NON_NEGATIVE_INTEGER_REGEXP.test(value) && lastChar === SMALL_LETTER_H ) { // If height and density are not both absent, then let error be yes. if (height || density) { pError = true; } const intVal = Number.parseInt(value, 10); // Apply the rules for parsing non-negative integers to the descriptor. // If the result is zero, let error be yes. Otherwise, let future-compat-h // be the result. if (intVal === 0) { pError = true; } else { height = intVal; } // Anything else, Let error be yes. } else { pError = true; } } // 15. If error is still no, then append a new image source to candidates whose // URL is url, associated with a width width if not absent and a pixel // density density if not absent. Otherwise, there is a parse error. if (!pError) { candidates.push([ /** @type {string} */ (url), start, start + /** @type {string} */ (url).length ]); } else { throw new Error( `Invalid srcset descriptor found in '${input}' at '${desc}'` ); } } /** * @returns {void} */ function tokenize() { // 8.1. Descriptor tokenizer: Skip whitespace collectCharacters(LEADING_SPACES_REGEXP); // 8.2. Let current descriptor be the empty string. // (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 character // If current descriptor is not empty, append current descriptor to // descriptors and let current descriptor be the empty string. // Set state to after descriptor. if (isSpace(charCode)) { if (descriptorStart !== -1) { descriptors.push(input.slice(descriptorStart, position)); descriptorStart = -1; state = "after descriptor"; } } // U+002C COMMA (,) // Advance position to the next character in input. If current descriptor // is not empty, append current descriptor to descriptors. Jump to the step // labeled descriptor parser. else if (charCode === COMMA) { position += 1; if (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 (isSpace(charCode)) { // Space character: Stay in this state. } // EOF: Jump to the step labeled descriptor parser. else if (Number.isNaN(charCode)) { parseDescriptors(); return; } // Anything else // Set state to in descriptor. Set position to the previous character in input. else { state = "in descriptor"; position -= 1; } } // Advance position to the next character in input. position += 1; } } // 3. Let candidates be an initially empty source set. // const candidates = []; // Moved to top // 4. Splitting loop: Collect a sequence of characters that are space // characters or U+002C COMMA characters. If any U+002C COMMA characters // were collected, that is a parse error. while (true) { collectCharacters(LEADING_COMMAS_OR_SPACES_REGEXP); // 5. If position is past the end of input, return candidates and abort these steps. if (position >= inputLength) { if (candidates.length === 0) { throw new Error("Must contain one or more image candidate strings"); } // (we're done, this is the sole return path) return candidates; } // 6. Collect a sequence of characters that are not space characters, // and let that be url. start = position; url = collectCharacters(LEADING_NOT_SPACES); // 7. Let descriptors be a new empty list. descriptors = []; // 8. If url ends with a U+002C COMMA character (,), follow these sub steps: // (1). Remove all trailing U+002C COMMA characters from url. If this removed // more than one character, that is a parse error. if (url && url.charCodeAt(url.length - 1) === COMMA) { url = url.replace(TRAILING_COMMAS_REGEXP, ""); // (Jump ahead to step 9 to skip tokenization and just push the candidate). parseDescriptors(); } // Otherwise, follow these sub steps: else { tokenize(); } // 16. Return to the step labeled splitting loop. } }; // Babel's `path.skip()`, children-only: set by `A.skipChildren()` during an // `enter` dispatch, consumed by the walk. let _walkSkip = false; // The walk's current position (`A.node` / `A.parent` read these; module-level // so the accessor methods' defaults avoid self-referential `this` typing). /** @type {HtmlNodeRef} */ let _curNode = 0; /** @type {HtmlNodeRef | null} */ let _curParent = null; /* eslint-disable jsdoc/require-template -- `A` below is the accessor const, not a type parameter */ /** * The HTML path (Babel's `path` shape): 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 */ // AST field-access seam (mirrors the CSS parser's `A`): every AST field a // consumer reads goes through one of these accessors, so the node // representation can change underneath without touching consumers. `n` is an // `HtmlNodeRef`; results are valid until the next `buildHtmlAst` call. const A = { // === path position (rebound by the walk before every visitor call) === /** * @returns {HtmlNodeRef} current node — only valid during a visitor callback */ get node() { return _curNode; }, /** * @returns {HtmlNodeRef | null} enclosing node (null = the document root) */ get parent() { return _curParent; }, /** Stop the walk descending into the current node (enter only). */ skipChildren() { _walkSkip = true; }, // === field reads — `n` defaults to the current node === /** * @param {HtmlNodeRef=} n node * @returns {number} `NodeType` */ type(n = _curNode) { return _hTy[n]; }, /** * @param {HtmlNodeRef=} n node * @returns {number} start offset */ start(n = _curNode) { return _hSt[n]; }, /** * @param {HtmlNodeRef=} n node * @returns {number} end offset */ end(n = _curNode) { return _hEn[n]; }, /** * @param {HtmlElement=} n element * @returns {string} lowercased (foreign-content: adjusted) tag name */ tagName(n = _curNode) { return _hStr[n]; }, /** * @param {HtmlElement=} n element * @returns {number} `NS_*` namespace */ namespace(n = _curNode) { return _hFl[n] & NS_MASK; }, /** * @param {HtmlElement=} n element * @returns {boolean} true for void elements */ selfClosing(n = _curNode) { return (_hFl[n] & FLAG_SELF_CLOSING) !== 0; }, // materialized attribute list — test/tooling convenience, allocates; the // parser reads attributes through the scalar accessors below /** * @param {HtmlElement=} n element * @returns {HtmlAttribute[]} materialized attributes */ attributes(n = _curNode) { const out = []; const start = _hAStart[n]; for (let i = start; i < start + _hACount[n]; i++) { out.push({ name: _aName[i], value: _aValueOf(i), serializedName: _aSerializedName(i), nameStart: _aNameStart[i], nameEnd: _aNameEnd[i], valueStart: _aValStart[i], valueEnd: _aValEnd[i] }); } return out; }, /** * @param {HtmlElement=} n element * @returns {number} attribute count */ attributeCount(n = _curNode) { return _hACount[n]; }, /** * The i-th attribute of an element, as an id for the `attribute*` reads. * @param {number} i attribute index * @param {HtmlElement=} n element * @returns {HtmlAttributeRef} attribute ref */ attributeAt(i, n = _curNode) { return _hAStart[n] + i; }, /** * Linear lookup by (lowercased) name. * @param {string} name attribute name * @param {HtmlElement=} n element * @returns {HtmlAttributeRef} attribute ref (0 = not present) */ findAttribute(name, n = _curNode) { return _aFind(_hAStart[n], _hACount[n], name); }, /** * @param {HtmlAttributeRef} a attribute ref * @returns {string} lowercased (foreign-content: adjusted) attribute name */ attributeName(a) { return _aName[a]; }, /** * @param {HtmlAttributeRef} a attribute ref * @returns {string} raw (undecoded) attribute value ("" when valueless) */ attributeValue(a) { return _aValueOf(a); }, /** * @param {HtmlAttributeRef} a attribute ref * @returns {number} name start offset */ attributeNameStart(a) { return _aNameStart[a]; }, /** * @param {HtmlAttributeRef} a attribute ref * @returns {number} name end offset */ attributeNameEnd(a) { return _aNameEnd[a]; }, /** * @param {HtmlAttributeRef} a attribute ref * @returns {number} value start offset (-1 when valueless or on adoption-agency clones) */ attributeValueStart(a) { return _aValStart[a]; }, /** * @param {HtmlAttributeRef} a attribute ref * @returns {number} value end offset */ attributeValueEnd(a) { return _aValEnd[a]; }, /** * @param {HtmlElement=} n element * @returns {number} end offset of the opening tag (after `>`) */ tagEnd(n = _curNode) { return _hTagEnd[n]; }, /** * @param {HtmlElement=} n element * @returns {number} end offset of the tag name */ nameEnd(n = _curNode) { return _hNameEnd[n]; }, /** * @param {HtmlElement=} n element * @returns {number} under `skip.text`, end offset of a raw-text element's body (`tagEnd` when empty) */ contentEnd(n = _curNode) { return _hCEnd[n]; }, /** * @param {HtmlElement=} n element * @returns {HtmlDocumentFragment} `<template>` content fragment (0 = none) */ templateContent(n = _curNode) { return _hTc[n]; }, /** * @param {HtmlText | HtmlComment=} n text / comment node * @returns {string} decoded text / comment data */ data(n = _curNode) { return _hStr[n]; }, /** * @param {HtmlDoctype=} n doctype node * @returns {string} doctype name */ doctypeName(n = _curNode) { return _hStr[n]; }, // The doctype ids are per-parse scalars (a document has at most one // doctype node); the node parameter is accepted for call-shape uniformity. /** * @param {HtmlDoctype=} _n doctype node * @returns {string | null} doctype public id */ doctypePublicId(_n) { return _hDocPub; }, /** * @param {HtmlDoctype=} _n doctype node * @returns {string | null} doctype system id */ doctypeSystemId(_n) { return _hDocSys; }, // === tree links (0 = none) === /** * @param {HtmlNodeRef=} n node * @returns {HtmlNodeRef} first child */ firstChild(n = _curNode) { return _hFirst[n]; }, /** * @param {HtmlNodeRef=} n node * @returns {HtmlNodeRef} next sibling */ nextSibling(n = _curNode) { return _hNext[n]; }, /** * @param {HtmlNodeRef=} n node * @returns {HtmlNodeRef} parent node (a `<template>`'s content links to its fragment) */ parentOf(n = _curNode) { return _hParent[n]; }, /** * @param {HtmlNodeRef=} n node * @returns {HtmlNodeRef[]} materialized child list — test/tooling convenience, allocates */ children(n = _curNode) { const out = []; for (let c = _hFirst[n]; c !== 0; c = _hNext[c]) out.push(c); return out; } }; module.exports.A = A; module.exports.NS_HTML = NS_HTML; module.exports.NS_MATHML = NS_MATHML; module.exports.NS_SVG = NS_SVG; module.exports.NodeType = NodeType; module.exports.QUOTE_DOUBLE = QUOTE_DOUBLE; module.exports.QUOTE_NONE = QUOTE_NONE; module.exports.QUOTE_SINGLE = QUOTE_SINGLE; module.exports.SVG_TAG_ADJUST = SVG_TAG_ADJUST; module.exports.SourceProcessor = SourceProcessor; // Exposed so HtmlParser can map user-configured (lowercased) tag names to // the adjusted camelCase names the AST carries for foreign content. module.exports.buildHtmlAst = buildHtmlAst; module.exports.decodeHtmlEntities = decodeHtmlEntities; module.exports.decodeHtmlEntitiesWithMap = decodeHtmlEntitiesWithMap; module.exports.parseSrcset = parseSrcset; module.exports.walkHtmlTokens = walkHtmlTokens;