Skip to main content

Crate tabnas_css

Crate tabnas_css 

Source
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

typeFields
stylesheetrules
ruleselectors, declarations
declarationproperty, value (raw, trimmed, comments stripped, quotes kept)
commentcomment (the text between /* and */)
media / supports / document / hosta prelude field of the same name (host has none), plus rules. document also always carries vendor
font-face / pagedeclarations (page also selectors, its prelude split on top-level commas)
keyframesname, an optional vendor, and keyframes of keyframe { values, declarations }
import / charset / namespacea same-named field with the raw params
custom-medianame, 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 = false

Re-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.jsonic text, and the option overrides the plugin installs with it.
lex
The lexer: the single cssToken matcher the plugin installs on the engine, and the scanners it reads spans with.
value
The AST value model: Node, an insertion-ordered map with a type discriminator, and Value, what a node field can hold.

Structs§

Css
A reusable CSS parser.
Options
The plugin’s options: CssOptions in the canonical port.
TranslationPart
One alchemy translation source and the entry point a host calls in it.
TranslationParts
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 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.
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: Css in the canonical port. Its options are an Options bag (Options::to_value).
translate
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.