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