Skip to main content

parse/
parse.rs

1/* Copyright (c) 2025 Richard Rodger, MIT License */
2
3//! Parse CSS and print the AST as JSON.
4//!
5//! ```text
6//! echo 'a { color: red }' | cargo run --example parse
7//! cargo run --example parse -- --position < style.css
8//! ```
9//!
10//! With `--lines`, each line of stdin is one document, with `\n`, `\r`, `\t`
11//! and `\\` decoded — the same escape codec the shared `test/spec/*.tsv`
12//! fixtures use — and one result is printed per line. That is the mode the
13//! TypeScript/Rust differential probe drives.
14//!
15//! A document that fails to parse prints `ERROR:<code>` rather than the AST,
16//! so accept/reject shows up in the same stream as the values.
17
18use std::io::Read;
19
20use tabnas_css::{Css, Options};
21
22fn main() {
23    let mut options = Options::default();
24    let mut lines = false;
25    for arg in std::env::args().skip(1) {
26        match arg.as_str() {
27            "--position" => options.position = true,
28            "--lowercase-properties" => options.lowercase_properties = true,
29            "--lines" => lines = true,
30            other => {
31                eprintln!("parse: unknown argument {other:?}");
32                eprintln!("usage: parse [--position] [--lowercase-properties] [--lines]");
33                std::process::exit(2);
34            }
35        }
36    }
37
38    let mut input = String::new();
39    if std::io::stdin().read_to_string(&mut input).is_err() {
40        eprintln!("parse: stdin is not valid UTF-8");
41        std::process::exit(2);
42    }
43
44    let css = Css::with_options(options);
45    if lines {
46        // Split on '\n' only: `str::lines` also strips a trailing '\r',
47        // which would quietly drop the very characters a carriage-return
48        // probe is there to test.
49        let mut rows: Vec<&str> = input.split('\n').collect();
50        if Some(&"") == rows.last() {
51            rows.pop();
52        }
53        for line in rows {
54            println!("{}", report(&css, &decode(line)));
55        }
56    } else {
57        println!("{}", report(&css, &input));
58    }
59}
60
61fn report(css: &Css, src: &str) -> String {
62    match css.parse(src) {
63        Ok(ast) => ast.to_json(),
64        Err(err) => format!("ERROR:{}", err.code),
65    }
66}
67
68/// Decode the `\n` `\r` `\t` `\\` escapes the shared fixtures use.
69fn decode(line: &str) -> String {
70    let mut out = String::with_capacity(line.len());
71    let mut chars = line.chars();
72    while let Some(c) = chars.next() {
73        if '\\' != c {
74            out.push(c);
75            continue;
76        }
77        match chars.next() {
78            Some('n') => out.push('\n'),
79            Some('r') => out.push('\r'),
80            Some('t') => out.push('\t'),
81            Some('\\') => out.push('\\'),
82            Some(other) => {
83                out.push('\\');
84                out.push(other);
85            }
86            None => out.push('\\'),
87        }
88    }
89    out
90}