Coverage Report

Created: 2026-10-28 07:08

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/rust/registry/src/index.crates.io-1949cf8c6b5b557f/parcel_selectors-0.28.3/attr.rs
Line
Count
Source
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
5
use crate::parser::SelectorImpl;
6
use cssparser::ToCss;
7
use std::fmt;
8
9
#[derive(Clone, Eq, PartialEq, Hash)]
10
pub struct AttrSelectorWithOptionalNamespace<'i, Impl: SelectorImpl<'i>> {
11
  pub namespace: Option<NamespaceConstraint<(Impl::NamespacePrefix, Impl::NamespaceUrl)>>,
12
  pub local_name: Impl::LocalName,
13
  pub local_name_lower: Impl::LocalName,
14
  pub operation: ParsedAttrSelectorOperation<Impl::AttrValue>,
15
  pub never_matches: bool,
16
}
17
18
#[cfg(feature = "into_owned")]
19
impl<'any, 'i, Impl: SelectorImpl<'i>, NewSel> static_self::IntoOwned<'any>
20
  for AttrSelectorWithOptionalNamespace<'i, Impl>
21
where
22
  Impl: static_self::IntoOwned<'any, Owned = NewSel>,
23
  NewSel: SelectorImpl<'any>,
24
  Impl::LocalName: static_self::IntoOwned<'any, Owned = NewSel::LocalName>,
25
  Impl::NamespacePrefix: static_self::IntoOwned<'any, Owned = NewSel::NamespacePrefix>,
26
  Impl::NamespaceUrl: static_self::IntoOwned<'any, Owned = NewSel::NamespaceUrl>,
27
  Impl::AttrValue: static_self::IntoOwned<'any, Owned = NewSel::AttrValue>,
28
{
29
  type Owned = AttrSelectorWithOptionalNamespace<'any, NewSel>;
30
31
  fn into_owned(self) -> Self::Owned {
32
    AttrSelectorWithOptionalNamespace {
33
      namespace: self.namespace.into_owned(),
34
      local_name: self.local_name.into_owned(),
35
      local_name_lower: self.local_name_lower.into_owned(),
36
      operation: self.operation.into_owned(),
37
      never_matches: self.never_matches,
38
    }
39
  }
40
}
41
42
impl<'i, Impl: SelectorImpl<'i>> AttrSelectorWithOptionalNamespace<'i, Impl> {
43
0
  pub fn namespace(&self) -> Option<NamespaceConstraint<&Impl::NamespaceUrl>> {
44
0
    self.namespace.as_ref().map(|ns| match ns {
45
0
      NamespaceConstraint::Any => NamespaceConstraint::Any,
46
0
      NamespaceConstraint::Specific((_, ref url)) => NamespaceConstraint::Specific(url),
47
0
    })
48
0
  }
49
}
50
51
#[derive(Clone, Eq, PartialEq, Hash)]
52
#[cfg_attr(
53
  feature = "serde",
54
  derive(serde::Serialize, serde::Deserialize),
55
  serde(tag = "type", rename_all = "kebab-case")
56
)]
57
#[cfg_attr(
58
  feature = "jsonschema",
59
  derive(schemars::JsonSchema),
60
  schemars(rename = "NamespaceConstraint")
61
)]
62
#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
63
pub enum NamespaceConstraint<NamespaceUrl> {
64
  Any,
65
66
  /// Empty string for no namespace
67
  Specific(NamespaceUrl),
68
}
69
70
#[derive(Clone, Eq, PartialEq, Hash)]
71
#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
72
pub enum ParsedAttrSelectorOperation<AttrValue> {
73
  Exists,
74
  WithValue {
75
    operator: AttrSelectorOperator,
76
    case_sensitivity: ParsedCaseSensitivity,
77
    expected_value: AttrValue,
78
  },
79
}
80
81
pub enum AttrSelectorOperation<AttrValue> {
82
  Exists,
83
  WithValue {
84
    operator: AttrSelectorOperator,
85
    case_sensitivity: CaseSensitivity,
86
    expected_value: AttrValue,
87
  },
88
}
89
90
impl<AttrValue> AttrSelectorOperation<AttrValue> {
91
0
  pub fn eval_str(&self, element_attr_value: &str) -> bool
92
0
  where
93
0
    AttrValue: AsRef<str>,
94
  {
95
0
    match *self {
96
0
      AttrSelectorOperation::Exists => true,
97
      AttrSelectorOperation::WithValue {
98
0
        operator,
99
0
        case_sensitivity,
100
0
        ref expected_value,
101
0
      } => operator.eval_str(element_attr_value, expected_value.as_ref(), case_sensitivity),
102
    }
103
0
  }
104
}
105
106
#[derive(Clone, Copy, Eq, PartialEq, Hash)]
107
#[cfg_attr(
108
  feature = "serde",
109
  derive(serde::Serialize, serde::Deserialize),
110
  serde(rename_all = "kebab-case")
111
)]
112
#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
113
#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
114
pub enum AttrSelectorOperator {
115
  Equal,
116
  Includes,
117
  DashMatch,
118
  Prefix,
119
  Substring,
120
  Suffix,
121
}
122
123
impl ToCss for AttrSelectorOperator {
124
0
  fn to_css<W>(&self, dest: &mut W) -> fmt::Result
125
0
  where
126
0
    W: fmt::Write,
127
  {
128
    // https://drafts.csswg.org/cssom/#serializing-selectors
129
    // See "attribute selector".
130
0
    dest.write_str(match *self {
131
0
      AttrSelectorOperator::Equal => "=",
132
0
      AttrSelectorOperator::Includes => "~=",
133
0
      AttrSelectorOperator::DashMatch => "|=",
134
0
      AttrSelectorOperator::Prefix => "^=",
135
0
      AttrSelectorOperator::Substring => "*=",
136
0
      AttrSelectorOperator::Suffix => "$=",
137
    })
138
0
  }
139
}
140
141
impl AttrSelectorOperator {
142
0
  pub fn eval_str(
143
0
    self,
144
0
    element_attr_value: &str,
145
0
    attr_selector_value: &str,
146
0
    case_sensitivity: CaseSensitivity,
147
0
  ) -> bool {
148
0
    let e = element_attr_value.as_bytes();
149
0
    let s = attr_selector_value.as_bytes();
150
0
    let case = case_sensitivity;
151
0
    match self {
152
0
      AttrSelectorOperator::Equal => case.eq(e, s),
153
0
      AttrSelectorOperator::Prefix => e.len() >= s.len() && case.eq(&e[..s.len()], s),
154
0
      AttrSelectorOperator::Suffix => e.len() >= s.len() && case.eq(&e[(e.len() - s.len())..], s),
155
0
      AttrSelectorOperator::Substring => case.contains(element_attr_value, attr_selector_value),
156
0
      AttrSelectorOperator::Includes => element_attr_value
157
0
        .split(SELECTOR_WHITESPACE)
158
0
        .any(|part| case.eq(part.as_bytes(), s)),
159
      AttrSelectorOperator::DashMatch => {
160
0
        case.eq(e, s) || (e.get(s.len()) == Some(&b'-') && case.eq(&e[..s.len()], s))
161
      }
162
    }
163
0
  }
164
}
165
166
/// The definition of whitespace per CSS Selectors Level 3 ยง 4.
167
pub static SELECTOR_WHITESPACE: &[char] = &[' ', '\t', '\n', '\r', '\x0C'];
168
169
#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
170
#[cfg_attr(
171
  feature = "serde",
172
  derive(serde::Serialize, serde::Deserialize),
173
  serde(rename_all = "kebab-case")
174
)]
175
#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
176
#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
177
pub enum ParsedCaseSensitivity {
178
  // 's' was specified.
179
  ExplicitCaseSensitive,
180
  // 'i' was specified.
181
  AsciiCaseInsensitive,
182
  // No flags were specified and HTML says this is a case-sensitive attribute.
183
  CaseSensitive,
184
  // No flags were specified and HTML says this is a case-insensitive attribute.
185
  AsciiCaseInsensitiveIfInHtmlElementInHtmlDocument,
186
}
187
188
impl Default for ParsedCaseSensitivity {
189
0
  fn default() -> Self {
190
0
    ParsedCaseSensitivity::CaseSensitive
191
0
  }
192
}
193
194
impl ParsedCaseSensitivity {
195
0
  pub fn to_unconditional(self, is_html_element_in_html_document: bool) -> CaseSensitivity {
196
0
    match self {
197
      ParsedCaseSensitivity::AsciiCaseInsensitiveIfInHtmlElementInHtmlDocument
198
0
        if is_html_element_in_html_document =>
199
      {
200
0
        CaseSensitivity::AsciiCaseInsensitive
201
      }
202
0
      ParsedCaseSensitivity::AsciiCaseInsensitiveIfInHtmlElementInHtmlDocument => CaseSensitivity::CaseSensitive,
203
      ParsedCaseSensitivity::CaseSensitive | ParsedCaseSensitivity::ExplicitCaseSensitive => {
204
0
        CaseSensitivity::CaseSensitive
205
      }
206
0
      ParsedCaseSensitivity::AsciiCaseInsensitive => CaseSensitivity::AsciiCaseInsensitive,
207
    }
208
0
  }
209
}
210
211
#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
212
pub enum CaseSensitivity {
213
  CaseSensitive,
214
  AsciiCaseInsensitive,
215
}
216
217
impl CaseSensitivity {
218
0
  pub fn eq(self, a: &[u8], b: &[u8]) -> bool {
219
0
    match self {
220
0
      CaseSensitivity::CaseSensitive => a == b,
221
0
      CaseSensitivity::AsciiCaseInsensitive => a.eq_ignore_ascii_case(b),
222
    }
223
0
  }
224
225
0
  pub fn contains(self, haystack: &str, needle: &str) -> bool {
226
0
    match self {
227
0
      CaseSensitivity::CaseSensitive => haystack.contains(needle),
228
      CaseSensitivity::AsciiCaseInsensitive => {
229
0
        if let Some((&n_first_byte, n_rest)) = needle.as_bytes().split_first() {
230
0
          haystack.bytes().enumerate().any(|(i, byte)| {
231
0
            if !byte.eq_ignore_ascii_case(&n_first_byte) {
232
0
              return false;
233
0
            }
234
0
            let after_this_byte = &haystack.as_bytes()[i + 1..];
235
0
            match after_this_byte.get(..n_rest.len()) {
236
0
              None => false,
237
0
              Some(haystack_slice) => haystack_slice.eq_ignore_ascii_case(n_rest),
238
            }
239
0
          })
240
        } else {
241
          // any_str.contains("") == true,
242
          // though these cases should be handled with *NeverMatches and never go here.
243
0
          true
244
        }
245
      }
246
    }
247
0
  }
248
}