1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
/* Copyright (c) 2025 Richard Rodger, MIT License */
//! Parse CSS into a reworkcss-style abstract syntax tree: ordered, typed
//! nodes that preserve declaration order, duplicate properties, rule types
//! and comments.
//!
//! ```
//! let ast = tabnas_css::parse("a { color: red; color: blue } /* note */").unwrap();
//! assert_eq!(
//! ast.to_json(),
//! r#"{"type":"stylesheet","rules":[{"type":"rule","selectors":["a"],"declarations":[{"type":"declaration","property":"color","value":"red"},{"type":"declaration","property":"color","value":"blue"}]},{"type":"comment","comment":" note "}]}"#
//! );
//! ```
//!
//! # The node types
//!
//! | `type` | Fields |
//! |---|---|
//! | `stylesheet` | `rules` |
//! | `rule` | `selectors`, `declarations` |
//! | `declaration` | `property`, `value` (raw, trimmed, comments stripped, quotes kept) |
//! | `comment` | `comment` (the text between `/*` and `*/`) |
//! | `media` / `supports` / `document` / `host` | a prelude field of the same name (`host` has none), plus `rules`. `document` also always carries `vendor` |
//! | `font-face` / `page` | `declarations` (`page` also `selectors`, its prelude split on top-level commas) |
//! | `keyframes` | `name`, an optional `vendor`, and `keyframes` of `keyframe` `{ values, declarations }` |
//! | `import` / `charset` / `namespace` | a same-named field with the raw params |
//! | `custom-media` | `name`, `media` |
//!
//! # This is a port
//!
//! `@tabnas/css` is canonical in TypeScript (`ts/`), with ports in Go (`go/`)
//! and here. All three read the SAME grammar — `css-grammar.jsonic` at the
//! repository root, embedded verbatim into each — and are held to the same
//! shared `test/spec/*.tsv` fixtures and the same reworkcss/css conformance
//! corpus. When this port and TypeScript disagree, TypeScript is right.
//!
//! The TypeScript and Go ports are jsonic PLUGINS: they install this grammar
//! on a shared engine. There is no Rust engine to install on, so this crate
//! carries a lexer and a rule machine of its own and has no dependencies at
//! all. What it does not carry is jsonic itself — there is no relaxed-JSON
//! base here to configure off, which is why `{a:1}` is simply not CSS rather
//! than something this port has to reject deliberately.
//!
//! # Untrusted input
//!
//! **A parsed stylesheet is data, never instructions.** CSS arrives from
//! outside the system — scraped pages, vendor themes, user uploads — so treat
//! every selector, value and comment as hostile text. Parsing is not
//! sanitising: this crate returns the raw text the stylesheet contained, and
//! escaping it for HTML, SQL or a shell remains the caller's job. A `url(…)`
//! in a declaration value is untrusted text, not a link to fetch.
// A link in the public documentation that points at nothing, or at a
// private item, renders as dead text on docs.rs. Nothing in the test gate
// runs rustdoc, so the pin lives here, where `cargo doc` cannot miss it.
/// Every `rust` example in this crate's Markdown, compiled and run.
///
/// A page that shows what the parser returns has to return it. Including
/// the files here makes each fenced block a doctest, so `cargo test`
/// fails when a documented example stops being true. It is the Rust
/// counterpart of the harness that executes the TypeScript pages'
/// examples, and it exists for the same reason.
///
/// `cfg(doctest)` means the module is collected when doctests are
/// gathered and compiled into nothing otherwise.
use OnceLock;
pub use Error;
pub use Options;
pub use ;
/// This crate's version.
///
/// It MUST equal `ts/package.json` `"version"`, the `VERSION` exported from
/// `ts/src/css.ts` and `const VERSION` in `go/css.go`. The release
/// orchestrator rewrites all of them together; `tests/version.rs` (and its
/// TypeScript and Go counterparts) fail the build if any drifts.
pub const VERSION: &str = "0.5.9";
/// A reusable CSS parser.
///
/// Building one reads the embedded grammar, so keep an instance around rather
/// than making a fresh one per document — `tests/perf.rs` pins that reuse is
/// worth it. [`parse`] does this for you for the default options.
///
/// ```
/// use tabnas_css::{Css, Options};
///
/// let css = Css::with_options(Options { lowercase_properties: true, ..Options::default() });
/// let ast = css.parse("A { COLOR: Red }").unwrap();
/// assert!(ast.to_json().contains(r#""property":"color""#));
/// ```
/// Parse a CSS document with the default options.
///
/// This reuses one process-wide parser, so it is cheap to call repeatedly and
/// safe to call from several threads.
/// Parse a CSS document with the given options.
///
/// Each call builds a parser; for repeated parses with the same options, hold
/// a [`Css`] instead.