Skip to main content

tl/queryselector/
selector.rs

1use crate::Node;
2
3/// A single query selector node
4#[derive(Debug, Clone)]
5pub enum Selector<'a, const MAX_SELECTOR_NODES: usize = 0> {
6    /// Tag selector: foo
7    Tag(&'a [u8]),
8    /// ID selector: #foo
9    Id(&'a [u8]),
10    /// Class selector: .foo
11    Class(&'a [u8]),
12    /// All selector: *
13    All,
14    /// And combinator: .foo.bar
15    #[cfg(feature = "std")]
16    And(
17        Box<Selector<'a, MAX_SELECTOR_NODES>>,
18        Box<Selector<'a, MAX_SELECTOR_NODES>>,
19    ),
20    /// Or combinator: .foo, .bar
21    #[cfg(feature = "std")]
22    Or(
23        Box<Selector<'a, MAX_SELECTOR_NODES>>,
24        Box<Selector<'a, MAX_SELECTOR_NODES>>,
25    ),
26    /// Descendant combinator: .foo .bar
27    #[cfg(feature = "std")]
28    Descendant(
29        Box<Selector<'a, MAX_SELECTOR_NODES>>,
30        Box<Selector<'a, MAX_SELECTOR_NODES>>,
31    ),
32    /// Parent combinator: .foo > .bar
33    #[cfg(feature = "std")]
34    Parent(
35        Box<Selector<'a, MAX_SELECTOR_NODES>>,
36        Box<Selector<'a, MAX_SELECTOR_NODES>>,
37    ),
38    /// Adjacent element sibling combinator: a + b.
39    #[cfg(feature = "std")]
40    Adjacent(Box<Self>, Box<Self>),
41    /// General element sibling combinator: a ~ b.
42    #[cfg(feature = "std")]
43    Sibling(Box<Self>, Box<Self>),
44    /// Negation pseudo-class: :not(selector).
45    #[cfg(feature = "std")]
46    Not(Box<Self>),
47    /// Descendant existence pseudo-class: :has(selector).
48    /// Relative selectors such as :has(> a) are not supported.
49    #[cfg(feature = "std")]
50    Has(Box<Self>),
51    /// Element position pseudo-class: :nth-child(an+b), using one-based indices.
52    #[cfg(feature = "std")]
53    NthChild(i32, i32),
54    /// Attribute: \[foo\]
55    Attribute(&'a [u8]),
56    /// Attribute with value: [foo=bar]
57    AttributeValue(&'a [u8], &'a [u8]),
58    /// Attribute with whitespace-separated list of values that contains a value: [foo~=bar]
59    AttributeValueWhitespacedContains(&'a [u8], &'a [u8]),
60    /// Attribute with value that starts with: [foo^=bar]
61    AttributeValueStartsWith(&'a [u8], &'a [u8]),
62    /// Attribute with value that ends with: [foo$=bar]
63    AttributeValueEndsWith(&'a [u8], &'a [u8]),
64    /// Attribute with value that contains: [foo*=bar]
65    AttributeValueSubstring(&'a [u8], &'a [u8]),
66}
67
68impl<'a, const MAX_SELECTOR_NODES: usize> Selector<'a, MAX_SELECTOR_NODES> {
69    /// Checks a node without tree context. Structural selectors require `query_selector`.
70    pub fn matches<'b>(&self, node: &Node<'b>) -> bool {
71        match self {
72            Self::Tag(tag) => node
73                .as_tag()
74                .is_some_and(|t| t._name.as_bytes().eq_ignore_ascii_case(tag)),
75            Self::Id(id) => node
76                .as_tag()
77                .is_some_and(|t| t._attributes.id == Some((*id).into())),
78            Self::Class(class) => node
79                .as_tag()
80                .is_some_and(|t| t._attributes.is_class_member(*class)),
81            #[cfg(feature = "std")]
82            Self::And(a, b) => a.matches(node) && b.matches(node),
83            #[cfg(feature = "std")]
84            Self::Or(a, b) => a.matches(node) || b.matches(node),
85            #[cfg(feature = "std")]
86            Self::Not(a) => node.as_tag().is_some() && !a.matches(node),
87            Self::All => node.as_tag().is_some(),
88            Self::Attribute(attribute) => node
89                .as_tag()
90                .is_some_and(|t| t._attributes.get(*attribute).is_some()),
91            Self::AttributeValue(attribute, value) => {
92                check_attribute(node, attribute, value, |attr, value| attr == value)
93            }
94            Self::AttributeValueEndsWith(attribute, value) => {
95                check_attribute(node, attribute, value, |attr, value| {
96                    !value.is_empty() && attr.ends_with(value)
97                })
98            }
99            Self::AttributeValueStartsWith(attribute, value) => {
100                check_attribute(node, attribute, value, |attr, value| {
101                    !value.is_empty() && attr.starts_with(value)
102                })
103            }
104            Self::AttributeValueSubstring(attribute, value) => {
105                check_attribute(node, attribute, value, |attr, value| {
106                    !value.is_empty() && attr.contains(value)
107                })
108            }
109            Self::AttributeValueWhitespacedContains(attribute, value) => {
110                check_attribute(node, attribute, value, |attr, value| {
111                    attr.split_whitespace().any(|x| x == value)
112                })
113            }
114            #[cfg(feature = "std")]
115            _ => false,
116        }
117    }
118}
119
120fn check_attribute<F>(node: &Node, attribute: &[u8], value: &[u8], callback: F) -> bool
121where
122    F: Fn(&str, &str) -> bool,
123{
124    let Ok(value) = core::str::from_utf8(value) else {
125        return false;
126    };
127    node.as_tag().is_some_and(|t| {
128        t._attributes
129            .get(attribute)
130            .flatten()
131            .and_then(|attr| attr.try_as_utf8_str())
132            .is_some_and(|attr| callback(attr, value))
133    })
134}
135
136#[cfg(feature = "std")]
137#[derive(Clone)]
138pub(crate) struct Context {
139    parent: Vec<Option<usize>>,
140    previous: Vec<Option<usize>>,
141    position: Vec<usize>,
142}
143
144#[cfg(feature = "std")]
145impl Context {
146    pub(crate) fn new(nodes: &[Node<'_>]) -> Self {
147        let mut result = Self {
148            parent: vec![None; nodes.len()],
149            previous: vec![None; nodes.len()],
150            position: vec![0; nodes.len()],
151        };
152        for (id, node) in nodes.iter().enumerate() {
153            if let Some(tag) = node.as_tag() {
154                let mut previous = None;
155                let mut position = 0;
156                for child in tag.children().top().iter() {
157                    let child = child.get_inner() as usize;
158                    if let Some(node) = nodes.get(child) {
159                        result.parent[child] = Some(id);
160                        if node.as_tag().is_some() {
161                            position += 1;
162                            result.previous[child] = previous;
163                            result.position[child] = position;
164                            previous = Some(child);
165                        }
166                    }
167                }
168            }
169        }
170        let mut previous = None;
171        let mut position = 0;
172        for (id, node) in nodes.iter().enumerate() {
173            if result.parent[id].is_none() && node.as_tag().is_some() {
174                position += 1;
175                result.previous[id] = previous;
176                result.position[id] = position;
177                previous = Some(id);
178            }
179        }
180        result
181    }
182}
183
184#[cfg(feature = "std")]
185impl<const N: usize> Selector<'_, N> {
186    pub(crate) fn needs_context(&self) -> bool {
187        match self {
188            Self::And(a, b) | Self::Or(a, b) => a.needs_context() || b.needs_context(),
189            Self::Not(a) => a.needs_context(),
190            Self::Parent(..)
191            | Self::Descendant(..)
192            | Self::Adjacent(..)
193            | Self::Sibling(..)
194            | Self::NthChild(..)
195            | Self::Has(..) => true,
196            _ => false,
197        }
198    }
199
200    pub(crate) fn matches_in(&self, id: usize, nodes: &[Node<'_>], ctx: &Context) -> bool {
201        self.matches_scoped(id, nodes, ctx, None)
202    }
203
204    fn matches_scoped(
205        &self,
206        id: usize,
207        nodes: &[Node<'_>],
208        ctx: &Context,
209        scope: Option<usize>,
210    ) -> bool {
211        let Some(node) = nodes.get(id) else {
212            return false;
213        };
214        if node.as_tag().is_none() {
215            return false;
216        }
217        match self {
218            Self::And(a, b) => {
219                a.matches_scoped(id, nodes, ctx, scope) && b.matches_scoped(id, nodes, ctx, scope)
220            }
221            Self::Or(a, b) => {
222                a.matches_scoped(id, nodes, ctx, scope) || b.matches_scoped(id, nodes, ctx, scope)
223            }
224            Self::Not(a) => !a.matches_scoped(id, nodes, ctx, scope),
225            Self::Parent(a, b) => {
226                b.matches_scoped(id, nodes, ctx, scope)
227                    && ctx.parent[id]
228                        .filter(|p| Some(*p) != scope)
229                        .is_some_and(|p| a.matches_scoped(p, nodes, ctx, scope))
230            }
231            Self::Adjacent(a, b) => {
232                b.matches_scoped(id, nodes, ctx, scope)
233                    && ctx.previous[id].is_some_and(|p| a.matches_scoped(p, nodes, ctx, scope))
234            }
235            Self::Descendant(a, b) | Self::Sibling(a, b) => {
236                if !b.matches_scoped(id, nodes, ctx, scope) {
237                    return false;
238                }
239                let links = if matches!(self, Self::Descendant(..)) {
240                    &ctx.parent
241                } else {
242                    &ctx.previous
243                };
244                let mut cursor = links[id]
245                    .filter(|next| !matches!(self, Self::Descendant(..)) || Some(*next) != scope);
246                // Bounded traversal also tolerates a cyclic graph introduced by mutation.
247                for _ in 0..nodes.len() {
248                    let Some(p) = cursor else {
249                        break;
250                    };
251                    if a.matches_scoped(p, nodes, ctx, scope) {
252                        return true;
253                    }
254                    cursor = links[p].filter(|next| {
255                        !matches!(self, Self::Descendant(..)) || Some(*next) != scope
256                    });
257                }
258                false
259            }
260            Self::NthChild(a, b) => {
261                let delta = ctx.position[id] as i64 - i64::from(*b);
262                let a = i64::from(*a);
263                if a == 0 {
264                    delta == 0
265                } else {
266                    delta % a == 0 && delta / a >= 0
267                }
268            }
269            Self::Has(a) => {
270                // A borrowed boundary replaces the per-anchor cloned index.
271                // Descendant arguments cannot use the anchor or its ancestors.
272                nodes.iter().enumerate().any(|(candidate, _)| {
273                    let mut cursor = ctx.parent[candidate];
274                    for _ in 0..nodes.len() {
275                        let Some(p) = cursor else {
276                            break;
277                        };
278                        if p == id {
279                            return a.matches_scoped(candidate, nodes, ctx, Some(id));
280                        }
281                        cursor = ctx.parent[p];
282                    }
283                    false
284                })
285            }
286            _ => self.matches(node),
287        }
288    }
289}