p_css 0.1.9 → 0.2.0.beta1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,356 @@
1
+ use crate::selectors::{AnB, AttrMatcher, CaseFlag, Combinator, Complex, Compound, Pseudo, Selector, Simple};
2
+ use crate::snapshot::{ElementData, Snapshot};
3
+ use crate::state::{State, StatefulKind};
4
+
5
+ // Entry point. Returns true if any of the SelectorList alternatives match.
6
+ // Runs entirely against owned data — safe to call without the GVL.
7
+ pub fn matches(snap: &Snapshot, slot: u32, selector: &Selector, state: Option<&State>) -> bool {
8
+ selector.list().iter().any(|c| match_complex(snap, slot, c, state))
9
+ }
10
+
11
+ fn match_complex(snap: &Snapshot, slot: u32, complex: &Complex, state: Option<&State>) -> bool {
12
+ let last = complex.compounds.len().saturating_sub(1);
13
+ match_at(snap, Some(slot), complex, last, state)
14
+ }
15
+
16
+ fn match_at(snap: &Snapshot, slot: Option<u32>, complex: &Complex, index: usize, state: Option<&State>) -> bool {
17
+ let Some(slot) = slot else { return false };
18
+
19
+ if !match_compound(snap, slot, &complex.compounds[index], state) {
20
+ return false;
21
+ }
22
+
23
+ if index == 0 {
24
+ return true;
25
+ }
26
+
27
+ let prev = index - 1;
28
+ let element = snap.element(slot);
29
+
30
+ match complex.combinators[prev] {
31
+ Combinator::Descendant => walk_until_match(snap, element.parent, complex, prev, Step::Parent, state),
32
+ Combinator::Child => match_at(snap, element.parent, complex, prev, state),
33
+ Combinator::NextSibling => match_at(snap, element.prev_sibling, complex, prev, state),
34
+ Combinator::SubsequentSibling => walk_until_match(snap, element.prev_sibling, complex, prev, Step::PrevSibling, state),
35
+ }
36
+ }
37
+
38
+ #[derive(Clone, Copy)]
39
+ enum Step {
40
+ Parent,
41
+ PrevSibling,
42
+ }
43
+
44
+ fn walk_until_match(
45
+ snap: &Snapshot,
46
+ start: Option<u32>,
47
+ complex: &Complex,
48
+ index: usize,
49
+ step: Step,
50
+ state: Option<&State>,
51
+ ) -> bool {
52
+ let mut current = start;
53
+
54
+ while let Some(slot) = current {
55
+ if match_at(snap, Some(slot), complex, index, state) {
56
+ return true;
57
+ }
58
+
59
+ let e = snap.element(slot);
60
+ current = match step {
61
+ Step::Parent => e.parent,
62
+ Step::PrevSibling => e.prev_sibling,
63
+ };
64
+ }
65
+
66
+ false
67
+ }
68
+
69
+ fn match_compound(snap: &Snapshot, slot: u32, compound: &Compound, state: Option<&State>) -> bool {
70
+ let element = snap.element(slot);
71
+ compound.components.iter().all(|s| match_simple(snap, slot, element, s, state))
72
+ }
73
+
74
+ fn match_simple(snap: &Snapshot, slot: u32, element: &ElementData, simple: &Simple, state: Option<&State>) -> bool {
75
+ match simple {
76
+ Simple::Type(name) => &element.tag == name,
77
+ Simple::Universal => true,
78
+ Simple::Id(name) => element.id.as_deref() == Some(name.as_str()),
79
+ Simple::Class(name) => element.classes.iter().any(|c| c == name),
80
+ Simple::Attribute(attr) => match_attribute(element, attr),
81
+ Simple::PseudoClass(p) => match_pseudo(snap, slot, element, p, state),
82
+ }
83
+ }
84
+
85
+ fn match_pseudo(snap: &Snapshot, slot: u32, element: &ElementData, pseudo: &Pseudo, state: Option<&State>) -> bool {
86
+ match pseudo {
87
+ Pseudo::Root => element.parent.is_none(),
88
+ Pseudo::FirstChild => element.prev_sibling.is_none(),
89
+ Pseudo::LastChild => element.next_sibling.is_none(),
90
+ Pseudo::OnlyChild => element.prev_sibling.is_none() && element.next_sibling.is_none(),
91
+ Pseudo::Empty => element.is_empty,
92
+ Pseudo::Defined => true,
93
+ Pseudo::FirstOfType => element.index_of_type == 1,
94
+ Pseudo::LastOfType => element.last_index_of_type == 1,
95
+ Pseudo::OnlyOfType => element.index_of_type == 1 && element.last_index_of_type == 1,
96
+
97
+ // Pure-Ruby parity: nth-* require a parent. Root elements never
98
+ // satisfy them, even when the index would be 1.
99
+ Pseudo::NthChild(anb) => element.parent.is_some() && match_anb(anb, element.index),
100
+ Pseudo::NthLastChild(anb) => element.parent.is_some() && match_anb(anb, element.last_index),
101
+ Pseudo::NthOfType(anb) => element.parent.is_some() && match_anb(anb, element.index_of_type),
102
+ Pseudo::NthLastOfType(anb) => element.parent.is_some() && match_anb(anb, element.last_index_of_type),
103
+
104
+ Pseudo::Is(list) => list.iter().any(|c| match_complex(snap, slot, c, state)),
105
+ Pseudo::Not(list) => !list.iter().any(|c| match_complex(snap, slot, c, state)),
106
+
107
+ Pseudo::Link => is_link(element),
108
+ Pseudo::Enabled => is_disableable(element) && !is_disabled(snap, slot),
109
+ Pseudo::Disabled => is_disableable(element) && is_disabled(snap, slot),
110
+ Pseudo::Checked => is_checked(element),
111
+ Pseudo::Required => is_input_like(element) && element.attrs.contains_key("required"),
112
+ Pseudo::Optional => is_input_like(element) && !element.attrs.contains_key("required"),
113
+ Pseudo::ReadOnly => is_read_only(snap, slot, element),
114
+ Pseudo::ReadWrite => is_read_write(snap, slot, element),
115
+ Pseudo::PlaceholderShown => is_placeholder_shown(element),
116
+
117
+ Pseudo::Hover => stateful(state, snap, slot, StatefulKind::Hover),
118
+ Pseudo::Focus => stateful(state, snap, slot, StatefulKind::Focus),
119
+ Pseudo::FocusWithin => stateful(state, snap, slot, StatefulKind::FocusWithin),
120
+ Pseudo::FocusVisible => stateful(state, snap, slot, StatefulKind::FocusVisible),
121
+ Pseudo::Active => stateful(state, snap, slot, StatefulKind::Active),
122
+ Pseudo::Visited => stateful(state, snap, slot, StatefulKind::Visited),
123
+ Pseudo::Target => stateful(state, snap, slot, StatefulKind::Target),
124
+
125
+ Pseudo::Has => false,
126
+ Pseudo::Lang(target) => match_lang(snap, slot, target.as_deref()),
127
+ Pseudo::Dir(target) => match_dir(snap, slot, target.as_deref()),
128
+ }
129
+ }
130
+
131
+ fn match_lang(snap: &Snapshot, slot: u32, target: Option<&str>) -> bool {
132
+ let Some(target) = target else { return false };
133
+
134
+ let mut cur = Some(slot);
135
+ while let Some(s) = cur {
136
+ let el = snap.element(s);
137
+
138
+ let actual = el.attrs.get("lang").or_else(|| el.attrs.get("xml:lang"));
139
+
140
+ if let Some(actual) = actual {
141
+ let actual = actual.to_ascii_lowercase();
142
+ return actual == target
143
+ || actual.starts_with(target) && actual.as_bytes().get(target.len()) == Some(&b'-');
144
+ }
145
+
146
+ cur = el.parent;
147
+ }
148
+
149
+ false
150
+ }
151
+
152
+ fn match_dir(snap: &Snapshot, slot: u32, target: Option<&str>) -> bool {
153
+ let Some(target) = target else { return false };
154
+
155
+ let mut cur = Some(slot);
156
+ while let Some(s) = cur {
157
+ let el = snap.element(s);
158
+
159
+ if let Some(actual) = el.attrs.get("dir") {
160
+ return actual.eq_ignore_ascii_case(target);
161
+ }
162
+
163
+ cur = el.parent;
164
+ }
165
+
166
+ target == "ltr"
167
+ }
168
+
169
+ fn stateful(state: Option<&State>, snap: &Snapshot, slot: u32, kind: StatefulKind) -> bool {
170
+ state.is_some_and(|s| s.matches(snap, slot, kind))
171
+ }
172
+
173
+ const LINK_TAGS: &[&str] = &["a", "area", "link"];
174
+ const DISABLEABLE_TAGS: &[&str] = &["button", "input", "select", "textarea", "optgroup", "option", "fieldset"];
175
+ const INPUT_TAGS: &[&str] = &["input", "textarea", "select"];
176
+ const RO_INPUT_TYPES: &[&str] = &["hidden", "range", "color", "checkbox", "radio", "file", "submit", "image", "reset", "button"];
177
+
178
+ fn is_link(element: &ElementData) -> bool {
179
+ LINK_TAGS.contains(&element.tag.as_str()) && element.attrs.contains_key("href")
180
+ }
181
+
182
+ fn is_disableable(element: &ElementData) -> bool {
183
+ DISABLEABLE_TAGS.contains(&element.tag.as_str())
184
+ }
185
+
186
+ fn is_input_like(element: &ElementData) -> bool {
187
+ INPUT_TAGS.contains(&element.tag.as_str())
188
+ }
189
+
190
+ fn is_checked(element: &ElementData) -> bool {
191
+ match element.tag.as_str() {
192
+ "input" => {
193
+ let ty = element.attrs.get("type").map(String::as_str).unwrap_or("");
194
+ (ty.eq_ignore_ascii_case("checkbox") || ty.eq_ignore_ascii_case("radio"))
195
+ && element.attrs.contains_key("checked")
196
+ }
197
+ "option" => element.attrs.contains_key("selected"),
198
+ _ => false,
199
+ }
200
+ }
201
+
202
+ // `:disabled` walks the ancestor chain. A fieldset[disabled] disables every
203
+ // descendant unless that descendant sits inside the fieldset's first <legend>.
204
+ fn is_disabled(snap: &Snapshot, slot: u32) -> bool {
205
+ let element = snap.element(slot);
206
+ if element.attrs.contains_key("disabled") {
207
+ return true;
208
+ }
209
+
210
+ let mut ancestor = element.parent;
211
+ while let Some(a_slot) = ancestor {
212
+ let a = snap.element(a_slot);
213
+ if a.tag == "fieldset" && a.attrs.contains_key("disabled") {
214
+ if !is_inside_first_legend(snap, slot, a_slot) {
215
+ return true;
216
+ }
217
+ }
218
+ ancestor = a.parent;
219
+ }
220
+
221
+ false
222
+ }
223
+
224
+ fn is_inside_first_legend(snap: &Snapshot, element_slot: u32, fieldset_slot: u32) -> bool {
225
+ let Some(first_legend) = first_legend_child(snap, fieldset_slot) else {
226
+ return false;
227
+ };
228
+
229
+ let mut cur = Some(element_slot);
230
+ while let Some(s) = cur {
231
+ if s == first_legend { return true; }
232
+ if s == fieldset_slot { return false; }
233
+ cur = snap.element(s).parent;
234
+ }
235
+ false
236
+ }
237
+
238
+ fn first_legend_child(snap: &Snapshot, parent_slot: u32) -> Option<u32> {
239
+ let mut cur = snap.element(parent_slot).first_child;
240
+ while let Some(s) = cur {
241
+ if snap.element(s).tag == "legend" {
242
+ return Some(s);
243
+ }
244
+ cur = snap.element(s).next_sibling;
245
+ }
246
+ None
247
+ }
248
+
249
+ fn is_read_only(snap: &Snapshot, slot: u32, element: &ElementData) -> bool {
250
+ match element.tag.as_str() {
251
+ "input" => {
252
+ let ty = element.attrs.get("type").map(String::as_str).unwrap_or("");
253
+ if RO_INPUT_TYPES.iter().any(|t| ty.eq_ignore_ascii_case(t)) {
254
+ return true;
255
+ }
256
+ element.attrs.contains_key("readonly") || is_disabled(snap, slot)
257
+ }
258
+ "textarea" => element.attrs.contains_key("readonly") || is_disabled(snap, slot),
259
+ _ => {
260
+ let ce = element.attrs.get("contenteditable")
261
+ .map(|s| s.to_ascii_lowercase())
262
+ .unwrap_or_default();
263
+ ce.is_empty() || (ce != "true" && ce != "plaintext-only")
264
+ }
265
+ }
266
+ }
267
+
268
+ fn is_read_write(snap: &Snapshot, slot: u32, element: &ElementData) -> bool {
269
+ match element.tag.as_str() {
270
+ "input" | "textarea" => !is_read_only(snap, slot, element),
271
+ _ => {
272
+ let ce = element.attrs.get("contenteditable")
273
+ .map(|s| s.to_ascii_lowercase())
274
+ .unwrap_or_default();
275
+ ce == "true" || ce == "plaintext-only"
276
+ }
277
+ }
278
+ }
279
+
280
+ fn is_placeholder_shown(element: &ElementData) -> bool {
281
+ if element.tag != "input" && element.tag != "textarea" {
282
+ return false;
283
+ }
284
+ if !element.attrs.contains_key("placeholder") {
285
+ return false;
286
+ }
287
+ let value = element.attrs.get("value").map(String::as_str).unwrap_or("");
288
+ value.is_empty()
289
+ }
290
+
291
+ // CSS Selectors §6.6.5.1: matches when there exists a non-negative integer
292
+ // `k` such that `index == step * k + offset`. When step is zero, that
293
+ // collapses to `index == offset`.
294
+ fn match_anb(anb: &AnB, index: u32) -> bool {
295
+ let index = index as i64;
296
+
297
+ if anb.step == 0 {
298
+ return index == anb.offset;
299
+ }
300
+
301
+ let diff = index - anb.offset;
302
+ diff % anb.step == 0 && (diff / anb.step) >= 0
303
+ }
304
+
305
+ fn match_attribute(element: &ElementData, attr: &crate::selectors::AttrSel) -> bool {
306
+ let actual = match attr.name.as_str() {
307
+ "id" => element.id.as_deref(),
308
+ "class" => element.attrs.get("class").map(String::as_str),
309
+ _ => element.attrs.get(&attr.name).map(String::as_str),
310
+ };
311
+
312
+ let Some(actual) = actual else { return false };
313
+ let Some(matcher) = attr.matcher else { return true };
314
+
315
+ let needle = attr.value.as_deref().unwrap_or("");
316
+ let case_insensitive = attr.case_flag == Some(CaseFlag::I);
317
+
318
+ let cmp_eq = |h: &str, n: &str| {
319
+ if case_insensitive { h.eq_ignore_ascii_case(n) } else { h == n }
320
+ };
321
+
322
+ let starts = |h: &str, n: &str| {
323
+ if case_insensitive {
324
+ h.len() >= n.len() && h[..n.len()].eq_ignore_ascii_case(n)
325
+ } else {
326
+ h.starts_with(n)
327
+ }
328
+ };
329
+
330
+ let ends = |h: &str, n: &str| {
331
+ if case_insensitive {
332
+ h.len() >= n.len() && h[h.len() - n.len()..].eq_ignore_ascii_case(n)
333
+ } else {
334
+ h.ends_with(n)
335
+ }
336
+ };
337
+
338
+ let contains = |h: &str, n: &str| {
339
+ if case_insensitive {
340
+ h.to_ascii_lowercase().contains(&n.to_ascii_lowercase())
341
+ } else {
342
+ h.contains(n)
343
+ }
344
+ };
345
+
346
+ match matcher {
347
+ AttrMatcher::Exact => cmp_eq(actual, needle),
348
+ AttrMatcher::Includes => !needle.is_empty()
349
+ && actual.split_whitespace().any(|w| cmp_eq(w, needle)),
350
+ AttrMatcher::Dash => cmp_eq(actual, needle)
351
+ || starts(actual, &format!("{}-", needle)),
352
+ AttrMatcher::Prefix => !needle.is_empty() && starts(actual, needle),
353
+ AttrMatcher::Suffix => !needle.is_empty() && ends(actual, needle),
354
+ AttrMatcher::Substring => !needle.is_empty() && contains(actual, needle),
355
+ }
356
+ }