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((¶ms[..name_end], lex::es_trim(¶ms[name_end..])))
601}