Skip to main content

rama_http/protocols/html/selector/
mod.rs

1//! Native CSS selector parsing and matching.
2//!
3//! This module provides a small, dependency-free CSS selector engine for
4//! the subset of selectors that can be evaluated while *streaming* HTML
5//! (no sibling lookahead, no full-document state). It is the foundation
6//! for rama's streaming HTML parser/rewriter, but is also usable on its
7//! own to match selectors against any tree that implements
8//! [`SelectorSubject`] — including the in-memory [`Dom`] provided here.
9//!
10//! # Supported selectors
11//!
12//! | Pattern | Meaning |
13//! | --- | --- |
14//! | `*` | any element |
15//! | `E` | an element of type `E` (ASCII case-insensitive) |
16//! | `E.cls` | an `E` with class `cls` |
17//! | `E#id` | an `E` with id `id` |
18//! | `E[a]` | an `E` with an `a` attribute |
19//! | `E[a=v]` | value exactly `v` |
20//! | `E[a~=v]` | whitespace-separated list containing `v` |
21//! | `E[a^=v]` `E[a$=v]` `E[a*=v]` | prefix / suffix / substring |
22//! | `E[a\|=v]` | `v` or `v-` prefix |
23//! | `E[a=v i]` / `E[a=v s]` | case-insensitive / case-sensitive value |
24//! | `E:nth-child(An+B)` `E:first-child` | structural position |
25//! | `E:nth-of-type(An+B)` `E:first-of-type` | structural position by type |
26//! | `E:not(s)` | negation of a combinator-free compound `s` |
27//! | `E F` / `E > F` | descendant / child combinator |
28//! | `a, b, c` | selector list (matches if any matches) |
29//!
30//! Sibling combinators (`+`, `~`), `:has()`, `:is()`, `:where()`,
31//! namespaces, pseudo-elements, and the non-streamable structural pseudos
32//! (`:last-child`, `:only-child`, `:last-of-type`, `:only-of-type`,
33//! `:nth-last-*`) are intentionally rejected with a [`SelectorError`].
34
35pub(crate) mod ast;
36mod builder;
37mod display;
38mod dom;
39mod matcher;
40mod parser;
41
42#[cfg(test)]
43mod tests;
44
45pub use self::ast::{Compound, Selector};
46pub use self::dom::{Dom, Element, NodeId};
47pub use self::matcher::SelectorSubject;
48
49use std::fmt;
50
51/// Error returned when parsing a CSS selector string fails, or when it
52/// uses a construct outside the streaming-safe [supported subset](self).
53#[derive(Debug, Clone, PartialEq, Eq)]
54#[non_exhaustive]
55pub enum SelectorError {
56    /// The selector string was empty.
57    EmptySelector,
58    /// An unexpected character/token was encountered.
59    UnexpectedToken,
60    /// The selector ended while more input was expected.
61    UnexpectedEnd,
62    /// A combinator was not followed by a compound selector (e.g. `div >`).
63    DanglingCombinator,
64    /// An attribute selector was missing its attribute name (e.g. `[=x]`).
65    MissingAttributeName,
66    /// An unexpected token inside an attribute selector.
67    UnexpectedTokenInAttribute,
68    /// A sibling combinator (`+` or `~`) was used; not supported while
69    /// streaming.
70    UnsupportedCombinator(char),
71    /// An unsupported pseudo-class or pseudo-element was used.
72    UnsupportedPseudoClass,
73    /// A selector used an explicit namespace (e.g. `svg|rect`).
74    NamespacedSelector,
75    /// A `:not()` had no argument.
76    EmptyNegation,
77    /// An `An+B` micro-syntax value was malformed.
78    InvalidNth,
79}
80
81impl fmt::Display for SelectorError {
82    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
83        match self {
84            Self::EmptySelector => f.write_str("empty selector"),
85            Self::UnexpectedToken => f.write_str("unexpected token in selector"),
86            Self::UnexpectedEnd => f.write_str("unexpected end of selector"),
87            Self::DanglingCombinator => f.write_str("dangling combinator in selector"),
88            Self::MissingAttributeName => {
89                f.write_str("missing attribute name in attribute selector")
90            }
91            Self::UnexpectedTokenInAttribute => {
92                f.write_str("unexpected token in attribute selector")
93            }
94            Self::UnsupportedCombinator(c) => {
95                write!(f, "unsupported combinator `{c}` in selector")
96            }
97            Self::UnsupportedPseudoClass => {
98                f.write_str("unsupported pseudo-class or pseudo-element in selector")
99            }
100            Self::NamespacedSelector => {
101                f.write_str("selectors with explicit namespaces are not supported")
102            }
103            Self::EmptyNegation => f.write_str("empty `:not()` in selector"),
104            Self::InvalidNth => f.write_str("invalid `An+B` value in selector"),
105        }
106    }
107}
108
109impl std::error::Error for SelectorError {}