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            let combinator = self.combinators[i];
132            match combinator {
133                Combinator::Descendant => {
134                    let pos = remaining_ancestors
135                        .iter()
136                        .rposition(|a| part.matches_node(a, None));
137                    match pos {
138                        Some(idx) => {
139                            remaining_ancestors = &remaining_ancestors[..idx];
140                        }
141                        None => return false,
142                    }
143                }
144                Combinator::Child => match remaining_ancestors.last() {
145                    Some(parent) if part.matches_node(parent, None) => {
146                        remaining_ancestors = &remaining_ancestors[..remaining_ancestors.len() - 1];
147                    }
148                    _ => return false,
149                },
150                Combinator::AdjacentSibling => match remaining_siblings.last() {
151                    Some(sib) if part.matches_node(sib, None) => {
152                        // Consume the matched sibling so a chain of
153                        // `+` combinators walks leftward through the
154                        // prev-sibling list (`.a + .b + .c`).
155                        remaining_siblings = &remaining_siblings[..remaining_siblings.len() - 1];
156                    }
157                    _ => return false,
158                },
159                Combinator::GeneralSibling => {
160                    let pos = remaining_siblings
161                        .iter()
162                        .rposition(|s| part.matches_node(s, None));
163                    match pos {
164                        Some(idx) => {
165                            remaining_siblings = &remaining_siblings[..idx];
166                        }
167                        None => return false,
168                    }
169                }
170            }
171        }
172        true
173    }
174}
175
176impl SimpleSelector {
177    /// CSS specificity for one simple selector: classes/pseudo each count
178    /// 10, type selector counts 1, no IDs in v1.
179    pub fn specificity(&self) -> u32 {
180        let classes = (self.classes.len() as u32) * 10;
181        let pseudo = u32::from(self.pseudo.is_some()) * 10;
182        let element = u32::from(self.element.is_some());
183        classes + pseudo + element
184    }
185
186    /// Does this simple selector match a node in the given state?
187    pub fn matches_node(&self, node: &Node<'_>, state: Option<PseudoClass>) -> bool {
188        if let Some(want) = &self.element
189            && want.as_str() != node.element
190        {
191            return false;
192        }
193        if !self
194            .classes
195            .iter()
196            .all(|c| node.classes.contains(&c.as_str()))
197        {
198            return false;
199        }
200        match (self.pseudo, state) {
201            (None, _) => true,
202            (Some(want), Some(have)) => want == have,
203            (Some(_), None) => false,
204        }
205    }
206}
207
208impl PseudoClass {
209    /// CSS pseudo-class names are ASCII case-insensitive — `:HOVER`,
210    /// `:Hover` and `:hover` are all equivalent.
211    pub fn from_ident(ident: &str) -> Option<Self> {
212        Some(match ident.to_ascii_lowercase().as_str() {
213            "hover" => Self::Hover,
214            "focus" => Self::Focus,
215            "active" => Self::Active,
216            "disabled" => Self::Disabled,
217            "selected" => Self::Selected,
218            _ => return None,
219        })
220    }
221
222    pub fn as_str(&self) -> &'static str {
223        match self {
224            Self::Hover => "hover",
225            Self::Focus => "focus",
226            Self::Active => "active",
227            Self::Disabled => "disabled",
228            Self::Selected => "selected",
229        }
230    }
231}