Something went wrong. Try again.
we (web engine): Experimental web browser project to understand the limits of Claude
Something went wrong. Try again.
Rust
at url-parser
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826//! 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<core::cmp::Ordering> { 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<Combinator> = 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<MatchedRule<'a>> { 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<MatchedRule<'a>>, 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<NodeId> { 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"), } }}