← Files DataARCHIVED FILE
scripts/css-syntax.mjs
2.22 KB · Sep 30, 2026 · 23:19 UTC
const closingCharacters = new Map([
["Block", "}"],
["Function", ")"],
["Parentheses", ")"],
["Brackets", "]"],
["Url", ")"],
]);
function unescapedCharacterAt(source, index, character) {
if (source[index] !== character) return false;
let slashes = 0;
for (let previous = index - 1; previous >= 0 && source[previous] === "\\"; previous--) slashes++;
return slashes % 2 === 0;
}
/** Parse complete CSS without accepting css-tree's normal error/EOF recovery. */
export function parseStrictCss(css, compiler, { filename = "data-app.css", onInvalid } = {}) {
const invalid = (reason) => {
const cause = reason instanceof Error ? reason : new Error(reason);
onInvalid?.(cause);
throw new Error(`Invalid CSS syntax in ${filename}: ${cause.message}`, { cause });
};
let ast;
try {
ast = compiler.parseCss(css, {
context: "stylesheet",
filename,
positions: true,
parseCustomProperty: true,
onParseError(error) {
throw error;
},
onComment(_value, location) {
// `/*/` is not a closed comment: its apparent delimiters overlap.
if (
location.end.offset - location.start.offset < 4 ||
css.slice(location.end.offset - 2, location.end.offset) !== "*/"
)
throw new Error("Unterminated CSS comment.");
},
});
} catch (error) {
invalid(error);
}
if (ast.type !== "StyleSheet") invalid("Expected a stylesheet.");
compiler.walkCss(ast, (node) => {
if (node.type === "Raw") invalid("Unsupported or recovered CSS syntax.");
// CSS Syntax permits an implicit closer at EOF. Publication must use the
// author's complete input, not a repaired block/function/string/URL.
const closing = closingCharacters.get(node.type);
if (closing && !unescapedCharacterAt(css, node.loc.end.offset - 1, closing))
invalid(`Unterminated CSS ${node.type.toLowerCase()}.`);
if (node.type === "String") {
const opening = css[node.loc.start.offset];
if (
!['"', "'"].includes(opening) ||
node.loc.end.offset - node.loc.start.offset < 2 ||
!unescapedCharacterAt(css, node.loc.end.offset - 1, opening)
)
invalid("Unterminated CSS string.");
}
});
return ast;
}
SHA-256: 1483783ec106058e6d0e861588c2648fbfca3dd312d616cddafc9e17e50702f1