selectors/
attr.rs

1/* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4
5use crate::parser::SelectorImpl;
6use cssparser::ToCss;
7use std::fmt;
8
9#[cfg(feature = "to_shmem")]
10use to_shmem_derive::ToShmem;
11
12#[derive(Clone, Eq, PartialEq)]
13#[cfg_attr(feature = "to_shmem", derive(ToShmem))]
14#[cfg_attr(feature = "to_shmem", shmem(no_bounds))]
15pub struct AttrSelectorWithOptionalNamespace<Impl: SelectorImpl> {
16    #[cfg_attr(feature = "to_shmem", shmem(field_bound))]
17    pub namespace: Option<NamespaceConstraint<(Impl::NamespacePrefix, Impl::NamespaceUrl)>>,
18    #[cfg_attr(feature = "to_shmem", shmem(field_bound))]
19    pub local_name: Impl::LocalName,
20    pub local_name_lower: Impl::LocalName,
21    #[cfg_attr(feature = "to_shmem", shmem(field_bound))]
22    pub operation: ParsedAttrSelectorOperation<Impl::AttrValue>,
23}
24
25impl<Impl: SelectorImpl> AttrSelectorWithOptionalNamespace<Impl> {
26    pub fn namespace(&self) -> Option<NamespaceConstraint<&Impl::NamespaceUrl>> {
27        self.namespace.as_ref().map(|ns| match ns {
28            NamespaceConstraint::Any => NamespaceConstraint::Any,
29            NamespaceConstraint::Specific((_, ref url)) => NamespaceConstraint::Specific(url),
30        })
31    }
32}
33
34#[derive(Clone, Eq, PartialEq)]
35#[cfg_attr(feature = "to_shmem", derive(ToShmem))]
36pub enum NamespaceConstraint<NamespaceUrl> {
37    Any,
38
39    /// Empty string for no namespace
40    Specific(NamespaceUrl),
41}
42
43#[derive(Clone, Eq, PartialEq)]
44#[cfg_attr(feature = "to_shmem", derive(ToShmem))]
45pub enum ParsedAttrSelectorOperation<AttrValue> {
46    Exists,
47    WithValue {
48        operator: AttrSelectorOperator,
49        case_sensitivity: ParsedCaseSensitivity,
50        value: AttrValue,
51    },
52}
53
54#[derive(Clone, Eq, PartialEq)]
55pub enum AttrSelectorOperation<AttrValue> {
56    Exists,
57    WithValue {
58        operator: AttrSelectorOperator,
59        case_sensitivity: CaseSensitivity,
60        value: AttrValue,
61    },
62}
63
64impl<AttrValue> AttrSelectorOperation<AttrValue> {
65    pub fn eval_str(&self, element_attr_value: &str) -> bool
66    where
67        AttrValue: AsRef<str>,
68    {
69        match *self {
70            AttrSelectorOperation::Exists => true,
71            AttrSelectorOperation::WithValue {
72                operator,
73                case_sensitivity,
74                ref value,
75            } => operator.eval_str(element_attr_value, value.as_ref(), case_sensitivity),
76        }
77    }
78}
79
80#[derive(Clone, Copy, Eq, PartialEq)]
81#[cfg_attr(feature = "to_shmem", derive(ToShmem))]
82pub enum AttrSelectorOperator {
83    Equal,
84    Includes,
85    DashMatch,
86    Prefix,
87    Substring,
88    Suffix,
89}
90
91impl ToCss for AttrSelectorOperator {
92    fn to_css<W>(&self, dest: &mut W) -> fmt::Result
93    where
94        W: fmt::Write,
95    {
96        // https://drafts.csswg.org/cssom/#serializing-selectors
97        // See "attribute selector".
98        dest.write_str(match *self {
99            AttrSelectorOperator::Equal => "=",
100            AttrSelectorOperator::Includes => "~=",
101            AttrSelectorOperator::DashMatch => "|=",
102            AttrSelectorOperator::Prefix => "^=",
103            AttrSelectorOperator::Substring => "*=",
104            AttrSelectorOperator::Suffix => "$=",
105        })
106    }
107}
108
109impl AttrSelectorOperator {
110    pub fn eval_str(
111        self,
112        element_attr_value: &str,
113        attr_selector_value: &str,
114        case_sensitivity: CaseSensitivity,
115    ) -> bool {
116        let e = element_attr_value.as_bytes();
117        let s = attr_selector_value.as_bytes();
118        let case = case_sensitivity;
119        match self {
120            AttrSelectorOperator::Equal => case.eq(e, s),
121            AttrSelectorOperator::Prefix => {
122                !s.is_empty() && e.len() >= s.len() && case.eq(&e[..s.len()], s)
123            },
124            AttrSelectorOperator::Suffix => {
125                !s.is_empty() && e.len() >= s.len() && case.eq(&e[(e.len() - s.len())..], s)
126            },
127            AttrSelectorOperator::Substring => {
128                !s.is_empty() && case.contains(element_attr_value, attr_selector_value)
129            },
130            AttrSelectorOperator::Includes => {
131                !s.is_empty()
132                    && element_attr_value
133                        .split(SELECTOR_WHITESPACE)
134                        .any(|part| case.eq(part.as_bytes(), s))
135            },
136            AttrSelectorOperator::DashMatch => {
137                case.eq(e, s) || (e.get(s.len()) == Some(&b'-') && case.eq(&e[..s.len()], s))
138            },
139        }
140    }
141}
142
143/// The definition of whitespace per CSS Selectors Level 3 ยง 4.
144pub static SELECTOR_WHITESPACE: &[char] = &[' ', '\t', '\n', '\r', '\x0C'];
145
146#[derive(Clone, Copy, Debug, Eq, PartialEq)]
147#[cfg_attr(feature = "to_shmem", derive(ToShmem))]
148pub enum ParsedCaseSensitivity {
149    /// 's' was specified.
150    ExplicitCaseSensitive,
151    /// 'i' was specified.
152    AsciiCaseInsensitive,
153    /// No flags were specified and HTML says this is a case-sensitive attribute.
154    CaseSensitive,
155    /// No flags were specified and HTML says this is a case-insensitive attribute.
156    AsciiCaseInsensitiveIfInHtmlElementInHtmlDocument,
157}
158
159#[derive(Clone, Copy, Debug, Eq, PartialEq)]
160pub enum CaseSensitivity {
161    CaseSensitive,
162    AsciiCaseInsensitive,
163}
164
165impl CaseSensitivity {
166    pub fn eq(self, a: &[u8], b: &[u8]) -> bool {
167        match self {
168            CaseSensitivity::CaseSensitive => a == b,
169            CaseSensitivity::AsciiCaseInsensitive => a.eq_ignore_ascii_case(b),
170        }
171    }
172
173    pub fn contains(self, haystack: &str, needle: &str) -> bool {
174        match self {
175            CaseSensitivity::CaseSensitive => haystack.contains(needle),
176            CaseSensitivity::AsciiCaseInsensitive => {
177                if let Some((&n_first_byte, n_rest)) = needle.as_bytes().split_first() {
178                    haystack.bytes().enumerate().any(|(i, byte)| {
179                        if !byte.eq_ignore_ascii_case(&n_first_byte) {
180                            return false;
181                        }
182                        let after_this_byte = &haystack.as_bytes()[i + 1..];
183                        match after_this_byte.get(..n_rest.len()) {
184                            None => false,
185                            Some(haystack_slice) => haystack_slice.eq_ignore_ascii_case(n_rest),
186                        }
187                    })
188                } else {
189                    // any_str.contains("") == true,
190                    // though these cases should be handled with *NeverMatches and never go here.
191                    true
192                }
193            },
194        }
195    }
196}