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,370 @@
1
+ use crate::matcher;
2
+ use crate::selectors::Selector;
3
+ use crate::state::State;
4
+ use magnus::{
5
+ prelude::*, value::ReprValue, Error, RArray, RHash, Ruby, TryConvert, Value,
6
+ };
7
+ use std::collections::HashMap;
8
+ use std::ffi::c_void;
9
+
10
+ // magnus 0.8 doesn't yet expose rb_thread_call_without_gvl, so we call
11
+ // it through rb-sys. The trampoline reads pointer-to-call-args, runs the
12
+ // batch of matches, and returns the boolean encoded into the raw void
13
+ // pointer.
14
+ struct MatchAnyArgs<'a> {
15
+ snap: &'a Snapshot,
16
+ slot: u32,
17
+ selectors: &'a [&'a Selector],
18
+ state: Option<&'a State>,
19
+ }
20
+
21
+ unsafe extern "C" fn match_any_trampoline(data: *mut c_void) -> *mut c_void {
22
+ let args = &*(data as *const MatchAnyArgs);
23
+ let result = args.selectors.iter().any(|s| matcher::matches(args.snap, args.slot, s, args.state));
24
+ result as usize as *mut c_void
25
+ }
26
+
27
+ struct MatchIndicesArgs<'a> {
28
+ snap: &'a Snapshot,
29
+ slot: u32,
30
+ selectors: &'a [&'a Selector],
31
+ out: *mut Vec<u32>,
32
+ state: Option<&'a State>,
33
+ }
34
+
35
+ unsafe extern "C" fn match_indices_trampoline(data: *mut c_void) -> *mut c_void {
36
+ let args = &*(data as *const MatchIndicesArgs);
37
+ let out = &mut *args.out;
38
+
39
+ for (i, sel) in args.selectors.iter().enumerate() {
40
+ if matcher::matches(args.snap, args.slot, sel, args.state) {
41
+ out.push(i as u32);
42
+ }
43
+ }
44
+ std::ptr::null_mut()
45
+ }
46
+
47
+ fn unwrap_state(value: Value) -> Result<Option<&'static State>, Error> {
48
+ if value.is_nil() {
49
+ return Ok(None);
50
+ }
51
+
52
+ // SAFETY: the State is held alive by Ruby for the duration of the
53
+ // surrounding matches?* call.
54
+ let state: &State = TryConvert::try_convert(value)?;
55
+ Ok(Some(unsafe { std::mem::transmute::<&State, &'static State>(state) }))
56
+ }
57
+
58
+ fn unwrap_selectors(array: RArray) -> Result<Vec<&'static Selector>, Error> {
59
+ // SAFETY: We hold the GVL during this conversion. The &Selector
60
+ // references borrow from the Ruby-wrapped Selector objects, which
61
+ // remain live for at least the duration of the surrounding call
62
+ // because the Array holds strong references to them.
63
+ let mut out = Vec::with_capacity(array.len());
64
+ for v in array {
65
+ let sel: &Selector = unsafe { std::mem::transmute(magnus::TryConvert::try_convert(v).map(|s: &Selector| s)?) };
66
+ out.push(sel);
67
+ }
68
+ Ok(out)
69
+ }
70
+
71
+ // Compact, GVL-free DOM representation. All Strings are owned, all
72
+ // references are indices into `elements`, so matching can run entirely
73
+ // without holding the GVL once the snapshot is built.
74
+ #[derive(Debug)]
75
+ pub struct ElementData {
76
+ pub tag: String, // ascii-lowercased
77
+ pub id: Option<String>,
78
+ pub classes: Vec<String>, // pre-split, no whitespace
79
+ pub attrs: HashMap<String, String>,
80
+ pub parent: Option<u32>,
81
+ pub prev_sibling: Option<u32>,
82
+ pub next_sibling: Option<u32>,
83
+ pub first_child: Option<u32>, // first element child, for ancestor traversal in :disabled
84
+ // `:empty` per CSS3 — no element children and no non-whitespace text.
85
+ // Comments/processing-instructions/doctypes don't disqualify.
86
+ pub is_empty: bool,
87
+ // 1-based positions among parent's element children, used by nth-*
88
+ // and (first|last|only)-of-type. Root elements get 1/1.
89
+ pub index: u32, // among all siblings
90
+ pub last_index: u32, // among all siblings, counted from the end
91
+ pub index_of_type: u32, // among same-tag siblings
92
+ pub last_index_of_type: u32, // among same-tag siblings, counted from the end
93
+ }
94
+
95
+ #[magnus::wrap(class = "CSS::Native::Snapshot", free_immediately, size)]
96
+ pub struct Snapshot {
97
+ elements: Vec<ElementData>,
98
+ id_to_slot: HashMap<i64, u32>,
99
+ }
100
+
101
+ impl Snapshot {
102
+ pub fn from_document(doc: Value) -> Result<Snapshot, Error> {
103
+ let node_set: Value = doc.funcall("css", ("*",))?;
104
+ let nodes: RArray = node_set.funcall("to_a", ())?;
105
+ let len = nodes.len();
106
+
107
+ // First pass: assign slot indices so parent/sibling lookups can resolve.
108
+ let mut id_to_slot = HashMap::with_capacity(len);
109
+ let mut node_values: Vec<Value> = Vec::with_capacity(len);
110
+
111
+ for (i, n) in nodes.into_iter().enumerate() {
112
+ let oid: i64 = n.funcall("object_id", ())?;
113
+ id_to_slot.insert(oid, i as u32);
114
+ node_values.push(n);
115
+ }
116
+
117
+ // Second pass: extract element data.
118
+ let mut elements = Vec::with_capacity(len);
119
+ for n in &node_values {
120
+ elements.push(build_element(*n, &id_to_slot)?);
121
+ }
122
+
123
+ // Third pass: compute sibling indices for :nth-* / *-of-type.
124
+ compute_sibling_indices(&mut elements);
125
+
126
+ Ok(Snapshot { elements, id_to_slot })
127
+ }
128
+
129
+ pub fn size(&self) -> usize {
130
+ self.elements.len()
131
+ }
132
+
133
+ pub fn slot_for(&self, object_id: i64) -> Option<u32> {
134
+ self.id_to_slot.get(&object_id).copied()
135
+ }
136
+
137
+ pub fn element(&self, slot: u32) -> &ElementData {
138
+ &self.elements[slot as usize]
139
+ }
140
+
141
+ pub fn matches(&self, element: Value, selector: &Selector, state: Value) -> Result<bool, Error> {
142
+ let slot = self.resolve_slot(element)?;
143
+ let state_ref = unwrap_state(state)?;
144
+ // GVL release adds ~1μs of release/reacquire overhead, which
145
+ // exceeds the work per single-selector match. We keep this path
146
+ // GVL-held; callers wanting thread parallelism should use the
147
+ // batch API below.
148
+ Ok(matcher::matches(self, slot, selector, state_ref))
149
+ }
150
+
151
+ pub fn matches_any(&self, element: Value, selectors: RArray, state: Value) -> Result<bool, Error> {
152
+ let slot = self.resolve_slot(element)?;
153
+ let sels = unwrap_selectors(selectors)?;
154
+ let state_ref = unwrap_state(state)?;
155
+
156
+ let args = MatchAnyArgs { snap: self, slot, selectors: &sels, state: state_ref };
157
+ let data = &args as *const MatchAnyArgs as *mut c_void;
158
+
159
+ let raw = unsafe {
160
+ rb_sys::rb_thread_call_without_gvl(
161
+ Some(match_any_trampoline),
162
+ data,
163
+ None,
164
+ std::ptr::null_mut(),
165
+ )
166
+ };
167
+
168
+ Ok(raw as usize != 0)
169
+ }
170
+
171
+ pub fn match_indices(&self, element: Value, selectors: RArray, state: Value) -> Result<RArray, Error> {
172
+ let slot = self.resolve_slot(element)?;
173
+ let sels = unwrap_selectors(selectors)?;
174
+ let state_ref = unwrap_state(state)?;
175
+
176
+ let mut out_buf: Vec<u32> = Vec::with_capacity(sels.len());
177
+ let buf_ptr = &mut out_buf as *mut Vec<u32>;
178
+ let args = MatchIndicesArgs { snap: self, slot, selectors: &sels, out: buf_ptr, state: state_ref };
179
+ let data = &args as *const MatchIndicesArgs as *mut c_void;
180
+
181
+ unsafe {
182
+ rb_sys::rb_thread_call_without_gvl(
183
+ Some(match_indices_trampoline),
184
+ data,
185
+ None,
186
+ std::ptr::null_mut(),
187
+ );
188
+ }
189
+
190
+ let ruby = Ruby::get().unwrap();
191
+ let result = ruby.ary_new_capa(out_buf.len());
192
+ for i in out_buf {
193
+ result.push(i as i64)?;
194
+ }
195
+ Ok(result)
196
+ }
197
+
198
+ pub fn compile_state(&self, hash: RHash) -> Result<State, Error> {
199
+ State::compile(self, hash)
200
+ }
201
+
202
+ fn resolve_slot(&self, element: Value) -> Result<u32, Error> {
203
+ let oid: i64 = element.funcall("object_id", ())?;
204
+
205
+ self.slot_for(oid).ok_or_else(|| {
206
+ Error::new(
207
+ Ruby::get().unwrap().exception_arg_error(),
208
+ "element not present in snapshot — rebuild after DOM mutation",
209
+ )
210
+ })
211
+ }
212
+ }
213
+
214
+ fn build_element(node: Value, id_to_slot: &HashMap<i64, u32>) -> Result<ElementData, Error> {
215
+ let tag = read_tag(node)?;
216
+ let id = read_str_attr(node, "id")?;
217
+ let classes = read_classes(node)?;
218
+ let attrs = collect_attrs(node)?;
219
+ let parent = resolve_ref(node, "parent", id_to_slot, true)?;
220
+ let prev_sibling = resolve_ref(node, "previous_element", id_to_slot, false)?;
221
+ let next_sibling = resolve_ref(node, "next_element", id_to_slot, false)?;
222
+ let is_empty = compute_is_empty(node)?;
223
+
224
+ Ok(ElementData {
225
+ tag, id, classes, attrs,
226
+ parent, prev_sibling, next_sibling,
227
+ first_child: None,
228
+ is_empty,
229
+ index: 1, last_index: 1, index_of_type: 1, last_index_of_type: 1,
230
+ })
231
+ }
232
+
233
+ // Group elements by parent, then assign 1-based indices in document order
234
+ // (overall and per-tag), plus their counted-from-end counterparts.
235
+ fn compute_sibling_indices(elements: &mut [ElementData]) {
236
+ let mut groups: HashMap<Option<u32>, Vec<u32>> = HashMap::new();
237
+ for (i, el) in elements.iter().enumerate() {
238
+ groups.entry(el.parent).or_default().push(i as u32);
239
+ }
240
+
241
+ for (parent, siblings) in &groups {
242
+ let total = siblings.len() as u32;
243
+
244
+ // Populate first_child on the parent element (if any).
245
+ if let (Some(parent_slot), Some(&first)) = (parent, siblings.first()) {
246
+ elements[*parent_slot as usize].first_child = Some(first);
247
+ }
248
+
249
+ // Per-tag totals first, then per-tag running positions.
250
+ let mut totals_by_tag: HashMap<String, u32> = HashMap::new();
251
+ for &slot in siblings {
252
+ *totals_by_tag.entry(elements[slot as usize].tag.clone()).or_insert(0) += 1;
253
+ }
254
+
255
+ let mut positions_by_tag: HashMap<String, u32> = HashMap::new();
256
+ for (i, &slot) in siblings.iter().enumerate() {
257
+ let tag = elements[slot as usize].tag.clone();
258
+ let pos_in_type = positions_by_tag.entry(tag.clone()).or_insert(0);
259
+ *pos_in_type += 1;
260
+ let pos_in_type = *pos_in_type;
261
+ let total_in_type = totals_by_tag[&tag];
262
+
263
+ let el = &mut elements[slot as usize];
264
+ el.index = (i as u32) + 1;
265
+ el.last_index = total - i as u32;
266
+ el.index_of_type = pos_in_type;
267
+ el.last_index_of_type = total_in_type - pos_in_type + 1;
268
+ }
269
+ }
270
+ }
271
+
272
+ fn compute_is_empty(node: Value) -> Result<bool, Error> {
273
+ let children: RArray = node.funcall::<_, _, Value>("children", ())?.funcall("to_a", ())?;
274
+
275
+ for child in children {
276
+ if child.funcall::<_, _, bool>("element?", ()).unwrap_or(false) {
277
+ return Ok(false);
278
+ }
279
+
280
+ if child.funcall::<_, _, bool>("text?", ()).unwrap_or(false) {
281
+ let content: String = child.funcall("content", ())?;
282
+ if content.chars().any(|c| !c.is_whitespace()) {
283
+ return Ok(false);
284
+ }
285
+ }
286
+ }
287
+
288
+ Ok(true)
289
+ }
290
+
291
+ fn read_tag(node: Value) -> Result<String, Error> {
292
+ let name: String = node.funcall("name", ())?;
293
+
294
+ if name.chars().any(|c| c.is_ascii_uppercase()) {
295
+ Ok(name.to_ascii_lowercase())
296
+ } else {
297
+ Ok(name)
298
+ }
299
+ }
300
+
301
+ fn read_str_attr(node: Value, attr: &str) -> Result<Option<String>, Error> {
302
+ let v: Value = node.funcall("[]", (attr,))?;
303
+ if v.is_nil() {
304
+ Ok(None)
305
+ } else {
306
+ Ok(Some(TryConvert::try_convert(v)?))
307
+ }
308
+ }
309
+
310
+ fn read_classes(node: Value) -> Result<Vec<String>, Error> {
311
+ match read_str_attr(node, "class")? {
312
+ None => Ok(Vec::new()),
313
+ Some(s) => Ok(s.split_whitespace().map(String::from).collect()),
314
+ }
315
+ }
316
+
317
+ fn collect_attrs(node: Value) -> Result<HashMap<String, String>, Error> {
318
+ let attrs: RHash = node.funcall("attributes", ())?;
319
+ let mut map = HashMap::with_capacity(attrs.len());
320
+
321
+ attrs.foreach(|k: String, v: Value| {
322
+ let value: String = v.funcall("value", ())?;
323
+ map.insert(k, value);
324
+ Ok(magnus::r_hash::ForEach::Continue)
325
+ })?;
326
+
327
+ Ok(map)
328
+ }
329
+
330
+ // `method_name` returns nil when the relation doesn't exist (root, no
331
+ // sibling, etc.) or returns a Nokogiri node that may be a non-element
332
+ // (Document, Text). We only record the slot when it's an element we
333
+ // already indexed.
334
+ fn resolve_ref(
335
+ node: Value,
336
+ method: &str,
337
+ id_to_slot: &HashMap<i64, u32>,
338
+ check_kind: bool,
339
+ ) -> Result<Option<u32>, Error> {
340
+ let ref_val: Value = node.funcall(method, ())?;
341
+
342
+ if ref_val.is_nil() {
343
+ return Ok(None);
344
+ }
345
+
346
+ if check_kind {
347
+ let is_elem: bool = ref_val.funcall("element?", ()).unwrap_or(false);
348
+ if !is_elem {
349
+ return Ok(None);
350
+ }
351
+ }
352
+
353
+ let oid: i64 = ref_val.funcall("object_id", ())?;
354
+ Ok(id_to_slot.get(&oid).copied())
355
+ }
356
+
357
+ pub fn init(ruby: &Ruby) -> Result<(), Error> {
358
+ let css = ruby.define_module("CSS")?;
359
+ let native = css.define_module("Native")?;
360
+ let class = native.define_class("Snapshot", ruby.class_object())?;
361
+
362
+ class.define_singleton_method("from_document", magnus::function!(Snapshot::from_document, 1))?;
363
+ class.define_method("size", magnus::method!(Snapshot::size, 0))?;
364
+ class.define_method("matches?", magnus::method!(Snapshot::matches, 3))?;
365
+ class.define_method("matches_any?", magnus::method!(Snapshot::matches_any, 3))?;
366
+ class.define_method("match_indices", magnus::method!(Snapshot::match_indices, 3))?;
367
+ class.define_method("compile_state", magnus::method!(Snapshot::compile_state, 1))?;
368
+
369
+ Ok(())
370
+ }
@@ -0,0 +1,174 @@
1
+ use crate::snapshot::Snapshot;
2
+ use magnus::{prelude::*, value::ReprValue, Error, RArray, RHash, TryConvert, Value};
3
+
4
+ #[derive(Debug, Clone, Copy, PartialEq, Eq)]
5
+ pub enum StatefulKind {
6
+ Hover,
7
+ Focus,
8
+ FocusWithin,
9
+ FocusVisible,
10
+ Active,
11
+ Visited,
12
+ Target,
13
+ }
14
+
15
+ // Pseudos whose "source" elements propagate the state up the ancestor
16
+ // chain (CSS Selectors §10): if a descendant is hovered/active/contains
17
+ // focus, its ancestors also satisfy the pseudo.
18
+ const PROPAGATING: &[StatefulKind] = &[
19
+ StatefulKind::Hover,
20
+ StatefulKind::Active,
21
+ StatefulKind::FocusWithin,
22
+ ];
23
+
24
+ #[derive(Debug)]
25
+ pub enum StateValue {
26
+ None,
27
+ All,
28
+ Slots(Vec<u32>),
29
+ }
30
+
31
+ #[magnus::wrap(class = "CSS::Native::State", free_immediately, size)]
32
+ pub struct State {
33
+ hover: StateValue,
34
+ focus: StateValue,
35
+ focus_within: StateValue,
36
+ focus_visible: StateValue,
37
+ active: StateValue,
38
+ visited: StateValue,
39
+ target: StateValue,
40
+ }
41
+
42
+ impl State {
43
+ pub fn empty() -> Self {
44
+ Self {
45
+ hover: StateValue::None,
46
+ focus: StateValue::None,
47
+ focus_within: StateValue::None,
48
+ focus_visible: StateValue::None,
49
+ active: StateValue::None,
50
+ visited: StateValue::None,
51
+ target: StateValue::None,
52
+ }
53
+ }
54
+
55
+ pub fn compile(snap: &Snapshot, hash: RHash) -> Result<State, Error> {
56
+ let mut state = State::empty();
57
+
58
+ hash.foreach(|k: Value, v: Value| {
59
+ // Keys can be Symbol or String — normalize to lowercased str.
60
+ let name: String = k.funcall("to_s", ())?;
61
+
62
+ let kind = match name.as_str() {
63
+ "hover" => StatefulKind::Hover,
64
+ "focus" => StatefulKind::Focus,
65
+ "focus-within" => StatefulKind::FocusWithin,
66
+ "focus-visible" => StatefulKind::FocusVisible,
67
+ "active" => StatefulKind::Active,
68
+ "visited" => StatefulKind::Visited,
69
+ "target" => StatefulKind::Target,
70
+ _ => return Ok(magnus::r_hash::ForEach::Continue),
71
+ };
72
+
73
+ state.set(kind, compile_value(snap, v)?);
74
+
75
+ Ok(magnus::r_hash::ForEach::Continue)
76
+ })?;
77
+
78
+ Ok(state)
79
+ }
80
+
81
+ fn set(&mut self, kind: StatefulKind, value: StateValue) {
82
+ match kind {
83
+ StatefulKind::Hover => self.hover = value,
84
+ StatefulKind::Focus => self.focus = value,
85
+ StatefulKind::FocusWithin => self.focus_within = value,
86
+ StatefulKind::FocusVisible => self.focus_visible = value,
87
+ StatefulKind::Active => self.active = value,
88
+ StatefulKind::Visited => self.visited = value,
89
+ StatefulKind::Target => self.target = value,
90
+ }
91
+ }
92
+
93
+ fn get(&self, kind: StatefulKind) -> &StateValue {
94
+ match kind {
95
+ StatefulKind::Hover => &self.hover,
96
+ StatefulKind::Focus => &self.focus,
97
+ StatefulKind::FocusWithin => &self.focus_within,
98
+ StatefulKind::FocusVisible => &self.focus_visible,
99
+ StatefulKind::Active => &self.active,
100
+ StatefulKind::Visited => &self.visited,
101
+ StatefulKind::Target => &self.target,
102
+ }
103
+ }
104
+
105
+ pub fn matches(&self, snap: &Snapshot, slot: u32, kind: StatefulKind) -> bool {
106
+ match self.get(kind) {
107
+ StateValue::None => false,
108
+ StateValue::All => true,
109
+ StateValue::Slots(slots) => {
110
+ if PROPAGATING.contains(&kind) {
111
+ propagating_match(snap, slot, slots)
112
+ } else {
113
+ slots.contains(&slot)
114
+ }
115
+ }
116
+ }
117
+ }
118
+ }
119
+
120
+ fn propagating_match(snap: &Snapshot, slot: u32, sources: &[u32]) -> bool {
121
+ // For each source, walk up its ancestor chain. The element matches if
122
+ // it IS the source or any of its ancestors.
123
+ sources.iter().any(|&source| {
124
+ let mut cur = Some(source);
125
+ while let Some(s) = cur {
126
+ if s == slot {
127
+ return true;
128
+ }
129
+ cur = snap.element(s).parent;
130
+ }
131
+ false
132
+ })
133
+ }
134
+
135
+ fn compile_value(snap: &Snapshot, value: Value) -> Result<StateValue, Error> {
136
+ let ruby = magnus::Ruby::get().unwrap();
137
+
138
+ // Strict singleton check — `bool::try_convert` does a truthy/falsy
139
+ // reduction (every non-nil/non-false value → true), which would map
140
+ // an Array of elements to All. Inspect the class instead.
141
+ if value.is_nil() || value.is_kind_of(ruby.class_false_class()) {
142
+ return Ok(StateValue::None);
143
+ }
144
+ if value.is_kind_of(ruby.class_true_class()) {
145
+ return Ok(StateValue::All);
146
+ }
147
+
148
+ let array: RArray = if value.is_kind_of(ruby.class_array()) {
149
+ RArray::from_value(value).unwrap()
150
+ } else {
151
+ // Set / other Enumerable
152
+ value.funcall("to_a", ())?
153
+ };
154
+
155
+ let mut slots = Vec::with_capacity(array.len());
156
+
157
+ for el in array {
158
+ let oid: i64 = el.funcall("object_id", ())?;
159
+ if let Some(s) = snap.slot_for(oid) {
160
+ slots.push(s);
161
+ }
162
+ }
163
+
164
+ Ok(StateValue::Slots(slots))
165
+ }
166
+
167
+ pub fn init(ruby: &magnus::Ruby) -> Result<(), Error> {
168
+ let css = ruby.define_module("CSS")?;
169
+ let native = css.define_module("Native")?;
170
+
171
+ native.define_class("State", ruby.class_object())?;
172
+
173
+ Ok(())
174
+ }