Skip to main content

domtree/
query.rs

1//! A small, hand-rolled CSS selector engine (behind the `query` feature).
2//!
3//! Zero dependencies (no `selectors`/`cssparser`), `no_std + alloc`, and it
4//! never panics: any selector string against any document simply matches or
5//! doesn't. Pass a `&str` to [`Dom::select`] / [`NodeRef::select`], or parse
6//! once with [`Selector::parse`] to reuse it and to get a [`SelectorError`]
7//! (with a byte position) for invalid input.
8//!
9//! ## Supported grammar
10//!
11//! - **Simple selectors:** type `div`, universal `*`, `#id`, `.class`, and any
12//!   compound of them (`div.card.active#main`).
13//! - **Selector lists** `a, b` and all four **combinators**: descendant
14//!   `a b`, child `a > b`, next-sibling `a + b`, subsequent-sibling `a ~ b`.
15//! - **Attributes:** `[a]`, `[a=v]`, `[a~=v]`, `[a|=v]`, `[a^=v]`, `[a$=v]`,
16//!   `[a*=v]`, with quoted or unquoted values and the ` i` / ` s` case flags.
17//!   As in CSS, an empty value never matches `^=`, `$=`, `*=` or `~=`.
18//!   Unquoted values may contain anything but whitespace, quotes and brackets
19//!   (a lenient superset of CSS, so `a[href=/about]` works).
20//! - **Structural pseudo-classes:** `:first-child`, `:last-child`,
21//!   `:only-child`, `:nth-child(An+B [of S])`, `:nth-last-child(An+B [of S])`,
22//!   `:first-of-type`, `:last-of-type`, `:only-of-type`, `:nth-of-type(An+B)`,
23//!   `:nth-last-of-type(An+B)`, `:empty`, `:root`, `:scope`. `An+B` accepts
24//!   `odd`, `even`, `n`, `-n+3`, `2n + 1`, `5`, ….
25//! - **Logical pseudo-classes:** `:not(list)`, `:is(list)`, `:where(list)`,
26//!   and `:has(relative list)` (`:has(img)`, `:has(> img)`, `:has(+ p)`,
27//!   `:has(~ p)`).
28//! - **Form state** (from attributes, as written in the markup): `:checked`
29//!   (`<input checked>`, `<option selected>`), `:disabled`, `:enabled`.
30//! - **`:contains("text")`** — *non-standard*, jQuery/cheerio-style scraping
31//!   extension: the element's [`text_content`](NodeRef::text_content) contains
32//!   the given text (case-sensitive substring; quotes optional).
33//! - **CSS escapes** in names and strings: `.md\:flex`, `#a\.b`, `.\31 0`
34//!   (hex), so Tailwind-style class names work.
35//!
36//! ## Matching rules
37//!
38//! - Tag names, attribute names and pseudo-class names are ASCII
39//!   case-insensitive; attribute values, ids and classes are case-sensitive
40//!   (unless the attribute selector has the ` i` flag). As in browsers, the
41//!   values of HTML's enumerated attributes (the HTML spec's list: `type`,
42//!   `method`, `dir`, `rel`, `lang`, `target`, …) compare case-insensitively
43//!   unless the selector says ` s`, so `[type=text]` matches `type=TEXT`.
44//! - Class lists split on ASCII whitespace only, as in HTML.
45//! - Structural pseudo-classes and sibling combinators count **elements only**;
46//!   text and comment nodes are skipped.
47//! - Top-level nodes have no parent but are siblings of each other (as if
48//!   children of an implicit document node), so `li + li` and `:first-child`
49//!   work on fragments. Every top-level element is `:root`.
50//! - `:scope` is the element a query runs on ([`NodeRef::select`],
51//!   [`NodeRef::matches`], [`NodeRef::closest`]); for [`Dom::select`] and
52//!   [`Selector::matches`] it is the same as `:root`.
53//!
54//! Matching never recurses on document depth and never backtracks
55//! exponentially. Within one query ([`Dom::select`], [`NodeRef::select`],
56//! [`NodeRef::closest`]) it memoizes `:nth-*` positions (including
57//! `of S`, nested or not) and the nearest-match searches behind descendant and
58//! `~` combinators, so selectors like `li:nth-child(odd)`, `h2 ~ p` or
59//! `div span` stay roughly linear on 50k-wide lists and 100k-deep nesting.
60//! The memo grows with what a query walks, so a query on one node or a small
61//! subtree costs the same late in a big document as early on (a loop of
62//! `row.select(..)` over every row stays linear), and its memory is bounded
63//! by the size of the document plus the length of the selector, never their
64//! product.
65//! Exceptions: `:has()` and `:contains()` scan each candidate's subtree, and
66//! a lone [`Selector::matches`] call starts with an empty memo (prefer
67//! `select` for bulk work).
68//!
69//! ## Deliberately missing
70//!
71//! Pseudo-elements (`::before`), dynamic and UI pseudo-classes (`:hover`,
72//! `:focus`, `:visited`, …), `:lang()`, `:dir()`, namespaces (`svg|rect`), the
73//! column combinator `||`, `/* comments */`, and `:has()` nested inside
74//! `:has()` (also invalid CSS). These are parse **errors** — they match nothing
75//! rather than being silently ignored. Arguments nest at most 32 levels deep.
76
77use alloc::borrow::Cow;
78use alloc::string::String;
79use alloc::vec::Vec;
80use core::cell::{Cell, RefCell};
81use core::fmt;
82use core::str::FromStr;
83
84use crate::node::NodeId;
85use crate::tree::{Dom, NodeRef};
86
87/// How deep `:not(:is(...))`-style arguments may nest before it's an error.
88const MAX_DEPTH: u32 = 32;
89
90// --- public API ----------------------------------------------------------
91
92/// A parsed, reusable CSS selector list.
93///
94/// Parse once, match many times: pass `&selector` anywhere a selector string
95/// is accepted ([`Dom::select`], [`NodeRef::closest`], …).
96///
97/// Supported: type, `*`, `#id`, `.class`, attribute selectors (`=`, `~=`,
98/// `|=`, `^=`, `$=`, `*=`, with ` i`/` s` flags), selector lists, all four
99/// combinators (descendant, `>`, `+`, `~`), `:first-child`, `:last-child`,
100/// `:only-child`, `:nth-child(An+B [of S])`, `:nth-last-child()`, the
101/// `*-of-type` variants, `:empty`, `:root`, `:scope`, `:not()`, `:is()`, `:where()`,
102/// `:has()` (relative: `:has(> img)`, `:has(+ p)`), `:checked`, `:disabled`,
103/// `:enabled`, CSS escapes (`.md\:flex`), and the non-standard jQuery-style
104/// `:contains("text")` (case-sensitive substring of the text content).
105/// Tag, attribute and pseudo-class names are ASCII case-insensitive; ids,
106/// classes and attribute values are case-sensitive, except the values of
107/// HTML's enumerated attributes (`type`, `method`, `dir`, `rel`, …), which
108/// compare case-insensitively as in browsers unless the selector says ` s`.
109/// Anything else (`:hover`, `::before`, namespaces, …) is a parse error, never
110/// silently ignored.
111///
112/// Sibling combinators and positional pseudo-classes count elements only.
113/// Top-level nodes count as siblings of each other, so `li + li` works on a
114/// fragment like `<li>a</li><li>b</li>`; each top-level element is `:root`.
115///
116/// ```
117/// use domtree::Selector;
118///
119/// let dom = domtree::parse("<ul><li class='on'>a</li><li>b</li></ul>");
120/// let sel = Selector::parse("li:not(.on)").unwrap();
121/// let hits: Vec<_> = dom.select(&sel).map(|n| n.text_content()).collect();
122/// assert_eq!(hits, ["b"]);
123///
124/// // `FromStr` works too.
125/// let sel: Selector = "ul > li".parse().unwrap();
126/// assert_eq!(dom.select(&sel).count(), 2);
127/// ```
128#[derive(Clone)]
129pub struct Selector {
130    list: Vec<Complex>,
131    source: String,
132}
133
134impl Selector {
135    /// Parse a selector list such as `"ul > li.active, a[href^='https:']"`.
136    ///
137    /// Anything outside the supported grammar is an error — including
138    /// pseudo-classes this engine doesn't implement (`:hover`, `::before`, …),
139    /// which are rejected rather than silently ignored.
140    ///
141    /// ```
142    /// use domtree::Selector;
143    ///
144    /// assert!(Selector::parse("div > p:first-child").is_ok());
145    ///
146    /// let err = Selector::parse("div > p)").unwrap_err();
147    /// assert_eq!(err.position(), 7);
148    /// assert_eq!(err.to_string(), "unexpected ')' at byte 7");
149    /// ```
150    pub fn parse(selector: &str) -> Result<Selector, SelectorError> {
151        let mut p = Parser {
152            src: selector,
153            pos: 0,
154            depth: 0,
155            in_has: false,
156            slots: SHARED_SLOTS,
157        };
158        let list = p.list(false)?;
159        match p.peek() {
160            None => Ok(Selector {
161                list,
162                source: selector.into(),
163            }),
164            Some(_) => p.unexpected(),
165        }
166    }
167
168    /// Whether `node` is an element matching this selector, evaluated against
169    /// the whole document `node` belongs to.
170    ///
171    /// ```
172    /// use domtree::Selector;
173    ///
174    /// let dom = domtree::parse("<div class='card'><h2>Title</h2></div>");
175    /// let h2 = dom.find_by_tag("h2").next().unwrap();
176    /// assert!(Selector::parse(".card > h2").unwrap().matches(h2));
177    /// assert!(!Selector::parse("h2:empty").unwrap().matches(h2));
178    /// ```
179    pub fn matches(&self, node: NodeRef<'_>) -> bool {
180        self.matches_in(node, None, &Memo::default())
181    }
182
183    /// Match with `:scope` set to `scope`, sharing `memo` across one query.
184    fn matches_in<'a>(&self, node: NodeRef<'a>, scope: Option<NodeId>, memo: &Memo<'a>) -> bool {
185        let cx = Ctx {
186            scope,
187            anchor: None,
188            memo,
189        };
190        any_matches(&self.list, node, cx)
191    }
192}
193
194/// Same as [`Selector::parse`].
195impl FromStr for Selector {
196    type Err = SelectorError;
197
198    fn from_str(s: &str) -> Result<Selector, SelectorError> {
199        Selector::parse(s)
200    }
201}
202
203impl fmt::Debug for Selector {
204    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
205        f.debug_tuple("Selector").field(&self.source).finish()
206    }
207}
208
209/// Why [`Selector::parse`] rejected a selector, and where.
210///
211/// ```
212/// let err = domtree::Selector::parse("a:hover").unwrap_err();
213/// assert_eq!(err.position(), 1);
214/// assert_eq!(err.message(), "unsupported pseudo-class ':hover'");
215/// assert_eq!(err.to_string(), "unsupported pseudo-class ':hover' at byte 1");
216/// ```
217#[derive(Debug, Clone, PartialEq, Eq)]
218pub struct SelectorError {
219    position: usize,
220    message: String,
221}
222
223impl SelectorError {
224    /// Byte offset into the selector string where the problem was found.
225    ///
226    /// ```
227    /// let err = domtree::Selector::parse("ul >").unwrap_err();
228    /// assert_eq!(err.position(), 4);
229    /// ```
230    pub fn position(&self) -> usize {
231        self.position
232    }
233
234    /// A short, human-readable description of the problem.
235    ///
236    /// ```
237    /// let err = domtree::Selector::parse("p::before").unwrap_err();
238    /// assert_eq!(err.message(), "pseudo-elements are not supported");
239    /// ```
240    pub fn message(&self) -> &str {
241        &self.message
242    }
243}
244
245impl fmt::Display for SelectorError {
246    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
247        write!(f, "{} at byte {}", self.message, self.position)
248    }
249}
250
251#[cfg(feature = "std")]
252impl std::error::Error for SelectorError {}
253
254/// Anything the selector methods accept: a `&str` or `&String` (parsed on the
255/// spot) or an already-parsed `&`[`Selector`].
256///
257/// A string that fails to parse matches nothing; use [`Selector::parse`] when
258/// you want the error.
259///
260/// ```
261/// use domtree::{AsSelector, Selector};
262///
263/// let dom = domtree::parse("<p class='x'>hi</p>");
264/// let owned = String::from("p.x");
265/// let parsed = Selector::parse("p.x").unwrap();
266/// assert_eq!(dom.select("p.x").count(), 1);
267/// assert_eq!(dom.select(&owned).count(), 1);
268/// assert_eq!(dom.select(&parsed).count(), 1);
269///
270/// assert!("p:hover".as_selector().is_none()); // invalid: matches nothing
271/// assert_eq!(dom.select("p:hover").count(), 0);
272/// ```
273pub trait AsSelector {
274    /// The parsed selector, or `None` if this is not a valid selector.
275    fn as_selector(&self) -> Option<Cow<'_, Selector>>;
276}
277
278impl AsSelector for str {
279    fn as_selector(&self) -> Option<Cow<'_, Selector>> {
280        Selector::parse(self).ok().map(Cow::Owned)
281    }
282}
283
284impl AsSelector for String {
285    fn as_selector(&self) -> Option<Cow<'_, Selector>> {
286        self.as_str().as_selector()
287    }
288}
289
290impl AsSelector for Selector {
291    fn as_selector(&self) -> Option<Cow<'_, Selector>> {
292        Some(Cow::Borrowed(self))
293    }
294}
295
296impl Dom {
297    /// Every element matching a CSS selector, in document order.
298    ///
299    /// `selector` may be a `&str`, a `&String` or a pre-parsed `&`[`Selector`]
300    /// (see [`AsSelector`]). It is parsed once per call; an invalid selector
301    /// string matches nothing (use [`Selector::parse`] to see the error).
302    ///
303    /// ```
304    /// let dom = domtree::parse(
305    ///     "<div><ul><li class='x'>a</li><li>b</li></ul><ol><li>c</li></ol></div>",
306    /// );
307    /// assert_eq!(dom.select("ul li.x").count(), 1); // descendant + class
308    /// assert_eq!(dom.select("ol > li").count(), 1); // child combinator
309    /// assert_eq!(dom.select("ul, ol").count(), 2); // selector list
310    /// assert_eq!(dom.select("li:nth-child(2)").count(), 1);
311    /// assert_eq!(dom.select("ul:has(.x) + ol").count(), 1);
312    /// ```
313    pub fn select<'a, S: AsSelector + ?Sized>(
314        &'a self,
315        selector: &S,
316    ) -> impl Iterator<Item = NodeRef<'a>> {
317        let sel = selector.as_selector();
318        let memo = Memo {
319            dense: self.len(),
320            ..Memo::default()
321        };
322        self.nodes()
323            .filter(move |n| sel.as_ref().is_some_and(|s| s.matches_in(*n, None, &memo)))
324    }
325
326    /// The first element (in document order) matching a CSS selector.
327    ///
328    /// ```
329    /// let dom = domtree::parse("<p>a</p><p class='x'>b</p><p class='x'>c</p>");
330    /// let p = dom.select_first("p.x").unwrap();
331    /// assert_eq!(p.text_content(), "b");
332    /// assert!(dom.select_first("table").is_none());
333    /// ```
334    pub fn select_first<S: AsSelector + ?Sized>(&self, selector: &S) -> Option<NodeRef<'_>> {
335        self.select(selector).next()
336    }
337}
338
339impl<'a> NodeRef<'a> {
340    /// Descendants of this node (not the node itself) matching `selector`, in
341    /// document order.
342    ///
343    /// Like the DOM's `querySelectorAll`, the selector is matched against the
344    /// whole document — only the *results* are limited to this subtree. So
345    /// `"div p"` also finds a `<p>` whose `<div>` is an ancestor of this node.
346    /// Use `:scope` to anchor at this node: `":scope > li"` is its `<li>`
347    /// children.
348    ///
349    /// ```
350    /// let dom = domtree::parse("<div><ul><li>a<ul><li>b</li></ul></li></ul></div>");
351    /// let ul = dom.select_first("ul").unwrap();
352    /// assert_eq!(ul.select("li").count(), 2);
353    /// assert_eq!(ul.select(":scope > li").count(), 1);
354    /// assert_eq!(ul.select("div li").count(), 2); // the <div> is outside `ul`
355    /// ```
356    pub fn select<'s, S: AsSelector + ?Sized>(
357        &self,
358        selector: &'s S,
359    ) -> impl Iterator<Item = NodeRef<'a>> + use<'a, 's, S> {
360        let sel = selector.as_selector();
361        let (scope, memo) = (Some(self.id()), Memo::default());
362        self.descendants()
363            .filter(move |n| sel.as_ref().is_some_and(|s| s.matches_in(*n, scope, &memo)))
364    }
365
366    /// The first descendant (in document order) matching `selector`; see
367    /// [`NodeRef::select`].
368    ///
369    /// ```
370    /// let dom = domtree::parse("<table><tr><td>1</td><td>2</td></tr></table>");
371    /// let row = dom.select_first("tr").unwrap();
372    /// assert_eq!(row.select_first("td:last-child").unwrap().text_content(), "2");
373    /// ```
374    pub fn select_first<S: AsSelector + ?Sized>(&self, selector: &S) -> Option<NodeRef<'a>> {
375        self.select(selector).next()
376    }
377
378    /// Whether this node is an element matching `selector` (DOM
379    /// `Element.matches`). An invalid selector matches nothing.
380    ///
381    /// ```
382    /// let dom = domtree::parse("<ul><li class='a'>x</li></ul>");
383    /// let li = dom.select_first("li").unwrap();
384    /// assert!(li.matches("ul > li.a"));
385    /// assert!(!li.matches("ol li"));
386    /// ```
387    pub fn matches<S: AsSelector + ?Sized>(&self, selector: &S) -> bool {
388        selector
389            .as_selector()
390            .is_some_and(|s| s.matches_in(*self, Some(self.id()), &Memo::default()))
391    }
392
393    /// The nearest element matching `selector`, starting with this node itself
394    /// and walking up its ancestors (DOM `Element.closest`).
395    ///
396    /// ```
397    /// let dom = domtree::parse("<div class='card'><p><b>hi</b></p></div>");
398    /// let b = dom.select_first("b").unwrap();
399    /// assert_eq!(b.closest(".card").unwrap().tag_name(), Some("div"));
400    /// assert_eq!(b.closest("b").unwrap().id(), b.id()); // self-inclusive
401    /// assert!(b.closest("table").is_none());
402    /// ```
403    pub fn closest<S: AsSelector + ?Sized>(&self, selector: &S) -> Option<NodeRef<'a>> {
404        let sel = selector.as_selector()?;
405        let (scope, memo) = (Some(self.id()), Memo::default());
406        core::iter::once(*self)
407            .chain(self.ancestors())
408            .find(|n| sel.matches_in(*n, scope, &memo))
409    }
410}
411
412// --- representation ------------------------------------------------------
413
414/// One complex selector (`ul > li a`), stored **right to left**:
415/// `compounds[0]` is the subject, and `combs[i]` links `compounds[i]` to the
416/// compound on its left, `compounds[i + 1]`.
417#[derive(Clone)]
418struct Complex {
419    compounds: Vec<Compound>,
420    combs: Vec<Comb>,
421    /// First [`Memo`] slot of this selector's searches (one per combinator).
422    slot: u32,
423    /// A `:has()` argument: matches depend on the `:has()` subject, so its
424    /// searches aren't memoized.
425    relative: bool,
426}
427
428#[derive(Clone, Copy, PartialEq, Eq)]
429enum Comb {
430    Descendant,
431    Child,
432    Next,
433    Later,
434}
435
436#[derive(Clone)]
437struct Compound {
438    /// Lower-cased type selector; `None` for `*` or no type.
439    tag: Option<String>,
440    /// Ordered cheapest-first, so `tag.class` rejections short-circuit before
441    /// anything expensive (`:has`, `:contains`, …) runs.
442    simples: Vec<Simple>,
443}
444
445#[derive(Clone)]
446enum Simple {
447    Id(String),
448    Class(String),
449    Attr {
450        name: String,
451        op: AttrOp,
452        value: String,
453        ci: bool,
454    },
455    Nth(Nth),
456    /// `:only-child` (`false`) / `:only-of-type` (`true`).
457    Only(bool),
458    Empty,
459    Root,
460    Scope,
461    /// The element a `:has()` argument is relative to (internal only).
462    Anchor,
463    Checked,
464    Disabled,
465    Enabled,
466    Not(Vec<Complex>),
467    Is(Vec<Complex>),
468    Has(Vec<Complex>),
469    Contains(String),
470}
471
472#[derive(Clone, Copy)]
473enum AttrOp {
474    Exists,
475    Equals,
476    Includes,
477    Dash,
478    Prefix,
479    Suffix,
480    Substring,
481}
482
483/// `:nth-*(An+B)` and the `:first-*` / `:last-*` shorthands (`0n+1`).
484#[derive(Clone)]
485struct Nth {
486    a: i32,
487    b: i32,
488    last: bool,
489    of_type: bool,
490    /// `:nth-child(An+B of S)`; empty when absent.
491    of: Vec<Complex>,
492    /// [`Memo`] slot for positions (and `of S` misses); without `of S`, one
493    /// of the [`SHARED_SLOTS`].
494    slot: u32,
495}
496
497/// [`Memo`] slots shared by every `:nth-*` without `of S` of the same kind
498/// (from the end or not, `*-of-type` or not): an element's position doesn't
499/// depend on `An+B`, so a thousand `:nth-child()`s need one table, not a
500/// thousand.
501const SHARED_SLOTS: u32 = 4;
502
503fn shared_slot(last: bool, of_type: bool) -> u32 {
504    u32::from(last) | u32::from(of_type) << 1
505}
506
507impl Simple {
508    fn cost(&self) -> u8 {
509        match self {
510            Simple::Id(_) | Simple::Class(_) => 0,
511            Simple::Not(_) | Simple::Is(_) => 2,
512            Simple::Has(_) | Simple::Contains(_) => 3,
513            _ => 1,
514        }
515    }
516}
517
518// --- parsing -------------------------------------------------------------
519
520/// A recursive-descent parser over the selector string. Recursion only
521/// happens for nested arguments (`:not(...)`), bounded by [`MAX_DEPTH`].
522struct Parser<'s> {
523    src: &'s str,
524    pos: usize,
525    depth: u32,
526    in_has: bool,
527    /// [`Memo`] slots handed out so far.
528    slots: u32,
529}
530
531impl<'s> Parser<'s> {
532    fn rest(&self) -> &'s str {
533        self.src.get(self.pos..).unwrap_or("")
534    }
535
536    fn peek(&self) -> Option<char> {
537        self.rest().chars().next()
538    }
539
540    fn bump(&mut self) -> Option<char> {
541        let c = self.peek()?;
542        self.pos += c.len_utf8();
543        Some(c)
544    }
545
546    fn eat(&mut self, c: char) -> bool {
547        let hit = self.peek() == Some(c);
548        if hit {
549            self.pos += c.len_utf8();
550        }
551        hit
552    }
553
554    /// Skip CSS whitespace; returns whether there was any.
555    fn ws(&mut self) -> bool {
556        let start = self.pos;
557        while self.peek().is_some_and(|c| c.is_ascii_whitespace()) {
558            self.pos += 1;
559        }
560        self.pos > start
561    }
562
563    /// Reserve `n` consecutive [`Memo`] slots; returns the first.
564    fn reserve(&mut self, n: usize) -> u32 {
565        let first = self.slots;
566        let n = u32::try_from(n).unwrap_or(u32::MAX);
567        self.slots = first.saturating_add(n);
568        first
569    }
570
571    fn error<T>(&self, at: usize, message: impl Into<String>) -> Result<T, SelectorError> {
572        Err(SelectorError {
573            position: at,
574            message: message.into(),
575        })
576    }
577
578    fn unexpected<T>(&self) -> Result<T, SelectorError> {
579        match self.peek() {
580            Some(c) => self.error(
581                self.pos,
582                ["unexpected '", c.encode_utf8(&mut [0; 4]), "'"].concat(),
583            ),
584            None => self.error(self.pos, "unexpected end of selector"),
585        }
586    }
587
588    /// `complex (',' complex)*`, stopping before a `)` or the end.
589    fn list(&mut self, relative: bool) -> Result<Vec<Complex>, SelectorError> {
590        let mut list = Vec::new();
591        loop {
592            list.push(self.complex(relative)?);
593            if !self.eat(',') {
594                return Ok(list);
595            }
596        }
597    }
598
599    /// A list nested in a pseudo-class argument, with the depth bounded.
600    fn nested(&mut self, relative: bool) -> Result<Vec<Complex>, SelectorError> {
601        if self.depth >= MAX_DEPTH {
602            return self.error(self.pos, "selectors nested too deeply");
603        }
604        self.depth += 1;
605        let list = self.list(relative);
606        self.depth -= 1;
607        list
608    }
609
610    fn combinator(&mut self) -> Option<Comb> {
611        let comb = match self.peek()? {
612            '>' => Comb::Child,
613            '+' => Comb::Next,
614            '~' => Comb::Later,
615            _ => return None,
616        };
617        self.pos += 1;
618        self.ws();
619        Some(comb)
620    }
621
622    fn complex(&mut self, relative: bool) -> Result<Complex, SelectorError> {
623        self.ws();
624        let mut compounds = Vec::new();
625        let mut combs = Vec::new();
626        if relative {
627            // `:has(> img)` is `<anchor> > img`; a bare `:has(img)` is a
628            // descendant of the anchor.
629            compounds.push(Compound {
630                tag: None,
631                simples: alloc::vec![Simple::Anchor],
632            });
633            combs.push(self.combinator().unwrap_or(Comb::Descendant));
634        }
635        loop {
636            compounds.push(self.compound()?);
637            let spaced = self.ws();
638            let comb = match self.combinator() {
639                Some(comb) => comb,
640                None => match self.peek() {
641                    None | Some(',' | ')') => break,
642                    Some(_) if spaced => Comb::Descendant,
643                    Some(_) => return self.unexpected(),
644                },
645            };
646            combs.push(comb);
647        }
648        compounds.reverse();
649        combs.reverse();
650        Ok(Complex {
651            slot: self.reserve(combs.len()),
652            compounds,
653            combs,
654            relative,
655        })
656    }
657
658    fn compound(&mut self) -> Result<Compound, SelectorError> {
659        let start = self.pos;
660        let mut tag = None;
661        if !self.eat('*') && self.peek().is_some_and(|c| c == '\\' || is_name_char(c)) {
662            tag = Some(self.name()?.to_ascii_lowercase());
663        }
664        let mut simples: Vec<Simple> = Vec::new();
665        loop {
666            let at = self.pos;
667            let simple = match self.peek() {
668                Some('#') => {
669                    self.pos += 1;
670                    Simple::Id(self.name()?)
671                }
672                Some('.') => {
673                    self.pos += 1;
674                    Simple::Class(self.name()?)
675                }
676                Some('[') => {
677                    self.pos += 1;
678                    self.attribute()?
679                }
680                Some(':') => {
681                    self.pos += 1;
682                    self.pseudo(at)?
683                }
684                _ => break,
685            };
686            // Insertion sort by cost (keeps the code tiny; compounds are short).
687            let i = simples
688                .iter()
689                .position(|s| s.cost() > simple.cost())
690                .unwrap_or(simples.len());
691            simples.insert(i, simple);
692        }
693        if self.pos == start {
694            return match self.peek() {
695                None => self.error(start, "expected a selector"),
696                Some(_) => self.unexpected(),
697            };
698        }
699        Ok(Compound { tag, simples })
700    }
701
702    /// An identifier-ish name: name characters plus CSS escapes.
703    fn name(&mut self) -> Result<String, SelectorError> {
704        let mut out = String::new();
705        while let Some(c) = self.peek() {
706            if c == '\\' {
707                self.pos += 1;
708                out.push(self.escape());
709            } else if is_name_char(c) {
710                self.pos += c.len_utf8();
711                out.push(c);
712            } else {
713                break;
714            }
715        }
716        if out.is_empty() {
717            return self.unexpected();
718        }
719        Ok(out)
720    }
721
722    /// Decode a CSS escape whose `\` was just consumed: 1–6 hex digits (plus
723    /// one optional trailing space), or any other character taken literally.
724    fn escape(&mut self) -> char {
725        let start = self.pos;
726        let mut code = 0u32;
727        while self.pos - start < 6 {
728            match self.peek().and_then(|c| c.to_digit(16)) {
729                Some(d) => {
730                    code = code * 16 + d;
731                    self.pos += 1;
732                }
733                None => break,
734            }
735        }
736        if self.pos > start {
737            if self.peek().is_some_and(|c| c.is_ascii_whitespace()) {
738                self.pos += 1;
739            }
740            return char::from_u32(code)
741                .filter(|&c| c != '\0')
742                .unwrap_or('\u{FFFD}');
743        }
744        self.bump().unwrap_or('\u{FFFD}')
745    }
746
747    /// A `"…"` or `'…'` string starting at the current position.
748    fn string(&mut self) -> Result<String, SelectorError> {
749        let start = self.pos;
750        let quote = self.bump();
751        let mut out = String::new();
752        loop {
753            match self.bump() {
754                None => return self.error(start, "unterminated string"),
755                Some('\\') => match self.peek() {
756                    Some('\n') => self.pos += 1, // line continuation
757                    Some(_) => out.push(self.escape()),
758                    None => {}
759                },
760                Some(c) if Some(c) == quote => return Ok(out),
761                Some(c) => out.push(c),
762            }
763        }
764    }
765
766    /// `[name]`, `[name op value flag?]`; the `[` was just consumed.
767    fn attribute(&mut self) -> Result<Simple, SelectorError> {
768        const OPS: [(&str, AttrOp); 6] = [
769            ("=", AttrOp::Equals),
770            ("~=", AttrOp::Includes),
771            ("|=", AttrOp::Dash),
772            ("^=", AttrOp::Prefix),
773            ("$=", AttrOp::Suffix),
774            ("*=", AttrOp::Substring),
775        ];
776        self.ws();
777        let name = self.name()?.to_ascii_lowercase();
778        self.ws();
779        if self.eat(']') {
780            return Ok(Simple::Attr {
781                name,
782                op: AttrOp::Exists,
783                value: String::new(),
784                ci: false,
785            });
786        }
787        let rest = self.rest();
788        let Some(&(token, op)) = OPS.iter().find(|(t, _)| rest.starts_with(t)) else {
789            return self.unexpected();
790        };
791        self.pos += token.len();
792        self.ws();
793        let value = match self.peek() {
794            Some('"' | '\'') => self.string()?,
795            _ => self.unquoted()?,
796        };
797        self.ws();
798        // As in browsers, HTML's enumerated attributes compare
799        // case-insensitively unless the selector says ` s`.
800        let mut ci = CASE_INSENSITIVE_ATTRS.contains(&name.as_str());
801        if let Some(flag @ ('i' | 'I' | 's' | 'S')) = self.peek() {
802            self.pos += 1;
803            ci = flag.eq_ignore_ascii_case(&'i');
804            self.ws();
805        }
806        if !self.eat(']') {
807            return self.unexpected();
808        }
809        Ok(Simple::Attr {
810            name,
811            op,
812            value,
813            ci,
814        })
815    }
816
817    /// An unquoted attribute value: anything up to whitespace, a quote or a
818    /// bracket (more lenient than CSS identifiers, e.g. `[href=/a/b]`).
819    fn unquoted(&mut self) -> Result<String, SelectorError> {
820        let mut out = String::new();
821        while let Some(c) = self.peek() {
822            if c.is_ascii_whitespace() || matches!(c, '[' | ']' | '"' | '\'') {
823                break;
824            }
825            self.pos += c.len_utf8();
826            if c == '\\' {
827                out.push(self.escape());
828            } else {
829                out.push(c);
830            }
831        }
832        if out.is_empty() {
833            return self.unexpected();
834        }
835        Ok(out)
836    }
837
838    /// A pseudo-class; `at` is the byte offset of its `:` (already consumed).
839    fn pseudo(&mut self, at: usize) -> Result<Simple, SelectorError> {
840        if self.peek() == Some(':') {
841            return self.error(at, "pseudo-elements are not supported");
842        }
843        let name = self.name()?.to_ascii_lowercase();
844        if !self.eat('(') {
845            let nth = |last, of_type| {
846                Simple::Nth(Nth {
847                    a: 0,
848                    b: 1,
849                    last,
850                    of_type,
851                    of: Vec::new(),
852                    slot: shared_slot(last, of_type),
853                })
854            };
855            return Ok(match name.as_str() {
856                "first-child" => nth(false, false),
857                "last-child" => nth(true, false),
858                "first-of-type" => nth(false, true),
859                "last-of-type" => nth(true, true),
860                "only-child" => Simple::Only(false),
861                "only-of-type" => Simple::Only(true),
862                "empty" => Simple::Empty,
863                "root" => Simple::Root,
864                "scope" => Simple::Scope,
865                "checked" => Simple::Checked,
866                "disabled" => Simple::Disabled,
867                "enabled" => Simple::Enabled,
868                _ => return self.error(at, ["unsupported pseudo-class ':", &name, "'"].concat()),
869            });
870        }
871        self.ws();
872        let simple = match name.as_str() {
873            "not" => Simple::Not(self.nested(false)?),
874            "is" | "where" => Simple::Is(self.nested(false)?),
875            "has" if self.in_has => return self.error(at, ":has() cannot be nested"),
876            "has" => {
877                self.in_has = true;
878                let list = self.nested(true);
879                self.in_has = false;
880                Simple::Has(list?)
881            }
882            "contains" => Simple::Contains(self.contains_arg()?),
883            "nth-child" | "nth-last-child" | "nth-of-type" | "nth-last-of-type" => {
884                self.nth(&name)?
885            }
886            _ => return self.error(at, ["unsupported pseudo-class ':", &name, "()'"].concat()),
887        };
888        self.ws();
889        if !self.eat(')') {
890            return self.unexpected();
891        }
892        Ok(simple)
893    }
894
895    /// `:contains("text")`, `:contains('text')` or `:contains(text)`.
896    fn contains_arg(&mut self) -> Result<String, SelectorError> {
897        if let Some('"' | '\'') = self.peek() {
898            return self.string();
899        }
900        let rest = self.rest();
901        let end = rest.find(')').unwrap_or(rest.len());
902        let text = rest.get(..end).unwrap_or("").trim_end();
903        if text.is_empty() {
904            return self.unexpected();
905        }
906        self.pos += text.len();
907        Ok(text.into())
908    }
909
910    fn nth(&mut self, name: &str) -> Result<Simple, SelectorError> {
911        let (a, b) = self.an_plus_b()?;
912        let of_type = name.ends_with("of-type");
913        let mut of = Vec::new();
914        if !of_type
915            && self.ws()
916            && self
917                .rest()
918                .get(..2)
919                .is_some_and(|k| k.eq_ignore_ascii_case("of"))
920        {
921            self.pos += 2;
922            if !self.ws() {
923                return self.unexpected();
924            }
925            of = self.nested(false)?;
926        }
927        let last = name.starts_with("nth-last");
928        Ok(Simple::Nth(Nth {
929            a,
930            b,
931            last,
932            of_type,
933            slot: if of.is_empty() {
934                shared_slot(last, of_type)
935            } else {
936                self.reserve(1)
937            },
938            of,
939        }))
940    }
941
942    /// `odd`, `even`, `B`, `An`, `An+B` (signs and whitespace as in CSS).
943    fn an_plus_b(&mut self) -> Result<(i32, i32), SelectorError> {
944        let start = self.pos;
945        for (word, ab) in [("odd", (2, 1)), ("even", (2, 0))] {
946            if self
947                .rest()
948                .get(..word.len())
949                .is_some_and(|w| w.eq_ignore_ascii_case(word))
950            {
951                self.pos += word.len();
952                return Ok(ab);
953            }
954        }
955        let neg = self.eat('-');
956        if !neg {
957            self.eat('+');
958        }
959        let digits = self.integer()?;
960        if !(self.eat('n') || self.eat('N')) {
961            return match digits {
962                Some(b) => Ok((0, if neg { -b } else { b })),
963                None => self.error(start, "invalid :nth-*() argument"),
964            };
965        }
966        let a = digits.unwrap_or(1);
967        let a = if neg { -a } else { a };
968        let before = self.pos;
969        self.ws();
970        let b = match self.peek() {
971            Some(sign @ ('+' | '-')) => {
972                self.pos += 1;
973                self.ws();
974                match self.integer()? {
975                    Some(b) if sign == '-' => -b,
976                    Some(b) => b,
977                    None => return self.unexpected(),
978                }
979            }
980            _ => {
981                self.pos = before; // leave the space for a following `of S`
982                0
983            }
984        };
985        Ok((a, b))
986    }
987
988    fn integer(&mut self) -> Result<Option<i32>, SelectorError> {
989        let start = self.pos;
990        let mut n: i32 = 0;
991        while let Some(d) = self.peek().and_then(|c| c.to_digit(10)) {
992            n = match n.checked_mul(10).and_then(|n| n.checked_add(d as i32)) {
993                Some(n) => n,
994                None => return self.error(start, "number too large"),
995            };
996            self.pos += 1;
997        }
998        Ok((self.pos > start).then_some(n))
999    }
1000}
1001
1002/// Attributes whose values HTML matches ASCII case-insensitively in selectors
1003/// (the HTML spec's list, "case-sensitivity of selectors").
1004#[rustfmt::skip]
1005const CASE_INSENSITIVE_ATTRS: &[&str] = &[
1006    "accept", "accept-charset", "align", "alink", "axis", "bgcolor", "charset", "checked", "clear",
1007    "codetype", "color", "compact", "declare", "defer", "dir", "direction", "disabled", "enctype",
1008    "face", "frame", "hreflang", "http-equiv", "lang", "language", "link", "media", "method",
1009    "multiple", "nohref", "noresize", "noshade", "nowrap", "readonly", "rel", "rev", "rules",
1010    "scope", "scrolling", "selected", "shape", "target", "text", "type", "valign", "valuetype",
1011    "vlink",
1012];
1013
1014fn is_name_char(c: char) -> bool {
1015    c.is_ascii_alphanumeric() || c == '-' || c == '_' || !c.is_ascii()
1016}
1017
1018// --- matching ------------------------------------------------------------
1019
1020/// Per-query context.
1021#[derive(Clone, Copy)]
1022struct Ctx<'m, 'a> {
1023    /// What `:scope` matches (`None`: the document, i.e. top-level elements).
1024    scope: Option<NodeId>,
1025    /// The subject of the `:has()` currently being evaluated.
1026    anchor: Option<NodeId>,
1027    memo: &'m Memo<'a>,
1028}
1029
1030/// Per-query memo, so one query stays roughly linear on wide and deep
1031/// documents instead of re-walking the same siblings or ancestors for every
1032/// candidate. Each `:nth-*` and each combinator owns a slot (numbered at parse
1033/// time) with an entry per node, filled in on demand. Entries depend only on
1034/// the selector, the document and `:scope`, all fixed for one query, so one
1035/// that is missing is recomputed, never wrong.
1036///
1037/// Entries live in [`Sparse`] hash maps, which grow with what they hold (so a
1038/// query on one node or a small subtree of a big document costs what it
1039/// walks, not the document's size) and drop their oldest entries past a size
1040/// proportional to the document (so memory stays bounded, whatever the
1041/// selector). [`Dom::select`], which visits every node, keeps `:nth-*`
1042/// positions in tables indexed by node instead, for the first [`DENSE`] slots.
1043#[derive(Default)]
1044struct Memo<'a> {
1045    /// The document's node count for [`Dom::select`] (dense tables), else 0.
1046    dense: usize,
1047    /// Slot `s`'s entry for node `i` is at `s * dense + i`.
1048    tables: RefCell<Vec<u32>>,
1049    map: RefCell<Sparse<'a>>,
1050    /// Map lookups so far; nothing goes in the map before [`WARMUP`].
1051    lookups: Cell<u32>,
1052}
1053
1054/// Entry: not computed yet (or an empty [`Map`] bucket).
1055const UNKNOWN: u32 = 0;
1056/// Entry: a search found nothing / an element fails `:nth-*(… of S)`.
1057const NONE: u32 = u32::MAX;
1058/// Dense tables per query, at most (4 bytes per node each).
1059const DENSE: u32 = 16;
1060/// Entries per node in the newer [`Sparse`] map before it is retired.
1061const SPARSE: usize = 2;
1062/// Map lookups before anything is stored: most queries on one node or a small
1063/// subtree are done by then, without allocating, and the rest lose little.
1064const WARMUP: u32 = 32;
1065
1066/// A [`Map`] entry, with what a search found (the entry is then the index of
1067/// the compound it matched, plus 1).
1068type Entry<'a> = (u32, Option<NodeRef<'a>>);
1069
1070/// Open-addressing hash map from `(slot, node index)` to an [`Entry`], with
1071/// linear probing; never more than half full.
1072#[derive(Default)]
1073struct Map<'a> {
1074    buckets: Vec<((u32, u32), Entry<'a>)>,
1075    len: usize,
1076}
1077
1078impl<'a> Map<'a> {
1079    /// The bucket holding `key`, or the empty one where it would go.
1080    fn probe(&self, key: (u32, u32)) -> usize {
1081        let mask = self.buckets.len().wrapping_sub(1);
1082        let hash = (u64::from(key.0) << 32 | u64::from(key.1)).wrapping_mul(0x9E37_79B9_7F4A_7C15);
1083        let mut b = (hash >> 32) as usize & mask;
1084        for _ in 0..self.buckets.len() {
1085            match self.buckets.get(b) {
1086                Some(&(k, (v, _))) if v != UNKNOWN && k != key => b = (b + 1) & mask,
1087                _ => break,
1088            }
1089        }
1090        b
1091    }
1092
1093    fn get(&self, key: (u32, u32)) -> Entry<'a> {
1094        match self.buckets.get(self.probe(key)) {
1095            Some(&(k, entry)) if k == key => entry,
1096            _ => (UNKNOWN, None),
1097        }
1098    }
1099
1100    /// Store `entry` (never `UNKNOWN`).
1101    fn set(&mut self, key: (u32, u32), entry: Entry<'a>) {
1102        if 2 * (self.len + 1) > self.buckets.len() {
1103            let size = (2 * self.buckets.len()).max(16);
1104            let empty = ((0, 0), (UNKNOWN, None));
1105            let old = core::mem::replace(&mut self.buckets, alloc::vec![empty; size]);
1106            self.len = 0;
1107            for (k, e) in old {
1108                if e.0 != UNKNOWN {
1109                    self.set(k, e);
1110                }
1111            }
1112        }
1113        let b = self.probe(key);
1114        if let Some(bucket) = self.buckets.get_mut(b) {
1115            self.len += usize::from(bucket.1.0 == UNKNOWN);
1116            *bucket = (key, entry);
1117        }
1118    }
1119}
1120
1121/// Memo entries in two [`Map`]s, newest first. Once the first holds
1122/// [`SPARSE`] entries per node (up to the highest node index stored) plus a
1123/// few per slot, it replaces the second: memory stays bounded, and the newest
1124/// entries, which a query going through the document in order reuses, are
1125/// always kept.
1126#[derive(Default)]
1127struct Sparse<'a> {
1128    maps: [Map<'a>; 2],
1129    /// One past the highest node index stored.
1130    nodes: usize,
1131    /// One past the highest slot stored.
1132    slots: usize,
1133}
1134
1135impl<'a> Sparse<'a> {
1136    fn get(&self, key: (u32, u32)) -> Entry<'a> {
1137        let [new, old] = &self.maps;
1138        match new.get(key) {
1139            (UNKNOWN, _) => old.get(key),
1140            entry => entry,
1141        }
1142    }
1143
1144    fn set(&mut self, key: (u32, u32), entry: Entry<'a>) {
1145        self.nodes = self.nodes.max(key.1 as usize + 1);
1146        self.slots = self.slots.max(key.0 as usize + 1);
1147        if self.maps[0].len >= SPARSE * self.nodes + 4 * self.slots {
1148            self.maps[1] = core::mem::take(&mut self.maps[0]);
1149        }
1150        self.maps[0].set(key, entry);
1151    }
1152}
1153
1154impl<'a> Memo<'a> {
1155    /// Whether `slot` has a dense table.
1156    fn dense(&self, slot: u32) -> bool {
1157        self.dense > 0 && slot < DENSE
1158    }
1159
1160    /// A `:nth-*` entry.
1161    fn get(&self, slot: u32, node: NodeRef<'_>) -> u32 {
1162        if !self.dense(slot) {
1163            return self.find(slot, node).0;
1164        }
1165        let at = slot as usize * self.dense + node.id().index();
1166        let tables = self.tables.try_borrow();
1167        tables
1168            .ok()
1169            .and_then(|t| t.get(at).copied())
1170            .unwrap_or(UNKNOWN)
1171    }
1172
1173    /// Store a `:nth-*` entry (never `UNKNOWN`); returns whether it was stored.
1174    fn set(&self, slot: u32, node: NodeRef<'_>, value: u32) -> bool {
1175        if !self.dense(slot) {
1176            return self.store(slot, node, (value, None));
1177        }
1178        let at = slot as usize * self.dense + node.id().index();
1179        let Ok(mut tables) = self.tables.try_borrow_mut() else {
1180            return false;
1181        };
1182        if tables.len() <= at {
1183            tables.resize((slot as usize + 1) * self.dense, UNKNOWN);
1184        }
1185        tables.get_mut(at).map(|e| *e = value).is_some()
1186    }
1187
1188    /// A [`Sparse`] entry.
1189    fn find(&self, slot: u32, node: NodeRef<'_>) -> Entry<'a> {
1190        self.lookups.set(self.lookups.get().saturating_add(1));
1191        let map = self.map.try_borrow();
1192        map.map_or((UNKNOWN, None), |m| m.get((slot, node.id().index() as u32)))
1193    }
1194
1195    /// Store a [`Sparse`] entry (never `UNKNOWN`); returns whether it was.
1196    fn store(&self, slot: u32, node: NodeRef<'_>, entry: Entry<'a>) -> bool {
1197        self.lookups.get() >= WARMUP
1198            && self
1199                .map
1200                .try_borrow_mut()
1201                .map(|mut m| m.set((slot, node.id().index() as u32), entry))
1202                .is_ok()
1203    }
1204
1205    /// The first node from `start` on (following `next`) where `test`
1206    /// succeeds. With a `slot`, every node walked is recorded with the result,
1207    /// so a later search that reaches one of them stops right there.
1208    fn search(
1209        &self,
1210        slot: Option<u32>,
1211        start: Option<NodeRef<'a>>,
1212        next: impl Fn(NodeRef<'a>) -> Option<NodeRef<'a>>,
1213        test: impl Fn(NodeRef<'a>) -> Option<(NodeRef<'a>, usize)>,
1214    ) -> Option<(NodeRef<'a>, usize)> {
1215        let mut walked = 0;
1216        let mut cur = start;
1217        let found = loop {
1218            let Some(n) = cur else { break None };
1219            if let Some(slot) = slot {
1220                match self.find(slot, n) {
1221                    (NONE, _) => break None,
1222                    (UNKNOWN, _) | (_, None) => walked += 1,
1223                    (i, Some(el)) => break Some((el, i as usize - 1)),
1224                }
1225            }
1226            if let Some(hit) = test(n) {
1227                break Some(hit);
1228            }
1229            cur = next(n);
1230        };
1231        let entry = match found {
1232            None => (NONE, None),
1233            Some((el, i)) => match u32::try_from(i + 1) {
1234                Ok(v) if v != NONE => (v, Some(el)),
1235                _ => return found,
1236            },
1237        };
1238        // Walk again rather than keep a list (no allocation).
1239        if let Some(slot) = slot {
1240            for n in core::iter::successors(start, |n| next(*n)).take(walked) {
1241                self.store(slot, n, entry);
1242            }
1243        }
1244        found
1245    }
1246}
1247
1248fn any_matches<'a>(list: &[Complex], el: NodeRef<'a>, cx: Ctx<'_, 'a>) -> bool {
1249    list.iter().any(|c| c.matches(el, cx))
1250}
1251
1252// Matching runs right to left without backtracking across a descendant (or
1253// `~`) combinator: the selector is cut at descendant combinators into chunks,
1254// and each chunk at `~` into runs of `>`/`+` (which leave no choice). For each
1255// chunk (run) it suffices to take the *nearest* ancestor (preceding sibling)
1256// where it matches: a farther one only leaves fewer candidates for the rest
1257// (a chunk always ends the same number of `>` levels above where it started).
1258// Those nearest-match searches are memoized per starting node, so across a
1259// whole query each node is walked past about once per combinator. Everything
1260// loops; nothing recurses on the document.
1261impl Complex {
1262    fn matches<'a>(&self, el: NodeRef<'a>, cx: Ctx<'_, 'a>) -> bool {
1263        let Some((mut el, mut i)) = self.chunk(0, el, cx) else {
1264            return false;
1265        };
1266        // Any remaining combinator is a descendant one.
1267        while self.combs.get(i).is_some() {
1268            let up = |a: NodeRef<'a>| a.parent();
1269            let found = cx
1270                .memo
1271                .search(self.slot(i), el.parent(), up, |a| self.chunk(i + 1, a, cx));
1272            let Some(hit) = found else { return false };
1273            (el, i) = hit;
1274        }
1275        true
1276    }
1277
1278    /// Match compounds `i..` linked by `>`, `+` and `~`, anchored at `el`.
1279    /// Returns the element the last of them matched, and its index.
1280    fn chunk<'a>(
1281        &self,
1282        i: usize,
1283        el: NodeRef<'a>,
1284        cx: Ctx<'_, 'a>,
1285    ) -> Option<(NodeRef<'a>, usize)> {
1286        let (mut el, mut i) = self.run(i, el, cx)?;
1287        while self.combs.get(i) == Some(&Comb::Later) {
1288            let prev = |p: NodeRef<'a>| p.prev_element_sibling();
1289            (el, i) = cx
1290                .memo
1291                .search(self.slot(i), prev(el), prev, |p| self.run(i + 1, p, cx))?;
1292        }
1293        Some((el, i))
1294    }
1295
1296    /// Match compounds `i..` linked by `>` and `+`, anchored at `el`.
1297    fn run<'a>(
1298        &self,
1299        mut i: usize,
1300        mut el: NodeRef<'a>,
1301        cx: Ctx<'_, 'a>,
1302    ) -> Option<(NodeRef<'a>, usize)> {
1303        loop {
1304            if !self.compounds.get(i)?.matches(el, cx) {
1305                return None;
1306            }
1307            el = match self.combs.get(i) {
1308                Some(Comb::Child) => el.parent()?,
1309                Some(Comb::Next) => el.prev_element_sibling()?,
1310                _ => return Some((el, i)),
1311            };
1312            i += 1;
1313        }
1314    }
1315
1316    /// The memo slot for the search across combinator `i`; `None` (don't
1317    /// memoize) for `:has()` arguments, whose matches depend on the subject.
1318    fn slot(&self, i: usize) -> Option<u32> {
1319        if self.relative {
1320            return None;
1321        }
1322        u32::try_from(i).ok().and_then(|i| self.slot.checked_add(i))
1323    }
1324}
1325
1326impl Compound {
1327    fn matches<'a>(&self, el: NodeRef<'a>, cx: Ctx<'_, 'a>) -> bool {
1328        let Some(tag) = el.tag_name() else {
1329            return false;
1330        };
1331        self.tag
1332            .as_deref()
1333            .is_none_or(|t| t.eq_ignore_ascii_case(tag))
1334            && self.simples.iter().all(|s| s.matches(el, cx))
1335    }
1336}
1337
1338impl Simple {
1339    fn matches<'a>(&self, el: NodeRef<'a>, cx: Ctx<'_, 'a>) -> bool {
1340        match self {
1341            Simple::Id(id) => el.attr("id") == Some(id.as_str()),
1342            Simple::Class(class) => el.has_class(class),
1343            Simple::Attr {
1344                name,
1345                op,
1346                value,
1347                ci,
1348            } => el
1349                .attr(name)
1350                .is_some_and(|have| attr_matches(*op, have, value, *ci)),
1351            Simple::Nth(nth) => nth.matches(el, cx),
1352            Simple::Only(of_type) => {
1353                let same =
1354                    |s: NodeRef<'_>| s.is_element() && (!of_type || s.tag_name() == el.tag_name());
1355                !core::iter::successors(el.prev_sibling(), NodeRef::prev_sibling).any(same)
1356                    && !core::iter::successors(el.next_sibling(), NodeRef::next_sibling).any(same)
1357            }
1358            Simple::Empty => el
1359                .children()
1360                .all(|c| c.comment().is_some() || c.text() == Some("")),
1361            Simple::Root => el.parent().is_none(),
1362            Simple::Scope => match cx.scope {
1363                Some(id) => el.id() == id,
1364                None => el.parent().is_none(),
1365            },
1366            Simple::Anchor => cx.anchor == Some(el.id()),
1367            Simple::Checked => match el.tag_name() {
1368                Some("input") => el.has_attr("checked"),
1369                Some("option") => el.has_attr("selected"),
1370                _ => false,
1371            },
1372            Simple::Disabled => is_form_control(el) && el.has_attr("disabled"),
1373            Simple::Enabled => is_form_control(el) && !el.has_attr("disabled"),
1374            Simple::Not(list) => !any_matches(list, el, cx),
1375            Simple::Is(list) => any_matches(list, el, cx),
1376            Simple::Has(list) => has(list, el, cx),
1377            Simple::Contains(text) => el.text_content().contains(text.as_str()),
1378        }
1379    }
1380}
1381
1382impl Nth {
1383    fn matches<'a>(&self, el: NodeRef<'a>, cx: Ctx<'_, 'a>) -> bool {
1384        let Some(pos) = self.position(el, cx) else {
1385            return false;
1386        };
1387        let (a, d) = (i64::from(self.a), i64::from(pos) - i64::from(self.b));
1388        if a == 0 {
1389            d == 0
1390        } else {
1391            d % a == 0 && d / a >= 0
1392        }
1393    }
1394
1395    /// Whether sibling `s` counts towards `el`'s position: same type for
1396    /// `*-of-type`, matching `S` for `of S` (misses are memoized).
1397    fn counts<'a>(&self, s: NodeRef<'a>, el: NodeRef<'a>, cx: Ctx<'_, 'a>) -> bool {
1398        if self.of_type {
1399            return s.tag_name() == el.tag_name();
1400        }
1401        if self.of.is_empty() {
1402            return true;
1403        }
1404        match cx.memo.get(self.slot, s) {
1405            UNKNOWN => {
1406                let hit = any_matches(&self.of, s, cx);
1407                if !hit {
1408                    cx.memo.set(self.slot, s, NONE);
1409                }
1410                hit
1411            }
1412            known => known != NONE,
1413        }
1414    }
1415
1416    /// `el`'s 1-based position among the siblings that count (from the end if
1417    /// `last`), or `None` if `el` doesn't count itself or is already past what
1418    /// `An+B` can reach. Walks back only to the nearest sibling with a known
1419    /// position and records every position on the way, so over a whole query
1420    /// each sibling list is walked about once.
1421    fn position<'a>(&self, el: NodeRef<'a>, cx: Ctx<'_, 'a>) -> Option<u32> {
1422        match cx.memo.get(self.slot, el) {
1423            UNKNOWN => {}
1424            NONE => return None,
1425            pos => return Some(pos),
1426        }
1427        if !self.counts(el, el, cx) {
1428            return None;
1429        }
1430        let step = |n: NodeRef<'a>| {
1431            if self.last {
1432                n.next_sibling()
1433            } else {
1434                n.prev_sibling()
1435            }
1436        };
1437        let (mut walked, mut base) = (0u32, 0);
1438        let mut sib = step(el);
1439        while let Some(s) = sib {
1440            if s.is_element() && self.counts(s, el, cx) {
1441                match cx.memo.get(self.slot, s) {
1442                    UNKNOWN => walked += 1,
1443                    pos => {
1444                        base = pos;
1445                        break;
1446                    }
1447                }
1448                // With a <= 0 no position past b can match: stop counting.
1449                if self.a <= 0 && i64::from(walked) >= i64::from(self.b) {
1450                    return None;
1451                }
1452            }
1453            sib = step(s);
1454        }
1455        let pos = walked.checked_add(base)?.checked_add(1)?;
1456        // Walk again to record the positions (no list to allocate).
1457        if cx.memo.set(self.slot, el, pos) {
1458            let (mut p, mut sib) = (pos, step(el));
1459            while p > base + 1 {
1460                let Some(s) = sib else { break };
1461                if s.is_element() && self.counts(s, el, cx) {
1462                    p -= 1;
1463                    cx.memo.set(self.slot, s, p);
1464                }
1465                sib = step(s);
1466            }
1467        }
1468        Some(pos)
1469    }
1470}
1471
1472fn attr_matches(op: AttrOp, have: &str, want: &str, ci: bool) -> bool {
1473    let (have, want) = (have.as_bytes(), want.as_bytes());
1474    let eq = |s: &[u8]| {
1475        if ci {
1476            s.eq_ignore_ascii_case(want)
1477        } else {
1478            s == want
1479        }
1480    };
1481    match op {
1482        AttrOp::Exists => true,
1483        AttrOp::Equals => eq(have),
1484        AttrOp::Dash => {
1485            eq(have)
1486                || (have.get(want.len()) == Some(&b'-') && have.get(..want.len()).is_some_and(eq))
1487        }
1488        // An empty value never matches the remaining operators (and must not
1489        // reach `windows(0)`).
1490        _ if want.is_empty() => false,
1491        AttrOp::Includes => have.split(u8::is_ascii_whitespace).any(eq),
1492        AttrOp::Prefix => have.get(..want.len()).is_some_and(eq),
1493        AttrOp::Suffix => have
1494            .len()
1495            .checked_sub(want.len())
1496            .and_then(|i| have.get(i..))
1497            .is_some_and(eq),
1498        AttrOp::Substring => have.windows(want.len()).any(eq),
1499    }
1500}
1501
1502/// `:has(rel, …)`: some element relative to `el` matches one of `rels`, each
1503/// anchored at `el`. The candidates are scanned iteratively.
1504fn has<'a>(rels: &[Complex], el: NodeRef<'a>, cx: Ctx<'_, 'a>) -> bool {
1505    let cx = Ctx {
1506        anchor: Some(el.id()),
1507        ..cx
1508    };
1509    rels.iter().any(|rel| match rel.combs.last() {
1510        // `:has(+ x)` / `:has(~ x)`: later siblings, plus their subtrees when
1511        // the selector steps back down (`:has(+ ul > li)`).
1512        Some(Comb::Next | Comb::Later) => {
1513            let deep = rel
1514                .combs
1515                .iter()
1516                .any(|c| matches!(c, Comb::Child | Comb::Descendant));
1517            let reach = if rel.combs.iter().all(|c| *c == Comb::Next) {
1518                rel.combs.len()
1519            } else {
1520                usize::MAX
1521            };
1522            core::iter::successors(el.next_element_sibling(), NodeRef::next_element_sibling)
1523                .take(reach)
1524                .any(|s| {
1525                    rel.matches(s, cx) || (deep && s.descendants().any(|d| rel.matches(d, cx)))
1526                })
1527        }
1528        _ => el.descendants().any(|d| rel.matches(d, cx)),
1529    })
1530}
1531
1532fn is_form_control(el: NodeRef<'_>) -> bool {
1533    matches!(
1534        el.tag_name(),
1535        Some("button" | "input" | "select" | "textarea" | "optgroup" | "option" | "fieldset")
1536    )
1537}