Expand description
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:
[profile.dev.package.tabnas-parser]
debug-assertions = falseRe-exports§
pub use lex::Error;pub use value::Node;pub use value::Value;pub use tabnas;pub use tabnas_jsonic;
Modules§
- grammar
- The grammar: the embedded
css-grammar.jsonictext, and the option overrides the plugin installs with it. - lex
- The lexer: the single
cssTokenmatcher the plugin installs on the engine, and the scanners it reads spans with. - value
- The AST value model:
Node, an insertion-ordered map with atypediscriminator, andValue, what a node field can hold.
Structs§
- Css
- A reusable CSS parser.
- Options
- The plugin’s options:
CssOptionsin the canonical port. - Translation
Part - One alchemy translation source and the entry point a host calls in it.
- Translation
Parts - The package-local structural translation interface: the manifest and the alchemy sources a host composes a translation from.
Constants§
- TREE_
RULE_ DEPTH - The most rules the tree form lets be open at once.
- VERSION
- This crate’s version.
Functions§
- css
- Install the plugin on
parserwithoptions. The same asparser.use_plugin(plugin(), Some(options.to_value())), which is what it does, so the install is recorded and runs again on a derived instance. - make
- A jsonic engine with the plugin installed, with the default options.
- make_
with - A jsonic engine with the plugin installed, with
options. - parse
- Parse a CSS document with the default options.
- parse_
with - Parse a CSS document with the given options.
- plugin
- The css grammar as an engine plugin, named
css:Cssin the canonical port. Its options are anOptionsbag (Options::to_value). - translate
- CSS’s translation parts. The manifest’s
translateobject says that CSS is read as and written from a tree of its own shape, the reader’s stylesheet (the schemacss-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 intranslate/oftabnas.plugin.jsonandalchemy/render.alc, whichnpm run embedints/writes andtests/translate.rsholds to the files.