1use std::collections::HashMap;
6
7use super::builder::{element_info, element_sibling_infos};
8use super::css_resolver::{ResolvedDeclaration, RuleSet, StyleOrigin, resolve_inline_style};
9use super::dom_snapshot::{DomSnapshot, NodeId};
10use crate::engine::css::matcher::ElementChain;
11use crate::engine::css::values::CssValue;
12
13#[derive(Debug, Clone)]
15pub struct InspectedDeclaration {
16 pub name: String,
17 pub value: CssValue,
18 pub important: bool,
19 pub applied: bool,
21}
22
23#[derive(Debug, Clone)]
25pub struct MatchedRule {
26 pub selector: String,
28 pub origin: StyleOrigin,
29 pub inline: bool,
31 pub declarations: Vec<InspectedDeclaration>,
32}
33
34#[derive(Clone, PartialEq, Eq, PartialOrd, Ord)]
42struct CascadeKey {
43 important: bool,
44 origin: StyleOrigin,
45 inline: bool,
46 specificity: (u32, u32, u32),
47 order: usize,
48}
49
50const INLINE_ORDER_BASE: usize = usize::MAX / 2;
51
52impl CascadeKey {
53 fn from_declaration(declaration: &ResolvedDeclaration) -> Self {
54 Self {
55 important: declaration.important,
56 origin: declaration.origin,
57 inline: false,
58 specificity: declaration.specificity,
59 order: declaration.order,
60 }
61 }
62}
63
64struct Pending {
65 name: String,
66 key: CascadeKey,
67 rule: usize,
68 declaration: usize,
69}
70
71pub fn collect_matched_rules(
77 snapshot: &DomSnapshot,
78 target: NodeId,
79 rule_set: &RuleSet,
80 inline_style_attr: Option<&str>,
81) -> Vec<MatchedRule> {
82 let Some(chain) = chain_for_node(snapshot, target) else {
83 return Vec::new();
84 };
85 let Some(element) = chain.first() else {
86 return Vec::new();
87 };
88
89 let mut rules: Vec<MatchedRule> = Vec::new();
91 let mut pendings: Vec<Pending> = Vec::new();
92
93 if let Some(style_attr) = inline_style_attr {
94 let inline_declarations = resolve_inline_style(style_attr);
95 if !inline_declarations.is_empty() {
96 let mut declarations = Vec::with_capacity(inline_declarations.len());
97 for (offset, (name, value, important)) in inline_declarations.into_iter().enumerate() {
98 pendings.push(Pending {
99 name: name.clone(),
100 key: CascadeKey {
101 important,
102 origin: StyleOrigin::Author,
103 inline: true,
104 specificity: (u32::MAX, 0, 0),
105 order: INLINE_ORDER_BASE + offset,
106 },
107 rule: 0,
108 declaration: declarations.len(),
109 });
110 declarations.push(InspectedDeclaration {
111 name,
112 value,
113 important,
114 applied: false,
115 });
116 }
117 rules.push(MatchedRule {
118 selector: "element.style".to_string(),
119 origin: StyleOrigin::Author,
120 inline: true,
121 declarations,
122 });
123 }
124 }
125
126 let mut rule_index: HashMap<(String, StyleOrigin), usize> = HashMap::new();
127 for group in rule_set.query_candidates(element) {
128 if !group.selector.matches(&chain) {
129 continue;
130 }
131 for &decl_idx in &group.decls {
132 let declaration = &rule_set.declarations()[decl_idx];
133 let selector = declaration.selector.to_string();
134 let key = (selector, declaration.origin);
135 let rule = *rule_index.entry(key).or_insert_with(|| {
136 rules.push(MatchedRule {
137 selector: declaration.selector.to_string(),
138 origin: declaration.origin,
139 inline: false,
140 declarations: Vec::new(),
141 });
142 rules.len() - 1
143 });
144 pendings.push(Pending {
145 name: declaration.name.clone(),
146 key: CascadeKey::from_declaration(declaration),
147 rule,
148 declaration: rules[rule].declarations.len(),
149 });
150 rules[rule].declarations.push(InspectedDeclaration {
151 name: declaration.name.clone(),
152 value: declaration.value.clone(),
153 important: declaration.important,
154 applied: false,
155 });
156 }
157 }
158
159 mark_cascade_winners(&mut rules, &pendings);
160 rules
161}
162
163fn mark_cascade_winners(rules: &mut [MatchedRule], pendings: &[Pending]) {
165 let mut winners: HashMap<&str, (CascadeKey, usize)> = HashMap::new();
166 for (index, pending) in pendings.iter().enumerate() {
167 match winners.get(pending.name.as_str()) {
168 Some((existing_key, _)) if *existing_key >= pending.key => {}
169 _ => {
170 winners.insert(&pending.name, (pending.key.clone(), index));
171 }
172 }
173 }
174 for (_, (_, index)) in winners {
175 let pending = &pendings[index];
176 rules[pending.rule].declarations[pending.declaration].applied = true;
177 }
178}
179
180fn chain_for_node(snapshot: &DomSnapshot, target: NodeId) -> Option<ElementChain> {
184 let root = *snapshot.roots().first()?;
185 let mut path = Vec::new();
186 find_path(snapshot, root, target, &mut path)?;
187
188 let mut elements = Vec::with_capacity(path.len());
189 for (depth, &node_id) in path.iter().enumerate() {
190 let info = if depth == 0 {
191 element_info(&snapshot.node(node_id).kind)
192 } else {
193 let siblings = snapshot.children(path[depth - 1]);
194 let position = siblings.iter().position(|&sibling| sibling == node_id)?;
195 element_sibling_infos(snapshot, siblings)[position].clone()
196 };
197 elements.push(info);
198 }
199
200 elements.reverse();
203 Some(ElementChain::from_vec(
204 elements.into_iter().flatten().collect(),
205 ))
206}
207
208fn find_path(
209 snapshot: &DomSnapshot,
210 current: NodeId,
211 target: NodeId,
212 path: &mut Vec<NodeId>,
213) -> Option<()> {
214 path.push(current);
215 if current == target {
216 return Some(());
217 }
218 for &child in snapshot.children(current) {
219 if find_path(snapshot, child, target, path).is_some() {
220 return Some(());
221 }
222 }
223 path.pop();
224 None
225}
226
227#[cfg(test)]
228mod tests {
229 use super::*;
230 use crate::engine::css::parser::Parser as CssParser;
231 use crate::engine::html::parser::Parser as HtmlParser;
232 use crate::engine::layouter::css_resolver::{CssResolver, MediaEnvironment, ResolvedStyles};
233 use crate::engine::layouter::types::ColorScheme;
234
235 fn leak(resolved: ResolvedStyles) -> &'static ResolvedStyles {
238 Box::leak(Box::new(resolved))
239 }
240
241 fn resolved_for(css: &str) -> ResolvedStyles {
242 CssResolver::resolve(&CssParser::new(css).parse().unwrap())
243 }
244
245 fn resolved_for_origin(css: &str, origin: StyleOrigin) -> ResolvedStyles {
246 CssResolver::resolve_with_origin(&CssParser::new(css).parse().unwrap(), origin)
247 }
248
249 fn media() -> MediaEnvironment {
250 MediaEnvironment::new((800.0, 600.0), ColorScheme::Light)
251 }
252
253 fn snapshot_for(html: &str) -> DomSnapshot {
254 let dom = HtmlParser::new(html).parse();
255 DomSnapshot::from_tree(&dom.root).0
256 }
257
258 fn node_with_tag(snapshot: &DomSnapshot, tag: &str) -> NodeId {
259 nodes_with_tag(snapshot, tag)
260 .into_iter()
261 .next()
262 .unwrap_or_else(|| panic!("no <{tag}> in fixture"))
263 }
264
265 fn nodes_with_tag(snapshot: &DomSnapshot, tag: &str) -> Vec<NodeId> {
268 snapshot
269 .nodes()
270 .iter()
271 .enumerate()
272 .filter(|(_, node)| node.kind.tag_name() == Some(tag))
273 .map(|(index, _)| index as NodeId)
274 .collect()
275 }
276
277 fn declaration<'a>(rule: &'a MatchedRule, name: &str) -> &'a InspectedDeclaration {
278 rule.declarations
279 .iter()
280 .find(|declaration| declaration.name == name)
281 .unwrap_or_else(|| panic!("no {name} declared by {}", rule.selector))
282 }
283
284 #[test]
285 fn later_specific_rule_overrides_and_flags_the_loser() {
286 let snapshot = snapshot_for("<body><p class=\"box\">t</p></body>");
287 let styles = leak(resolved_for("p { color: red; } .box { color: blue; }"));
288 let rules = RuleSet::from_declarations(styles, &media());
289 let target = node_with_tag(&snapshot, "p");
290
291 let matched = collect_matched_rules(&snapshot, target, &rules, None);
292
293 let tag_rule = matched.iter().find(|rule| rule.selector == "p").unwrap();
294 let class_rule = matched.iter().find(|rule| rule.selector == ".box").unwrap();
295 assert!(!declaration(tag_rule, "color").applied);
296 assert!(declaration(class_rule, "color").applied);
297 }
298
299 #[test]
300 fn chain_reconstruction_carries_sibling_indexes() {
301 let snapshot = snapshot_for("<ul><li>a</li><li>b</li><span>s</span></ul>");
302
303 fn indexes(snapshot: &DomSnapshot, id: NodeId) -> (usize, usize) {
304 let chain = chain_for_node(snapshot, id).expect("chain");
305 let element = chain.first().unwrap();
306 (element.element_index, element.element_count)
307 }
308
309 let lis = nodes_with_tag(&snapshot, "li");
310 assert_eq!(lis.len(), 2);
311 assert_eq!(indexes(&snapshot, lis[0]), (1, 3));
312 assert_eq!(indexes(&snapshot, lis[1]), (2, 3));
313
314 let chain = chain_for_node(&snapshot, lis[1]).unwrap();
316 assert_eq!(chain.first().unwrap().tag_name, "li");
317 }
318
319 #[test]
320 fn structural_pseudo_classes_use_reconstructed_sibling_indexes() {
321 let snapshot = snapshot_for("<ul><li>a</li><li>b</li></ul>");
322 let styles = leak(resolved_for("li:nth-child(2) { color: green; }"));
323 let rules = RuleSet::from_declarations(styles, &media());
324 let lis = nodes_with_tag(&snapshot, "li");
325
326 let matched = collect_matched_rules(&snapshot, lis[1], &rules, None);
327 assert!(
328 matched.iter().any(
329 |rule| rule.selector == "li:nth-child(2)" && declaration(rule, "color").applied
330 ),
331 "second li must match :nth-child(2)"
332 );
333
334 let matched = collect_matched_rules(&snapshot, lis[0], &rules, None);
335 assert!(matched.is_empty(), "first li must not match :nth-child(2)");
336 }
337
338 #[test]
339 fn descendant_combinator_selectors_match_through_ancestors() {
340 let snapshot = snapshot_for("<div class=\"outer\"><section><p>t</p></section></div>");
341 let styles = leak(resolved_for("div.outer p { color: teal; }"));
342 let rules = RuleSet::from_declarations(styles, &media());
343
344 let matched = collect_matched_rules(&snapshot, node_with_tag(&snapshot, "p"), &rules, None);
345 assert_eq!(matched[0].selector, "div.outer p");
346 assert!(declaration(&matched[0], "color").applied);
347 }
348
349 #[test]
350 fn inline_styles_beat_author_rules_but_not_important_ones() {
351 let snapshot = snapshot_for("<body><p style=\"color: black\">t</p></body>");
352 let target = node_with_tag(&snapshot, "p");
353
354 let styles = leak(resolved_for("p { color: red; }"));
355 let rules = RuleSet::from_declarations(styles, &media());
356 let matched = collect_matched_rules(&snapshot, target, &rules, Some("color: black"));
357 let inline = matched.first().expect("inline entry leads");
358 assert!(inline.inline && inline.origin == StyleOrigin::Author);
359 assert_eq!(inline.selector, "element.style");
360 assert!(declaration(inline, "color").applied);
361
362 let styles = leak(resolved_for("p { color: red !important; }"));
363 let rules = RuleSet::from_declarations(styles, &media());
364 let matched = collect_matched_rules(&snapshot, target, &rules, Some("color: black"));
365 let inline = matched.first().unwrap();
366 let stylesheet = matched.iter().find(|rule| rule.selector == "p").unwrap();
367 assert!(!declaration(inline, "color").applied);
368 assert!(declaration(stylesheet, "color").applied);
369 }
370
371 #[test]
372 fn user_agent_origin_ranks_below_author_origin() {
373 let snapshot = snapshot_for("<body><p>t</p></body>");
374 let mut combined = resolved_for_origin("p { margin-top: 0px; }", StyleOrigin::UserAgent);
375 combined.extend(resolved_for_origin(
376 "p { margin-top: 4px; }",
377 StyleOrigin::Author,
378 ));
379 let styles = leak(combined);
380 let rules = RuleSet::from_declarations(styles, &media());
381
382 let matched = collect_matched_rules(&snapshot, node_with_tag(&snapshot, "p"), &rules, None);
383 let ua = matched
384 .iter()
385 .find(|rule| rule.origin == StyleOrigin::UserAgent)
386 .expect("user-agent rule reported");
387 let author_rule = matched
388 .iter()
389 .find(|rule| rule.origin == StyleOrigin::Author)
390 .expect("author rule reported");
391 assert!(!declaration(ua, "margin-top").applied);
392 assert!(declaration(author_rule, "margin-top").applied);
393 }
394}