Skip to main content

hjkl_css/
resolve.rs

1//! Stylesheet → declaration list for a given (target, ancestors, state).
2//!
3//! Walks every rule in source order, keeps the best per-property match
4//! ranked by `(important, specificity, rule_idx, decl_idx)`. `!important`
5//! wins over any non-important declaration regardless of specificity;
6//! within either group, higher specificity wins, with rule order (then
7//! intra-rule declaration order) breaking ties.
8
9use std::collections::HashMap;
10
11use crate::ast::{Node, PseudoClass, Rule, Stylesheet};
12use crate::value::Value;
13
14/// Resolved style for one node — property -> value, with cascade already
15/// applied. Adapter crates (e.g. hjkl-css-floem) convert this to their
16/// own builder type.
17///
18/// Internally each property stores the `(rule_idx, decl_idx)` of the
19/// winning declaration so that [`iter`](ResolvedStyle::iter) can replay
20/// properties in CSS source order, which is the correct semantic for
21/// adapters that apply shorthand/longhand overrides.
22#[derive(Debug, Clone, Default, PartialEq)]
23pub struct ResolvedStyle {
24    /// Maps property name → (winning `(rule_idx, decl_idx)`, value).
25    /// `decl_idx` is the declaration's position inside its rule, capturing
26    /// intra-rule source order for shorthand/longhand replay.
27    pub(crate) properties: HashMap<String, ((usize, usize), Value)>,
28}
29
30impl ResolvedStyle {
31    pub fn get(&self, property: &str) -> Option<&Value> {
32        self.properties.get(property).map(|(_, v)| v)
33    }
34
35    pub fn len(&self) -> usize {
36        self.properties.len()
37    }
38
39    pub fn is_empty(&self) -> bool {
40        self.properties.is_empty()
41    }
42
43    /// Iterate declarations in **CSS source order**: ascending `rule_idx`
44    /// of the winning declaration, with `decl_idx` (position inside the
45    /// rule) breaking ties. The result matches the order in which the
46    /// declarations appeared in the stylesheet so that adapters applying
47    /// properties sequentially produce CSS-spec behaviour for
48    /// shorthand/longhand collisions.
49    ///
50    /// Example: `x { border-color: blue; border: 1px solid red; }` yields
51    /// `("border-color", blue)` then `("border", red)`. An adapter that
52    /// applies `border-color` first then `border` ends up with red, which
53    /// is the CSS-correct winner.
54    ///
55    /// If you need alphabetical order for snapshots or serialization,
56    /// collect the iterator and sort the resulting `Vec` by key.
57    ///
58    /// **Performance:** allocates a `Vec` and sorts it on every call. For
59    /// one-shot adapter conversion this cost is negligible.
60    pub fn iter(&self) -> impl Iterator<Item = (&str, &Value)> {
61        type Entry<'a> = (&'a String, &'a ((usize, usize), Value));
62        let mut entries: Vec<Entry<'_>> = self.properties.iter().collect();
63        // Primary: ascending (rule_idx, decl_idx) — CSS source order.
64        // Secondary: alphabetical key. The secondary tie-break is
65        // unreachable today because `decl_idx` is the enumerate index of
66        // a declaration inside its rule, so two declarations cannot share
67        // the same (rule_idx, decl_idx). It is kept anyway to lock in a
68        // deterministic order in case the cascade ever stores synthetic
69        // entries that bypass the enumerate invariant.
70        entries.sort_by(|(ka, (pa, _)), (kb, (pb, _))| pa.cmp(pb).then_with(|| ka.cmp(kb)));
71        entries.into_iter().map(|(k, (_, v))| (k.as_str(), v))
72    }
73}
74
75/// Per-property cascade key. Ordering: higher tuple wins.
76/// `(important, specificity, rule_idx, decl_idx)`.
77type CascadeKey = (bool, u32, usize, usize);
78
79impl Stylesheet {
80    /// Resolve every property that targets `target` given its `ancestors`
81    /// (root → parent, exclusive of `target`) and `prev_siblings` (oldest
82    /// → immediately preceding sibling, exclusive of `target`). `state` is
83    /// the pseudo-class active on `target`.
84    pub fn resolve(
85        &self,
86        target: &Node<'_>,
87        ancestors: &[Node<'_>],
88        prev_siblings: &[Node<'_>],
89        state: Option<PseudoClass>,
90    ) -> ResolvedStyle {
91        let mut best: HashMap<String, (CascadeKey, Value)> = HashMap::new();
92        for (rule_idx, rule) in self.rules.iter().enumerate() {
93            let Some(spec) =
94                best_matching_specificity(rule, target, ancestors, prev_siblings, state)
95            else {
96                continue;
97            };
98            for (decl_idx, decl) in rule.declarations.iter().enumerate() {
99                let key: CascadeKey = (decl.important, spec, rule_idx, decl_idx);
100                let replace = match best.get(&decl.property) {
101                    Some((existing, _)) => existing <= &key,
102                    None => true,
103                };
104                if replace {
105                    best.insert(decl.property.clone(), (key, decl.value.clone()));
106                }
107            }
108        }
109        ResolvedStyle {
110            // Store (rule_idx, decl_idx, value) so iter() can replay in
111            // full CSS source order, including intra-rule order.
112            properties: best
113                .into_iter()
114                .map(|(k, ((_, _, rule_idx, decl_idx), v))| (k, ((rule_idx, decl_idx), v)))
115                .collect(),
116        }
117    }
118}
119
120fn best_matching_specificity(
121    rule: &Rule,
122    target: &Node<'_>,
123    ancestors: &[Node<'_>],
124    prev_siblings: &[Node<'_>],
125    state: Option<PseudoClass>,
126) -> Option<u32> {
127    rule.selectors
128        .iter()
129        .filter(|s| s.matches(target, ancestors, prev_siblings, state))
130        .map(|s| s.specificity())
131        .max()
132}