//! CSS selector matching engine. //! //! Matches selectors against DOM elements and computes specificity. use we_css::parser::{ AttributeOp, AttributeSelector, Combinator, CompoundSelector, Declaration, Rule, Selector, SelectorComponent, SelectorList, SimpleSelector, StyleRule, Stylesheet, }; use we_dom::{Document, NodeData, NodeId}; /// Selector specificity as (a, b, c): /// a = number of ID selectors /// b = number of class selectors, attribute selectors, pseudo-classes /// c = number of type selectors, pseudo-elements #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct Specificity { pub a: u32, pub b: u32, pub c: u32, } impl Specificity { pub fn new(a: u32, b: u32, c: u32) -> Self { Self { a, b, c } } } impl PartialOrd for Specificity { fn partial_cmp(&self, other: &Self) -> Option { Some(self.cmp(other)) } } impl Ord for Specificity { fn cmp(&self, other: &Self) -> core::cmp::Ordering { self.a .cmp(&other.a) .then(self.b.cmp(&other.b)) .then(self.c.cmp(&other.c)) } } /// A matched rule: a style rule paired with the specificity of the selector /// that matched, plus source order for tie-breaking. #[derive(Debug, Clone)] pub struct MatchedRule<'a> { pub rule: &'a StyleRule, pub specificity: Specificity, pub source_order: usize, } /// Check if a selector list matches an element. Returns true if any selector /// in the comma-separated list matches. pub fn matches_selector_list(doc: &Document, node: NodeId, selector_list: &SelectorList) -> bool { selector_list .selectors .iter() .any(|sel| matches_selector(doc, node, sel)) } /// Check if a complex selector matches an element. /// /// A complex selector is a chain of compound selectors joined by combinators, /// stored left-to-right: `[Compound, Combinator, Compound, ...]`. /// We match right-to-left: the rightmost compound (key selector) must match /// the target element, then we walk the tree based on combinators. pub fn matches_selector(doc: &Document, node: NodeId, selector: &Selector) -> bool { // Extract compounds and combinators from the components list. // Components alternate: Compound, Combinator, Compound, Combinator, ... let compounds: Vec<&CompoundSelector> = selector .components .iter() .filter_map(|c| match c { SelectorComponent::Compound(cs) => Some(cs), _ => None, }) .collect(); let combinators: Vec = selector .components .iter() .filter_map(|c| match c { SelectorComponent::Combinator(comb) => Some(*comb), _ => None, }) .collect(); if compounds.is_empty() { return false; } // The key selector is the last compound. if !matches_compound(doc, node, compounds[compounds.len() - 1]) { return false; } // Walk right-to-left through the remaining compounds. // compounds[i] is connected to compounds[i+1] by combinators[i]. let mut current = node; for i in (0..compounds.len() - 1).rev() { let compound = compounds[i]; let combinator = combinators[i]; match combinator { Combinator::Descendant => { // Walk up ancestor chain until we find a match or run out. let mut found = false; let mut ancestor = doc.parent(current); while let Some(anc) = ancestor { if matches_compound(doc, anc, compound) { current = anc; found = true; break; } ancestor = doc.parent(anc); } if !found { return false; } } Combinator::Child => { // Direct parent must match. if let Some(parent) = doc.parent(current) { if matches_compound(doc, parent, compound) { current = parent; } else { return false; } } else { return false; } } Combinator::AdjacentSibling => { // Previous element sibling must match. if let Some(prev) = prev_element_sibling(doc, current) { if matches_compound(doc, prev, compound) { current = prev; } else { return false; } } else { return false; } } Combinator::GeneralSibling => { // Any previous element sibling must match. let mut found = false; let mut prev = prev_element_sibling(doc, current); while let Some(sib) = prev { if matches_compound(doc, sib, compound) { current = sib; found = true; break; } prev = prev_element_sibling(doc, sib); } if !found { return false; } } } } true } /// Check if a compound selector matches an element. All simple selectors /// in the compound must match. pub fn matches_compound(doc: &Document, node: NodeId, compound: &CompoundSelector) -> bool { // Must be an element node. if !matches!(doc.node_data(node), NodeData::Element { .. }) { return false; } compound.simple.iter().all(|s| matches_simple(doc, node, s)) } /// Check if a simple selector matches an element. pub fn matches_simple(doc: &Document, node: NodeId, selector: &SimpleSelector) -> bool { match selector { SimpleSelector::Universal => { matches!(doc.node_data(node), NodeData::Element { .. }) } SimpleSelector::Type(name) => doc .tag_name(node) .map(|tag| tag.eq_ignore_ascii_case(name)) .unwrap_or(false), SimpleSelector::Class(class_name) => { if let Some(class_attr) = doc.get_attribute(node, "class") { class_attr.split_ascii_whitespace().any(|c| c == class_name) } else { false } } SimpleSelector::Id(id) => doc .get_attribute(node, "id") .map(|val| val == id) .unwrap_or(false), SimpleSelector::Attribute(attr_sel) => matches_attribute(doc, node, attr_sel), SimpleSelector::PseudoClass(_name) => { // Pseudo-classes are not yet implemented; always return false. false } } } /// Check if an attribute selector matches an element. fn matches_attribute(doc: &Document, node: NodeId, sel: &AttributeSelector) -> bool { let attr_value = doc.get_attribute(node, &sel.name); match (&sel.op, &sel.value) { // [attr] — attribute presence (None, _) => attr_value.is_some(), (Some(op), Some(expected)) => { let actual = match attr_value { Some(v) => v, None => return false, }; match op { // [attr=val] AttributeOp::Exact => actual == expected, // [attr~=val] — whitespace-separated list contains val AttributeOp::Includes => actual.split_ascii_whitespace().any(|w| w == expected), // [attr|=val] — equals val or starts with val- AttributeOp::DashMatch => { actual == expected || (actual.starts_with(expected.as_str()) && actual.as_bytes().get(expected.len()) == Some(&b'-')) } // [attr^=val] AttributeOp::Prefix => actual.starts_with(expected.as_str()), // [attr$=val] AttributeOp::Suffix => actual.ends_with(expected.as_str()), // [attr*=val] AttributeOp::Substring => actual.contains(expected.as_str()), } } // op without value — shouldn't happen from parser, but treat as no match (Some(_), None) => false, } } /// Calculate specificity of a selector. pub fn specificity(selector: &Selector) -> Specificity { let mut a = 0u32; let mut b = 0u32; let mut c = 0u32; for component in &selector.components { if let SelectorComponent::Compound(compound) = component { for simple in &compound.simple { match simple { SimpleSelector::Id(_) => a += 1, SimpleSelector::Class(_) | SimpleSelector::Attribute(_) | SimpleSelector::PseudoClass(_) => b += 1, SimpleSelector::Type(_) => c += 1, SimpleSelector::Universal => {} } } } } Specificity::new(a, b, c) } /// Collect all matching rules from a stylesheet for a given element, /// sorted by specificity and source order (ascending — highest priority last). pub fn collect_matching_rules<'a>( doc: &Document, node: NodeId, stylesheet: &'a Stylesheet, ) -> Vec> { let mut matched = Vec::new(); collect_from_rules(doc, node, &stylesheet.rules, &mut matched, &mut 0); matched.sort_by(|a, b| { a.specificity .cmp(&b.specificity) .then(a.source_order.cmp(&b.source_order)) }); matched } fn collect_from_rules<'a>( doc: &Document, node: NodeId, rules: &'a [Rule], matched: &mut Vec>, order: &mut usize, ) { for rule in rules { match rule { Rule::Style(style_rule) => { let current_order = *order; *order += 1; // Check each selector in the selector list. for selector in &style_rule.selectors.selectors { if matches_selector(doc, node, selector) { matched.push(MatchedRule { rule: style_rule, specificity: specificity(selector), source_order: current_order, }); // Only add the rule once even if multiple selectors match. break; } } } Rule::Media(media_rule) => { // For now, assume all media rules match (media query evaluation // is out of scope for this issue). collect_from_rules(doc, node, &media_rule.rules, matched, order); } Rule::Import(_) => { // Imports are resolved at a higher level; skip here. } } } } /// Find the previous element sibling (skipping text/comment nodes). fn prev_element_sibling(doc: &Document, node: NodeId) -> Option { let mut prev = doc.prev_sibling(node); while let Some(p) = prev { if matches!(doc.node_data(p), NodeData::Element { .. }) { return Some(p); } prev = doc.prev_sibling(p); } None } /// Get all declarations that apply to a node, in cascade order. /// Returns declarations from matching rules, sorted by specificity and source order. /// Important declarations are placed after normal declarations. pub fn get_declarations_for_node<'a>( doc: &Document, node: NodeId, stylesheet: &'a Stylesheet, ) -> Vec<&'a Declaration> { let matched_rules = collect_matching_rules(doc, node, stylesheet); let mut normal: Vec<(Specificity, usize, &'a Declaration)> = Vec::new(); let mut important: Vec<(Specificity, usize, &'a Declaration)> = Vec::new(); for matched in &matched_rules { for decl in &matched.rule.declarations { if decl.important { important.push((matched.specificity, matched.source_order, decl)); } else { normal.push((matched.specificity, matched.source_order, decl)); } } } // Normal declarations are already sorted by specificity/order from collect_matching_rules. // Important declarations override normal ones regardless of specificity, // but among themselves they follow specificity order too. let mut result: Vec<&'a Declaration> = Vec::new(); for (_, _, decl) in &normal { result.push(decl); } for (_, _, decl) in &important { result.push(decl); } result } #[cfg(test)] mod tests { use super::*; use we_css::parser::Parser; #[allow(dead_code)] struct TestDom { doc: Document, html: NodeId, body: NodeId, div_main: NodeId, p_intro: NodeId, text_hello: NodeId, p2: NodeId, span: NodeId, a_link: NodeId, } fn make_test_dom() -> TestDom { let mut doc = Document::new(); let root = doc.root(); let html = doc.create_element("html"); doc.append_child(root, html); let body = doc.create_element("body"); doc.append_child(html, body); let div = doc.create_element("div"); doc.set_attribute(div, "id", "main"); doc.set_attribute(div, "class", "container wide"); doc.append_child(body, div); let p1 = doc.create_element("p"); doc.set_attribute(p1, "class", "intro"); doc.append_child(div, p1); let text1 = doc.create_text("Hello"); doc.append_child(p1, text1); let p2 = doc.create_element("p"); doc.append_child(div, p2); let span = doc.create_element("span"); doc.set_attribute(span, "data-x", "foo"); doc.append_child(p2, span); let a = doc.create_element("a"); doc.set_attribute(a, "href", "https://example.com"); doc.set_attribute(a, "class", "link"); doc.append_child(div, a); TestDom { doc, html, body, div_main: div, p_intro: p1, text_hello: text1, p2, span, a_link: a, } } // ----------------------------------------------------------------------- // Type selector // ----------------------------------------------------------------------- #[test] fn type_selector_matches() { let t = make_test_dom(); let sel = parse_first_selector("p {}"); assert!(matches_selector(&t.doc, t.p_intro, &sel)); assert!(matches_selector(&t.doc, t.p2, &sel)); assert!(!matches_selector(&t.doc, t.div_main, &sel)); } #[test] fn type_selector_case_insensitive() { let t = make_test_dom(); let sel = parse_first_selector("P {}"); assert!(matches_selector(&t.doc, t.p_intro, &sel)); } // ----------------------------------------------------------------------- // Universal selector // ----------------------------------------------------------------------- #[test] fn universal_selector_matches_any_element() { let t = make_test_dom(); let sel = parse_first_selector("* {}"); assert!(matches_selector(&t.doc, t.html, &sel)); assert!(matches_selector(&t.doc, t.div_main, &sel)); assert!(matches_selector(&t.doc, t.p_intro, &sel)); assert!(!matches_selector(&t.doc, t.text_hello, &sel)); } // ----------------------------------------------------------------------- // Class selector // ----------------------------------------------------------------------- #[test] fn class_selector_matches() { let t = make_test_dom(); let sel = parse_first_selector(".intro {}"); assert!(matches_selector(&t.doc, t.p_intro, &sel)); assert!(!matches_selector(&t.doc, t.p2, &sel)); } #[test] fn class_selector_matches_multi_class() { let t = make_test_dom(); let sel = parse_first_selector(".wide {}"); assert!(matches_selector(&t.doc, t.div_main, &sel)); } // ----------------------------------------------------------------------- // ID selector // ----------------------------------------------------------------------- #[test] fn id_selector_matches() { let t = make_test_dom(); let sel = parse_first_selector("#main {}"); assert!(matches_selector(&t.doc, t.div_main, &sel)); assert!(!matches_selector(&t.doc, t.p_intro, &sel)); } // ----------------------------------------------------------------------- // Attribute selectors // ----------------------------------------------------------------------- #[test] fn attribute_presence() { let t = make_test_dom(); let sel = parse_first_selector("[data-x] {}"); assert!(matches_selector(&t.doc, t.span, &sel)); assert!(!matches_selector(&t.doc, t.p_intro, &sel)); } #[test] fn attribute_exact_match() { let t = make_test_dom(); let sel = parse_first_selector("[data-x=\"foo\"] {}"); assert!(matches_selector(&t.doc, t.span, &sel)); let sel2 = parse_first_selector("[data-x=\"bar\"] {}"); assert!(!matches_selector(&t.doc, t.span, &sel2)); } #[test] fn attribute_includes() { let t = make_test_dom(); let sel = parse_first_selector("[class~=\"container\"] {}"); assert!(matches_selector(&t.doc, t.div_main, &sel)); let sel2 = parse_first_selector("[class~=\"wide\"] {}"); assert!(matches_selector(&t.doc, t.div_main, &sel2)); let sel3 = parse_first_selector("[class~=\"contain\"] {}"); assert!(!matches_selector(&t.doc, t.div_main, &sel3)); } #[test] fn attribute_prefix() { let t = make_test_dom(); let sel = parse_first_selector("[href^=\"https\"] {}"); assert!(matches_selector(&t.doc, t.a_link, &sel)); } #[test] fn attribute_suffix() { let t = make_test_dom(); let sel = parse_first_selector("[href$=\".com\"] {}"); assert!(matches_selector(&t.doc, t.a_link, &sel)); } #[test] fn attribute_substring() { let t = make_test_dom(); let sel = parse_first_selector("[href*=\"example\"] {}"); assert!(matches_selector(&t.doc, t.a_link, &sel)); } #[test] fn attribute_dash_match() { let mut doc = Document::new(); let root = doc.root(); let el = doc.create_element("div"); doc.set_attribute(el, "lang", "en-US"); doc.append_child(root, el); let sel = parse_first_selector("[lang|=\"en\"] {}"); assert!(matches_selector(&doc, el, &sel)); let sel2 = parse_first_selector("[lang|=\"en-US\"] {}"); assert!(matches_selector(&doc, el, &sel2)); let sel3 = parse_first_selector("[lang|=\"fr\"] {}"); assert!(!matches_selector(&doc, el, &sel3)); } // ----------------------------------------------------------------------- // Compound selectors // ----------------------------------------------------------------------- #[test] fn compound_selector() { let t = make_test_dom(); let sel = parse_first_selector("p.intro {}"); assert!(matches_selector(&t.doc, t.p_intro, &sel)); assert!(!matches_selector(&t.doc, t.p2, &sel)); } #[test] fn compound_type_id_class() { let t = make_test_dom(); let sel = parse_first_selector("div#main.container {}"); assert!(matches_selector(&t.doc, t.div_main, &sel)); } // ----------------------------------------------------------------------- // Descendant combinator // ----------------------------------------------------------------------- #[test] fn descendant_combinator() { let t = make_test_dom(); let sel = parse_first_selector("body p {}"); assert!(matches_selector(&t.doc, t.p_intro, &sel)); assert!(matches_selector(&t.doc, t.p2, &sel)); assert!(!matches_selector(&t.doc, t.div_main, &sel)); } #[test] fn descendant_combinator_deep() { let t = make_test_dom(); let sel = parse_first_selector("html span {}"); assert!(matches_selector(&t.doc, t.span, &sel)); } // ----------------------------------------------------------------------- // Child combinator // ----------------------------------------------------------------------- #[test] fn child_combinator() { let t = make_test_dom(); let sel = parse_first_selector("div > p {}"); assert!(matches_selector(&t.doc, t.p_intro, &sel)); assert!(matches_selector(&t.doc, t.p2, &sel)); let sel2 = parse_first_selector("div > span {}"); assert!(!matches_selector(&t.doc, t.span, &sel2)); } // ----------------------------------------------------------------------- // Adjacent sibling combinator // ----------------------------------------------------------------------- #[test] fn adjacent_sibling_combinator() { let t = make_test_dom(); let sel = parse_first_selector("p.intro + p {}"); assert!(matches_selector(&t.doc, t.p2, &sel)); let sel2 = parse_first_selector("p + a {}"); assert!(matches_selector(&t.doc, t.a_link, &sel2)); // p.intro + a — not adjacent (p2 is between) let sel3 = parse_first_selector("p.intro + a {}"); assert!(!matches_selector(&t.doc, t.a_link, &sel3)); } // ----------------------------------------------------------------------- // General sibling combinator // ----------------------------------------------------------------------- #[test] fn general_sibling_combinator() { let t = make_test_dom(); let sel = parse_first_selector("p ~ a {}"); assert!(matches_selector(&t.doc, t.a_link, &sel)); let sel2 = parse_first_selector("p.intro ~ a {}"); assert!(matches_selector(&t.doc, t.a_link, &sel2)); } // ----------------------------------------------------------------------- // Selector list // ----------------------------------------------------------------------- #[test] fn selector_list_matches() { let t = make_test_dom(); let ss = Parser::parse("span, a {}"); let rule = match &ss.rules[0] { Rule::Style(r) => r, _ => panic!("expected style rule"), }; assert!(matches_selector_list(&t.doc, t.span, &rule.selectors)); assert!(matches_selector_list(&t.doc, t.a_link, &rule.selectors)); assert!(!matches_selector_list(&t.doc, t.div_main, &rule.selectors)); } // ----------------------------------------------------------------------- // Specificity // ----------------------------------------------------------------------- #[test] fn specificity_type_selector() { let sel = parse_first_selector("p {}"); assert_eq!(specificity(&sel), Specificity::new(0, 0, 1)); } #[test] fn specificity_class_selector() { let sel = parse_first_selector(".intro {}"); assert_eq!(specificity(&sel), Specificity::new(0, 1, 0)); } #[test] fn specificity_id_selector() { let sel = parse_first_selector("#main {}"); assert_eq!(specificity(&sel), Specificity::new(1, 0, 0)); } #[test] fn specificity_compound() { let sel = parse_first_selector("div#main.container {}"); assert_eq!(specificity(&sel), Specificity::new(1, 1, 1)); } #[test] fn specificity_complex() { let sel = parse_first_selector("body div > p.intro {}"); assert_eq!(specificity(&sel), Specificity::new(0, 1, 3)); } #[test] fn specificity_universal() { let sel = parse_first_selector("* {}"); assert_eq!(specificity(&sel), Specificity::new(0, 0, 0)); } #[test] fn specificity_attribute() { let sel = parse_first_selector("[data-x] {}"); assert_eq!(specificity(&sel), Specificity::new(0, 1, 0)); } #[test] fn specificity_ordering() { let s1 = Specificity::new(0, 0, 1); let s2 = Specificity::new(0, 1, 0); let s3 = Specificity::new(1, 0, 0); assert!(s1 < s2); assert!(s2 < s3); assert!(s1 < s3); } // ----------------------------------------------------------------------- // Collect matching rules // ----------------------------------------------------------------------- #[test] fn collect_matching_rules_basic() { let t = make_test_dom(); let ss = Parser::parse( r#" p { color: red; } .intro { font-size: 16px; } div { margin: 0; } "#, ); let matched = collect_matching_rules(&t.doc, t.p_intro, &ss); assert_eq!(matched.len(), 2); assert_eq!(matched[0].specificity, Specificity::new(0, 0, 1)); assert_eq!(matched[1].specificity, Specificity::new(0, 1, 0)); } #[test] fn collect_matching_rules_source_order() { let t = make_test_dom(); let ss = Parser::parse( r#" p { color: red; } p { color: blue; } "#, ); let matched = collect_matching_rules(&t.doc, t.p_intro, &ss); assert_eq!(matched.len(), 2); assert!(matched[0].source_order < matched[1].source_order); } // ----------------------------------------------------------------------- // Important declarations // ----------------------------------------------------------------------- #[test] fn important_declarations_come_after_normal() { let t = make_test_dom(); let ss = Parser::parse( r#" p { color: red !important; } #main p { color: blue; } "#, ); let decls = get_declarations_for_node(&t.doc, t.p_intro, &ss); assert_eq!(decls.len(), 2); assert!(!decls[0].important); assert!(decls[1].important); } // ----------------------------------------------------------------------- // Text node doesn't match // ----------------------------------------------------------------------- #[test] fn text_node_never_matches() { let t = make_test_dom(); let sel = parse_first_selector("* {}"); assert!(!matches_selector(&t.doc, t.text_hello, &sel)); } // ----------------------------------------------------------------------- // Complex multi-level selector // ----------------------------------------------------------------------- #[test] fn complex_multi_level() { let t = make_test_dom(); let sel = parse_first_selector("html body div > p.intro {}"); assert!(matches_selector(&t.doc, t.p_intro, &sel)); assert!(!matches_selector(&t.doc, t.p2, &sel)); } // ----------------------------------------------------------------------- // Helper // ----------------------------------------------------------------------- fn parse_first_selector(css: &str) -> Selector { let ss = Parser::parse(css); match &ss.rules[0] { Rule::Style(r) => r.selectors.selectors[0].clone(), _ => panic!("expected style rule"), } } }