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
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
/* 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, and a plugin
//!
//! `@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.
//!
//! All three are plugins on the tabnas engine, layered on the relaxed-JSON
//! grammar jsonic, whose fixed tokens and machinery the grammar reuses:
//! [`plugin`] installs the grammar, its option overrides and the `cssToken`
//! matcher on an engine, and [`make`] builds one with it installed. [`Css`] is
//! that engine behind this crate's own [`Value`], in which a tree of any
//! depth can be dropped, cloned, compared and printed without recursion. The
//! crate re-exports the two it is built on, [`tabnas`] and [`tabnas_jsonic`]:
//! name their types through those paths, and a second copy of either, which
//! a dependency of your own can resolve to, never enters the build.
//!
//! # 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.
//!
//! **Cap the input's size.** A parse holds memory, and takes time, in
//! proportion to its input, and nesting costs the most. Measured in a release
//! build: 100,000 nested style rules (0.6 MB of CSS) peaked at 678 MiB, about
//! 7 KiB per open level, and the densest nesting, `a{` repeated, holds about
//! 2.8 KiB and takes about 6 µs per byte of input. A flat stylesheet of
//! 100,000 rules (1.7 MB) peaked at 210 MiB, 580 MiB with positions on, and
//! one long token holds about 18 bytes per byte. [`Css::parse`] has no depth
//! limit; the engine's own tree form ([`plugin`], [`make`], [`Css::tabnas`])
//! stops at [`TREE_RULE_DEPTH`] open rules with `cancel`. The engine's
//! recovery and relexing modes, which [`Css::parse`] never uses, take time
//! quadratic in the input in the engine this crate builds on.
//!
//! **Turn the engine's debug self-check off in your debug builds.** With
//! debug assertions on, the engine compares its whole rule stack with a
//! shadow copy on every step, which is quadratic in nesting depth (4,000
//! nested rules took 134 s in a debug build). This crate's manifest turns
//! it off, but a profile applies only to the root package, so a crate that
//! parses untrusted, possibly deep CSS sets the same key in its own:
//!
//! ```toml
//! [profile.dev.package.tabnas-parser]
//! debug-assertions = false
//! ```
// The engine's error is large, and a parse returns it by value, as every
// grammar crate in the fleet does.
// A link in the public documentation that points at nothing, or at a
// private item, renders as dead text on docs.rs. Denied here, so any
// `cargo doc` fails on one; ci/rust/run.sh runs it with warnings denied.
/// 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 fmt;
use OnceLock;
use ;
pub use Error;
pub use TREE_RULE_DEPTH;
pub use ;
/// The engine, as this crate links it. Name its types through this path
/// (`tabnas_css::tabnas::Value`): a dependency of your own on the engine can
/// resolve to another copy of the crate, whose types are not these.
pub use tabnas;
/// The jsonic grammar, as this crate links it, for installing [`plugin`] on
/// a jsonic engine of your own (`tabnas_css::tabnas_jsonic::make()`).
pub use tabnas_jsonic;
/// 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.15";
/// The plugin's options: `CssOptions` in the canonical port.
/// JavaScript's `!!value`.
/// The css grammar as an engine plugin, named `css`: `Css` in the canonical
/// port. Its options are an [`Options`] bag ([`Options::to_value`]).
///
/// It installs the grammar's rules (each alternate tagged with the group
/// `css`), the canonical option overrides and the `cssToken` matcher. Use it
/// on jsonic, as [`make`] does and the canonical port documents; on a bare
/// engine it gives the same results.
///
/// The engine's own parse returns the tree as an engine [`tabnas::Value`],
/// the canonical port's plain objects: equal to them value for value, though
/// the engine writes a whole number as `1.0` in JSON, a `position.end` that
/// was never recorded is absent rather than an undefined key, and an error's
/// column counts Unicode scalars as the engine's always do, where this
/// crate's [`Error`] counts UTF-16 code units (convert with [`Error::from`]).
/// That tree is bounded at [`TREE_RULE_DEPTH`] open rules. [`Css::parse`] has
/// none of these differences and no limit. With the engine's recovery on,
/// the parse returns a partial stylesheet, which is not always the
/// canonical port's: see `doc/concepts.md`.
///
/// ```
/// let mut parser = tabnas_css::tabnas_jsonic::make();
/// parser.use_plugin(tabnas_css::plugin(), None).unwrap();
/// let ast = parser.parse("a { color: red }").unwrap();
/// assert_eq!(
/// ast.to_json().to_string(),
/// r#"{"type":"stylesheet","rules":[{"type":"rule","selectors":["a"],"declarations":[{"type":"declaration","property":"color","value":"red"}]}]}"#
/// );
/// ```
/// Install the plugin on `parser` with `options`. The same as
/// `parser.use_plugin(plugin(), Some(options.to_value()))`, which is what it
/// does, so the install is recorded and runs again on a derived instance.
/// A jsonic engine with the plugin installed, with the default options.
/// A jsonic engine with the plugin installed, with `options`.
/// A reusable CSS parser.
///
/// Building one installs the grammar on a new engine, 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.
/// One alchemy translation source and the entry point a host calls in it.
/// The package-local structural translation interface: the manifest and
/// the alchemy sources a host composes a translation from.
const TRANSLATION: TranslationParts = TranslationParts ;
/// CSS's translation parts. The manifest's `translate` object says that CSS
/// is read as and written from a tree of its own shape, the reader's
/// stylesheet (the schema `css-ast`, whose root is an object), and the
/// render, `css-render`, writes that tree back as CSS text. There is no
/// embed: a host composes a translation into CSS only from CSS itself or
/// from a program that builds the tree. The texts are the crate's copies
/// in `translate/` of `tabnas.plugin.json` and `alchemy/render.alc`, which
/// `npm run embed` in `ts/` writes and `tests/translate.rs` holds to the
/// files.
///
/// ```
/// let parts = tabnas_css::translate().expect("CSS carries translation parts");
/// assert_eq!(parts.render.map(|part| part.entry), Some("css-render"));
/// assert!(parts.embed.is_none());
/// ```
pub const