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}