Skip to main content

hjkl_css/
ast.rs

1//! Parsed stylesheet representation. Each rule pairs one [`Selector`]
2//! (a chain of [`SimpleSelector`]s joined by [`Combinator`]s) with one
3//! declaration block.
4
5use crate::value::Value;
6
7#[derive(Debug, Clone, PartialEq, Default)]
8pub struct Stylesheet {
9    pub rules: Vec<Rule>,
10}
11
12#[derive(Debug, Clone, PartialEq)]
13pub struct Rule {
14    pub selectors: Vec<Selector>,
15    pub declarations: Vec<Declaration>,
16}
17
18/// A compound selector — one or more [`SimpleSelector`]s joined by
19/// [`Combinator`]s. `parts.len() == combinators.len() + 1`.
20/// `parts[0]` is the leftmost (ancestor/sibling) end; `parts.last()`
21/// is the subject that is matched against the target node.
22///
23/// For a simple flat selector (no combinator) `parts` has one entry and
24/// `combinators` is empty.
25#[derive(Debug, Clone, PartialEq)]
26pub struct Selector {
27    pub parts: Vec<SimpleSelector>,
28    pub combinators: Vec<Combinator>,
29}
30
31/// One simple (non-compound) selector. AND-combined: `button.primary:hover`
32/// fills `element=Some("button")`, `classes=["primary"]`, `pseudo=Hover`.
33#[derive(Debug, Clone, PartialEq, Default)]
34pub struct SimpleSelector {
35    pub element: Option<String>,
36    pub classes: Vec<String>,
37    pub pseudo: Option<PseudoClass>,
38}
39
40/// Relationship between two adjacent [`SimpleSelector`]s in a
41/// [`Selector`] chain.
42#[derive(Debug, Clone, Copy, PartialEq, Eq)]
43pub enum Combinator {
44    /// `.a .b` — `.b` is a descendant (any depth) of `.a`.
45    Descendant,
46    /// `.a > .b` — `.b` is a direct child of `.a`.
47    Child,
48    /// `.a + .b` — `.b` immediately follows `.a` as a sibling.
49    AdjacentSibling,
50    /// `.a ~ .b` — `.b` follows `.a` as any sibling.
51    GeneralSibling,
52}
53
54#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
55pub enum PseudoClass {
56    Hover,
57    Focus,
58    Active,
59    Disabled,
60    Selected,
61}
62
63#[derive(Debug, Clone, PartialEq)]
64pub struct Declaration {
65    pub property: String,
66    pub value: Value,
67    /// Set when the source had `!important`. The cascade in
68    /// [`crate::Stylesheet::resolve`] honours this — important
69    /// declarations beat non-important ones regardless of specificity,
70    /// with source order breaking ties within either tier.
71    pub important: bool,
72}
73
74/// One node in the view tree as far as CSS matching cares.
75#[derive(Debug, Clone, Copy, PartialEq)]
76pub struct Node<'a> {
77    pub element: &'a str,
78    pub classes: &'a [&'a str],
79}
80
81impl Selector {
82    /// CSS specificity: sum of each part's specificity. Combinators
83    /// contribute 0.
84    pub fn specificity(&self) -> u32 {
85        self.parts.iter().map(SimpleSelector::specificity).sum()
86    }
87
88    /// Match against `target` given its `ancestors` (root → parent,
89    /// exclusive of the target) and `prev_siblings` (oldest → the
90    /// immediately preceding sibling, exclusive of the target).
91    /// `state` is the pseudo-class active on the target; ancestors are
92    /// always matched without pseudo-class.
93    ///
94    /// # Sibling combinator limitation (v1)
95    /// `AdjacentSibling` and `GeneralSibling` match the sibling against
96    /// `prev_siblings`. If the rule continues leftward past the sibling
97    /// combinator into a *Descendant* or *Child* step (e.g.
98    /// `.grandparent > .prev + .target`), the next combinator is evaluated
99    /// against the target's own `ancestors` rather than the sibling's
100    /// ancestors. This may false-negative when the continuation requires
101    /// introspecting the sibling's subtree context. Fully recursive
102    /// sibling-vs-ancestor context requires the adapter to supply
103    /// sibling-of-sibling data, which is out of scope for v1. Chained
104    /// sibling combinators (`.a + .b + .c`) walk the prev-sibling list
105    /// correctly and do not hit this limitation.
106    pub fn matches(
107        &self,
108        target: &Node<'_>,
109        ancestors: &[Node<'_>],
110        prev_siblings: &[Node<'_>],
111        state: Option<PseudoClass>,
112    ) -> bool {
113        let n = self.parts.len();
114        if n == 0 {
115            return false;
116        }
117        // Subject is the rightmost part.
118        if !self.parts[n - 1].matches_node(target, state) {
119            return false;
120        }
121        if n == 1 {
122            return true;
123        }
124        // Walk left through the remaining parts. Each Child/Descendant step
125        // shrinks `remaining_ancestors`; each AdjacentSibling/GeneralSibling
126        // step shrinks `remaining_siblings`.
127        let mut remaining_ancestors: &[Node<'_>] = ancestors;
128        let mut remaining_siblings: &[Node<'_>] = prev_siblings;
129        for i in (0..n - 1).rev() {
130            let part = &self.parts[i];
131            // `parts.len() == combinators.len() + 1` is upheld by the parser,
132            // but `parts`/`combinators` are public, so a hand-built selector
133            // could violate it. Bail out rather than panic on a bad index.
134            let Some(&combinator) = self.combinators.get(i) else {
135                return false;
136            };
137            match combinator {
138                Combinator::Descendant => {
139                    let pos = remaining_ancestors
140                        .iter()
141                        .rposition(|a| part.matches_node(a, None));
142                    match pos {
143                        Some(idx) => {
144                            remaining_ancestors = &remaining_ancestors[..idx];
145                        }
146                        None => return false,
147                    }
148                }
149                Combinator::Child => match remaining_ancestors.last() {
150                    Some(parent) if part.matches_node(parent, None) => {
151                        remaining_ancestors = &remaining_ancestors[..remaining_ancestors.len() - 1];
152                    }
153                    _ => return false,
154                },
155                Combinator::AdjacentSibling => match remaining_siblings.last() {
156                    Some(sib) if part.matches_node(sib, None) => {
157                        // Consume the matched sibling so a chain of
158                        // `+` combinators walks leftward through the
159                        // prev-sibling list (`.a + .b + .c`).
160                        remaining_siblings = &remaining_siblings[..remaining_siblings.len() - 1];
161                    }
162                    _ => return false,
163                },
164                Combinator::GeneralSibling => {
165                    let pos = remaining_siblings
166                        .iter()
167                        .rposition(|s| part.matches_node(s, None));
168                    match pos {
169                        Some(idx) => {
170                            remaining_siblings = &remaining_siblings[..idx];
171                        }
172                        None => return false,
173                    }
174                }
175            }
176        }
177        true
178    }
179}
180
181impl SimpleSelector {
182    /// CSS specificity for one simple selector: classes/pseudo each count
183    /// 10, type selector counts 1, no IDs in v1.
184    pub fn specificity(&self) -> u32 {
185        let classes = (self.classes.len() as u32) * 10;
186        let pseudo = u32::from(self.pseudo.is_some()) * 10;
187        let element = u32::from(self.element.is_some());
188        classes + pseudo + element
189    }
190
191    /// Does this simple selector match a node in the given state?
192    pub fn matches_node(&self, node: &Node<'_>, state: Option<PseudoClass>) -> bool {
193        if let Some(want) = &self.element
194            && want.as_str() != node.element
195        {
196            return false;
197        }
198        if !self
199            .classes
200            .iter()
201            .all(|c| node.classes.contains(&c.as_str()))
202        {
203            return false;
204        }
205        match (self.pseudo, state) {
206            (None, _) => true,
207            (Some(want), Some(have)) => want == have,
208            (Some(_), None) => false,
209        }
210    }
211}
212
213impl PseudoClass {
214    /// CSS pseudo-class names are ASCII case-insensitive — `:HOVER`,
215    /// `:Hover` and `:hover` are all equivalent.
216    pub fn from_ident(ident: &str) -> Option<Self> {
217        Some(match ident.to_ascii_lowercase().as_str() {
218            "hover" => Self::Hover,
219            "focus" => Self::Focus,
220            "active" => Self::Active,
221            "disabled" => Self::Disabled,
222            "selected" => Self::Selected,
223            _ => return None,
224        })
225    }
226
227    pub fn as_str(&self) -> &'static str {
228        match self {
229            Self::Hover => "hover",
230            Self::Focus => "focus",
231            Self::Active => "active",
232            Self::Disabled => "disabled",
233            Self::Selected => "selected",
234        }
235    }
236}