scraper/
selector.rs

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