rama_http/protocols/html/selector/
ast.rs1#[derive(Debug, Clone, PartialEq, Eq)]
14pub struct Selector {
15 pub(crate) selectors: Vec<ComplexSelector>,
16}
17
18#[derive(Debug, Clone, PartialEq, Eq)]
21pub(crate) struct ComplexSelector {
22 pub(crate) parts: Vec<SelectorPart>,
26}
27
28#[derive(Debug, Clone, PartialEq, Eq)]
29pub(crate) struct SelectorPart {
30 pub(crate) combinator: Option<Combinator>,
31 pub(crate) compound: Compound,
32}
33
34#[derive(Debug, Clone, Copy, PartialEq, Eq)]
36pub(crate) enum Combinator {
37 Descendant,
39 Child,
41}
42
43#[derive(Debug, Clone, PartialEq, Eq, Default)]
49pub struct Compound {
50 pub(crate) name: Option<LocalName>,
52 pub(crate) explicit_universal: bool,
55 pub(crate) id: Option<Box<str>>,
56 pub(crate) classes: Vec<Box<str>>,
57 pub(crate) attributes: Vec<AttributeSelector>,
58 pub(crate) nth: Vec<Nth>,
59 pub(crate) negations: Vec<Self>,
63}
64
65#[derive(Debug, Clone, PartialEq, Eq)]
67pub(crate) struct AttributeSelector {
68 pub(crate) name: Box<str>,
70 pub(crate) operator: Option<AttributeOperator>,
72 pub(crate) value: Box<str>,
74 pub(crate) case: CaseSensitivity,
75}
76
77#[derive(Debug, Clone, Copy, PartialEq, Eq)]
79pub(crate) enum AttributeOperator {
80 Equals,
82 Includes,
84 DashMatch,
86 Prefix,
88 Suffix,
90 Substring,
92}
93
94#[derive(Debug, Clone, Copy, PartialEq, Eq)]
96pub(crate) enum CaseSensitivity {
97 CaseSensitive,
99 AsciiCaseInsensitive,
101}
102
103#[derive(Debug, Clone, Copy, PartialEq, Eq)]
109pub(crate) struct Nth {
110 pub(crate) ty: NthType,
111 pub(crate) a: i32,
112 pub(crate) b: i32,
113}
114
115#[derive(Debug, Clone, Copy, PartialEq, Eq)]
116pub(crate) enum NthType {
117 Child,
119 OfType,
121}
122
123impl Nth {
124 pub(crate) fn matches_index(self, index: usize) -> bool {
127 let Ok(i) = i64::try_from(index) else {
131 return false;
132 };
133 let a = i64::from(self.a);
134 let b = i64::from(self.b);
135 let Some(diff) = i.checked_sub(b) else {
136 return false;
137 };
138 if a == 0 {
139 return diff == 0;
140 }
141 diff % a == 0 && diff / a >= 0
142 }
143}
144
145#[derive(Debug, Clone, PartialEq, Eq)]
148pub(crate) struct LocalName {
149 name: Box<str>,
151 packed: Option<u64>,
154}
155
156impl LocalName {
157 pub(crate) fn new(name: &str) -> Self {
158 let lower = name.to_ascii_lowercase();
159 let packed = pack_name(lower.as_bytes());
160 Self {
161 name: lower.into_boxed_str(),
162 packed,
163 }
164 }
165
166 pub(crate) fn as_str(&self) -> &str {
167 &self.name
168 }
169
170 pub(crate) fn matches(&self, raw: &str) -> bool {
173 self.matches_bytes(raw.as_bytes())
174 }
175
176 pub(crate) fn matches_bytes(&self, raw: &[u8]) -> bool {
178 if let Some(packed) = self.packed
179 && let Some(other) = pack_name(raw)
180 {
181 return packed == other;
182 }
183 raw.eq_ignore_ascii_case(self.name.as_bytes())
184 }
185}
186
187impl AttributeSelector {
188 pub(crate) fn matches_value(&self, actual: &[u8]) -> bool {
192 let expected = self.value.as_bytes();
193 let ci = matches!(self.case, CaseSensitivity::AsciiCaseInsensitive);
194 match self.operator {
195 None => true,
196 Some(AttributeOperator::Equals) => bytes_eq(actual, expected, ci),
197 Some(AttributeOperator::Includes) => {
198 !expected.is_empty()
199 && !expected.iter().any(u8::is_ascii_whitespace)
200 && actual
201 .split(u8::is_ascii_whitespace)
202 .any(|token| bytes_eq(token, expected, ci))
203 }
204 Some(AttributeOperator::DashMatch) => {
205 bytes_eq(actual, expected, ci)
206 || (actual.len() > expected.len()
207 && bytes_starts_with(actual, expected, ci)
208 && actual.get(expected.len()) == Some(&b'-'))
209 }
210 Some(AttributeOperator::Prefix) => {
211 !expected.is_empty() && bytes_starts_with(actual, expected, ci)
212 }
213 Some(AttributeOperator::Suffix) => {
214 !expected.is_empty() && bytes_ends_with(actual, expected, ci)
215 }
216 Some(AttributeOperator::Substring) => {
217 !expected.is_empty() && bytes_contains(actual, expected, ci)
218 }
219 }
220 }
221}
222
223fn bytes_eq(a: &[u8], b: &[u8], case_insensitive: bool) -> bool {
224 if case_insensitive {
225 a.eq_ignore_ascii_case(b)
226 } else {
227 a == b
228 }
229}
230
231fn bytes_starts_with(haystack: &[u8], needle: &[u8], case_insensitive: bool) -> bool {
232 haystack
233 .get(..needle.len())
234 .is_some_and(|head| bytes_eq(head, needle, case_insensitive))
235}
236
237fn bytes_ends_with(haystack: &[u8], needle: &[u8], case_insensitive: bool) -> bool {
238 haystack
239 .len()
240 .checked_sub(needle.len())
241 .and_then(|start| haystack.get(start..))
242 .is_some_and(|tail| bytes_eq(tail, needle, case_insensitive))
243}
244
245fn bytes_contains(haystack: &[u8], needle: &[u8], case_insensitive: bool) -> bool {
246 if needle.len() > haystack.len() {
247 return false;
248 }
249 if !case_insensitive {
250 return haystack
251 .windows(needle.len())
252 .any(|window| window == needle);
253 }
254 (0..=haystack.len() - needle.len()).any(|i| {
255 haystack
256 .get(i..i + needle.len())
257 .is_some_and(|window| window.eq_ignore_ascii_case(needle))
258 })
259}
260
261fn pack_name(bytes: &[u8]) -> Option<u64> {
265 if bytes.len() > 8 {
266 return None;
267 }
268 let mut packed = 0u64;
269 for &b in bytes {
270 if !b.is_ascii() {
271 return None;
272 }
273 packed = (packed << 8) | u64::from(b.to_ascii_lowercase());
274 }
275 Some(packed)
276}