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    /// Attribute: \[foo\]
39    Attribute(&'a [u8]),
40    /// Attribute with value: [foo=bar]
41    AttributeValue(&'a [u8], &'a [u8]),
42    /// Attribute with whitespace-separated list of values that contains a value: [foo~=bar]
43    AttributeValueWhitespacedContains(&'a [u8], &'a [u8]),
44    /// Attribute with value that starts with: [foo^=bar]
45    AttributeValueStartsWith(&'a [u8], &'a [u8]),
46    /// Attribute with value that ends with: [foo$=bar]
47    AttributeValueEndsWith(&'a [u8], &'a [u8]),
48    /// Attribute with value that contains: [foo*=bar]
49    AttributeValueSubstring(&'a [u8], &'a [u8]),
50}
51
52impl<'a, const MAX_SELECTOR_NODES: usize> Selector<'a, MAX_SELECTOR_NODES> {
53    /// Checks if the given node matches this selector
54    pub fn matches<'b>(&self, node: &Node<'b>) -> bool {
55        match self {
56            Self::Tag(tag) => node.as_tag().is_some_and(|t| t._name.as_bytes().eq(*tag)),
57            Self::Id(id) => node
58                .as_tag()
59                .is_some_and(|t| t._attributes.id == Some((*id).into())),
60            Self::Class(class) => node
61                .as_tag()
62                .is_some_and(|t| t._attributes.is_class_member(*class)),
63            #[cfg(feature = "std")]
64            Self::And(a, b) => a.matches(node) && b.matches(node),
65            #[cfg(feature = "std")]
66            Self::Or(a, b) => a.matches(node) || b.matches(node),
67            Self::All => true,
68            Self::Attribute(attribute) => node
69                .as_tag()
70                .is_some_and(|t| t._attributes.get(*attribute).is_some()),
71            Self::AttributeValue(attribute, value) => {
72                check_attribute(node, attribute, value, |attr, value| attr == value)
73            }
74            Self::AttributeValueEndsWith(attribute, value) => {
75                check_attribute(node, attribute, value, |attr, value| attr.ends_with(value))
76            }
77            Self::AttributeValueStartsWith(attribute, value) => {
78                check_attribute(node, attribute, value, |attr, value| {
79                    attr.starts_with(value)
80                })
81            }
82            Self::AttributeValueSubstring(attribute, value) => {
83                check_attribute(node, attribute, value, |attr, value| attr.contains(value))
84            }
85            Self::AttributeValueWhitespacedContains(attribute, value) => {
86                check_attribute(node, attribute, value, |attr, value| {
87                    attr.split_whitespace().any(|x| x == value)
88                })
89            }
90            #[cfg(feature = "std")]
91            _ => false,
92        }
93    }
94}
95
96fn check_attribute<F>(node: &Node, attribute: &[u8], value: &[u8], callback: F) -> bool
97where
98    F: Fn(&str, &str) -> bool,
99{
100    let Ok(value) = core::str::from_utf8(value) else {
101        return false;
102    };
103    node.as_tag().is_some_and(|t| {
104        t._attributes
105            .get(attribute)
106            .flatten()
107            .and_then(|attr| attr.try_as_utf8_str())
108            .is_some_and(|attr| callback(attr, value))
109    })
110}