accessibility_scraper/
selector.rs

1//! CSS selectors.
2
3use std::convert::TryFrom;
4use std::fmt;
5
6use smallvec::SmallVec;
7
8use fast_html5ever::{LocalName, Namespace};
9use selectors::{
10    matching,
11    parser::{self, SelectorParseErrorKind},
12};
13
14use crate::error::SelectorErrorKind;
15use crate::ElementRef;
16
17/// Wrapper around CSS selectors.
18///
19/// Represents a "selector group", i.e. a comma-separated list of selectors.
20#[derive(Debug, Clone, PartialEq, Eq)]
21pub struct Selector {
22    /// The CSS selectors.
23    selectors: SmallVec<[parser::Selector<Simple>; 1]>,
24}
25
26impl Selector {
27    /// Parses a CSS selector group.
28
29    pub fn parse(selectors: &'_ str) -> Result<Self, SelectorErrorKind> {
30        let mut parser_input = cssparser::ParserInput::new(selectors);
31        let mut parser = cssparser::Parser::new(&mut parser_input);
32
33        parser::SelectorList::parse(&Parser, &mut parser)
34            .map(|list| Selector { selectors: list.0 })
35            .map_err(SelectorErrorKind::from)
36    }
37
38    /// Returns true if the element matches this selector.
39    pub fn matches(&self, element: &ElementRef) -> bool {
40        self.matches_with_scope(element, None)
41    }
42
43    /// Returns true if the element matches this selector.
44    /// The optional `scope` argument is used to specify which element has `:scope` pseudo-class.
45    /// When it is `None`, `:scope` will match the root element.
46    pub fn matches_with_scope(&self, element: &ElementRef, scope: Option<ElementRef>) -> bool {
47        let mut nth_index_cache = Default::default();
48        let mut context = matching::MatchingContext::new(
49            matching::MatchingMode::Normal,
50            None,
51            Some(&mut nth_index_cache),
52            matching::QuirksMode::NoQuirks,
53            // matching::NeedsSelectorFlags::No,
54            // matching::IgnoreNthChildForInvalidation::No,
55        );
56        context.scope_element = scope.map(|x| selectors::Element::opaque(&x));
57        self.selectors
58            .iter()
59            .any(|s| matching::matches_selector(s, 0, None, element, &mut context, &mut |_, _| {}))
60    }
61}
62
63/// An implementation of `Parser` for `selectors`
64#[derive(Debug, Clone, Copy)]
65pub struct Parser;
66
67impl<'i> parser::Parser<'i> for Parser {
68    type Impl = Simple;
69    type Error = SelectorParseErrorKind<'i>;
70}
71
72/// A simple implementation of `SelectorImpl` with no pseudo-classes or pseudo-elements.
73#[derive(Debug, Clone, Copy, PartialEq, Eq)]
74pub struct Simple;
75
76impl parser::SelectorImpl for Simple {
77    type AttrValue = CssString;
78    type Identifier = CssLocalName;
79    type ClassName = CssLocalName;
80    type LocalName = CssLocalName;
81    type PartName = LocalName;
82    type NamespacePrefix = CssLocalName;
83    type NamespaceUrl = Namespace;
84    type BorrowedNamespaceUrl = Namespace;
85    type BorrowedLocalName = CssLocalName;
86
87    type NonTSPseudoClass = NonTSPseudoClass;
88    type PseudoElement = PseudoElement;
89
90    // see: https://github.com/servo/servo/pull/19747#issuecomment-357106065
91    type ExtraMatchingData = ();
92}
93
94/// Wraps [`String`] so that it can be used with [`selectors`]
95#[derive(Debug, Clone, PartialEq, Eq)]
96pub struct CssString(pub String);
97
98impl<'a> From<&'a str> for CssString {
99    fn from(val: &'a str) -> Self {
100        Self(val.to_owned())
101    }
102}
103
104impl AsRef<str> for CssString {
105    fn as_ref(&self) -> &str {
106        &self.0
107    }
108}
109
110impl cssparser::ToCss for CssString {
111    fn to_css<W>(&self, dest: &mut W) -> fmt::Result
112    where
113        W: fmt::Write,
114    {
115        cssparser::serialize_string(&self.0, dest)
116    }
117}
118
119/// Wraps [`LocalName`] so that it can be used with [`selectors`]
120#[derive(Debug, Default, Clone, PartialEq, Eq)]
121pub struct CssLocalName(pub LocalName);
122
123impl std::fmt::Display for CssString {
124    fn fmt(&self, _: &mut core::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
125        Ok(())
126    }
127}
128
129impl std::fmt::Display for CssLocalName {
130    fn fmt(&self, _: &mut core::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
131        Ok(())
132    }
133}
134
135impl<'a> From<&'a str> for CssLocalName {
136    fn from(val: &'a str) -> Self {
137        Self(val.into())
138    }
139}
140
141impl cssparser::ToCss for CssLocalName {
142    fn to_css<W>(&self, dest: &mut W) -> fmt::Result
143    where
144        W: fmt::Write,
145    {
146        dest.write_str(&self.0)
147    }
148}
149
150/// Non Tree-Structural Pseudo-Class.
151#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
152pub enum NonTSPseudoClass {
153    /// any link
154    AnyLink,
155    /// a link
156    Link,
157    /// a visited link
158    Visited,
159    /// a active element
160    Active,
161    /// a focused element
162    Focus,
163    /// a element that is hovered
164    Hover,
165    /// a element that has enabled checked
166    Enabled,
167    /// a element that has disabled prop
168    Disabled,
169    /// a element that has the checked property
170    Checked,
171    /// an indeterminate element
172    Indeterminate,
173}
174
175impl parser::NonTSPseudoClass for NonTSPseudoClass {
176    type Impl = Simple;
177    fn is_active_or_hover(&self) -> bool {
178        false
179    }
180    fn is_user_action_state(&self) -> bool {
181        false
182    }
183    fn has_zero_specificity(&self) -> bool {
184        false
185    }
186}
187
188impl cssparser::ToCss for NonTSPseudoClass {
189    fn to_css<W>(&self, dest: &mut W) -> fmt::Result
190    where
191        W: fmt::Write,
192    {
193        dest.write_str(match *self {
194            NonTSPseudoClass::AnyLink => ":any-link",
195            NonTSPseudoClass::Link => ":link",
196            NonTSPseudoClass::Visited => ":visited",
197            NonTSPseudoClass::Active => ":active",
198            NonTSPseudoClass::Focus => ":focus",
199            NonTSPseudoClass::Hover => ":hover",
200            NonTSPseudoClass::Enabled => ":enabled",
201            NonTSPseudoClass::Disabled => ":disabled",
202            NonTSPseudoClass::Checked => ":checked",
203            NonTSPseudoClass::Indeterminate => ":indeterminate",
204        })
205    }
206}
207
208/// CSS Pseudo-Element
209#[derive(Debug, Clone, Copy, PartialEq, Eq)]
210pub enum PseudoElement {}
211
212impl parser::PseudoElement for PseudoElement {
213    type Impl = Simple;
214}
215
216impl cssparser::ToCss for PseudoElement {
217    fn to_css<W>(&self, dest: &mut W) -> fmt::Result
218    where
219        W: fmt::Write,
220    {
221        dest.write_str("")
222    }
223}
224
225impl<'i> TryFrom<&'i str> for Selector {
226    type Error = SelectorErrorKind<'i>;
227
228    fn try_from(s: &'i str) -> Result<Self, Self::Error> {
229        Selector::parse(s)
230    }
231}
232
233#[cfg(test)]
234mod tests {
235    use super::*;
236    use std::convert::TryInto;
237
238    #[test]
239    fn selector_conversions() {
240        let s = "#testid.testclass";
241        let _sel: Selector = s.try_into().unwrap();
242
243        let s = s.to_owned();
244        let _sel: Selector = (*s).try_into().unwrap();
245    }
246
247    #[test]
248    #[should_panic]
249    fn invalid_selector_conversions() {
250        let s = "<failing selector>";
251        let _sel: Selector = s.try_into().unwrap();
252    }
253}