/* MIT License http://www.opensource.org/licenses/mit-license.php Author Tobias Koppers @sokra */ "use strict"; const { CSS_TYPE } = require("../ModuleSourceTypeConstants"); const LocConverter = require("../util/LocConverter"); const GenericSourceProcessor = require("../util/SourceProcessor"); const { deferredWrite } = GenericSourceProcessor; const { EMBEDDED_LANGUAGES, askEmbeddedRenderer, buildDataURI, collectEmbeddedDiagnostics, decodeDataURIPayload, embeddedText, languageOfMediaType, parseDataURI } = require("../util/dataURL"); const { makeCacheable } = require("../util/identifier"); /** * Renders source this stylesheet embeds — a `data:` URL's payload today. * Returning it unchanged, or anything but text, declines it, and the URL is * emitted as written. * @typedef {(source: string, info: { type: string, hostType: string }) => string | undefined} EmbeddedSourceRenderer */ /** * One embedded source recorded for a caller that can only answer * asynchronously, and the text to print once it has. * @typedef {import("../util/dataURL").DeferredEmbeddedSource} DeferredEmbeddedSource */ const { ABSOLUTE_UNIT_SCALE, ALPHA_VALUE_PROPERTIES, ANGLE_UNITS, AUTO_SECOND_VALUE_PROPERTIES, BOX_FAMILY_PREFIX, BOX_LONGHANDS, BOX_SHORTHANDS, CALC_REJECTING_PROPERTIES, CANONICAL_NAMES, CLAMPED_VALUE_RANGES, COLOR_ARGUMENT_FUNCTIONS, COLOR_KEYWORDS, COLOR_NAME_TO_SHORTEST, COLOR_ONLY_PROPERTIES, COMPOUND_CONTINUATIONS, CSS_WIDE_KEYWORDS, CUBIC_BEZIER_KEYWORDS, CUSTOM_IDENT_LIST_PROPERTIES, DEFAULT_GRADIENT_DIRECTIONS, DISPLAY_SHORT_FORMS, DROPPABLE_WHEN_EMPTY_AT_RULES, EASING_KEYWORDS, FAMILY_LONGHANDS, FAMILY_SLOT_CLASSES, FAMILY_SLOT_KEYWORDS, FEATURELESS_PSEUDO_CLASSES, FILTER_FUNCTION_OMITTED, FLEX_KEYWORDS, FONT_SIZE_KEYWORDS, FONT_STRETCH_PERCENTAGES, FONT_WEIGHT_NUMBERS, GENERIC_FONT_FAMILIES, GRADIENT_LAST_POSITIONS, INITIAL_VALUE_KEYWORDS, INTEGER_PROPERTIES, KEYWORD_ONLY_PROPERTIES, LEGACY_PSEUDO_ELEMENTS, LENGTH_ONLY_FUNCTIONS, LINEAR_GRADIENTS, MATH_FUNCTIONS, MATH_FUNCTION_ARITY, MATH_FUNCTION_FOLD, MATH_FUNCTION_KEYWORDS, MATH_FUNCTION_SUM_ARGUMENTS, MERGEABLE_AT_RULES, MERGE_LONGHANDS, NEGATIVE_ACCEPTING_PROPERTIES, NEVER, NTH_NAMED_EQUIVALENTS, NTH_PSEUDO_FUNCTIONS, OMITTABLE_INITIAL_KEYWORDS, ONE_VALUE_PAIR_SHORTHANDS, PAIR_LONGHANDS, PLACE_SHORTHANDS, POSITION_PROPERTIES, POSITION_X_KEYWORDS, POSITION_Y_KEYWORDS, PREFIXED_AT_RULES, PREFIXED_PROPERTIES, PREFIXED_SELECTORS, PREFIXED_SPELLING_KEYWORDS, PREFIXED_VALUES, PREFIX_WINDOWS, PREFIX_WINDOW_STARTS, RATIO_PROPERTIES, REPEAT_STYLE_KEYWORDS, REPEAT_STYLE_PROPERTIES, RGB_TO_NAME, SELECTOR_FUNCTIONS, SELECTOR_SUPPORTED_FROM, SHADOW_PROPERTIES, SHORTHAND_INITIAL_KEYWORDS, SLASH_BOX_SHORTHANDS, SLASH_LONGHANDS, STEPPED_FUNCTIONS, SUBSTITUTION_FUNCTIONS, SUPPORTED_FROM, SUPPORT_BROWSERS, SUPPORT_PROFILES, TRANSITION_BEHAVIORS, UNIT_CONVERSION_TARGETS, UNIT_GROUP_BASE, UNSHARED_LONGHAND_KEYWORDS, X_AXIS_TRANSFORMS, ZERO_ANGLE_FUNCTIONS, ZERO_UNIT_KEEPING_PROPERTIES, exactAdd, exactDivide, exactMultiply } = require("./data"); // 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 {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_T = "t".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); const CC_TILDE = "~".charCodeAt(0); const CC_EQUALS_SIGN = "=".charCodeAt(0); // Lexer token types (CSS Syntax Level 3 §4) plus the ``. 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; }; // Every ASCII uppercase letter in the tail of a name that carries one; the fold // below is the only place either is used, and only for such a name. const _ASCII_UPPER_RE = /[A-Z]/g; /** @type {(one: string) => string} */ const _toAsciiLower = (one) => String.fromCharCode(one.charCodeAt(0) | 0x20); /** * A name CSS matches ASCII case-insensitively — a property, at-keyword, * function, pseudo, media feature or unit — written lowercase, so the same name * is the same bytes wherever it is spelled. Only ASCII letters map, the matching * being ASCII-only; a name carrying an escape is written back as authored, since * `\G` and `\g` name different characters. * @param {string} s the name * @returns {string} it, lowercased */ const asciiLowerCaseName = (s) => { // Asked natively first: `toLowerCase` hands back the very string it was given // where nothing folds, so this is a pointer compare and no allocation for // every name in a stylesheet written in one case — which walking the // characters here was costing more than the fold itself. if (s.toLowerCase() === s) return s; // Something folds, but only ASCII may: `Ä` is its own character, and a name // whose only capital is one of those is written back as authored. let at = -1; for (let i = 0; i < s.length; i++) { const c = s.charCodeAt(i); if (c >= CC_UPPER_A && c <= CC_UPPER_Z) { at = i; break; } } if (at === -1) return s; // An escape names a character by case (`\G` is not `\g`), so a name carrying // one is written back as authored. if (s.includes("\\")) return s; return s.slice(0, at) + s.slice(at).replace(_ASCII_UPPER_RE, _toAsciiLower); }; /** * A custom property name (``): 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; }; /** * CSS Syntax §4.3.7, entered at the `\` so a caller scanning raw source can * step over an escape without re-deriving what it spans. * @param {string} input input * @param {number} pos position of the `\` * @returns {number} position past the escape sequence */ const skipEscape = (input, pos) => _consumeAnEscapedCodePoint(input, pos + 1); /** * 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); } // Sticky fast-forward classes: a native run-skip over the ordinary characters of // a string / url token, so long values (data: URIs, base64) don't cost one JS // char read each. The negated classes match exactly the per-char terminators the // loops below handle (quotes / backslash / newlines for strings; plus `(`, `)`, // whitespace and non-printable code points for urls). const _STRING_SAFE = /[^"'\\\n\r\f]+/y; // eslint-disable-next-line no-control-regex -- url terminators include the control range and DEL (they make a bad-url) const _URL_SAFE = /[^\u0000-\u0020\u007F"'()\\]+/y; /** * 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 (;;) { _STRING_SAFE.lastIndex = pos; if (_STRING_SAFE.test(input)) pos = _STRING_SAFE.lastIndex; 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 (;;) { _URL_SAFE.lastIndex = pos; if (_URL_SAFE.test(input)) pos = _URL_SAFE.lastIndex; 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); } } } } /** Longest `url` spelling: each code point as `\` + 6 hex digits + CRLF. */ const MAX_ESCAPED_URL_LENGTH = 3 * (1 + 6 + 2); /** * Whether an ident spans `url` — `\75 rl` names it too, so the unescaped value * decides. The gates keep every other function name off the slice. * @param {string} input input * @param {number} start ident start offset * @param {number} end ident end offset (exclusive) * @returns {boolean} true when the ident names `url` */ const _identNamesUrl = (input, start, end) => { const length = end - start; if (length === 3) { return ( (input.charCodeAt(start) | 0x20) === CC_LOWER_U && (input.charCodeAt(start + 1) | 0x20) === CC_LOWER_R && (input.charCodeAt(start + 2) | 0x20) === CC_LOWER_L ); } if (length < 4 || length > MAX_ESCAPED_URL_LENGTH) return false; const first = input.charCodeAt(start); if ((first | 0x20) !== CC_LOWER_U && first !== CC_REVERSE_SOLIDUS) { return false; } return equalsLowerCase(unescapeIdentifier(input.slice(start, end)), "url"); }; /** * 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); if ( input.charCodeAt(pos) === CC_LEFT_PARENTHESIS && _identNamesUrl(input, start, pos) ) { 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 // // `_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 . case HC_WHITESPACE: return consumeSpace(input, pos, out); // `"` / `'` → (or 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); // `#` → if an ident/escape follows, else a . case HC_NUMBER_SIGN: return consumeNumberSign(input, pos, out); // `+` → if it starts a number, else a . case HC_PLUS_SIGN: return consumePlusSign(input, pos, out); // `-` → number / (`-->`) / ident / . case HC_HYPHEN_MINUS: return consumeHyphenMinus(input, pos, out); // `.` → if a digit follows, else a . case HC_FULL_STOP: return consumeFullStop(input, pos, out); // `<` → (``) 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); _setupParse(ts.input, ts.locConverter); // 2. Consume a stylesheet’s contents from input, and return the result. const rules = consumeAStylesheetsContents(ts); const store = _finishStore(); return /** @type {Rule[]} */ (_readRefList(store, rules)); }; /** * 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); _setupParse(ts.input, ts.locConverter); // 2. Consume a block’s contents from input, and return the result. const { decls, rules } = consumeABlocksContents(ts); const store = _finishStore(); return { decls: /** @type {Declaration[]} */ (_readRefList(store, decls)), rules: rules.length > 0 ? /** @type {Rule[]} */ (_readRefList(store, rules)) : _READER_EMPTY }; }; /** * 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); _setupParse(ts.input, ts.locConverter); // 2. Discard whitespace from input. while (ts.next().type === TT_WHITESPACE) ts.advance(); // 3. If the next token from input is an , return a syntax error. // Otherwise, if the next token from input is an , 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.advance(); // 5. If the next token from input is an , return rule. Otherwise, return a syntax error. return ts.next().type === TT_EOF ? _finishOne(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); _setupParse(ts.input, ts.locConverter); // 2. Discard whitespace from input. while (ts.next().type === TT_WHITESPACE) ts.advance(); // 3. Consume a declaration from input. If anything was returned, return it. Otherwise, return a syntax error. const decl = consumeADeclaration(ts); return decl === undefined ? undefined : _finishOne(decl); }; /** * 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); _setupParse(ts.input, ts.locConverter); // 2. Discard whitespace from input. while (ts.next().type === TT_WHITESPACE) ts.advance(); // 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.advance(); // 6. If input is empty, return value. Otherwise, return a syntax error. return ts.next().type === TT_EOF ? _finishOne(result) : 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); _setupParse(ts.input, ts.locConverter); // 2. Consume a list of component values from input, and return the result. // (`null` needs `bailOnCurly`, which is not passed here.) const values = /** @type {Node[]} */ (consumeAListOfComponentValues(ts)); const store = _finishStore(); return /** @type {ComponentValue[]} */ (_readRefList(store, values)); }; /** * 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 ``-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); _setupParse(ts.input, ts.locConverter); // 2. Let groups be an empty list. /** @type {Node[][]} */ 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 as the stop token, and append the result to groups. groups.push( /** @type {Node[]} */ (consumeAListOfComponentValues(ts, TT_COMMA)) ); // 3.2 Discard a token from input. ts.discard(); } // 4. Return groups — wrap each group's refs against the retained snapshot. const store = _finishStore(); return groups.map( (g) => /** @type {ComponentValue[]} */ (_readRefList(store, g)) ); }; // === 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 (``) 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(); // / / // Discard a token from input. if (t.type === TT_WHITESPACE || t.type === TT_CDO || t.type === TT_CDC) { ts.discard(); } // // Return rules. else if (t.type === TT_EOF) { return rules; } // // 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 (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 . // Consume a token from input, and let rule be a new at-rule with its name set to the returned token’s 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 store consumes the // scratch array when sealing, so it must be complete by then. const prelude = _takeList(); // declarations / childRules stay null (no block); the `;` / EOF / nested-`}` // forms set blockStart / blockEnd to -1 explicitly at their return below. // 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(); // // // 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); _setBlock(rule, -1); _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 rule’s prelude. else if (t.type === TT_RIGHT_CURLY_BRACKET) { if (nested) { _setPrelude(rule, prelude); _setBlock(rule, -1); _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); if (_streamBlocks) { _streamConsumeBlock(ts, rule); return rule; } consumeABlock(ts); _setBody(rule, _blockDecls, _blockRules); _setBlock(rule, _blockStart); _setEnd(rule, _blockEnd); return rule; } // anything else // Consume a component value from input and append the returned value to rule’s 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. Allocated lazily // on first push: skip mode usually pushes nothing (empty selector prelude), // and `_makeContainer` already zeroed the rule's list length, so a null // prelude reads as empty in the walk. /** @type {Node[] | null} */ let prelude = null; // A returned qualified rule always has a block (only the `{` exit returns a // rule), so `_setBlock` always runs — no blockStart / blockEnd default needed. // 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; // Non-skip: first two non-whitespace prelude nodes (computed at the `{`). let first = /** @type {Node} */ (/** @type {unknown} */ (0)); let second = /** @type {Node} */ (/** @type {unknown} */ (0)); // Skip mode: the same two tokens tracked by token type + start, so a dropped // leaf selector token needs no materialized node just for the disambiguation. // `0` (no token type) = unset. let firstTT = 0; let firstStart = 0; let secondTT = 0; // Process input for (;;) { const t = ts.next(); // // 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 rule’s prelude. else if (t.type === TT_RIGHT_CURLY_BRACKET) { if (nested) return undefined; if (skip) { // Stray `}` is a non-ws token; record its type/start, drop the node. if (firstTT === 0) { firstTT = t.type; firstStart = t.start; } else if (secondTT === 0) { secondTT = t.type; } ts.advance(); } else { (prelude || (prelude = _takeList())).push(consumeATokenAsNode(ts)); } continue; } // <{-token> // If the first two non- values of rule's prelude are an whose value starts with "--" followed by a , 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) { // `--foo: {` disambiguation: are the first two non-ws prelude tokens an // ident starting with `--` followed by a colon? Skip mode reads the // tracked token type/start; non-skip reads the materialized prelude. let dashedDeclaration; if (skip) { dashedDeclaration = firstTT === TT_IDENTIFIER && ts.input.startsWith("--", firstStart) && secondTT === TT_COLON; } else if (prelude === null) { dashedDeclaration = false; } else { 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]; dashedDeclaration = 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; } if (dashedDeclaration) { /* 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; } if (prelude !== null) _setPrelude(rule, prelude); if (_streamBlocks) { _streamConsumeBlock(ts, rule); return rule; } consumeABlock(ts); _setBody(rule, _blockDecls, _blockRules); _setBlock(rule, _blockStart); _setEnd(rule, _blockEnd); return rule; } // anything else // Consume a component value from input and append the result to rule’s prelude. if (skip) { const tt = t.type; // Track the first two non-whitespace tokens for the disambiguation above. if (tt !== TT_WHITESPACE) { if (firstTT === 0) { firstTT = tt; firstStart = t.start; } else if (secondTT === 0) { secondTT = tt; } } // Only functions (which may hold a url like `:unknown(url(x))`) and the // `(` / `[` blocks that must be balanced are materialized; the url // visitor keeps url / function nodes. Every other selector leaf token has // no non-modules consumer — drop it without building a node. if ( tt === TT_FUNCTION || (tt >= TT_LEFT_PARENTHESIS && tt <= TT_LEFT_CURLY_BRACKET) ) { const node = consumeAComponentValue(ts, t); const ty = _nodeTypeOf(node); if (ty === T_FUNCTION || ty === T_URL) { (prelude || (prelude = _takeList())).push(node); } } else { ts.advance(); } } else { (prelude || (prelude = _takeList())).push(consumeAComponentValue(ts, t)); } } }; /** * 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. * * Results go into the `_block*` slots rather than a returned object: there is one * block per rule, and every caller reads all four immediately, so the object was * pure garbage. Read them before parsing anything else. * @param {TokenStream} ts token stream * @returns {void} */ 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(); consumeABlocksContentsInto(ts); // Read before anything else runs: a nested block would overwrite these. const decls = _bcDecls; const rules = _bcRules; const close = ts.next(); const end = close.type === TT_RIGHT_CURLY_BRACKET ? close.end : close.start; ts.discard(); _blockDecls = decls; _blockRules = rules; _blockStart = blockStart; _blockEnd = end; }; /** * 2-token lookahead: is the next non-whitespace pair ` `? * 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 , so the next significant char // being `:` is equivalent to the next token being a . 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. * * `streamDepth` / `streamRule` are the other half of block streaming (see * `_streamConsumeBlock`): the block collects into its own arrays exactly as it * always has, and only once it has grown past `_STREAM_MIN_NODES` does it * activate and switch to the sink. Keeping that check here rather than behind * `onNode` is what makes a small block cost the same as it does with nothing * streaming — no call, no type test per child — and the block's frame is written * only when something could still read it (see `_streamPublishFrame`), so a block * of nothing but declarations never touches one. * @param {TokenStream} ts token stream * @param {((node: Declaration | Rule) => void)=} onNode optional per-node sink (streaming); nodes are not collected when given * @param {number=} streamDepth this block's frame depth while the walk streams, else undefined * @param {Rule=} streamRule the rule this block belongs to, needed only to stream it * @param {boolean=} nested whether a `{` opened this — true inside a block, where a `}` closes it; false for the whole input, where one closes nothing (default true) * Results go into `_bcDecls` / `_bcRules` rather than a returned object — one * block's contents per rule made that object pure garbage. `consumeABlocksContents` * below wraps this for the callers that do want an object. * @returns {void} */ const consumeABlocksContentsInto = ( ts, onNode, streamDepth, streamRule, nested = true ) => { /** @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; let sink = onNode; // -1 = nothing to stream, and then the growth check below can never fire. const d = streamDepth === undefined ? -1 : streamDepth; let limit = _STREAM_NO_LIMIT; // Where this block began, kept in locals until something needs the frame. let markNode = 0; let markFlat = 0; let published = false; if (d >= 0) { markNode = _nodeCount; markFlat = _flatTop; limit = markNode + _STREAM_MIN_NODES; _bcStreamed = false; } // Process input: for (;;) { const t = ts.next(); // / // Discard a token from input (`t` was just peeked and is non-EOF). if (t.type === TT_WHITESPACE || t.type === TT_SEMICOLON) { ts.advance(); continue; } // <}-token> // Return decls and rules — but only nested, where it closes the block. At // top level nothing opened one, so it is a parse error whose bad // declaration runs to the next `;`, which is what a browser reads a // `style=""` holding one as. if (t.type === TT_RIGHT_CURLY_BRACKET && !nested) { consumeTheRemnantsOfABadDeclaration(ts, false); continue; } // / <}-token> // Return decls and rules. if (t.type === TT_EOF || t.type === TT_RIGHT_CURLY_BRACKET) { _bcDecls = decls; _bcRules = rules || _EMPTY_LIST; // Read from the frame, not from `sink`: a descendant can activate this // block during a child rule that then fails to parse. if (d >= 0) _bcStreamed = published && _frameActive[d] === 1; return; } /** @type {Rule | undefined} */ let childRule; // // Consume an at-rule from input, with nested set to true. If a rule was returned, append it to rules. if (t.type === TT_AT_KEYWORD) { if (d >= 0 && !published && sink === undefined) { published = true; _streamPublishFrame(d, streamRule, markNode, markFlat, decls, rules); } childRule = consumeAnAtRule(ts, nested); } // 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 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 ` `; 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, nested); if (decl) { ts.discardMark(); // A declaration opens no block, so nothing can have activated this // one behind our back — only its own growth has to be checked. if (sink) { sink(decl); } else { decls.push(decl); if (_nodeCount > limit) { if (_streamWriter !== undefined && rules === null) { // The longhand merge needs every declaration at once; a child // rule is where that is given up rather than hold the block. limit = _nodeCount + _STREAM_MIN_NODES; } else { if (!published) { published = true; // Grew past the threshold on declarations alone, so the // frame has to be written here instead. _streamPublishFrame( d, streamRule, markNode, markFlat, decls, rules ); } _streamActivate(d); sink = _streamOnNode; limit = _STREAM_NO_LIMIT; } } } continue; } ts.restoreMark(); } // A child rule opens a block, and a block is the only thing that can reach // back up and activate this one — so this is the last moment the frame has // to be readable, and a block of nothing but declarations never gets here. if (d >= 0 && !published && sink === undefined) { published = true; _streamPublishFrame(d, streamRule, markNode, markFlat, decls, rules); } const rawStart = t.start; childRule = consumeAQualifiedRule(ts, TT_SEMICOLON, true); if (!childRule && _printing) { // Workaround, deliberately off-spec: §5.4 drops input both productions // reject, which would silently delete IE hacks and template // placeholders from minified output. Keep the source verbatim instead. // Printing only, so the spec-exact tree is what every consumer sees. childRule = /** @type {Rule | undefined} */ ( _makeRaw(rawStart, ts.next().start) ); } } // A child rule holds a block, so a descendant of it may have activated this // block while it was being consumed — `decls` / `rules` are then already // walked and recycled, and everything from here on belongs to the sink. // Checked even when the rule did not parse, since the block it opened is // what activated us and the next child must not reach a recycled list. if (!sink && published && _frameActive[d] === 1) sink = _streamOnNode; if (!childRule) continue; if (sink) { sink(childRule); continue; } if (rules === null) { rules = []; // The frame is written before any child rule is consumed, so it is // already pointing at this block and needs the list it did not yet have. if (published) _frameRules[d] = rules; } rules.push(childRule); if (_nodeCount > limit) { _streamActivate(d); sink = _streamOnNode; limit = _STREAM_NO_LIMIT; } } }; /** * Object-returning wrapper over `consumeABlocksContentsInto` for the callers that * are not per-rule (the `parseABlocksContents` entry point and the printer map). * @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 */ const consumeABlocksContents = (ts, onNode) => { consumeABlocksContentsInto(ts, onNode); return { decls: _bcDecls, rules: _bcRules }; }; /** * The same production read as the whole input rather than as a block's inside — * what an HTML `style=""` holds. Nothing opened a block, so a `}` closes none. * @param {TokenStream} ts token stream * @param {((node: Rule | Declaration) => void)=} onNode streaming sink * @returns {void} */ const consumeADeclarationList = (ts, onNode) => { consumeABlocksContentsInto(ts, onNode, undefined, undefined, false); }; /** * 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(); // / // 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; // nameEnd (= start) / important (unset) 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 , 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.advance(); // 3. If the next token is a , 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.advance(); } else { consumeTheRemnantsOfABadDeclaration(ts, nested); return undefined; } // 4. Discard whitespace from input. while (ts.next().type === TT_WHITESPACE) ts.advance(); // 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 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 store consumes it when sealing. _setEnd(decl, ts.next().start); // 6. If the last two non-s in decl's value are a with the value "!" followed by an 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); // Trimmed by popping so the pooled list keeps its backing store. while (value.length > prev) value.pop(); } } // 7. While the last item in decl's value is a , 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>, and also contains any other non-whitespace token, return nothing. // (That is, a top-level {}-block is the whole value of a non-custom property or nothing — 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) { let block = false; let beside = false; for (let i = 0; i < value.length && !(block && beside); i++) { const v = value[i]; const type = _nodeTypeOf(v); if (type === T_SIMPLE_BLOCK && _nodeTokenOf(v) === "{") { // A second one stands beside the first. if (block) beside = true; block = true; } else if (type !== T_WHITESPACE) { beside = true; } } if (block && beside) 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 `{` that something non-whitespace already stands before (left unconsumed) — for callers that would reject the list anyway (consume-a-declaration step 8) and restore a mark; a `{}` block alone is the whole value there, so that one is consumed * @returns {ComponentValue[] | null} consumed component values, or `null` when `bailOnCurly` hit */ const consumeAListOfComponentValues = ( ts, stopToken, nested = false, bailOnCurly = false ) => { const values = /** @type {ComponentValue[]} */ (_takeList()); // Process input for (;;) { const t = ts.next(); // // 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. // Only once something stands before it: a `{}` block alone is a // declaration's whole value (§5.4.6 step 8), which the caller keeps. if (bailOnCurly && t.type === TT_LEFT_CURLY_BRACKET) { let doomed = false; for (let i = 0; i < values.length; i++) { if (_nodeTypeOf(values[i]) !== T_WHITESPACE) { doomed = true; break; } } if (doomed) return null; } // anything else // Consume a component value from input, and append the result to values. // Skipped leaf types short-circuit before materializing: no column 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 ) { // `t` was just peeked and is a skipped value leaf (never EOF). ts.advance(); 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)); } // // 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 (and not // EOF), so `advance` past it and materialize it directly — no redundant // `next()` and one fewer call per leaf component value (the bulk of the nodes // on a large stylesheet). ts.advance(); 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 = _takeList(); // Discard a token from input. ts.discard(); // Process input for (;;) { const t = ts.next(); // // 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 block’s 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 . // Consume a token from input, and let function be a new function with its name equal the returned token’s 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 = _takeList(); // Process input for (;;) { const t = ts.next(); if (t.type === TT_EOF || t.type === TT_RIGHT_PARENTHESIS) { // // <)-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 function’s 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 ) { // `t` was just peeked and is a skipped value leaf (never EOF). ts.advance(); 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 // `` (or any source slice that may carry CSS escape sequences). // `escapeIdentifier` produces a CSS-Syntax-3-conformant `` 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 = `\` 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 `, 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 they’re 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) { // It’s not safe to remove the space, so don’t. 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\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\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} VisitorFn * @typedef {import("../util/SourceProcessor").VisitorBucket} VisitorBucket * @typedef {import("../util/SourceProcessor").VisitorMap} VisitorMap * @typedef {import("../util/SourceProcessor").CompiledVisitorMap} 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} */ const TOP_LEVEL_CONSUMERS = { stylesheet: /** @type {TopLevelConsumer} */ (consumeAStylesheetsContents), "block-contents": consumeADeclarationList }; /** * What the minifying printer may rewrite. Every entry is on unless it is * `false`, so a document one transform breaks can still be minified by the rest. * @typedef {object} CssTransformOptions * @property {(boolean | "all" | "some" | string | RegExp | ((comment: string) => boolean))=} comments which comments survive: `"some"` (the default) the ones that carry something, `true` / `"all"` every one, `false` none, or the ones a pattern matches / a predicate accepts, over the comment's own text * @property {boolean=} mergeLonghands write a family of longhands as the one shorthand that sets them * @property {boolean=} mergeRules join rules that print the same block, at-rules that share a prelude, and a named `@layer` block a later sibling opens again * @property {boolean=} normalizeQuotes normalize a string's, `url()`'s, font family's and attribute value's quoting * @property {boolean=} reduceFunctions compute a call into the shorter call naming the same value (`calc()` and the math functions, transforms, gradients, easing functions, filters) * @property {boolean=} removeDeadRules drop a rule or declaration nothing can read: an empty rule, and one an identical later one supersedes * @property {boolean=} shortenColors write each color in the shortest spelling of the same value * @property {boolean=} shortenMediaQueries write a media feature in its range spelling and collapse an `and` of two into the interval * @property {boolean=} shortenNumbers write each number in its shortest equal spelling * @property {boolean=} shortenSelectors rewrite a selector into a shorter equal one * @property {boolean=} shortenValues write a value the shortest way its property's own grammar allows */ /** * @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. Ignored while printing (`minimize`), which needs every node * @property {boolean=} minimize print the safely-minified serialization (collapsed whitespace, dropped redundant separators, the `printer`'s value transforms) as `process` walks and return `{ code, map }` (default false = walk only, return `undefined`) * @property {string=} source name of the input in the emitted source map (`sources[0]` / `file`); only read while printing * @property {string=} content the input's contents for the map's `sourcesContent`; only read while printing * @property {CssEnvironment=} environment what the target can read (the CSS entries of `output.environment`), so a spelling it would not understand is never reached for; only read while printing, and an absent entry means the modern spelling is available * @property {boolean=} convertLengthUnits rewrite a length into a shorter unit it is exactly equal in (`16px` -> `1pc`); off by default because it earns nothing once the asset is compressed, and only read while printing. A time is always rewritten * @property {boolean=} rewriteCustomProperties shorten a custom property's value the way any other value is shortened (`--x:#ffffff` -> `#fff`); off by default because `getPropertyValue()` hands that text back, and only read while printing. What it may rewrite is what any other value's tokens may be, a color in a substitution's fallback included — that being the property's value rather than the function's own argument * @property {EmbeddedSourceRenderer=} renderEmbeddedSource renders source this stylesheet embeds: the payload of a `url()` `data:` URL whose media type names a language webpack knows (SVG, CSS, HTML, JSON, JavaScript). Absent, a data URL is emitted exactly as written * @property {CssTransformOptions=} transforms which of the meaning-preserving rewrites the minifying print makes; each is on unless it is `false` * @property {DeferredEmbeddedSource[]=} deferEmbeddedSource collects what `renderEmbeddedSource` would be offered instead of offering it, for a caller whose renderer is asynchronous: the print leaves a marker for each and `finish` puts the answers in their place, so one parse serves both. Takes precedence over `renderEmbeddedSource` */ /** * The environment the stylesheet is built for. Every CSS ability the printer * reaches for is read off this selection, so nothing states one separately. * @typedef {object} CssEnvironment * @property {string[]=} browsers the browserslist selection (`["chrome 100", "safari 15"]`), so vendor prefixes and every spelling a target has to be able to read are decided for exactly these browsers; absent leaves prefixes untouched and assumes every ability * @property {boolean=} vendorPrefixes whether prefixes are written at all; false leaves them alone while the selection still decides which spellings a target reads */ /** * `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} CompiledVisitorBucket */ /** @type {CompiledVisitorMap} */ let _visitors = /** @type {CompiledVisitorMap} */ (/** @type {unknown} */ ([])); let _recurseBlocks = true; // Printing needs a faithful serialization, so it keeps dropped input as `T_RAW` // nodes; a walk-only parse leaves this false and allocates none. let _printing = false; // The selectors / at-rules met so far in each open block, so a prefixed rule can // be added or dropped against the sibling it needs without buffering the whole // stylesheet (streaming holds ~one node). Keyed by the block's rule — null for // the stylesheet itself — and dropped as that block finishes, so it holds one // set per open ancestor and never a recycled node. Null unless minifying for a // target. Markers: `signature` (the unprefixed rule is present) and // `signature\0prefix`. /** @type {Map | null} */ let _seenPrefixRules = null; // Whether vendor prefixes are written at all. The selection still answers which // spellings a target reads, so `vendorPrefixes: false` turns off only this. let _prefixingOn = false; // The prefixed rule just printed, and the signature of the unprefixed twin that // would make it dead weight — read by whoever writes that rule out, so it can go // as a piece of its own. Null when the rule just printed is not one. /** @type {{ node: Node, signature: string } | null} */ let _prefixDropCandidate = null; // The browserslist selection this parse prefixes for, as the versions selected // for each browser by its `SUPPORT_BROWSERS` slot; null unless minifying for // one, which is what turns prefixing on. /** @type {(number[] | undefined)[] | null} */ let _prefixBrowsers = null; /** @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 store 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 = (_src, start, end) => { const node = _makeLeaf(T_COMMENT, start, end); _currentNode = node; _currentParent = null; _currentIndex = 0; 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; }; /** * A node's post-order tail, shared by `_walkValue` / `_walkRule`: fire its `exit` * visitors, then — when printing — its printer, so the print step lives in one * place instead of being repeated in each walker. Rebinds the path onto `node` * (descending into children moved it). `writer` undefined = walk only, no print. * @param {Node} node the finished node * @param {Node | null} parent enclosing node * @param {number} index node's index within its sibling list * @param {CompiledVisitorBucket | undefined} b the node's visitor bucket * @param {PrintContext | undefined} writer print context when printing, else undefined */ const _exitNode = (node, parent, index, b, writer) => { if (b === undefined && writer === undefined) return; _currentNode = node; _currentParent = parent; _currentIndex = index; if (b !== undefined) { const x = b.exit; for (let i = 0; i < x.length; i++) x[i](A); } if (writer !== undefined) writer.printNode(node, A); }; // The letters those two sets' names start with, as a mask over `a`-`z`. Derived // from the sets, so a name added to either is covered without touching this. const _MATH_NAME_FIRST_LETTERS = (() => { let mask = 0; for (const name of MATH_FUNCTIONS) mask |= 1 << (name.charCodeAt(0) - 0x61); for (const name of STEPPED_FUNCTIONS) { mask |= 1 << (name.charCodeAt(0) - 0x61); } return mask; })(); /** * @param {number} start the name's first byte offset in `_input` * @returns {boolean} whether the name could be a math or stepped function's */ const _mayBeMathFunction = (start) => { // ASCII-lowercased in place: only a letter lands in `a`-`z` this way, and an // escape or a digit starts neither set's names. const first = _input.charCodeAt(start) | 0x20; if (first < 0x61 || first > 0x7a) return false; return (_MATH_NAME_FIRST_LETTERS & (1 << (first - 0x61))) !== 0; }; /** * 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. When * `writer` is given, fires this node's printer once its children and visitors are * done (post-order); the generic context owns everything the printer then does. * @param {Node} node component-value root * @param {Node | null} parent enclosing node * @param {number} index node's index within its sibling list * @param {PrintContext | undefined} writer print context when printing, else undefined */ const _walkValue = (node, parent, index, writer) => { const ty = _types[_nodeIndex(node)]; const b = _visitors[ty]; let skip = false; if (b !== undefined && b.enter.length !== 0) { _walkSkip = false; _currentNode = node; _currentParent = parent; _currentIndex = index; const e = b.enter; for (let i = 0; i < e.length; i++) e[i](A); skip = _walkSkip; _walkSkip = false; } // Everything below a math function is a math expression, `(…)` groups // included, so the depth rides the recursion the way `_inValue` does — and // still counts this node when its own printer runs, which is what joins the // children it applies to. let enteredMath = false; let enteredStepped = false; if (!skip && (ty === T_FUNCTION || ty === T_SIMPLE_BLOCK)) { const i0 = _nodeIndex(node); const vs = _listStarts[i0]; const ve = vs + _listLens[i0]; // Read off the source before the name is cut out of it: most calls in a // stylesheet start with a letter neither set uses, `var()` above all. if (ty === T_FUNCTION && _mayBeMathFunction(_starts[i0])) { const name = toLowerCaseIfNeeded(_input.slice(_starts[i0], _aux0[i0])); if (MATH_FUNCTIONS.has(name)) { enteredMath = true; _mathFunctionDepth++; } if (STEPPED_FUNCTIONS.has(name)) { enteredStepped = true; _steppedFunctionDepth++; } } const previousGradient = _inGradient; if ( ty === T_FUNCTION && // Every gradient name ends in `gradient`, so one code point rejects the // rest before the name is sliced. (_input.charCodeAt(_aux0[i0] - 1) | 0x20) === CC_LOWER_T && GRADIENT_FUNCTION_RE.test(_gradientName(i0)) ) { _inGradient = true; } for (let i = vs; i < ve; i++) { _walkValue(_nodeRef(_flat[i]), node, i - vs, writer); } _inGradient = previousGradient; } _exitNode(node, parent, index, b, writer); if (enteredMath) _mathFunctionDepth--; if (enteredStepped) _steppedFunctionDepth--; }; // A gradient, whatever it is prefixed with. const GRADIENT_FUNCTION_RE = /(?:^|-)(?:linear|radial|conic)-gradient$/i; /** * A function's name, for the gradient test. * @param {number} i0 the function's node index * @returns {string} the name as written */ const _gradientName = (i0) => _input.slice(_starts[i0], _aux0[i0]); /** * Set the flags an at-rule's own name decides, on the way into its prelude: * which of them is set is the same question wherever the walk asks it, and the * collected and streaming paths would otherwise have to agree by hand. The * caller puts them back — each rides its own recursion. * @param {number} i0 the at-rule's node index * @returns {void} */ const _enterAtRulePrelude = (i0) => { const atName = _input.slice(_starts[i0] + 1, _aux0[i0]); if (equalsLowerCase(atName, "property")) { _inPropertyRule = true; } else if (equalsLowerCase(atName, "function")) { _inFunctionRule = true; } else if (equalsLowerCase(atName, "font-feature-values")) { _inFeatureValuesRule = true; } if (equalsLowerCase(atName, "supports")) { _inSupportsPrelude = true; } // `@media` and `@container` are the two preludes whose `(…)` holds a media // feature, the only place the range spelling is a spelling of. else if ( equalsLowerCase(atName, "media") || equalsLowerCase(atName, "container") ) { _inMediaConditionPrelude = true; } }; /** * 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. When * `writer` is given, fires this node's printer once its children and visitors are * done (post-order); the generic context owns everything the printer then does. * @param {Node} node structural-tree root * @param {Node | null} parent enclosing node * @param {number} index node's index within its sibling list (declarations and child rules index independently) * @param {PrintContext | undefined} writer print context when printing, else undefined */ const _walkRule = (node, parent, index, writer) => { const i0 = _nodeIndex(node); const ty = _types[i0]; const b = _visitors[ty]; let skip = false; if (b !== undefined && b.enter.length !== 0) { _walkSkip = false; _currentNode = node; _currentParent = parent; _currentIndex = index; 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 = _listStarts[i0]; const pe = ps + _listLens[i0]; // A `@supports` prelude holds a declaration being *tested*, not applied, // so rewriting its value would change what the test asks — the flag rides // the recursion, since the conditions nest. const prevSupports = _inSupportsPrelude; const prevMedia = _inMediaConditionPrelude; const prevProperty = _inPropertyRule; const prevFunction = _inFunctionRule; const prevFeatureValues = _inFeatureValuesRule; if (ty === T_AT_RULE) _enterAtRulePrelude(i0); for (let i = ps; i < pe; i++) { _walkValue(_nodeRef(_flat[i]), node, i - ps, writer); } _inSupportsPrelude = prevSupports; _inMediaConditionPrelude = prevMedia; if (_recurseBlocks) { // Declarations then child rules — downstream consumers don't need them strictly interleaved in source order. const bi = _bodyIdx[i0]; if (bi !== 0) { const decls = _declBodies[bi - 1]; for (let i = 0; i < decls.length; i++) { _walkRule(decls[i], node, i, writer); } const ch = _ruleBodies[bi - 1]; for (let i = 0; i < ch.length; i++) _walkRule(ch[i], node, i, writer); } } _inPropertyRule = prevProperty; _inFunctionRule = prevFunction; _inFeatureValuesRule = prevFeatureValues; } else if (ty === T_DECLARATION) { const vs = _listStarts[i0]; const ve = vs + _listLens[i0]; // A declaration's value is a value context (its hashes are colors, not // ids); the flag rides the recursion so a hash at any depth (e.g. inside a // gradient) knows it, while selector-prelude hashes never see it set. const prev = _inValue; const prevDeclaration = _valueDeclaration; const prevCustom = _inCustomProperty; const prevSubstituted = _inSubstitutedValue; _inValue = true; if (_printing) _valueDeclaration = node; // A custom property's value is the one an engine hands back verbatim, so // its tokens print squeezed but as written (see `_inCustomProperty`). // `@property`'s `initial-value` is typed by its sibling `syntax`, so it // is opaque the same way: `0px` there is not the `0` a length accepts. // Only the custom property itself is what `rewriteCustomProperties` // asks for; those two stay opaque whatever it says. if ( (!_rewriteCustomProperties && _input.startsWith("--", _starts[i0])) || (_inPropertyRule && rangeEqualsLowerCase( _input, _starts[i0], _aux0[i0], "initial-value" )) || (_inFunctionRule && rangeEqualsLowerCase(_input, _starts[i0], _aux0[i0], "result")) ) { _inCustomProperty = true; } // So is a value holding a substitution: the engine keeps it as the token // stream it was written as until the substitution resolves, so no rewrite // inside one prints the value it would hand back. Read once per // declaration, before its children print — a `var()` sibling of the token // being rewritten has not been visited yet. // Every substitution is a function call, so a value with no `(` in it // cannot hold one — checked over the span in place, because slicing it // out allocates a string per declaration only to throw it away. if (_printing && _hasSubstitutionInSpan(_starts[i0], _ends[i0])) { _inSubstitutedValue = true; } for (let i = vs; i < ve; i++) { _walkValue(_nodeRef(_flat[i]), node, i - vs, writer); } _inValue = prev; _valueDeclaration = prevDeclaration; _inCustomProperty = prevCustom; _inSubstitutedValue = prevSubstituted; } } _exitNode(node, parent, index, b, writer); }; // === Nested-block streaming === // Top-level nodes already stream, but inside one big block nothing could be // released until the whole subtree finished. Enter an open rule once its block // has grown enough to be worth it, then walk and recycle each later child as it // completes: peak storage becomes the open path rather than the block. // Printing streams with it: a rule's `prelude{` is held back when the block // opens (`_streamOpen`), each finished child is emitted straight after it // (`_streamEmitChild`), and the `}` closes it (`_streamClose`) — so the printer // never assembles a parent from children a streamed body has already released. // The two things that need a whole block at once still hold: a longhand family // only merges in a block with no child rule, which is exactly the block this // declines to stream, and the last of a set of identical declarations is reached // by taking the earlier ones back out of the output rather than by looking ahead. // On for every walk, and not an option: a block under the threshold is collected // and walked in one batch exactly as it always was, so there is nothing to trade // off. It is still a flag because the standalone `parseA*` entry points hand back // a materialized tree and must not stream — `_setupParse` leaves it off for them, // and only `grammar` turns it on. let _streamBlocks = false; /** @type {Node | null} node already walked inline, so its sink only recycles */ let _streamWalked = null; // One frame per open block, indexed by depth rather than pushed and popped: // nesting is shallow and strictly LIFO, so an indexed store costs a write where // parallel stacks cost bounds and capacity checks on every rule. Activation // needs to reach every ancestor, which locals could not offer. // // A frame is written lazily, by `_streamPublishFrame`, and only where an // ancestor could still be reached for: just before a child rule — the one thing // that can open a block that reaches back up — and on the block's own growth // past the threshold. Every other block, which is every leaf rule in a // stylesheet, keeps its state in `consumeABlocksContentsInto`'s locals and never // touches these at all. A node is an id, so every slot but the two buffers is // numeric: typed columns keep a write off the GC's books. const _STREAM_MAX_DEPTH = 64; let _depth = 0; // The deepest frame this parse wrote, so the reset clears what was used rather // than all `_STREAM_MAX_DEPTH` slots — nesting is 1 deep in almost every sheet. let _frameHighWater = 0; /** @type {Int32Array} node id of the open rule */ const _frameRule = new Int32Array(_STREAM_MAX_DEPTH); const _frameMark = new Int32Array(_STREAM_MAX_DEPTH); const _frameFlatMark = new Int32Array(_STREAM_MAX_DEPTH); const _frameBodyMark = new Int32Array(_STREAM_MAX_DEPTH); const _frameActive = new Uint8Array(_STREAM_MAX_DEPTH); // Sibling counters, kept apart because the walk indexes declarations and child // rules independently (see `_walkRule`) and a streamed block must agree with it. const _frameDeclIndex = new Int32Array(_STREAM_MAX_DEPTH); const _frameRuleIndex = new Int32Array(_STREAM_MAX_DEPTH); // Whether a finished child of this frame is walked at all: `skipChildren()` and // `recurseBlocks: false` both decline them, folded into one test per child. const _frameWalk = new Uint8Array(_STREAM_MAX_DEPTH); // What the block buffered before it activated. `consumeABlocksContentsInto` // collects into these very arrays, so a descendant that crosses the threshold // first can still drain what its ancestors hold. /** @type {(Declaration[] | null)[]} */ const _frameDecls = Array.from({ length: _STREAM_MAX_DEPTH }, () => null); /** @type {(Rule[] | null)[]} */ const _frameRules = Array.from({ length: _STREAM_MAX_DEPTH }, () => null); const _frameParentIndex = new Int32Array(_STREAM_MAX_DEPTH); // The `@property` / `@function` / `@font-feature-values` state this frame was // entered under. A streamed block outlives the call that set it, so it is // restored when the frame closes. const _framePrevProperty = new Uint8Array(_STREAM_MAX_DEPTH); const _framePrevFunction = new Uint8Array(_STREAM_MAX_DEPTH); const _framePrevFeatureValues = new Uint8Array(_STREAM_MAX_DEPTH); // Printing a streamed block: where its first item landed in the output, how deep // its opener sits in the writer's pending stack, and the direct declarations it // has emitted so far — the last of a set of identical ones is the only one that // can be read, and the earlier are taken back as each later one arrives. const _frameFirstChunk = new Int32Array(_STREAM_MAX_DEPTH); const _framePendingDepth = new Int32Array(_STREAM_MAX_DEPTH); /** @type {(Map | null)[]} */ const _frameSeenDeclarations = Array.from( { length: _STREAM_MAX_DEPTH }, () => null ); // The child rules a streamed block has emitted, by their printed text: the last // of a set of identical ones is the only one read, and the earlier are taken // back as each later one arrives. /** @type {(Map | null)[]} */ const _frameSeenRules = Array.from({ length: _STREAM_MAX_DEPTH }, () => null); // The openers enclosing each streamed depth, joined: what a rule there is read // under, so one elsewhere under the same conditions keys the same. /** @type {(string | null)[]} */ const _frameChainKey = Array.from({ length: _STREAM_MAX_DEPTH }, () => null); /** * A rule the printer has written, at `[at, at + len)` of the piece holding it. * `key` is the text it printed to and `scope` what encloses it; a later rule * with both the same is this one written again. Neither is rewritten as the span * travels outward — only `scope` is replaced, by one standing for more — so the * collected and the streamed path split a rule the same way, which is what makes * the two agree on what a duplicate is. * @typedef {{ scope: RuleScope, key: string, at: number, len: number }} RuleSpan * @typedef {{ prefix: string, rules: Map, inner: Map }} RuleScope * @typedef {{ bodyAt: number, prelude: string, keyPrelude: string, qualified: boolean, spans: RuleSpan[] }} BlockSpans */ // Where each finished block's rules landed in it, youngest last — by print // order, since a node id is a slot the next top-level node reuses. /** @type {BlockSpans[]} */ const _blockSpans = []; const _NO_RULE_SPANS = /** @type {RuleSpan[]} */ ( /** @type {unknown} */ (Object.freeze([])) ); const _NO_BLOCK_SPANS = /** @type {BlockSpans[]} */ ( /** @type {unknown} */ (Object.freeze([])) ); // Stands in for a block node that carries no rules of its own, so a parent // still takes exactly one entry off for each of its non-declaration children. /** @type {BlockSpans} */ const _NO_BLOCK_ENTRY = Object.freeze({ bodyAt: -1, prelude: "", keyPrelude: "", qualified: false, spans: _NO_RULE_SPANS }); // The same for a qualified rule, which is still one rule of its own however // little it can say about what it nests. /** @type {BlockSpans} */ const _NO_BLOCK_ENTRY_QUALIFIED = Object.freeze({ bodyAt: -1, prelude: "", keyPrelude: "", qualified: true, spans: _NO_RULE_SPANS }); /** * @typedef {{ piece: number, text: string, spans: RuleSpan[], empties: number }} TakenPiece */ // Every rule taken so far, sheet-wide, by what encloses it and then by the text // it printed to — neither of which changes once written (see `RuleSpan`). /** @type {Map | null} */ let _ruleScopes = null; /** @type {RuleScope | null} */ let _rootRuleScope = null; /** * The scope for `prefix`, made once and shared — so a chain reached a level at a * time and the same one reached in a single step are the one scope. * @param {string} prefix what encloses the rules in it, "" at the top level * @returns {RuleScope} the scope */ const _ruleScopeFor = (prefix) => { let scopes = _ruleScopes; if (scopes === null) { scopes = new Map(); _ruleScopes = scopes; } let scope = scopes.get(prefix); if (scope === undefined) { scope = { prefix, rules: new Map(), inner: new Map() }; scopes.set(prefix, scope); } return scope; }; /** * The top-level scope, where a rule nothing encloses lands. * @returns {RuleScope} the root scope */ const _rootScope = () => { let root = _rootRuleScope; if (root === null) { root = _ruleScopeFor(""); _rootRuleScope = root; } return root; }; /** * The scope a scope's rules land in once `head` encloses them. Memoized on the * scope, so the join runs once per block rather than once per rule in it. * @param {RuleScope} scope the scope the rules are in now * @param {string} head what now encloses them * @returns {RuleScope} the enclosing scope */ const _enclosingRuleScope = (scope, head) => { if (head.length === 0) return scope; let inner = scope.inner.get(head); if (inner === undefined) { inner = _ruleScopeFor(`${head}${scope.prefix}`); scope.inner.set(head, inner); } return inner; }; // CSS Cascade 5 §6.4.1: an `!important` declaration is read from the earliest // layer, so a copy in a later `@layer {` makes nothing dead. let _anonymousLayers = 0; const BARE_AT_RULE_RE = /^@([^\s({;]+)\s*\{$/; /** * What an opener contributes to the key the rules under it are read by. The * name is compared unescaped, since `@l\61yer {` opens one of these too. * @param {string} opener the block's prelude, its `{` included * @returns {string} the opener, or a token no other occurrence shares */ const _openerKey = (opener) => { const bare = BARE_AT_RULE_RE.exec(opener); return bare !== null && toLowerCaseIfNeeded(unescapeIdentifier(bare[1])) === "layer" ? `\u0000${++_anonymousLayers}@layer{` : opener; }; // The named layer blocks a streamed block has emitted, by their opener: a later // one of the same name is folded into the piece the first went out as. /** @type {(Map | null)[]} */ const _frameSeenLayers = Array.from({ length: _STREAM_MAX_DEPTH }, () => null); /** @type {PrintContext | undefined} the print context while streaming, else undefined */ let _streamWriter; // A block earns the streaming machinery only once it holds enough to be worth // releasing. `_nodeCount` restarts at each top-level node, so // `_nodeCount - mark` is exactly how far this block has grown; under the // threshold `consumeABlocksContentsInto` collects into its own arrays exactly as // it does when nothing streams, and the rule is walked once, in one batch. // Tuned: a block that crosses only just, near its own `}`, activates for almost // nothing back, so the threshold sits above the few-hundred-rule `@media` a // stylesheet actually tends to hold. It bounds what is ever buffered, and even // so that is a fraction of the block it replaces. const _STREAM_MIN_NODES = 16384; // `_nodeCount` never reaches this, so a non-streaming block's per-child growth // check is one compare that can never fire — no second code path to maintain. const _STREAM_NO_LIMIT = 0x7fffffff; // How many finished bodies a streamed block lets pile up before handing them // back in one go (see `_streamOnNode`). const _STREAM_BODY_SLACK = 64; /** * Make an open block's frame readable, so a descendant that crosses the * threshold can enter and drain it on the way in. Called at most once per block, * and only by blocks that could still need it. * @param {number} d the block's depth * @param {Rule | undefined} rule the rule whose block this is * @param {number} markNode `_nodeCount` when the block opened * @param {number} markFlat `_flatTop` when the block opened * @param {Declaration[]} decls the block's declarations so far * @param {Rule[] | null} rules the block's child rules so far */ const _streamPublishFrame = (d, rule, markNode, markFlat, decls, rules) => { _frameRule[d] = _nodeIndex(/** @type {Rule} */ (rule)); _frameMark[d] = markNode; _frameFlatMark[d] = markFlat; _frameActive[d] = 0; // The very arrays the block is collecting into, so a descendant can drain what // it buffered without it being on the stack. _frameDecls[d] = decls; _frameRules[d] = rules; }; /** * Hold back the opener of a streamed rule — `prelude{`, which its prelude * already determines. Held rather than emitted so a block that prints to nothing * can still be dropped whole at its `}`. * @param {number} d the block's depth * @param {Rule} rule the open rule */ const _streamOpen = (d, rule) => { const writer = /** @type {PrintContext} */ (_streamWriter); const minify = writer.options.mode === "minify"; // Bind the whole path, not just the node: a prelude can be read in terms of // what encloses it — a keyframe selector is `from` only inside `@keyframes` — // and `_streamEnterRule` binds the parent only when the rule has an `enter` // visitor to run, which printing on its own does not. _currentNode = rule; _currentParent = d === 0 ? null : _nodeRef(_frameRule[d - 1]); _currentIndex = _frameParentIndex[d]; const opener = `${_rulePrelude(A, writer, minify)}${minify ? "" : " "}{`; // A streamed rule writes straight through, so the rule held back for a join // lands before its opener — after the opener is read, since taking one clears // the store the prelude's own children were printed into. _flushTopLevel(writer); _framePendingDepth[d] = writer.pushPending(opener); _frameSeenDeclarations[d] = null; _frameSeenRules[d] = null; _frameSeenLayers[d] = null; _frameFirstChunk[d] = writer.markCut(); const above = d === 0 ? "" : _frameChainKey[d - 1]; _frameChainKey[d] = !minify || above === null ? null : `${above}${_openerKey(opener)}`; // The outermost opener carries the whole run's source anchor: it is the one // the kept comments precede and the mapping points at. if (d === 0) { const i0 = _nodeIndex(rule); const start = _starts[i0]; const loc = _locConverter.get(start); writer.anchorPending(start, loc.line - 1, loc.column); } }; /** * Emit one finished child of a streamed block, and with it every opener still * held back — something inside them printed, so they are not empty after all. * @param {number} d the block's depth * @param {Node} child the finished child */ const _streamEmitChild = (d, child) => { const writer = /** @type {PrintContext} */ (_streamWriter); const text = writer.get(child); // A declaration the minifier dropped prints to nothing, and an empty rule to // nothing as well: neither makes the block it sits in non-empty. if (text.length === 0) return; writer.flushPending(); const minify = writer.options.mode === "minify"; // Taking a dead rule or declaration back and folding two named layer blocks // into one are two options, so each gather below runs only where its own // is on. const dropping = minify && _transforms.removeDeadRules; const merging = minify && _transforms.mergeRules; if (!minify || _nodeTypeOf(child) !== T_DECLARATION) { const body = minify ? text : `\n${text}`; // Drained here and nowhere else: every path out of this branch has to take // the child's entry with it, or the next child reads it as its own. const own = _drainTopLevelSpans(child); // The one child rule that can be taken back: a prefixed one an unprefixed // twin later in this block would make dead weight. Its parent never // assembles a body here, so the piece is what the twin drops. const candidate = _prefixDropCandidate; if (candidate !== null && candidate.node === child) { _prefixDropCandidate = null; const scope = _prefixScope(_nodeRef(_frameRule[d])); const pending = scope.pending; if (pending !== null && pending.delete(candidate.signature)) { if (scope.retractable === null) scope.retractable = new Map(); scope.retractable.set( candidate.signature, writer.emitRetractable(body) ); return; } } // A named layer block is one layer however far its siblings stand apart, so // a repeat of one folds into the first — `_mergeNamedLayerBlocks` is the // same gather for a block whose body is assembled in one go. A sibling // naming a layer any other way writes into one of these, and the order // within a layer is the cascade's, so nothing folds back over it. const positional = merging && POSITIONAL_AT_RULE_RE.test(text); if (positional) { const opener = _namedLayerOpener(text); if (opener === null) { if (_frameSeenLayers[d] !== null) { /** @type {Map} */ (_frameSeenLayers[d]).clear(); } } else { let layers = _frameSeenLayers[d]; if (layers === null) { layers = new Map(); _frameSeenLayers[d] = layers; } const first = layers.get(opener); const deep = _opensNestedLayer(text, opener); _noteLayerBlock( layers, opener.slice(NAMED_LAYER_OPENER_HEAD, -1).trim(), deep ); const chain = _frameChainKey[d]; const inner = chain === null ? null : _topLevelSpans(own, text); if (first !== undefined && !(deep && first.subtree)) { const grown = first.taken; if (grown === undefined || inner === null || chain === null) { writer.foldIntoRetractable(first.at, text.slice(opener.length)); return; } // The body lands in front of the piece's own `}`, past everything // already keyed in it, so only what arrives now needs moving. const base = grown.text.length - 1 - opener.length; grown.text = `${grown.text.slice(0, -1)}${text.slice(opener.length)}`; writer.rewriteRetractable(grown.piece, grown.text); for (const span of inner) span.at += base; _noteSpans(writer, grown, inner, chain); return; } const at = writer.emitRetractable(body); layers.set(opener, { at, subtree: false, taken: inner === null || chain === null ? undefined : _takeDeduped(writer, at, body, inner, own, chain) }); return; } } // The same rule twice in a block is read once: the later writes the same // declarations to the same elements and wins the tie, so it takes the // earlier back — as each identical declaration does below. if (dropping && !positional) { const chain = _frameChainKey[d]; const piece = writer.emitRetractable(body); if (chain !== null) { // Sheet-wide, keyed on what the rule is read under: the same map the // collected path registers into, so the two agree across a sheet that // takes both. const spans = _topLevelSpans(own, text); if (spans !== null) { _takeDeduped(writer, piece, text, spans, own, chain); return; } } let rules = _frameSeenRules[d]; if (rules === null) { rules = new Map(); _frameSeenRules[d] = rules; } const previous = rules.get(text); if (previous !== undefined) writer.retract(previous); rules.set(text, piece); return; } writer._emit(body); return; } // Only the last of a set of identical declarations can be read, so each one // takes back the one it repeats — which is the only thing here that needs a // piece of its own. Keyed on the printed text, as the collected printer keys // its own pass. if (!dropping) { writer._emit(text); return; } const at = writer.emitRetractable(text); let seen = _frameSeenDeclarations[d]; if (seen === null) { seen = new Map(); _frameSeenDeclarations[d] = seen; } const previous = seen.get(text); if (previous !== undefined) writer.retract(previous); seen.set(text, at); }; /** * Close a streamed rule: emit its `}`, or drop the whole rule when its block * printed to nothing and the block itself carries no meaning. * @param {number} d the block's depth * @param {Rule} rule the rule being closed */ const _streamClose = (d, rule) => { const writer = /** @type {PrintContext} */ (_streamWriter); const minify = writer.options.mode === "minify"; _frameSeenDeclarations[d] = null; _frameSeenRules[d] = null; _frameSeenLayers[d] = null; if (writer.isPending(_framePendingDepth[d])) { // Nothing inside printed. An empty rule paints nothing, so dropping it // leaves the cascade as it was — but only where the block itself carries no // meaning (see `DROPPABLE_WHEN_EMPTY_AT_RULES`). const i0 = _nodeIndex(rule); if ( minify && _transforms.removeDeadRules && // Not while a `@namespace` after it could still be read: taking this // rule out would move one up to where the engine honours it. !(d === 0 && _namespacePrologueOpen) && (_types[i0] === T_QUALIFIED_RULE || DROPPABLE_WHEN_EMPTY_AT_RULES.has( _input.slice(_starts[i0] + 1, _aux0[i0]).toLowerCase() )) ) { writer.dropPending(); return; } writer.flushPending(); } else if (minify) { // The `;` the last declaration carries is the one a `}` makes redundant. writer.dropTrailing(_frameFirstChunk[d], CC_SEMICOLON); } writer._emit(minify ? "}" : "\n}"); }; /** * Fire an open rule's `enter` and walk its prelude — both are final at `{`. * @param {Rule} rule the open rule * @param {Node | null} parent enclosing rule * @param {number} index the rule's index among its siblings * @param {number} d the block's frame depth * @returns {boolean} whether `skipChildren()` declined its children */ const _streamEnterRule = (rule, parent, index, d) => { const i0 = _nodeIndex(rule); const ty = _types[i0]; const b = _visitors[ty]; if (b !== undefined && b.enter.length !== 0) { _walkSkip = false; _currentNode = rule; _currentParent = parent; _currentIndex = index; const e = b.enter; for (let i = 0; i < e.length; i++) e[i](A); const skip = _walkSkip; _walkSkip = false; if (skip) return true; } const ps = _listStarts[i0]; const pe = ps + _listLens[i0]; const prevSupports = _inSupportsPrelude; const prevMedia = _inMediaConditionPrelude; // Body-scoped, unlike the two prelude flags: the block streams past this call, // so `_streamConsumeBlock` puts them back once its `}` is reached. _framePrevProperty[d] = _inPropertyRule ? 1 : 0; _framePrevFunction[d] = _inFunctionRule ? 1 : 0; _framePrevFeatureValues[d] = _inFeatureValuesRule ? 1 : 0; if (ty === T_AT_RULE) _enterAtRulePrelude(i0); for (let i = ps; i < pe; i++) { _walkValue(_nodeRef(_flat[i]), rule, i - ps, _streamWriter); } _inSupportsPrelude = prevSupports; _inMediaConditionPrelude = prevMedia; return false; }; /** * A block at `d` has grown past the threshold. Every ancestor still buffering * must be entered first, outermost in, so `enter` stays in source order; each * one then walks what it buffered (its two lists are each source-ordered, so * merging on start offset restores source order) before the deeper level does. * @param {number} d depth of the block that crossed the threshold */ const _streamActivate = (d) => { for (let k = 0; k <= d; k++) { if (_frameActive[k] === 1) continue; _frameActive[k] = 1; const rule = /** @type {Rule} */ (_nodeRef(_frameRule[k])); // Claim the body slot before `enter` can read it: a streamed rule hands its // children to the visitors, so its own block has to report as empty rather // than as `null`, which is how a rule with no block at all reads. Everything // a later child claims sits above this, so the slots come back per child. _setBody(rule, _EMPTY_LIST, _EMPTY_LIST); _frameBodyMark[k] = _declBodies.length; // The enclosing block has just been activated and drained, so its rule // counter is exactly how many siblings precede this one. const parentIndex = k === 0 ? 0 : _frameRuleIndex[k - 1]++; _frameParentIndex[k] = parentIndex; const skipped = _streamEnterRule( rule, k === 0 ? null : _nodeRef(_frameRule[k - 1]), parentIndex, k ); if (_streamWriter !== undefined) _streamOpen(k, rule); const walk = !skipped && _recurseBlocks; _frameWalk[k] = walk ? 1 : 0; const decls = _frameDecls[k]; const rules = _frameRules[k]; _frameDecls[k] = null; _frameRules[k] = null; const dn = decls === null ? 0 : decls.length; const rn = rules === null ? 0 : rules.length; // Both counters end at what was buffered, so the children still to come // carry on from there whether or not this block's body was walked. _frameDeclIndex[k] = dn; _frameRuleIndex[k] = rn; if (!walk) continue; // Merging the two source-ordered lists on start offset restores source // order, while each child keeps the sibling index the batch walk gives it. let di = 0; let ri = 0; while (di < dn || ri < rn) { /** @type {Node} */ let child; if ( ri >= rn || (di < dn && _starts[_nodeIndex(/** @type {Declaration[]} */ (decls)[di])] < _starts[_nodeIndex(/** @type {Rule[]} */ (rules)[ri])]) ) { child = /** @type {Declaration[]} */ (decls)[di]; _walkRule(child, rule, di, _streamWriter); di++; } else { child = /** @type {Rule[]} */ (rules)[ri]; _walkRule(child, rule, ri, _streamWriter); ri++; } if (_streamWriter !== undefined) { _streamEmitChild(k, child); _streamWriter.dropStore(); } } } // Only the deepest block may release: an ancestor's mark sits below the ids // its still-open descendants are using. if (_nodeCount > _peak) _peak = _nodeCount; if (_flatTop > _flatPeak) _flatPeak = _flatTop; _nodeCount = _frameMark[d]; _flatTop = _frameFlatMark[d]; }; /** * Per-child sink of an *activated* block: walk the finished child, then release * every id it used. Module-level (no per-rule closure) — the depth-indexed frame * carries the open rule, its sibling counters and its mark. A block only reaches * for this once it has activated, so a small block never calls it. * @param {Rule | Declaration} node the finished child */ const _streamOnNode = (node) => { const d = _depth - 1; // A child rule that streamed on its own already entered, walked and exited // inline and released its ids; it reaches the sink already finished. if (node === _streamWalked) { // Already entered, printed and closed itself inline, straight into the // output — there is nothing left of it to emit here. _streamWalked = null; } else if (_frameWalk[d] === 1) { const index = _nodeTypeOf(node) === T_DECLARATION ? _frameDeclIndex[d]++ : _frameRuleIndex[d]++; _walkRule(node, _nodeRef(_frameRule[d]), index, _streamWriter); if (_streamWriter !== undefined) { _streamEmitChild(d, node); _streamWriter.dropStore(); } } if (_nodeCount > _peak) _peak = _nodeCount; if (_flatTop > _flatPeak) _flatPeak = _flatTop; _nodeCount = _frameMark[d]; _flatTop = _frameFlatMark[d]; // The child is walked, so the body slots it and its subtree claimed go back // too — otherwise the one thing a streamed block still grew per child. Handed // back in batches: shortening the two arrays costs more than the compare, and // what a batch holds onto is a few dozen finished bodies. const bodyMark = _frameBodyMark[d]; if (_declBodies.length >= bodyMark + _STREAM_BODY_SLACK) { _declBodies.length = bodyMark; _ruleBodies.length = bodyMark; } }; /** * Consume a rule's block, streaming its children once the block grows past * `_STREAM_MIN_NODES`. A block that stays small is handed back materialized for * the ordinary walk, so it costs a collect and nothing else. * @param {TokenStream} ts token stream * @param {Rule} rule the rule whose block follows */ const _streamConsumeBlock = (ts, rule) => { const d = _depth; const blockStart = ts.next().start; ts.discard(); if (d >= _STREAM_MAX_DEPTH) { // Deeper than the frame table: fall back to the materializing path. consumeABlocksContentsInto(ts); const decls = _bcDecls; const rules = _bcRules; const c = ts.next(); ts.discard(); _setBody(rule, decls, rules); _setBlock(rule, blockStart); _setEnd(rule, c.type === TT_RIGHT_CURLY_BRACKET ? c.end : c.start); return; } // Nothing is written to the frame here: the block keeps its state in locals // and writes one only if it could still be read (see `_streamPublishFrame`), // so a rule whose block is all declarations costs what it always did. _depth = d + 1; if (_depth > _frameHighWater) _frameHighWater = _depth; consumeABlocksContentsInto(ts, undefined, d, rule); _depth = d; const streamed = _bcStreamed; const decls = _bcDecls; const rules = _bcRules; const close = ts.next(); const end = close.type === TT_RIGHT_CURLY_BRACKET ? close.end : close.start; ts.discard(); _setBlock(rule, blockStart); _setEnd(rule, end); if (!streamed) { // Never grew: hand the body back, so the ordinary walk visits it and // `A.declarations` / `A.childRules` still read it. _setBody(rule, decls, rules); return; } // Streamed: `_streamActivate` already claimed the body slot, empty. The exit // visitors still run here; the rule's own `}` follows them, where the printer // would have run for a rule printed in one piece. _exitNode( rule, d === 0 ? null : _nodeRef(_frameRule[d - 1]), _frameParentIndex[d], _visitors[_types[_nodeIndex(rule)]], undefined ); if (_streamWriter !== undefined) _streamClose(d, rule); _inPropertyRule = _framePrevProperty[d] === 1; _inFunctionRule = _framePrevFunction[d] === 1; _inFeatureValuesRule = _framePrevFeatureValues[d] === 1; _streamWalked = rule; }; /** @typedef {{ node: Node, offset: number, line: number | undefined, column: number | undefined, entry: RuleEntry, owned: boolean, own: BlockSpans | undefined }} HeldTopLevel one top-level rule held back for a join, with where it stood and what it recorded */ // The one top-level rule held back, so the next can be joined onto it (see // `_mergeAdjacentRules` — the same merge, across nodes the writer takes one at a // time). Null when nothing is held. /** @type {HeldTopLevel | null} */ let _heldTopLevel = null; // The named layer blocks the stylesheet's own children have emitted, by their // opener: a later one of the same name is folded into the piece the first went // out as, as `_frameSeenLayers` does inside a block. /** @type {Map | null} */ let _seenTopLevelLayers = null; const AT_RULE_NAME_RE = /^@([^\s({;]+)/; /** * The name an at-rule prelude opens with, lowercased. * @param {string} prelude the prelude, `@` included * @returns {string} the name, or "" when it names none */ const _atRuleName = (prelude) => { const match = AT_RULE_NAME_RE.exec(prelude); return match === null ? "" : match[1].toLowerCase(); }; // Stands for a top-level node that is one rule whatever its text ends up being, // a qualified rule joined onto the one beside it included. /** @type {BlockSpans} */ const _WHOLE_TEXT_SPANS = Object.freeze({ bodyAt: 0, prelude: "", keyPrelude: "", qualified: true, spans: _NO_RULE_SPANS }); /** * What a finished top-level node recorded while it printed. A node id is a slot * the next top-level node reuses, so this reads at the one moment the node still * names itself — the writer holds a rule back a node, and by then it does not. * @param {Node} node the top-level node, freshly printed * @returns {BlockSpans | undefined} its entry, or undefined when it records none */ const _drainTopLevelSpans = (node) => { // Its children's entries were spliced off as their parent assembled its body, // so what is left is this node's own. // A block a streamed parent emitted leaves its entry behind — nothing spliced // it off — so only a lone entry is this node's own. const own = _blockSpans.length === 1 ? _blockSpans[0] : undefined; _blockSpans.length = 0; if (own !== undefined) return own; return _nodeTypeOf(node) === T_QUALIFIED_RULE ? _WHOLE_TEXT_SPANS : undefined; }; /** * The rules a finished top-level node carries, where they land in its text. A * qualified rule is one rule however it nests; an at-rule states conditions its * body is read under, so its spans come out with its prelude on their keys. * @param {BlockSpans | undefined} own what it recorded, from {@link _drainTopLevelSpans} * @param {string} text its printed text * @returns {RuleSpan[] | null} the spans, or null when it carries no rule */ const _topLevelSpans = (own, text) => { if (own === undefined) return null; if (own === _WHOLE_TEXT_SPANS) { return [{ scope: _rootScope(), key: text, at: 0, len: text.length }]; } /** @type {RuleSpan[]} */ const out = own.qualified ? [{ scope: _rootScope(), key: text, at: 0, len: text.length }] : []; for (const span of own.spans) { out.push({ scope: _enclosingRuleScope(span.scope, own.keyPrelude), key: span.key, at: own.bodyAt + span.at, len: span.len }); } return out; }; /** * Take a top-level node, and with it every rule an identical later one has yet * to make dead. A rule already taken whose key comes round again is read for * nothing, so the piece holding it is written again without it — pieces are * joined only when the sheet ends, so one taken long ago is still reachable. * @param {PrintContext} writer the print context * @param {number} piece the piece the node was taken as * @param {string} text the node's printed text * @param {RuleSpan[]} spans where its rules land in it * @param {BlockSpans | undefined} own what the node recorded while printing * @param {string} chain what encloses it, when it is not a top-level node * @returns {TakenPiece} the piece, for a fold that grows it */ const _takeDeduped = (writer, piece, text, spans, own, chain) => { // A block the cuts empty is one written empty here, which the printer drops — // but only where the block itself carries no meaning. const empties = own !== undefined && own !== _WHOLE_TEXT_SPANS && _transforms.removeDeadRules && (own.qualified || (own.prelude.charCodeAt(0) === CC_AT_SIGN && DROPPABLE_WHEN_EMPTY_AT_RULES.has(_atRuleName(own.prelude)))) ? own.bodyAt : -1; /** @type {TakenPiece} */ const taken = { piece, text, spans: [], empties }; _noteSpans(writer, taken, spans, chain); return taken; }; /** * Note the rules a piece carries, taking back the piece each identical earlier * one went out as. Called again for the same piece as a fold grows it. * @param {PrintContext} writer the print context * @param {TakenPiece} taken the piece they landed in * @param {RuleSpan[]} spans where they land in it * @param {string} chain what encloses them, "" at the top level * @returns {void} */ const _noteSpans = (writer, taken, spans, chain) => { // Every span joins the piece before any cutting: a cut here moves what // `taken.spans` holds, and one not yet pushed would keep its old offset. for (const span of spans) { if (chain.length !== 0) span.scope = _enclosingRuleScope(span.scope, chain); taken.spans.push(span); } for (const span of spans) { const rules = span.scope.rules; const before = rules.get(span.key); if (before !== undefined) _cutSpan(writer, before.taken, before.span); rules.set(span.key, { taken, span }); } }; /** * Write a piece again without one rule, and move the spans after it back by * what went, so a later cut in the same piece still names its own text. * @param {PrintContext} writer the print context * @param {TakenPiece} taken the piece the rule was taken in * @param {RuleSpan} span the rule to cut * @returns {void} */ const _cutSpan = (writer, taken, span) => { if (span.len === 0) return; let from = span.at; let len = span.len; // The rule stood last in its block, so the `;` in front of it is one the // printer drops itself — it goes with the rule rather than being left behind. if ( taken.text.charCodeAt(from + len) === CC_RIGHT_CURLY && taken.text.charCodeAt(from - 1) === CC_SEMICOLON ) { from -= 1; len += 1; } taken.text = `${taken.text.slice(0, from)}${taken.text.slice(from + len)}`; if (taken.empties !== -1 && taken.text.length === taken.empties + 1) { taken.text = ""; } writer.rewriteRetractable(taken.piece, taken.text); const end = from + len; for (const other of taken.spans) { if (other === span) continue; // One inside what went is gone with it. One that held it still describes // what is left of it, which a later copy of it restates in full, so it // only shrinks by what went. if (other.at >= from && other.at + other.len <= end) other.len = 0; else if (other.at <= from && other.at + other.len >= end) other.len -= len; else if (other.at > from) other.at -= len; } span.len = 0; }; /** * Hand one finished top-level node to the writer, gathering a named `@layer` * block into the first sibling of its name — they are one layer however far * apart they stand, and what separates them is in another layer or in none, * ordered against these by the cascade rather than by where it sits, so moving * the later body up is not a move the cascade can see. `_mergeNamedLayerBlocks` * is the same gather one block down. The node goes out with whatever text it * ended up carrying, so a block a join already grew is the one gathered into. * @param {PrintContext} writer the print context * @param {Node} node the top-level node * @param {number} offset the node's source offset * @param {number | undefined} line 0-based source line of the node's start * @param {number | undefined} column 0-based source column of the node's start * @param {string} text the text the node goes out as * @param {BlockSpans=} own what it recorded while printing * @returns {void} */ const _emitTopLevel = (writer, node, offset, line, column, text, own) => { const printing = writer.options.mode === "minify"; // Folding two named layer blocks into one, and taking back a rule an // identical later one makes dead, each answer to their own option. const minify = printing && _transforms.removeDeadRules; const opener = printing && _transforms.mergeRules ? _namedLayerOpener(text) : null; // Nothing folds back over a kept comment — it was written above what follows // it — nor over a sibling naming a layer any other way, since it writes into // one of these and the order within a layer is the cascade's. if ( _seenTopLevelLayers !== null && (writer.hasInsertBefore(offset) || (opener === null && minify && POSITIONAL_AT_RULE_RE.test(text))) ) { _seenTopLevelLayers.clear(); } if (opener === null) { // Taking back a rule an identical later one makes dead is a rewrite, which // is minifying's to make: beautifying hands back every rule it was given. const spans = minify ? _topLevelSpans(own, text) : null; if (spans === null) { writer.take(node, offset, line, column, text); return; } _takeDeduped( writer, writer.takeRetractable(node, offset, line, column, text), text, spans, own, "" ); return; } if (_seenTopLevelLayers === null) _seenTopLevelLayers = new Map(); const first = _seenTopLevelLayers.get(opener); const deep = _opensNestedLayer(text, opener); _noteLayerBlock( _seenTopLevelLayers, opener.slice(NAMED_LAYER_OPENER_HEAD, -1).trim(), deep ); if (first === undefined || (deep && first.subtree)) { _seenTopLevelLayers.set(opener, { at: writer.takeRetractable(node, offset, line, column, text), subtree: false }); return; } // Both bodies keep their order, so the layer reads as it was written. writer.foldIntoRetractable(first.at, text.slice(opener.length)); }; /** * Emit the rule held back for a join, as it stands. Its own text is what a * grown join left it with, and what it recorded while printing goes with it. * @param {PrintContext} writer the print context * @param {HeldTopLevel} held the held rule * @returns {void} */ const _emitHeld = (writer, held) => { _emitTopLevel( writer, held.node, held.offset, held.line, held.column, held.entry.text, held.own ); }; /** * Hand a finished top-level node to the writer, holding a qualified rule back * one node so an adjacent one printing the same block can join its selectors. * @param {Node} node the top-level node * @param {PrintContext} writer the print context * @param {number} offset the node's source offset * @param {number=} line 0-based source line of the node's start, when mapping * @param {number=} column 0-based source column of the node's start, when mapping * @returns {void} */ const _takeTopLevel = (node, writer, offset, line, column) => { const held = _heldTopLevel; // The node's own text first — taking the held one clears the store it sits in. const own = writer.get(node); // Read where the node stands, not where it is emitted: the writer holds a // rule back one node, by when the prologue has closed behind it. A rule taken // back from in front of a `@namespace` would move one up into a live // position, so one standing there is never offered. const drained = _drainTopLevelSpans(node); const spans = _namespacePrologueOpen ? undefined : drained; const entry = _ruleEntryOf(node, own); // A prefixed rule an unprefixed twin would make dead weight goes out as a piece // of its own, so the twin can take it back from wherever it stands rather than // only from the next rule. It is not offered for joining first: a rule that may // still be taken back must be one piece and one rule. const candidate = _prefixDropCandidate; _prefixDropCandidate = null; if (candidate !== null && candidate.node === node) { if (held !== null) { _heldTopLevel = null; _emitHeld(writer, held); } const at = writer.takeRetractable(node, offset, line, column, own); const scope = _prefixScope(null); if (scope.retractable === null) scope.retractable = new Map(); scope.retractable.set(candidate.signature, at); return; } if (entry.prelude === -1) { // Not a rule this can join: flush what is held, then take it as it stands. // A node printing nothing leaves the two around it adjacent in the output. // Its kept comments stay queued: they land where it stood, which is after // the held rule, and until then they block a join across that gap. if (own.length === 0) return; if (held !== null) { _heldTopLevel = null; _emitHeld(writer, held); } _emitTopLevel(writer, node, offset, line, column, own, spans); return; } // A kept comment between the two was written above the second rule. if (held !== null && !writer.hasInsertBefore(offset)) { const merged = _joinRuleEntries(held.entry, entry, held.owned); if (merged !== null) { held.entry = merged; held.owned = true; // The join rewrote its text, so what it recorded no longer names its // own offsets — it is one rule, and only that. held.own = _WHOLE_TEXT_SPANS; return; } } if (held !== null) { _emitHeld(writer, held); } // What was written above this rule is emitted above it now, so the next // node's check sees only the gap between the two — and nothing folds back // over it, so the layer blocks above it stop being ones to gather into. if (_seenTopLevelLayers !== null && writer.hasInsertBefore(offset)) { _seenTopLevelLayers.clear(); } writer.flushInsertsBefore(offset); _heldTopLevel = { node, offset, line, column, entry, owned: false, own: spans }; }; /** * Emit the held top-level rule, if any. Called once the stylesheet is consumed. * @param {PrintContext=} writer the print context * @returns {void} */ const _flushTopLevel = (writer) => { const held = _heldTopLevel; if (held === null) return; _heldTopLevel = null; if (writer !== undefined) { _emitHeld(writer, held); } }; /** * Emit a whole `block-contents` print: the held nodes are one declaration list, * so they go through the same composition a rule's block does and land as one * piece. The list is what a `style=""` attribute holds, so it is small. * @param {(Rule | Declaration)[]} nodes the top-level nodes, in source order * @param {PrintContext} writer the print context holding each one's text * @returns {void} */ const _emitBlockContents = (nodes, writer) => { /** @type {Declaration[]} */ const decls = []; /** @type {Rule[]} */ const rules = []; for (const node of nodes) { if (_types[_nodeIndex(node)] === T_DECLARATION) { decls.push(/** @type {Declaration} */ (node)); } else { rules.push(/** @type {Rule} */ (node)); } } const minify = writer.options.mode === "minify"; const { body } = _composeBlockBody( A, decls, rules.length === 0 ? null : rules, writer, minify, minify ? "" : "\n", null ); // The list prints as one piece, so it anchors as one: at where it starts. A // declaration list is what an attribute holds, so that is the whole of it. const offset = nodes.length === 0 ? 0 : A.start(nodes[0]); const at = writer.mapWanted ? _locConverter.get(offset) : undefined; // A leading separator only ever parts one item from the one before it, and at // top level there is nothing before the first. writer.take( undefined, offset, at === undefined ? undefined : at.line - 1, at === undefined ? undefined : at.column, minify ? body : body.slice(1) ); }; /** * Note that a top-level node stood here. Called once per top-level node, after * it has printed and before the next one does, so the empty-rule drop knows * whether taking a rule out could still move a `@namespace` up. * @param {Rule | Declaration} node the finished top-level node * @returns {void} */ const _closeNamespacePrologue = (node) => { if (_namespacePrologueOpen && _types[_nodeIndex(node)] === T_QUALIFIED_RULE) { _namespacePrologueOpen = false; } }; /** * The `grammar` streaming sink: walk one top-level node, then recycle the * buffers for the next. * @param {Rule | Declaration} node top-level node * @param {PrintContext=} writer print context when printing, else undefined */ const _walkTopLevel = (node, writer) => { if (node === _streamWalked) { _streamWalked = null; // A streamed rule's text is already out, so nothing can take it back — and // the node is about to be recycled, which its candidacy must not outlive. _prefixDropCandidate = null; _closeNamespacePrologue(node); _recycleTopLevel(node); return; } _walkRule(node, null, 0, writer); // A declaration list is composed as a whole, so its nodes are kept — their // printed text stays reachable through the writer, which recycling would lose. if (_blockContentsNodes !== null) { _blockContentsNodes.push(node); return; } // The walk fired each node's printer into the context; hand this finished // top-level node to the writer with its source position — it flushes any kept // comments before it, records the source-map anchor and appends its text (the // loc converter is 1-based line / 0-based column; source maps are 0-based). if (writer !== undefined) { // Only the start is wanted, and only for a map: `loc` would walk to the end // as well and box both, per top-level node, for a line nobody reads. if (writer.mapWanted) { const start = _locConverter.get(_starts[_nodeIndex(node)]); _takeTopLevel(node, writer, A.start(node), start.line - 1, start.column); } else { _takeTopLevel(node, writer, A.start(node)); } } _closeNamespacePrologue(node); // The held rule keeps its own entry, so the map is done with. _ruleEntry.clear(); if (_nodeCount > _peak) _peak = _nodeCount; if (_flatTop > _flatPeak) _flatPeak = _flatTop; _nodeCount = 0; _flatTop = 0; // Body indices recycle with node ids. _declBodies.length = 0; _ruleBodies.length = 0; }; /** * `_walkTopLevel` without the walk, for a visitor-less `process` call: the * walk's only effect is visitor dispatch, so just recycle the buffers. * @param {Rule | Declaration} node top-level node */ const _recycleTopLevel = (node) => { if (_nodeCount > _peak) _peak = _nodeCount; if (_flatTop > _flatPeak) _flatPeak = _flatTop; _nodeCount = 0; _flatTop = 0; _declBodies.length = 0; _ruleBodies.length = 0; }; // The store 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 _SHRINK_CAPACITY = 65536; // `@license` / `@preserve` mark a comment for preservation (terser keeps the // same annotations for JS); the JS-only `@cc_on` is not meaningful in CSS. const _KEEP_COMMENT_RE = /@(?:license|preserve)/i; /** * A comment worth carrying through minification, matching terser's default set: * a `/*!` banner (fast char-code check, no allocation) or a `@license` / * `@preserve` annotated comment. `/*#` covers the `sourceMappingURL` / * `sourceURL` pragmas, which are a link to drop, not a comment. * @param {string} src source text * @param {number} start comment start offset (at `/`) * @param {number} end comment end offset (past the closing `/`) * @returns {boolean} whether the comment survives minification */ const _isKeptComment = (src, start, end) => { const marker = src.charCodeAt(start + 2); const kept = _commentsKept; // A `/*#` pragma is a link to a source map rather than a comment, so the // banner level keeps it; a selector of the author's still decides it. if (marker === CC_NUMBER_SIGN && kept === "some") return true; if (typeof kept === "boolean") return kept; if (kept === "some") { return ( marker === CC_EXCLAMATION || _KEEP_COMMENT_RE.test(src.slice(start, end)) ); } // A pattern or a predicate of the author's, over the comment's own text — // which stands in for the banner rule rather than beside it, as terser's // `format.comments` does. return kept(src.slice(start + 2, end - 2)) === true; }; // Every rewrite on, which is what a print with no `transforms` option makes and // what the walk-only passes hold. Frozen, since it is shared rather than copied. /** @type {Required} */ const _ALL_TRANSFORMS = Object.freeze({ comments: "some", mergeLonghands: true, mergeRules: true, normalizeQuotes: true, reduceFunctions: true, removeDeadRules: true, shortenColors: true, shortenMediaQueries: true, shortenNumbers: true, shortenSelectors: true, shortenValues: true }); /** * The rewrites this print makes, resolved once: each name is what the options * set it to, and what `_ALL_TRANSFORMS` gives it where they set nothing — so no * read of the result asks whether the option was given. `comments` keeps * whatever it was given, which is still falsy only when it is `false`. * * Always a copy, and always made the one way: spreading the table and then * writing over a name it already has leaves the object's shape alone, so every * read of the result — and there is one per token — sees a single hidden class. * Handing the frozen table itself back where nothing is set would save this * object and cost each of those reads a second shape, `Object.freeze` giving a * frozen object a map of its own. * @param {CssTransformOptions | undefined} options the `transforms` option * @returns {Required} every rewrite resolved */ const _transformsFrom = (options) => { /** @type {EXPECTED_ANY} */ const out = { ..._ALL_TRANSFORMS }; if (options !== undefined) { for (const name of Object.keys(_ALL_TRANSFORMS)) { const value = /** @type {EXPECTED_ANY} */ (options)[name]; if (value !== undefined) out[name] = value; } } return out; }; // The same table, as a copy, for the reason `_transformsFrom` always makes one. /** @type {Required} */ const _DEFAULT_TRANSFORMS = _transformsFrom(undefined); /** * The `comments` option resolved to what it means per comment: `true` every * one, `false` none, `"some"` the ones that carry something, or a predicate * over the comment's own text — which is what a pattern compiles to here, so no * print re-reads one per comment. A pattern is matched from the start each * time, since a `g` flag would otherwise carry an index between comments. * @param {Required["comments"]} comments the option * @returns {boolean | "some" | ((comment: string) => boolean)} what it keeps */ const _keptComments = (comments) => { if (comments === "all") return true; if (typeof comments === "boolean" || comments === "some") return comments; if (typeof comments === "function") return comments; const pattern = typeof comments === "string" ? new RegExp(comments) : comments; return (comment) => { pattern.lastIndex = 0; return pattern.test(comment); }; }; /** * The per-transform switches out of a wider options object — what * `optimization.minimize.css` names beside `environment` and the two rewrites * that are off until asked for. Exported so the minify functions the minimizer * plugin ships to its workers can pick them without repeating the names. * @param {EXPECTED_OBJECT} options an options object that may carry them * @returns {CssTransformOptions | undefined} the switches, or undefined when none is set */ const pickTransforms = (options) => { /** @type {EXPECTED_ANY} */ let out; for (const name of Object.keys(_ALL_TRANSFORMS)) { const value = /** @type {EXPECTED_ANY} */ (options)[name]; if (value === undefined) continue; if (out === undefined) out = {}; out[name] = value; } return out; }; /** * 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). When `writer` is given the same walk also prints: it is * threaded down the walk and each node's printer builds its text into it as the * node finishes — one parse, no re-tokenization. `skip` is ignored while printing * (it needs every node). * @param {string} input source text * @param {CompiledVisitorMap} visitors compiled visitor map * @param {PrintContext | undefined} writer the print context to build output into, or undefined (walk only) * @param {CssProcessOptions} options process options */ const grammar = (input, visitors, writer, options) => { const locConverter = options.locConverter || new LocConverter(input); _setupParse(input, locConverter); // Printing (a writer) needs every node for a faithful serialization, so `skip` // is ignored while printing — it only applies to walk-only visitor passes. const skip = writer !== undefined ? undefined : 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; // The walk always streams: there is nothing to trade off, since a block under // the threshold is collected and walked in one batch exactly as it always was. // Printing streams with it — the rule's opener goes out when its block opens // and each child straight after it — so the printer never assembles a parent // from text a streamed body has already released. _streamBlocks = true; _streamWriter = writer; _printing = writer !== undefined; _convertLengthUnits = writer !== undefined && writer.options.convertLengthUnits === true; _rewriteCustomProperties = writer !== undefined && writer.options.rewriteCustomProperties === true; _transforms = _transformsFrom( writer === undefined ? undefined : writer.options.transforms ); _commentsKept = _keptComments(_transforms.comments); _unitScale = _unitScaleFor(_convertLengthUnits); _renderEmbeddedSource = writer === undefined ? undefined : writer.options.renderEmbeddedSource; _deferEmbeddedSource = writer === undefined ? undefined : writer.options.deferEmbeddedSource; const environment = writer !== undefined ? writer.options.environment : undefined; // Vendor prefixing is on for a minifying print with a browserslist selection, // and off for everything else — an empty selection names no browser to // prefix for, so it leaves prefixes alone rather than dropping every one. const browsers = writer !== undefined && writer.options.mode === "minify" && environment !== undefined && environment.browsers !== undefined && environment.browsers.length !== 0 ? environment.browsers : undefined; if (browsers === undefined) { _seenPrefixRules = null; _prefixBrowsers = null; _prefixingOn = false; } else { _useBrowsers(browsers); // The selection answers two questions, and `vendorPrefixes` turns off only // the first: which prefixes to write, and which spellings a target reads. _prefixingOn = _prefixBrowsers !== null && /** @type {CssEnvironment} */ (environment).vendorPrefixes !== false; _seenPrefixRules = _prefixingOn ? new Map() : null; } // Read once per print: the selection cannot change under a single stylesheet, // and each of these is asked per declaration. _hexAlphaAllowed = _targetSupports("colorHexAlpha"); _doublePositionAllowed = _targetSupports("gradientDoublePosition"); _insetShorthandAllowed = _targetSupports("insetShorthand"); _rangeSpellingAllowed = _targetSupports("mediaQueryRange"); _placeShorthandAllowed = _targetSupports("placeShorthand"); _overflowTwoValuesAllowed = _targetSupports("overflowTwoValues"); _visitors = visitors; _commentBucket = visitors[T_COMMENT]; // Comment sink: fire the `Comment` visitor bucket (if registered) and, when // printing, keep license/important comments (`/*!`, `@license`, `@preserve`) — // the ecosystem default (cssnano / clean-css / csso keep `/*!`; terser adds the // annotations). A kept comment is handed to the writer, which re-emits it before // the next top-level node. Both print modes keep the same ones, so beautifying // and minifying the same stylesheet carry the same banners. No comment visitor // and no printing => no callback (comments are skipped at zero cost). /** @type {((input: string, start: number, end: number) => number) | undefined} */ let onComment; if (writer !== undefined) { const w = writer; onComment = (src, start, end) => { if (_commentBucket !== undefined) _grammarOnComment(src, start, end); if (_isKeptComment(src, start, end)) { w.insert(start, src.slice(start, end)); } return end; }; } else if (_commentBucket !== undefined) { onComment = _grammarOnComment; } // 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, onComment); const as = options.as || "stylesheet"; const consume = TOP_LEVEL_CONSUMERS[as] || consumeAStylesheetsContents; // A block's contents are one declaration list, so printing them is composing // that list — the same production a rule's block is, and the only top-level // one that is held rather than streamed. if (as === "block-contents" && writer !== undefined) { _blockContentsNodes = []; } // With zero registered buckets the walk is a pure no-op traversal — hand the // consumer a recycle-only sink instead. let anyVisitor = false; for (let i = 0; i < visitors.length; i++) { if (visitors[i] !== undefined) { anyVisitor = true; break; } } try { // Bind the writer into the per-node sink only when printing, so the // walk-only path keeps passing the module function with no per-parse closure. // A printer consumes the walk, so only a writer-less, visitor-less process // call can take the recycle-only sink. consume( ts, writer === undefined ? anyVisitor ? _walkTopLevel : _recycleTopLevel : (node) => _walkTopLevel(node, writer) ); if (_blockContentsNodes === null) { _flushTopLevel(writer); } else { _emitBlockContents( _blockContentsNodes, /** @type {PrintContext} */ (writer) ); } } finally { _blockContentsNodes = null; // Each held write keeps its offered source — a decoded `data:` payload can // be a whole document — and a closure over it, so both go with the parse. _renderEmbeddedSource = undefined; _deferEmbeddedSource = undefined; _heldTopLevel = null; _seenTopLevelLayers = null; _ruleEntry.clear(); _seenPrefixRules = null; // The parsed selection itself stays in `_parsedBrowsersMemo`, which the next // asset of the same build reuses; only this parse's pointer is dropped. _prefixBrowsers = null; _prefixDropCandidate = null; // Drop the module-level column references so the last parsed source (and // its LocConverter / child lists / visitors) don't stay alive between // parses. _streamBlocks = false; _streamWalked = null; _streamWriter = undefined; _depth = 0; // A frame is left as the block closed it (nothing reads a closed one), so // the last open path's buffers are dropped here rather than per block. const used = _frameHighWater + 1; _frameDecls.fill(null, 0, used); _frameRules.fill(null, 0, used); _frameSeenDeclarations.fill(null, 0, used); _frameSeenRules.fill(null, 0, used); _frameChainKey.fill(null, 0, used); _frameSeenLayers.fill(null, 0, used); _frameHighWater = 0; _blockSpans.length = 0; _ruleScopes = null; _rootRuleScope = null; _anonymousLayers = 0; _input = ""; _locConverter = /** @type {LocConverter} */ (/** @type {unknown} */ (null)); _declBodies.length = 0; _ruleBodies.length = 0; _flatTop = 0; _listPool.length = 0; if (_flat.length > _SHRINK_CAPACITY) { _flatGrowHint = _flatPeak; _flat = new Int32Array(0); } _visitors = /** @type {CompiledVisitorMap} */ (/** @type {unknown} */ ([])); _commentBucket = undefined; if (_capacity > _SHRINK_CAPACITY) { // +1: node ids are 1-based and grow fires at `id >= capacity`. _growHint = _peak + 1; _capacity = 0; _releaseColumns(); } _peak = 0; _flatPeak = 0; } }; /** * Whether a pending token separator is safe to drop *before* `cc`: next to * `{ } ; , )` it never changes meaning (`)` only ever closes a group). * @param {number} cc first code point of the text about to be written * @returns {boolean} true when the separator can be dropped */ const _dropSeparatorBefore = (cc) => cc === CC_LEFT_CURLY || cc === CC_RIGHT_CURLY || cc === CC_SEMICOLON || cc === CC_COMMA || cc === CC_RIGHT_PARENTHESIS; /** * Whether a pending token separator is safe to drop *after* `cc`: next to * `{ } ; , (` it never changes meaning. The parens are asymmetric on purpose — * dropping a space *before* `(` could turn `x (y)` into the function `x(y)`, and * one *after* `)` could turn `:not(a) b` into the compound `:not(a)b`, so `(` is * only safe on this (after) side and `)` only on the before side. * @param {number} cc last emitted code point * @returns {boolean} true when the separator can be dropped */ const _dropSeparatorAfter = (cc) => cc === CC_LEFT_CURLY || cc === CC_RIGHT_CURLY || cc === CC_SEMICOLON || cc === CC_COMMA || cc === CC_LEFT_PARENTHESIS; /** * What the CSS printer may be told, on top of the `mode` every language has — * the printing slice of `CssProcessOptions`, named once so nothing outside CSS * has to enumerate it. * @typedef {Pick} CssPrintOptions */ /** @typedef {import("../util/SourceProcessor").PrintContext} PrintContext */ // === Spacing: the whole safe-spacing discipline is CSS-specific (a token // separator is safe to drop next to `{ } ; , ( )` — the asymmetric paren rule in // `_dropSeparator*`), so it lives with `printer`, not the generic context. // A lone space marker a whitespace token prints; `_join` resolves it. Real tokens // print with their delimiters (a string keeps its quotes), so it never collides. const _SEP = " "; /** * Whether a token separator between two code points must be kept. * @param {number} lastCode last emitted code point * @param {number} nextCode first code point of the next fragment * @returns {boolean} true when the separator must be kept */ const _keepSeparator = (lastCode, nextCode) => !_dropSeparatorAfter(lastCode) && !_dropSeparatorBefore(nextCode); // A child / adjacent-sibling / general-sibling combinator. Whitespace around // one is insignificant, but only at the top level of a selector — inside `[…]` // (`~=`) or `(…)` (`nth-child(2n+1)`) these are matchers/`An+B`, so combinator // trimming is applied to a qualified rule's prelude join alone (`isSelector`). /** * @param {number} cc a code point * @returns {boolean} true for `>` / `+` / `~` */ const _isCombinator = (cc) => cc === CC_GREATER_THAN_SIGN || cc === CC_PLUS_SIGN || cc === CC_TILDE; // What separates the parts of a query condition: a media-feature range // comparison (`<`, `>`, `=`, and the `<=` / `>=` pairs they build) and the `:` // of a plain feature test. Whitespace around one is insignificant there, but the // same code points mean something else in a selector (combinators, and `:` // starting a pseudo-class) and in a declaration value (`calc()` operators) — so // this trimming is applied to a `(…)` condition outside a value alone. /** * @param {number} cc a code point * @returns {boolean} true for `<` / `>` / `=` / `:` */ const _isConditionSeparator = (cc) => cc === CC_LESS_THAN_SIGN || cc === CC_GREATER_THAN_SIGN || cc === CC_EQUALS_SIGN || cc === CC_COLON; // What a `_join` may drop a separator next to. const _TRIM_NOTHING = 0; const _TRIM_COMBINATORS = 1; // a qualified rule's selector prelude const _TRIM_CONDITIONS = 2; // a query condition's `(…)` block const _TRIM_MATH = 3; // a math function's `*` / `/`, which need no whitespace // A declaration value's whitespace only separates tokens, so it is kept exactly // where joining would fuse two — unlike a selector, where it is a combinator. const _TRIM_SEPARATORS = 4; /** * @param {number} trim one of the `_TRIM_*` modes * @param {number} cc a code point * @returns {boolean} whether a separator next to `cc` may be dropped */ const _isTrimmable = (trim, cc) => { if (trim === _TRIM_COMBINATORS) return _isCombinator(cc); if (trim === _TRIM_CONDITIONS) return _isConditionSeparator(cc); // CSS Values 4 §10.1: `+` and `-` require whitespace on both sides — without // it the sign would read as part of the next number — but `*` and `/` do not. if (trim === _TRIM_MATH) { return cc === CC_ASTERISK || cc === CC_SOLIDUS; } return false; }; /** * Whether a code point can continue an identifier. The lookup table only spans * ASCII, so non-ASCII — always an ident code point (§4.2) — is folded in here. * @param {number} cc a code point * @returns {boolean} true when `cc` is an ident code point */ const _isIdentLike = (cc) => cc >= 128 || _isIdentCodePoint(cc); /** * Whether what precedes `at` lets a number start there: an ident running into * the position carries the digits instead, and `#` and `@` carry their own. * @param {string} out everything emitted so far * @param {number} at index a number would start at * @returns {boolean} true when a number can start at `at` */ const _startsNumber = (out, at) => { if (at === 0) return true; const before = out.charCodeAt(at - 1); return !( _isIdentLike(before) || before === CC_REVERSE_SOLIDUS || before === CC_NUMBER_SIGN || before === CC_AT_SIGN ); }; /** * Whether the run of digits `out` ends with is a number rather than the tail of * an ident: `1` is, the `1` of `.p1` is not, and only a number can take a `.` on. * @param {string} out everything emitted so far * @returns {boolean} true when `out` ends in a number */ const _endsWithNumber = (out) => { let at = out.length; while (at > 0 && _isDigit(out.charCodeAt(at - 1))) at--; if (at === out.length) return false; // A sign signs a number only where a number could start: the `-1` of // `margin:-1` is signed, the `-1` of `.p-1` and the `-5` of `1e-5` are ident. const sign = at === 0 ? 0 : out.charCodeAt(at - 1); if ( (sign === CC_HYPHEN_MINUS || sign === CC_PLUS_SIGN) && _startsNumber(out, at - 1) ) { return true; } return _startsNumber(out, at); }; /** * Whether emitting `fragment` directly after `out` would re-tokenize as one * token. Two printed siblings normally concatenate to exactly their source, but * not when a dropped comment was all that separated them, or when one was * rewritten (`1.0.5` is two numbers, printed as `1` and `.5`) — then the junction * can fuse and change the declaration. The test is deliberately exact rather than * conservative: a space inserted where none is needed would turn a compound * selector (`.a.b`) into a descendant one (`.a .b`), so over-separating is as * unsafe as under-separating. * Takes the preceding character rather than the text before it: what it is * appended to is a rope, and reading a character off one flattens the whole of * it — once per fragment, over text that grows with every fragment. * @param {number} last character code before the fragment * @param {string} fragment the fragment about to be emitted * @param {string} out everything emitted so far, read only where a digit meets a `.` * @returns {boolean} true when a separator has to be inserted between them */ const _wouldFuseTokens = (last, fragment, out) => { const next = fragment.charCodeAt(0); // An escape swallows whatever follows it. if (last === CC_REVERSE_SOLIDUS) return true; // `/` + `*` opens a comment. if (last === CC_SOLIDUS && next === CC_ASTERISK) return true; if (_isIdentLike(last)) { // One ident / dimension / hash / at-keyword continues; `(` after an ident // makes it a function token instead. if ( _isIdentLike(next) || next === CC_LEFT_PARENTHESIS || next === CC_REVERSE_SOLIDUS ) { return true; } // `1` + `.5` reads back as the single number `1.5`. Only a number takes // the `.` on, and only when a digit follows it: the `1` ending the ident of // `.p1` does not, so `.p1` + `.c1` stays the one compound selector it is. if (_isDigit(last) && next === CC_FULL_STOP) { return _isDigit(fragment.charCodeAt(1)) && _endsWithNumber(out); } // `123` + `%` reads back as the one percentage token `123%`, and only a // number takes the `%` on — the `1` ending an ident does not. if (_isDigit(last) && next === CC_PERCENTAGE) return _endsWithNumber(out); // A trailing `--` plus `>` would close a CDC. return last === CC_HYPHEN_MINUS && next === CC_GREATER_THAN_SIGN; } // A number may start right after `.` or `+` (`-` is an ident code point, so it // is already covered above). if (last === CC_FULL_STOP) return _isDigit(next); // CSS Syntax 3 §4.3.10: a `+` starts a number only before a digit, or before a // `.` that itself has one. `.a+.m` is the sibling combinator and a class. if (last === CC_PLUS_SIGN) { return ( _isDigit(next) || (next === CC_FULL_STOP && _isDigit(fragment.charCodeAt(1))) ); } // `#` / `@` + ident starts a hash / at-keyword token. if (last === CC_NUMBER_SIGN || last === CC_AT_SIGN) { return _isIdentLike(next) || next === CC_REVERSE_SOLIDUS; } // `<` + `!` opens a CDO. return last === CC_LESS_THAN_SIGN && next === CC_EXCLAMATION; }; /** * Join sibling fragments (a prelude / value / args / block body), resolving each * `_SEP` marker into a single space kept only where dropping it would merge two * tokens (minifying) or unconditionally (beautifying). `trim` additionally drops * whitespace next to the code points that carry their own meaning in this * context — a selector's combinators, a query condition's comparisons. * Fragments that had no separator at all still get one where concatenating them * would fuse two tokens into one (see {@link _wouldFuseTokens}). * @param {string[]} parts printed fragments in source order * @param {boolean} beautify whether every separator is kept * @param {number=} trim one of the `_TRIM_*` modes (default `_TRIM_NOTHING`) * @returns {string} the joined text */ const _join = (parts, beautify, trim = _TRIM_NOTHING) => { let out = ""; let last = -1; let pending = false; for (let i = 0; i < parts.length; i++) { const f = parts[i]; if (f.length === 0) continue; if (f === _SEP) { pending = true; continue; } const first = f.charCodeAt(0); if (pending) { const keep = beautify || (trim === _TRIM_SEPARATORS ? _wouldFuseTokens(last, f, out) : _keepSeparator(last, first) && !_isTrimmable(trim, last) && !_isTrimmable(trim, first)); if (out.length !== 0 && keep) { out += " "; } pending = false; } else if (out.length !== 0 && _wouldFuseTokens(last, f, out)) { // Nothing separated these in source (a comment stood here, or a token was // rewritten), yet joining them would read back as one token. Whitespace // is a descendant combinator in a selector, so there only an empty // comment parts them without saying anything the source did not. out += trim === _TRIM_COMBINATORS ? "/**/" : " "; } out += f; last = f.charCodeAt(f.length - 1); } return out; }; // === Safe (meaning-preserving) value transforms, applied by `printer` // when minifying. Each is value-identical — the same computed style — so they // never change what the stylesheet means. /** * The byte offset where `s`'s leading number ends (before its unit / `%`). * @param {string} s a number / dimension / percentage token's text * @returns {number} the numeric part's length */ const _numberEnd = (s) => { const n = s.length; let i = 0; const c = s.charCodeAt(0); if (c === CC_PLUS_SIGN || c === CC_HYPHEN_MINUS) i++; while (i < n && _isDigit(s.charCodeAt(i))) i++; if (i < n && s.charCodeAt(i) === CC_FULL_STOP) { i++; while (i < n && _isDigit(s.charCodeAt(i))) i++; } // exponent (`e` / `E`) const e = s.charCodeAt(i); if (e === 101 || e === 69) { let j = i + 1; const sign = s.charCodeAt(j); if (sign === CC_PLUS_SIGN || sign === CC_HYPHEN_MINUS) j++; let k = j; while (k < n && _isDigit(s.charCodeAt(k))) k++; if (k > j) i = k; } return i; }; /** * Normalize a numeric string (no unit): drop a leading zero (`0.5`→`.5`) and * trailing fractional zeros (`1.50`→`1.5`, `1.0`→`1`). Value-preserving; * scientific notation is left untouched. * @param {string} num numeric text (may carry a sign) * @returns {string} the normalized number */ const _normalizeNumber = (num) => { if (num.includes("e") || num.includes("E")) return num; let sign = ""; let s = num; const c0 = s.charCodeAt(0); if (c0 === CC_PLUS_SIGN || c0 === CC_HYPHEN_MINUS) { if (c0 === CC_HYPHEN_MINUS) sign = "-"; s = s.slice(1); } const dot = s.indexOf("."); if (dot !== -1) { let end = s.length; while (end > dot + 1 && s.charCodeAt(end - 1) === CC_0) end--; if (end === dot + 1) end = dot; // fraction emptied → drop the dot too s = s.slice(0, end); } while ( s.length > 1 && s.charCodeAt(0) === CC_0 && s.charCodeAt(1) !== CC_FULL_STOP ) { s = s.slice(1); // redundant integer leading zeros } if ( s.length > 1 && s.charCodeAt(0) === CC_0 && s.charCodeAt(1) === CC_FULL_STOP ) { s = s.slice(1); // "0.5" → ".5" } if (s === "" || s === ".") s = "0"; return sign + s; }; // Chromium serializes a computed number at 6 significant digits, and lays a // length out in 1/64px — so 6 digits is below what a stylesheet can observe until // the value reaches ~23000px, which no real one does. Measured across `width` // (px and %), `opacity`, `scale()`, `letter-spacing`, `transition-duration` and // `flex-grow`, at container widths from 100px to 1000000px. const _SIGNIFICANT_DIGITS = 6; // Above this the rounded absolute error would pass 1/64px, so the digits stay. const _ROUNDING_LIMIT = 1e4; /** * Round a numeric string to `_SIGNIFICANT_DIGITS`, or return it unchanged when * rounding would not shorten it (or would leave the range the measurement * covers). Scientific notation is left alone — `toPrecision` may produce it, and * an exponent is not shorter here anyway. * @param {string} num the normalized numeric text * @returns {string} the rounded number, or `num` */ const _roundSignificant = (num) => { // A number written in at most `_SIGNIFICANT_DIGITS` characters carries at most // that many significant digits — a sign, a dot and leading zeros only take // away from them — so rounding to that precision hands back what it was given. // Almost every number in a stylesheet is this short. if (num.length <= _SIGNIFICANT_DIGITS) return num; if (num.includes("e") || num.includes("E")) return num; const value = Number(num); if (!Number.isFinite(value) || Math.abs(value) >= _ROUNDING_LIMIT) return num; const rounded = value.toPrecision(_SIGNIFICANT_DIGITS); if (rounded.includes("e") || rounded.includes("E")) return num; if (Number(rounded) === value) return num; const text = _normalizeNumber(rounded); return text.length < num.length ? text : num; }; // The absolute units that still convert once `convertLengthUnits` is off: every // group but `length`, which is the one that option gates. Built on first use and // kept, so a parse points at one table or the other rather than testing per token. /** @type {Map | null} */ let _nonLengthUnitScale = null; /** * The unit table a parse converts through, by what its options allow. * @param {boolean} lengths whether a length may be rewritten into another unit * @returns {Map} the table to convert through */ const _unitScaleFor = (lengths) => { if (lengths) return ABSOLUTE_UNIT_SCALE; if (_nonLengthUnitScale === null) { _nonLengthUnitScale = new Map(); for (const [unit, scale] of ABSOLUTE_UNIT_SCALE) { if (scale[0] !== "length") _nonLengthUnitScale.set(unit, scale); } } return _nonLengthUnitScale; }; /** * Rewrite a dimension into the shortest unit it is exactly equal in. Only the * units CSS Values 4 fixes against each other, and only when the conversion * round-trips exactly in doubles — which is what keeps `cm` / `mm` / `q`, none * of them binary-exact in `px`, mostly where they were. * @param {string} num the normalized numeric text * @param {string} unit the token's unit, as written * @returns {string} the shortest equal dimension */ const _convertUnit = (num, unit) => { // The table this parse converts through: without `convertLengthUnits` the // length units are not in it, so a `px` — the commonest dimension a // stylesheet writes — costs a lookup that misses rather than a parse thrown // away. const from = _unitScale.get(toLowerCaseIfNeeded(unit)); if (from === undefined) return num + unit; const value = Number(num); if (!Number.isFinite(value)) return num + unit; // A zero length drops its unit outright, so rewriting it says nothing — but a // zero time keeps one, and `s` is the shorter of the two it can carry. if (value === 0 && from[0] !== "time") return num + unit; const base = value * from[1]; let best = num + unit; for (const [candidate, to] of _unitScale) { if (to[0] !== from[0] || to === from) continue; if (!UNIT_CONVERSION_TARGETS.has(candidate)) continue; const converted = base / to[1]; if (converted * to[1] !== base) continue; const text = String(converted); if (text.includes("e") || text.includes("E")) continue; const dimension = _normalizeNumber(text) + candidate; if (dimension.length < best.length) best = dimension; } return best; }; /** * Whether the declaration being printed has an `` anywhere in its * grammar, so a number in it may be one. * @returns {boolean} true inside such a declaration */ const _inIntegerProperty = () => _valueDeclaration !== null && INTEGER_PROPERTIES.has(toLowerCaseIfNeeded(A.name(_valueDeclaration))); /** * Whether the declaration being printed is one an engine takes no `calc()` in, * so a folded term keeps the `calc()` it was written with. * @returns {boolean} true inside such a declaration */ const _inCalcRejectingProperty = () => _valueDeclaration !== null && CALC_REJECTING_PROPERTIES.has(toLowerCaseIfNeeded(A.name(_valueDeclaration))); // A folded term, as a number and its unit. const _TERM_VALUE_RE = /^(-?(?:\d+\.?\d*|\.\d+))([a-z%]*)$/i; /** * Whether writing a value bare would lose the clamp the spec puts on a `calc()` * in this property — the literal is thrown out where the `calc()` computes the * bound. A unit the range is not stated in loses it too: Chrome takes * `oblique 100grad` off and `oblique 2rad` at face value, while clamping either * inside a `calc()`. * @param {string} property the lower-cased property name * @param {string} number the value * @param {string} unit its unit, "" when it carries none * @returns {boolean} true when the `calc()` has to stay */ const _losesClamp = (property, number, unit) => { const clamped = CLAMPED_VALUE_RANGES.get(property); if (clamped === undefined) return false; if (!equalsLowerCase(unit, clamped[0])) return true; const value = Number(number); return value < clamped[1] || value > clamped[2]; }; /** * The same, for a term the math fold is about to write in place of its * `calc()`. * @param {string} term the folded term * @returns {boolean} true when writing it bare would change the declaration */ const _foldLosesClamp = (term) => { if (_valueDeclaration === null) return false; const property = toLowerCaseIfNeeded(A.name(_valueDeclaration)); if (!CLAMPED_VALUE_RANGES.has(property)) return false; const match = _TERM_VALUE_RE.exec(term); return match === null || _losesClamp(property, match[1], match[2]); }; /** * Put back the fraction that keeps a number a `` token, in the spelling * the rest of the printer uses — one fractional digit, and no leading zero. * @param {string} num a normalized number with no fraction left * @returns {string} the same value, still spelled as a `` */ const _keepFraction = (num) => { const out = `${num}.0`; if (out.charCodeAt(0) === CC_0) return out.slice(1); return out.charCodeAt(0) === CC_HYPHEN_MINUS && out.charCodeAt(1) === CC_0 ? `-${out.slice(2)}` : out; }; /** * Normalize a number / dimension / percentage token: normalize the numeric part, * then round it and reach for a shorter equal unit. Neither is done inside a * `@supports` prelude, where the declaration is being tested rather than applied * — an engine may read `px` and not `pc`. Angles keep every digit: `rotate()` * runs its argument through trig, which amplifies a truncated one. * @param {string} text the token's source text * @returns {string} the normalized token */ const _normalizeNumericToken = (text) => { const end = _numberEnd(text); // A unit identifier matches ASCII case-insensitively, so `1PX` is `1px` — and // the three units spelled with a capital keep the spelling everything writes. let unit = text.slice(end); // One fold of the unit for both jobs below — how it is printed, and whether // it is an angle. `toLowerCase` hands back the string it was given where // nothing folds, so a unit already lowercase is the same object. const lowered = unit.toLowerCase(); // A substituted value is handed back as the tokens it was written as, so the // spelling there is the author's; `lowered` still answers the angle question. if (lowered !== unit && !_inSubstitutedValue) { // Only a unit that carried a capital can have a canonical spelling to look // up, so the table is read for the shouted units alone. const folded = asciiLowerCaseName(unit); const canonical = CANONICAL_NAMES.get(folded); unit = canonical === undefined ? folded : canonical; } if (!_transforms.shortenNumbers) return text.slice(0, end) + unit; const num = _normalizeNumber(text.slice(0, end)); // An all-zero fraction still makes this a ``, not an `` // (`grid-row:1.0` computes `auto`). Property read last, it is the costlier one. const dot = text.indexOf("."); if ( unit === "" && dot !== -1 && dot < end && !num.includes(".") && _inIntegerProperty() ) { return _keepFraction(num); } // `round()`, `mod()` and `rem()` are step functions of their arguments, so a // rewrite that holds everywhere else does not hold in one: `4.5cm` and `45mm` // are the same length, and headless Chromium reads `round(down,4.5cm,1.5cm)` // as `3cm` but `round(down,45mm,15mm)` as `4.5cm`. if (_inSupportsPrelude || _inCustomProperty || _steppedFunctionDepth !== 0) { return num + unit; } if (ANGLE_UNITS.has(lowered)) return num + unit; return _convertUnit(_roundSignificant(num), unit); }; // One operand as the value printer leaves it: a number, a dimension or a // percentage. Sticky, so the tokenizer walks the expression without slicing it. const _CALC_OPERAND = /(?:\d*\.\d+|\d+)(?:e[+-]?\d+)?(%|[a-z]+)?/iy; // `calc(` opening a nested expression. The inner one already printed, so it // arrives here as text and is treated as the parentheses it is. const _CALC_OPEN = /calc\(/iy; /** * The key a unit accumulates under. Units fixed against each other share their * group's key and are counted in its base unit, so `1in + 1px` is one term; * everything else keys on the unit itself, so `1em + 1px` stays two and is * declined below. * @param {string} unit the operand's unit, `""` for a plain number * @returns {[string, number]} the key and how many base units one of it is */ const _calcUnitKey = (unit) => { if (unit === "") return ["", 1]; const lower = unit.toLowerCase(); const absolute = ABSOLUTE_UNIT_SCALE.get(lower); return absolute === undefined ? [lower, 1] : [absolute[0], absolute[1]]; }; /** * The units an expression was written with, which a gated length collapse may * still print back into. * @param {{ type: string, value: number, unit: string }[]} tokens the tokens * @returns {Set} the lowercased units */ const _writtenUnits = (tokens) => { /** @type {Set} */ const units = new Set(); for (const token of tokens) { if (token.unit !== "") units.add(toLowerCaseIfNeeded(token.unit)); } return units; }; /** * Tokenize one printed `calc()` body. * @param {string} text the body * @returns {{ type: string, value: number, unit: string }[] | null} the tokens, or `null` when something here is not arithmetic */ const _tokenizeCalc = (text) => { /** @type {{ type: string, value: number, unit: string }[]} */ const tokens = []; let i = 0; let spaced = false; while (i < text.length) { const c = text[i]; if (c === " ") { spaced = true; i++; continue; } if (c === "(" || c === ")") { tokens.push({ type: c, value: 0, unit: "" }); spaced = false; i++; continue; } if (c === "*" || c === "/" || c === ",") { tokens.push({ type: c, value: 0, unit: "" }); spaced = false; i++; continue; } // CSS Values 4 §10.1: `+` and `-` are operators only with whitespace on // both sides. Without it the sign belongs to the number, and two operands // with no operator between them is not an expression at all. if ((c === "+" || c === "-") && spaced && text[i + 1] === " ") { tokens.push({ type: c, value: 0, unit: "" }); spaced = false; i++; continue; } _CALC_OPEN.lastIndex = i; if (_CALC_OPEN.test(text)) { tokens.push({ type: "(", value: 0, unit: "" }); spaced = false; i = _CALC_OPEN.lastIndex; continue; } let sign = 1; if (c === "+" || c === "-") { if (c === "-") sign = -1; i++; } _CALC_OPERAND.lastIndex = i; const match = _CALC_OPERAND.exec(text); if (match === null || match.index !== i) return null; const unit = match[1] === undefined ? "" : match[1]; const value = Number(match[0].slice(0, match[0].length - unit.length)); if (!Number.isFinite(value)) return null; tokens.push({ type: "value", value: sign * value, unit }); spaced = false; i = _CALC_OPERAND.lastIndex; } return tokens; }; /** * Evaluate a tokenized expression into `key -> coefficient`, the sum CSS Values * 4 §10.11 reduces a calculation to. * @param {{ type: string, value: number, unit: string }[]} tokens the tokens * @param {{ at: number }} cursor the read position, carried through the recursion * @returns {Map | null} the sum, or `null` when it cannot be evaluated exactly */ const _evaluateCalcSum = (tokens, cursor) => { const sum = _evaluateCalcProduct(tokens, cursor); if (sum === null) return null; for (;;) { const token = tokens[cursor.at]; if (token === undefined || (token.type !== "+" && token.type !== "-")) { return sum; } cursor.at++; const right = _evaluateCalcProduct(tokens, cursor); if (right === null) return null; const sign = token.type === "+" ? 1 : -1; for (const [key, coefficient] of right) { const scaled = exactMultiply(coefficient, sign); if (scaled === null) return null; const previous = sum.get(key); if (previous === undefined) { sum.set(key, scaled); continue; } const added = exactAdd(previous, scaled); if (added === null) return null; sum.set(key, added); } } }; /** * ``: a chain of `*` and `/`. The grammar takes only a `` * on the right of a `/`, and a product needs one side to be a plain number for * the result to stay a sum of the units already there. * @param {{ type: string, value: number, unit: string }[]} tokens the tokens * @param {{ at: number }} cursor the read position * @returns {Map | null} the sum, or `null` */ const _evaluateCalcProduct = (tokens, cursor) => { let sum = _evaluateCalcValue(tokens, cursor); if (sum === null) return null; for (;;) { const token = tokens[cursor.at]; if (token === undefined || (token.type !== "*" && token.type !== "/")) { return sum; } cursor.at++; const right = _evaluateCalcValue(tokens, cursor); if (right === null) return null; if (token.type === "/") { const divisor = right.get(""); if (divisor === undefined || right.size !== 1) return null; const out = new Map(); for (const [key, coefficient] of sum) { const quotient = exactDivide(coefficient, divisor); if (quotient === null) return null; out.set(key, quotient); } sum = out; continue; } const leftNumber = sum.size === 1 ? sum.get("") : undefined; const rightNumber = right.size === 1 ? right.get("") : undefined; // One side has to be a plain number; two dimensions multiply into a type no // property here accepts. const factor = rightNumber !== undefined ? rightNumber : leftNumber; const other = rightNumber !== undefined ? sum : right; if (factor === undefined) return null; const out = new Map(); for (const [key, coefficient] of other) { const product = exactMultiply(coefficient, factor); if (product === null) return null; out.set(key, product); } sum = out; } }; /** * ``: an operand, or a parenthesized sum. * @param {{ type: string, value: number, unit: string }[]} tokens the tokens * @param {{ at: number }} cursor the read position * @returns {Map | null} the sum, or `null` */ const _evaluateCalcValue = (tokens, cursor) => { const token = tokens[cursor.at]; if (token === undefined) return null; if (token.type === "(") { cursor.at++; const inner = _evaluateCalcSum(tokens, cursor); if (inner === null) return null; const close = tokens[cursor.at]; if (close === undefined || close.type !== ")") return null; cursor.at++; return inner; } if (token.type !== "value") return null; cursor.at++; const [key, scale] = _calcUnitKey(token.unit); const value = exactMultiply(token.value, scale); return value === null ? null : new Map([[key, value]]); }; /** * Round a folded result the way an authored number is rounded — the fold prints * a double back in full, and `6 / 10 - 0.375` is `.22499999999999998`. The * exclusions are the token printer's own: a `@supports` prelude and a custom * property keep what was written, a stepped function is a step of its argument, * and an angle keeps every digit because `rotate()` runs it through trig. * @param {string} text the printed numeric text * @param {string} unit the unit it carries, empty or `%` for none * @returns {string} the rounded text, or `text` */ const _roundCalcResult = (text, unit) => { if (_inSupportsPrelude || _inCustomProperty || _steppedFunctionDepth !== 0) { return text; } return ANGLE_UNITS.has(toLowerCaseIfNeeded(unit)) ? text : _roundSignificant(text); }; /** * Print one collapsed term back. * @param {string} key the sum's one key * @param {number} coefficient its value, in the key's base unit * @param {Set} written the units the expression was written with * @returns {string | null} the printed value, or `null` when it does not print back exactly */ const _printCalcTerm = (key, coefficient, written) => { if (key === "" || key === "%") { const text = _normalizeNumber(String(coefficient)); return Number(text) === coefficient ? _roundCalcResult(text, key) + key : null; } const base = UNIT_GROUP_BASE.get(key); // A unit outside the conversion table counts in itself, so the coefficient is // already what it prints as. if (base === undefined) { const text = _normalizeNumber(String(coefficient)); return Number(text) === coefficient ? _roundCalcResult(text, key) + key : null; } // Counted in the group's base unit, so every unit of the group is a candidate // and each is divided into directly: `1cm + 1mm` is exactly `11mm`, and // reaching it through `px` first would lose it — no `px` count equals it. let best = null; const lengthGated = key === "length" && !_convertLengthUnits; for (const [candidate, to] of ABSOLUTE_UNIT_SCALE) { if (to[0] !== key) continue; // Gated, a sum may still collapse into a unit it was written with — that // introduces none — but not into one reached only to save bytes. if (lengthGated && candidate !== base[0] && !written.has(candidate)) { continue; } if (candidate !== base[0] && !UNIT_CONVERSION_TARGETS.has(candidate)) { continue; } const value = exactDivide(coefficient, to[1]); if (value === null) continue; const text = _normalizeNumber(String(value)); if (Number(text) !== value || text.includes("e") || text.includes("E")) { continue; } const dimension = _roundCalcResult(text, candidate) + candidate; if (best === null || dimension.length < best.length) best = dimension; } return best; }; /** * Print a whole reduced sum back as a `calc()` body. A sum still holding two * keys is one an engine resolves against layout (a percentage against a length, * an `em` against a `px`), and the terms of it are printed in the order they * were first written. A zero term is kept rather than dropped: which keys may be * added to which is a type rule, and dropping one can make an expression an * engine rejects into one it accepts (`calc(1px + 1deg - 1deg)`). * @param {Map} sum the reduced sum * @param {Set} written the units the expression was written with * @returns {string | null} the body, or `null` when a term does not print exactly */ const _printCalcSum = (sum, written) => { let text = ""; for (const [key, coefficient] of sum) { const term = _printCalcTerm( key, text === "" ? coefficient : Math.abs(coefficient), written ); if (term === null) return null; text += text === "" ? term : `${coefficient < 0 ? " - " : " + "}${term}`; } return text === "" ? null : text; }; /** * Split a tokenized argument list on its top-level commas. * @param {{ type: string, value: number, unit: string }[]} tokens the tokens * @returns {{ type: string, value: number, unit: string }[][]} one list per argument */ const _splitMathArguments = (tokens) => { /** @type {{ type: string, value: number, unit: string }[][]} */ const args = [[]]; let depth = 0; for (const token of tokens) { if (token.type === "(") { depth++; } else if (token.type === ")") { depth--; } else if (token.type === "," && depth === 0) { args.push([]); continue; } args[args.length - 1].push(token); } return args; }; /** * Split on the commas at the top of a function body, so a nested call's own * arguments stay in the piece that holds it. * @param {string} text the body between one call's parentheses * @returns {string[]} its arguments, still as written */ const _splitTopLevelArguments = (text) => { /** @type {string[]} */ const parts = []; let depth = 0; let start = 0; for (let i = 0; i < text.length; i++) { const code = text.charCodeAt(i); if (code === 0x28) { depth++; } else if (code === 0x29) { depth--; } else if (code === 0x2c && depth === 0) { parts.push(text.slice(start, i)); start = i + 1; } } parts.push(text.slice(start)); return parts; }; /** * Reduce the `` arguments of a call the whole-call fold cannot read, * leaving every other argument as written. `calc-size(auto, 10px + 5px)` is * `calc-size(auto,15px)`: the basis is not an expression, so only the size is * touched. * @param {string} fn the lowercased function name * @param {string} inner the text between its parentheses * @returns {string | null} the rewritten body, or `null` */ const _reduceMathArguments = (fn, inner) => { if (!_transforms.reduceFunctions) return null; const positions = MATH_FUNCTION_SUM_ARGUMENTS.get(fn); // The same rewrite a stepped function's arguments refuse. if (positions === undefined || _steppedFunctionDepth > 0) return null; const parts = _splitTopLevelArguments(inner); let changed = false; for (const position of positions) { if (position >= parts.length) return null; const tokens = _tokenizeCalc(parts[position]); if (tokens === null) return null; const cursor = { at: 0 }; const sum = _evaluateCalcSum(tokens, cursor); if (sum === null || cursor.at !== tokens.length) return null; const text = _printCalcSum(sum, _writtenUnits(tokens)); if (text === null) return null; if (text !== parts[position].trim()) changed = true; parts[position] = text; } return changed ? parts.map((part) => part.trim()).join(",") : null; }; /** * Fold a printed math function body to the one value it is equal to. Only a * fully collapsed result is returned: an expression still holding two units (a * percentage against a length, an `em` against a `px`) resolves against layout * and has to stay written out. * @param {string} fn the lowercased function name * @param {string} inner the printed body * @returns {string | null} the value, or `null` to leave the expression as it is */ const _foldMathFunction = (fn, inner) => { if (!_transforms.reduceFunctions) return null; const arity = MATH_FUNCTION_ARITY.get(fn); if (arity === undefined) return null; // A fold standing in a stepped function's argument prints in whichever unit // is shortest, which is the rewrite that function's own arguments refuse: // Chromium reads `round(down,4.5cm,1.5cm)` and `round(down,45mm,15mm)` as // different lengths. The stepped function's own result is not an argument of // one, so it is the depth above this call that decides. if (_steppedFunctionDepth - (STEPPED_FUNCTIONS.has(fn) ? 1 : 0) > 0) { return null; } // A leading keyword the grammar offers (`round(down, …)`) is not an // expression, so it comes off before the arguments are read. let keyword = ""; let body = inner; const choices = MATH_FUNCTION_KEYWORDS.get(fn); if (choices !== undefined) { const comma = inner.indexOf(","); if (comma !== -1) { const head = toLowerCaseIfNeeded(inner.slice(0, comma).trim()); if (choices.includes(head)) { keyword = head; body = inner.slice(comma + 1); } } } const tokens = _tokenizeCalc(body); if (tokens === null) return null; const args = _splitMathArguments(tokens); if (args.length < arity[0] || args.length > arity[1]) return null; /** @type {Map[]} */ const sums = []; for (const argument of args) { const cursor = { at: 0 }; const sum = _evaluateCalcSum(argument, cursor); if (sum === null || cursor.at !== argument.length) return null; sums.push(sum); } // `calc()` is the one that is not a single value: it is whatever sum its // argument reduced to, which may still hold two units. const written = _writtenUnits(tokens); if (fn === "calc") return _printCalcSum(sums[0], written); const fold = MATH_FUNCTION_FOLD.get(fn); if (fold === undefined) return null; const argument = fold.read(sums); if (argument === null) return null; const value = fold.apply(argument[1], keyword, fold.table); if (value === null) return null; return _printCalcTerm( fold.result === "same" ? argument[0] : fold.result, value, written ); }; const _HEX = "0123456789abcdef"; /** * @param {number} n byte value 0..255 * @returns {string} its two lowercase hex digits */ const _hex2 = (n) => _HEX[(n >> 4) & 15] + _HEX[n & 15]; /** * The shortest opaque color text for (r, g, b): a named color where one is * shorter than the hex (`RGB_TO_NAME` holds only those), else a collapsed 3-hex * or a 6-hex. * @param {number} r red 0..255 * @param {number} g green 0..255 * @param {number} b blue 0..255 * @returns {string} the shortest form */ const _shortestColor = (r, g, b) => { const name = RGB_TO_NAME.get((r << 16) | (g << 8) | b); if (name !== undefined) return name; const rr = _hex2(r); const gg = _hex2(g); const bb = _hex2(b); return rr[0] === rr[1] && gg[0] === gg[1] && bb[0] === bb[1] ? `#${rr[0]}${gg[0]}${bb[0]}` : `#${rr}${gg}${bb}`; }; /** * The shortest text for (r, g, b, a) with an alpha below 1 — a 4- or 8-digit hex, * always shorter than the `rgba()` it came from. No named color has an alpha. * @param {number} r red 0..255 * @param {number} g green 0..255 * @param {number} b blue 0..255 * @param {number} a alpha 0..255 * @returns {string} the shortest form */ const _shortestAlphaColor = (r, g, b, a) => { const rr = _hex2(r); const gg = _hex2(g); const bb = _hex2(b); const aa = _hex2(a); return rr[0] === rr[1] && gg[0] === gg[1] && bb[0] === bb[1] && aa[0] === aa[1] ? `#${rr[0]}${gg[0]}${bb[0]}${aa[0]}` : `#${rr}${gg}${bb}${aa}`; }; /** * Minify a hash token (`#…`) when it is a hex color: opaque (3/6 digits, and * 4/8 whose alpha is opaque) → the shortest color; with a real alpha (4/8 * digits) → lowercase + collapse pairs (kept hex). Returns null when it is not a * hex color — e.g. a selector id — so it is * left verbatim. * @param {string} text the hash token text * @returns {string | null} the minified hex/name, or null */ const _minifyHash = (text) => { if (!_transforms.shortenColors) return null; const body = text.slice(1); const n = body.length; if (n !== 3 && n !== 4 && n !== 6 && n !== 8) return null; for (let i = 0; i < n; i++) { if (!_isHexDigit(body.charCodeAt(i))) return null; } const low = body.toLowerCase(); if (n === 3) { return _shortestColor( Number.parseInt(low[0] + low[0], 16), Number.parseInt(low[1] + low[1], 16), Number.parseInt(low[2] + low[2], 16) ); } if (n === 6) { return _shortestColor( Number.parseInt(low.slice(0, 2), 16), Number.parseInt(low.slice(2, 4), 16), Number.parseInt(low.slice(4, 6), 16) ); } // A fully opaque alpha says nothing, and the form without it asks nothing of // the target's hex-alpha support either. if (n === 4) { if (low[3] !== "f") return `#${low}`; return _shortestColor( Number.parseInt(low[0] + low[0], 16), Number.parseInt(low[1] + low[1], 16), Number.parseInt(low[2] + low[2], 16) ); } if (low[6] === "f" && low[7] === "f") { return _shortestColor( Number.parseInt(low.slice(0, 2), 16), Number.parseInt(low.slice(2, 4), 16), Number.parseInt(low.slice(4, 6), 16) ); } return low[0] === low[1] && low[2] === low[3] && low[4] === low[5] && low[6] === low[7] ? `#${low[0]}${low[2]}${low[4]}${low[6]}` : `#${low}`; }; /** * @param {number} x a number * @returns {number} `x` rounded and clamped to a 0..255 byte */ const _clamp255 = (x) => (x < 0 ? 0 : x > 255 ? 255 : Math.round(x)); /** * Minify an `rgb()` / `rgba()` color function to the shortest opaque form. Only * rgb/rgba (exact integer / percent math); returns null for anything else — * a different function, non-numeric args, or a partly-transparent alpha — * leaving it as its normalized function form. Transparent black collapses to the * `transparent` keyword (identical value, universally supported, shorter). hsl is * intentionally not converted: its hue math can round to a different byte, which * would change the color. * @param {string} fn the lowercased function name * @param {string} inner the already-joined argument text * @param {boolean} hexAlpha whether the target reads a 4-/8-digit hex (see `output.environment`) * @returns {string | null} the shortest color, or null to keep the function */ const _minifyColorFunction = (fn, inner, hexAlpha) => { if ((fn !== "rgb" && fn !== "rgba") || !_transforms.shortenColors) { return null; } // Cut on the separators `rgb()` allows — whitespace, `,`, and the `/` before // the alpha — in one pass. Every color in the sheet reaches this, and the // regex form built a replaced copy of the arguments, a split array and a // filtered copy of that to reach the same few strings. /** @type {string[]} */ const args = []; let from = -1; for (let i = 0; i <= inner.length; i++) { // Spelled out rather than asked for: a call per character loses to the // regex engine outright, and anything looser than the five whitespace // code points parts a token CSS does not — a control character between // two numbers leaves an invalid declaration, which must stay invalid. const c = inner.charCodeAt(i); if ( i === inner.length || c === CC_SPACE || c === CC_TAB || c === CC_LINE_FEED || c === CC_CARRIAGE_RETURN || c === CC_FORM_FEED || c === CC_COMMA || c === CC_SOLIDUS ) { if (from === -1) continue; // A fifth argument is not a color this rewrite is defined for. if (args.length === 4) return null; args.push(inner.slice(from, i)); from = -1; } else if (from === -1) { from = i; } } if (args.length !== 3 && args.length !== 4) return null; /** @type {number[]} */ const channels = []; let alpha = 1; let percentChannelCount = 0; for (let i = 0; i < args.length; i++) { const s = args[i]; if (!/^[+-]?(?:\d+\.?\d*|\.\d+)%?$/.test(s)) return null; const pct = s.endsWith("%"); const v = Number.parseFloat(pct ? s.slice(0, -1) : s); if (Number.isNaN(v)) return null; if (i === 3) { alpha = pct ? v / 100 : v; } else { if (pct) percentChannelCount++; channels.push(_clamp255(pct ? (v * 255) / 100 : v)); } } // Mixed number/percentage channels are invalid CSS (ignored by browsers); // rewriting would activate a dead declaration. if (percentChannelCount !== 0 && percentChannelCount !== 3) return null; // Fully transparent *black* only — `transparent` is `rgba(0,0,0,0)`, so a // non-black transparent (e.g. `rgba(255,0,0,0)`) is a different value. if ( alpha === 0 && channels[0] === 0 && channels[1] === 0 && channels[2] === 0 ) { return "transparent"; } // A hex alpha is the same color: the engine quantizes the channel to 8 bits, // so the byte a decimal alpha lands on is the one it already stores — // `.5`, `.501`, `.502` and `.5019` all compute to `rgba(0,0,0,0.5)` in // headless Chromium, and every one of the 256 bytes round-trips. if (alpha < 1) { return hexAlpha ? _shortestAlphaColor( channels[0], channels[1], channels[2], _clamp255(alpha * 255) ) : null; } return _shortestColor(channels[0], channels[1], channels[2]); }; // A browser list's parse, and each list's per-property prefix decision, memoized // on the list identity: one process minifies for one target, so the work is done // once and every later lookup is a `Map.get`. // A build minifies every asset against one browserslist selection, but in a // worker each asset arrives with a freshly deserialized array — so the memo is // keyed on the joined text, and one selection at a time is all it ever holds. /** @type {string[] | null} */ let _browsersArray = null; /** @type {string | null} */ let _browsersKey = null; /** @type {(number[] | undefined)[] | null} */ let _parsedBrowsersMemo = null; // Each axis' table -> its per-construct decision for the selection in hand. Kept // per table rather than under one tagged key, so a lookup allocates no string. /** @type {Map, Map | null>>} */ let _neededPrefixMemo = new Map(); // Where a browser's version sits in a `SUPPORT_PROFILES` row, which is also the // slot a prefix window names it by and the one the parsed selection holds it in. // Built from the order the rows are stated in, so the three cannot drift apart. /** @type {Map} */ const SUPPORT_BROWSER_SLOT = new Map( SUPPORT_BROWSERS.map((browser, at) => [browser, at]) ); /** * Parse a browserslist selection into the versions selected for each browser * slot, memoized on the selection itself, and make it the one this parse * prefixes for. Null where it resolves to no browser at all, which is not a * target to prefix for. * @param {string[]} browsers the browserslist selection * @returns {void} */ const _useBrowsers = (browsers) => { // The same selection usually arrives as the same array — one resolution per // target serves the whole build — so the text it joins to is read only where // it does not, which is a worker handed each asset's own copy. if (browsers === _browsersArray) { _prefixBrowsers = _parsedBrowsersMemo; return; } _browsersArray = browsers; const key = browsers.join(","); if (key !== _browsersKey) { _browsersKey = key; _neededPrefixMemo = new Map(); // By the slot the tables name a browser with, so neither the profile rows // nor the prefix windows carry a name to look one up by. /** @type {(number[] | undefined)[]} */ const parsed = Array.from({ length: SUPPORT_BROWSERS.length }); let named = 0; for (const entry of browsers) { const space = entry.indexOf(" "); const name = space === -1 ? entry : entry.slice(0, space); const version = space === -1 ? null : _encodeBrowserVersion(entry.slice(space + 1)); // A browserslist name no compat dataset covers (`op_mini`, `and_uc`, // `and_qq`, `baidu`, `kaios`, `bb`), or a version that did not parse, is // skipped — the same browsers lightningcss's target mapping drops, so both // minifiers prefix for the same selection. const slot = version === null ? undefined : SUPPORT_BROWSER_SLOT.get(name); if (slot === undefined) continue; // Every selected version is kept: a prefix window is an interval, so an // older selection outside it does not answer for a newer one inside. const versions = parsed[slot]; if (versions === undefined) { parsed[slot] = [/** @type {number} */ (version)]; named++; } else { versions.push(/** @type {number} */ (version)); } } _parsedBrowsersMemo = named === 0 ? null : parsed; } _prefixBrowsers = _parsedBrowsersMemo; }; /** * A browserslist version to the same `major * 100000 + minor` integer the table * is keyed in. A range (`10.0-10.2`) takes its low end, Safari `TP` is newest, * and `all` (Opera Mini) is oldest (`0`). * @param {string} version the version part of a `"name version"` entry * @returns {number | null} the encoded version, or null when unreadable */ const _encodeBrowserVersion = (version) => { // Newer than any real version, but below `NEVER` — a still-prefixed feature // carries `to === NEVER`, and `TP < NEVER` must hold for TP to need it. if (version === "TP") return NEVER - 1; if (version === "all") return 0; const dash = version.indexOf("-"); const low = dash === -1 ? version : version.slice(0, dash); const dot = low.indexOf("."); const major = Number.parseInt(dot === -1 ? low : low.slice(0, dot), 10); if (Number.isNaN(major)) return null; const minor = dot === -1 ? 0 : Number.parseInt(low.slice(dot + 1), 10) || 0; return major * 100000 + minor; }; /** * Whether every target browser reads a CSS ability, so a spelling that needs it * may be reached for. No selection names no browser to answer for, so the * ability is assumed — a build with no browserslist keeps every rewrite. A * browser the table does not name is one nothing states support for, which * answers no. * @param {string} feature a `SUPPORTED_FROM` key * @returns {boolean} true when the selection reads it */ const _targetSupports = (feature) => _readsAll(SUPPORTED_FROM.get(feature)); /** * Whether every target browser is at or past the versions in one support * profile, named by the row it reads. * @param {number | undefined} profile a `SUPPORT_PROFILES` index * @returns {boolean} true when the selection reads it */ const _readsAll = (profile) => { const parsed = _prefixBrowsers; if (parsed === null) return true; if (profile === undefined) return false; // One row of `SUPPORT_BROWSERS.length` versions, laid end to end with the rest. const row = profile * SUPPORT_BROWSERS.length; for (let at = 0; at < parsed.length; at++) { const versions = parsed[at]; if (versions === undefined) continue; const from = SUPPORT_PROFILES[row + at]; for (let i = 0; i < versions.length; i++) { if (versions[i] < from) return false; } } return true; }; /** * The vendor spellings at least one target browser still needs for a construct — * a target at version V needs spelling S when some `[browser, from, to]` of S has * `from <= V < to`. Null when not minifying for a target (leave prefixes alone), * the construct is never prefixed, or no target needs any of its spellings. * @param {Map} table its axis' prefix table * @param {string} name the construct name (unprefixed, lowercased) * @returns {Set | null} the needed spellings, or null */ const _neededPrefixes = (table, name) => { const parsed = _prefixBrowsers; if (parsed === null) return null; let cache = _neededPrefixMemo.get(table); if (cache === undefined) { cache = new Map(); _neededPrefixMemo.set(table, cache); } const cached = cache.get(name); if (cached !== undefined) return cached; const prefixes = table.get(name); /** @type {Set | null} */ let result = null; if (prefixes !== undefined) { const needed = new Set(); for (const [prefix, windows] of prefixes) { // The list's own `browser, from, to` triples, in the flat table the // starts index into. const end = PREFIX_WINDOW_STARTS[windows + 1]; for (let at = PREFIX_WINDOW_STARTS[windows]; at < end; at += 3) { const versions = parsed[PREFIX_WINDOWS[at]]; if (versions === undefined) continue; const from = PREFIX_WINDOWS[at + 1]; const to = PREFIX_WINDOWS[at + 2]; let hit = false; for (let i = 0; i < versions.length; i++) { if (versions[i] >= from && versions[i] < to) { hit = true; break; } } if (hit) { needed.add(prefix); break; } } } if (needed.size !== 0) result = needed; } cache.set(name, result); return result; }; // A vendor prefix at the start of a name (`-webkit-`, `-moz-`, `-ms-`, `-o-`, // `-khtml-`), captured so it can be split from the construct it sits on. const VENDOR_PREFIX = /^(-[a-z]+-)(?=[a-z])/; /** * The base an at-rule's vendor spelling belongs to. Every at-rule spelling is * its base with a prefix on it, so the prefix comes off again. * @param {string} name a prefixed at-rule name, lowercased * @returns {string} the unprefixed name */ const _unprefixedAtRule = (name) => name.slice( /** @type {RegExpExecArray} */ (VENDOR_PREFIX.exec(name))[1].length ); /** * Whether a present vendor-spelled construct is dead weight: its spelling is one * the table knows for the base construct and no target browser needs it, so an * unprefixed sibling already covers every target. * @param {Map} table its axis' prefix table * @param {string} base the unprefixed construct name * @param {string} spelling the vendor spelling found in its place * @returns {boolean} true when the vendor-spelled construct can be dropped */ const _prefixRemovable = (table, base, spelling) => { const spellings = table.get(base); if (spellings === undefined) return false; if (!spellings.some(([known]) => known === spelling)) return false; const needed = _neededPrefixes(table, base); return needed === null || !needed.has(spelling); }; /** * What a block's rules have shown so far: the signatures met (`seen`), and the * prefixed ones an unprefixed twin would make dead — as an output piece the * writer can take back (`retractable`, for a rule written straight out), or as * the node itself (`pending`, for a rule whose parent still has to assemble it). * @typedef {object} PrefixScope * @property {Set} seen the signatures met, and the `signature\0prefix` * markers of the prefixed spellings among them * @property {Map | null} retractable each dead-if-twinned rule's * piece index, by the signature of the twin that would make it dead * @property {Map | null} pending each dead-if-twinned nested rule, * by that same signature * @property {Set | null} dead the nested rules a twin has since made dead, * read by their parent as it assembles its body */ /** * The rules already met in the block a rule sits in, made on first use. * @param {Node | null} parent the block's rule, null for the stylesheet itself * @returns {PrefixScope} the block's running sibling state */ const _prefixScope = (parent) => { const scopes = /** @type {Map} */ ( _seenPrefixRules ); let scope = scopes.get(parent); if (scope === undefined) { scope = { seen: new Set(), retractable: null, pending: null, dead: null }; scopes.set(parent, scope); } return scope; }; /** * Drop the prefixed rule this one is the unprefixed twin of. The pair is usually * adjacent — every stylesheet writes the prefixed spelling first — but nothing * in between matters: a piece stays retractable until the stylesheet ends, and a * nested rule until its parent assembles its body. * @param {PrefixScope} scope the block's running sibling state * @param {string} signature the unprefixed rule's sibling signature * @returns {void} */ const _dropPrefixTwin = (scope, signature) => { const retractable = scope.retractable; if (retractable !== null) { const at = retractable.get(signature); if (at !== undefined) { /** @type {PrintContext} */ (_streamWriter).retract(at); retractable.delete(signature); return; } } const pending = scope.pending; if (pending === null) return; const node = pending.get(signature); if (node === undefined) return; pending.delete(signature); if (scope.dead === null) scope.dead = new Set(); scope.dead.add(node); }; /** * Remember a prefixed rule an unprefixed twin later in its block would make dead * weight, both ways it can be dropped: as the rule just printed, which whoever * writes it out takes as a piece of its own, and — for a nested one — as the * node its parent skips while assembling its body, for the parents that do. * @param {PrefixScope} scope the block's running sibling state * @param {string} signature the rule's sibling signature * @param {boolean} top whether the rule is the stylesheet's own * @returns {void} */ const _holdPrefixTwin = (scope, signature, top) => { _prefixDropCandidate = { node: _currentNode, signature }; if (top) return; if (scope.pending === null) scope.pending = new Map(); scope.pending.set(signature, _currentNode); }; /** * An at-rule (`@keyframes`), prefixed against the target: a prefixed copy is * prepended for each prefix a target still needs, and a prefixed rule no target * needs is dropped once its unprefixed twin has been seen. The `@name`'s prefix * is stripped for the sibling signature, so `@-webkit-keyframes x` and * `@keyframes x` pair up. Siblings are the rules of the block it sits in. * @param {CssPath} path the accessor on the at-rule * @param {string} ruleText the rule's own serialized text * @param {string} prelude the rule's serialized prelude (`@name …`) * @param {PrefixScope} scope the block's running sibling state * @param {boolean} top whether the rule is the stylesheet's own, which is the only * one whose text is still a piece a twin can take back * @returns {string} the rule text, with prefixed copies added or itself dropped */ const _prefixAtRule = (path, ruleText, prelude, scope, top) => { const seen = scope.seen; const name = path.name().toLowerCase(); const prefixed = VENDOR_PREFIX.test(name); const base = prefixed ? _unprefixedAtRule(name) : name; if (!PREFIXED_AT_RULES.has(base)) return ruleText; // This rule's cross-prefix identity: the prelude with the `@name` folded to its // unprefixed, lowercased spelling, so a cased `@Keyframes` and a prefixed // `@-webkit-keyframes` share it (at-rule names are case-insensitive). const signature = `@${base}${prelude.slice(1 + name.length)}`; if (prefixed) { const removable = _prefixRemovable(PREFIXED_AT_RULES, base, name); if (seen.has(signature) && removable) return ""; seen.add(`${signature}\0${name}`); // Its twin may still be a later rule, which is where it is dropped. if (removable) _holdPrefixTwin(scope, signature, top); return ruleText; } seen.add(signature); _dropPrefixTwin(scope, signature); const needed = _neededPrefixes(PREFIXED_AT_RULES, base); if (needed === null) return ruleText; let out = ""; for (const spelling of needed) { // Skip only a spelling the source itself already carries (marked when the // prefixed at-rule is met); a copy is still added for every unprefixed rule // of this signature, so a later `@keyframes` that overrides an earlier keeps // its prefixed twin winning too. if (seen.has(`${signature}\0${spelling}`)) continue; out += `@${spelling}${ruleText.slice(1 + name.length)}`; } return out + ruleText; }; // The prefixed spelling of a prefixable selector back to `[base, prefix]` // (`-webkit-input-placeholder` -> `["placeholder", "-webkit-input-"]`), built // once from the forward table so a prefixed selector can be recognized for // removal. BCD's prefix concatenates onto the base, so the spelling is exact. /** @type {Map | null} */ let _prefixedSelectorNames = null; /** * @param {string} name a selector's pseudo name * @returns {[string, string] | undefined} its `[base, prefix]` when prefixed */ const _prefixedSelectorName = (name) => { if (_prefixedSelectorNames === null) { _prefixedSelectorNames = new Map(); for (const [base, spellings] of PREFIXED_SELECTORS) { for (const [spelling] of spellings) { _prefixedSelectorNames.set(spelling, [base, spelling]); } } } return _prefixedSelectorNames.get(name); }; // A property's vendor spelling back to the property it stands for, built once // from the forward table. Read rather than derived: an engine as often renamed // the property (`-ms-flex-order` for `order`) as prefixed its name, and only a // prefix can be stripped back off. /** @type {Map | null} */ let _prefixedPropertyNames = null; /** * @param {string} property a declaration's property name * @returns {string | undefined} the property it is a vendor spelling of */ const _prefixedPropertyName = (property) => { if (_prefixedPropertyNames === null) { _prefixedPropertyNames = new Map(); for (const [base, spellings] of PREFIXED_PROPERTIES) { for (const [spelling] of spellings) { _prefixedPropertyNames.set(spelling, base); } } } return _prefixedPropertyNames.get(property); }; /** * One prefixed copy of a declaration, for a spelling a target still needs. A * spelling whose older property read other keywords carries them with it, and * writes nothing at all for a value that property cannot read — the map is its * whole grammar. * @param {string} spelling the vendor spelling to write * @param {string} text the declaration's printed text * @param {number} colon where its `:` sits * @returns {string} the copy, empty where none can be written */ const _prefixedDeclaration = (spelling, text, colon) => { const keywords = PREFIXED_SPELLING_KEYWORDS.get(spelling); if (keywords === undefined) return spelling + text.slice(colon); const end = _printedValueEnd(text, colon); const value = toLowerCaseIfNeeded(text.slice(colon + 1, end)); // A CSS-wide keyword is every property's, whatever its own grammar says. if (CSS_WIDE_KEYWORDS.has(value)) return spelling + text.slice(colon); const legacy = keywords.get(value); // `!important` and the `;` carry over; the value between them is rewritten. return legacy === undefined ? "" : `${spelling}:${legacy}${text.slice(end)}`; }; // A selector prelude part split at its `(`: a functional pseudo prints as one // token (`dir(rtl)`), so its name is matched and rewritten apart from the // argument that carries over unchanged. A bare pseudo has no `(`. /** * @param {string} part a printed prelude token * @returns {string} its name, without any `(argument)` */ const _selectorPartName = (part) => { const paren = part.indexOf("("); return paren === -1 ? part : part.slice(0, paren); }; // The shape of every name some engine spells its own way: one bit per length, // under the pair of letters it starts with. A declaration whose first two // letters and length are not one of those can be neither prefixed nor // value-spelled, so its name is never read — which is most of them, `content`, // `opacity` and `margin-left` among the commonest. 676 slots, filled the first // time a parse prefixes for a target and kept after. /** @type {Int32Array | null} */ let _prefixableNameShapes = null; /** * @returns {Int32Array} the lengths, by the two letters a name starts with */ const _prefixableNames = () => { if (_prefixableNameShapes === null) { const shapes = new Int32Array(26 * 26); for (const table of [PREFIXED_PROPERTIES, PREFIXED_VALUES]) { for (const name of table.keys()) { const slot = _nameShapeSlot(name.charCodeAt(0), name.charCodeAt(1)); if (slot === -1) continue; shapes[slot] |= 1 << (name.length > 31 ? 31 : name.length); } } _prefixableNameShapes = shapes; } return _prefixableNameShapes; }; /** * The shape table's slot for a name's first two characters, or `-1` where they * are not both ASCII letters. Names match ASCII case-insensitively, so `A`-`Z` * reads as its lowercase. * @param {number} first the first character's code * @param {number} second the second character's code * @returns {number} the slot, or -1 */ const _nameShapeSlot = (first, second) => { const letter = (first | 0x20) - CC_LOWER_A; if (letter < 0 || letter > 25) return -1; const next = (second | 0x20) - CC_LOWER_A; return next < 0 || next > 25 ? -1 : letter * 26 + next; }; /** * Whether a printed declaration's name is worth reading — one some engine * spells its own way, or a spelling itself. * @param {string} text the declaration's printed text * @param {number} colon where its `:` sits * @returns {boolean} true when the name may take part in prefixing */ const _mayPrefixDeclaration = (text, colon) => { // A spelling to drop leads with `-`; a custom property leads with `--` and is // neither prefixed nor spelled. if (text.charCodeAt(0) === CC_HYPHEN_MINUS) { return text.charCodeAt(1) !== CC_HYPHEN_MINUS; } const slot = _nameShapeSlot(text.charCodeAt(0), text.charCodeAt(1)); if (slot === -1) return false; const lengths = _prefixableNames(); return ((lengths[slot] >>> (colon < 0 || colon > 31 ? 31 : colon)) & 1) !== 0; }; /** * The property a printed declaration sets, read from its text: a merged box * shorthand writes a different property than the node it is stored on. * @param {string} text the declaration's printed text * @param {number} colon where its `:` sits * @returns {string} the lowercased property name */ const _printedProperty = (text, colon) => toLowerCaseIfNeeded(colon === -1 ? text : text.slice(0, colon)); // What a printed declaration carries after its value: the `;` closing it, and // the `!important` before that where it has one (minifying leaves no space). const _IMPORTANT = "!important"; /** * The value a printed declaration sets, read from its text — the whole value, so * only a declaration that is one keyword matches a keyword. * @param {string} text the declaration's printed text * @param {number} colon where its `:` sits * @returns {string} the lowercased value */ const _printedValue = (text, colon) => toLowerCaseIfNeeded(text.slice(colon + 1, _printedValueEnd(text, colon))); /** * A comma list's top-level items. What a later declaration has to write again for * an earlier one to be dead rather than the fallback an engine reads instead. * @param {string} value one printed declaration value * @returns {string[]} its items, in order */ const _valueItems = (value) => { /** @type {string[]} */ const out = []; let depth = 0; let quote = 0; let start = 0; for (let i = 0; i <= value.length; i++) { const cc = i === value.length ? CC_COMMA : value.charCodeAt(i); if (quote !== 0) { if (cc === CC_REVERSE_SOLIDUS) i++; else if (cc === quote) quote = 0; continue; } if (cc === CC_QUOTATION_MARK || cc === CC_APOSTROPHE) { quote = cc; } else if (cc === CC_LEFT_PARENTHESIS) { depth++; } else if (cc === CC_RIGHT_PARENTHESIS) { depth--; } else if (cc === CC_COMMA && depth === 0) { out.push(value.slice(start, i).trim()); start = i + 1; } } return out; }; // A vendor prefix on an item's first token, which is the name slot a // `` list reads there. const _VENDOR_ITEM_PREFIX_RE = /^-[a-z]+-/i; /** * Whether a later declaration of a `` list leaves an earlier one * unreadable rather than standing as its fallback: every item the earlier writes * is written again, and every item the later adds is one of them under another * vendor spelling. A name is what that slot takes, so an engine knowing none of * the spellings still parses the value — there is nothing to fall back to. * `transition:box-shadow .25s` before `transition:box-shadow .25s,-webkit-box-shadow .25s` * is the shape, which is what a tool adding prefixes writes. * @param {string} later the later declaration's printed value * @param {string} earlier the earlier declaration's printed value * @returns {boolean} true when the earlier one can no longer be read */ const _coveredByLater = (later, earlier) => { const laterItems = _valueItems(later); const earlierItems = _valueItems(earlier); if (earlierItems.length === 0 || laterItems.length < earlierItems.length) { return false; } for (const item of earlierItems) { if (!laterItems.includes(item)) return false; } for (const item of laterItems) { if (earlierItems.includes(item)) continue; const bare = item.replace(_VENDOR_ITEM_PREFIX_RE, ""); let variant = false; for (const other of earlierItems) { if (other.replace(_VENDOR_ITEM_PREFIX_RE, "") === bare) { variant = true; break; } } if (!variant) return false; } return true; }; /** * Where a printed declaration's value ends: before the `!important` it may carry * and the `;` closing it. * @param {string} text the declaration's printed text * @param {number} colon where its `:` sits * @returns {number} the offset one past the value */ const _printedValueEnd = (text, colon) => { let end = text.length; if (text.charCodeAt(end - 1) === CC_SEMICOLON) end--; const bang = end - _IMPORTANT.length; if (bang > colon && text.startsWith(_IMPORTANT, bang)) end = bang; return end; }; // A property's vendor value spellings back to the keyword each stands for, built // on first use from the forward table. /** @type {Map, Map>} */ const _prefixedValueKeywords = new Map(); /** * @param {Map} table one property's value table * @param {string} value a value that may be a vendor spelling * @returns {string | undefined} the keyword it spells, when it is one */ const _prefixedValueKeyword = (table, value) => { let reverse = _prefixedValueKeywords.get(table); if (reverse === undefined) { reverse = new Map(); for (const [keyword, spellings] of table) { for (const [spelling] of spellings) reverse.set(spelling, keyword); } _prefixedValueKeywords.set(table, reverse); } return reverse.get(value); }; /** * Whether a prelude token is a pseudo some engine spells with a prefix. It must * sit right after a `:` for the caller to ask, so a class of the same spelling is * not mistaken for it; the name matches ASCII case-insensitively, as property and * at-rule names do. * @param {string} part a printed prelude token * @returns {boolean} true when the table knows it, prefixed or not */ const _prefixablePseudo = (part) => { const name = toLowerCaseIfNeeded(_selectorPartName(part)); return ( PREFIXED_SELECTORS.has(name) || _prefixedSelectorName(name) !== undefined ); }; /** * One selector of a rule's prelude: the tokens it spans, and the sole prefixable * pseudo in it — `-1` where it has none, `-2` where it has more than one, which * leaves the whole rule alone. * @typedef {object} PrefixableSelector * @property {number} start its first prelude token * @property {number} end one past its last * @property {number} at the prefixable pseudo's token, or -1 / -2 */ /** * Split a prelude into its selectors, marking the prefixable pseudo in each. * @param {string[]} parts the printed prelude tokens * @returns {PrefixableSelector[] | null} the selectors, or null when none carries one */ const _prefixableSelectors = (parts) => { // Nothing is built for a prelude that carries no prefixable pseudo, which is // nearly every one: the scan below only reads the tokens after a `:`. let any = false; for (let i = 1; i < parts.length; i++) { if (parts[i - 1] === ":" && _prefixablePseudo(parts[i])) { any = true; break; } } if (!any) return null; /** @type {PrefixableSelector[]} */ const selectors = []; let start = 0; let at = -1; for (let i = 0; i <= parts.length; i++) { if (i === parts.length || parts[i] === ",") { // Whitespace around the comma says nothing, and would print back as a // space inside a rewritten list. let from = start; let to = i; while (from < to && parts[from] === _SEP) from++; while (to > from && parts[to - 1] === _SEP) to--; selectors.push({ start: from, end: to, at }); start = i + 1; at = -1; continue; } if (i !== 0 && parts[i - 1] === ":" && _prefixablePseudo(parts[i])) { at = at === -1 ? i : -2; } } return selectors; }; /** * A qualified rule prefixed against the target, for each selector of its list * that is a prefixable pseudo (`::placeholder`): a copy carrying the pseudo's * engine spelling is prepended for each prefix a target needs, and a * prefixed-only rule no target needs is dropped once its unprefixed twin has * been seen. A copy holds only the selectors that need that one prefix — an * engine drops a whole list over one selector it cannot parse, so a copy must * never mix spellings. The author's colons carry over. * @param {string} ruleText the rule's own serialized text * @param {string[]} parts the printed prelude tokens * @param {string} soft the space before `{` (empty minifying) * @param {string} body the rule's serialized block body * @param {PrefixScope} scope the block's running sibling state * @param {boolean} top whether the rule is the stylesheet's own, which is the only * one whose text is still a piece a twin can take back * @returns {string} the rule text, with prefixed copies added or itself dropped */ const _prefixQualifiedRule = (ruleText, parts, soft, body, scope, top) => { const seen = scope.seen; const selectors = _prefixableSelectors(parts); if (selectors === null) return ruleText; // A functional pseudo (`dir(rtl)`) keeps its argument; only the name is // matched and swapped, and the argument is part of the sibling signature so // `:dir(rtl)` and `:dir(ltr)` stay distinct. /** @type {string[]} */ const bases = []; /** @type {string[]} */ const args = []; /** @type {(string | undefined)[]} */ const carried = []; /** @type {string | undefined} */ let listPrefix; for (const selector of selectors) { const at = selector.at; if (at === -2) return ruleText; if (at === -1) { bases.push(""); args.push(""); carried.push(undefined); continue; } const raw = parts[at]; const nameOnly = toLowerCaseIfNeeded(_selectorPartName(raw)); const found = _prefixedSelectorName(nameOnly); // One spelling for the whole list: a list mixing a vendor-spelled pseudo // with a plainly spelled one, or with a second spelling, is neither this // rule's twin nor a copy of it. if (found !== undefined) { if (listPrefix !== undefined && listPrefix !== found[1]) return ruleText; listPrefix = found[1]; } bases.push(found === undefined ? nameOnly : found[0]); args.push(raw.slice(_selectorPartName(raw).length)); carried.push(found === undefined ? undefined : found[1]); } /** * @param {(index: number) => string | null} spell each selector's pseudo, or null to leave it out * @returns {string} the prelude those selectors print as */ const preludeOf = (spell) => { /** @type {string[]} */ const out = []; for (let i = 0; i < selectors.length; i++) { const name = spell(i); if (name === null) continue; if (out.length !== 0) out.push(","); const { start, end, at } = selectors[i]; for (let j = start; j < end; j++) out.push(j === at ? name : parts[j]); } return _join(out, false, _TRIM_COMBINATORS); }; // This rule's cross-prefix identity: every prefixable pseudo folded to its // unprefixed spelling, so a prefixed list and its twin pair up. const signature = `s${preludeOf((i) => selectors[i].at === -1 ? "" : bases[i] + args[i] )}`; if (listPrefix !== undefined) { // Every pseudo of the list carries that spelling, and each is dead weight on // its own. const removable = bases.every( (base, i) => selectors[i].at === -1 || _prefixRemovable( PREFIXED_SELECTORS, base, /** @type {string} */ (listPrefix) ) ); if (seen.has(signature) && removable) return ""; seen.add(`${signature}\0${listPrefix}`); // Its twin may still be a later rule, which is where it is dropped. if (removable) _holdPrefixTwin(scope, signature, top); return ruleText; } seen.add(signature); _dropPrefixTwin(scope, signature); /** @type {(Set | null)[]} */ // One copy per spelling, never per engine: an engine that does not know one of // a list's selectors drops the list whole, and two names of one engine arrived // in different versions — `::-webkit-input-placeholder` in Chrome 6 and // `:-webkit-full-screen` in 15, so a list of both is nothing to Chrome 6 // through 14. Selectors that take the same spelling take the same versions // with it, so those do share a copy. /** @type {(Set | null)[]} */ const needed = []; /** @type {Set | null} */ let spellings = null; for (let i = 0; i < selectors.length; i++) { const one = selectors[i].at === -1 ? null : _neededPrefixes(PREFIXED_SELECTORS, bases[i]); needed.push(one); if (one === null) continue; if (spellings === null) spellings = new Set(); for (const spelling of one) spellings.add(spelling); } if (spellings === null) return ruleText; let out = ""; for (const spelling of spellings) { // Skip only a spelling the source itself already carries (marked above when // the vendor-spelled rule is met); a copy is still added for every // unprefixed rule of this signature, so a later one that overrides an // earlier keeps its prefixed twin winning too. if (seen.has(`${signature}\0${spelling}`)) continue; const list = preludeOf((i) => { const one = needed[i]; return one === null || !one.has(spelling) ? null : spelling + args[i]; }); out += `${list}${soft}{${body}}`; } return out + ruleText; }; // A plain number, optionally a percentage — the only argument form these color // conversions are proven for. const _COLOR_NUMBER_RE = /^([+-]?(?:\d+\.?\d*|\.\d+))(%?)$/; // The hue's default unit; the other angle units would need their own conversion // before the same boundary test could be applied. const _COLOR_HUE_RE = /^([+-]?(?:\d+\.?\d*|\.\d+))(?:deg)?$/i; // How far a channel must sit from a `.5` rounding boundary to be converted. // Implementations only ever disagree at a tie: across 317520 `hsl()` / `hwb()` // samples checked against headless Chromium, every one of the divergences was // within 9.24e-14 of a boundary, and none of the 264084 converted outside it was // wrong. The margin is many orders above that and far below a visible difference. const _ROUNDING_MARGIN = 1e-6; // The Lab family needs a far wider one. Its conversion is a chain of matrices // rather than a handful of multiplications, and webpack's chain and Chromium's // disagree by up to 0.035 of a byte — measured over ~6000 `lab()` / `lch()` / // `oklab()` / `oklch()` samples read back from a canvas. This is ~3x that bound. const _LAB_ROUNDING_MARGIN = 0.1; /** * Round a raw 0..255 channel, or return null when it sits close enough to a `.5` * boundary that another implementation's last bit could round it the other way. * @param {number} raw the channel, 0..255 * @param {number} margin how far from a boundary the channel must sit * @returns {number | null} the byte, or null to keep the function */ const _channelByte = (raw, margin) => { // Past this the color is one sRGB cannot show, so clamping it into a hex would // pick a different color rather than respell the same one. Inside it, clamping // lands on the byte rounding would have picked anyway — which is what lets a // Lab white point a few 1e-6 past 255, or a channel a hundredth of a byte // under 0, still convert. if (raw < -0.5 || raw > 255.5) return null; if (Math.abs(Math.abs(raw - Math.floor(raw)) - 0.5) < margin) return null; const byte = Math.round(raw); return byte < 0 ? 0 : byte > 255 ? 255 : byte; }; /** * CSS Color 4 §7.1's `hsl()` -> sRGB, on 0..1 inputs, in 0..255 output. * @param {number} h hue in degrees, already wrapped to 0..360 * @param {number} s saturation 0..1 * @param {number} l lightness 0..1 * @returns {number[]} the raw `[r, g, b]` channels, 0..255 */ const _hslToRgb = (h, s, l) => { const a = s * Math.min(l, 1 - l); return [0, 8, 4].map((n) => { const k = (n + h / 30) % 12; return (l - a * Math.max(-1, Math.min(k - 3, 9 - k, 1))) * 255; }); }; /** * CSS Color 4 §7.2's `hwb()` -> sRGB. Whiteness and blackness summing to 1 or * more is the achromatic case the spec spells out separately. * @param {number} h hue in degrees, already wrapped to 0..360 * @param {number} w whiteness 0..1 * @param {number} b blackness 0..1 * @returns {number[]} the raw `[r, g, b]` channels, 0..255 */ const _hwbToRgb = (h, w, b) => { if (w + b >= 1) { const gray = (w / (w + b)) * 255; return [gray, gray, gray]; } return _hslToRgb(h, 1, 0.5).map( (channel) => (channel / 255) * (1 - w - b) * 255 + w * 255 ); }; /** * Linear-light sRGB -> the gamma-encoded channel CSS serializes (CSS Color 4 * §10.2). Out-of-gamut input is returned as-is so the caller's range check sees * it rather than a clipped value. * @param {number} c one linear component * @returns {number} the encoded component, nominally 0..1 */ const _gammaEncode = (c) => { const sign = c < 0 ? -1 : 1; const abs = Math.abs(c); return abs > 0.0031308 ? sign * (1.055 * abs ** (1 / 2.4) - 0.055) : 12.92 * c; }; // The D50 white point Lab is defined against (CSS Color 4 §12), as XYZ. const _D50 = [0.3457 / 0.3585, 1, (1 - 0.3457 - 0.3585) / 0.3585]; // Bradford-adapted D50 -> D65, then XYZ -> linear-light sRGB. Applied as the two // steps CSS Color 4's own sample code uses rather than one composed matrix: the // composition loses enough precision to push `lab(100% 0 0)` outside the gamut // check below. const _D50_TO_D65 = [ [0.9554734527042182, -0.023098536874261423, 0.0632593086610217], [-0.028369706963208136, 1.0099954580058226, 0.021041398966943008], [0.012314001688319899, -0.020507696433477912, 1.3303659366080753] ]; const _XYZ_TO_LINEAR_SRGB = [ [3.2409699419045226, -1.537383177570094, -0.4986107602930034], [-0.9692436362808796, 1.8759675015077202, 0.04155505740717559], [0.05563007969699366, -0.20397695888897652, 1.0569715142428786] ]; /** * @param {number[][]} matrix a 3x3 matrix * @param {number[]} vector the column it multiplies * @returns {number[]} the product */ const _multiply3 = (matrix, vector) => matrix.map( (row) => row[0] * vector[0] + row[1] * vector[1] + row[2] * vector[2] ); /** * CIE Lab -> linear-light sRGB: Lab -> XYZ (D50) -> XYZ (D65) -> linear sRGB, * with the matrices and constants CSS Color 4 §12 gives. * @param {number} l lightness 0..100 * @param {number} a the a axis * @param {number} b the b axis * @returns {number[]} the linear `[r, g, b]` components, nominally 0..1 */ const _labToLinearSrgb = (l, a, b) => { const e = 216 / 24389; const k = 24389 / 27; const fy = (l + 16) / 116; const fx = a / 500 + fy; const fz = fy - b / 200; const xyz = [ (fx ** 3 > e ? fx ** 3 : (116 * fx - 16) / k) * _D50[0], (l > k * e ? fy ** 3 : l / k) * _D50[1], (fz ** 3 > e ? fz ** 3 : (116 * fz - 16) / k) * _D50[2] ]; return _multiply3(_XYZ_TO_LINEAR_SRGB, _multiply3(_D50_TO_D65, xyz)); }; /** * Oklab -> linear-light sRGB (CSS Color 4 §9.2's matrices). * @param {number} l lightness 0..1 * @param {number} a the a axis * @param {number} b the b axis * @returns {number[]} the linear `[r, g, b]` components, nominally 0..1 */ const _oklabToLinearSrgb = (l, a, b) => { const lp = l + 0.3963377773761749 * a + 0.2158037573099136 * b; const mp = l - 0.1055613458156586 * a - 0.0638541728258133 * b; const sp = l - 0.0894841775298119 * a - 1.2914855480194092 * b; const lc = lp ** 3; const mc = mp ** 3; const sc = sp ** 3; return [ 4.076741661347994 * lc - 3.307711590408193 * mc + 0.230969928729428 * sc, -1.2684380040921763 * lc + 2.6097574006633715 * mc - 0.3413193963102197 * sc, -0.004196086541837188 * lc - 0.7034186144594493 * mc + 1.7076147009309444 * sc ]; }; /** * The `` that stands for 100% of each Lab-family axis (CSS Color 4 * §9, §12), as `[lightness, axis]`. * @type {Map} */ const _LAB_SCALES = new Map([ ["lab", [100, 125]], ["lch", [100, 150]], ["oklab", [1, 0.4]], ["oklch", [1, 0.4]] ]); /** * Minify `hsl()` / `hwb()` / `lab()` / `lch()` / `oklab()` / `oklch()` to the * shortest hex. * * Two guards keep the rewrite from changing the color. A channel landing within * `_ROUNDING_MARGIN` of a `.5` boundary keeps the function, because that is the * only place implementations disagree — it is why esbuild and lightningcss emit * different bytes for `hwb(194 0% 0%)`. And a Lab-family color outside the sRGB * gamut keeps its function too: hex cannot express it, so converting would clip * it to a different color. Both are limitations, not correctness gaps — the * declined share is small and every other minifier converts them regardless. * @param {string} fn the lowercased function name * @param {string} inner the already-joined argument text * @param {boolean} hexAlpha whether the target reads a 4-/8-digit hex (see `output.environment`) * @returns {string | null} the shortest color, or null to keep the function */ const _minifyPolarColorFunction = (fn, inner, hexAlpha) => { if (!_transforms.shortenColors) return null; const isHsl = fn === "hsl" || fn === "hsla"; const labScale = _LAB_SCALES.get(fn); if (!isHsl && fn !== "hwb" && labScale === undefined) return null; const args = inner .replace(/\//g, " ") .split(/[\s,]+/) .filter((part) => part.length !== 0); if (args.length !== 3 && args.length !== 4) return null; let alpha = 1; if (args.length === 4) { const parsed = _COLOR_NUMBER_RE.exec(args[3]); if (parsed === null) return null; alpha = Number(parsed[1]) / (parsed[2] === "%" ? 100 : 1); if (!(alpha >= 0 && alpha <= 1)) return null; // A partly transparent color needs the hex-alpha spelling, which is the // target's question, not this one's. if (alpha < 1 && !hexAlpha) return null; } /** @type {number[]} */ let raw; if (isHsl || fn === "hwb") { const hue = _COLOR_HUE_RE.exec(args[0]); const first = _COLOR_NUMBER_RE.exec(args[1]); const second = _COLOR_NUMBER_RE.exec(args[2]); if (hue === null || first === null || second === null) return null; if (first[2] !== "%" || second[2] !== "%") return null; const x = Number(first[1]) / 100; const y = Number(second[1]) / 100; if (x < 0 || x > 1 || y < 0 || y > 1) return null; let h = Number(hue[1]) % 360; if (h < 0) h += 360; raw = isHsl ? _hslToRgb(h, x, y) : _hwbToRgb(h, x, y); } else { const scale = /** @type {number[]} */ (labScale); const isPolar = fn === "lch" || fn === "oklch"; const lightness = _COLOR_NUMBER_RE.exec(args[0]); const second = _COLOR_NUMBER_RE.exec(args[1]); const third = isPolar ? _COLOR_HUE_RE.exec(args[2]) : _COLOR_NUMBER_RE.exec(args[2]); if (lightness === null || second === null || third === null) return null; const l = Number(lightness[1]) * (lightness[2] === "%" ? scale[0] / 100 : 1); let a; let b; if (isPolar) { const chroma = Number(second[1]) * (second[2] === "%" ? scale[1] / 100 : 1); const hue = (Number(third[1]) * Math.PI) / 180; a = chroma * Math.cos(hue); b = chroma * Math.sin(hue); } else { a = Number(second[1]) * (second[2] === "%" ? scale[1] / 100 : 1); b = Number(third[1]) * (third[2] === "%" ? scale[1] / 100 : 1); } const linear = fn === "lab" || fn === "lch" ? _labToLinearSrgb(l, a, b) : _oklabToLinearSrgb(l, a, b); // `_channelByte` decides the gamut from the encoded value, where the // question is whether clamping would move the byte. raw = linear.map((c) => _gammaEncode(c) * 255); } const margin = labScale === undefined ? _ROUNDING_MARGIN : _LAB_ROUNDING_MARGIN; /** @type {number[]} */ const channels = []; for (const value of raw) { const byte = _channelByte(value, margin); if (byte === null) return null; channels.push(byte); } if (alpha < 1) { // The alpha is the author's own number rather than a conversion's output, so // it takes no boundary guard: the engine quantizes it to 8 bits either way, // exactly as `rgba()`'s does. return _shortestAlphaColor( channels[0], channels[1], channels[2], _clamp255(alpha * 255) ); } return _shortestColor(channels[0], channels[1], channels[2]); }; // Some mobile WebKit builds honor `-webkit-tap-highlight-color:rgba(0,0,0,0)` // but ignore the equivalent `transparent`, so that one property keeps the // function form (cssnano guards the same property). /** * @returns {boolean} whether the value being printed belongs to `-webkit-tap-highlight-color` */ const _inTapHighlightColor = () => _valueDeclaration !== null && A.name(_valueDeclaration).toLowerCase() === "-webkit-tap-highlight-color"; /** * Write one `:nth-*()` in its shortest equal spelling: the An+B microsyntax * carries its own whitespace and signs, two keywords name a step of two, and a * step selecting exactly one child is the child that has its own name. * @param {string} name the lowercased function name * @param {string} inner the already-joined argument text * @returns {string} the whole replacement, `name(inner)` when nothing is shorter */ const _minifyAnPlusB = (name, inner) => { if (!_transforms.shortenSelectors) return `${name}(${inner})`; // `An+B of S` selects among S, which no plain spelling names. if (/\bof\b/i.test(inner)) return `${name}(${inner})`; const nth = _minifyNth(inner); if (nth === null) return `${name}(${inner})`; const first = NTH_NAMED_EQUIVALENTS.get(name); return nth === "1" && first !== undefined ? first : `${name}(${nth})`; }; /** * Whether the declaration being printed is the font-family longhand, the one * place a `` stands on its own rather than among other slots. * @returns {boolean} true inside such a declaration */ const _inFontFamily = () => _valueDeclaration !== null && equalsLowerCase(A.name(_valueDeclaration), "font-family"); /** * Whether the string is the whole entry in its comma-separated slot: * `` takes ` | +`, never a mix of the two. * @param {CssPath} path the accessor positioned on the string token * @returns {boolean} true when nothing else shares its slot */ const _isLoneFamilyName = (path) => { const parent = path.parent; // Directly in the declaration's value: inside a function the string is an // argument rather than a family. if (parent === null || path.type(parent) !== T_DECLARATION) return false; const self = path.start(path.node); const count = path.childCount(parent); let sharedSlot = false; let seenSelf = false; for (let at = 0; at < count; at++) { const child = path.childAt(parent, at); const type = path.type(child); if (type === T_COMMA) { if (seenSelf) return !sharedSlot; sharedSlot = false; continue; } if (type === T_WHITESPACE || type === T_COMMENT) continue; if (path.start(child) === self) seenSelf = true; else sharedSlot = true; } return seenSelf && !sharedSlot; }; // One identifier, and a family name is a run of them parted by whitespace. The // escapes a quoted name may carry are not identifier text, so one declines. const _PLAIN_IDENT_RE = /^-?[A-Za-z_€-￿][\w€-￿-]*$/; /** * Unquote a font family whose text is already a run of identifiers, which is * the other spelling `` names. Returns null wherever the quotes * carry something: a generic family's own keyword, a CSS-wide keyword, or text * no identifier could spell. * @param {string} raw the string token as written, quotes included * @returns {string | null} the unquoted name, or null to keep the string */ const _unquoteFontFamily = (raw) => { if (!_transforms.normalizeQuotes) return null; const quote = raw.charCodeAt(0); if (quote !== CC_QUOTATION_MARK && quote !== CC_APOSTROPHE) return null; if (raw.length < 2 || raw.charCodeAt(raw.length - 1) !== quote) return null; const text = raw.slice(1, -1); if (text.includes("\\")) return null; const words = text.split(" "); if (words.length === 0 || text.length + 1 >= raw.length) return null; for (const word of words) { if (!_PLAIN_IDENT_RE.test(word)) return null; const lowered = toLowerCaseIfNeeded(word); // Unquoted, either would read as the grammar's keyword instead of a name. if (CSS_WIDE_KEYWORDS.has(lowered)) return null; // A generic family is one identifier, so only a lone word can be read as // one — `Apple Color Emoji` is three, whatever the last of them spells. if (words.length === 1 && GENERIC_FONT_FAMILIES.has(lowered)) return null; } return text; }; /** * Whether the declaration being printed takes a color and never an identifier * of the author's own, so a named color in it is unambiguously that color. * @returns {boolean} true inside such a declaration */ const _inColorOnlyProperty = () => _valueDeclaration !== null && COLOR_ONLY_PROPERTIES.has(toLowerCaseIfNeeded(A.name(_valueDeclaration))); /** * Whether the declaration being printed spells no name of the author's: its * grammar is keywords alone, or it takes a color, which is keywords and numbers. * An identifier standing directly in such a value is one of those keywords. * @returns {boolean} true inside such a declaration */ const _inKeywordOnlyValue = () => { if (_valueDeclaration === null) return false; // Memoized on the declaration: every identifier in one value asks the same // question, and answering it reads the name, folds it and looks it up twice. if (_keywordOnlyFor !== _valueDeclaration) { _keywordOnlyFor = _valueDeclaration; const property = _standardSpelling( toLowerCaseIfNeeded(A.name(_valueDeclaration)) ); _keywordOnly = KEYWORD_ONLY_PROPERTIES.has(property) || COLOR_ONLY_PROPERTIES.has(property); } return _keywordOnly; }; // A plain (non-scientific, unitless) number — the only argument form these // equivalences are proven for; anything else (a `var()`, a dimension) keeps the // function, since rewriting an invalid declaration would activate it. const _PLAIN_NUMBER_RE = /^[+-]?(?:\d+\.?\d*|\.\d+)$/; /** * Minify an easing function to its shorter, value-identical form: * `cubic-bezier()` to the keyword defining the same curve, `steps()` to * `step-start` / `step-end`, and the default `end` position away. Returns null * for anything else, keeping the function. * @param {string} fn the lowercased function name * @param {string} inner the already-joined argument text * @returns {string | null} the shorter easing function, or null to keep the function */ const _minifyEasingFunction = (fn, inner) => { if (!_transforms.reduceFunctions) return null; if (fn !== "cubic-bezier" && fn !== "steps") return null; const args = inner.split(","); if (fn === "cubic-bezier") { if (args.length !== 4) return null; let key = ""; for (let i = 0; i < 4; i++) { const s = args[i].trim(); if (!_PLAIN_NUMBER_RE.test(s)) return null; key += (i === 0 ? "" : ",") + Number.parseFloat(s); } const keyword = CUBIC_BEZIER_KEYWORDS.get(key); return keyword === undefined ? null : keyword; } if (args.length !== 2) return null; const count = args[0].trim(); if (!/^\d+$/.test(count)) return null; const position = args[1].trim().toLowerCase(); if (position === "start" || position === "jump-start") { // `start` is `jump-start`'s alias. return count === "1" ? "step-start" : `steps(${count},start)`; } if (position !== "end" && position !== "jump-end") return null; // `end` is `steps()`'s default position, so it always drops out. return count === "1" ? "step-end" : `steps(${count})`; }; // Code points a url-token cannot carry unescaped, so a `url("…")` string only // drops its quotes without them. // eslint-disable-next-line no-control-regex -- a control code point is exactly what a url-token may not carry const _UNQUOTABLE_URL_RE = /[\s"'()\\\u0000-\u001F\u007F]/; // …of those, the ones a backslash alone escapes into a url-token. Every other // one it cannot: a control code point takes a hex escape that is never shorter, // and a backslash already there starts an escape this printer did not write, so // the string keeps its quotes rather than having that escape rewritten. const _URL_ESCAPABLE = new Set([" ", '"', "'", "(", ")"]); /** * Rewrite a `url()`'s quoted body as the url-token spelling the same URL, or * null where none does. Every code point is classified — escaped, kept, or * refused — so nothing reaches the output unexamined. * @param {string} body the string's content, quotes excluded * @returns {string | null} the url-token text, or null to keep the quotes */ const _escapeUrlBody = (body) => { let out = ""; let escapes = 0; for (const character of body) { if (_URL_ESCAPABLE.has(character)) { // Two escapes cost the two bytes the quotes did, so nothing is saved. if (++escapes > 1) return null; out += `\\${character}`; continue; } const code = /** @type {number} */ (character.codePointAt(0)); if (character === "\\" || code < 0x20 || code === 0x7f) return null; out += character; } return escapes === 0 ? null : out; }; const _PERCENT_ESCAPE_RE = /%[0-9a-f]{2}/gi; // A `data:` URL up to the comma that ends its metadata. Only past that comma is // an escape content the URL parser decodes before anything reads it; anywhere // else it is structure — `%26` in a query is a literal `&`, not a separator. const _DATA_URL_METADATA_RE = /^data:[^,]*,/i; /** * Write each percent-escape a data URI's payload does not need as the byte it * names — three bytes for one, over the markup an inline SVG is made of. * @param {string} body the url's content, quotes excluded * @param {boolean} bare whether it is written as a url-token rather than a string * @param {string} quote the quote the body is written in, empty for a url-token * @returns {string} the body, its needless escapes decoded */ const _decodePercentEscapes = (body, bare, quote) => { const metadata = _DATA_URL_METADATA_RE.exec(body); if (metadata === null) return body; return ( metadata[0] + body.slice(metadata[0].length).replace(_PERCENT_ESCAPE_RE, (escape) => { const code = Number.parseInt(escape.slice(1), 16); // Each escape names one byte, not one code point: `%C3%A9` is two bytes // of one character, and writing them apart would re-encode as four. if (code < 0x20 || code >= 0x7f) return escape; const one = String.fromCharCode(code); // `#` would start the fragment and `%` the next escape, so those two // stay; the quote and the escape character would end or extend the // string, and a url-token carries none of what the quotes were holding. if (one === "#" || one === "%" || one === quote || one === "\\") { return escape; } return bare && _UNQUOTABLE_URL_RE.test(one) ? escape : one; }) ); }; /** * `url("a.png")` → `url(a.png)` when the quoted URL is also a valid url-token, * and a data URI's payload written as the bytes its escapes name. Nothing else * about the URL is rewritten, since webpack passes it through to a server that * may read it verbatim. * @param {string} fn the lowercased function name * @param {string} inner the already-joined argument text * @returns {string | null} the shorter `url()`, or null to keep the function */ const _minifyUrlFunction = (fn, inner) => { if (fn !== "url") return null; const quote = inner.charCodeAt(0); if (quote !== CC_QUOTATION_MARK && quote !== CC_APOSTROPHE) return null; if (inner.length < 2 || inner.charCodeAt(inner.length - 1) !== quote) { return null; } const written = inner.slice(1, -1); const mark = String.fromCharCode(quote); // The renderer reads source, so it is offered the decoded payload whatever // the switches say — `escapes` decides how the escapes are *printed*, not // whether a data URL holds a document. const decoded = _decodePercentEscapes(written, false, mark); // A `data:` payload a renderer rewrites is serialized afresh: what it hands // back is no longer what the quotes were written around. Quoted as written // where the answer is not in yet, the unquoted form below being unable to // carry what a document payload contains. if (_deferEmbeddedSource !== undefined) { const deferred = _deferDataUrl( decoded, (url) => `${fn}(${_serializeUrl(url, mark)})`, `${fn}(${mark}${written}${mark})` ); if (deferred !== null) return deferred; } const rendered = _renderDataUrl(decoded); if (rendered !== null) return `${fn}(${_serializeUrl(rendered, mark)})`; // Taking the quotes off what is a url-token without them is `normalizeQuotes`; // writing a percent-escape as the byte it names is not a switch of its own — // an escape and the byte name one string, so nothing reads them apart. const body = decoded; if (!_transforms.normalizeQuotes) { return body === written ? null : `${fn}(${mark}${body}${mark})`; } if (!_UNQUOTABLE_URL_RE.test(body)) return `${fn}(${body})`; // A code point a url-token cannot carry can still be escaped into one, which // costs a byte where the two quotes cost two — so one of them is shorter // escaped and two are not. const escaped = _escapeUrlBody(body); if (escaped !== null) return `${fn}(${escaped})`; // It keeps its quotes, and with them whatever the escapes gave back. return body === written ? null : `${fn}(${mark}${body}${mark})`; }; /** * Whether `text` re-tokenizes as exactly one `` — no escapes, and * no leading digit (`1x` is a dimension, `-1` a number) — so an attribute * selector's quoted value can drop its quotes. * @param {string} text the string's content, quotes excluded * @returns {boolean} true when it is a bare identifier */ const _isBareIdent = (text) => { const n = text.length; if (n === 0) return false; for (let i = 0; i < n; i++) { if (!_isIdentLike(text.charCodeAt(i))) return false; } const first = text.charCodeAt(0); if (_isDigit(first)) return false; // `-` alone is a delim, `-1` a number; `-x` / `--x` are idents. if (first === CC_HYPHEN_MINUS) return n > 1 && !_isDigit(text.charCodeAt(1)); return true; }; /** * Whether the code point an escape stands for can be written literally at * `index` of an identifier: it must be an ASCII ident code point, and the first * one must also be able to *start* an ident (`\31 x` is the class `1x`, but a * literal `1x` is a dimension). Non-ASCII stays escaped — writing it literally * would make the stylesheet's own encoding load-bearing. * @param {number} value the escape's code point * @param {string} written the identifier text emitted so far * @returns {boolean} true when the literal code point re-tokenizes the same */ const _canUnescape = (value, written) => { if (value >= 128 || !_isIdentCodePoint(value)) return false; if (written.length === 0) { return _isLetter(value) || value === CC_LOW_LINE; } // `-1` is a number and `--1` a valid ident, so only the second code point of // a leading `-` is constrained. if (written === "-") return !_isDigit(value); return true; }; /** * Shorten the escapes in an identifier, two ways that both re-tokenize to the * same name: write the code point literally where it needs no escape at all * (`\41 bc` → `Abc`), and otherwise drop the whitespace that terminates a hex * escape when the code point after it, *inside this same token*, cannot extend * it (`\32 xl` → `\32xl`). A terminator that ends the token stays: the walk's * own separator would take its place and swallow the whitespace that follows * (`.\32 x` is the class `2` and a descendant `x`, not the class `2x`). * @param {string} text the identifier's source text * @returns {string} the identifier, escapes shortened */ const _minifyIdentEscapes = (text) => { if (!text.includes("\\")) return text; const n = text.length; let out = ""; let i = 0; while (i < n) { const c = text.charCodeAt(i); if (c !== CC_REVERSE_SOLIDUS || i + 1 >= n) { out += text[i]; i++; continue; } let digitEnd = i + 1; let value = 0; while ( digitEnd < n && digitEnd - i <= 6 && _isHexDigit(text.charCodeAt(digitEnd)) ) { value = value * 16 + Number.parseInt(text[digitEnd], 16); digitEnd++; } const digits = digitEnd - i - 1; if (digits === 0) { // An identity escape (`\:`), which is what makes the code point literal — // dropping it would change the token. out += text[i] + text[i + 1]; i += 2; continue; } // A hex escape ends at the first non-hex-digit; one whitespace there is // consumed as its terminator rather than being part of the name — and a // CRLF pair is one whitespace (`consumeExtraNewline`), so dropping only the // CR would leave a raw newline inside the identifier. let end = digitEnd; if (end < n && _isWhiteSpace(text.charCodeAt(end))) { end = consumeExtraNewline(text.charCodeAt(end), text, end + 1); } if (end !== digitEnd && end === n) { // The terminator is also where the identifier stops. Rewriting it hands // that job to the walk's own separator, which is not the same thing — // `.a\31 .b` is one compound selector, `.a1 .b` two. out += text.slice(i); break; } if (_canUnescape(value, out)) { out += String.fromCharCode(value); i = end; continue; } const keepTerminator = end !== digitEnd && ((digits !== 6 && _isHexDigit(text.charCodeAt(end))) || _isWhiteSpace(text.charCodeAt(end))); out += text.slice(i, keepTerminator ? end : digitEnd); i = end; } return out; }; /** * Write a token's terminator back. A `\` left dangling at EOF has to be replaced * rather than kept: kept, it would escape the terminator and leave the token open * on exactly the input this repairs. What it stands for differs by token — nothing * in a string (§4.3.5), U+FFFD in a url (§4.3.6 reads it as an escape, §4.3.7 ends * one at EOF with the replacement character). * @param {string} raw the token's source text, missing its terminator * @param {string} terminator the character that closes it * @param {string} dangling what a `\` left at EOF contributed to the token's value * @returns {string} the terminated token */ const _terminate = (raw, terminator, dangling) => { let backslashes = 0; while ( raw.length - 1 - backslashes > 0 && raw.charCodeAt(raw.length - 1 - backslashes) === CC_REVERSE_SOLIDUS ) { backslashes++; } const body = backslashes % 2 === 1 ? `${raw.slice(0, -1)}${dangling}` : raw; return `${body}${terminator}`; }; /** * The `url()` counterpart of `_isClosedString`: §4.3.6 ends a url-token at EOF, so * it has no closing `)` and the printer's next byte would land inside it. * @param {string} raw the url-token's source text * @returns {boolean} true when the `)` is absent or itself escaped */ const _isUnterminatedUrl = (raw) => { const n = raw.length; if (n < 2 || raw.charCodeAt(n - 1) !== CC_RIGHT_PARENTHESIS) return true; let backslashes = 0; for (let i = n - 2; i > 0 && raw.charCodeAt(i) === CC_REVERSE_SOLIDUS; i--) { backslashes++; } return backslashes % 2 === 1; }; /** * Normalize a string token's quotes, following cssnano's `postcss-normalize-string`: * prefer `"`, switch to whichever quote needs fewer escapes, and unescape a quote * the chosen wrapper no longer escapes. A string already holding a literal quote * keeps its wrapper — the other kind is the cheap one there. Value-identical; a * `\`-newline continuation is left alone because dropping it could fuse into a * preceding hex escape. * A hex escape stays as written: the character it names costs gzip bytes where * it saves raw ones (measured in `configCases/css/minimize-values`). * @param {string} raw the string token's source text, quotes included * @returns {string} the normalized string token */ const _minifyString = (raw) => { if (!_transforms.normalizeQuotes) return raw; const quote = raw.charCodeAt(0); const n = raw.length; // A string the tokenizer closed at EOF has no matching final quote; rewriting // one would move where it ends. if (n < 2 || raw.charCodeAt(n - 1) !== quote) return raw; // The overwhelmingly common string — already `"`-wrapped, no escape and no // literal `'` — is unchanged; two native scans beat the counting loop below. if ( quote === CC_QUOTATION_MARK && !raw.includes("\\") && !raw.includes("'") ) { return raw; } let literalQuotes = 0; let escapedDoubleQuotes = 0; let escapedSingleQuotes = 0; for (let i = 1; i < n - 1; i++) { const c = raw.charCodeAt(i); if (c === CC_REVERSE_SOLIDUS) { // The escape swallows the final quote (`"x\"` at EOF): the string never // closed, so its last character is content, not a wrapper to rewrite. if (i + 1 === n - 1) return raw; const next = raw.charCodeAt(i + 1); if (next === CC_QUOTATION_MARK) escapedDoubleQuotes++; else if (next === CC_APOSTROPHE) escapedSingleQuotes++; i++; } else if (c === CC_QUOTATION_MARK || c === CC_APOSTROPHE) { literalQuotes++; } } if (literalQuotes !== 0) return raw; if (escapedDoubleQuotes === 0 && escapedSingleQuotes === 0) { return quote === CC_QUOTATION_MARK ? raw : `"${raw.slice(1, -1)}"`; } let want = quote; if (quote === CC_APOSTROPHE && escapedDoubleQuotes === 0) { want = CC_QUOTATION_MARK; } else if (quote === CC_QUOTATION_MARK && escapedSingleQuotes === 0) { want = CC_APOSTROPHE; } const wrapper = String.fromCharCode(want); let out = wrapper; for (let i = 1; i < n - 1; i++) { const c = raw.charCodeAt(i); if (c !== CC_REVERSE_SOLIDUS) { out += raw[i]; continue; } const next = raw.charCodeAt(i + 1); out += (next === CC_QUOTATION_MARK || next === CC_APOSTROPHE) && next !== want ? raw[i + 1] : raw[i] + raw[i + 1]; i++; } return out + wrapper; }; /** * Lowercase the pseudo-class and pseudo-element names in a selector's printed * parts, in place: the name after a `:` matches ASCII case-insensitively, while * a type selector's does not (`linearGradient` is an SVG element) and neither * does an id, a class or an attribute's value. A functional pseudo prints as one * token and lowercases its own name, its argument being the author's. * @param {string[]} parts the prelude's printed parts * @returns {void} */ /** * Lowercase the names in a media condition's printed parts, in place: a feature * name, a media type, the keywords between them and a dimension's unit all match * ASCII case-insensitively. A custom media query's `--name` does not, and a * string or a nested function (a style query) carries text no such rule covers. * @param {string[]} parts the condition's printed parts * @returns {void} */ const _lowercaseConditionParts = (parts) => { for (let i = 0; i < parts.length; i++) { const part = parts[i]; // Folded first, for the reason `_foldPseudoNames` folds first. const lowered = asciiLowerCaseName(part); if (lowered === part) continue; const first = part.charCodeAt(0); if ( first === CC_QUOTATION_MARK || first === CC_APOSTROPHE || (first === CC_HYPHEN_MINUS && part.charCodeAt(1) === CC_HYPHEN_MINUS) || part.includes("(") ) { continue; } parts[i] = lowered; } }; /** * Fold a selector's pseudo names to lowercase and drop the redundant colon of a * CSS2 pseudo-element, in one pass over the printed parts: both read the part * after a `:`, and the fold is what the legacy name is then matched against. * Empty parts are stepped over — a comment between the colons printed away, and * the tokenizer had already dropped it anyway. * @param {string[]} parts the prelude's printed parts * @returns {void} */ const _foldPseudoNames = (parts) => { const dropping = _transforms.shortenSelectors; // The two non-empty parts before the current one. let first = -1; let second = -1; for (let i = 0; i < parts.length; i++) { let part = parts[i]; if (part.length === 0) continue; // A pseudo's name matches ASCII case-insensitively, a type selector's does // not (`linearGradient` is an SVG element) and neither does an id, a class // or an attribute's value. A functional pseudo prints as one token and // folds its own name, its argument being the author's. if (second !== -1 && parts[second] === ":") { const lowered = asciiLowerCaseName(part); if (lowered !== part && !part.includes("(")) { part = lowered; parts[i] = lowered; } // Folded first, so the legacy name below is matched against the folded // text rather than folded a second time. if ( dropping && first !== -1 && parts[first] === ":" && LEGACY_PSEUDO_ELEMENTS.has(part) ) { parts[first] = ""; } } first = second; second = i; } }; // The `An+B` microsyntax (CSS Syntax 3 §6), whose whitespace and `+` are its // own: `2n + 1`, `+3` and `2N+1` all say what a shorter spelling does. const _NTH_RE = /^\s*(?:([+-]?)\s*(\d*)[nN]\s*(?:([+-])\s*(\d+))?|([+-]?)\s*(\d+))\s*$/; /** * Write one `An+B` in its shortest equal spelling. * @param {string} text the argument between the parentheses * @returns {string | null} the shortest spelling, or null when it is not `An+B` */ const _minifyNth = (text) => { const lower = text.trim().toLowerCase(); let a; let b; if (lower === "even") { a = 2; b = 0; } else if (lower === "odd") { a = 2; b = 1; } else { const parts = _NTH_RE.exec(text); if (parts === null) return null; if (parts[6] !== undefined) { a = 0; b = Number(`${parts[5] === "-" ? "-" : ""}${parts[6]}`); } else { a = Number( `${parts[1] === "-" ? "-" : ""}${parts[2] === "" ? "1" : parts[2]}` ); b = parts[4] === undefined ? 0 : Number(`${parts[3]}${parts[4]}`); } } // Past the safe range a rewrite would print a different integer than it read. if (!Number.isSafeInteger(a) || !Number.isSafeInteger(b)) return null; if (a === 0) return String(b); // A step forward only ever reaches `B` again from below, and an index under 1 // matches nothing — so those terms are dropped by naming the first real one. // Landing on the step itself is the bare `An`, which starts there anyway. if (a > 0) { if (b < 1) b = ((((b - 1) % a) + a) % a) + 1; if (b === a) b = 0; } // The one An+B a keyword names in fewer bytes. if (a === 2 && b === 1) return "odd"; const step = `${a === 1 ? "" : a === -1 ? "-" : a}n`; return b === 0 ? step : `${step}${b > 0 ? "+" : "-"}${Math.abs(b)}`; }; // One `urange` (CSS Syntax 3 §11.2): `U+` then hex digits, then either a `-` // and a second run or trailing `?` wildcards. Case is insignificant. const _URANGE_RE = /^u\+(?:([\da-f]{0,6})(\?{1,6})|([\da-f]{1,6})(?:-([\da-f]{1,6}))?)$/i; /** * Write one `urange` in its shortest equal spelling: leading zeros carry * nothing, and a range whose start is a prefix followed by zeros and whose end * is that prefix followed by `f`s is what the `?` wildcard says. * @param {string} range one urange token * @returns {string} the shortest spelling, or `range` when nothing is shorter */ const _minifyUnicodeRange = (range) => { if (!_transforms.shortenNumbers) return range; const parts = _URANGE_RE.exec(range); if (parts === null) return range; /** @type {(hex: string) => string} */ const strip = (hex) => hex.replace(/^0+(?=.)/, ""); // `U+00??` covers what `U+??` does — the zeros are as leading as any other. if (parts[2] !== undefined) { const head = parts[1].replace(/^0+/, ""); const out = `U+${head}${parts[2]}`; return out.length < range.length ? out : range; } const start = strip(parts[3]); if (parts[4] === undefined) { const out = `U+${start}`; return out.length < range.length ? out : range; } const end = strip(parts[4]); let best = `U+${start}-${end}`; // The wildcard needs both bounds the same width to compare digit by digit. if (start.length <= end.length) { const padded = start.padStart(end.length, "0"); let wild = 0; while ( wild < padded.length && padded.charAt(padded.length - 1 - wild) === "0" && end.charAt(end.length - 1 - wild).toLowerCase() === "f" ) { wild++; } while (wild > 0) { const head = padded.slice(0, padded.length - wild); // `equalsLowerCase` lowercases only its first argument. if ( equalsLowerCase(head, end.slice(0, end.length - wild).toLowerCase()) ) { const out = `U+${head.replace(/^0+/, "")}${"?".repeat(wild)}`; if (out.length < best.length) best = out; break; } wild--; } } return best.length < range.length ? best : range; }; // `@keyframes` and every vendor spelling of it, whose prelude is a name and // whose child preludes are keyframe selectors rather than selector lists. const KEYFRAMES_AT_RULE_RE = /^(?:-[a-z]+-)?keyframes$/i; // Media Queries 4 §2.4: `min-`/`max-` prefixes exist only on range-type media // features, and `min-X: Y` is exactly `X >= Y`. The range spelling is the newer // one, so it is only reached for where the target reads it. const _RANGE_PREFIX_RE = /^(min|max)-(.+)$/i; /** * Rewrite a media feature's `min-` / `max-` prefix to the range spelling, in the * printed parts, in place. Only a whole `(:)` — a condition made * of anything else (a boolean feature, an `and` chain, a nested block) is left * for its own parts to handle. * @param {string[]} parts the block's printed parts * @returns {void} */ const _useRangeSpelling = (parts) => { if (!_transforms.shortenMediaQueries) return; // The separators go with the join's condition trim, so a spaced // `( min-width : 1px )` is the same feature as a tight one. /** @type {number[]} */ const filled = []; for (let i = 0; i < parts.length; i++) { if (parts[i].length !== 0 && parts[i] !== _SEP) filled.push(i); } if (filled.length < 3 || parts[filled[1]] !== ":") return; const feature = _RANGE_PREFIX_RE.exec(parts[filled[0]]); if (feature === null) return; parts[filled[0]] = feature[2]; parts[filled[1]] = feature[1].toLowerCase() === "min" ? ">=" : "<="; }; // One printed `()`, the shape the range spelling // leaves behind. Media Queries 4 §2.4.3 writes an interval with both // comparisons pointing the same way, so a pair is only ever joined as `<`. const _RANGE_CONDITION_RE = /^\(([-\w]+)(>=|<=|>|<)([^()<>]+)\)$/; /** * Collapse an `and` of two one-sided ranges on one feature into the interval * that says the same (`(width>=1px) and (width<=2px)` is `(1px<=width<=2px)`), * in the printed parts, in place. Only a bounded pair — two comparisons the * same way round are not an interval, and `or` is not a conjunction. * @param {string[]} parts the prelude's printed parts * @returns {void} */ const _collapseRangeInterval = (parts) => { if (!_transforms.shortenMediaQueries) return; /** @type {number[]} */ const filled = []; for (let i = 0; i < parts.length; i++) { if (parts[i].length !== 0 && parts[i] !== _SEP) filled.push(i); } for (let i = 0; i + 2 < filled.length; i++) { if (!equalsLowerCase(parts[filled[i + 1]], "and")) continue; const left = _RANGE_CONDITION_RE.exec(parts[filled[i]]); if (left === null) continue; const right = _RANGE_CONDITION_RE.exec(parts[filled[i + 2]]); if (right === null) continue; if (!equalsLowerCase(left[1], right[1])) continue; // One has to bound it from below and the other from above. const lower = left[2].startsWith(">") ? left : right; const upper = left[2].startsWith(">") ? right : left; if (!lower[2].startsWith(">") || !upper[2].startsWith("<")) continue; const low = `${lower[3]}${lower[2] === ">=" ? "<=" : "<"}`; parts[filled[i]] = `(${low}${left[1]}${upper[2]}${upper[3]})`; parts[filled[i + 1]] = ""; parts[filled[i + 2]] = ""; i += 2; } }; /** * Drop the universal selector a compound already implies (`*:before` is * `:before`), in the printed parts, in place. `*` carries no specificity and * matches every element, so a qualified compound means the same without it. * @param {string[]} parts the prelude's printed parts * @returns {void} */ /** * Whether the simple selector at `at` selects a featureless element, which * matches no type or universal selector — so the `*` before it is what keeps * the rule from matching, not a spelling of it. * @param {string[]} parts the printed selector, one piece per token * @param {number} at where the simple selector starts * @returns {boolean} true when the `*` before it has to stay */ const _selectsFeatureless = (parts, at) => { if (parts[at] !== ":") return false; let name = at + 1; while (name < parts.length && parts[name].length === 0) name++; if (name >= parts.length) return false; // A functional pseudo-class arrives with its arguments attached. const open = parts[name].indexOf("("); const named = open === -1 ? parts[name] : parts[name].slice(0, open); return FEATURELESS_PSEUDO_CLASSES.has(toLowerCaseIfNeeded(named)); }; /** * Drop the universal selector a compound already implies (`*:before` is * `:before`), in the printed parts, in place. `*` carries no specificity and * matches every element, so a qualified compound means the same without it — * except before a featureless pseudo-class, which no element with features * matches. * @param {string[]} parts the prelude's printed parts * @returns {void} */ const _dropImpliedUniversalSelector = (parts) => { if (!_transforms.shortenSelectors) return; let previous = -1; for (let i = 0; i < parts.length; i++) { const part = parts[i]; if (part.length === 0) continue; if ( previous !== -1 && parts[previous] === "*" && COMPOUND_CONTINUATIONS.has(part.charAt(0)) && (previous === 0 || parts[previous - 1] !== "|") && !_selectsFeatureless(parts, i) ) { parts[previous] = ""; } previous = i; } }; /** * Split a selector list at the commas that separate its selectors. A comma * inside `:is(…)`, an attribute value or a string belongs to one selector. * @param {string} prelude the printed selector list * @returns {string[]} the selectors, in written order */ const _splitSelectorList = (prelude) => { /** @type {string[]} */ const out = []; let depth = 0; let quote = 0; let start = 0; for (let i = 0; i < prelude.length; i++) { const code = prelude.charCodeAt(i); if (code === CC_REVERSE_SOLIDUS) { i++; continue; } if (quote !== 0) { if (code === quote) quote = 0; continue; } if (code === CC_QUOTATION_MARK || code === CC_APOSTROPHE) { quote = code; } else if (code === CC_LEFT_PARENTHESIS || code === CC_LEFT_SQUARE) { depth++; } else if (code === CC_RIGHT_PARENTHESIS || code === CC_RIGHT_SQUARE) { depth--; } else if (code === CC_COMMA && depth === 0) { out.push(prelude.slice(start, i)); start = i + 1; } } out.push(prelude.slice(start)); return out; }; // A substitution expands to a whole token sequence, so two identical references // are not one repeated value: with `--x:1px 2px`, `margin:var(--x) var(--x)` is // four values, not two. `--name(` is a custom function (CSS Functions). const _SUBSTITUTION_RE = new RegExp( `(?:^|[^\\w-])(?:${[...SUBSTITUTION_FUNCTIONS].join("|")})\\(|--[\\w-]*\\(`, "i" ); /** * Whether a value reads a substitution, so its tokens are not what the engine * will see there. * @param {string} text the text to look in * @returns {boolean} true when a substitution may stand in it */ const _hasSubstitution = (text) => // Every shape `_SUBSTITUTION_RE` accepts ends in `(`, and most values hold // none at all — so one scan for it answers them without running the shape. text.includes("(") && _SUBSTITUTION_RE.test(text); /** * {@link _hasSubstitution} over a span of the input, which is cut out only once * the span turns out to hold a `(` at all. * @param {number} from the span's start offset * @param {number} to the span's end offset * @returns {boolean} true when a substitution may stand in it */ const _hasSubstitutionInSpan = (from, to) => { // The walk asks for a declaration's span and the printer asks again for the // same one, so one entry spares the second scan and the slice under it. if (from === _substitutionSpanFrom && to === _substitutionSpanTo) { return _substitutionSpanHas; } let has = false; for (let i = from; i < to; i++) { if (_input.charCodeAt(i) === CC_LEFT_PARENTHESIS) { has = _SUBSTITUTION_RE.test(_input.slice(from, to)); break; } } _substitutionSpanFrom = from; _substitutionSpanTo = to; _substitutionSpanHas = has; return has; }; // The span `_hasSubstitutionInSpan` last answered for. Offsets index the current // input, so a new stylesheet resets them. let _substitutionSpanFrom = -1; let _substitutionSpanTo = -1; let _substitutionSpanHas = false; /** * Split a declaration value into its top-level components. Whitespace parts * them, and so does a `/` delim — it separates `border-radius`'s two boxes and * needs no whitespace of its own. A comment is no tree node, so the gap it * leaves between two children parts them too: it ends both tokens, and a * rewritten value joins its components with a space that has to stand where * the comment did. * @param {CssPath} path the accessor positioned on the declaration * @param {Node} node the declaration whose value's children are read * @param {PrintContext} writer the print context (children's printed text) * @returns {string[]} the components, with a `/` delim as its own entry */ const _valueComponents = (path, node, writer) => { /** @type {string[]} */ const components = []; let current = ""; let previousEnd = -1; const count = path.childCount(node); for (let at = 0; at < count; at++) { const child = path.childAt(node, at); const type = path.type(child); const text = writer.get(child); const start = path.start(child); if (current.length !== 0 && start !== previousEnd) { components.push(current); current = ""; } previousEnd = path.end(child); if (type === T_WHITESPACE || (type === T_DELIM && text === "/")) { if (current.length !== 0) components.push(current); current = ""; if (type === T_DELIM) components.push("/"); continue; } current += text; } if (current.length !== 0) components.push(current); return components; }; /** * A custom property's value, token for token: every token is written back as * written, and a run of whitespace and dropped comments between two of them is * one boundary — the space they need, or nothing where a comma or a block edge * is already one, which leaves every `var()` the same token stream. * @param {CssPath} path the accessor positioned on the declaration * @param {ComponentValue[]} children the tokens to print * @param {PrintContext} writer the print context (holds the kept comments) * @param {number} from source offset the tokens start at * @param {number} to source offset they end at * @returns {string} the printed value */ const _customPropertyValue = (path, children, writer, from, to) => { let out = ""; let at = from; // The boundary held back: whether a whitespace token stood in it, whether a // dropped comment did, and whether a comma closed it. let spaced = false; let dropped = false; let comma = false; for (const child of children) { const start = path.start(child); // Every other token is a child of its own, so a gap is comments only. if (at !== start) { const kept = writer.takeInserts(at, start); if (kept === "") { dropped = true; } else { if (spaced && out !== "") out += " "; out += kept; spaced = false; dropped = false; comma = false; } } at = path.end(child); const type = path.type(child); if (type === T_WHITESPACE) { spaced = true; continue; } if (type === T_COMMA) { out += ","; spaced = false; dropped = false; comma = true; continue; } const text = _customPropertyToken(path, child, writer); // A comma or a block delimiter is a boundary of its own, so the whitespace // beside one says nothing — except before `(`, which an ident in front of // makes a function token, and that is what `_wouldFuseTokens` answers. if (!comma && out !== "") { const last = out.charCodeAt(out.length - 1); const fuses = dropped && _wouldFuseTokens(last, text, out); const parted = CUSTOM_PROPERTY_CLOSERS.includes(out[out.length - 1]) || CUSTOM_PROPERTY_OPENERS.includes(text[0]); if (fuses || (spaced && !parted)) out += " "; } out += text; spaced = false; dropped = false; comma = false; } // A trailing boundary separates the last token from a `)` or the value's end, // neither of which it can fuse with; only a kept comment in it still prints. if (at !== to) { const kept = writer.takeInserts(at, to); if (kept !== "") { if (spaced && out !== "") out += " "; out += kept; } } return out; }; /** What makes a token worth reading through rather than writing back whole. */ const CUSTOM_PROPERTY_REWRITABLE_RE = /[\t\n\f\r ,]|\/\*/; // Block delimiters a token cannot fuse across, so whitespace beside one goes. // `(` is not among the openers: an ident in front of it makes a function token. const CUSTOM_PROPERTY_CLOSERS = ")]}"; const CUSTOM_PROPERTY_OPENERS = "[{"; /** * One token of a custom property's value, as written — recursing into a * function or block so the boundaries nested in one print as they do at the top * level. A source holding neither whitespace, a comma nor a comment holds none * at any depth, so it is written back whole. The one token not written as it * stands is a url or a string the input ran out of inside, which the engine * closes there and so does this. * @param {CssPath} path the accessor positioned on the declaration * @param {ComponentValue} child the token to print * @param {PrintContext} writer the print context (holds the kept comments) * @returns {string} the printed token */ const _customPropertyToken = (path, child, writer) => { // Rewriting: the token already printed minified, its own nesting included. if (_rewriteCustomProperties) return writer.get(child); const source = path.source(child); const type = path.type(child); // The input ran out inside this token, so the engine closed it there and the // printer has to as well — otherwise the `}` written after it is read as part // of the token rather than as the end of the rule. if (path.end(child) === _input.length) { // A url token holds a value its text no longer spells only when an escape // was truncated; `url(foo` the engine echoes open, and so does this. if ( type === T_URL && source !== _input.slice(path.start(child), path.end(child)) ) { return `${source})`; } if (type === T_STRING && !_isClosedString(source)) { return source + source[0]; } } if ( (type !== T_FUNCTION && type !== T_SIMPLE_BLOCK) || !CUSTOM_PROPERTY_REWRITABLE_RE.test(source) ) { return source; } const start = path.start(child); const end = path.end(child); let opened = path.nameEnd(child) + 1; let closer = ")"; if (type === T_SIMPLE_BLOCK) { const block = path.blockToken(child); opened = start + 1; closer = block === "[" ? "]" : block === "{" ? "}" : ")"; } // Closed at EOF: there is no closer to write back, and it ends the value. const closed = _input[end - 1] === closer; const inner = _customPropertyValue( path, path.children(child), writer, opened, closed ? end - 1 : end ); return `${_input.slice(start, opened)}${inner}${closed ? closer : ""}`; }; /** * Split top-level components into the layers a comma parts. A comma is no * separator of its own, so it rides on the component it follows and a layer's * last component has to be cut back out of it. * @param {string[]} components the value's top-level components * @returns {string[][]} one entry per layer, each its own component list */ const _valueLayers = (components) => { /** @type {string[][]} */ const layers = []; /** @type {string[]} */ let current = []; for (const component of components) { let depth = 0; let start = 0; for (let i = 0; i < component.length; i++) { const character = component[i]; if (character === "(") { depth++; } else if (character === ")") { depth--; } else if (character === "," && depth === 0) { if (i > start) current.push(component.slice(start, i)); layers.push(current); current = []; start = i + 1; } } if (start < component.length) current.push(component.slice(start)); } layers.push(current); return layers; }; /** * Drop the box values CSS's `{1,4}` notation already implies: an omitted value * is copied from the opposite side, so a 4th equal to the 2nd, a 3rd equal to * the 1st and a 2nd equal to the 1st are each redundant. * @param {string[]} values one box's components * @returns {string[] | null} the kept values, or `null` when the box cannot be collapsed */ const _collapseBox = (values) => { const n = values.length; if (n === 0 || n > 4) return null; for (const value of values) { if (_hasSubstitution(value)) return null; if (n > 1 && CSS_WIDE_KEYWORDS.has(toLowerCaseIfNeeded(value))) { return null; } } let end = n; if (end === 4 && values[3] === values[1]) end = 3; if (end === 3 && values[2] === values[0]) end = 2; if (end === 2 && values[1] === values[0]) end = 1; return values.slice(0, end); }; /** * Whether a shorthand's slots mix a bare non-zero number with a length or a * percentage. No box slot reads both, so one of them was never read at all. * @param {string[]} values one shorthand's slot values * @returns {boolean} true when the two kinds stand together */ const _mixesBareNumberWithLength = (values) => { let bareNumber = false; let measured = false; for (const value of values) { const kind = _componentKind(value); if (kind === _COMPONENT_NUMBER) bareNumber = true; else if (kind !== _COMPONENT_OTHER) measured = true; } return bareNumber && measured; }; // What a shorthand component is, coarsely: a fold is only safe between two of // the same kind, since a slot taking one takes the other. `0` is a length // wherever a dimension is, and a bare non-zero number is neither (`padding:.25` // is invalid, and folding it in would make the whole shorthand invalid). const _COMPONENT_OTHER = 0; const _COMPONENT_LENGTH = 1; const _COMPONENT_PERCENTAGE = 2; const _COMPONENT_NUMBER = 3; /** * @param {string} component one printed component of a shorthand's value * @returns {number} one of the `_COMPONENT_*` kinds */ const _componentKind = (component) => { if (!NUMERIC_COMPONENT_RE.test(component)) return _COMPONENT_OTHER; const last = component.charCodeAt(component.length - 1); if (last === CC_PERCENTAGE) return _COMPONENT_PERCENTAGE; // A unit is what makes a non-zero number a length, and a digit is no unit — // `.25` is a bare number, which no box slot taking a length accepts. if (!_isDigit(last)) return _COMPONENT_LENGTH; return ZERO_NUMBER_RE.test(component) ? _COMPONENT_LENGTH : _COMPONENT_NUMBER; }; // A number with an optional sign and fraction, then a unit or `%` or nothing. const NUMERIC_COMPONENT_RE = /^[-+]?(?:\d+(?:\.\d+)?|\.\d+)(?:%|[a-z]+)?$/i; // The same, written zero — every spelling of it, so `0.0` is a length too. const ZERO_NUMBER_RE = /^[-+]?0*(?:\.0*)?$/; /** * One box or pair shorthand's value read back as the slots it fills — `1px 2px` * as all four sides, so a longhand can take one of them over. * @param {string} value the shorthand's printed value * @param {number} slots how many the shorthand holds (4 for a box, 2 for a pair) * @returns {string[] | null} the filled slots, or null when it fills none of them */ const _expandBox = (value, slots) => { const written = _splitTopLevelSpaces(value); if (written.length === 0 || written.length > slots) return null; const out = [...written]; // The `{1,4}` rule: an omitted slot takes the one two back, and the second // takes the first. for (let i = written.length; i < slots; i++) out.push(out[i < 2 ? 0 : i - 2]); return out; }; /** * Fold a longhand into the shorthand of its own family standing directly before * it, where the merged spelling is shorter than the two declarations. Adjacent * only: anything between them is read between them, so folding would move what * it wrote past it. * @param {CssPath} path the accessor * @param {Node[]} items the block's children * @param {string[]} texts their printed text, rewritten in place * @param {Set} superseded the indices already dropped, added to here * @returns {void} */ const _foldFollowingLonghands = (path, items, texts, superseded) => { let shorthand = -1; for (let i = 0; i < items.length; i++) { if (path.type(items[i]) !== T_DECLARATION || texts[i].length === 0) { shorthand = -1; continue; } if (superseded.has(i)) continue; if (shorthand !== -1 && _foldInto(path, items, texts, shorthand, i)) { superseded.add(i); continue; } shorthand = i; } }; /** * @param {CssPath} path the accessor * @param {Node[]} items the block's children * @param {string[]} texts their printed text, rewritten in place * @param {number} at the shorthand's index * @param {number} from the longhand's index * @returns {boolean} true when the two were folded into one */ const _foldInto = (path, items, texts, at, from) => { if (path.important(items[at]) !== path.important(items[from])) return false; const shorthandText = texts[at]; const longhandText = texts[from]; const shorthandColon = shorthandText.indexOf(":"); const longhandColon = longhandText.indexOf(":"); if (shorthandColon <= 0 || longhandColon <= 0) return false; const property = _printedProperty(shorthandText, shorthandColon); const sides = BOX_LONGHANDS.get(property) || PAIR_LONGHANDS.get(property); if (sides === undefined) return false; const slot = sides.indexOf(_printedProperty(longhandText, longhandColon)); if (slot === -1) return false; const shorthandValue = _printedValue(shorthandText, shorthandColon); const longhandValue = _printedValue(longhandText, longhandColon); // A substitution may stand for any number of slots, and a comma list is no // box at all — neither reads back as the slots this fills. if (_hasSubstitution(shorthandValue) || _hasSubstitution(longhandValue)) { return false; } if (_valueItems(shorthandValue).length !== 1) return false; if (_splitTopLevelSpaces(longhandValue).length !== 1) return false; // Validity is per property, and a component the property does not accept // makes the whole shorthand invalid — which loses the slots that were fine. // So the two are folded only where they are the same narrow numeric class, // which every box slot of that family accepts wherever one of them does. const kind = _componentKind(longhandValue); if (kind === _COMPONENT_OTHER) return false; for (const component of _splitTopLevelSpaces(shorthandValue)) { if (_componentKind(component) !== kind) return false; } const filled = _expandBox(shorthandValue, sides.length); if (filled === null) return false; filled[slot] = longhandValue; const collapsed = _collapseBox(filled); if (collapsed === null) return false; if ( collapsed.length !== 1 && ONE_VALUE_PAIR_SHORTHANDS.has(property) && !_overflowTwoValuesAllowed ) { return false; } if ( collapsed.length !== 1 && PLACE_SHORTHANDS.has(property) && !_placeShorthandAllowed ) { return false; } const important = path.important(items[at]) ? _IMPORTANT : ""; const folded = `${property}:${collapsed.join(" ")}${important};`; if (folded.length >= shorthandText.length + longhandText.length) return false; texts[at] = folded; return true; }; /** * @param {string[]} a one component list * @param {string[]} b another component list * @returns {boolean} whether they are the same components in the same order */ const _sameComponents = (a, b) => a.length === b.length && a.every((value, i) => value === b[i]); /** * Turn components back into fragments: a separator between each pair, except * around the `/`, which needs none. * @param {string[]} components components in order * @returns {string[]} the fragments to join */ const _spaced = (components) => { /** @type {string[]} */ const parts = []; for (const component of components) { if ( parts.length !== 0 && component !== "/" && parts[parts.length - 1] !== "/" ) { parts.push(_SEP); } parts.push(component); } return parts; }; /** * Collapse a `{1,4}` box-notation value (see `BOX_SHORTHANDS`). `border-radius` * carries two boxes — ` / ` — which collapse independently, * and a vertical box equal to the horizontal one is what the `/`-less form * already means. * @param {CssPath} path the accessor positioned on the declaration * @param {string} property the declaration's lowercased property name * @param {Node} node the declaration whose value's children are read * @param {PrintContext} writer the print context (children's printed text) * @returns {string[] | null} the fragments to join, or `null` to keep the value as it is */ const _collapseBoxShorthand = (path, property, node, writer) => { const components = _valueComponents(path, node, writer); const slash = components.indexOf("/"); if (slash === -1) { const box = _collapseBox(components); return box === null || box.length === components.length ? null : _spaced(box); } // Only `border-radius` takes a second box. On the others a `/` is invalid, so // the browser already drops the declaration — collapsing it would switch it on. if (!SLASH_BOX_SHORTHANDS.has(property)) return null; if (components.lastIndexOf("/") !== slash) return null; const horizontal = _collapseBox(components.slice(0, slash)); const vertical = _collapseBox(components.slice(slash + 1)); if (horizontal === null || vertical === null) return null; // Rebuilt even when neither box collapsed: the `/` is a delim token, so the // whitespace around it is insignificant either way. return _spaced( _sameComponents(horizontal, vertical) ? horizontal : [...horizontal, "/", ...vertical] ); }; // A number with no unit and no percent, which `flex` reads as a factor. const _BARE_NUMBER_RE = /^[+-]?(?:\d+\.?\d*|\.\d+)(?:e[+-]?\d+)?$/i; /** * Rewrite a `flex` value to its keyword spelling where one exists. * @param {CssPath} path the accessor positioned on the declaration * @param {Node} node the declaration whose value's children are read * @param {PrintContext} writer the print context (children's printed text) * @returns {string[] | null} the fragments to join, or `null` to keep the value as it is */ const _collapseFlexShorthand = (path, node, writer) => { const components = _valueComponents(path, node, writer); if (components.length !== 3) return null; const keyword = FLEX_KEYWORDS.get(components.join(" ").toLowerCase()); if (keyword !== undefined) return [keyword]; // `<'flex-shrink'>` follows the grow factor and defaults to 1, so a `1` there // says nothing — but only over a basis that cannot be read as a factor // itself: CSS Flexbox 1 §7.1.1 reads a unitless zero not preceded by two // factors as a factor, so `1 1 0` is not `1 0`. return components[1] === "1" && !_BARE_NUMBER_RE.test(components[2]) ? [components[0], components[2]] : null; }; /** * Rewrite a `font-weight` keyword to the number it is defined as. Only the * longhand (and the `@font-face` descriptor, where the keywords mean the same): * inside the `font` shorthand a `normal` may be the style or the variant * instead, and the shorthand's own grammar decides which. * @param {CssPath} path the accessor positioned on the declaration * @param {Node} node the declaration whose value's children are read * @param {PrintContext} writer the print context (children's printed text) * @returns {string[] | null} the fragments to join, or `null` to keep the value as it is */ const _collapseFontWeight = (path, node, writer) => { const count = path.childCount(node); let only = ""; for (let at = 0; at < count; at++) { const text = writer.get(path.childAt(node, at)); if (text.length === 0) continue; // A second component is a value this rewrite is not defined for. if (only.length !== 0) return null; only = text; } if (only.length === 0) return null; const number = FONT_WEIGHT_NUMBERS.get(only.toLowerCase()); return number === undefined ? null : [number]; }; // The slots of one ``, told apart by what each can spell. const _TRANSITION_TIME_RE = /^[+-]?(?:\d+\.?\d*|\.\d+)(?:s|ms)$/i; const _TRANSITION_EASING_FUNCTION_RE = /^(?:cubic-bezier|steps|linear)\(/i; /** * Write one `transition` in the order its grammar lists the slots. `||` makes * them order-free, so the same declaration has many spellings and one of them * repeats across a stylesheet. The two `