Files
2026-09-10 03:12:32 +00:00

4059 lines
150 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
MIT License http://www.opensource.org/licenses/mit-license.php
Author Tobias Koppers @sokra
*/
"use strict";
const LocConverter = require("../util/LocConverter");
const GenericSourceProcessor = require("../util/SourceProcessor");
const { makeCacheable } = require("../util/identifier");
// spec: https://drafts.csswg.org/css-syntax/
/**
* @typedef {object} CssWhitespaceToken
* @property {number} type
* @property {number} start byte offset of the first whitespace code point
* @property {number} end byte offset just past the last whitespace code point
*/
/**
* @typedef {object} CssCommentToken
* @property {number} type
* @property {number} start byte offset of the opening `/`
* @property {number} end byte offset just past the closing `/`
*/
/**
* @typedef {object} CssStringToken
* @property {number} type
* @property {number} start byte offset of the opening quote
* @property {number} end byte offset just past the closing quote (or EOF for unterminated strings)
*/
/**
* @typedef {object} CssBadStringToken
* @property {number} type
* @property {number} start byte offset of the opening quote
* @property {number} end byte offset where parsing gave up (typically the newline that broke the string)
*/
/**
* @typedef {object} CssLeftCurlyBracketToken
* @property {number} type
* @property {number} start byte offset of `{`
* @property {number} end `start + 1`
*/
/**
* @typedef {object} CssRightCurlyBracketToken
* @property {number} type
* @property {number} start byte offset of `}`
* @property {number} end `start + 1`
*/
/**
* @typedef {object} CssLeftSquareBracketToken
* @property {number} type
* @property {number} start byte offset of `[`
* @property {number} end `start + 1`
*/
/**
* @typedef {object} CssRightSquareBracketToken
* @property {number} type
* @property {number} start byte offset of `]`
* @property {number} end `start + 1`
*/
/**
* @typedef {object} CssLeftParenthesisToken
* @property {number} type
* @property {number} start byte offset of `(`
* @property {number} end `start + 1`
*/
/**
* @typedef {object} CssRightParenthesisToken
* @property {number} type
* @property {number} start byte offset of `)`
* @property {number} end `start + 1`
*/
/**
* @typedef {object} CssFunctionToken
* @property {number} type
* @property {number} start byte offset of the function name's first code point
* @property {number} end byte offset just past the `(` that closes the function token
*/
/**
* @typedef {object} CssUrlToken
* @property {number} type
* @property {number} start byte offset of the `url(` keyword (i.e. the `u`)
* @property {number} end byte offset just past the closing `)` (or EOF)
* @property {number} contentStart byte offset of the first code point of the unquoted URL content (post leading whitespace)
* @property {number} contentEnd byte offset just past the last code point of the unquoted URL content (pre trailing whitespace / `)` / EOF)
*/
/**
* @typedef {object} CssBadUrlToken
* @property {number} type
* @property {number} start byte offset of the `url(` keyword
* @property {number} end byte offset where parsing gave up (past the recovery `)` or EOF)
*/
/**
* @typedef {object} CssColonToken
* @property {number} type
* @property {number} start byte offset of `:`
* @property {number} end `start + 1`
*/
/**
* @typedef {object} CssAtKeywordToken
* @property {number} type
* @property {number} start byte offset of `@`
* @property {number} end byte offset just past the last ident-sequence code point
*/
/**
* @typedef {object} CssDelimToken
* @property {number} type
* @property {number} start byte offset of the delim code point
* @property {number} end `start + 1`
*/
/**
* @typedef {object} CssIdentToken
* @property {number} type
* @property {number} start byte offset of the first ident code point
* @property {number} end byte offset just past the last ident-sequence code point
*/
/**
* @typedef {object} CssPercentageToken
* @property {number} type
* @property {number} start byte offset of the first numeric code point
* @property {number} end byte offset just past the `%`
*/
/**
* @typedef {object} CssNumberToken
* @property {number} type
* @property {number} start byte offset of the first numeric code point
* @property {number} end byte offset just past the last numeric code point
*/
/**
* @typedef {object} CssDimensionToken
* @property {number} type
* @property {number} start byte offset of the first numeric code point
* @property {number} end byte offset just past the last unit ident code point
* @property {number} unitStart byte offset of the first unit-ident code point (== end of the numeric run)
*/
/**
* @typedef {object} CssHashToken
* @property {number} type
* @property {number} start byte offset of `#`
* @property {number} end byte offset just past the last ident-sequence code point
* @property {boolean} isId true when the hash starts an ident sequence (`#foo`), false for non-ident hashes (`#1abc`)
*/
/**
* @typedef {object} CssSemicolonToken
* @property {number} type
* @property {number} start byte offset of `;`
* @property {number} end `start + 1`
*/
/**
* @typedef {object} CssCommaToken
* @property {number} type
* @property {number} start byte offset of `,`
* @property {number} end `start + 1`
*/
/**
* @typedef {object} CssCdoToken
* @property {number} type
* @property {number} start byte offset of `<`
* @property {number} end byte offset just past `<!--`
*/
/**
* @typedef {object} CssCdcToken
* @property {number} type
* @property {number} start byte offset of `-`
* @property {number} end byte offset just past `-->`
*/
/**
* @typedef {CssWhitespaceToken | CssCommentToken | CssStringToken | CssBadStringToken | CssLeftCurlyBracketToken | CssRightCurlyBracketToken | CssLeftSquareBracketToken | CssRightSquareBracketToken | CssLeftParenthesisToken | CssRightParenthesisToken | CssFunctionToken | CssUrlToken | CssBadUrlToken | CssColonToken | CssAtKeywordToken | CssDelimToken | CssIdentToken | CssPercentageToken | CssNumberToken | CssDimensionToken | CssHashToken | CssSemicolonToken | CssCommaToken | CssCdoToken | CssCdcToken} CssToken
*/
const CC_LINE_FEED = "\n".charCodeAt(0);
const CC_CARRIAGE_RETURN = "\r".charCodeAt(0);
const CC_FORM_FEED = "\f".charCodeAt(0);
const CC_TAB = "\t".charCodeAt(0);
const CC_SPACE = " ".charCodeAt(0);
const CC_SOLIDUS = "/".charCodeAt(0);
const CC_REVERSE_SOLIDUS = "\\".charCodeAt(0);
const CC_ASTERISK = "*".charCodeAt(0);
const CC_LEFT_PARENTHESIS = "(".charCodeAt(0);
const CC_RIGHT_PARENTHESIS = ")".charCodeAt(0);
const CC_LEFT_CURLY = "{".charCodeAt(0);
const CC_RIGHT_CURLY = "}".charCodeAt(0);
const CC_LEFT_SQUARE = "[".charCodeAt(0);
const CC_RIGHT_SQUARE = "]".charCodeAt(0);
const CC_QUOTATION_MARK = '"'.charCodeAt(0);
const CC_APOSTROPHE = "'".charCodeAt(0);
const CC_FULL_STOP = ".".charCodeAt(0);
const CC_COLON = ":".charCodeAt(0);
const CC_SEMICOLON = ";".charCodeAt(0);
const CC_COMMA = ",".charCodeAt(0);
const CC_PERCENTAGE = "%".charCodeAt(0);
const CC_AT_SIGN = "@".charCodeAt(0);
const CC_LOW_LINE = "_".charCodeAt(0);
const CC_LOWER_A = "a".charCodeAt(0);
const CC_LOWER_D = "d".charCodeAt(0);
const CC_LOWER_F = "f".charCodeAt(0);
const CC_LOWER_E = "e".charCodeAt(0);
const CC_LOWER_U = "u".charCodeAt(0);
const CC_LOWER_R = "r".charCodeAt(0);
const CC_LOWER_L = "l".charCodeAt(0);
const CC_LOWER_Z = "z".charCodeAt(0);
const CC_EXCLAMATION = "!".charCodeAt(0);
const CC_UPPER_A = "A".charCodeAt(0);
const CC_UPPER_F = "F".charCodeAt(0);
const CC_UPPER_E = "E".charCodeAt(0);
const CC_UPPER_Z = "Z".charCodeAt(0);
const CC_0 = "0".charCodeAt(0);
const CC_9 = "9".charCodeAt(0);
const CC_NUMBER_SIGN = "#".charCodeAt(0);
const CC_PLUS_SIGN = "+".charCodeAt(0);
const CC_HYPHEN_MINUS = "-".charCodeAt(0);
const CC_LESS_THAN_SIGN = "<".charCodeAt(0);
const CC_GREATER_THAN_SIGN = ">".charCodeAt(0);
// Lexer token types (CSS Syntax Level 3 §4) plus the `<eof-token>`. Numeric so
// the per-token `type` slot stays compact and `next` / `consume` / the consume
// algorithms dispatch on integer `===` instead of string comparison. Exported
// alongside `readToken` (the per-token lexer primitive) for the unit test.
const TT_COMMENT = 1;
const TT_WHITESPACE = 2;
const TT_STRING = 3;
const TT_BAD_STRING_TOKEN = 4;
const TT_HASH = 5;
const TT_DELIM = 6;
// The three opening brackets are kept contiguous (7..9) so "is this an opening
// bracket?" is a single range check (`>= TT_LEFT_PARENTHESIS && <= TT_LEFT_CURLY_BRACKET`).
const TT_LEFT_PARENTHESIS = 7;
const TT_LEFT_SQUARE_BRACKET = 8;
const TT_LEFT_CURLY_BRACKET = 9;
const TT_RIGHT_PARENTHESIS = 10;
const TT_RIGHT_SQUARE_BRACKET = 11;
const TT_RIGHT_CURLY_BRACKET = 12;
const TT_COMMA = 13;
const TT_COLON = 14;
const TT_SEMICOLON = 15;
const TT_AT_KEYWORD = 16;
const TT_FUNCTION = 17;
const TT_URL = 18;
const TT_BAD_URL_TOKEN = 19;
const TT_IDENTIFIER = 20;
const TT_NUMBER = 21;
const TT_PERCENTAGE = 22;
const TT_DIMENSION = 23;
const TT_CDO = 24;
const TT_CDC = 25;
const TT_EOF = 26;
// The opening bracket types (7..9) and their mirror closers (10..12) are laid
// out so a closer is always `opener + 3`; `consumeASimpleBlock` uses that
// directly. The associated block char is a dense array indexed by the opener's
// offset from `TT_LEFT_PARENTHESIS` — a plain element load instead of a numeric
// object-key lookup.
/** @type {SimpleBlockToken[]} */
const BLOCK_TOKEN_CHAR = ["(", "[", "{"];
/**
* @param {number} cc char code
* @returns {boolean} true, if cc is a newline (per the spec: LF, CR, or FF)
*/
const _isNewline = (cc) =>
cc === CC_LINE_FEED || cc === CC_CARRIAGE_RETURN || cc === CC_FORM_FEED;
/**
* If the source had a CR followed by an LF, advance past the LF —
* the spec normalises CRLF to LF during preprocessing.
* @param {number} cc char code already consumed (the CR)
* @param {string} input input
* @param {number} pos position just past `cc`
* @returns {number} position past the CRLF pair (or unchanged for bare CR)
*/
const consumeExtraNewline = (cc, input, pos) => {
if (cc === CC_CARRIAGE_RETURN && input.charCodeAt(pos) === CC_LINE_FEED) {
pos++;
}
return pos;
};
/**
* @param {number} cc char code
* @returns {boolean} true, if cc is space or tab
*/
const _isSpace = (cc) => cc === CC_SPACE || cc === CC_TAB;
/**
* @param {number} cc char code
* @returns {boolean} true, if cc is whitespace (space/tab/newline)
*/
// Space-first: U+0020 is the common case, so it short-circuits before the
// rarer tab / newline tests.
const _isWhiteSpace = (cc) => _isSpace(cc) || _isNewline(cc);
// Whitespace membership table for the run-consumption loop — one load instead
// of up to five compares per char. EOF (NaN) / non-ASCII index to undefined.
const _wsTable = new Uint8Array(128);
_wsTable[CC_SPACE] = 1;
_wsTable[CC_TAB] = 1;
_wsTable[CC_LINE_FEED] = 1;
_wsTable[CC_CARRIAGE_RETURN] = 1;
_wsTable[CC_FORM_FEED] = 1;
/**
* @param {number} cc char code
* @returns {boolean} true, if cc is a digit
*/
const _isDigit = (cc) => cc >= CC_0 && cc <= CC_9;
/**
* @param {number} cc char code
* @returns {boolean} true, if cc is a hex digit
*/
const _isHexDigit = (cc) =>
_isDigit(cc) ||
(cc >= CC_UPPER_A && cc <= CC_UPPER_F) ||
(cc >= CC_LOWER_A && cc <= CC_LOWER_F);
/**
* @param {number} cc char code
* @returns {boolean} is letter (a-z / A-Z)
*/
const _isLetter = (cc) =>
(cc >= CC_LOWER_A && cc <= CC_LOWER_Z) ||
(cc >= CC_UPPER_A && cc <= CC_UPPER_Z);
/**
* Spec: ident-start = letter / non-ASCII / `_`. Internal helper that
* accepts an explicit char code (lookahead).
* @param {number} cc char code
* @returns {boolean} true, if cc is an ident-start code point
*/
const _isIdentStartCodePointCC = (cc) =>
_isLetter(cc) || cc >= 0x80 || cc === CC_LOW_LINE;
/**
* Spec: ident-code = ident-start / digit / hyphen-minus.
*/
// Full `charCodeAt` range (0..0xFFFF) so the per-code-point ident test is one
// table load with no `cc < 128` branch — `_consumeAnIdentSequence` runs this on
// every character of every ident / class / property name (the tokenizer's
// hottest loop). Every non-ASCII code unit (>= 0x80) is an ident code point per
// spec, so those default to 1; only the ASCII rows carry real classification.
// Callers must index with `cc | 0`: EOF (`charCodeAt` → NaN) becomes 0 (NUL,
// not an ident) — a raw NaN index is an out-of-range access that permanently
// degrades the load site's IC.
const _identCharTable = new Uint8Array(0x10000).fill(1);
for (let i = 0; i < 128; i++) {
_identCharTable[i] =
_isLetter(i) || i === CC_LOW_LINE || _isDigit(i) || i === CC_HYPHEN_MINUS
? 1
: 0;
}
/**
* @param {number} cc char code
* @returns {boolean} true, if cc is an ident-sequence code point
*/
const _isIdentCodePoint = (cc) => _identCharTable[cc | 0] === 1;
/**
* ASCII case-insensitive equality against a lowercase literal — avoids the
* `toLowerCase()` allocation and matches CSS's ASCII case-insensitive keyword
* matching. `lit` must be lowercase ASCII.
* @param {string} s string to test
* @param {string} lit lowercase ASCII literal to match
* @returns {boolean} true, if `s` equals `lit` ignoring ASCII case
*/
const equalsLowerCase = (s, lit) => {
if (s.length !== lit.length) return false;
for (let i = 0; i < lit.length; i++) {
let c = s.charCodeAt(i);
if (c >= CC_UPPER_A && c <= CC_UPPER_Z) c |= 0x20;
if (c !== lit.charCodeAt(i)) return false;
}
return true;
};
/**
* Case-sensitive equality of a source range against a literal — no slice.
* @param {string} input source
* @param {number} start range start
* @param {number} end range end (exclusive)
* @param {string} lit literal to match
* @returns {boolean} true when the range equals `lit`
*/
const rangeEquals = (input, start, end, lit) =>
end - start === lit.length && input.startsWith(lit, start);
/**
* ASCII case-insensitive equality of a source range against a lowercase ASCII literal — no slice.
* @param {string} input source
* @param {number} start range start
* @param {number} end range end (exclusive)
* @param {string} lit lowercase ASCII literal to match
* @returns {boolean} true when the range equals `lit` ignoring ASCII case
*/
const rangeEqualsLowerCase = (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 >= CC_UPPER_A && c <= CC_UPPER_Z) c |= 0x20;
if (c !== lit.charCodeAt(i)) return false;
}
return true;
};
/**
* `s.toLowerCase()` that returns `s` itself (no allocation) when it can't
* change — no ASCII uppercase and no non-ASCII (whose Unicode case mapping is
* left to the real `toLowerCase`).
* @param {string} s string
* @returns {string} lowercased string
*/
const toLowerCaseIfNeeded = (s) => {
for (let i = 0; i < s.length; i++) {
const c = s.charCodeAt(i);
if ((c >= CC_UPPER_A && c <= CC_UPPER_Z) || c > 127) return s.toLowerCase();
}
return s;
};
/**
* A custom property name (`<dashed-ident>`): a `--`-prefixed identifier other than bare `--`.
* @param {string} identifier identifier
* @returns {boolean} true when identifier is dashed, otherwise false
*/
const isDashedIdentifier = (identifier) =>
identifier.startsWith("--") && identifier.length >= 3;
/**
* Consume an escaped code point.
* @param {string} input input
* @param {number} pos position just past the `\`
* @returns {number} position past the escape sequence
*/
const _consumeAnEscapedCodePoint = (input, pos) => {
// Caller has verified the `\` and the next code point form a valid
// escape. Hex digits: consume up to 6 hex digits, then one optional
// whitespace. Non-hex: consume one code point.
// `\` at EOF: nothing to consume; return pos so callers don't overrun.
if (pos >= input.length) return pos;
const cc = input.charCodeAt(pos);
pos++;
if (pos === input.length) return pos;
if (_isHexDigit(cc)) {
for (let i = 0; i < 5; i++) {
if (!_isHexDigit(input.charCodeAt(pos))) break;
pos++;
}
const trail = input.charCodeAt(pos);
if (_isWhiteSpace(trail)) {
pos++;
pos = consumeExtraNewline(trail, input, pos);
}
}
return pos;
};
/**
* Spec: "two code points are a valid escape" — first is `\`, second is
* not a newline.
* @param {string} input input
* @param {number} pos position of the second code point
* @param {number=} f first code point (defaults to `input.charCodeAt(pos - 1)`)
* @param {number=} s second code point (defaults to `input.charCodeAt(pos)`)
* @returns {boolean} true, if the two code points form a valid escape
*/
const _ifTwoCodePointsAreValidEscape = (input, pos, f, s) => {
const first = f || input.charCodeAt(pos - 1);
const second = s || input.charCodeAt(pos);
if (first !== CC_REVERSE_SOLIDUS) return false;
if (_isNewline(second)) return false;
return true;
};
/**
* Spec: "three code points would start an ident sequence".
* @param {string} input input
* @param {number} pos position
* @param {number=} f first code point (defaults to `input.charCodeAt(pos - 1)`)
* @param {number=} s second code point (defaults to `input.charCodeAt(pos)`)
* @param {number=} t third code point (defaults to `input.charCodeAt(pos + 1)`)
* @returns {boolean} true, if the three code points start an ident sequence
*/
const _ifThreeCodePointsWouldStartAnIdentSequence = (input, pos, f, s, t) => {
const first = f || input.charCodeAt(pos - 1);
const second = s || input.charCodeAt(pos);
const third = t || input.charCodeAt(pos + 1);
if (first === CC_HYPHEN_MINUS) {
return (
_isIdentStartCodePointCC(second) ||
second === CC_HYPHEN_MINUS ||
_ifTwoCodePointsAreValidEscape(input, pos, second, third)
);
}
if (_isIdentStartCodePointCC(first)) return true;
if (first === CC_REVERSE_SOLIDUS) {
return _ifTwoCodePointsAreValidEscape(input, pos, first, second);
}
return false;
};
/**
* Spec: "three code points would start a number".
* @param {string} input input
* @param {number} pos position
* @param {number=} f first code point
* @param {number=} s second code point
* @param {number=} t third code point
* @returns {boolean} true, if the three code points start a number
*/
const _ifThreeCodePointsWouldStartANumber = (input, pos, f, s, t) => {
const first = f || input.charCodeAt(pos - 1);
const second = s || input.charCodeAt(pos);
const third = t || input.charCodeAt(pos + 1);
if (first === CC_PLUS_SIGN || first === CC_HYPHEN_MINUS) {
if (_isDigit(second)) return true;
return second === CC_FULL_STOP && _isDigit(third);
}
if (first === CC_FULL_STOP) return _isDigit(second);
/* istanbul ignore next -- @preserve: spec-general; every caller passes `pos` just past a +/-/. so `first` is never a bare digit here */
return _isDigit(first);
};
/**
* Consume an ident sequence (no validation of the first code points).
* @param {string} input input
* @param {number} pos position
* @returns {number} position just past the last ident-sequence code point
*/
const _consumeAnIdentSequence = (input, pos) => {
// Hot loop (every ident, at-keyword, hash, function name, unit). Both checks
// are inlined from `_isIdentCodePoint` / `_ifTwoCodePointsAreValidEscape`: the
// ident test is a single full-range table load (no `cc < 128` branch), and the
// escape test reads the following code point only when `cc` is a `\` (rare)
// instead of eagerly.
for (;;) {
const cc = input.charCodeAt(pos) | 0;
pos++;
if (_identCharTable[cc] === 1) {
continue;
}
if (cc === CC_REVERSE_SOLIDUS && !_isNewline(input.charCodeAt(pos))) {
pos = _consumeAnEscapedCodePoint(input, pos);
continue;
}
return pos - 1;
}
};
/**
* @param {number} cc char code
* @returns {boolean} true, if cc is a non-printable code point
*/
const _isNonPrintableCodePoint = (cc) =>
(cc >= 0x00 && cc <= 0x08) ||
cc === 0x0b ||
(cc >= 0x0e && cc <= 0x1f) ||
cc === 0x7f;
/**
* Consume the body of a number per the spec (does not classify integer
* vs number — caller / token type handles that).
* @param {string} input input
* @param {number} pos position at the first numeric / sign code point
* @returns {number} position just past the number
*/
const _consumeANumber = (input, pos) => {
let cc = input.charCodeAt(pos);
if (cc === CC_HYPHEN_MINUS || cc === CC_PLUS_SIGN) {
pos++;
}
while (_isDigit(input.charCodeAt(pos))) pos++;
if (
input.charCodeAt(pos) === CC_FULL_STOP &&
_isDigit(input.charCodeAt(pos + 1))
) {
pos++;
while (_isDigit(input.charCodeAt(pos))) pos++;
}
cc = input.charCodeAt(pos);
if (
(cc === CC_LOWER_E || cc === CC_UPPER_E) &&
(((input.charCodeAt(pos + 1) === CC_HYPHEN_MINUS ||
input.charCodeAt(pos + 1) === CC_PLUS_SIGN) &&
_isDigit(input.charCodeAt(pos + 2))) ||
_isDigit(input.charCodeAt(pos + 1)))
) {
pos++;
cc = input.charCodeAt(pos);
if (cc === CC_PLUS_SIGN || cc === CC_HYPHEN_MINUS) {
pos++;
}
while (_isDigit(input.charCodeAt(pos))) pos++;
}
return pos;
};
/**
* Spec recovery: when the tokenizer realises it's mid-bad-url, consume
* until `)` or EOF.
* @param {string} input input
* @param {number} pos position
* @returns {number} position past the recovery `)` or EOF
*/
const _consumeTheRemnantsOfABadUrl = (input, pos) => {
for (;;) {
if (pos === input.length) return pos;
const cc = input.charCodeAt(pos);
pos++;
if (cc === CC_RIGHT_PARENTHESIS) return pos;
if (_ifTwoCodePointsAreValidEscape(input, pos)) {
pos = _consumeAnEscapedCodePoint(input, pos);
}
}
};
/**
* A mutable lexer token. The `next` / `consume` hot path reuses a single
* instance per `TokenStream` (the lexer writes into it instead of allocating
* one object per token), which also keeps the parser's `t.type` reads
* monomorphic. All fields are present from construction so the shape never
* transitions; type-specific fields (`isId` / `contentStart` / `contentEnd` /
* `unitStart`) carry stale values for unrelated token types and are only read
* by `tokenToNode` for the matching type. Pass a fresh one per `readToken` call
* to collect the raw token list (e.g. tests).
* @typedef {object} MutableToken
* @property {number} type one of the `TT_*` constants
* @property {number} start byte offset of the token's first code point
* @property {number} end byte offset just past the token's last code point
* @property {boolean} isId hash tokens: starts an ident sequence
* @property {number} contentStart url tokens: first content code point
* @property {number} contentEnd url tokens: just past the last content code point
* @property {number} unitStart dimension tokens: first unit-ident code point
*/
/**
* @returns {MutableToken} a fresh lexer token with the canonical shape
*/
const createToken = () => ({
type: TT_EOF,
start: 0,
end: 0,
isId: false,
contentStart: 0,
contentEnd: 0,
unitStart: 0
});
/**
* Populate `out`'s common fields and return it — the lexer functions' return
* statement (kept tiny so V8 can inline it).
* @param {MutableToken} out token to populate
* @param {number} type one of the `TT_*` constants
* @param {number} start byte offset of the token's first code point
* @param {number} end byte offset just past the token's last code point
* @returns {MutableToken} `out`
*/
const fill = (out, type, start, end) => {
out.type = type;
out.start = start;
out.end = end;
return out;
};
/**
* Whitespace token. Caller advances past the leading code point so
* `start = pos - 1`.
* @param {string} input input
* @param {number} pos position just past the first whitespace code point
* @param {MutableToken} out token to populate
* @returns {MutableToken | undefined} the resulting token, or undefined at EOF
*/
function consumeSpace(input, pos, out) {
const start = pos - 1;
while (_wsTable[input.charCodeAt(pos)] === 1) pos++;
return fill(out, TT_WHITESPACE, start, pos);
}
/**
* Consume a string token. Caller advanced past the opening quote so
* `pos - 1` holds the ending code point and `pos - 1` is the start.
* @param {string} input input
* @param {number} pos position just past the opening quote
* @param {MutableToken} out token to populate
* @returns {MutableToken | undefined} the resulting token, or undefined at EOF
*/
function consumeAStringToken(input, pos, out) {
const start = pos - 1;
const endingCodePoint = input.charCodeAt(pos - 1);
for (;;) {
if (pos === input.length) {
return fill(out, TT_STRING, start, pos);
}
const cc = input.charCodeAt(pos);
pos++;
if (cc === endingCodePoint) {
return fill(out, TT_STRING, start, pos);
}
if (_isNewline(cc)) {
pos--;
return fill(out, TT_BAD_STRING_TOKEN, start, pos);
}
if (cc === CC_REVERSE_SOLIDUS) {
// `\` at EOF: string ends here; emit the token so ranges cover all input.
if (pos === input.length) return fill(out, TT_STRING, start, pos);
if (_isNewline(input.charCodeAt(pos))) {
const ccNl = input.charCodeAt(pos);
pos++;
pos = consumeExtraNewline(ccNl, input, pos);
} else if (_ifTwoCodePointsAreValidEscape(input, pos)) {
pos = _consumeAnEscapedCodePoint(input, pos);
}
}
}
}
/**
* `#` — hash or delim.
* @param {string} input input
* @param {number} pos position just past `#`
* @param {MutableToken} out token to populate
* @returns {MutableToken | undefined} the resulting token, or undefined at EOF
*/
function consumeNumberSign(input, pos, out) {
const start = pos - 1;
const first = input.charCodeAt(pos);
const second = input.charCodeAt(pos + 1);
if (
_isIdentCodePoint(first) ||
_ifTwoCodePointsAreValidEscape(input, pos, first, second)
) {
const third = input.charCodeAt(pos + 2);
out.isId = _ifThreeCodePointsWouldStartAnIdentSequence(
input,
pos,
first,
second,
third
);
pos = _consumeAnIdentSequence(input, pos);
return fill(out, TT_HASH, start, pos);
}
return fill(out, TT_DELIM, start, pos);
}
/**
* `-` — number / cdc / ident / delim.
* @param {string} input input
* @param {number} pos position just past `-`
* @param {MutableToken} out token to populate
* @returns {MutableToken | undefined} the resulting token, or undefined at EOF
*/
function consumeHyphenMinus(input, pos, out) {
// Read the two lookahead code points once; the lead is the known `-`.
const second = input.charCodeAt(pos);
const third = input.charCodeAt(pos + 1);
if (
_ifThreeCodePointsWouldStartANumber(
input,
pos,
CC_HYPHEN_MINUS,
second,
third
)
) {
pos--;
return consumeANumericToken(input, pos, out);
}
if (second === CC_HYPHEN_MINUS && third === CC_GREATER_THAN_SIGN) {
return fill(out, TT_CDC, pos - 1, pos + 2);
}
if (
_ifThreeCodePointsWouldStartAnIdentSequence(
input,
pos,
CC_HYPHEN_MINUS,
second,
third
)
) {
pos--;
return consumeAnIdentLikeToken(input, pos, out);
}
return fill(out, TT_DELIM, pos - 1, pos);
}
/**
* `.` — number or delim.
* @param {string} input input
* @param {number} pos position just past `.`
* @param {MutableToken} out token to populate
* @returns {MutableToken | undefined} the resulting token, or undefined at EOF
*/
function consumeFullStop(input, pos, out) {
const start = pos - 1;
if (_ifThreeCodePointsWouldStartANumber(input, pos)) {
pos--;
return consumeANumericToken(input, pos, out);
}
return fill(out, TT_DELIM, start, pos);
}
/**
* `+` — number or delim.
* @param {string} input input
* @param {number} pos position just past `+`
* @param {MutableToken} out token to populate
* @returns {MutableToken | undefined} the resulting token, or undefined at EOF
*/
function consumePlusSign(input, pos, out) {
const start = pos - 1;
if (_ifThreeCodePointsWouldStartANumber(input, pos)) {
pos--;
return consumeANumericToken(input, pos, out);
}
return fill(out, TT_DELIM, start, pos);
}
/**
* Numeric token: number / percentage / dimension.
* @param {string} input input
* @param {number} pos position at the first numeric/sign code point
* @param {MutableToken} out token to populate
* @returns {MutableToken | undefined} the resulting token, or undefined at EOF
*/
function consumeANumericToken(input, pos, out) {
const start = pos;
pos = _consumeANumber(input, pos);
const first = input.charCodeAt(pos);
// A unit can only begin with `-`, `\`, or an ident-start code point — exactly
// the cases where the §4 "would start an ident sequence" check can be true. For
// a plain number (next char is whitespace / `;` / `,` / `)` / EOF, the common
// case) skip the two lookahead reads and the call entirely.
if (
(first === CC_HYPHEN_MINUS ||
first === CC_REVERSE_SOLIDUS ||
_isIdentStartCodePointCC(first)) &&
_ifThreeCodePointsWouldStartAnIdentSequence(
input,
pos,
first,
input.charCodeAt(pos + 1),
input.charCodeAt(pos + 2)
)
) {
out.unitStart = pos;
pos = _consumeAnIdentSequence(input, pos);
return fill(out, TT_DIMENSION, start, pos);
}
if (first === CC_PERCENTAGE) {
return fill(out, TT_PERCENTAGE, start, pos + 1);
}
return fill(out, TT_NUMBER, start, pos);
}
/**
* Consume an unquoted url token. Caller has already eaten `url(` and
* any leading whitespace.
* @param {string} input input
* @param {number} pos position at the first content code point
* @param {number} fnStart byte offset of the `u` in `url(`
* @param {MutableToken} out token to populate
* @returns {MutableToken | undefined} the resulting token, or undefined at EOF
*/
function consumeAUrlToken(input, pos, fnStart, out) {
while (_isWhiteSpace(input.charCodeAt(pos))) pos++;
const contentStart = pos;
out.contentStart = contentStart;
for (;;) {
if (pos === input.length) {
out.contentEnd = pos;
return fill(out, TT_URL, fnStart, pos);
}
const cc = input.charCodeAt(pos);
pos++;
if (cc === CC_RIGHT_PARENTHESIS) {
out.contentEnd = pos - 1;
return fill(out, TT_URL, fnStart, pos);
}
if (_isWhiteSpace(cc)) {
const end = pos - 1;
while (_isWhiteSpace(input.charCodeAt(pos))) pos++;
if (pos === input.length) {
out.contentEnd = end;
return fill(out, TT_URL, fnStart, pos);
}
if (input.charCodeAt(pos) === CC_RIGHT_PARENTHESIS) {
pos++;
out.contentEnd = end;
return fill(out, TT_URL, fnStart, pos);
}
pos = _consumeTheRemnantsOfABadUrl(input, pos);
return fill(out, TT_BAD_URL_TOKEN, fnStart, pos);
}
if (
cc === CC_QUOTATION_MARK ||
cc === CC_APOSTROPHE ||
cc === CC_LEFT_PARENTHESIS ||
_isNonPrintableCodePoint(cc)
) {
pos = _consumeTheRemnantsOfABadUrl(input, pos);
return fill(out, TT_BAD_URL_TOKEN, fnStart, pos);
}
if (cc === CC_REVERSE_SOLIDUS) {
if (_ifTwoCodePointsAreValidEscape(input, pos)) {
pos = _consumeAnEscapedCodePoint(input, pos);
} else {
pos = _consumeTheRemnantsOfABadUrl(input, pos);
return fill(out, TT_BAD_URL_TOKEN, fnStart, pos);
}
}
}
}
/**
* Consume an ident-like token: ident / function / url / bad-url.
* @param {string} input input
* @param {number} pos position at the first ident-start code point
* @param {MutableToken} out token to populate
* @returns {MutableToken | undefined} the resulting token, or undefined at EOF
*/
function consumeAnIdentLikeToken(input, pos, out) {
const start = pos;
pos = _consumeAnIdentSequence(input, pos);
// `url` case-insensitively (ASCII lower via `| 0x20`) without a
// `slice().toLowerCase()` allocation per identifier; an escaped ident can't
// be exactly 3 raw chars, so the length gate keeps this equivalent.
if (
pos - start === 3 &&
(input.charCodeAt(start) | 0x20) === CC_LOWER_U &&
(input.charCodeAt(start + 1) | 0x20) === CC_LOWER_R &&
(input.charCodeAt(start + 2) | 0x20) === CC_LOWER_L &&
input.charCodeAt(pos) === CC_LEFT_PARENTHESIS
) {
pos++;
const end = pos;
while (
_isWhiteSpace(input.charCodeAt(pos)) &&
_isWhiteSpace(input.charCodeAt(pos + 1))
) {
pos++;
}
if (
input.charCodeAt(pos) === CC_QUOTATION_MARK ||
input.charCodeAt(pos) === CC_APOSTROPHE ||
(_isWhiteSpace(input.charCodeAt(pos)) &&
(input.charCodeAt(pos + 1) === CC_QUOTATION_MARK ||
input.charCodeAt(pos + 1) === CC_APOSTROPHE))
) {
// End at `end` (the `(`'s closer position), not `pos` — the
// lookahead-eaten whitespace must be re-tokenized as a whitespace
// token rather than swallowed silently. The reader resumes at
// `token.end`, so returning `end` here does that.
return fill(out, TT_FUNCTION, start, end);
}
return consumeAUrlToken(input, pos, start, out);
}
if (input.charCodeAt(pos) === CC_LEFT_PARENTHESIS) {
pos++;
return fill(out, TT_FUNCTION, start, pos);
}
return fill(out, TT_IDENTIFIER, start, pos);
}
/**
* `<` — CDO or delim.
* @param {string} input input
* @param {number} pos position just past `<`
* @param {MutableToken} out token to populate
* @returns {MutableToken | undefined} the resulting token, or undefined at EOF
*/
function consumeLessThan(input, pos, out) {
if (
input.charCodeAt(pos) === CC_EXCLAMATION &&
input.charCodeAt(pos + 1) === CC_HYPHEN_MINUS &&
input.charCodeAt(pos + 2) === CC_HYPHEN_MINUS
) {
return fill(out, TT_CDO, pos - 1, pos + 3);
}
return fill(out, TT_DELIM, pos - 1, pos);
}
/**
* `@` — at-keyword or delim.
* @param {string} input input
* @param {number} pos position just past `@`
* @param {MutableToken} out token to populate
* @returns {MutableToken | undefined} the resulting token, or undefined at EOF
*/
function consumeCommercialAt(input, pos, out) {
const start = pos - 1;
if (
_ifThreeCodePointsWouldStartAnIdentSequence(
input,
pos,
input.charCodeAt(pos),
input.charCodeAt(pos + 1),
input.charCodeAt(pos + 2)
)
) {
pos = _consumeAnIdentSequence(input, pos);
return fill(out, TT_AT_KEYWORD, start, pos);
}
return fill(out, TT_DELIM, start, pos);
}
/**
* `\` — escape starts an ident-like token, otherwise it's a delim.
* @param {string} input input
* @param {number} pos position just past `\`
* @param {MutableToken} out token to populate
* @returns {MutableToken | undefined} the resulting token, or undefined at EOF
*/
function consumeReverseSolidus(input, pos, out) {
if (_ifTwoCodePointsAreValidEscape(input, pos)) {
pos--;
return consumeAnIdentLikeToken(input, pos, out);
}
return fill(out, TT_DELIM, pos - 1, pos);
}
// `consumeAToken` dispatch: the §4 token rules keyed by the lead code point are
// === Tokenizer lead-character dispatch (CSS Syntax Level 3 §4 "consume a token") ===
//
// `consumeAToken` selects a sub-routine from the first ("lead") code point of each
// token. The §4 rules are keyed on specific code points (`"` `#` `(` digit
// ident-start …) that sit SPARSELY across the ASCII range, so a plain `switch (cc)`
// compiles to a jump table spanning U+0009..U+007D in which the most common lead —
// an ident-start letter — is not a case and reaches its handler only after the
// digit/whitespace tests miss. `_charClass` precomputes, for every ASCII code
// point, a dense handler id (`HC_*`, 0..12) so `consumeAToken` is one array load +
// a compact 13-entry jump table and idents dispatch directly. Non-ASCII
// (cc >= 128) is always ident-start per §4, so it skips the table.
//
// Extending for a spec change: repoint the code point in the build loop below; if
// it needs a new sub-routine, add an `HC_*` id, a `case` in `consumeAToken`, and a
// row here. This list is the authoritative "which lead code point dispatches
// where" map (§4 "consume a token", step by lead code point):
//
// HC_WHITESPACE whitespace U+0009 TAB U+000A LF U+000C FF U+000D CR U+0020 SPACE
// HC_STRING string start U+0022 " U+0027 '
// HC_SINGLE one-char token ( ) , : ; [ ] { } (its token type comes from `_singleTT`)
// HC_NUMBER_SIGN hash / delim U+0023 #
// HC_PLUS_SIGN number / delim U+002B +
// HC_HYPHEN_MINUS number / CDC / ident / delim U+002D -
// HC_FULL_STOP number / delim U+002E .
// HC_LESS_THAN CDO / delim U+003C <
// HC_AT_SIGN at-keyword / delim U+0040 @
// HC_REVERSE_SOLIDUS escape / delim U+005C \
// HC_DIGIT number U+0030..U+0039 0-9
// HC_IDENT ident-like U+0041..U+005A A-Z U+0061..U+007A a-z U+005F _ (plus cc >= 128)
// HC_DELIM anything else -> a single <delim-token>
//
// `_singleTT[cc]` is the token type for the HC_SINGLE code points (a second table
// so they share one handler instead of one `case` each). The default class 0 is
// the delim handler (anything not matched below), so it needs no named constant.
const HC_WHITESPACE = 1;
const HC_STRING = 2;
const HC_SINGLE = 3;
const HC_NUMBER_SIGN = 4;
const HC_PLUS_SIGN = 5;
const HC_HYPHEN_MINUS = 6;
const HC_FULL_STOP = 7;
const HC_LESS_THAN = 8;
const HC_AT_SIGN = 9;
const HC_REVERSE_SOLIDUS = 10;
const HC_DIGIT = 11;
const HC_IDENT = 12;
// Full `charCodeAt` range so `consumeAToken` dispatches with one table load and
// no `cc < 128` branch. Every non-ASCII code point (>= 0x80) is an ident-start
// lead per §4, so those rows are seeded to `HC_IDENT`; the ASCII rows below
// overwrite 0..127 with their real class.
const _charClass = new Uint8Array(0x10000).fill(HC_IDENT, 128);
const _singleTT = new Uint8Array(128);
_singleTT[CC_LEFT_PARENTHESIS] = TT_LEFT_PARENTHESIS;
_singleTT[CC_RIGHT_PARENTHESIS] = TT_RIGHT_PARENTHESIS;
_singleTT[CC_COMMA] = TT_COMMA;
_singleTT[CC_COLON] = TT_COLON;
_singleTT[CC_SEMICOLON] = TT_SEMICOLON;
_singleTT[CC_LEFT_SQUARE] = TT_LEFT_SQUARE_BRACKET;
_singleTT[CC_RIGHT_SQUARE] = TT_RIGHT_SQUARE_BRACKET;
_singleTT[CC_LEFT_CURLY] = TT_LEFT_CURLY_BRACKET;
_singleTT[CC_RIGHT_CURLY] = TT_RIGHT_CURLY_BRACKET;
// Each ASCII code point belongs to exactly one class; HC_SINGLE is seeded from
// `_singleTT` above, the rest follow §4's lead-code-point rules, and everything
// unmatched stays the delim class (0). Keep this in sync with the table above.
for (let i = 0; i < 128; i++) {
if (_singleTT[i] !== 0) {
_charClass[i] = HC_SINGLE;
} else if (_isWhiteSpace(i)) {
_charClass[i] = HC_WHITESPACE;
} else if (i === CC_QUOTATION_MARK || i === CC_APOSTROPHE) {
_charClass[i] = HC_STRING;
} else if (i === CC_NUMBER_SIGN) {
_charClass[i] = HC_NUMBER_SIGN;
} else if (i === CC_PLUS_SIGN) {
_charClass[i] = HC_PLUS_SIGN;
} else if (i === CC_HYPHEN_MINUS) {
_charClass[i] = HC_HYPHEN_MINUS;
} else if (i === CC_FULL_STOP) {
_charClass[i] = HC_FULL_STOP;
} else if (i === CC_LESS_THAN_SIGN) {
_charClass[i] = HC_LESS_THAN;
} else if (i === CC_AT_SIGN) {
_charClass[i] = HC_AT_SIGN;
} else if (i === CC_REVERSE_SOLIDUS) {
_charClass[i] = HC_REVERSE_SOLIDUS;
} else if (_isDigit(i)) {
_charClass[i] = HC_DIGIT;
} else if (_isIdentStartCodePointCC(i)) {
_charClass[i] = HC_IDENT;
}
// else stays the delim class (0)
}
/**
* Per-character dispatcher. The outer loop has already advanced past
* the lead code point (`pos - 1` is the lead).
* @param {string} input input
* @param {number} pos position just past the lead code point
* @param {number} cc the lead code point (`input.charCodeAt(pos - 1)`, already read by the caller)
* @param {MutableToken} out token to populate
* @returns {MutableToken | undefined} the resulting token, or undefined at EOF
*/
function consumeAToken(input, pos, cc, out) {
// `u` / `U` would start a unicode-range token in the spec; those are not
// produced, so they map to HC_IDENT and fall through to ident-like.
switch (_charClass[cc]) {
// Run of whitespace → one <whitespace-token>.
case HC_WHITESPACE:
return consumeSpace(input, pos, out);
// `"` / `'` → <string-token> (or <bad-string-token> on a raw newline).
case HC_STRING:
return consumeAStringToken(input, pos, out);
// One-code-point token: its type is looked up in `_singleTT` (the `(` `)`
// `,` `:` `;` `[` `]` `{` `}` set), so all of them share this arm.
case HC_SINGLE:
return fill(out, _singleTT[cc], pos - 1, pos);
// `#` → <hash-token> if an ident/escape follows, else a <delim-token>.
case HC_NUMBER_SIGN:
return consumeNumberSign(input, pos, out);
// `+` → <number-token> if it starts a number, else a <delim-token>.
case HC_PLUS_SIGN:
return consumePlusSign(input, pos, out);
// `-` → number / <CDC-token> (`-->`) / ident / <delim-token>.
case HC_HYPHEN_MINUS:
return consumeHyphenMinus(input, pos, out);
// `.` → <number-token> if a digit follows, else a <delim-token>.
case HC_FULL_STOP:
return consumeFullStop(input, pos, out);
// `<` → <CDO-token> (`<!--`), else a <delim-token>.
case HC_LESS_THAN:
return consumeLessThan(input, pos, out);
// `@` → <at-keyword-token> if an ident follows, else a <delim-token>.
case HC_AT_SIGN:
return consumeCommercialAt(input, pos, out);
// `\` → ident-like token if it's a valid escape, else a <delim-token>.
case HC_REVERSE_SOLIDUS:
return consumeReverseSolidus(input, pos, out);
// Digit → numeric token; `pos - 1` re-includes the digit the caller passed.
case HC_DIGIT:
return consumeANumericToken(input, pos - 1, out);
// Ident-start (letter / `_` / non-ASCII, incl. `u`/`U`) → ident / function /
// url token; `pos - 1` re-includes the lead code point.
case HC_IDENT:
return consumeAnIdentLikeToken(input, pos - 1, out);
default:
// HC_DELIM. EOF is impossible here (caller guarded with the outer
// loop's `pos < input.length` check). Anything else: a <delim-token>.
return fill(out, TT_DELIM, pos - 1, pos);
}
}
/**
* Read one raw token (comment / whitespace / value token) starting at byte
* `pos`, writing it into the caller-supplied `out` and returning `out`. The
* token's `end` is the next read position. Returns `undefined` at end-of-input —
* `pos >= length`, an unterminated comment, or a string ending on a trailing
* escape. This is the shared lexer core: `next` reuses one `out` across calls so
* the parse hot path allocates no per-token object; loop over it with a fresh
* `out` per call to collect the raw token list (e.g. tests). Comment tokens are
* returned here; `next` filters them.
* @param {string} input input
* @param {number} pos byte offset to read from
* @param {MutableToken} out token to populate
* @returns {MutableToken | undefined} the token, or undefined at EOF
*/
function readToken(input, pos, out) {
if (pos >= input.length) return undefined;
const cc = input.charCodeAt(pos);
// Comment: `/*…*/` is yielded as a token (filtered by `next`).
if (cc === CC_SOLIDUS && input.charCodeAt(pos + 1) === CC_ASTERISK) {
const start = pos;
// Jump to the closing `*/` in one native scan instead of a per-character
// loop — comment bodies (license banners, source comments) can be long.
// No close: unterminated comment runs to EOF so ranges cover all input.
const close = input.indexOf("*/", pos + 2);
return fill(
out,
TT_COMMENT,
start,
close === -1 ? input.length : close + 2
);
}
// `consumeAToken` dispatches on the lead code point at `pos` (it expects the
// position just past the lead and the already-read lead code point).
return consumeAToken(input, pos + 1, cc, out);
}
// AST shape mirrors tabatkins/parse-css (the CSS Syntax Level 3 reference), with two deviations: nodes carry a `range` byte offset pair + a lazy `loc` getter, and have no methods beyond it.
/**
* AST node / leaf-token `type` discriminators (spec name where it has one, else
* parse-css's PascalCase). Numeric for the same reasons as the `TT_*` token
* constants: a compact `Node#type` slot and integer `===` / `Map` keys on the
* visitor hot path. Kept as a `NodeType` namespace (not bare constants) because
* consumers reference members as `NodeType.AtRule`; exported so visitor maps
* (`SourceProcessor#use`) and `CssParser` name nodes instead of a string
* literal. A lexer token type never reaches a `Node#type`.
* @enum {number}
*/
const NodeType = {
Ident: 1,
Function: 2,
AtKeyword: 3,
Hash: 4,
String: 5,
BadString: 6,
Url: 7,
BadUrl: 8,
Delim: 9,
Number: 10,
Percentage: 11,
Dimension: 12,
Whitespace: 13,
Colon: 14,
Semicolon: 15,
Comma: 16,
// Preserved tokens for stray closers / CDO / CDC (kept as component values per §5.4.8 "consume a token and return it").
RightParenthesis: 17,
RightSquareBracket: 18,
RightCurlyBracket: 19,
CDO: 20,
CDC: 21,
SimpleBlock: 22,
Declaration: 23,
AtRule: 24,
QualifiedRule: 25,
Stylesheet: 26,
// Comments are never tree nodes; this type exists only so a `NodeType.Comment`
// visitor can be registered (fired during tokenization — see `grammar`).
Comment: 27
};
const {
Ident: T_IDENT,
Function: T_FUNCTION,
AtKeyword: T_AT_KEYWORD,
Hash: T_HASH,
String: T_STRING,
BadString: T_BAD_STRING,
Url: T_URL,
BadUrl: T_BAD_URL,
Delim: T_DELIM,
Number: T_NUMBER,
Percentage: T_PERCENTAGE,
Dimension: T_DIMENSION,
Whitespace: T_WHITESPACE,
Colon: T_COLON,
Semicolon: T_SEMICOLON,
Comma: T_COMMA,
RightParenthesis: T_RIGHT_PARENTHESIS,
RightSquareBracket: T_RIGHT_SQUARE_BRACKET,
RightCurlyBracket: T_RIGHT_CURLY_BRACKET,
CDO: T_CDO,
CDC: T_CDC,
SimpleBlock: T_SIMPLE_BLOCK,
Declaration: T_DECLARATION,
AtRule: T_AT_RULE,
QualifiedRule: T_QUALIFIED_RULE,
Stylesheet: T_STYLESHEET,
Comment: T_COMMENT
} = NodeType;
/**
* Base AST node. All concrete nodes (tokens, simple blocks, functions,
* declarations) inherit from this and carry the `[start, end)` byte `range`
* of the source slice they cover. `loc` is computed on demand from a
* shared `LocConverter` so we don't pay for line/column conversion until
* a consumer (warning, error, dependency) actually needs it.
*/
class Node {
/**
* @param {number} type node type discriminator
* @param {number} start byte offset of the node's first code point
* @param {number} end byte offset just past the node's last code point
* @param {LocConverter} locConverter shared loc converter
*/
constructor(type, start, end, locConverter) {
/** @type {number} */
this.type = type;
// Byte range as two inline fields rather than a `[start, end]` array —
// one fewer allocation per node and ~56 bytes lighter (×100k+ nodes).
/** @type {number} */
this.start = start;
/** @type {number} */
this.end = end;
/** @type {LocConverter} */
this._locConverter = locConverter;
}
/**
* The `[start, end)` byte range as a tuple — compatibility view over
* `start` / `end` (builds the array lazily; hot code reads the
* fields directly).
* @returns {[number, number]} the byte range
*/
get range() {
return [this.start, this.end];
}
get loc() {
const lc = this._locConverter;
// `LocConverter#get` mutates and returns the converter itself, so we
// must snapshot `line`/`column` between the two calls.
const s = lc.get(this.start);
const sl = s.line;
const sc = s.column;
const e = lc.get(this.end);
return {
start: { line: sl, column: sc },
end: { line: e.line, column: e.column }
};
}
/**
* Serialize back to source — re-slices the original input (zero-alloc for
* untouched nodes).
* @returns {string} the source slice for this node
*/
toString() {
return this._locConverter._input.slice(this.start, this.end);
}
/**
* For name-bearing nodes (function / at-rule): the `name` with CSS escapes
* resolved, for case-insensitive keyword matching (`\75 rl` → `url`). Computed
* on read via `unescapeIdentifier`'s no-escape fast path. Callers without a
* `name` must not read this.
* @returns {string} the unescaped name
*/
get unescapedName() {
return unescapeIdentifier(
/** @type {{ name: string }} */ (/** @type {unknown} */ (this)).name
);
}
}
/**
* @param {string} s numeric text
* @returns {"+" | "-" | ""} the spec sign ("" when unsigned)
*/
const _signOf = (s) => {
const c = s.charCodeAt(0);
return c === CC_PLUS_SIGN ? "+" : c === CC_HYPHEN_MINUS ? "-" : "";
};
/**
* @param {string} s numeric text (no unit / `%`)
* @returns {"integer" | "number"} the spec type flag
*/
const _typeFlagOf = (s) =>
s.includes(".") || s.includes("e") || s.includes("E") ? "number" : "integer";
/**
* Leaf token node — the only `Node` subclass. `value` is the raw source slice
* (identifier text, quoted string including quotes, a dimension's full `123px`,
* …). Token-specific extras are named by the `HashToken` / `UrlToken` /
* `NumberToken` / `DimensionToken` shape typedefs below.
*
* The numeric accessors (`numericValue` / `typeFlag` / `sign` / `unit`) are
* getters, not stored fields: a number / dimension / percentage token costs
* nothing beyond the base node unless a consumer reads them, and every leaf
* token keeps a single object shape so the walker's `node.type` dispatch stays
* monomorphic. They are only meaningful on the matching token type.
*/
class Token extends Node {
/**
* @param {number} type node type
* @param {number} start byte offset of the token's first code point
* @param {number} end byte offset just past the token's last code point
* @param {LocConverter} locConverter shared loc converter
*/
// No own fields: a leaf token is exactly a `Node` plus the value getters
// below. `value` is derived from the byte range on read instead of cached in
// a `_value` slot — most tokens (whitespace, punctuation) never read it, and
// dropping the slot is ~8 bytes saved on every token (the bulk of all nodes).
// hash / at-keyword strip their `#` / `@` prefix; url uses its content range
// (`contentStart` / `contentEnd`, the token's only own fields).
/**
* @returns {string} the token's value (raw source slice unless overridden)
*/
get value() {
const input = this._locConverter._input;
const type = this.type;
// hash (`#name` → `name`) and at-keyword (`@name` → `name`) drop one char.
if (type === T_HASH || type === T_AT_KEYWORD) {
return input.slice(this.start + 1, this.end);
}
if (type === T_URL) {
const u = /** @type {UrlToken} */ (/** @type {unknown} */ (this));
return input.slice(u.contentStart, u.contentEnd);
}
return input.slice(this.start, this.end);
}
/**
* The token's value with CSS escapes resolved (`\2d` → `-`, `\75 rl` → `url`),
* per https://www.w3.org/TR/css-syntax-3/#consume-escaped-code-point — the
* form to match keywords / export as a CSS-Modules name against. For a string
* token it is the content between the quotes (the spec string value). Computed
* on read; `unescapeIdentifier` fast-returns the value unchanged when it has no
* escapes (the common case), so nothing is stored per token.
* @returns {string} the unescaped value
*/
get unescaped() {
const v = this.value;
// A string token's `value` carries its delimiting quotes; its value is the content between them.
return this.type === T_STRING
? unescapeIdentifier(v.slice(1, -1))
: unescapeIdentifier(v);
}
/**
* Parsed numeric value (number / percentage / dimension tokens). Derived from
* `value` on access — the `%` is dropped for percentages and the unit for
* dimensions (split with `_consumeANumber`, recomputed here so nothing is
* stored per token).
* @returns {number} the parsed numeric value
*/
get numericValue() {
const v = this.value;
if (this.type === T_DIMENSION) {
return Number(v.slice(0, _consumeANumber(v, 0)));
}
if (this.type === T_PERCENTAGE) return Number(v.slice(0, -1));
return Number(v);
}
/**
* Spec type flag. For number / dimension tokens it's "integer" / "number"
* (derived from `value`); for hash tokens it's "id" / "unrestricted" (re-derived
* from the source). The two senses share the name in the spec; both are computed
* on read so every leaf token keeps the same object shape (no own `typeFlag`).
* @returns {"integer" | "number" | "id" | "unrestricted"} the spec type flag
*/
get typeFlag() {
if (this.type === T_HASH) {
// Re-derive id-ness from the source (whether the name after `#` starts an
// ident sequence) rather than storing an `_isId` slot — keeps hash tokens
// the same shape as every other leaf token. Matches the lexer's
// `consumeNumberSign`, which sets `isId` from the same check.
const input = this._locConverter._input;
const p = this.start + 1;
return _ifThreeCodePointsWouldStartAnIdentSequence(
input,
p,
input.charCodeAt(p),
input.charCodeAt(p + 1),
input.charCodeAt(p + 2)
)
? "id"
: "unrestricted";
}
const v = this.value;
return _typeFlagOf(
this.type === T_DIMENSION ? v.slice(0, _consumeANumber(v, 0)) : v
);
}
/**
* @returns {"+" | "-" | ""} the spec sign (number / percentage / dimension tokens)
*/
get sign() {
return _signOf(this.value);
}
/**
* @returns {string} the unit, lower-cased per spec (dimension tokens)
*/
get unit() {
const v = this.value;
return v.slice(_consumeANumber(v, 0)).toLowerCase();
}
}
/**
* The non-leaf `Node` subclass: functions, simple blocks, declarations, at-rules
* and qualified rules. It declares the union of every container field up front so
* all five share **one** hidden class — the consume algorithms only overwrite the
* slots relevant to their type, never adding a property, so the shape never
* transitions. This caps the shape count: the walker's hot `.type` / `.value` /
* `.prelude` loads otherwise see eight distinct node maps (five container types
* plus the token / hash / url token maps), tipping V8's inline cache into the
* slow megamorphic path; folding the five containers into one leaves four maps
* (token, hash, url, container) — within the polymorphic limit, so those loads
* stay inline-cached. Unused-for-the-type slots keep their defaults (the field
* typedefs below document which fields each type uses). The stylesheet node is
* rare (one per parse) and stays a bare `Node`.
*/
class Container extends Node {
/**
* @param {number} type node type
* @param {number} start byte offset of the node's first code point
* @param {number} end byte offset just past the node's last code point
* @param {LocConverter} locConverter shared loc converter
*/
constructor(type, start, end, locConverter) {
super(type, start, end, locConverter);
/** @type {string} name (function / at-rule / declaration) */
this.name = "";
/** @type {number} */
this.nameStart = start;
/** @type {number} */
this.nameEnd = start;
/** @type {ComponentValue[] | null} component values (function / block / declaration) */
this.value = null;
/** @type {ComponentValue[] | null} prelude (at-rule / qualified rule) */
this.prelude = null;
/** @type {Declaration[] | null} */
this.declarations = null;
/** @type {Rule[] | null} */
this.childRules = null;
/** @type {number} `{` start offset, or -1 (at-rule / qualified rule) */
this.blockStart = -1;
/** @type {number} `}` end offset, or -1 (at-rule / qualified rule) */
this.blockEnd = -1;
/** @type {boolean} stripped `!important` (declaration) */
this.important = false;
/** @type {SimpleBlockToken | undefined} opening char (simple block) */
this.token = undefined;
}
}
/**
* Number token (`123`, `-1.5`, `+2e3`). `value` is the raw source slice (the spec's "value"); `numericValue` / `typeFlag` / `sign` are lazy getters derived from it (see `Token`).
* @typedef {Token & { numericValue: number, typeFlag: "integer" | "number", sign: "+" | "-" | "" }} NumberToken
*/
/**
* Percentage token (`50%`). `value` is the raw slice including `%`; `numericValue` (without `%`) and `sign` are lazy getters.
* @typedef {Token & { numericValue: number, sign: "+" | "-" | "" }} PercentageToken
*/
/**
* Dimension token (`100px`, `1.5em`). `value` is the raw slice (number + unit); `numericValue` / `typeFlag` / `sign` (of the numeric part) and `unit` (lower-cased) are lazy getters.
* @typedef {Token & { numericValue: number, typeFlag: "integer" | "number", sign: "+" | "-" | "", unit: string }} DimensionToken
*/
// Spec "Assert: …" preconditions are comments only (callers satisfy them); a future `strict` option could reinstate them as throws.
/**
* Hash token (`#foo`). `value` is the name without the leading `#`; `typeFlag` is the spec type flag ("id" when the name forms a valid `<id>` selector, "unrestricted" otherwise).
* @typedef {Token & { typeFlag: "id" | "unrestricted" }} HashToken
*/
/**
* Old-style unquoted URL token (`url(unquoted)`). `value` is the unquoted body;
* `contentStart` / `contentEnd` mark the inner content range in the source.
* @typedef {Token & { contentStart: number, contentEnd: number }} UrlToken
*/
/**
* Function node: `name(component-values...)`. `name` is the raw source slice
* before the `(` (callers lowercase / unescape as needed); `nameStart` / `nameEnd`
* are its `[start, end)` byte offsets; `value` is the component values inside the parentheses.
* @typedef {Node & { name: string, nameStart: number, nameEnd: number, value: ComponentValue[] }} FunctionNode
*/
/** @typedef {"[" | "(" | "{"} SimpleBlockToken */
/**
* Simple block (`[...]`, `(...)` not preceded by an ident, `{...}`). `token` is
* the opening character. `value` is the component values inside. This shape is
* produced by `consumeASimpleBlock` (§5.4.9) and appears in preludes.
*
* Note: `consumeABlock` (§5.4.4) returns the parsed block's separate `decls` /
* `rules` lists (per §5.4.5), not a SimpleBlock wrapper — see
* `AtRule` / `QualifiedRule`'s `declarations` and `childRules` fields.
* @typedef {Node & { token: SimpleBlockToken, value: ComponentValue[] }} SimpleBlock
*/
/**
* A CSS component value (CSS Syntax §5.4.8): a preserved token, a function, or
* a simple block (`Token` also covers `HashToken` / `UrlToken`).
* @typedef {Token | FunctionNode | SimpleBlock} ComponentValue
*/
/**
* A CSS rule — an at-rule or a qualified rule.
* @typedef {AtRule | QualifiedRule} Rule
*/
/**
* Declaration: `name: value [!important][;]`. `name` is the raw property-name
* slice; `value` is the trimmed component-value list (whitespace stripped from
* both ends); `important` records a stripped `!important`.
* @typedef {Node & { name: string, nameStart: number, nameEnd: number, value: ComponentValue[], important: boolean }} Declaration
*/
/**
* At-rule: `@name <prelude> ;` or `@name <prelude> { ... }`. `name` is the
* at-keyword without the leading `@`; `prelude` is the component values up to
* the at-rule's `;` / block / enclosing `}`. Per §5.4.2 the block is consumed
* into separate `declarations` (a `Declaration[]`) and `childRules` (a `Rule[]`,
* each an at-rule or qualified rule); both are `null` for a `;`-terminated
* at-rule. `blockStart` / `blockEnd` are the `{` start / `}` end offsets
* (webpack extension, not in spec; the spec doesn't track brace positions), or
* `-1` / `-1` when there is no block. `range[1]` points past `}` for a block, or
* at the `;` / `}` / EOF position otherwise (callers check the byte at `range[1]`
* to tell them apart).
* @typedef {Node & { name: string, nameStart: number, nameEnd: number, prelude: ComponentValue[], declarations: Declaration[] | null, childRules: Rule[] | null, blockStart: number, blockEnd: number }} AtRule
*/
/**
* Qualified rule: `<prelude> { <block> }`. `prelude` is the component values
* before the `{` (selectors, keyframe parameters, …); `declarations` and
* `childRules` are the parsed `{ ... }` body (split per tabatkins/parse-css.js
* reference impl), or both `null` when EOF was hit before `{`. `blockStart` /
* `blockEnd` are the `{` start / `}` end offsets (webpack extension), or `-1` /
* `-1` when there is no block.
* @typedef {Node & { prelude: ComponentValue[], declarations: Declaration[] | null, childRules: Rule[] | null, blockStart: number, blockEnd: number }} QualifiedRule
*/
/**
* Stylesheet (CSS Syntax §5.3.4): the result of `parseAStylesheet`. `rules`
* holds the top-level at-rules / qualified rules (top-level declarations are
* parse errors and never produced).
* @typedef {Node & { rules: Rule[] }} Stylesheet
*/
// Lexer-token-type → AST-node-type map. A single \`new Token\` construct site
// (vs a ~20-case switch with a \`new Token\` in each arm) keeps V8 on the fast
// monomorphic allocation path — the switch form showed up as generic construct
// stubs in profiles. URL is the one type with extra own state, handled first.
const _ttToNodeType = new Uint8Array(27);
_ttToNodeType[TT_WHITESPACE] = T_WHITESPACE;
_ttToNodeType[TT_IDENTIFIER] = T_IDENT;
_ttToNodeType[TT_STRING] = T_STRING;
_ttToNodeType[TT_DELIM] = T_DELIM;
_ttToNodeType[TT_NUMBER] = T_NUMBER;
_ttToNodeType[TT_PERCENTAGE] = T_PERCENTAGE;
_ttToNodeType[TT_DIMENSION] = T_DIMENSION;
_ttToNodeType[TT_HASH] = T_HASH;
_ttToNodeType[TT_AT_KEYWORD] = T_AT_KEYWORD;
_ttToNodeType[TT_BAD_STRING_TOKEN] = T_BAD_STRING;
_ttToNodeType[TT_BAD_URL_TOKEN] = T_BAD_URL;
_ttToNodeType[TT_COLON] = T_COLON;
_ttToNodeType[TT_COMMA] = T_COMMA;
_ttToNodeType[TT_SEMICOLON] = T_SEMICOLON;
_ttToNodeType[TT_RIGHT_PARENTHESIS] = T_RIGHT_PARENTHESIS;
_ttToNodeType[TT_RIGHT_SQUARE_BRACKET] = T_RIGHT_SQUARE_BRACKET;
_ttToNodeType[TT_RIGHT_CURLY_BRACKET] = T_RIGHT_CURLY_BRACKET;
_ttToNodeType[TT_CDO] = T_CDO;
_ttToNodeType[TT_CDC] = T_CDC;
// === AST construction backend ===
// The consume algorithms build nodes through these module-level primitives
// rather than `new Token` / `new Container` directly, so the node
// representation can be swapped under the parser. The object backend below
// builds the retainable `Node` / `Token` / `Container` tree the `parseA*`
// entry points return; the Struct-of-Arrays backend (added separately) writes
// the same nodes into reused typed arrays for the streaming `grammar`, where
// per-node allocation dominates cost. Child lists are plain arrays in both
// backends (a node ref is an object or an integer index); only node creation /
// field access differs, so only those ops are swapped.
// Current loc converter for the active parse (object backend reads it).
let _objLocConverter = /** @type {LocConverter} */ (
/** @type {unknown} */ (null)
);
// Active skip state (from `CssProcessOptions.skip`), applied by the grammar.
// `_skipTypes` is indexed by `NodeType` (1 = skip): drop that component-value
// leaf / container from declaration value and function-arg lists. The two
// prelude flags scan a rule's prelude without materializing its tree (url tokens
// / functions kept, so `url()` in a selector or `@import url(…)` still resolves).
// A skipped node is still tokenized (positions stay correct) but never pushed,
// so it is never walked or read — the caller must only skip what nothing reads.
// `useObjectBackend` restores the defaults so `parseA*` build the full tree.
const _NO_SKIP_TYPES = new Uint8Array(32);
// Shared frozen empty list for block bodies with no decls / no child rules (the
// common case — most rules carry only declarations). Every consumer reads these
// lists read-only and null-guards, so one immutable instance replaces ~one empty
// array allocation per rule; frozen so any errant push fails loud.
const _EMPTY_LIST = /** @type {Rule[]} */ (
/** @type {unknown} */ (Object.freeze([]))
);
/** @type {Uint8Array} */
let _skipTypes = _NO_SKIP_TYPES;
// Fast-path flag: true only when a real skip set is active, so the (dominant)
// no-skip parses pay one boolean test instead of a node-type lookup per value.
let _skipActive = false;
let _skipSelectorPrelude = false;
let _skipAtRulePrelude = false;
/** @type {(type: number, start: number, end: number) => Node} */
let _makeLeaf;
/** @type {(start: number, end: number, contentStart: number, contentEnd: number) => Node} */
let _makeUrl;
/** @type {(type: number, start: number, end: number) => Node} */
let _makeContainer;
/** @type {(start: number) => Node} */
let _makeStylesheet;
// Offsets only — the object backend derives the name string itself (an
// at-rule's name skips its `@`), so the SoA backend never pays for a slice
// it would immediately discard (it re-derives names from offsets on read).
/** @type {(r: Node, nameStart: number, nameEnd: number) => void} */
let _setName;
/** @type {(r: Node, v: number) => void} */
let _setEnd;
/** @type {(r: Node, blockStart: number, blockEnd: number) => void} */
let _setBlock;
/** @type {(r: Node) => void} */
let _setImportant;
/** @type {(r: Node, ch: SimpleBlockToken) => void} */
let _setToken;
/** @type {(r: Node, list: Node[]) => void} */
let _setValue;
/** @type {(r: Node, list: Node[]) => void} */
let _setPrelude;
/** @type {(r: Node, decls: Node[], childRules: Node[]) => void} */
let _setBody;
/** @type {(r: Node, list: Node[]) => void} */
let _setRules;
/** @type {(r: Node) => number} */
let _nodeTypeOf;
/** @type {(r: Node) => number} */
let _nodeStartOf;
/** @type {(r: Node) => string} */
let _nodeValueOf;
/** @type {(r: Node) => SimpleBlockToken} */
let _nodeTokenOf;
// Child lists are plain arrays in every backend; refs are pushed by value.
// Content-list allocation: the object backend uses a per-site `[]` literal
// (each site keeps its own V8 allocation site — a shared helper collapses them
// and defeats escape analysis / pretenuring), the SoA backend recycles scratch
// arrays through a pool — a sealed list is copied into the flat value buffer
// and its array returned to the pool by `_soaSetValue`, so the streaming parse
// allocates no per-list array. An abandoned (never-sealed) list simply falls
// out of the pool. `_soaActive` gates the pool at each site.
let _soaActive = false;
/** @type {Node[][]} */
const _listPool = [];
const _soaList = () =>
_listPool.length > 0
? /** @type {Node[]} */ (_listPool.pop())
: /** @type {Node[]} */ ([]);
// -- object backend: builds the retainable class-instance tree --
/** @type {typeof _makeLeaf} */
const _objLeaf = (type, start, end) =>
new Token(type, start, end, _objLocConverter);
/** @type {typeof _makeUrl} */
const _objUrl = (start, end, cs, ce) => {
const u = /** @type {UrlToken} */ (
new Token(T_URL, start, end, _objLocConverter)
);
u.contentStart = cs;
u.contentEnd = ce;
return u;
};
/** @type {typeof _makeContainer} */
const _objContainer = (type, start, end) =>
new Container(type, start, end, _objLocConverter);
/** @type {typeof _makeStylesheet} */
const _objStylesheet = (start) => {
const s = /** @type {Stylesheet} */ (
new Node(T_STYLESHEET, start, start, _objLocConverter)
);
s.rules = [];
return s;
};
// The backend function sets are module-level constants (not closures rebuilt
// per parse): the dispatch slots keep one function identity forever, so the
// per-node call sites in the consume algorithms stay monomorphic instead of
// seeing a fresh closure per parse. They capture only module-level state.
/** @type {typeof _setName} */
const _objSetName = (r, ns, ne) => {
const c = /** @type {Container} */ (r);
// An at-rule's `nameStart` points at its `@`, which the name excludes.
c.name = _objLocConverter._input.slice(
r.type === T_AT_RULE ? ns + 1 : ns,
ne
);
c.nameStart = ns;
c.nameEnd = ne;
};
/** @type {typeof _setEnd} */
const _objSetEnd = (r, v) => {
r.end = v;
};
/** @type {typeof _setBlock} */
const _objSetBlock = (r, bs, be) => {
const c = /** @type {Container} */ (r);
c.blockStart = bs;
c.blockEnd = be;
};
/** @type {typeof _setImportant} */
const _objSetImportant = (r) => {
/** @type {Container} */ (r).important = true;
};
/** @type {typeof _setToken} */
const _objSetToken = (r, ch) => {
/** @type {Container} */ (r).token = ch;
};
/** @type {typeof _setValue} */
const _objSetValue = (r, list) => {
/** @type {Container} */ (r).value = /** @type {ComponentValue[]} */ (list);
};
/** @type {typeof _setPrelude} */
const _objSetPrelude = (r, list) => {
/** @type {Container} */ (r).prelude = /** @type {ComponentValue[]} */ (list);
};
/** @type {typeof _setBody} */
const _objSetBody = (r, decls, childRules) => {
const c = /** @type {Container} */ (r);
c.declarations = /** @type {Declaration[]} */ (decls);
c.childRules = /** @type {Rule[]} */ (childRules);
};
/** @type {typeof _setRules} */
const _objSetRules = (r, list) => {
/** @type {Stylesheet} */ (r).rules = /** @type {Rule[]} */ (list);
};
/** @type {typeof _nodeTypeOf} */
const _objNodeTypeOf = (r) => r.type;
/** @type {typeof _nodeStartOf} */
const _objNodeStartOf = (r) => r.start;
/** @type {typeof _nodeValueOf} */
const _objNodeValueOf = (r) => /** @type {Token} */ (r).value;
/** @type {typeof _nodeTokenOf} */
const _objNodeTokenOf = (r) =>
/** @type {SimpleBlockToken} */ (/** @type {Container} */ (r).token);
/** @param {LocConverter} lc loc converter for this parse */
const useObjectBackend = (lc) => {
_objLocConverter = lc;
_soaActive = false;
// `parseA*` return the full tree, so nothing is skipped.
_skipTypes = _NO_SKIP_TYPES;
_skipActive = false;
_skipSelectorPrelude = false;
_skipAtRulePrelude = false;
_makeLeaf = _objLeaf;
_makeUrl = _objUrl;
_makeContainer = _objContainer;
_makeStylesheet = _objStylesheet;
_setName = _objSetName;
_setEnd = _objSetEnd;
_setBlock = _objSetBlock;
_setImportant = _objSetImportant;
_setToken = _objSetToken;
_setValue = _objSetValue;
_setPrelude = _objSetPrelude;
_setBody = _objSetBody;
_setRules = _objSetRules;
_nodeTypeOf = _objNodeTypeOf;
_nodeStartOf = _objNodeStartOf;
_nodeValueOf = _objNodeValueOf;
_nodeTokenOf = _objNodeTokenOf;
};
// -- struct-of-arrays backend: writes nodes into reused typed arrays --
// A node ref is its integer id; fields live in parallel arrays indexed by id.
// Three reused int slots (`_soaAux0/1/2`) plus a flags byte carry the per-type
// extras; child lists hang off three object arrays. Aux slot meaning by type:
// url: aux0 contentStart, aux1 contentEnd
// function: aux0 nameEnd
// declaration: aux0 nameEnd, flags bit0 important
// at-rule: aux0 nameEnd, aux1 blockStart, aux2 blockEnd
// qualified: aux1 blockStart, aux2 blockEnd
// `name` / `nameStart` / a simple block's `token` are derived from the source
// on read (see the SoA accessors), so they need no slot. A node's main content
// (value | prelude | stylesheet rules) is a `_soaFlat` span (see below).
// `grammar` resets `_soaNodeCount` to 0 after each top-level rule's walk, so the
// buffers are reused across rules and the parse allocates almost nothing.
let _soaCapacity = 0;
let _soaNodeCount = 0;
let _soaTypes = new Uint8Array(0);
let _soaStarts = new Int32Array(0);
let _soaEnds = new Int32Array(0);
let _soaAux0 = new Int32Array(0);
let _soaAux1 = new Int32Array(0);
let _soaAux2 = new Int32Array(0);
let _soaFlags = new Uint8Array(0);
// Content-list spans: a container's value / prelude is `_soaFlat[start, start+len)`
// (node refs), recycled per top-level rule like the node columns.
let _soaListStarts = new Int32Array(0);
let _soaListLens = new Int32Array(0);
let _soaFlat = new Int32Array(0);
let _soaFlatTop = 0;
// Peak usage of the current parse, and use-once regrow hints: after an
// over-capacity shrink the next grow jumps straight back to the previous
// parse's peak (one exact-fit allocation instead of re-doubling up).
let _soaPeak = 0;
let _soaFlatPeak = 0;
let _soaGrowHint = 0;
let _soaFlatGrowHint = 0;
/** @param {number} need minimum flat-buffer capacity */
const _soaFlatGrow = (need) => {
let cap = _soaFlat.length || 4096;
if (_soaFlatGrowHint > cap) cap = _soaFlatGrowHint;
_soaFlatGrowHint = 0;
while (cap < need) cap *= 2;
const next = new Int32Array(cap);
next.set(_soaFlat);
_soaFlat = next;
};
/** @type {(Node[] | null)[]} */
const _soaDeclarationLists = [];
/** @type {(Node[] | null)[]} */
const _soaChildRuleLists = [];
let _soaInput = "";
let _soaLocConverter = /** @type {LocConverter} */ (
/** @type {unknown} */ (null)
);
// Node refs are integers here but typed `Node` across the parser; these are
// identity casts that just satisfy the type system at the boundary.
/** @type {(n: Node) => number} */
const _nodeIndex = (n) => /** @type {number} */ (/** @type {unknown} */ (n));
/** @type {(i: number) => Node} */
const _nodeRef = (i) => /** @type {Node} */ (/** @type {unknown} */ (i));
/** @param {number} need minimum capacity */
const _soaGrow = (need) => {
let cap = _soaCapacity || 4096;
if (_soaGrowHint > cap) cap = _soaGrowHint;
_soaGrowHint = 0;
while (cap < need) cap *= 2;
const ty = new Uint8Array(cap);
ty.set(_soaTypes);
_soaTypes = ty;
const st = new Int32Array(cap);
st.set(_soaStarts);
_soaStarts = st;
const en = new Int32Array(cap);
en.set(_soaEnds);
_soaEnds = en;
const a0 = new Int32Array(cap);
a0.set(_soaAux0);
_soaAux0 = a0;
const a1 = new Int32Array(cap);
a1.set(_soaAux1);
_soaAux1 = a1;
const a2 = new Int32Array(cap);
a2.set(_soaAux2);
_soaAux2 = a2;
const fl = new Uint8Array(cap);
fl.set(_soaFlags);
_soaFlags = fl;
const ls = new Int32Array(cap);
ls.set(_soaListStarts);
_soaListStarts = ls;
const ll = new Int32Array(cap);
ll.set(_soaListLens);
_soaListLens = ll;
_soaCapacity = cap;
};
/** @type {(type: number, start: number, end: number) => Node} */
const _soaAllocNode = (type, start, end) => {
// Ids are 1-based: a node ref is used in truthiness checks (`if (!parent)`),
// so 0 must stay reserved for "no node".
// Leaves never read the flag / list slots — `_soaAllocContainer` clears
// them instead, keeping the dominant leaf allocation at three writes.
const i = _soaNodeCount + 1;
if (i >= _soaCapacity) _soaGrow(i + 1);
_soaTypes[i] = type;
_soaStarts[i] = start;
_soaEnds[i] = end;
_soaNodeCount = i;
return _nodeRef(i);
};
/** @type {(type: number, start: number, end: number) => Node} */
const _soaAllocContainer = (type, start, end) => {
const r = _soaAllocNode(type, start, end);
const i = _nodeIndex(r);
_soaFlags[i] = 0;
// Clear list slots so a reused id never exposes a previous node's children
// (content lists are flat spans, so zeroing the length suffices).
_soaListLens[i] = 0;
_soaDeclarationLists[i] = null;
_soaChildRuleLists[i] = null;
return r;
};
// Raw token value (the lazy `Token.value` form): hash / at-keyword drop their
// one-char prefix, url uses its content range. Shared by the parser's
// mid-parse reads and the SoA accessor.
/**
* @param {number} i node id
* @returns {string} raw token value
*/
const _soaValueOf = (i) => {
const ty = _soaTypes[i];
if (ty === T_HASH || ty === T_AT_KEYWORD) {
return _soaInput.slice(_soaStarts[i] + 1, _soaEnds[i]);
}
if (ty === T_URL) return _soaInput.slice(_soaAux0[i], _soaAux1[i]);
return _soaInput.slice(_soaStarts[i], _soaEnds[i]);
};
// Module-level constants for the same reason as the `_objSet*` set above.
/** @type {typeof _makeUrl} */
const _soaMakeUrl = (start, end, cs, ce) => {
const r = _soaAllocNode(T_URL, start, end);
_soaAux0[_nodeIndex(r)] = cs;
_soaAux1[_nodeIndex(r)] = ce;
return r;
};
/** @type {typeof _makeStylesheet} */
const _soaMakeStylesheet = (start) =>
_soaAllocContainer(T_STYLESHEET, start, start);
// name / nameStart are derived from start + nameEnd; only nameEnd is stored.
/** @type {typeof _setName} */
const _soaSetName = (r, ns, ne) => {
_soaAux0[_nodeIndex(r)] = ne;
};
/** @type {typeof _setEnd} */
const _soaSetEnd = (r, v) => {
_soaEnds[_nodeIndex(r)] = v;
};
/** @type {typeof _setBlock} */
const _soaSetBlock = (r, bs, be) => {
const i = _nodeIndex(r);
_soaAux1[i] = bs;
_soaAux2[i] = be;
};
/** @type {typeof _setImportant} */
const _soaSetImportant = (r) => {
_soaFlags[_nodeIndex(r)] |= 1;
};
// A simple block's token is derived from its opening char on read.
/** @type {typeof _setToken} */
const _soaSetToken = (r, ch) => {};
/** @type {typeof _setValue} */
const _soaSetValue = (r, list) => {
// Seal the finished list: copy its refs into the flat buffer and hand the
// scratch array back to the pool. The caller never touches `list` again.
const i = _nodeIndex(r);
const len = list.length;
const start = _soaFlatTop;
if (start + len > _soaFlat.length) _soaFlatGrow(start + len);
for (let k = 0; k < len; k++) {
_soaFlat[start + k] = _nodeIndex(list[k]);
}
_soaFlatTop = start + len;
_soaListStarts[i] = start;
_soaListLens[i] = len;
list.length = 0;
_listPool.push(list);
};
/** @type {typeof _setBody} */
const _soaSetBody = (r, decls, childRules) => {
const i = _nodeIndex(r);
_soaDeclarationLists[i] = decls;
_soaChildRuleLists[i] = childRules;
};
/** @type {typeof _nodeTypeOf} */
const _soaNodeTypeOf = (r) => _soaTypes[_nodeIndex(r)];
/** @type {typeof _nodeStartOf} */
const _soaNodeStartOf = (r) => _soaStarts[_nodeIndex(r)];
/** @type {typeof _nodeValueOf} */
const _soaNodeValueOf = (r) => _soaValueOf(_nodeIndex(r));
/** @type {typeof _nodeTokenOf} */
const _soaNodeTokenOf = (r) =>
/** @type {SimpleBlockToken} */ (_soaInput[_soaStarts[_nodeIndex(r)]]);
/**
* @param {string} input source
* @param {LocConverter} lc loc converter
*/
const useSoaBackend = (input, lc) => {
_soaInput = input;
_soaLocConverter = lc;
_soaNodeCount = 0;
_soaFlatTop = 0;
_soaActive = true;
// The alloc primitives are the slot functions directly — no wrapper hop.
_makeLeaf = _soaAllocNode;
_makeUrl = _soaMakeUrl;
_makeContainer = _soaAllocContainer;
_makeStylesheet = _soaMakeStylesheet;
_setName = _soaSetName;
_setEnd = _soaSetEnd;
_setBlock = _soaSetBlock;
_setImportant = _soaSetImportant;
_setToken = _soaSetToken;
_setValue = _soaSetValue;
// value / prelude / rules all land in the one content-list slot.
_setPrelude = _soaSetValue;
_setBody = _soaSetBody;
_setRules = _soaSetValue;
_nodeTypeOf = _soaNodeTypeOf;
_nodeStartOf = _soaNodeStartOf;
_nodeValueOf = _soaNodeValueOf;
_nodeTokenOf = _soaNodeTokenOf;
};
/**
* Materialize a single non-block, non-function lexer token as its leaf AST node — the spec's "consume a token" result (§5.4.8 "anything else"), preserving stray closers / CDO / CDC.
* @param {MutableToken} t token from the lexer
* @returns {Node} the leaf token node
*/
const tokenToNode = (t) => {
const tt = t.type;
// URL is the only leaf with own state (its content range); all others are a
// plain leaf whose node type comes from the map.
if (tt === TT_URL) {
const ut = /** @type {CssUrlToken} */ (t);
return _makeUrl(t.start, t.end, ut.contentStart, ut.contentEnd);
}
return _makeLeaf(_ttToNodeType[tt], t.start, t.end);
};
/**
* Position-based view over the lexer — webpack's stand-in for the spec's
* "normalize into a token stream" (CSS Syntax §9). It unifies the lexer and the
* stream in one class: the `readToken` primitive lexes one token (the CSS
* tokenizer), and the spec token-stream operations `next` / `consume` /
* `discard` / `mark` / `restoreMark` / `discardMark` drive it from a byte
* cursor. `parse*` entry points wrap a source string in one of these and every
* `consume*` algorithm reads tokens from it.
*
* No token buffer is kept: the cursor is a byte offset and the only state is
* the next token (lazily tokenized once and cached until consumed). The
* declaration-vs-qualified-rule backtracking in `consumeABlocksContents`
* rewinds by `mark`ing / `restoreMark`ing that byte offset, which simply
* re-tokenizes the rewound span — comment tokens are filtered here and fire
* `onComment` once each, tracked by a monotonic high-water mark so a
* re-tokenized span never re-fires them.
*
* `SourceProcessor` is handed this class (not an instance) and threads it to
* the grammar, so a different language can drive the same visitor machinery by
* swapping the tokenizer — the per-token `readToken` primitive — for its own.
*/
class TokenStream {
/**
* @param {string} input source
* @param {number=} pos start byte offset (default `0`)
* @param {LocConverter=} locConverter shared loc converter (default a fresh one over `input`)
* @param {((input: string, start: number, end: number) => number)=} onComment comment-token callback
*/
constructor(
input,
pos = 0,
locConverter = new LocConverter(input),
onComment = undefined
) {
/** @type {string} */
this.input = input;
/** @type {LocConverter} */
this.locConverter = locConverter;
this._onComment = onComment;
// Byte offset where the next token is tokenized from.
/** @type {number} */
this._pos = pos;
// Comments before this offset have already fired `onComment`; a
// re-tokenized (backtracked) span never re-fires them.
/** @type {number} */
this._commentHigh = pos;
// Single reused token the lexer writes into on the `next` path — see
// `MutableToken`. `_hasNext` marks it cached — a boolean instead of an
// object slot, so caching a token never pays a GC write barrier.
/** @type {MutableToken} */
this._tok = createToken();
/** @type {boolean} whether `_tok` holds the (lazily tokenized) next token */
this._hasNext = false;
/** @type {number[]} byte offsets to rewind to */
this._marks = [];
}
/**
* The next token (CSS Syntax §3 "next token") — the upcoming token without
* consuming it; the `<eof-token>` once the source is exhausted. This is the
* token the consume algorithms dispatch on (the spec's "process"). Tokenized
* from `_pos` on first use and cached until consumed; comment tokens are
* skipped here, firing `onComment` once each.
* @returns {MutableToken} the next token
*/
next() {
if (!this._hasNext) {
const input = this.input;
const tok = this._tok;
let pos = this._pos;
for (;;) {
const t = readToken(input, pos, tok);
if (t === undefined) {
fill(tok, TT_EOF, input.length, input.length);
break;
}
if (t.type === TT_COMMENT) {
if (t.start >= this._commentHigh) {
if (this._onComment) this._onComment(input, t.start, t.end);
this._commentHigh = t.end;
}
pos = t.end;
continue;
}
break;
}
this._hasNext = true;
}
return this._tok;
}
/**
* Consume a token (CSS Syntax §3 "consume a token") — return the next token
* and advance the cursor past it. The returned token is valid until the next
* `next` re-tokenizes (the reused instance is not cleared by advancing).
* @returns {MutableToken} the consumed token
*/
consume() {
const t = this.next();
if (t.type !== TT_EOF) {
this._pos = t.end;
this._hasNext = false;
}
return t;
}
/**
* Discard a token (CSS Syntax §3 "discard a token") — advance the cursor past
* the next token without returning it.
* @returns {void}
*/
discard() {
const t = this.next();
if (t.type !== TT_EOF) {
this._pos = t.end;
this._hasNext = false;
}
}
/**
* Mark (CSS Syntax §3 "mark") — push the current cursor position.
* @returns {void}
*/
mark() {
this._marks.push(this._pos);
}
/**
* Restore a mark (CSS Syntax §3 "restore a mark") — pop the last mark and
* rewind the cursor to it. The rewound span is re-tokenized on the next read;
* already-fired comments are not re-fired (`_commentHigh`).
* @returns {void}
*/
restoreMark() {
this._pos = /** @type {number} */ (this._marks.pop());
this._hasNext = false;
}
/**
* Discard a mark (CSS Syntax §3 "discard a mark") — pop without rewinding.
* @returns {void}
*/
discardMark() {
this._marks.pop();
}
}
/**
* Normalize a `parse*` entry point's first argument into a `TokenStream`
* (CSS Syntax §9 "normalize into a token stream"). An existing `TokenStream`
* is returned as-is (consumed from its current position — it already carries
* the shared `LocConverter` and comment hook), so `pos` / `onComment` are
* ignored. A raw source string is tokenized from `pos` with a fresh
* `LocConverter`; pass a `TokenStream` instead to share one converter across
* sub-parses.
* @param {string | TokenStream} input source string or an existing stream
* @param {number=} pos start byte offset (string input only; default `0`)
* @param {((input: string, start: number, end: number) => number)=} onComment comment callback (string input only)
* @returns {TokenStream} the stream to consume from
*/
const normalizeIntoTokenStream = (input, pos, onComment) =>
input instanceof TokenStream
? input
: new TokenStream(input, pos || 0, new LocConverter(input), onComment);
// === Parser entry points (CSS Syntax Level 3 §5.3) ===
// Each `parseA*` is a thin public wrapper over a `consumeA*` algorithm
// (§5.4): it takes raw source + a start position (webpack's stand-in for
// the spec's "normalize into a token stream") and runs the matching
// consume algorithm. The split mirrors tabatkins/parse-css — `parse*`
// are the documented entry points, `consume*` are the internal
// algorithms that drive the tokenizer.
/**
* @typedef {object} ParseOptions
* @property {((input: string, start: number, end: number) => number)=} comment optional comment-token callback; the public `parse*` entry points use it to build the `TokenStream` so the outer parser's comment tracker still sees magic comments inside the consumed range
*/
/**
* Parse a stylesheet, CSS Syntax Level 3
* [§5.3.4](https://drafts.csswg.org/css-syntax/#parse-stylesheet).
* @param {string | TokenStream} input source string or an existing token stream
* @param {number=} pos start position (string input only)
* @param {ParseOptions=} options optional comment-token callback (string input only)
* @returns {Stylesheet} the parsed stylesheet
*/
const parseAStylesheet = (input, pos = 0, options = {}) => {
// 1. If input is a byte stream for a stylesheet, decode bytes from input, and set input to the result.
// 2. Normalize input, and set input to the result.
const ts = normalizeIntoTokenStream(input, pos, options.comment);
useObjectBackend(ts.locConverter);
// 3. Create a new stylesheet, with its location set to location (or null, if location was not passed).
const start = ts.next().start;
const stylesheet = /** @type {Stylesheet} */ (_makeStylesheet(start));
// 4. Consume a stylesheet's contents from input, and set the stylesheet's rules to the result.
_setRules(stylesheet, consumeAStylesheetsContents(ts));
_setEnd(stylesheet, ts.next().start);
// 5. Return the stylesheet.
return stylesheet;
};
/**
* Parse a stylesheet's contents, CSS Syntax Level 3
* [§5.3.5](https://drafts.csswg.org/css-syntax/#parse-stylesheets-contents) —
* the top-level rule list via `consumeAStylesheetsContents` (§5.4.1): top-level
* declarations are parse errors (never produced) and top-level CDO (`<!--`) /
* CDC (`-->`) tokens are discarded.
* @param {string | TokenStream} input source string or an existing token stream
* @param {number=} pos start position (string input only)
* @param {ParseOptions=} options optional comment-token callback (string input only)
* @returns {Rule[]} top-level rules
*/
const parseAStylesheetsContents = (input, pos = 0, options = {}) => {
// 1. Normalize input, and set input to the result.
const ts = normalizeIntoTokenStream(input, pos, options.comment);
useObjectBackend(ts.locConverter);
// 2. Consume a stylesheets contents from input, and return the result.
return consumeAStylesheetsContents(ts);
};
/**
* Parse a block's contents, CSS Syntax Level 3
* [§5.3.6](https://drafts.csswg.org/css-syntax/#parse-block-contents).
* @param {string | TokenStream} input source string or an existing token stream
* @param {number=} pos start position (string input only; just past the opening `{`, or 0)
* @param {ParseOptions=} options optional comment-token callback (string input only)
* @returns {{ decls: Declaration[], rules: Rule[] }} block decls + rules
*/
const parseABlocksContents = (input, pos = 0, options = {}) => {
// 1. Normalize input, and set input to the result.
const ts = normalizeIntoTokenStream(input, pos, options.comment);
useObjectBackend(ts.locConverter);
// 2. Consume a blocks contents from input, and return the result.
return consumeABlocksContents(ts);
};
/**
* Parse a rule, CSS Syntax Level 3
* [§5.3.7](https://drafts.csswg.org/css-syntax/#parse-rule) — discards leading
* whitespace, consumes one at-rule / qualified rule, and requires only trailing
* whitespace; `undefined` (syntax error) otherwise.
* @param {string | TokenStream} input source string or an existing token stream
* @param {number=} pos start position (string input only)
* @param {ParseOptions=} options optional comment-token callback (string input only)
* @returns {Rule | undefined} the parsed rule
*/
const parseARule = (input, pos = 0, options = {}) => {
// 1. Normalize input, and set input to the result.
const ts = normalizeIntoTokenStream(input, pos, options.comment);
useObjectBackend(ts.locConverter);
// 2. Discard whitespace from input.
while (ts.next().type === TT_WHITESPACE) ts.discard();
// 3. If the next token from input is an <EOF-token>, return a syntax error.
// Otherwise, if the next token from input is an <at-keyword-token>, consume an at-rule from input, and let rule be the return value.
// Otherwise, consume a qualified rule from input and let rule be the return value.
// If nothing or an invalid rule error was returned, return a syntax error.
const head = ts.next();
if (head.type === TT_EOF) return undefined;
const rule =
head.type === TT_AT_KEYWORD
? consumeAnAtRule(ts)
: consumeAQualifiedRule(ts);
if (!rule) return undefined;
// 4. Discard whitespace from input.
while (ts.next().type === TT_WHITESPACE) ts.discard();
// 5. If the next token from input is an <EOF-token>, return rule. Otherwise, return a syntax error.
return ts.next().type === TT_EOF ? rule : undefined;
};
/**
* Parse a declaration, CSS Syntax Level 3
* [§5.3.8](https://drafts.csswg.org/css-syntax/#parse-declaration).
* @param {string | TokenStream} input source string or an existing token stream
* @param {number=} pos start position (string input only)
* @param {ParseOptions=} options optional comment-token callback (string input only)
* @returns {Declaration | undefined} the parsed declaration, or undefined
*/
const parseADeclaration = (input, pos = 0, options = {}) => {
// 1. Normalize input, and set input to the result.
const ts = normalizeIntoTokenStream(input, pos, options.comment);
useObjectBackend(ts.locConverter);
// 2. Discard whitespace from input.
while (ts.next().type === TT_WHITESPACE) ts.discard();
// 3. Consume a declaration from input. If anything was returned, return it. Otherwise, return a syntax error.
return consumeADeclaration(ts);
};
/**
* Parse a component value, CSS Syntax Level 3 [§5.3.9](https://drafts.csswg.org/css-syntax/#parse-component-value) — strict entry point that consumes one value and returns `undefined` if non-whitespace input trails (use `consumeAComponentValue` for "one value, ignore the rest").
* @param {string | TokenStream} input source string or an existing token stream
* @param {number=} pos start position (string input only)
* @param {ParseOptions=} options optional comment-token callback (string input only)
* @returns {ComponentValue | undefined} the parsed component value, or `undefined` on empty / trailing-garbage input
*/
const parseAComponentValue = (input, pos = 0, options = {}) => {
// 1. Normalize input, and set input to the result.
const ts = normalizeIntoTokenStream(input, pos, options.comment);
useObjectBackend(ts.locConverter);
// 2. Discard whitespace from input.
while (ts.next().type === TT_WHITESPACE) ts.discard();
// 3. If input is empty, return a syntax error.
if (ts.next().type === TT_EOF) return undefined;
// 4. Consume a component value from input and let value be the return value.
const result = consumeAComponentValue(ts);
// 5. Discard whitespace from input.
while (ts.next().type === TT_WHITESPACE) ts.discard();
// 6. If input is empty, return value. Otherwise, return a syntax error.
if (ts.next().type === TT_EOF) return result;
return undefined;
};
/**
* Parse a list of component values, CSS Syntax Level 3
* [§5.3.10](https://drafts.csswg.org/css-syntax/#parse-list-of-components).
* @param {string | TokenStream} input source string or an existing token stream
* @param {number=} pos start position (string input only)
* @param {ParseOptions=} options comment callback
* @returns {ComponentValue[]} component values
*/
const parseAListOfComponentValues = (input, pos = 0, options = {}) => {
// 1. Normalize input, and set input to the result.
const ts = normalizeIntoTokenStream(input, pos, options.comment);
useObjectBackend(ts.locConverter);
// 2. Consume a list of component values from input, and return the result.
// (`null` needs `bailOnCurly`, which is not passed here.)
return /** @type {ComponentValue[]} */ (consumeAListOfComponentValues(ts));
};
/**
* Parse a comma-separated list of component values, CSS Syntax Level 3 [§5.3.11](https://drafts.csswg.org/css-syntax/#parse-comma-list) — consumes one `<comma-token>`-stopped group of component values per iteration until EOF.
* @param {string | TokenStream} input source string or an existing token stream
* @param {number=} pos start position (string input only)
* @param {ParseOptions=} options comment callback
* @returns {ComponentValue[][]} comma-separated groups of component values
*/
const parseACommaSeparatedListOfComponentValues = (
input,
pos = 0,
options = {}
) => {
// 1. Normalize input, and set input to the result.
const ts = normalizeIntoTokenStream(input, pos, options.comment);
useObjectBackend(ts.locConverter);
// 2. Let groups be an empty list.
/** @type {ComponentValue[][]} */
const groups = [];
// 3. While input is not empty:
while (ts.next().type !== TT_EOF) {
// 3.1. Consume a list of component values from input, with <comma-token> as the stop token, and append the result to groups.
groups.push(
/** @type {ComponentValue[]} */ (
consumeAListOfComponentValues(ts, TT_COMMA)
)
);
// 3.2 Discard a token from input.
ts.discard();
}
// 4. Return groups.
return groups;
};
// === Parser algorithms (CSS Syntax Level 3 §5.4) ===
// The mutually-recursive consume algorithms the `parse*` entry points drive:
// each reads tokens from a `TokenStream` and reuses `consumeAComponentValue`
// for nested values, mirroring tabatkins/parse-css.
/**
* Consume a stylesheet's contents, CSS Syntax Level 3 [§5.4.1](https://drafts.csswg.org/css-syntax/#consume-stylesheet-contents) — the top-level rule list: whitespace and CDO (`<!--`) / CDC (`-->`) tokens are discarded, an at-keyword starts an at-rule, and anything else starts a qualified rule (so top-level declarations are parse errors and never produced).
*
* `onRule` is a webpack extension to the algorithm's output: when given, each
* consumed rule is handed to it immediately and not collected, so the walker can
* process one top-level rule at a time without materializing the whole
* stylesheet (the returned list is then empty). When omitted the rules are
* collected and returned as the spec specifies.
* @param {TokenStream} ts token stream
* @param {((rule: Rule) => void)=} onRule optional per-rule sink (streaming); rules are not collected when given
* @returns {Rule[]} top-level rules (empty when `onRule` is given)
*/
const consumeAStylesheetsContents = (ts, onRule) => {
// Let rules be an initially empty list of rules.
/** @type {Rule[]} */
const rules = [];
// Process input
for (;;) {
const t = ts.next();
// <whitespace-token> / <CDO-token> / <CDC-token>
// Discard a token from input.
if (t.type === TT_WHITESPACE || t.type === TT_CDO || t.type === TT_CDC) {
ts.discard();
}
// <EOF-token>
// Return rules.
else if (t.type === TT_EOF) {
return rules;
}
// <at-keyword-token>
// Consume an at-rule from input. If anything is returned, append it to rules.
else if (t.type === TT_AT_KEYWORD) {
const at = consumeAnAtRule(ts);
if (at) {
if (onRule) onRule(at);
else rules.push(at);
}
}
// anything else
// Consume a qualified rule from input. If a rule is returned, append it to rules.
else {
const rule = consumeAQualifiedRule(ts);
if (rule) {
if (onRule) onRule(rule);
else rules.push(rule);
}
}
}
};
/**
* Consume an at-rule, CSS Syntax Level 3 [§5.4.2](https://drafts.csswg.org/css-syntax/#consume-at-rule) — the next token must be an <at-keyword-token> (asserted); consumes the prelude up to `;` / `{` / `}` / EOF; `{` consumes the block (§5.4.4) onto `.block`, `;` / EOF is discarded, a top-level `}` (when not `nested`) is appended via `consumeAComponentValue`.
* @param {TokenStream} ts token stream
* @param {boolean=} nested true inside a `{}` block — a top-level `}` ends the at-rule (left for the caller)
* @returns {AtRule | undefined} the parsed at-rule
*/
const consumeAnAtRule = (ts, nested = false) => {
// Assert (spec): the next token is an <at-keyword-token>.
// Consume a token from input, and let rule be a new at-rule with its name set to the returned tokens value, its prelude initially set to an empty list, and no declarations or child rules.
const head = ts.consume();
const rule = /** @type {AtRule} */ (
_makeContainer(T_AT_RULE, head.start, head.end)
);
_setName(rule, head.start, head.end);
// Sealed (`_setPrelude`) at each return — the SoA backend consumes the
// scratch array when sealing, so it must be complete by then.
const prelude = _soaActive ? _soaList() : [];
// declarations / childRules / blockStart (-1) / blockEnd (-1) keep their
// defaults (no block: the `;` / EOF / nested-`}` at-rule forms).
// Like `consumeAQualifiedRule`: skip mode scans the prelude without
// materializing it (url tokens / functions kept so `@import url(…)` still
// resolves); the block boundary is found by scanning, not the prelude nodes.
const skip = _skipAtRulePrelude;
// Process input
for (;;) {
const t = ts.next();
// <semicolon-token>
// <EOF-token>
// Discard a token from input. If rule is valid in the current context, return it; otherwise return nothing.
if (t.type === TT_SEMICOLON || t.type === TT_EOF) {
ts.discard();
_setPrelude(rule, prelude);
_setEnd(rule, t.start);
return rule;
}
// <}-token>
// If nested is true: if rule is valid in the current context, return it; otherwise return nothing.
// Otherwise, consume a token and append the result to rules prelude.
else if (t.type === TT_RIGHT_CURLY_BRACKET) {
if (nested) {
_setPrelude(rule, prelude);
_setEnd(rule, t.start);
return rule;
}
const node = consumeATokenAsNode(ts);
if (!skip) prelude.push(node);
continue;
}
// <{-token>
// Consume a block from input, and assign the result to rule's declarations and child rules.
else if (t.type === TT_LEFT_CURLY_BRACKET) {
_setPrelude(rule, prelude);
const block = consumeABlock(ts);
_setBody(rule, block.decls, block.rules);
_setBlock(rule, block.blockStart, block.blockEnd);
_setEnd(rule, block.blockEnd);
return rule;
}
// anything else
// Consume a component value from input and append the returned value to rules prelude.
const node = consumeAComponentValue(ts, t);
if (!skip) {
prelude.push(node);
} else if (
_nodeTypeOf(node) === T_FUNCTION ||
_nodeTypeOf(node) === T_URL
) {
prelude.push(node);
}
}
};
/**
* Consume a token (CSS Syntax §3 "consume a token"): advance past the next
* token and return it as a leaf AST node. Used directly where the spec says
* "consume a token from input" (e.g. the parse-error branches in §5.4.7 /
* §5.4.2 / §5.4.3), distinct from `consumeAComponentValue` which would recurse
* into a simple block / function.
* @param {TokenStream} ts token stream
* @returns {Token} the consumed token as a leaf node
*/
const consumeATokenAsNode = (ts) => {
const t = ts.consume();
return /** @type {Token} */ (tokenToNode(t));
};
/**
* Consume a qualified rule, CSS Syntax Level 3 [§5.4.3](https://drafts.csswg.org/css-syntax/#consume-qualified-rule) — consumes the prelude (each component value via `consumeAComponentValue`) up to its `{` block; EOF, the optional `stopToken`, or a nested top-level `}` is a parse error returning nothing (the block-less prelude is dropped), while a non-nested top-level `}` is consumed as a parse error and the prelude continues. A returned rule always has a block.
* @param {TokenStream} ts token stream
* @param {number=} stopToken token type that ends the prelude (parse error → nothing)
* @param {boolean=} nested true inside a `{}` block — a top-level `}` ends the rule (left for the caller)
* @returns {QualifiedRule | undefined} parsed qualified rule, or `undefined` on a parse error
*/
const consumeAQualifiedRule = (ts, stopToken, nested = false) => {
const start = ts.next().start;
// Let rule be a new qualified rule with its prelude, declarations, and child rules all initially set to empty lists.
const rule = /** @type {QualifiedRule} */ (
_makeContainer(T_QUALIFIED_RULE, start, start)
);
// Sealed (`_setPrelude`) at the `{` exit — the only path that returns the
// rule; the parse-error exits abandon the scratch unsealed.
const prelude = _soaActive ? _soaList() : [];
// declarations / childRules / blockStart (-1) / blockEnd (-1) keep their
// defaults (no block until a `{` is reached).
// Skip mode leaves `prelude` empty (selector text is recovered from the
// rule's byte range, not its nodes); `first`/`second` still track the first
// two non-whitespace tokens the `--foo: {` disambiguation below needs.
const skip = _skipSelectorPrelude;
let first = /** @type {Node} */ (/** @type {unknown} */ (0));
let second = /** @type {Node} */ (/** @type {unknown} */ (0));
// Process input
for (;;) {
const t = ts.next();
// <EOF-token>
// stop token (if passed)
// This is a parse error. Return nothing.
if (t.type === TT_EOF || t.type === stopToken) {
return undefined;
}
// <}-token>
// This is a parse error. If nested is true, return nothing. Otherwise, consume a token and append the result to rules prelude.
else if (t.type === TT_RIGHT_CURLY_BRACKET) {
if (nested) return undefined;
const node = consumeATokenAsNode(ts);
if (skip) {
if (!first) first = node;
else if (!second) second = node;
} else {
prelude.push(node);
}
continue;
}
// <{-token>
// If the first two non-<whitespace-token> values of rule's prelude are an <ident-token> whose value starts with "--" followed by a <colon-token>, then:
// - If nested is true, consume the remnants of a bad declaration from input, with nested set to true, and return nothing.
// - If nested is false, consume a block from input, and return nothing.
// (This disambiguates custom-property declarations from nested qualified rules — `--foo: { … }` at top level of a block is a declaration, not a rule.)
// Otherwise, consume a block from input, and let child rules be the result.
else if (t.type === TT_LEFT_CURLY_BRACKET) {
if (!skip) {
let firstIdx = 0;
/* istanbul ignore next -- @preserve: leading whitespace is discarded before the rule, so the prelude never starts with it */
while (
firstIdx < prelude.length &&
_nodeTypeOf(prelude[firstIdx]) === T_WHITESPACE
) {
firstIdx++;
}
let secondIdx = firstIdx + 1;
while (
secondIdx < prelude.length &&
_nodeTypeOf(prelude[secondIdx]) === T_WHITESPACE
) {
secondIdx++;
}
first = prelude[firstIdx];
second = prelude[secondIdx];
}
if (
first &&
_nodeTypeOf(first) === T_IDENT &&
// Test the source bytes directly — avoids forcing the lazy `value`
// slice just to check the `--` custom-property prefix.
ts.input.startsWith("--", _nodeStartOf(first)) &&
second &&
_nodeTypeOf(second) === T_COLON
) {
/* istanbul ignore if -- @preserve: when nested, `declarationStartLikely` routes every `--x:` to consumeADeclaration (which accepts custom properties), so this fallthrough is unreachable */
if (nested) {
consumeTheRemnantsOfABadDeclaration(ts, true);
} else {
consumeABlock(ts);
}
return undefined;
}
_setPrelude(rule, prelude);
const block = consumeABlock(ts);
_setBody(rule, block.decls, block.rules);
_setBlock(rule, block.blockStart, block.blockEnd);
_setEnd(rule, block.blockEnd);
return rule;
}
// anything else
// Consume a component value from input and append the result to rules prelude.
const node = consumeAComponentValue(ts, t);
if (skip) {
// Keep only url-bearing nodes (url tokens, or functions that may hold a
// url like `:unknown(url(x))`) so the url visitor still rewrites them;
// other selector tokens have no non-modules consumer, so drop them.
const ty = _nodeTypeOf(node);
if (ty === T_FUNCTION || ty === T_URL) prelude.push(node);
// Track the first two non-whitespace tokens for the disambiguation above.
if (t.type !== TT_WHITESPACE) {
if (!first) first = node;
else if (!second) second = node;
}
} else {
prelude.push(node);
}
}
};
/**
* Consume a block, CSS Syntax Level 3 [§5.4.4](https://drafts.csswg.org/css-syntax/#consume-block) — the next token must be `<{-token>`; discards it, consumes the block's contents (§5.4.5), discards the closing `}`, and returns its `decls` / `rules` pair. We also return the `[start of {, end of }]` offsets so callers can record the block's source position.
* @param {TokenStream} ts token stream
* @returns {{ decls: Declaration[], rules: Rule[], blockStart: number, blockEnd: number }} block decls + rules and the `{` start / `}` end offsets
*/
const consumeABlock = (ts) => {
// Capture the opening `{`'s start before advancing — the stream reuses one
// token instance, so `consumeABlocksContents` below would overwrite it.
const blockStart = ts.next().start;
// Assert (spec): the next token is <{-token>.
// Discard a token from input. Consume a block's contents from input and let result be the result. Discard a token from input.
ts.discard();
const { decls, rules } = consumeABlocksContents(ts);
const close = ts.next();
const end = close.type === TT_RIGHT_CURLY_BRACKET ? close.end : close.start;
ts.discard();
return { decls, rules, blockStart, blockEnd: end };
};
/**
* 2-token lookahead: is the next non-whitespace pair `<ident> <colon>`?
* Peeks raw code points without advancing; comments still fire `onComment` later.
* @param {TokenStream} ts token stream
* @returns {boolean} true if consume-a-declaration's step 1 + step 3 would both succeed on the current input
*/
const declarationStartLikely = (ts) => {
const t = ts.next();
if (t.type !== TT_IDENTIFIER) return false;
const input = ts.input;
const len = input.length;
let pos = t.end;
for (;;) {
if (pos >= len) return false;
const cc = input.charCodeAt(pos);
if (_isWhiteSpace(cc)) {
pos++;
continue;
}
// Skip a `/* … */` comment (the tokenizer filters comments between tokens).
if (cc === CC_SOLIDUS && input.charCodeAt(pos + 1) === CC_ASTERISK) {
pos += 2;
while (
pos < len &&
!(
input.charCodeAt(pos) === CC_ASTERISK &&
input.charCodeAt(pos + 1) === CC_SOLIDUS
)
) {
pos++;
}
pos += 2;
continue;
}
// `:` is always a standalone <colon-token>, so the next significant char
// being `:` is equivalent to the next token being a <colon-token>.
return cc === CC_COLON;
}
};
/**
* Consume a block's contents, CSS Syntax Level 3 [§5.4.5](https://drafts.csswg.org/css-syntax/#consume-block-contents). Per tabatkins/parse-css.js reference impl: returns separate `decls` and `rules` flat lists, both preserved on EOF / `}` (the spec text's "Return rules" single-list model drops trailing decls because there's no implicit flush before EOF / `}`).
*
* `onNode` is the same streaming extension `consumeAStylesheetsContents` exposes:
* when given, each consumed declaration / rule is handed to it immediately (in
* source order) instead of being collected, so the returned lists are empty.
* @param {TokenStream} ts token stream
* @param {((node: Declaration | Rule) => void)=} onNode optional per-node sink (streaming); nodes are not collected when given
* @returns {{ decls: Declaration[], rules: Rule[] }} consumed decls + rules (both empty when `onNode` is given; stops at the enclosing `}` / EOF, left in the stream)
*/
const consumeABlocksContents = (ts, onNode) => {
/** @type {Declaration[]} */
const decls = [];
// Child rules are the common empty case (most rules carry only declarations),
// so `rules` is allocated lazily and returned as the shared frozen
// `_EMPTY_LIST` when nothing was appended — one fewer array per rule. `decls`
// stays eager so the hot declaration append keeps a branch-free `push`.
/** @type {Rule[] | null} */
let rules = null;
// Process input:
for (;;) {
const t = ts.next();
// <whitespace-token> / <semicolon-token>
// Discard a token from input.
if (t.type === TT_WHITESPACE || t.type === TT_SEMICOLON) {
ts.discard();
}
// <EOF-token> / <}-token>
// Return decls and rules.
else if (t.type === TT_EOF || t.type === TT_RIGHT_CURLY_BRACKET) {
return { decls, rules: rules || _EMPTY_LIST };
}
// <at-keyword-token>
// Consume an at-rule from input, with nested set to true. If a rule was returned, append it to rules.
else if (t.type === TT_AT_KEYWORD) {
const atRule = consumeAnAtRule(ts, true);
if (atRule) {
if (onNode) onNode(atRule);
else (rules || (rules = [])).push(atRule);
}
}
// anything else
// Mark input. Consume a declaration from input, with nested set to true.
// If a declaration was returned, append it to decls, and discard a mark from input.
// Otherwise, restore a mark from input, then consume a qualified rule from input, with nested set to true, and <semicolon-token> as the stop token. If a rule was returned, append it to rules.
else {
// 2-token peek: consume-a-declaration's steps 1 / 3 require `<ident> <colon>`; if absent it would call consume-the-remnants-of-a-bad-declaration (potentially the rest of the enclosing block) only for the restoreMark to undo it (O(N²) on flat blocks of qualified rules). Skip straight to consume-a-qualified-rule — same observable result.
if (declarationStartLikely(ts)) {
ts.mark();
const decl = consumeADeclaration(ts, true);
if (decl) {
if (onNode) onNode(decl);
else decls.push(decl);
ts.discardMark();
continue;
}
ts.restoreMark();
}
const rule = consumeAQualifiedRule(ts, TT_SEMICOLON, true);
if (rule) {
if (onNode) onNode(rule);
else (rules || (rules = [])).push(rule);
}
}
}
};
/**
* Consume the remnants of a bad declaration, CSS Syntax Level 3 [§5.4.11](https://drafts.csswg.org/css-syntax/#consume-the-remnants-of-a-bad-declaration). Advances the stream past a malformed declaration's tail so the caller (`consumeABlocksContents`) can resume cleanly.
* @param {TokenStream} ts token stream
* @param {boolean} nested whether the call originates from inside a `{}` block
* @returns {void}
*/
const consumeTheRemnantsOfABadDeclaration = (ts, nested) => {
// Process input:
for (;;) {
const t = ts.next();
// <eof-token> / <semicolon-token>
// Discard a token from input, and return.
if (t.type === TT_EOF || t.type === TT_SEMICOLON) {
ts.discard();
return;
}
// <}-token>
// If nested is true, return. Otherwise, discard a token.
if (t.type === TT_RIGHT_CURLY_BRACKET) {
if (nested) return;
ts.discard();
continue;
}
// anything else
// Consume a component value from input, and do nothing.
consumeAComponentValue(ts);
}
};
/**
* Consume a declaration, CSS Syntax Level 3 [§5.4.6](https://drafts.csswg.org/css-syntax/#consume-declaration).
* @param {TokenStream} ts token stream
* @param {boolean=} nested true inside a `{}` block — a top-level `}` ends the value
* @returns {Declaration | undefined} parsed declaration, or `undefined` on the spec's "return nothing" branches (steps 1, 3, 8)
*/
const consumeADeclaration = (ts, nested = false) => {
const { input } = ts;
// Let decl be a new declaration, with an initially empty name and a value set to an empty list.
const start = ts.next().start;
// name "" / nameStart / nameEnd (= start) / important (false) keep their
// `Container` defaults; `value` is set unconditionally at step 5 below.
const decl = /** @type {Declaration} */ (
_makeContainer(T_DECLARATION, start, start)
);
// 1. If the next token is an <ident-token>, consume a token from input and set decl's name to the returned token's value.
// Otherwise, consume the remnants of a bad declaration from input, with nested, and return nothing.
if (ts.next().type === TT_IDENTIFIER) {
const head = ts.consume();
_setName(decl, head.start, head.end);
} else {
consumeTheRemnantsOfABadDeclaration(ts, nested);
return undefined;
}
// 2. Discard whitespace from input.
while (ts.next().type === TT_WHITESPACE) ts.discard();
// 3. If the next token is a <colon-token>, discard a token from input.
// Otherwise, consume the remnants of a bad declaration from input, with nested, and return nothing.
if (ts.next().type === TT_COLON) {
ts.discard();
} else {
consumeTheRemnantsOfABadDeclaration(ts, nested);
return undefined;
}
// 4. Discard whitespace from input.
while (ts.next().type === TT_WHITESPACE) ts.discard();
// Step 8's custom-property test, computed early so the value parse can bail.
const isCustomProperty = input.startsWith("--", start);
// 5. Consume a list of component values from input, with nested, and with <semicolon-token> as the stop token, and set decl's value to the result.
// A nested non-custom declaration bails on a top-level `{` — step 8 would
// reject it and the caller restores its mark, so parsing the block (the
// entire nested-rule body, re-parsed as a qualified rule after the
// restore) would be pure waste.
const value = consumeAListOfComponentValues(
ts,
TT_SEMICOLON,
nested,
nested && !isCustomProperty
);
if (value === null) return undefined;
// `_setValue` waits until step 9: steps 6-8 still trim / scan the scratch,
// and the SoA backend consumes it when sealing.
_setEnd(decl, ts.next().start);
// 6. If the last two non-<whitespace-token>s in decl's value are a <delim-token> with the value "!" followed by an <ident-token> with a value that is an ASCII case-insensitive match for "important", remove them from decl's value and set decl's important flag.
{
let last = value.length - 1;
while (last >= 0 && _nodeTypeOf(value[last]) === T_WHITESPACE) last--;
let prev = last - 1;
while (prev >= 0 && _nodeTypeOf(value[prev]) === T_WHITESPACE) prev--;
// `!` delim first: it's almost always absent, and `_nodeValueOf` allocates
// a slice — this order pays it only for genuine `!important` candidates.
if (
prev >= 0 &&
_nodeTypeOf(value[prev]) === T_DELIM &&
input.charCodeAt(_nodeStartOf(value[prev])) === CC_EXCLAMATION &&
_nodeTypeOf(value[last]) === T_IDENT &&
equalsLowerCase(_nodeValueOf(value[last]), "important")
) {
_setImportant(decl);
value.length = prev;
}
}
// 7. While the last item in decl's value is a <whitespace-token>, remove that token.
while (
value.length > 0 &&
_nodeTypeOf(value[value.length - 1]) === T_WHITESPACE
) {
value.pop();
}
// 8. If decl's name starts with "--" (a custom property), it can contain any value (including a top-level `{}` block) — accept it.
// Otherwise, if decl's value contains a top-level simple block with an associated token of <{-token>, return nothing.
// (That is, a top-level {}-block is only allowed as the entire value of a non-custom property — for CSS Nesting, `consumeABlocksContents`'s `mark` / `restore a mark` will retry the input as a qualified rule.)
// Otherwise, accept the declaration. (The spec also checks "contains any non-whitespace-tokens at the top level" → return nothing; we keep empty-value declarations because callers — e.g. `@value name:;` — rely on them.)
if (!isCustomProperty) {
for (let i = 0; i < value.length; i++) {
const v = value[i];
if (_nodeTypeOf(v) === T_SIMPLE_BLOCK && _nodeTokenOf(v) === "{") {
return undefined;
}
}
}
// 9. Return decl.
_setValue(decl, value);
return decl;
};
/**
* Consume a list of component values, CSS Syntax Level 3 [§5.4.7](https://drafts.csswg.org/css-syntax/#consume-list-of-components) — consumes component values until EOF, the optional `stopToken`, or — when `nested` — a top-level `}` (left in the stream); a non-nested `}` is a parse error appended as a token.
* @param {TokenStream} ts token stream
* @param {number=} stopToken token type that terminates the list (left unconsumed)
* @param {boolean=} nested true inside a `{}` block — a top-level `}` ends the list (left unconsumed)
* @param {boolean=} bailOnCurly abort with `null` on a top-level `{` (left unconsumed) — for callers that would reject the list anyway (consume-a-declaration step 8) and restore a mark
* @returns {ComponentValue[] | null} consumed component values, or `null` when `bailOnCurly` hit
*/
const consumeAListOfComponentValues = (
ts,
stopToken,
nested = false,
bailOnCurly = false
) => {
const values = /** @type {ComponentValue[]} */ (_soaActive ? _soaList() : []);
// Process input
for (;;) {
const t = ts.next();
// <eof-token>
// stop token (if passed)
// Return values.
if (t.type === TT_EOF || t.type === stopToken) {
return values;
}
// <}-token>
// If nested is true, return values.
// Otherwise, this is a parse error. Consume a token from input and append the result to values.
if (t.type === TT_RIGHT_CURLY_BRACKET) {
if (nested) return values;
const closer = consumeATokenAsNode(ts);
// Keep unless the type is explicitly marked skip (1); an out-of-range
// lookup on a short `skip.types` yields `undefined`, which must not drop.
if (!_skipActive || _skipTypes[_nodeTypeOf(closer)] !== 1) {
values.push(closer);
}
continue;
}
// A top-level `{` dooms the list for a bailing caller — stop before the
// whole block is parsed only to be thrown away on the caller's restore.
if (bailOnCurly && t.type === TT_LEFT_CURLY_BRACKET) return null;
// anything else
// Consume a component value from input, and append the result to values.
// Skipped leaf types short-circuit before materializing: no SoA slot is
// written and no node is built (blocks / functions never skip here).
const tt = t.type;
if (
_skipActive &&
tt !== TT_FUNCTION &&
!(tt >= TT_LEFT_PARENTHESIS && tt <= TT_LEFT_CURLY_BRACKET) &&
_skipTypes[_ttToNodeType[tt]] === 1
) {
ts.consume();
continue;
}
const node = consumeAComponentValue(ts, t);
if (!_skipActive || _skipTypes[_nodeTypeOf(node)] !== 1) values.push(node);
}
};
/**
* Consume a component value, CSS Syntax Level 3 [§5.4.8](https://drafts.csswg.org/css-syntax/#consume-component-value) — consumes the next value (simple block, function, or single token); callers guard against EOF before calling.
* @param {TokenStream} ts token stream
* @param {MutableToken=} t the next token, if the caller already peeked it (defaults to `ts.next()`)
* @returns {SimpleBlock | FunctionNode | ComponentValue} the consumed component value
*/
const consumeAComponentValue = (ts, t = ts.next()) => {
// `t` is the next token; hot callers already peeked it and pass it in to
// skip a redundant `ts.next()` per component value.
// <{-token> / <[-token> / <(-token> (the three contiguous opening brackets)
// Consume a simple block from input and return the result.
if (t.type >= TT_LEFT_PARENTHESIS && t.type <= TT_LEFT_CURLY_BRACKET) {
return /** @type {SimpleBlock} */ (consumeASimpleBlock(ts));
}
// <function-token>
// Consume a function from input and return the result.
if (t.type === TT_FUNCTION) {
return /** @type {FunctionNode} */ (consumeAFunction(ts));
}
// anything else
// Consume a token from input and return the result. (Asserted: not EOF.)
// Inlined `consumeATokenAsNode`: `t` is already the peeked next token, so
// advance past it and materialize it directly — one fewer call per leaf
// component value (the bulk of the nodes on a large stylesheet).
ts.consume();
return /** @type {ComponentValue} */ (tokenToNode(t));
};
/**
* Consume a simple block, CSS Syntax Level 3 [§5.4.9](https://drafts.csswg.org/css-syntax/#consume-simple-block) — the next token must be `(`, `[`, or `{` (asserted); consumes component values via `consumeAComponentValue` until the mirror closing token (`)`, `]`, `}`) or EOF, returning the partial block on EOF (parse error).
* @param {TokenStream} ts token stream
* @returns {SimpleBlock | undefined} the parsed simple block
*/
const consumeASimpleBlock = (ts) => {
const open = ts.next();
// Assert (spec): the next token of input is <{-token>, <[-token>, or <(-token>.
// Mirror closing token (`opener + 3`) and the associated block char.
const ending = open.type + 3;
const token = BLOCK_TOKEN_CHAR[open.type - TT_LEFT_PARENTHESIS];
// Let block be a new simple block with its associated token set to the next token and with its value initially set to an empty list.
const block = /** @type {SimpleBlock} */ (
_makeContainer(T_SIMPLE_BLOCK, open.start, open.end)
);
_setToken(block, token);
// Sealed (`_setValue`) at the return, once complete.
const val = _soaActive ? _soaList() : [];
// Discard a token from input.
ts.discard();
// Process input
for (;;) {
const t = ts.next();
// <eof-token>
// ending token
// Discard a token from input. Return block.
if (t.type === TT_EOF || t.type === ending) {
ts.discard();
_setValue(block, val);
_setEnd(block, t.end);
return block;
}
// anything else
// Consume a component value from input and append the result to blocks value.
val.push(consumeAComponentValue(ts, t));
}
};
/**
* Consume a function, CSS Syntax Level 3 [§5.4.10](https://drafts.csswg.org/css-syntax/#consume-function) — consumes component values up to the matching `)` or EOF (the partial function on EOF is a parse error).
* @param {TokenStream} ts token stream
* @returns {FunctionNode | undefined} the consumed function node
*/
const consumeAFunction = (ts) => {
// Assert (spec): the next token is a <function-token>.
// Consume a token from input, and let function be a new function with its name equal the returned tokens value, and a value set to an empty list.
const tFn = ts.consume();
const fn = /** @type {FunctionNode} */ (
_makeContainer(T_FUNCTION, tFn.start, tFn.end)
);
_setName(fn, tFn.start, tFn.end - 1);
// Sealed (`_setValue`) at the return, once complete.
const val = _soaActive ? _soaList() : [];
// Process input
for (;;) {
const t = ts.next();
if (t.type === TT_EOF || t.type === TT_RIGHT_PARENTHESIS) {
// <eof-token>
// <)-token>
// Discard a token from input. Return function.
ts.discard();
_setValue(fn, val);
_setEnd(fn, t.end);
return fn;
}
// anything else
// Consume a component value from input and append the result to functions value.
// Same pre-materialization skip as `consumeAListOfComponentValues`.
const tt = t.type;
if (
_skipActive &&
tt !== TT_FUNCTION &&
!(tt >= TT_LEFT_PARENTHESIS && tt <= TT_LEFT_CURLY_BRACKET) &&
_skipTypes[_ttToNodeType[tt]] === 1
) {
ts.consume();
continue;
}
const node = consumeAComponentValue(ts, t);
if (!_skipActive || _skipTypes[_nodeTypeOf(node)] !== 1) val.push(node);
}
};
// Identifier escape / unescape — operate on the raw text of an
// `<ident-token>` (or any source slice that may carry CSS escape sequences).
// `escapeIdentifier` produces a CSS-Syntax-3-conformant `<ident-token>` from
// an arbitrary string (so the result can be re-tokenized as the same name);
// `unescapeIdentifier` reverses tokenizer-time escapes per
// https://www.w3.org/TR/css-syntax-3/#consume-escaped-code-point.
// Both are pure string functions and have no dependency on the AST; they
// live here so the AST module is a one-stop shop for CSS-syntax-level
// utilities. `CssParser.js` re-exports them for back-compat with callers
// that previously reached them via `getCssParser()`.
const regexSingleEscape = /[ -,./:-@[\]^`{-~]/;
const regexExcessiveSpaces = /(^|\\+)?(\\[A-F0-9]{1,6}) (?![a-fA-F0-9 ])/g;
// ASCII escape class per char code: 0 = pass through, 1 = `\<char>` single
// escape, 2 = `\HEX ` (control chars). Built from the original predicates so
// behaviour is identical; replaces two regex tests per character with one load.
const ESCAPE_CLASS_HEX = 2;
const ESCAPE_CLASS_SINGLE = 1;
const _escapeClassTable = new Uint8Array(128);
for (let i = 0; i < 128; i++) {
const ch = String.fromCharCode(i);
_escapeClassTable[i] = /[\t\n\f\r\v]/.test(ch)
? ESCAPE_CLASS_HEX
: ch === "\\" || regexSingleEscape.test(ch)
? ESCAPE_CLASS_SINGLE
: 0;
}
/**
* Returns escaped identifier.
* @param {string} str string
* @returns {string} escaped identifier
*/
const _escapeIdentifier = (str) => {
let output = "";
// Flush safe runs in bulk: only escaped chars break the run, so an
// identifier needing no escapes returns `str` unchanged (no allocation).
let lastFlush = 0;
let needSpaceFix = false;
for (let i = 0; i < str.length; i++) {
const cc = str.charCodeAt(i);
const cls = cc < 128 ? _escapeClassTable[cc] : 0;
if (cls === 0) continue;
output += str.slice(lastFlush, i);
if (cls === ESCAPE_CLASS_SINGLE) {
output += `\\${str[i]}`;
} else {
output += `\\${cc.toString(16).toUpperCase()} `;
needSpaceFix = true;
}
lastFlush = i + 1;
}
output = lastFlush === 0 ? str : output + str.slice(lastFlush);
// `-` and digits are class 0 (never escaped above), so testing `str`'s lead
// char codes is equivalent to regexes over `output` — and keeps the common
// nothing-to-do call regex-free.
const first = str.charCodeAt(0);
if (
first === CC_HYPHEN_MINUS &&
(str.charCodeAt(1) === CC_HYPHEN_MINUS || _isDigit(str.charCodeAt(1)))
) {
output = `\\-${output.slice(1)}`;
} else if (_isDigit(first)) {
// A leading digit becomes `\3<digit> `, another `\HEX ` run to clean up.
output = `\\3${str.charAt(0)} ${output.slice(1)}`;
needSpaceFix = true;
}
// Remove spaces after `\HEX` escapes that are not followed by a hex digit,
// since theyre redundant. Only `\HEX ` runs (above) can produce them; plain
// single escapes can't, so skip the scan when none were emitted. Note this is
// only possible if the escape isn't preceded by an odd number of backslashes.
if (needSpaceFix) {
output = output.replace(regexExcessiveSpaces, ($0, $1, $2) => {
/* istanbul ignore if -- @preserve: this escaper never emits an odd run of backslashes before a `\HEX` escape (literal `\` is doubled) */
if ($1 && $1.length % 2) {
// Its not safe to remove the space, so dont.
return $0;
}
// Strip the space.
return ($1 || "") + $2;
});
}
return output;
};
/**
* Returns hex. Reads up to six hex digits from `str` starting at `start` —
* indexed rather than sliced, and case-folded inline, so the common
* non-hex escape (e.g. `\:` in `focus\:sr-only`) allocates nothing.
* @param {string} str string
* @param {number} start index just past the `\`
* @returns {[string, number] | undefined} hex
*/
const gobbleHex = (str, start) => {
let hex = "";
for (let i = 0; i < 6; i++) {
const code = str.charCodeAt(start + i);
// valid hex char [0-9 | A-F | a-f]; out-of-range reads NaN -> invalid
const valid =
(code >= 48 && code <= 57) ||
(code >= 65 && code <= 70) ||
(code >= 97 && code <= 102);
if (!valid) break;
// parseInt below is case-insensitive, so keep the original char.
hex += str[start + i];
}
if (hex.length === 0) return undefined;
// One trailing whitespace terminates the escape, matching the tokenizer's
// `_consumeAnEscapedCodePoint` — including after a full 6-digit escape, for
// any CSS whitespace (not just space), plus the extra LF of a CRLF pair.
// https://drafts.csswg.org/css-syntax/#consume-escaped-code-point
let consumed = hex.length;
const trail = str.charCodeAt(start + hex.length);
if (_isWhiteSpace(trail)) {
consumed = consumeExtraNewline(trail, str, start + hex.length + 1) - start;
}
const codePoint = Number.parseInt(hex, 16);
const isSurrogate = codePoint >= 0xd800 && codePoint <= 0xdfff;
// Add special case for
// "If this number is zero, or is for a surrogate, or is greater than the maximum allowed code point"
// https://drafts.csswg.org/css-syntax/#maximum-allowed-code-point
if (isSurrogate || codePoint === 0x0000 || codePoint > 0x10ffff) {
return ["", consumed];
}
return [String.fromCodePoint(codePoint), consumed];
};
/**
* Unescape identifier.
* @param {string} str string
* @returns {string} unescaped string
*/
const _unescapeIdentifier = (str) => {
// `indexOf` is the no-escape fast path and the start offset in one — the
// leading safe run is skipped and an unescaped ident returns as-is.
const first = str.indexOf("\\");
if (first === -1) return str;
let ret = "";
// Flush safe runs in bulk instead of appending char by char.
let lastFlush = 0;
for (let i = first; i < str.length; i++) {
if (str[i] !== "\\") continue;
ret += str.slice(lastFlush, i);
const gobbled = gobbleHex(str, i + 1);
if (gobbled !== undefined) {
ret += gobbled[0];
i += gobbled[1];
} else if (str[i + 1] === "\\") {
// Retain one `\` of an escaped `\\` pair.
// https://github.com/postcss/postcss-selector-parser/commit/268c9a7656fb53f543dc620aa5b73a30ec3ff20e
ret += "\\";
i += 1;
} else if (str.length === i + 1) {
// A trailing lone `\` is retained.
// https://github.com/postcss/postcss-selector-parser/commit/01a6b346e3612ce1ab20219acc26abdc259ccefb
ret += "\\";
}
// Otherwise the lone `\` is dropped; the next char flushes with its run.
lastFlush = i + 1;
}
ret += str.slice(lastFlush);
return ret;
};
// Cacheable per `compiler.root` — CssParser binds once per parse via
// `.bindCache(...)` and reuses for every identifier.
const escapeIdentifier = makeCacheable(_escapeIdentifier);
const unescapeIdentifier = makeCacheable(_unescapeIdentifier);
// A url-token / url-string value's escaped newlines (`url("im\<newline>g.png")`).
const STRING_MULTILINE = /\\[\n\r\f]/g;
// Leading / trailing CSS whitespace inside a quoted url value.
const TRIM_WHITE_SPACES = /(^[ \t\n\r\f]*|[ \t\n\r\f]*$)/g;
// One CSS escape: `\` + up to 6 hex digits (+ optional whitespace) or any char.
const UNESCAPE = /\\([0-9a-f]{1,6}[ \t\n\r\f]?|[\s\S])/gi;
/**
* Normalize a url value (a url-token's content or a url string's body) into
* the form requests are resolved from: escaped newlines removed (string form),
* edge whitespace trimmed, CSS escapes and percent-encoding decoded
* (`data:` URIs excepted).
* @param {string} str url string
* @param {boolean} isString is url wrapped in quotes
* @returns {string} normalized url
*/
const normalizeUrl = (str, isString) => {
// Fast paths: skip the regex engine for the common URL with no escape and
// no edge whitespace (e.g. `./img.png`). Each guard is equivalent to the
// regex being a no-op.
// Remove escaped newlines from a string-token url like `url("im\<newline>g.png")`.
if (isString && str.includes("\\")) {
str = str.replace(STRING_MULTILINE, "");
}
// Remove unnecessary spaces from `url(" img.png ")`
if (
str.length !== 0 &&
(_isWhiteSpace(str.charCodeAt(0)) ||
_isWhiteSpace(str.charCodeAt(str.length - 1)))
) {
str = str.replace(TRIM_WHITE_SPACES, "");
}
// Unescape
if (str.includes("\\")) {
str = str.replace(UNESCAPE, (match) => {
if (match.length > 2) {
return String.fromCharCode(Number.parseInt(match.slice(1).trim(), 16));
}
return match[1];
});
}
// Char-code gate so the dominant non-`data:` url skips the regex test.
if ((str.charCodeAt(0) | 0x20) === CC_LOWER_D && /^data:/i.test(str)) {
return str;
}
if (str.includes("%")) {
// Convert `url('%2E/img.png')` -> `url('./img.png')`
try {
str = decodeURIComponent(str);
} catch (_err) {
// Ignore
}
}
return str;
};
// CSS-typed views over the generic visitor machinery (`util/SourceProcessor`),
// re-exported so consumers keep importing them from this module.
/**
* @typedef {import("../util/SourceProcessor").VisitorFn<CssPath>} VisitorFn
* @typedef {import("../util/SourceProcessor").VisitorBucket<CssPath>} VisitorBucket
* @typedef {import("../util/SourceProcessor").VisitorMap<CssPath>} VisitorMap
* @typedef {import("../util/SourceProcessor").CompiledVisitorMap<CssPath>} CompiledVisitorMap
*/
/**
* A CSS Syntax §5.4 top-level consumer that streams each top-level node it
* produces to `onNode` (in source order) rather than collecting it. Every entry
* in `TOP_LEVEL_CONSUMERS` shares this shape, so the walk's `grammar` drives any
* `as` mode through one call — a future mode is just another map entry.
* @typedef {(ts: TokenStream, onNode: (node: Rule | Declaration) => void) => void} TopLevelConsumer
*/
/**
* `as` value → the §5.4 consumer that streams its top-level nodes. Keyed by the
* public `CssParserOptions.as` enum.
* @type {Record<string, TopLevelConsumer>}
*/
const TOP_LEVEL_CONSUMERS = {
stylesheet: /** @type {TopLevelConsumer} */ (consumeAStylesheetsContents),
"block-contents": consumeABlocksContents
};
/**
* @typedef {object} CssProcessOptions
* @property {LocConverter=} locConverter shared loc converter (default a fresh one over the input)
* @property {boolean=} recurseBlocks walk into block bodies' nested rules (default true)
* @property {("stylesheet" | "block-contents")=} as which top-level production to consume the source as (see `TOP_LEVEL_CONSUMERS`): `"stylesheet"` (default) or `"block-contents"` (a block's contents, e.g. an HTML `style` attribute)
* @property {SkipOptions=} skip what the grammar may leave un-materialized to go faster — safe only for parts nothing reads in the active parse; default skip nothing
*/
/**
* `CssProcessOptions.skip`: two independent axes, so each reads unambiguously.
* @typedef {object} SkipOptions
* @property {Uint8Array=} types component-value node types to drop from declaration value / function-arg lists (indexed by `NodeType`, 1 = skip; build with `buildSkipSet`)
* @property {boolean=} selectorPrelude drop qualified-rule (selector) preludes — the rule and its block are still produced (default false)
* @property {boolean=} atRulePrelude drop at-rule preludes — the at-rule and its block are still produced (default false)
*/
// Per-parse walk state in module slots (same pattern as `_skip*`) so the walk
// functions below are module-level constants: one function identity across
// parses keeps the recursive per-node call sites monomorphic and drops the
// per-parse closure allocations.
/** @typedef {import("../util/SourceProcessor").CompiledVisitorBucket<CssPath>} CompiledVisitorBucket */
/** @type {CompiledVisitorMap} */
let _visitors = /** @type {CompiledVisitorMap} */ (/** @type {unknown} */ ([]));
let _recurseBlocks = true;
/** @type {CompiledVisitorBucket | undefined} */
let _commentBucket;
// Comments reach the visitor map through `NodeType.Comment` instead of a
// side callback. They fire during tokenization — in source order among
// comments, not interleaved with the node walk — on a transient SoA node so
// `A.start`/`end`/`loc`/`source` work. No comment visitor → no callback →
// the tokenizer skips comments with zero overhead.
/** @type {(input: string, start: number, end: number) => number} */
const _grammarOnComment = (_input, start, end) => {
const node = _soaAllocNode(T_COMMENT, start, end);
_currentNode = node;
_currentParent = null;
const bucket = /** @type {CompiledVisitorBucket} */ (_commentBucket);
const e = bucket.enter;
for (let i = 0; i < e.length; i++) e[i](A);
const x = bucket.exit;
for (let i = 0; i < x.length; i++) x[i](A);
return end;
};
/**
* Walk a component-value subtree; children are already materialized. Fetches
* the node's visitor bucket once (reused for enter + exit) and uses index
* loops — `for…of` would allocate an iterator per node on this hot path.
* @param {Node} node component-value root
* @param {Node | null} parent enclosing node
*/
const _walkValue = (node, parent) => {
const ty = _soaTypes[_nodeIndex(node)];
const b = _visitors[ty];
let skip = false;
if (b !== undefined && b.enter.length !== 0) {
_walkSkip = false;
_currentNode = node;
_currentParent = parent;
const e = b.enter;
for (let i = 0; i < e.length; i++) e[i](A);
skip = _walkSkip;
_walkSkip = false;
}
if (!skip && (ty === T_FUNCTION || ty === T_SIMPLE_BLOCK)) {
const i0 = _nodeIndex(node);
const vs = _soaListStarts[i0];
const ve = vs + _soaListLens[i0];
for (let i = vs; i < ve; i++) _walkValue(_nodeRef(_soaFlat[i]), node);
}
if (b !== undefined) {
// Rebind: descending into children moved the path.
_currentNode = node;
_currentParent = parent;
const x = b.exit;
for (let i = 0; i < x.length; i++) x[i](A);
}
};
/**
* Walk a structural subtree; an at-rule / qualified-rule's block was parsed
* eagerly (§5.4.4), so its `value` holds the nested rules / declarations.
* @param {Node} node structural-tree root
* @param {Node | null} parent enclosing node
*/
const _walkRule = (node, parent) => {
const i0 = _nodeIndex(node);
const ty = _soaTypes[i0];
const b = _visitors[ty];
let skip = false;
if (b !== undefined && b.enter.length !== 0) {
_walkSkip = false;
_currentNode = node;
_currentParent = parent;
const e = b.enter;
for (let i = 0; i < e.length; i++) e[i](A);
skip = _walkSkip;
_walkSkip = false;
}
if (!skip) {
if (ty === T_AT_RULE || ty === T_QUALIFIED_RULE) {
const ps = _soaListStarts[i0];
const pe = ps + _soaListLens[i0];
for (let i = ps; i < pe; i++) _walkValue(_nodeRef(_soaFlat[i]), node);
if (_recurseBlocks) {
// Declarations then child rules — downstream consumers don't need them strictly interleaved in source order.
const decls = _soaDeclarationLists[i0];
if (decls) {
for (let i = 0; i < decls.length; i++) _walkRule(decls[i], node);
}
const ch = _soaChildRuleLists[i0];
if (ch) for (let i = 0; i < ch.length; i++) _walkRule(ch[i], node);
}
} else if (ty === T_DECLARATION) {
const vs = _soaListStarts[i0];
const ve = vs + _soaListLens[i0];
for (let i = vs; i < ve; i++) _walkValue(_nodeRef(_soaFlat[i]), node);
}
}
if (b !== undefined) {
// Rebind: descending into children moved the path.
_currentNode = node;
_currentParent = parent;
const x = b.exit;
for (let i = 0; i < x.length; i++) x[i](A);
}
};
/**
* The `grammar` streaming sink: walk one top-level node, then recycle the SoA
* buffers for the next.
* @param {Rule | Declaration} node top-level node
*/
const _walkTopLevel = (node) => {
_walkRule(node, null);
if (_soaNodeCount > _soaPeak) _soaPeak = _soaNodeCount;
if (_soaFlatTop > _soaFlatPeak) _soaFlatPeak = _soaFlatTop;
_soaNodeCount = 0;
_soaFlatTop = 0;
};
// The SoA buffers grow to the largest single top-level rule ever parsed and
// live at module level; above this capacity they are re-shrunk after a parse
// so one pathological rule can't pin megabytes for the process lifetime.
const _SOA_SHRINK_CAPACITY = 65536;
/**
* The CSS `SourceProcessor` grammar: consume top-level rules one at a time
* (§5.4.1) and walk each immediately, firing `enter` / `exit` in source order
* without building a whole-stylesheet array first. `recurseBlocks: false` skips
* walking block bodies' (eagerly parsed) nested rules (caller drives nested
* traversal itself).
* @param {string} input source text
* @param {CompiledVisitorMap} visitors compiled visitor map
* @param {CssProcessOptions} options process options
*/
const grammar = (input, visitors, options) => {
const locConverter = options.locConverter || new LocConverter(input);
useSoaBackend(input, locConverter);
const skip = options.skip;
_skipTypes = (skip && skip.types) || _NO_SKIP_TYPES;
_skipActive = _skipTypes !== _NO_SKIP_TYPES;
_skipSelectorPrelude = skip !== undefined && skip.selectorPrelude === true;
_skipAtRulePrelude = skip !== undefined && skip.atRulePrelude === true;
_recurseBlocks = options.recurseBlocks !== false;
_visitors = visitors;
_commentBucket = visitors[T_COMMENT];
// Stream each top-level node (selected by `as`) to the walker the moment it's
// consumed, rather than collecting them first — so the whole AST is never
// held at once; peak heap is ~one top-level node's subtree.
const ts = new TokenStream(
input,
0,
locConverter,
_commentBucket === undefined ? undefined : _grammarOnComment
);
const consume =
TOP_LEVEL_CONSUMERS[options.as || "stylesheet"] ||
consumeAStylesheetsContents;
try {
consume(ts, _walkTopLevel);
} finally {
// Drop the module-level SoA references so the last parsed source (and
// its LocConverter / child lists / visitors) don't stay alive between
// parses.
_soaInput = "";
_soaLocConverter = /** @type {LocConverter} */ (
/** @type {unknown} */ (null)
);
_soaDeclarationLists.length = 0;
_soaChildRuleLists.length = 0;
_soaFlatTop = 0;
_listPool.length = 0;
if (_soaFlat.length > _SOA_SHRINK_CAPACITY) {
_soaFlatGrowHint = _soaFlatPeak;
_soaFlat = new Int32Array(0);
}
_visitors = /** @type {CompiledVisitorMap} */ (/** @type {unknown} */ ([]));
_commentBucket = undefined;
if (_soaCapacity > _SOA_SHRINK_CAPACITY) {
// +1: node ids are 1-based and grow fires at `id >= capacity`.
_soaGrowHint = _soaPeak + 1;
_soaCapacity = 0;
_soaTypes = new Uint8Array(0);
_soaStarts = new Int32Array(0);
_soaEnds = new Int32Array(0);
_soaAux0 = new Int32Array(0);
_soaAux1 = new Int32Array(0);
_soaAux2 = new Int32Array(0);
_soaFlags = new Uint8Array(0);
_soaListStarts = new Int32Array(0);
_soaListLens = new Int32Array(0);
}
_soaPeak = 0;
_soaFlatPeak = 0;
}
};
/**
* The generic visitor coordinator (`util/SourceProcessor`) bound to the CSS
* `grammar`. Babel-style usage:
*
* ```
* new SourceProcessor({ skip }).use({ [NodeType.AtRule]: (path) => {} }).process(source);
* ```
* @extends {GenericSourceProcessor<CssPath, CssProcessOptions>}
*/
class SourceProcessor extends GenericSourceProcessor {
/**
* @param {CssProcessOptions=} options default process options (`skip`, `as`, …) for every `process` call
*/
constructor(options) {
super(grammar, options);
}
}
/**
* Build a `SkipOptions.types` set (drop these component-value node types from
* value / function-arg lists) from a list of `NodeType`s. Preludes are separate
* (`SkipOptions.selectorPrelude` / `atRulePrelude`). The caller owns the safety
* contract: only pass types nothing reads in the intended parse. Two
* grammar-internal caveats beyond consumer needs: dropping both `Delim` and
* `Ident` loses `!important` detection, and dropping `SimpleBlock` loses the
* custom-property `{}`-value check (and its subtree). Precompute once per
* configuration and reuse across parses.
* @param {number[]} nodeTypes component-value node types to drop
* @returns {Uint8Array} skip-types set indexed by `NodeType`
*/
const buildSkipSet = (nodeTypes) => {
const set = new Uint8Array(32);
for (let i = 0; i < nodeTypes.length; i++) set[nodeTypes[i]] = 1;
return set;
};
/* eslint-disable jsdoc/require-template -- `A` below is the accessor const, not a type parameter */
/**
* The CSS 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} CssPath
*/
/* eslint-enable jsdoc/require-template */
// A fresh (safely retainable) array view of a node's flat content span —
// visitors that read `A.children` / `A.prelude` may keep the result.
/** @type {(n: Node) => Node[]} */
const _materializeList = (n) => {
const i = _nodeIndex(n);
const start = _soaListStarts[i];
const len = _soaListLens[i];
/** @type {Node[]} */
const out = [];
for (let k = 0; k < len; k++) out.push(_nodeRef(_soaFlat[start + k]));
return out;
};
// 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 {Node} */
let _currentNode = /** @type {Node} */ (/** @type {unknown} */ (0));
/** @type {Node | null} */
let _currentParent = null;
// AST field-access seam. Every AST-node field read by `CssParser` goes through
// one of these accessors so the node representation can change underneath the
// consumer without touching it. Today they are backed by the `Node` / `Token` /
// `Container` objects (`n` is a node); the Struct-of-Arrays migration rewrites
// the bodies to index typed arrays (`n` becomes an integer node id) without any
// consumer edit. `value` is the leaf-token string; container child lists are
// `children` / `prelude` / `declarations` / `childRules`.
const A = {
// === path position (rebound by the walk before every visitor call) ===
/**
* @returns {Node} current node — only valid during a visitor callback
*/
get node() {
return _currentNode;
},
/**
* @returns {Node | null} enclosing node (null = a top-level node)
*/
get parent() {
return _currentParent;
},
/** Stop the walk descending into the current node (enter only). */
skipChildren() {
_walkSkip = true;
},
// === field reads — `n` defaults to the current node ===
/**
* @param {Node=} n node
* @returns {number} node type
*/
type(n = _currentNode) {
return _soaTypes[_nodeIndex(n)];
},
/**
* @param {Node=} n node
* @returns {number} start offset
*/
start(n = _currentNode) {
return _soaStarts[_nodeIndex(n)];
},
/**
* @param {Node=} n node
* @returns {number} end offset
*/
end(n = _currentNode) {
return _soaEnds[_nodeIndex(n)];
},
/**
* @param {Node=} n node
* @returns {[number, number]} start / end offsets
*/
range(n = _currentNode) {
const i = _nodeIndex(n);
return [_soaStarts[i], _soaEnds[i]];
},
/**
* @param {Node=} n node
* @returns {{ start: { line: number, column: number }, end: { line: number, column: number } }} source location
*/
loc(n = _currentNode) {
const i = _nodeIndex(n);
const lc = _soaLocConverter;
const s = lc.get(_soaStarts[i]);
const sl = s.line;
const sc = s.column;
const e = lc.get(_soaEnds[i]);
return {
start: { line: sl, column: sc },
end: { line: e.line, column: e.column }
};
},
/**
* @param {Node=} n node
* @returns {string} raw source slice
*/
source(n = _currentNode) {
const i = _nodeIndex(n);
return _soaInput.slice(_soaStarts[i], _soaEnds[i]);
},
/**
* @param {Node=} n node
* @returns {string} raw token value
*/
value(n = _currentNode) {
return _soaValueOf(_nodeIndex(n));
},
/**
* @param {Node=} n node
* @returns {string} unescaped token value
*/
unescaped(n = _currentNode) {
const i = _nodeIndex(n);
const v = _soaValueOf(i);
return _soaTypes[i] === T_STRING
? unescapeIdentifier(v.slice(1, -1))
: unescapeIdentifier(v);
},
/**
* @param {Node=} n node
* @returns {string} hash / numeric type flag
*/
typeFlag(n = _currentNode) {
const i = _nodeIndex(n);
if (_soaTypes[i] === T_HASH) {
const input = _soaInput;
const p = _soaStarts[i] + 1;
return _ifThreeCodePointsWouldStartAnIdentSequence(
input,
p,
input.charCodeAt(p),
input.charCodeAt(p + 1),
input.charCodeAt(p + 2)
)
? "id"
: "unrestricted";
}
const v = _soaValueOf(i);
return _typeFlagOf(
_soaTypes[i] === T_DIMENSION ? v.slice(0, _consumeANumber(v, 0)) : v
);
},
/**
* @param {Node=} n node
* @returns {number} url content start offset
*/
contentStart(n = _currentNode) {
return _soaAux0[_nodeIndex(n)];
},
/**
* @param {Node=} n node
* @returns {number} url content end offset
*/
contentEnd(n = _currentNode) {
return _soaAux1[_nodeIndex(n)];
},
/**
* @param {Node=} n node
* @returns {string} rule / declaration / function name
*/
name(n = _currentNode) {
const i = _nodeIndex(n);
return _soaTypes[i] === T_AT_RULE
? _soaInput.slice(_soaStarts[i] + 1, _soaAux0[i])
: _soaInput.slice(_soaStarts[i], _soaAux0[i]);
},
/**
* @param {Node=} n node
* @returns {number} name start offset
*/
nameStart(n = _currentNode) {
return _soaStarts[_nodeIndex(n)];
},
/**
* @param {Node=} n node
* @returns {number} name end offset
*/
nameEnd(n = _currentNode) {
return _soaAux0[_nodeIndex(n)];
},
/**
* @param {Node=} n node
* @returns {string} unescaped name
*/
unescapedName(n = _currentNode) {
return unescapeIdentifier(A.name(n));
},
/**
* @param {Node=} n node
* @returns {ComponentValue[]} function / block children
*/
children(n = _currentNode) {
return /** @type {ComponentValue[]} */ (_materializeList(n));
},
/**
* @param {Node=} n node
* @returns {ComponentValue[]} rule prelude
*/
prelude(n = _currentNode) {
return /** @type {ComponentValue[]} */ (_materializeList(n));
},
/**
* @param {Node=} n node
* @returns {Declaration[] | null} block declarations
*/
declarations(n = _currentNode) {
return /** @type {Declaration[] | null} */ (
_soaDeclarationLists[_nodeIndex(n)]
);
},
/**
* @param {Node=} n node
* @returns {Rule[] | null} block child rules
*/
childRules(n = _currentNode) {
return /** @type {Rule[] | null} */ (_soaChildRuleLists[_nodeIndex(n)]);
},
/**
* @param {Node=} n node
* @returns {number} block start offset
*/
blockStart(n = _currentNode) {
return _soaAux1[_nodeIndex(n)];
},
/**
* @param {Node=} n node
* @returns {number} block end offset
*/
blockEnd(n = _currentNode) {
return _soaAux2[_nodeIndex(n)];
},
/**
* @param {Node=} n node
* @returns {boolean} `!important` flag
*/
important(n = _currentNode) {
return (_soaFlags[_nodeIndex(n)] & 1) !== 0;
},
/**
* @param {Node=} n node
* @returns {SimpleBlockToken} block opening token
*/
blockToken(n = _currentNode) {
return /** @type {SimpleBlockToken} */ (
_soaInput[_soaStarts[_nodeIndex(n)]]
);
},
// Writers — `CssParser` rewrites a rule's end / block-end when it folds an
// inline ICSS `:import` / `:export` body into a single dependency. The
// node stays explicit here: writes should never be implicit on position.
/**
* @param {Node} n node
* @param {number} v new end offset
*/
setEnd(n, v) {
_soaEnds[_nodeIndex(n)] = v;
},
/**
* @param {Node} n node
* @param {number} v new block end offset
*/
setBlockEnd(n, v) {
_soaAux2[_nodeIndex(n)] = v;
}
};
// The two AST runtime classes — `Node` and its sole subclass `Token` (the
// other node shapes are `@typedef`s over `Node`, exported as types only). Plus
// the full CSS-Syntax-3 §5.3 `parseA*` entry-point surface, `consumeASimpleBlock`
// (the one §5.4 algorithm exposed as a byte entry point for `CssParser`), the
// `TokenStream` (so callers can pass a pre-built stream to any `parseA*`), and
// the `escape` / `unescapeIdentifier` string utils.
module.exports.A = A;
module.exports.Node = Node;
module.exports.NodeType = NodeType;
module.exports.SourceProcessor = SourceProcessor;
module.exports.TT_AT_KEYWORD = TT_AT_KEYWORD;
module.exports.TT_BAD_STRING_TOKEN = TT_BAD_STRING_TOKEN;
module.exports.TT_BAD_URL_TOKEN = TT_BAD_URL_TOKEN;
module.exports.TT_CDC = TT_CDC;
module.exports.TT_CDO = TT_CDO;
module.exports.TT_COLON = TT_COLON;
module.exports.TT_COMMA = TT_COMMA;
module.exports.TT_COMMENT = TT_COMMENT;
module.exports.TT_DELIM = TT_DELIM;
module.exports.TT_DIMENSION = TT_DIMENSION;
module.exports.TT_EOF = TT_EOF;
module.exports.TT_FUNCTION = TT_FUNCTION;
module.exports.TT_HASH = TT_HASH;
module.exports.TT_IDENTIFIER = TT_IDENTIFIER;
module.exports.TT_LEFT_CURLY_BRACKET = TT_LEFT_CURLY_BRACKET;
module.exports.TT_LEFT_PARENTHESIS = TT_LEFT_PARENTHESIS;
module.exports.TT_LEFT_SQUARE_BRACKET = TT_LEFT_SQUARE_BRACKET;
module.exports.TT_NUMBER = TT_NUMBER;
module.exports.TT_PERCENTAGE = TT_PERCENTAGE;
module.exports.TT_RIGHT_CURLY_BRACKET = TT_RIGHT_CURLY_BRACKET;
module.exports.TT_RIGHT_PARENTHESIS = TT_RIGHT_PARENTHESIS;
module.exports.TT_RIGHT_SQUARE_BRACKET = TT_RIGHT_SQUARE_BRACKET;
module.exports.TT_SEMICOLON = TT_SEMICOLON;
module.exports.TT_STRING = TT_STRING;
module.exports.TT_URL = TT_URL;
module.exports.TT_WHITESPACE = TT_WHITESPACE;
module.exports.Token = Token;
module.exports.TokenStream = TokenStream;
module.exports.buildSkipSet = buildSkipSet;
module.exports.equalsLowerCase = equalsLowerCase;
module.exports.escapeIdentifier = escapeIdentifier;
module.exports.isDashedIdentifier = isDashedIdentifier;
// CSS Syntax §4.2 "whitespace" (space / tab / newline / CR / FF) — the
// tokenizer's whitespace class, exported under the spec's name.
module.exports.isWhitespace = _isWhiteSpace;
module.exports.normalizeUrl = normalizeUrl;
module.exports.parseABlocksContents = parseABlocksContents;
module.exports.parseACommaSeparatedListOfComponentValues =
parseACommaSeparatedListOfComponentValues;
module.exports.parseAComponentValue = parseAComponentValue;
module.exports.parseADeclaration = parseADeclaration;
module.exports.parseAListOfComponentValues = parseAListOfComponentValues;
module.exports.parseARule = parseARule;
module.exports.parseAStylesheet = parseAStylesheet;
module.exports.parseAStylesheetsContents = parseAStylesheetsContents;
module.exports.rangeEquals = rangeEquals;
module.exports.rangeEqualsLowerCase = rangeEqualsLowerCase;
module.exports.readToken = readToken;
module.exports.toLowerCaseIfNeeded = toLowerCaseIfNeeded;
module.exports.unescapeIdentifier = unescapeIdentifier;