///|
/// A CSS syntax tree.
///
/// The shape follows lightningcss for vocabulary -- typed selectors, typed
/// at-rule preludes, n-ary conditions -- and biome for tolerance: every enum a
/// parse can fail inside of has a `Bogus` member, so a parse always yields a
/// tree rather than an error, and the tree always covers the whole file.
///
/// It is a SEMANTIC tree, not a lossless one. `parse` then `write` gives back
/// CSS that means the same thing, not the same bytes: the printer decides
/// layout, and a declaration's original spacing is not recorded. Two things are
/// preserved anyway, because losing them is a semantic loss rather than a
/// formatting one:
///
/// * a number keeps its source spelling in `Number::repr`, so `1.50` and
/// `+1` and `1e3` survive a round trip;
/// * a hex colour keeps its digits and their case.
///
/// lightningcss quantises colours to `RGBA{u8,u8,u8,u8}`, which makes `#fff`,
/// `#ffffff` and `white` indistinguishable. That is right for a minifier and
/// wrong for anything that has to hand the stylesheet back to a person, so the
/// typed reading of a value lives in `css/value` as a lens over the syntax
/// rather than in the syntax itself.
///
/// Nothing here depends on `error-report`: a `Bogus` carries an `@kind.ErrorKind`,
/// which is a plain closed enum, so building a tree and printing it links
/// neither the diagnostic library nor the parser.
///|
/// What could not be parsed, kept so that the tree covers the whole source.
///
/// `text` is the exact source the node replaced. Keeping it is what lets the
/// printer echo an unparsable region verbatim instead of deleting it -- a
/// stylesheet with one vendor hack the parser does not know should come back
/// with that hack still in it.
pub(all) struct Bogus {
kind : @kind.ErrorKind
span : @span.Span
text : String
} derive(Eq, Debug)
///|
pub fn Bogus::new(
kind : @kind.ErrorKind,
span : @span.Span,
text? : String = "",
) -> Bogus {
{ kind, span, text, }
}
///|
/// A `/* ... */` comment.
///
/// `text` is the body, without the delimiters. Comments are attached to the
/// construct they precede rather than being free-floating, because a printer
/// that keeps them has to know where to put them back.
pub(all) struct Comment {
text : String
span : @span.Span
} derive(Eq, Debug)
// ---------------------------------------------------------------- stylesheet
///|
pub(all) struct Stylesheet {
items : Array[TopItem]
span : @span.Span
} derive(Eq, Debug)
///|
pub(all) enum TopItem {
Rule(CssRule)
Comment(Comment)
Bogus(Bogus)
} derive(Eq, Debug)
///|
/// A rule, at any level.
///
/// Style rules and at-rules are one enum rather than two because CSS nesting
/// lets either appear wherever the other can, and splitting them would mean
/// every body was a pair of lists that had lost their relative order.
pub(all) enum CssRule {
Style(StyleRule)
Media(MediaRule)
Supports(SupportsRule)
Container(ContainerRule)
Layer(LayerRule)
Keyframes(KeyframesRule)
FontFace(DeclBlock)
Page(PageRule)
Property(PropertyRule)
Import(ImportRule)
Charset(String, @span.Span)
Namespace(NamespaceRule)
Scope(ScopeRule)
StartingStyle(Array[BlockItem], @span.Span)
CounterStyle(String, DeclBlock)
/// An at-rule whose name this library does not know, kept with its prelude
/// unparsed. `block` distinguishes `@x { ... }` from `@x ...;`, which you
/// must know in order to print it back.
Unknown(UnknownAtRule)
Bogus(Bogus)
} derive(Eq, Debug)
///|
/// The body of a rule: declarations and nested rules, in source order.
///
/// One sequence rather than two fields, because the cascade is order-sensitive
/// and CSS nesting interleaves them. lightningcss splits `!important` out into
/// a second vector for the same reason a minifier would; that reordering is
/// unrecoverable, so importance is a field on `Declaration` here.
pub(all) enum BlockItem {
Decl(Declaration)
Rule(CssRule)
Comment(Comment)
Bogus(Bogus)
} derive(Eq, Debug)
///|
/// A block that may hold only declarations -- `@font-face`, `@page`, a
/// keyframe. Typed separately so the parser can refuse a nested rule there
/// rather than accepting one and printing something invalid.
pub(all) struct DeclBlock {
decls : Array[BlockItem]
span : @span.Span
} derive(Eq, Debug)
///|
pub(all) struct StyleRule {
selectors : Array[Selector]
body : Array[BlockItem]
span : @span.Span
} derive(Eq, Debug)
// -------------------------------------------------------------- declarations
///|
pub(all) struct Declaration {
property : PropertyName
value : Array[ComponentValue]
important : Bool
span : @span.Span
} derive(Eq, Debug)
///|
/// `Custom` carries the leading `--`, so that the two cases never have to be
/// re-joined to be printed and a `--` can never be lost.
pub(all) enum PropertyName {
Ident(String)
Custom(String)
} derive(Eq, Debug)
///|
pub fn PropertyName::text(self : PropertyName) -> String {
match self {
Ident(s) => s
Custom(s) => s
}
}
// --------------------------------------------------------------------- values
///|
/// A number, with the spelling it had in the source.
///
/// `repr` exists because CSS distinguishes `1` from `1.0` nowhere semantically
/// but everywhere visually, and a formatter that rewrote every number would
/// produce a diff on every line of a file it was asked only to reindent.
/// `value` is what arithmetic uses; `repr` is what the printer emits.
pub(all) struct Number {
repr : String
value : Double
is_int : Bool
} derive(Eq, Debug)
///|
pub fn Number::of_int(n : Int) -> Number {
{ repr: n.to_string(), value: n.to_double(), is_int: true, }
}
///|
/// A component value: the generic model that every declaration value uses.
///
/// Generic rather than per-property, and that is the load-bearing choice in the
/// whole design. lightningcss types some five hundred properties, which buys
/// static guarantees and costs the ability to represent anything the table does
/// not know -- a vendor hack, a property added last month, a value behind a
/// flag. A stylesheet full of those is not an edge case, it is Tuesday.
///
/// The typed reading is recovered on demand by the lenses in `css/value`
/// (`as_length`, `as_color`, ...), the way biome layers `value_ext.rs` over its
/// untyped tree. That keeps coverage total and makes the typed view something
/// you opt into per question rather than something the parser must decide up
/// front for every property in existence.
pub(all) enum ComponentValue {
/// A bare identifier, including a `--dashed-ident`.
Ident(String)
/// A quoted string, decoded.
Str(String)
Num(Number)
/// A number with a unit: `10px`, `1.5rem`, `200ms`.
Dimension(Number, String)
Percentage(Number)
/// The digits of a hex colour, without the `#`, case preserved.
Hex(String)
/// A `url(...)`, decoded, quoted or not.
Url(String)
Function(String, Array[ComponentValue])
/// A parenthesised group that is not a function call.
Paren(Array[ComponentValue])
Bracket(Array[ComponentValue])
/// A `,` separating values inside a list.
Comma
/// A `/`, which means something different in `grid-area`, in `font` and in
/// `rgb(... / ...)` -- so it is kept as itself rather than interpreted.
Slash
/// Any other operator run: `+`, `-`, `*`, `<`, `=`.
Delim(String)
Bogus(Bogus)
} derive(Eq, Debug)
// ------------------------------------------------------------------ selectors
///|
/// A selector.
///
/// `Complex` is left-nested by construction: its right operand is a `Compound`,
/// not a `Selector`, so `a > b + c` has exactly one shape and associativity
/// needs no normalisation pass. lightningcss stores a flat right-to-left
/// component vector, which is the right thing for a matcher and the wrong thing
/// for a printer; biome's recursion is what this follows.
pub(all) enum Selector {
Simple(Compound)
Complex(Selector, Combinator, Compound)
/// A selector that starts with a combinator, as a nested rule may:
/// `> .child`.
Relative(Combinator, Selector)
Bogus(Bogus)
} derive(Eq, Debug)
///|
pub(all) enum Combinator {
Descendant
Child
NextSibling
SubsequentSibling
Column
} derive(Eq, Debug)
///|
/// One element's worth of selector: an optional type, then qualifiers.
pub(all) struct Compound {
type_sel : TypeSelector?
quals : Array[Qualifier]
span : @span.Span
} derive(Eq, Debug)
///|
pub(all) enum TypeSelector {
Universal(NsPrefix?)
Named(NsPrefix?, String)
} derive(Eq, Debug)
///|
/// `|a` is `None_`, `*|a` is `Any`, `svg|a` is `Named`.
pub(all) enum NsPrefix {
None_
Any
Named(String)
} derive(Eq, Debug)
///|
pub(all) enum Qualifier {
Class(String)
Id(String)
Attr(AttrSelector)
Pseudo(PseudoClass)
Element(PseudoElement)
/// The `&` nesting selector.
Nesting
Bogus(Bogus)
} derive(Eq, Debug)
///|
/// Grouped by the *shape* of the argument, not by the name -- biome's choice,
/// and the one that makes printing expressible. `Simple` takes nothing, `Sub`
/// takes a selector list, `Nth` takes `An+B` with an optional `of` clause.
pub(all) enum PseudoClass {
Simple(String)
Nth(String, AnB, Array[Selector]?)
Sub(String, Array[Selector])
Lang(Array[String])
Dir(String)
/// A name this library does not know, kept with its arguments unparsed
/// rather than dropped.
Unknown(String, Array[ComponentValue])
} derive(Eq, Debug)
///|
pub(all) struct AnB {
a : Int
b : Int
} derive(Eq, Debug)
///|
pub(all) enum PseudoElement {
Simple(String)
Sub(String, Array[Selector])
Fn(String, Array[ComponentValue])
} derive(Eq, Debug)
///|
pub(all) struct AttrSelector {
ns : NsPrefix?
name : String
matcher : (AttrOp, AttrValue)?
case_ : AttrCase
} derive(Eq, Debug)
///|
pub(all) enum AttrOp {
Exact
Includes
DashMatch
Prefix
Suffix
Substring
} derive(Eq, Debug)
///|
pub fn AttrOp::text(self : AttrOp) -> String {
match self {
Exact => "="
Includes => "~="
DashMatch => "|="
Prefix => "^="
Suffix => "$="
Substring => "*="
}
}
///|
pub(all) enum AttrValue {
Str(String)
Ident(String)
} derive(Eq, Debug)
///|
pub(all) enum AttrCase {
Default
Insensitive
Sensitive
} derive(Eq, Debug)
// ----------------------------------------------------------------- conditions
///|
/// A `@media`, `@supports` or `@container` condition.
///
/// `Operation` is n-ary and stored flattened: `a and b and c` is one node with
/// three children, not two nested nodes. biome nests them because its grammar
/// language cannot express an n-ary list with interleaved keywords; there is no
/// such constraint here, and the flat form is what makes the round-trip
/// property a plain equality instead of an equality-modulo-associativity.
pub(all) enum Condition {
Feature(Feature)
/// `@supports (display: grid)`.
Decl(Declaration)
/// `@supports selector(a > b)`.
SelectorFn(Array[Selector])
Operation(LogicalOp, Array[Condition])
/// A condition this library could not read, kept unparsed. CSS itself
/// requires an unknown condition to evaluate false rather than to invalidate
/// the rule, so dropping it would change what the stylesheet means.
Unknown(Array[ComponentValue])
Bogus(Bogus)
} derive(Eq, Debug)
///|
pub(all) enum LogicalOp {
And
Or
Not
} derive(Eq, Debug)
///|
/// A media or container feature test, in all four shapes CSS gives it.
pub(all) enum Feature {
/// `(hover)`
Boolean(String)
/// `(width: 40rem)`
Plain(String, Array[ComponentValue])
/// `(width <= 40rem)`
Range(String, RangeOp, Array[ComponentValue])
/// `(20rem <= width <= 40rem)`
Interval(
Array[ComponentValue],
RangeOp,
String,
RangeOp,
Array[ComponentValue]
)
} derive(Eq, Debug)
///|
pub(all) enum RangeOp {
Lt
Le
Gt
Ge
Eq
} derive(Eq, Debug)
///|
pub fn RangeOp::text(self : RangeOp) -> String {
match self {
Lt => "<"
Le => "<="
Gt => ">"
Ge => ">="
Eq => "="
}
}
// -------------------------------------------------------------------- at-rules
///|
pub(all) struct MediaQuery {
qualifier : MediaQualifier?
media_type : String?
condition : Condition?
} derive(Eq, Debug)
///|
pub(all) enum MediaQualifier {
Only
Not
} derive(Eq, Debug)
///|
pub(all) struct MediaRule {
queries : Array[MediaQuery]
body : Array[BlockItem]
span : @span.Span
} derive(Eq, Debug)
///|
pub(all) struct SupportsRule {
condition : Condition
body : Array[BlockItem]
span : @span.Span
} derive(Eq, Debug)
///|
pub(all) struct ContainerRule {
name : String?
condition : Condition
body : Array[BlockItem]
span : @span.Span
} derive(Eq, Debug)
///|
/// A dotted layer name: `base.theme` is `["base", "theme"]`.
pub(all) struct LayerName {
parts : Array[String]
} derive(Eq, Debug)
///|
/// `body` is `None` for the statement form `@layer a, b;` and `Some` for the
/// block form. The two are different rules in CSS, not one rule with an
/// optional body, and the parser must know which it is to print it back.
pub(all) struct LayerRule {
names : Array[LayerName]
body : Array[BlockItem]?
span : @span.Span
} derive(Eq, Debug)
///|
pub(all) enum KeyframesName {
Ident(String)
Str(String)
} derive(Eq, Debug)
///|
pub(all) enum KeyframeSelector {
From
To
Percentage(Number)
} derive(Eq, Debug)
///|
pub(all) struct KeyframeBlock {
selectors : Array[KeyframeSelector]
decls : DeclBlock
span : @span.Span
} derive(Eq, Debug)
///|
pub(all) struct KeyframesRule {
name : KeyframesName
frames : Array[KeyframeBlock]
span : @span.Span
} derive(Eq, Debug)
///|
pub(all) struct PageRule {
selectors : Array[String]
body : Array[BlockItem]
span : @span.Span
} derive(Eq, Debug)
///|
pub(all) struct PropertyRule {
name : String
decls : DeclBlock
span : @span.Span
} derive(Eq, Debug)
///|
/// `layer` has three states and they are all different: absent, `layer`
/// (anonymous), and `layer(name)`. The nested option is what says so in the
/// type rather than in a comment.
pub(all) struct ImportRule {
url : String
layer : LayerName??
supports : Condition?
media : Array[MediaQuery]
span : @span.Span
} derive(Eq, Debug)
///|
pub(all) struct NamespaceRule {
prefix : String?
url : String
span : @span.Span
} derive(Eq, Debug)
///|
pub(all) struct ScopeRule {
start : Array[Selector]?
end : Array[Selector]?
body : Array[BlockItem]
span : @span.Span
} derive(Eq, Debug)
///|
pub(all) struct UnknownAtRule {
name : String
prelude : Array[ComponentValue]
block : Array[BlockItem]?
span : @span.Span
} derive(Eq, Debug)