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