Skip to main content

tabnas_css/
machine.rs

1/* Copyright (c) 2025 Richard Rodger, MIT License */
2
3//! The rule machine: it runs the rule table read out of
4//! [`grammar`](crate::grammar) over the tokens [`lex`] produces,
5//! and the grammar-local actions build the AST as it goes.
6//!
7//! The canonical TypeScript port and the Go port hand this job to the tabnas
8//! engine. There is no Rust engine, so this module is the one piece of the
9//! port with no line-for-line counterpart — but it is not a new design: it
10//! implements the same rule/alt contract those alts are written against, so
11//! `css-grammar.jsonic` stays the single source of truth for all three ports.
12//!
13//! A rule runs in two phases. In the OPEN phase its `open` alts are tried in
14//! order; in the CLOSE phase its `close` alts are. An alt matches when the
15//! next tokens are its `s` sequence (no `s` matches unconditionally), and then
16//!
17//! - its `a` actions run, against this rule's matched tokens and node;
18//! - `b` of the matched tokens are pushed back — matched and recorded, but
19//!   left in the buffer for whatever reads next;
20//! - `p` pushes a child rule (this rule moves to its CLOSE phase and resumes
21//!   when the child pops), `r` replaces this rule with a fresh one, and
22//!   neither means: open → close, or close → pop.
23//!
24//! Tokens are lexed LAZILY — only as many as the alt being tried needs, under
25//! the rule trying it. That is what decides whether a comment becomes a node,
26//! so it is behaviour, not an optimisation: see [`crate::lex`].
27//!
28//! ## Node ownership
29//!
30//! In the canonical ports a child rule inherits its parent's node BY
31//! REFERENCE, so a selector pushed three rules down lands in the parent's
32//! node. Rust has no such aliasing without interior mutability, so the node
33//! MOVES down the stack instead: a pushed child takes the node, a constructor
34//! action hands it back before installing its own, and a popping rule either
35//! returns it or delivers its own node to the parent as `child`. Only the top
36//! frame ever runs, so the node is always where the running rule is.
37
38use crate::grammar::{Alt, Grammar, RuleDef};
39use crate::lex::{self, Error, Lex, Tin, Token};
40use crate::value::{Node, Value};
41
42/// Plugin options. Mirrors the TypeScript `CssOptions` and the Go
43/// `CssOptions`, and the defaults match `Css.defaults` / `Defaults`.
44#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
45pub struct Options {
46    /// Lowercase declaration property names (CSS property names are
47    /// case-insensitive). Selectors, values and at-rule preludes are
48    /// untouched. Off by default.
49    pub lowercase_properties: bool,
50    /// Attach a `position: { start: { line, column }, end: { … } }` (1-based)
51    /// to every node. Off by default — positions add noise.
52    pub position: bool,
53}
54
55/// Which phase a rule frame is in.
56#[derive(Clone, Copy, PartialEq, Eq, Debug)]
57enum Phase {
58    Open,
59    Close,
60}
61
62/// One rule on the stack.
63struct Frame<'g> {
64    /// The grammar rule this frame is running.
65    ///
66    /// Borrowed from the grammar, so pushing a rule costs no allocation —
67    /// the machine takes one step per token and then some.
68    name: &'g str,
69    /// Open or close.
70    phase: Phase,
71    /// The node being built. `None` while a child frame holds it.
72    node: Option<Node>,
73    /// Whether `node` belongs to the parent frame and must be handed back.
74    inherited: bool,
75    /// Tokens matched by the open alt (`r.o` in the canonical ports).
76    o: Vec<Token>,
77    /// Tokens matched by the close alt (`r.c`).
78    c: Vec<Token>,
79    /// The node the last child rule built (`r.child.node`).
80    child: Option<Node>,
81}
82
83impl<'g> Frame<'g> {
84    fn new(name: &'g str, node: Option<Node>, inherited: bool) -> Frame<'g> {
85        Frame {
86            name,
87            phase: Phase::Open,
88            node,
89            inherited,
90            o: Vec::new(),
91            c: Vec::new(),
92            child: None,
93        }
94    }
95}
96
97/// How many rule steps may pass without a token being consumed before the
98/// machine calls it a loop.
99///
100/// A budget counted in total steps would have to grow with the source and
101/// could still misfire on a large valid stylesheet. Non-progress cannot: every
102/// loop this grammar can make — `items` → `statement` → `items`, `decls` →
103/// `decl` → `decls` — consumes at least one token per turn, and the longest
104/// legitimate no-consume chain is a handful of frames.
105const MAX_IDLE_STEPS: usize = 64;
106
107/// Run the grammar over `src`.
108pub fn parse(grammar: &Grammar, src: &str, options: Options) -> Result<Value, Error> {
109    // The engine short-circuits an exactly-empty source before the rule loop
110    // runs, so the start rule never builds its node. reworkcss returns an
111    // empty stylesheet for "", so declare that here rather than expect the
112    // grammar to produce it — the canonical ports do the same, via
113    // `lex.emptyResult`.
114    if src.is_empty() {
115        let mut sheet = Node::new();
116        sheet.set("type", "stylesheet");
117        sheet.set("rules", Value::List(Vec::new()));
118        return Ok(Value::Node(sheet));
119    }
120
121    Machine {
122        grammar,
123        lex: Lex::new(src, options.lowercase_properties),
124        options,
125        stack: vec![Frame::new("stylesheet", None, false)],
126        idle: 0,
127    }
128    .run()
129}
130
131struct Machine<'g> {
132    grammar: &'g Grammar,
133    lex: Lex,
134    options: Options,
135    stack: Vec<Frame<'g>>,
136    idle: usize,
137}
138
139impl<'g> Machine<'g> {
140    fn run(mut self) -> Result<Value, Error> {
141        loop {
142            let top = self.stack.len() - 1;
143            let name: &'g str = self.stack[top].name;
144            let phase = self.stack[top].phase;
145
146            let grammar: &'g Grammar = self.grammar;
147            let def: &'g RuleDef = grammar.rule(name).ok_or_else(|| Error {
148                code: "unexpected".to_string(),
149                message: format!("no grammar rule named '{name}'"),
150                line: self.lex.point().r_i,
151                column: self.lex.point().c_i,
152            })?;
153            let alts: &'g [Alt] = match phase {
154                Phase::Open => &def.open,
155                Phase::Close => &def.close,
156            };
157
158            // Try the alts in order, lexing only what each one needs — and,
159            // within an alt, only as far as it still matches.
160            //
161            // The second half is behaviour, not thrift. `b{,/*!important`
162            // reaches `decl` with a `#GC` in hand: its `#TX #CL` alt fails on
163            // the FIRST token, so the unterminated comment behind it is never
164            // read and the parse fails as `unexpected`. Lexing the whole `s`
165            // up front would read it and fail as `unterminated_comment`
166            // instead — a different error code for the same document, and
167            // the fixtures compare codes exactly.
168            let mut chosen: Option<&'g Alt> = None;
169            for alt in alts {
170                let mut matched = true;
171                for (i, want) in alt.s.iter().enumerate() {
172                    let seen = self.lex.peek(i + 1, name)?;
173                    if want != seen[i].tin.name() {
174                        matched = false;
175                        break;
176                    }
177                }
178                if matched {
179                    chosen = Some(alt);
180                    break;
181                }
182            }
183            let Some(alt) = chosen else {
184                return Err(self.no_alt(name, phase));
185            };
186
187            // Record the matched tokens, then consume all but the `b` pushed
188            // back. `b` with no `s` is a no-op, as it is in the canonical
189            // ports: there are no matched tokens to push back.
190            let matched: Vec<Token> = self.lex.peek(alt.s.len(), name)?.to_vec();
191            let consume = alt.s.len().saturating_sub(alt.b);
192            self.lex.consume(consume);
193            if 0 < consume {
194                self.idle = 0;
195            } else {
196                self.idle += 1;
197                if MAX_IDLE_STEPS < self.idle {
198                    return Err(self.stuck(name));
199                }
200            }
201            match phase {
202                Phase::Open => self.stack[top].o = matched,
203                Phase::Close => self.stack[top].c = matched,
204            }
205
206            for action in &alt.a {
207                self.act(action);
208            }
209
210            if let Some(child) = &alt.p {
211                // Descend: the child takes this frame's node, and this frame
212                // resumes in its close phase once the child pops.
213                let node = self.stack[top].node.take();
214                let inherited = node.is_some();
215                self.stack[top].phase = Phase::Close;
216                let child = Frame::new(child, node, inherited);
217                self.stack.push(child);
218            } else if let Some(replacement) = &alt.r {
219                // Replace: a fresh rule in this frame's place, carrying the
220                // node and the parent relationship but no child.
221                let node = self.stack[top].node.take();
222                let inherited = self.stack[top].inherited;
223                self.stack[top] = Frame::new(replacement, node, inherited);
224            } else if Phase::Open == phase {
225                self.stack[top].phase = Phase::Close;
226            } else {
227                // Pop. A frame that merely borrowed its parent's node hands
228                // it straight back; one that built its own delivers it as the
229                // parent's `child`, which is what the pushers append.
230                let done = self.stack.pop().expect("stack is never empty here");
231                let Some(parent) = self.stack.last_mut() else {
232                    return Ok(done.node.map(Value::Node).unwrap_or(Value::Undefined));
233                };
234                if done.inherited {
235                    parent.node = done.node;
236                } else {
237                    parent.child = done.node;
238                }
239            }
240        }
241    }
242
243    /// No alt matched: report the token that stopped the parse.
244    fn no_alt(&mut self, name: &str, phase: Phase) -> Error {
245        let point = self.lex.point();
246        let (line, column, what) = match self.lex.peek(1, name) {
247            Ok(seen) => {
248                let tok = &seen[0];
249                let what = if Tin::Zz == tok.tin {
250                    "end of input".to_string()
251                } else {
252                    format!("{:?}", tok.val)
253                };
254                (tok.r_i, tok.c_i, what)
255            }
256            // The lookahead itself failed; that error is the real one.
257            Err(err) => return err,
258        };
259        Error {
260            code: "unexpected".to_string(),
261            message: format!(
262                "unexpected character(s): {what} (no '{name}' {} alternative matches)",
263                if Phase::Open == phase {
264                    "open"
265                } else {
266                    "close"
267                }
268            ),
269            line: if 0 < line { line } else { point.r_i },
270            column: if 0 < column { column } else { point.c_i },
271        }
272    }
273
274    /// The machine made no progress for [`MAX_IDLE_STEPS`] steps.
275    fn stuck(&self, name: &str) -> Error {
276        let point = self.lex.point();
277        Error {
278            code: "unexpected".to_string(),
279            message: format!(
280                "the grammar made no progress at rule '{name}' after \
281                 {MAX_IDLE_STEPS} steps without consuming a token"
282            ),
283            line: point.r_i,
284            column: point.c_i,
285        }
286    }
287
288    // --- Grammar actions: build the AST ------------------------------------
289
290    /// Run one `@cssXxx` action against the top frame.
291    ///
292    /// The constructors overwrite the frame's node; the field setters mutate
293    /// it; the pushers append a finished child node to one of its arrays.
294    /// An unknown reference is ignored, as an unresolved `@ref` is in the
295    /// canonical ports.
296    fn act(&mut self, action: &str) {
297        match action {
298            // Node constructors.
299            "@cssSheet" => {
300                let mut node = Node::new();
301                node.set("type", "stylesheet");
302                node.set("rules", Value::List(Vec::new()));
303                if self.options.position {
304                    node.set("position", position(Some((1, 1)), None));
305                }
306                self.set_node(node);
307            }
308            "@cssRule" => {
309                let mut node = Node::new();
310                node.set("type", "rule");
311                node.set("selectors", Value::List(Vec::new()));
312                node.set("declarations", Value::List(Vec::new()));
313                self.with_pos(node, false);
314            }
315            "@cssDecl" => {
316                let property = self.open_val();
317                let mut node = Node::new();
318                node.set("type", "declaration");
319                node.set("property", property);
320                node.set("value", "");
321                self.with_pos(node, false);
322            }
323            "@cssComment" => {
324                let comment = self.open_val();
325                let mut node = Node::new();
326                node.set("type", "comment");
327                node.set("comment", comment);
328                self.with_pos(node, true);
329            }
330            "@cssKeyframe" => {
331                let mut node = Node::new();
332                node.set("type", "keyframe");
333                node.set("values", Value::List(Vec::new()));
334                node.set("declarations", Value::List(Vec::new()));
335                self.with_pos(node, false);
336            }
337            "@cssAtRules" => {
338                let node = self.open_token().map(make_at_rules).unwrap_or_default();
339                self.with_pos(node, false);
340            }
341            "@cssAtDecls" => {
342                let node = self.open_token().map(make_at_decls).unwrap_or_default();
343                self.with_pos(node, false);
344            }
345            "@cssKeyframes" => {
346                let node = self.open_token().map(make_keyframes).unwrap_or_default();
347                self.with_pos(node, false);
348            }
349            "@cssAtStmt" => {
350                let node = self.open_token().map(make_at_stmt).unwrap_or_default();
351                self.with_pos(node, true);
352            }
353
354            // Field setters (mutate the node this rule inherited).
355            "@cssSelector" => {
356                let v = self.open_val();
357                if let Some(node) = self.node_mut() {
358                    node.push_to("selectors", Value::Str(v));
359                }
360            }
361            "@cssKfValue" => {
362                let v = self.open_val();
363                if let Some(node) = self.node_mut() {
364                    node.push_to("values", Value::Str(v));
365                }
366            }
367            "@cssDeclVal" => {
368                let v = self.open_val();
369                let end = self
370                    .options
371                    .position
372                    .then(|| self.open_token().map(lex::end_pos))
373                    .flatten();
374                if let Some(node) = self.node_mut() {
375                    node.set("value", Value::Str(v));
376                    if let Some(end) = end {
377                        set_end(node, end);
378                    }
379                }
380            }
381
382            // The closing-brace / end-of-input end position, recorded on the
383            // node the block belongs to. Runs in a close phase, so the
384            // matched `}`/end token is the frame's first close token.
385            "@cssEnd" => {
386                if !self.options.position {
387                    return;
388                }
389                let end = self.close_token().map(lex::end_pos);
390                if let (Some(end), Some(node)) = (end, self.node_mut()) {
391                    set_end(node, end);
392                }
393            }
394
395            // Array pushers.
396            "@cssPushRule" => self.push_child("rules"),
397            "@cssPushDecl" => self.push_child("declarations"),
398            "@cssPushKf" => self.push_child("keyframes"),
399
400            _ => {}
401        }
402    }
403
404    fn top(&mut self) -> &mut Frame<'g> {
405        self.stack.last_mut().expect("stack is never empty")
406    }
407
408    fn open_token(&self) -> Option<&Token> {
409        self.stack.last().and_then(|f| f.o.first())
410    }
411
412    fn close_token(&self) -> Option<&Token> {
413        self.stack.last().and_then(|f| f.c.first())
414    }
415
416    /// The value of the open alt's first matched token (`r.o[0].val`).
417    fn open_val(&self) -> String {
418        self.open_token().map(|t| t.val.clone()).unwrap_or_default()
419    }
420
421    fn node_mut(&mut self) -> Option<&mut Node> {
422        self.top().node.as_mut()
423    }
424
425    /// Install a freshly built node, handing any inherited one back to the
426    /// parent first (the canonical ports simply reassign `r.node`, which
427    /// drops this rule's reference and leaves the parent's intact).
428    fn set_node(&mut self, node: Node) {
429        let top = self.stack.len() - 1;
430        if self.stack[top].inherited {
431            let borrowed = self.stack[top].node.take();
432            self.stack[top].inherited = false;
433            if 0 < top {
434                self.stack[top - 1].node = borrowed;
435            }
436        }
437        self.stack[top].node = Some(node);
438    }
439
440    /// Install a node, recording its start position from the open token —
441    /// and its end too when `end` is set, for a node that is one token wide.
442    fn with_pos(&mut self, mut node: Node, end: bool) {
443        if self.options.position {
444            if let Some(tok) = self.open_token() {
445                let start = lex::start_pos(tok);
446                let stop = end.then(|| lex::end_pos(tok));
447                node.set("position", position(Some(start), stop));
448            }
449        }
450        self.set_node(node);
451    }
452
453    /// Append the child node this frame's last child built.
454    fn push_child(&mut self, field: &str) {
455        let top = self.stack.len() - 1;
456        let Some(child) = self.stack[top].child.take() else {
457            return;
458        };
459        if let Some(node) = self.stack[top].node.as_mut() {
460            node.push_to(field, Value::Node(child));
461        }
462    }
463}
464
465/// A `position` value. `end` is [`Value::Undefined`] when unknown: the key
466/// exists, in insertion order, but does not serialise — which is what the
467/// canonical port's `end: undefined` does.
468fn position(start: Option<(usize, usize)>, end: Option<(usize, usize)>) -> Value {
469    let mut pos = Node::new();
470    if let Some((line, column)) = start {
471        pos.set("start", point(line, column));
472    }
473    match end {
474        Some((line, column)) => pos.set("end", point(line, column)),
475        None => pos.set("end", Value::Undefined),
476    }
477    Value::Node(pos)
478}
479
480fn point(line: usize, column: usize) -> Value {
481    let mut p = Node::new();
482    p.set("line", Value::Num(line as f64));
483    p.set("column", Value::Num(column as f64));
484    Value::Node(p)
485}
486
487/// Record `end` on a node's existing position, leaving a node without one
488/// alone (positions are off by default, and then there is nothing to record).
489fn set_end(node: &mut Node, (line, column): (usize, usize)) {
490    if let Some(Value::Node(pos)) = node.get_mut("position") {
491        pos.set("end", point(line, column));
492    }
493}
494
495/// Build a block at-rule node whose body is a list of rules (`@media`,
496/// `@supports`, `@document`, `@host`, and generic block at-rules).
497fn make_at_rules(tok: &Token) -> Node {
498    let kw = tok.val.as_str();
499    let prelude = tok.use_text.as_str();
500    let mut node = Node::new();
501    match kw {
502        "media" => {
503            node.set("type", "media");
504            node.set("media", prelude);
505        }
506        "supports" => {
507            node.set("type", "supports");
508            node.set("supports", prelude);
509        }
510        "host" => {
511            node.set("type", "host");
512        }
513        _ if "document" == kw || kw.ends_with("-document") => {
514            // `vendor` is always present on a document node (the empty string
515            // when the at-keyword carries no prefix), as in the reworkcss
516            // model.
517            node.set("type", "document");
518            node.set("document", prelude);
519            node.set("vendor", lex::vendor_prefix(kw).unwrap_or(""));
520        }
521        // A generic block at-rule with a rules body (@container, @layer,
522        // @scope, …): the keyword is both the node type and the field
523        // carrying its prelude.
524        _ => {
525            node.set("type", kw);
526            node.set(kw, prelude);
527        }
528    }
529    node.set("rules", Value::List(Vec::new()));
530    node
531}
532
533/// Build a block at-rule node whose body is declarations (`@font-face`,
534/// `@page`, and generic declaration at-rules).
535fn make_at_decls(tok: &Token) -> Node {
536    let kw = tok.val.as_str();
537    let mut node = Node::new();
538    node.set("type", kw);
539    if "page" == kw {
540        // A @page prelude is a selector group: `@page toc, index:blank`.
541        let selectors = lex::split_selectors(&tok.use_text)
542            .into_iter()
543            .map(Value::Str)
544            .collect();
545        node.set("selectors", Value::List(selectors));
546    }
547    node.set("declarations", Value::List(Vec::new()));
548    node
549}
550
551/// Build a `@keyframes` node, possibly vendor-prefixed.
552fn make_keyframes(tok: &Token) -> Node {
553    let kw = tok.val.as_str();
554    let mut node = Node::new();
555    node.set("type", "keyframes");
556    node.set("name", tok.use_text.as_str());
557    if let Some(vendor) = lex::vendor_prefix(kw) {
558        node.set("vendor", vendor);
559    }
560    node.set("keyframes", Value::List(Vec::new()));
561    node
562}
563
564/// Build a statement at-rule node (`@import`, `@charset`, `@namespace`, …):
565/// the at-keyword is the node type and the field carrying its params.
566fn make_at_stmt(tok: &Token) -> Node {
567    let kw = tok.val.as_str();
568    let params = tok.use_text.as_str();
569    let mut node = Node::new();
570    // `@custom-media --name <media query>` splits its params into a name and
571    // a media query, as in the reworkcss model.
572    if "custom-media" == kw {
573        if let Some((name, media)) = split_custom_media(params) {
574            node.set("type", "custom-media");
575            node.set("name", name);
576            node.set("media", media);
577            return node;
578        }
579    }
580    node.set("type", kw);
581    node.set(kw, params);
582    node
583}
584
585/// `^(--\S+)\s*([\s\S]*)$` — the `@custom-media` params split, without a
586/// regex engine.
587///
588/// The boundary is ECMAScript whitespace, not Rust's: a JavaScript regex `\s`
589/// matches the same set `String.prototype.trim` strips, so `--n\u{85}(x)` is
590/// ONE `\S+` run there and must be one name here. See
591/// [`lex::es_is_whitespace`].
592fn split_custom_media(params: &str) -> Option<(&str, &str)> {
593    if !params.starts_with("--") {
594        return None;
595    }
596    let name_end = params.find(lex::es_is_whitespace).unwrap_or(params.len());
597    if name_end <= 2 {
598        return None;
599    }
600    Some((&params[..name_end], lex::es_trim(&params[name_end..])))
601}