Skip to main content

rama_http/protocols/html/selector/
matcher.rs

1//! Matching of parsed selectors against an element tree.
2//!
3//! The matcher walks a complex selector right-to-left and is allocation-
4//! free: name, class, id and attribute comparisons all operate on borrowed
5//! string slices supplied by the [`SelectorSubject`] implementation.
6
7use super::ast::{
8    AttributeSelector, Combinator, ComplexSelector, Compound, NthType, Selector, SelectorPart,
9};
10
11/// A read-only view of an element, providing exactly what the selector
12/// matcher needs. Implement it for your own tree to match selectors
13/// against it, or use the in-memory [`Dom`](super::Dom).
14///
15/// `Self` is expected to be a cheap handle (e.g. an index into an arena),
16/// since [`parent`](SelectorSubject::parent) returns it by value.
17///
18/// Attribute and tag names are matched ASCII case-insensitively (per HTML),
19/// while class names, ids and attribute *values* are case-sensitive by
20/// default. The `name` passed to [`attribute`](SelectorSubject::attribute)
21/// is already ASCII-lowercased.
22pub trait SelectorSubject: Sized {
23    /// The element's tag name (any ASCII case).
24    fn local_name(&self) -> &str;
25
26    /// The value of the attribute with the given (ASCII-lowercased) name.
27    fn attribute(&self, name: &str) -> Option<&str>;
28
29    /// The element's parent, if any.
30    fn parent(&self) -> Option<Self>;
31
32    /// The element's 1-based position among all element siblings
33    /// (for `:nth-child`). A root element returns `1`.
34    fn nth_child_index(&self) -> usize;
35
36    /// The element's 1-based position among element siblings of the same
37    /// type (for `:nth-of-type`). A root element returns `1`.
38    fn nth_of_type_index(&self) -> usize;
39
40    /// Whether the element has the given (case-sensitive) id.
41    ///
42    /// Defaults to an exact comparison against the `id` attribute.
43    fn has_id(&self, id: &str) -> bool {
44        self.attribute("id") == Some(id)
45    }
46
47    /// Whether the element has the given (case-sensitive) class.
48    ///
49    /// Defaults to scanning the whitespace-separated `class` attribute.
50    fn has_class(&self, class: &str) -> bool {
51        self.attribute("class")
52            .is_some_and(|value| value.split_ascii_whitespace().any(|c| c == class))
53    }
54}
55
56impl Selector {
57    /// Returns whether `subject` matches this selector.
58    pub fn matches<S: SelectorSubject>(&self, subject: &S) -> bool {
59        self.selectors.iter().any(|c| complex_matches(c, subject))
60    }
61}
62
63fn complex_matches<S: SelectorSubject>(complex: &ComplexSelector, subject: &S) -> bool {
64    match_from(&complex.parts, complex.parts.len() - 1, subject)
65}
66
67/// Matches `parts[..=idx]` against `subject` as the right-most compound,
68/// recursing leftward through the combinators.
69fn match_from<S: SelectorSubject>(parts: &[SelectorPart], idx: usize, subject: &S) -> bool {
70    let part = &parts[idx];
71    if !compound_matches(&part.compound, subject) {
72        return false;
73    }
74    if idx == 0 {
75        return true;
76    }
77
78    let Some(combinator) = part.combinator else {
79        return false;
80    };
81    match combinator {
82        Combinator::Child => subject
83            .parent()
84            .is_some_and(|parent| match_from(parts, idx - 1, &parent)),
85        Combinator::Descendant => {
86            let mut ancestor = subject.parent();
87            while let Some(node) = ancestor {
88                if match_from(parts, idx - 1, &node) {
89                    return true;
90                }
91                ancestor = node.parent();
92            }
93            false
94        }
95    }
96}
97
98fn compound_matches<S: SelectorSubject>(compound: &Compound, subject: &S) -> bool {
99    if let Some(name) = &compound.name
100        && !name.matches(subject.local_name())
101    {
102        return false;
103    }
104    if let Some(id) = &compound.id
105        && !subject.has_id(id)
106    {
107        return false;
108    }
109    if !compound.classes.iter().all(|c| subject.has_class(c)) {
110        return false;
111    }
112    if !compound
113        .attributes
114        .iter()
115        .all(|a| attribute_matches(a, subject))
116    {
117        return false;
118    }
119    for nth in &compound.nth {
120        let index = match nth.ty {
121            NthType::Child => subject.nth_child_index(),
122            NthType::OfType => subject.nth_of_type_index(),
123        };
124        if !nth.matches_index(index) {
125            return false;
126        }
127    }
128    // `:not(...)` — must match none of the negated compounds.
129    !compound
130        .negations
131        .iter()
132        .any(|neg| compound_matches(neg, subject))
133}
134
135fn attribute_matches<S: SelectorSubject>(selector: &AttributeSelector, subject: &S) -> bool {
136    subject
137        .attribute(&selector.name)
138        .is_some_and(|actual| selector.matches_value(actual.as_bytes()))
139}