///|
/// CSS Selector types based on Selectors Level 4
///|
/// Attribute matching mode
pub enum AttributeMatch {
/// [attr] - has attribute
Exists
/// [attr=value] - exact match
Exact(String)
/// [attr~=value] - whitespace-separated list contains value
Includes(String)
/// [attr|=value] - equals or starts with value followed by hyphen
DashMatch(String)
/// [attr^=value] - starts with
Prefix(String)
/// [attr$=value] - ends with
Suffix(String)
/// [attr*=value] - contains
Substring(String)
}
///|
pub impl Show for AttributeMatch with fn output(self, logger) {
match self {
Exists => logger.write_string("Exists")
Exact(v) => {
logger.write_string("Exact(")
logger.write_string(v)
logger.write_string(")")
}
Includes(v) => {
logger.write_string("Includes(")
logger.write_string(v)
logger.write_string(")")
}
DashMatch(v) => {
logger.write_string("DashMatch(")
logger.write_string(v)
logger.write_string(")")
}
Prefix(v) => {
logger.write_string("Prefix(")
logger.write_string(v)
logger.write_string(")")
}
Suffix(v) => {
logger.write_string("Suffix(")
logger.write_string(v)
logger.write_string(")")
}
Substring(v) => {
logger.write_string("Substring(")
logger.write_string(v)
logger.write_string(")")
}
}
}
///|
/// Attribute selector
pub struct AttributeSelector {
name : String
match_type : AttributeMatch
/// Case-insensitive flag [attr=value i]
case_insensitive : Bool
/// True when written with `*|` (any-namespace) prefix, e.g.
/// `[*|att]`. The implicit (`[att]`) and `[|att]` (explicit
/// no-namespace) forms both leave this false — they share the same
/// canonical serialization.
any_namespace : Bool
}
///|
pub impl Show for AttributeSelector with fn output(self, logger) {
logger.write_string("[")
if self.any_namespace {
logger.write_string("*|")
}
logger.write_string(self.name)
match self.match_type {
Exists => ()
_ => {
logger.write_string(" ")
self.match_type.output(logger)
}
}
if self.case_insensitive {
logger.write_string(" i")
}
logger.write_string("]")
}
///|
/// Nth expression: An+B
pub struct NthExpr {
a : Int // coefficient
b : Int // offset
}
///|
pub impl Show for NthExpr with fn output(self, logger) {
if self.a == 0 {
logger.write_string(self.b.to_string())
} else if self.a == 1 {
logger.write_string("n")
if self.b > 0 {
logger.write_string("+")
logger.write_string(self.b.to_string())
} else if self.b < 0 {
logger.write_string(self.b.to_string())
}
} else if self.a == -1 {
logger.write_string("-n")
if self.b > 0 {
logger.write_string("+")
logger.write_string(self.b.to_string())
} else if self.b < 0 {
logger.write_string(self.b.to_string())
}
} else {
logger.write_string(self.a.to_string())
logger.write_string("n")
if self.b > 0 {
logger.write_string("+")
logger.write_string(self.b.to_string())
} else if self.b < 0 {
logger.write_string(self.b.to_string())
}
}
}
///|
/// Pseudo-class selector
pub enum PseudoClass {
// Tree-structural
FirstChild
LastChild
OnlyChild
NthChild(NthExpr)
NthLastChild(NthExpr)
FirstOfType
LastOfType
OnlyOfType
NthOfType(NthExpr)
NthLastOfType(NthExpr)
Root
Empty
// Negation
// CSS Selectors 4: `` argument.
Not(Array[ComplexSelector])
// Matches-any
Is(Array[ComplexSelector])
Where(Array[ComplexSelector])
Has(Array[RelativeSelector])
State(String)
/// `:host` — Shadow DOM host element with no further constraints.
/// Spec: https://drafts.csswg.org/css-scoping-1/#host-selector
Host
/// `:host()` — Shadow DOM host matching a selector.
HostFunc(CompoundSelector)
/// `:host-context()` — Host that has a matching ancestor.
HostContextFunc(CompoundSelector)
/// `:heading` — any HTML heading element (h1-h6).
/// `:heading(N, ...)` — headings of the specified levels.
/// Spec: CSS Selectors 4 (draft).
Heading(Array[Int])
/// `:dir(ltr|rtl)` — directionality. Spec: CSS Selectors 4.
Dir(String)
/// `:lang(+)` — language-tag match. Spec: Selectors 4.
Lang(Array[String])
// User-action
Hover
Active
Focus
FocusVisible
FocusWithin
// Link
Link
Visited
AnyLink
// Input
Enabled
Disabled
Checked
Indeterminate
Required
Optional
Valid
Invalid
ReadOnly
ReadWrite
}
///|
pub impl Show for PseudoClass with fn output(self, logger) {
match self {
FirstChild => logger.write_string(":first-child")
LastChild => logger.write_string(":last-child")
OnlyChild => logger.write_string(":only-child")
NthChild(expr) => {
logger.write_string(":nth-child(")
expr.output(logger)
logger.write_string(")")
}
NthLastChild(expr) => {
logger.write_string(":nth-last-child(")
expr.output(logger)
logger.write_string(")")
}
FirstOfType => logger.write_string(":first-of-type")
LastOfType => logger.write_string(":last-of-type")
OnlyOfType => logger.write_string(":only-of-type")
NthOfType(expr) => {
logger.write_string(":nth-of-type(")
expr.output(logger)
logger.write_string(")")
}
NthLastOfType(expr) => {
logger.write_string(":nth-last-of-type(")
expr.output(logger)
logger.write_string(")")
}
Root => logger.write_string(":root")
Empty => logger.write_string(":empty")
Not(_) => logger.write_string(":not(...)")
Is(_) => logger.write_string(":is(...)")
Where(_) => logger.write_string(":where(...)")
Has(_) => logger.write_string(":has(...)")
State(state) => {
logger.write_string(":state(")
logger.write_string(state)
logger.write_string(")")
}
Host => logger.write_string(":host")
HostFunc(_) => logger.write_string(":host(...)")
HostContextFunc(_) => logger.write_string(":host-context(...)")
Heading(levels) =>
if levels.is_empty() {
logger.write_string(":heading")
} else {
logger.write_string(":heading(")
for i, n in levels {
if i > 0 {
logger.write_string(", ")
}
logger.write_string(n.to_string())
}
logger.write_string(")")
}
Dir(dir) => {
logger.write_string(":dir(")
logger.write_string(dir)
logger.write_string(")")
}
Lang(tags) => {
logger.write_string(":lang(")
for i, t in tags {
if i > 0 {
logger.write_string(", ")
}
logger.write_string(t)
}
logger.write_string(")")
}
Hover => logger.write_string(":hover")
Active => logger.write_string(":active")
Focus => logger.write_string(":focus")
FocusVisible => logger.write_string(":focus-visible")
FocusWithin => logger.write_string(":focus-within")
Link => logger.write_string(":link")
Visited => logger.write_string(":visited")
AnyLink => logger.write_string(":any-link")
Enabled => logger.write_string(":enabled")
Disabled => logger.write_string(":disabled")
Checked => logger.write_string(":checked")
Indeterminate => logger.write_string(":indeterminate")
Required => logger.write_string(":required")
Optional => logger.write_string(":optional")
Valid => logger.write_string(":valid")
Invalid => logger.write_string(":invalid")
ReadOnly => logger.write_string(":read-only")
ReadWrite => logger.write_string(":read-write")
}
}
///|
/// Pseudo-element selector
pub enum PseudoElement {
Before
After
FirstLine
FirstLetter
Marker
Placeholder
Selection
Backdrop
FileSelectorButton
PlaceholderShown
TargetText
SpellingError
GrammarError
Cue
/// `::part(+)` — Shadow DOM exposed-part targeting.
/// Spec: https://drafts.csswg.org/css-shadow-parts-1/#part
Part(Array[String])
/// `::slotted()` — Shadow DOM slot targeting.
/// Spec: https://drafts.csswg.org/css-scoping-1/#slotted-pseudo
Slotted(Array[CompoundSelector])
}
///|
pub impl Show for PseudoElement with fn output(self, logger) {
match self {
Before => logger.write_string("::before")
After => logger.write_string("::after")
FirstLine => logger.write_string("::first-line")
FirstLetter => logger.write_string("::first-letter")
Marker => logger.write_string("::marker")
Placeholder => logger.write_string("::placeholder")
Selection => logger.write_string("::selection")
Backdrop => logger.write_string("::backdrop")
FileSelectorButton => logger.write_string("::file-selector-button")
PlaceholderShown => logger.write_string("::placeholder-shown")
TargetText => logger.write_string("::target-text")
SpellingError => logger.write_string("::spelling-error")
GrammarError => logger.write_string("::grammar-error")
Cue => logger.write_string("::cue")
Part(names) => {
logger.write_string("::part(")
for i, name in names {
if i > 0 {
logger.write_string(" ")
}
logger.write_string(name)
}
logger.write_string(")")
}
Slotted(_) => logger.write_string("::slotted(...)")
}
}
///|
/// Simple selector - a single component
pub enum SimpleSelector {
/// Type selector: div, span
Type(String)
/// Universal selector: *
Universal
/// ID selector: #id
Id(String)
/// Class selector: .class
Class(String)
/// Attribute selector: [attr], [attr=value]
Attribute(AttributeSelector)
/// Pseudo-class: :hover, :first-child
PseudoClass(PseudoClass)
/// Pseudo-element: ::before, ::after
PseudoElement(PseudoElement)
}
///|
pub impl Show for SimpleSelector with fn output(self, logger) {
match self {
Type(name) => logger.write_string(name)
Universal => logger.write_string("*")
Id(id) => {
logger.write_string("#")
logger.write_string(id)
}
Class(cls) => {
logger.write_string(".")
logger.write_string(cls)
}
Attribute(attr) => attr.output(logger)
PseudoClass(pc) => pc.output(logger)
PseudoElement(pe) => pe.output(logger)
}
}
///|
/// Compound selector - a sequence of simple selectors without combinator
/// e.g., div.foo#bar:hover
pub struct CompoundSelector {
/// Type or universal selector (at most one, must be first if present)
type_selector : SimpleSelector?
/// Other simple selectors (id, class, attribute, pseudo)
subclasses : Array[SimpleSelector]
/// True when the type selector was written with the `*|` (any-
/// namespace) prefix as in `*|*` or `*|name`. CSS canonical form
/// drops the `*|*` universal when subclasses are present.
any_ns_universal : Bool
}
///|
pub impl Show for CompoundSelector with fn output(self, logger) {
match self.type_selector {
Some(Universal) =>
// Canonical form drops `*|*` when combined with other subclasses.
if self.any_ns_universal && self.subclasses.length() > 0 {
()
} else {
logger.write_string("*")
}
Some(sel) => sel.output(logger)
None => ()
}
for sub in self.subclasses {
sub.output(logger)
}
}
///|
/// Combinator between compound selectors
pub enum Combinator {
/// Descendant combinator (whitespace)
Descendant
/// Child combinator (>)
Child
/// Next-sibling combinator (+)
NextSibling
/// Subsequent-sibling combinator (~)
SubsequentSibling
}
///|
pub impl Show for Combinator with fn output(self, logger) {
match self {
Descendant => logger.write_string(" ")
Child => logger.write_string(" > ")
NextSibling => logger.write_string(" + ")
SubsequentSibling => logger.write_string(" ~ ")
}
}
///|
/// A step in a complex selector: combinator + compound selector
pub struct ComplexSelectorStep {
combinator : Combinator
selector : CompoundSelector
}
///|
/// Complex selector - compound selectors joined by combinators
/// e.g., div > p.intro + span
pub struct ComplexSelector {
/// The rightmost compound selector (the subject by default)
head : CompoundSelector
/// Chain of combinator + compound pairs going left
/// e.g., for "div > p.intro", head = "p.intro", tail = [(Child, "div")]
tail : Array[ComplexSelectorStep]
}
///|
pub impl Show for ComplexSelector with fn output(self, logger) {
// Print from left to right (tail is reversed order)
let parts : Array[String] = []
for i = self.tail.length() - 1; i >= 0; i = i - 1 {
let step = self.tail[i]
let buf = StringBuilder::new()
step.selector.output(buf)
parts.push(buf.to_string())
match step.combinator {
Descendant => parts.push(" ")
Child => parts.push(" > ")
NextSibling => parts.push(" + ")
SubsequentSibling => parts.push(" ~ ")
}
}
for part in parts {
logger.write_string(part)
}
self.head.output(logger)
}
///|
/// Relative selector for :has()
pub struct RelativeSelector {
combinator : Combinator?
selector : ComplexSelector
}
///|
/// Selector list - comma-separated selectors
pub struct SelectorList {
selectors : Array[ComplexSelector]
}
///|
pub impl Show for SelectorList with fn output(self, logger) {
for i, sel in self.selectors {
if i > 0 {
logger.write_string(", ")
}
sel.output(logger)
}
}
///|
/// Specificity as (a, b, c) tuple
/// a: ID selectors count
/// b: class selectors, attribute selectors, pseudo-classes count
/// c: type selectors, pseudo-elements count
pub(all) struct Specificity {
a : Int
b : Int
c : Int
} derive(Eq, Compare)
///|
pub impl Show for Specificity with fn output(self, logger) {
logger.write_string("(")
logger.write_string(self.a.to_string())
logger.write_string(",")
logger.write_string(self.b.to_string())
logger.write_string(",")
logger.write_string(self.c.to_string())
logger.write_string(")")
}
///|
pub fn Specificity::zero() -> Specificity {
{ a: 0, b: 0, c: 0 }
}
///|
pub fn Specificity::add(self : Specificity, other : Specificity) -> Specificity {
{ a: self.a + other.a, b: self.b + other.b, c: self.c + other.c }
}
///|
/// Compare specificities for cascade ordering
/// Returns positive if self > other, negative if self < other, 0 if equal
pub fn Specificity::compare_to(self : Specificity, other : Specificity) -> Int {
if self.a != other.a {
return self.a - other.a
}
if self.b != other.b {
return self.b - other.b
}
self.c - other.c
}
///|
/// Create a simple compound selector with just a type selector
pub fn CompoundSelector::from_type(tag_name : String) -> CompoundSelector {
{
type_selector: Some(Type(tag_name)),
subclasses: [],
any_ns_universal: false,
}
}
///|
/// Create a simple compound selector with just a class selector
pub fn CompoundSelector::from_class(class_name : String) -> CompoundSelector {
{
type_selector: None,
subclasses: [Class(class_name)],
any_ns_universal: false,
}
}
///|
/// Create a simple compound selector with just an ID selector
pub fn CompoundSelector::from_id(id : String) -> CompoundSelector {
{ type_selector: None, subclasses: [Id(id)], any_ns_universal: false }
}
///|
/// Create a simple compound selector with universal selector
pub fn CompoundSelector::universal() -> CompoundSelector {
{ type_selector: Some(Universal), subclasses: [], any_ns_universal: false }
}
///|
/// Create a complex selector from a single compound
pub fn ComplexSelector::simple(compound : CompoundSelector) -> ComplexSelector {
{ head: compound, tail: [] }
}