Skip to main content

rama_http/protocols/html/selector/
ast.rs

1//! Abstract syntax tree for the supported CSS selector subset.
2//!
3//! The subset is limited to what a streaming matcher can evaluate (no
4//! sibling lookahead, no full-document state). The parser is hand-rolled so
5//! rama pulls in no extra dependencies — no `cssparser`, no Servo
6//! `selectors`. See `parser.rs` for the grammar.
7
8/// A parsed CSS selector string.
9///
10/// A selector string may contain a comma-separated list of complex
11/// selectors (e.g. `"a, .b > c"`); the [`Selector`] matches an element if
12/// *any* of those complex selectors matches it.
13#[derive(Debug, Clone, PartialEq, Eq)]
14pub struct Selector {
15    pub(crate) selectors: Vec<ComplexSelector>,
16}
17
18/// A single complex selector: a sequence of compound selectors joined by
19/// combinators (e.g. `div.menu > a`).
20#[derive(Debug, Clone, PartialEq, Eq)]
21pub(crate) struct ComplexSelector {
22    /// Compound selectors in source (left-to-right) order. The combinator
23    /// stored with each part is the one that *precedes* it; the first
24    /// part's combinator is always `None`.
25    pub(crate) parts: Vec<SelectorPart>,
26}
27
28#[derive(Debug, Clone, PartialEq, Eq)]
29pub(crate) struct SelectorPart {
30    pub(crate) combinator: Option<Combinator>,
31    pub(crate) compound: Compound,
32}
33
34/// A combinator between two compound selectors.
35#[derive(Debug, Clone, Copy, PartialEq, Eq)]
36pub(crate) enum Combinator {
37    /// Descendant combinator (whitespace), e.g. `a b`.
38    Descendant,
39    /// Child combinator `>`, e.g. `a > b`.
40    Child,
41}
42
43/// A compound selector: an optional type/universal selector plus zero or
44/// more subclass selectors, all of which must match the same element.
45///
46/// Build one with the infallible [`Compound::tag`] / [`Compound::class`] /
47/// … constructors (see the [builder](super) methods).
48#[derive(Debug, Clone, PartialEq, Eq, Default)]
49pub struct Compound {
50    /// Element type. `None` matches any element.
51    pub(crate) name: Option<LocalName>,
52    /// Whether a universal `*` was explicitly written. Only affects
53    /// serialization; matching treats `None` and `Universal` identically.
54    pub(crate) explicit_universal: bool,
55    pub(crate) id: Option<Box<str>>,
56    pub(crate) classes: Vec<Box<str>>,
57    pub(crate) attributes: Vec<AttributeSelector>,
58    pub(crate) nth: Vec<Nth>,
59    /// Arguments of `:not(...)`. The element matches the compound only if
60    /// it matches *none* of these (so `:not(a, b)` is stored as two
61    /// entries, matching neither).
62    pub(crate) negations: Vec<Self>,
63}
64
65/// An attribute selector such as `[href]` or `[class~="menu" i]`.
66#[derive(Debug, Clone, PartialEq, Eq)]
67pub(crate) struct AttributeSelector {
68    /// ASCII-lowercased attribute name.
69    pub(crate) name: Box<str>,
70    /// Match operator. `None` means presence-only (`[name]`).
71    pub(crate) operator: Option<AttributeOperator>,
72    /// Match value; empty when `operator` is `None`.
73    pub(crate) value: Box<str>,
74    pub(crate) case: CaseSensitivity,
75}
76
77/// Attribute value match operator.
78#[derive(Debug, Clone, Copy, PartialEq, Eq)]
79pub(crate) enum AttributeOperator {
80    /// `[a=v]` — exactly equal.
81    Equals,
82    /// `[a~=v]` — whitespace-separated list contains `v`.
83    Includes,
84    /// `[a|=v]` — equal to `v` or starts with `v-`.
85    DashMatch,
86    /// `[a^=v]` — begins with `v`.
87    Prefix,
88    /// `[a$=v]` — ends with `v`.
89    Suffix,
90    /// `[a*=v]` — contains `v`.
91    Substring,
92}
93
94/// Whether an attribute value is matched case-sensitively.
95#[derive(Debug, Clone, Copy, PartialEq, Eq)]
96pub(crate) enum CaseSensitivity {
97    /// Default for HTML attribute values.
98    CaseSensitive,
99    /// Requested via the `i` flag (ASCII case-insensitive).
100    AsciiCaseInsensitive,
101}
102
103/// A structural `:nth-*` selector as a step `a` and offset `b`: it matches
104/// sibling positions `a*n + b` for integers `n >= 0`.
105///
106/// `:first-child` is stored as `Child` with `a = 0, b = 1`, and
107/// `:first-of-type` as `OfType` with `a = 0, b = 1`.
108#[derive(Debug, Clone, Copy, PartialEq, Eq)]
109pub(crate) struct Nth {
110    pub(crate) ty: NthType,
111    pub(crate) a: i32,
112    pub(crate) b: i32,
113}
114
115#[derive(Debug, Clone, Copy, PartialEq, Eq)]
116pub(crate) enum NthType {
117    /// `:nth-child` / `:first-child` — counts all element siblings.
118    Child,
119    /// `:nth-of-type` / `:first-of-type` — counts same-type siblings.
120    OfType,
121}
122
123impl Nth {
124    /// Returns whether a 1-based sibling `index` matches step `a`/offset `b`
125    /// (i.e. `index == a*n + b` for some `n >= 0`).
126    pub(crate) fn matches_index(self, index: usize) -> bool {
127        // Work in `i64` (selectors are `i32`), converting/subtracting with
128        // checked ops so a pathologically large `index` or `b` can never
129        // overflow — it simply fails to match.
130        let Ok(i) = i64::try_from(index) else {
131            return false;
132        };
133        let a = i64::from(self.a);
134        let b = i64::from(self.b);
135        let Some(diff) = i.checked_sub(b) else {
136            return false;
137        };
138        if a == 0 {
139            return diff == 0;
140        }
141        diff % a == 0 && diff / a >= 0
142    }
143}
144
145/// An ASCII-lowercased element (tag) name with a precomputed packed key
146/// for allocation-free comparison against raw element names.
147#[derive(Debug, Clone, PartialEq, Eq)]
148pub(crate) struct LocalName {
149    /// ASCII-lowercased name.
150    name: Box<str>,
151    /// `Some(_)` when `name` fits in 8 ASCII bytes, enabling an integer
152    /// fast path in [`LocalName::matches`].
153    packed: Option<u64>,
154}
155
156impl LocalName {
157    pub(crate) fn new(name: &str) -> Self {
158        let lower = name.to_ascii_lowercase();
159        let packed = pack_name(lower.as_bytes());
160        Self {
161            name: lower.into_boxed_str(),
162            packed,
163        }
164    }
165
166    pub(crate) fn as_str(&self) -> &str {
167        &self.name
168    }
169
170    /// ASCII-case-insensitive comparison against a raw element name,
171    /// without allocating.
172    pub(crate) fn matches(&self, raw: &str) -> bool {
173        self.matches_bytes(raw.as_bytes())
174    }
175
176    /// Byte-slice variant of [`Self::matches`].
177    pub(crate) fn matches_bytes(&self, raw: &[u8]) -> bool {
178        if let Some(packed) = self.packed
179            && let Some(other) = pack_name(raw)
180        {
181            return packed == other;
182        }
183        raw.eq_ignore_ascii_case(self.name.as_bytes())
184    }
185}
186
187impl AttributeSelector {
188    /// Whether `actual` (the element's attribute value) satisfies this
189    /// selector's operator. Presence-only selectors (`[name]`) match any
190    /// value; callers must have already confirmed the attribute exists.
191    pub(crate) fn matches_value(&self, actual: &[u8]) -> bool {
192        let expected = self.value.as_bytes();
193        let ci = matches!(self.case, CaseSensitivity::AsciiCaseInsensitive);
194        match self.operator {
195            None => true,
196            Some(AttributeOperator::Equals) => bytes_eq(actual, expected, ci),
197            Some(AttributeOperator::Includes) => {
198                !expected.is_empty()
199                    && !expected.iter().any(u8::is_ascii_whitespace)
200                    && actual
201                        .split(u8::is_ascii_whitespace)
202                        .any(|token| bytes_eq(token, expected, ci))
203            }
204            Some(AttributeOperator::DashMatch) => {
205                bytes_eq(actual, expected, ci)
206                    || (actual.len() > expected.len()
207                        && bytes_starts_with(actual, expected, ci)
208                        && actual.get(expected.len()) == Some(&b'-'))
209            }
210            Some(AttributeOperator::Prefix) => {
211                !expected.is_empty() && bytes_starts_with(actual, expected, ci)
212            }
213            Some(AttributeOperator::Suffix) => {
214                !expected.is_empty() && bytes_ends_with(actual, expected, ci)
215            }
216            Some(AttributeOperator::Substring) => {
217                !expected.is_empty() && bytes_contains(actual, expected, ci)
218            }
219        }
220    }
221}
222
223fn bytes_eq(a: &[u8], b: &[u8], case_insensitive: bool) -> bool {
224    if case_insensitive {
225        a.eq_ignore_ascii_case(b)
226    } else {
227        a == b
228    }
229}
230
231fn bytes_starts_with(haystack: &[u8], needle: &[u8], case_insensitive: bool) -> bool {
232    haystack
233        .get(..needle.len())
234        .is_some_and(|head| bytes_eq(head, needle, case_insensitive))
235}
236
237fn bytes_ends_with(haystack: &[u8], needle: &[u8], case_insensitive: bool) -> bool {
238    haystack
239        .len()
240        .checked_sub(needle.len())
241        .and_then(|start| haystack.get(start..))
242        .is_some_and(|tail| bytes_eq(tail, needle, case_insensitive))
243}
244
245fn bytes_contains(haystack: &[u8], needle: &[u8], case_insensitive: bool) -> bool {
246    if needle.len() > haystack.len() {
247        return false;
248    }
249    if !case_insensitive {
250        return haystack
251            .windows(needle.len())
252            .any(|window| window == needle);
253    }
254    (0..=haystack.len() - needle.len()).any(|i| {
255        haystack
256            .get(i..i + needle.len())
257            .is_some_and(|window| window.eq_ignore_ascii_case(needle))
258    })
259}
260
261/// Packs an element name into a `u64` when it is at most 8 ASCII bytes,
262/// ASCII-lowercasing each byte. Returns `None` otherwise, signalling the
263/// caller to fall back to a byte comparison.
264fn pack_name(bytes: &[u8]) -> Option<u64> {
265    if bytes.len() > 8 {
266        return None;
267    }
268    let mut packed = 0u64;
269    for &b in bytes {
270        if !b.is_ascii() {
271            return None;
272        }
273        packed = (packed << 8) | u64::from(b.to_ascii_lowercase());
274    }
275    Some(packed)
276}