Parsing CSS
Package: @lanexio/parser-grammar-css Stable
Layer: 2 (Grammar). Depends only on @lanexio/parser-core.
Runtime: Universal (browser, server, edge worker).
Overview
Section titled “Overview”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
Section titled “Import”import { parseCss, CssKind, CSS_KIND_NAMES_BY_ID, cssGrammar, type ParseCssOptions, type CssKindType,} from '@lanexio/parser-grammar-css';Parse a stylesheet
Section titled “Parse a stylesheet”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 nodesconsole.log(tree.root.kind); // Stylesheet root kind id (0x0900)parseCss accepts a Uint8Array. Always use TextEncoder when converting a string to bytes.
At-rules and nested blocks
Section titled “At-rules and nested blocks”const tree = parseCss(encoder.encode(`@media screen and (min-width: 768px) { .container { max-width: 720px; }}`));// Produces: Stylesheet → AtRule("@media") → Block → QualifiedRule(".container") → ...Comments and whitespace preserved
Section titled “Comments and whitespace preserved”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
Section titled “ParseCssOptions”ParseCssOptions is an empty record (Record<string, never>). Reserved for future extension.
Detect LexError nodes
Section titled “Detect LexError nodes”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.
Navigate by field name
Section titled “Navigate by field name”CSS trees carry field metadata for the three structural roles, so
childByField() works on rules and declarations:
| Field name | Node | Parent |
|---|---|---|
selector | Selector | QualifiedRule |
property | PropertyName | Declaration |
value | Value | Declaration |
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.
CssKind constants
Section titled “CssKind constants”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; // 0x0900CssKind.QualifiedRule; // 0x0901CssKind.AtRule; // 0x0902CssKind.Declaration; // 0x0903CssKind.Block; // emitted for { } blocks only; [ ] and ( ) flatten into Value // 0x0904CssKind.Function; // reserved — not yet emitted; function calls flatten into Value // 0x0905CssKind.Selector; // 0x0906CssKind.PropertyName; // 0x0907CssKind.Value; // 0x0908CssKind.Comment; // 0x0909CssKind.Whitespace; // 0x090aCssKind.Error; // 0x09ffCssKind values are stable across versions. Never use raw numbers — always reference CssKind.<name>.
Full exports
Section titled “Full exports”| Export | Type | Description |
|---|---|---|
parseCss | (bytes: Uint8Array, options?: ParseCssOptions) => LexTree | Parse CSS Syntax Level 3. Never throws. |
ParseCssOptions | Record<string, never> | Options for parseCss (empty, reserved). |
CssKind | const object | Numeric kind IDs for all CSS node types (0x0900 block). |
CssKindType | type union | Union of all CssKind values. |
CSS_KIND_NAMES_BY_ID | Readonly<Record<number, string>> | Kind-name lookup by numeric ID. |
CssField | const object | Field role ids: Selector, Property, Value (and None). |
CSS_FIELD_NAMES_BY_ID | Readonly<Record<number, string>> | Field-name lookup; wired into every parsed tree. |
cssGrammar | LanexioParserPureGrammar | Grammar descriptor — pass to createParser from @lanexio/parser. |
Conformance
Section titled “Conformance”| Metric | Value |
|---|---|
| CourtBouillon corpus (never-throw + error-detection agreement) | 125 / 125 |
| Error cases (not thrown) | 24 |
| verify:no-throw | 54 entry points |
| Bundle | 4,779 / 16,384 B (29.2%) |
| Fuzz soak (300s) | 4.3M rounds / 0 crashes |
Companion packages
Section titled “Companion packages”@lanexio/parser-core— shared buffer protocol,LexTree,LexNode,LexCursor.@lanexio/parser— unified entry point; pass this package’s grammar descriptor tocreateParserfor a string-accepting, never-throwing parser handle.