css (Rust)
A parser that reads CSS
into a faithful abstract syntax tree (the
reworkcss/css model): ordered, typed
nodes that preserve declaration order, duplicate properties, rule types, and
comments.
This is the Rust port of @tabnas/css. The TypeScript package is canonical,
the Go module tracks it, and so does this crate. All three read the same
grammar file and are held to the same shared conformance fixtures, so the
same stylesheet gives the same tree in every one.
Where a port does differ, the difference is a row of
../test/divergent.tsv with a cell per runtime, and
each suite asserts its own cell. The four rows there today are all Go against
TypeScript; this crate follows TypeScript on every one, including the column
arithmetic it would be tidier to round off.
Install
The crate is not on crates.io yet. This repository's release workflow publishes the npm package and tags the Go module; putting a crate on crates.io is a separate decision with its own trusted-publishing setup, and until that happens the dependency is the repository:
[]
= { = "https://github.com/tabnas/css" }
Cargo finds the crate in rs/. Once it is published, cargo add tabnas-css
replaces that line and nothing else changes.
The crate has no dependencies of its own.
One example
parse is the one-call entry point: pass source, get the AST and an error:
let ast = parse.unwrap;
// {"type":"stylesheet","rules":[
// {"type":"rule","selectors":["a"],"declarations":[
// {"type":"declaration","property":"color","value":"red"}]}]}
Every node is a Node, an insertion-ordered map with a type key. Fields
such as rules, declarations and selectors are Value::List. Read them
with Node::get and the Value accessors, or call Value::to_json and hand
the text to whatever consumes it:
use Value;
let ast = parse.unwrap;
let rule = ast.as_node.unwrap
.get.and_then.unwrap
.as_node.unwrap;
assert_eq!;
parse reuses one process-wide parser and is safe to call from several
threads. Building a parser reads the grammar, which costs more than a parse
does, so a hot loop that needs options should hold a Css rather than call
parse_with each time.
Options
Options has two fields, both false by default:
lowercase_properties. Normalise property names to lower case. Selectors and values are left untouched.position. Attach aposition(1-basedstartandend, each a line and a column) to every node.
use ;
let css = with_options;
let ast = css.parse.unwrap;
// every node gains "position":{"start":{"line":1,"column":1},"end":{…}}
Columns count UTF-16 code units, which is what a JavaScript string index counts, so a column here is the column the canonical port reports for the same source.
CSS nesting
A style rule or an at-rule may appear inside another style rule's declaration
block. Nested nodes are appended to the parent's declarations in source
order, interleaved with declarations:
parse.unwrap;
// rule declarations: [
// {"type":"declaration","property":"color","value":"red"},
// {"type":"rule","selectors":["& b"],"declarations":[
// {"type":"declaration","property":"top","value":"0"}]}]
Why this crate has no dependencies
The TypeScript and Go ports are plugins: they install a grammar on a shared parsing engine and borrow its lexer, its rule machine and its relaxed-JSON base. There is no Rust build of that engine, so this crate carries a lexer and a rule machine of its own, reading the same grammar file the other two embed.
One consequence is visible to a caller. Those ports have to switch the
relaxed-JSON base off so that {a:1} is rejected; here there is no base to
switch off, and {a:1} is rejected because it is not CSS.
Untrusted input
A parsed stylesheet is data, never instructions. CSS arrives from outside the
system, 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.
Documentation
Full documentation follows the Diátaxis framework:
- Tutorial. A guided first parse, start to finish.
- How-to guide. Short recipes for individual tasks.
- Reference. The public API, every option, and the complete AST node reference.
- Concepts. How the parser is built, and how the Rust version differs from TypeScript.
For the canonical TypeScript implementation, see
../ts/README.md. For the Go port, see
../go/README.md.
License
MIT.