Skip to content

Parsing CSS

Package: @lanexio/parser-grammar-css Stable Layer: 2 (Grammar). Depends only on @lanexio/parser-core. Runtime: Universal (browser, server, edge worker).

parseCss implements a CSS Syntax Module Level 3 parser with a two-pass architecture: tokenization (§4) followed by tree construction (§§5/9). It handles qualified rules, at-rules, declarations, { } blocks, comments, and whitespace — producing a structured AST with Stylesheet, QualifiedRule, AtRule, Declaration, Selector, PropertyName, and Value nodes. Function calls and [ ]/( ) groups are captured inside their enclosing Value range rather than as separate nodes.

Input is always a Uint8Array. Output is always a LexTree. Malformed CSS produces CssKind.Error nodes — parseCss never throws.

The parser runs all 125 CourtBouillon css-parsing-tests inputs with full never-throw and error-detection agreement (the harness verifies never-throw, error-node presence matching fixture expectations, and root-kind correctness — not yet structural tree comparison). Comments and whitespace are preserved for round-trip fidelity.

import {
parseCss,
CssKind,
CSS_KIND_NAMES_BY_ID,
cssGrammar,
type ParseCssOptions,
type CssKindType,
} from '@lanexio/parser-grammar-css';
import { parseCss } from '@lanexio/parser-grammar-css';
const encoder = new TextEncoder();
const tree = parseCss(encoder.encode('body { color: red; font-size: 16px; }'));
console.log(tree.nodeCount); // total nodes
console.log(tree.root.kind); // Stylesheet root kind id (0x0900)

parseCss accepts a Uint8Array. Always use TextEncoder when converting a string to bytes.

const tree = parseCss(encoder.encode(`
@media screen and (min-width: 768px) {
.container { max-width: 720px; }
}
`));
// Produces: Stylesheet → AtRule("@media") → Block → QualifiedRule(".container") → ...
import { parseCss, CssKind } from '@lanexio/parser-grammar-css';
const tree = parseCss(encoder.encode('/* header */\nbody { color: red; }'));
for (const node of tree.root.children()) {
if (node.kind === CssKind.Comment) {
console.log('comment:', /* extract bytes via node.range */);
}
}

ParseCssOptions is an empty record (Record<string, never>). Reserved for future extension.

import { parseCss, CssKind } from '@lanexio/parser-grammar-css';
const encoder = new TextEncoder();
const tree = parseCss(encoder.encode('body { color: }')); // missing value
for (const node of tree.root.children()) {
if (node.kind === CssKind.Error) {
console.log('parse error at', node.range);
}
}

parseCss never throws. Malformed CSS produces CssKind.Error nodes. The parser always recovers.

CSS trees carry field metadata for the three structural roles, so childByField() works on rules and declarations:

Field nameNodeParent
selectorSelectorQualifiedRule
propertyPropertyNameDeclaration
valueValueDeclaration
import { parseCss, CssKind } from '@lanexio/parser-grammar-css';
const encoder = new TextEncoder();
const tree = parseCss(encoder.encode('body { color: red; }'));
for (const rule of tree.root.children()) {
if (rule.kind !== CssKind.QualifiedRule) continue;
console.log('selector:', rule.childByField('selector')?.text); // "body"
// Declarations live inside the rule's Block.
for (const child of rule.children()) {
if (child.kind !== CssKind.Block) continue;
for (const decl of child.children()) {
if (decl.kind !== CssKind.Declaration) continue;
const prop = decl.childByField('property');
const value = decl.childByField('value');
console.log(prop?.text, ':', value?.text); // color : red
}
}
}

CssField exposes the numeric ids (CssField.Selector, CssField.Property, CssField.Value) for hot loops comparing node.fieldId directly.

import { CssKind } from '@lanexio/parser-grammar-css';
// CssKind is a const object. Use 'as const' pattern, never enum.
const kind: CssKindType = CssKind.Declaration;
// Node kind IDs (0x0900 block)
CssKind.Stylesheet; // 0x0900
CssKind.QualifiedRule; // 0x0901
CssKind.AtRule; // 0x0902
CssKind.Declaration; // 0x0903
CssKind.Block; // emitted for { } blocks only; [ ] and ( ) flatten into Value // 0x0904
CssKind.Function; // reserved — not yet emitted; function calls flatten into Value // 0x0905
CssKind.Selector; // 0x0906
CssKind.PropertyName; // 0x0907
CssKind.Value; // 0x0908
CssKind.Comment; // 0x0909
CssKind.Whitespace; // 0x090a
CssKind.Error; // 0x09ff

CssKind values are stable across versions. Never use raw numbers — always reference CssKind.<name>.

ExportTypeDescription
parseCss(bytes: Uint8Array, options?: ParseCssOptions) => LexTreeParse CSS Syntax Level 3. Never throws.
ParseCssOptionsRecord<string, never>Options for parseCss (empty, reserved).
CssKindconst objectNumeric kind IDs for all CSS node types (0x0900 block).
CssKindTypetype unionUnion of all CssKind values.
CSS_KIND_NAMES_BY_IDReadonly<Record<number, string>>Kind-name lookup by numeric ID.
CssFieldconst objectField role ids: Selector, Property, Value (and None).
CSS_FIELD_NAMES_BY_IDReadonly<Record<number, string>>Field-name lookup; wired into every parsed tree.
cssGrammarLanexioParserPureGrammarGrammar descriptor — pass to createParser from @lanexio/parser.
MetricValue
CourtBouillon corpus (never-throw + error-detection agreement)125 / 125
Error cases (not thrown)24
verify:no-throw54 entry points
Bundle4,779 / 16,384 B (29.2%)
Fuzz soak (300s)4.3M rounds / 0 crashes
  • @lanexio/parser-core — shared buffer protocol, LexTree, LexNode, LexCursor.
  • @lanexio/parser — unified entry point; pass this package’s grammar descriptor to createParser for a string-accepting, never-throwing parser handle.