sws_scraper/
selector.rs

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