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.
- checksums.yaml +4 -4
- data/Cargo.lock +282 -0
- data/Cargo.toml +3 -0
- data/ext/css_native/Cargo.toml +12 -0
- data/ext/css_native/extconf.rb +4 -0
- data/ext/css_native/src/lib.rs +117 -0
- data/ext/css_native/src/matcher.rs +356 -0
- data/ext/css_native/src/selectors.rs +411 -0
- data/ext/css_native/src/snapshot.rs +370 -0
- data/ext/css_native/src/state.rs +174 -0
- data/ext/css_native/src/tokenizer.rs +596 -0
- data/lib/css/native.rb +179 -0
- data/lib/css/version.rb +1 -1
- metadata +34 -5
|
@@ -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
|
+
}
|