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}