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,411 @@
|
|
|
1
|
+
use magnus::{
|
|
2
|
+
exception::ExceptionClass, prelude::*, value::ReprValue, Error, RArray, RClass, Ruby, TryConvert, Value,
|
|
3
|
+
};
|
|
4
|
+
|
|
5
|
+
#[derive(Debug, Clone, Copy)]
|
|
6
|
+
pub enum Combinator {
|
|
7
|
+
Descendant,
|
|
8
|
+
Child,
|
|
9
|
+
NextSibling,
|
|
10
|
+
SubsequentSibling,
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
#[derive(Debug, Clone, Copy)]
|
|
14
|
+
pub enum AttrMatcher {
|
|
15
|
+
Exact,
|
|
16
|
+
Includes,
|
|
17
|
+
Dash,
|
|
18
|
+
Prefix,
|
|
19
|
+
Suffix,
|
|
20
|
+
Substring,
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
24
|
+
pub enum CaseFlag {
|
|
25
|
+
I,
|
|
26
|
+
S,
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
#[derive(Debug)]
|
|
30
|
+
pub struct AttrSel {
|
|
31
|
+
pub name: String, // ASCII-lowercased
|
|
32
|
+
pub matcher: Option<AttrMatcher>,
|
|
33
|
+
pub value: Option<String>,
|
|
34
|
+
pub case_flag: Option<CaseFlag>,
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
#[derive(Debug)]
|
|
38
|
+
pub struct AnB {
|
|
39
|
+
pub step: i64,
|
|
40
|
+
pub offset: i64,
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
#[derive(Debug)]
|
|
44
|
+
pub enum Pseudo {
|
|
45
|
+
Root,
|
|
46
|
+
FirstChild,
|
|
47
|
+
LastChild,
|
|
48
|
+
OnlyChild,
|
|
49
|
+
Empty,
|
|
50
|
+
Defined,
|
|
51
|
+
FirstOfType,
|
|
52
|
+
LastOfType,
|
|
53
|
+
OnlyOfType,
|
|
54
|
+
NthChild(AnB),
|
|
55
|
+
NthLastChild(AnB),
|
|
56
|
+
NthOfType(AnB),
|
|
57
|
+
NthLastOfType(AnB),
|
|
58
|
+
// :is(SelectorList) / :where / :matches all share matching semantics
|
|
59
|
+
// (any selector in the list matches the element). They diverge only
|
|
60
|
+
// on specificity, which is handled by the Ruby SpecificityCalculator
|
|
61
|
+
// before we reach the matcher.
|
|
62
|
+
Is(Vec<Complex>),
|
|
63
|
+
Not(Vec<Complex>),
|
|
64
|
+
// Form / link state
|
|
65
|
+
Link,
|
|
66
|
+
Enabled,
|
|
67
|
+
Disabled,
|
|
68
|
+
Checked,
|
|
69
|
+
Required,
|
|
70
|
+
Optional,
|
|
71
|
+
ReadOnly,
|
|
72
|
+
ReadWrite,
|
|
73
|
+
PlaceholderShown,
|
|
74
|
+
// Stateful — resolved by the caller-supplied State at match time
|
|
75
|
+
Hover,
|
|
76
|
+
Focus,
|
|
77
|
+
FocusWithin,
|
|
78
|
+
FocusVisible,
|
|
79
|
+
Active,
|
|
80
|
+
Visited,
|
|
81
|
+
Target,
|
|
82
|
+
// `:has(...)` — pure Ruby always returns false. We accept any
|
|
83
|
+
// argument and match the same.
|
|
84
|
+
Has,
|
|
85
|
+
// `:lang(target)` / `:dir(target)`. None means "no usable target
|
|
86
|
+
// ident was found"; matches always false (Ruby parity).
|
|
87
|
+
Lang(Option<String>),
|
|
88
|
+
Dir(Option<String>),
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
#[derive(Debug)]
|
|
92
|
+
pub enum Simple {
|
|
93
|
+
Type(String), // ASCII-lowercased
|
|
94
|
+
Universal,
|
|
95
|
+
Id(String),
|
|
96
|
+
Class(String),
|
|
97
|
+
Attribute(AttrSel),
|
|
98
|
+
PseudoClass(Pseudo),
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
#[derive(Debug)]
|
|
102
|
+
pub struct Compound {
|
|
103
|
+
pub components: Vec<Simple>,
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
#[derive(Debug)]
|
|
107
|
+
pub struct Complex {
|
|
108
|
+
pub compounds: Vec<Compound>,
|
|
109
|
+
pub combinators: Vec<Combinator>,
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
#[magnus::wrap(class = "CSS::Native::Selector", free_immediately, size)]
|
|
113
|
+
pub struct Selector {
|
|
114
|
+
list: Vec<Complex>,
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
impl Selector {
|
|
118
|
+
pub fn list(&self) -> &[Complex] {
|
|
119
|
+
&self.list
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
pub fn compile(ast: Value) -> Result<Selector, Error> {
|
|
123
|
+
let list = match class_name(ast)?.as_str() {
|
|
124
|
+
"CSS::Selectors::SelectorList" => {
|
|
125
|
+
let selectors: RArray = ast.funcall("selectors", ())?;
|
|
126
|
+
let mut out = Vec::with_capacity(selectors.len());
|
|
127
|
+
for s in selectors {
|
|
128
|
+
out.push(convert_complex(s)?);
|
|
129
|
+
}
|
|
130
|
+
out
|
|
131
|
+
}
|
|
132
|
+
"CSS::Selectors::ComplexSelector" => vec![convert_complex(ast)?],
|
|
133
|
+
"CSS::Selectors::CompoundSelector" => vec![Complex {
|
|
134
|
+
compounds: vec![convert_compound(ast)?],
|
|
135
|
+
combinators: vec![],
|
|
136
|
+
}],
|
|
137
|
+
other => return Err(unsupported(&format!("expected SelectorList/Complex/Compound, got {}", other))),
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
Ok(Selector { list })
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
fn convert_complex(value: Value) -> Result<Complex, Error> {
|
|
145
|
+
let compounds_arr: RArray = value.funcall("compounds", ())?;
|
|
146
|
+
let combinators_arr: RArray = value.funcall("combinators", ())?;
|
|
147
|
+
|
|
148
|
+
let mut compounds = Vec::with_capacity(compounds_arr.len());
|
|
149
|
+
for c in compounds_arr {
|
|
150
|
+
compounds.push(convert_compound(c)?);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
let mut combinators = Vec::with_capacity(combinators_arr.len());
|
|
154
|
+
for c in combinators_arr {
|
|
155
|
+
combinators.push(convert_combinator(c)?);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
Ok(Complex { compounds, combinators })
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
fn convert_compound(value: Value) -> Result<Compound, Error> {
|
|
162
|
+
let components_arr: RArray = value.funcall("components", ())?;
|
|
163
|
+
let mut components = Vec::with_capacity(components_arr.len());
|
|
164
|
+
|
|
165
|
+
for c in components_arr {
|
|
166
|
+
components.push(convert_simple(c)?);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
Ok(Compound { components })
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
fn convert_simple(value: Value) -> Result<Simple, Error> {
|
|
173
|
+
let class = class_name(value)?;
|
|
174
|
+
|
|
175
|
+
match class.as_str() {
|
|
176
|
+
"CSS::Selectors::TypeSelector" => {
|
|
177
|
+
let name: String = value.funcall("name", ())?;
|
|
178
|
+
Ok(Simple::Type(ascii_lower(name)))
|
|
179
|
+
}
|
|
180
|
+
"CSS::Selectors::UniversalSelector" => Ok(Simple::Universal),
|
|
181
|
+
"CSS::Selectors::IdSelector" => {
|
|
182
|
+
let name: String = value.funcall("name", ())?;
|
|
183
|
+
Ok(Simple::Id(name))
|
|
184
|
+
}
|
|
185
|
+
"CSS::Selectors::ClassSelector" => {
|
|
186
|
+
let name: String = value.funcall("name", ())?;
|
|
187
|
+
Ok(Simple::Class(name))
|
|
188
|
+
}
|
|
189
|
+
"CSS::Selectors::AttributeSelector" => Ok(Simple::Attribute(convert_attr(value)?)),
|
|
190
|
+
"CSS::Selectors::PseudoClass" => convert_pseudo_class(value),
|
|
191
|
+
other => Err(unsupported(&format!("{} not supported by native matcher", other))),
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
fn convert_pseudo_class(value: Value) -> Result<Simple, Error> {
|
|
196
|
+
let name: String = value.funcall("name", ())?;
|
|
197
|
+
let arg: Value = value.funcall("argument", ())?;
|
|
198
|
+
let lower = name.to_ascii_lowercase();
|
|
199
|
+
|
|
200
|
+
let pseudo = match (lower.as_str(), arg.is_nil()) {
|
|
201
|
+
("root", true) => Pseudo::Root,
|
|
202
|
+
("scope", true) => Pseudo::Root, // unscoped :scope behaves like :root
|
|
203
|
+
("first-child", true) => Pseudo::FirstChild,
|
|
204
|
+
("last-child", true) => Pseudo::LastChild,
|
|
205
|
+
("only-child", true) => Pseudo::OnlyChild,
|
|
206
|
+
("empty", true) => Pseudo::Empty,
|
|
207
|
+
("defined", true) => Pseudo::Defined,
|
|
208
|
+
("first-of-type", true) => Pseudo::FirstOfType,
|
|
209
|
+
("last-of-type", true) => Pseudo::LastOfType,
|
|
210
|
+
("only-of-type", true) => Pseudo::OnlyOfType,
|
|
211
|
+
|
|
212
|
+
("link", true) | ("any-link", true) => Pseudo::Link,
|
|
213
|
+
("enabled", true) => Pseudo::Enabled,
|
|
214
|
+
("disabled", true) => Pseudo::Disabled,
|
|
215
|
+
("checked", true) => Pseudo::Checked,
|
|
216
|
+
("required", true) => Pseudo::Required,
|
|
217
|
+
("optional", true) => Pseudo::Optional,
|
|
218
|
+
("read-only", true) => Pseudo::ReadOnly,
|
|
219
|
+
("read-write", true) => Pseudo::ReadWrite,
|
|
220
|
+
("placeholder-shown", true) => Pseudo::PlaceholderShown,
|
|
221
|
+
|
|
222
|
+
("hover", true) => Pseudo::Hover,
|
|
223
|
+
("focus", true) => Pseudo::Focus,
|
|
224
|
+
("focus-within", true) => Pseudo::FocusWithin,
|
|
225
|
+
("focus-visible", true) => Pseudo::FocusVisible,
|
|
226
|
+
("active", true) => Pseudo::Active,
|
|
227
|
+
("visited", true) => Pseudo::Visited,
|
|
228
|
+
("target", true) => Pseudo::Target,
|
|
229
|
+
|
|
230
|
+
("has", _ ) => Pseudo::Has,
|
|
231
|
+
("lang", false) => Pseudo::Lang(extract_ident_argument(arg)?.map(|s| s.to_ascii_lowercase())),
|
|
232
|
+
("dir", false) => Pseudo::Dir (extract_ident_argument(arg)?.map(|s| s.to_ascii_lowercase())),
|
|
233
|
+
|
|
234
|
+
("nth-child", false) => Pseudo::NthChild(convert_anb(arg)?),
|
|
235
|
+
("nth-last-child", false) => Pseudo::NthLastChild(convert_anb(arg)?),
|
|
236
|
+
("nth-of-type", false) => Pseudo::NthOfType(convert_anb(arg)?),
|
|
237
|
+
("nth-last-of-type", false) => Pseudo::NthLastOfType(convert_anb(arg)?),
|
|
238
|
+
|
|
239
|
+
("is", false) | ("where", false) | ("matches", false) => Pseudo::Is(convert_selector_list(arg)?),
|
|
240
|
+
("not", false) => Pseudo::Not(convert_selector_list(arg)?),
|
|
241
|
+
|
|
242
|
+
_ => return Err(unsupported(&format!(
|
|
243
|
+
":{}{} not supported by native matcher",
|
|
244
|
+
name,
|
|
245
|
+
if arg.is_nil() { "" } else { "(...)" }
|
|
246
|
+
))),
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
Ok(Simple::PseudoClass(pseudo))
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
fn convert_selector_list(arg: Value) -> Result<Vec<Complex>, Error> {
|
|
253
|
+
if class_name(arg)? != "CSS::Selectors::SelectorList" {
|
|
254
|
+
return Err(unsupported(&format!(
|
|
255
|
+
"expected SelectorList argument, got {}",
|
|
256
|
+
class_name(arg)?
|
|
257
|
+
)));
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
let selectors: RArray = arg.funcall("selectors", ())?;
|
|
261
|
+
let mut out = Vec::with_capacity(selectors.len());
|
|
262
|
+
|
|
263
|
+
for s in selectors {
|
|
264
|
+
out.push(convert_complex(s)?);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
Ok(out)
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// Pure-Ruby `ident_argument`: scan the function's argument tokens for
|
|
271
|
+
// the first ident or string token and return its value. The argument
|
|
272
|
+
// arrives as a Ruby Array; non-Token entries are skipped.
|
|
273
|
+
fn extract_ident_argument(arg: Value) -> Result<Option<String>, Error> {
|
|
274
|
+
let ruby = magnus::Ruby::get().unwrap();
|
|
275
|
+
|
|
276
|
+
if !arg.is_kind_of(ruby.class_array()) {
|
|
277
|
+
return Ok(None);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
let array = RArray::from_value(arg).unwrap();
|
|
281
|
+
|
|
282
|
+
for v in array {
|
|
283
|
+
if class_name(v)? != "CSS::Token" {
|
|
284
|
+
continue;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
let type_sym: Value = v.funcall("type", ())?;
|
|
288
|
+
let type_str: String = type_sym.funcall("to_s", ())?;
|
|
289
|
+
|
|
290
|
+
if type_str == "ident" || type_str == "string" {
|
|
291
|
+
return v.funcall("value", ()).map(Some);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
Ok(None)
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
fn convert_anb(arg: Value) -> Result<AnB, Error> {
|
|
299
|
+
if class_name(arg)? != "CSS::Selectors::AnB" {
|
|
300
|
+
return Err(unsupported(&format!("nth-* argument is not AnB (got {})", class_name(arg)?)));
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
let step: i64 = arg.funcall("step", ())?;
|
|
304
|
+
let offset: i64 = arg.funcall("offset", ())?;
|
|
305
|
+
|
|
306
|
+
Ok(AnB { step, offset })
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
fn convert_attr(value: Value) -> Result<AttrSel, Error> {
|
|
310
|
+
let name: String = value.funcall("name", ())?;
|
|
311
|
+
let matcher = convert_attr_matcher(value.funcall("matcher", ())?)?;
|
|
312
|
+
let value_str = optional_string(value.funcall("value", ())?)?;
|
|
313
|
+
let case_flag = convert_case_flag(value.funcall("case_flag", ())?)?;
|
|
314
|
+
|
|
315
|
+
Ok(AttrSel {
|
|
316
|
+
name: ascii_lower(name),
|
|
317
|
+
matcher,
|
|
318
|
+
value: value_str,
|
|
319
|
+
case_flag,
|
|
320
|
+
})
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
fn convert_attr_matcher(value: Value) -> Result<Option<AttrMatcher>, Error> {
|
|
324
|
+
if value.is_nil() {
|
|
325
|
+
return Ok(None);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
let s: String = value.funcall("to_s", ())?;
|
|
329
|
+
|
|
330
|
+
Ok(Some(match s.as_str() {
|
|
331
|
+
"exact" => AttrMatcher::Exact,
|
|
332
|
+
"includes" => AttrMatcher::Includes,
|
|
333
|
+
"dash" => AttrMatcher::Dash,
|
|
334
|
+
"prefix" => AttrMatcher::Prefix,
|
|
335
|
+
"suffix" => AttrMatcher::Suffix,
|
|
336
|
+
"substring" => AttrMatcher::Substring,
|
|
337
|
+
other => return Err(unsupported(&format!("unknown attribute matcher: {}", other))),
|
|
338
|
+
}))
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
fn convert_case_flag(value: Value) -> Result<Option<CaseFlag>, Error> {
|
|
342
|
+
if value.is_nil() {
|
|
343
|
+
return Ok(None);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
let s: String = value.funcall("to_s", ())?;
|
|
347
|
+
|
|
348
|
+
Ok(Some(match s.as_str() {
|
|
349
|
+
"i" => CaseFlag::I,
|
|
350
|
+
"s" => CaseFlag::S,
|
|
351
|
+
other => return Err(unsupported(&format!("unknown case flag: {}", other))),
|
|
352
|
+
}))
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
fn convert_combinator(value: Value) -> Result<Combinator, Error> {
|
|
356
|
+
let s: String = value.funcall("to_s", ())?;
|
|
357
|
+
|
|
358
|
+
Ok(match s.as_str() {
|
|
359
|
+
"descendant" => Combinator::Descendant,
|
|
360
|
+
"child" => Combinator::Child,
|
|
361
|
+
"next_sibling" => Combinator::NextSibling,
|
|
362
|
+
"subsequent_sibling" => Combinator::SubsequentSibling,
|
|
363
|
+
other => return Err(unsupported(&format!("unknown combinator: {}", other))),
|
|
364
|
+
})
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
fn class_name(value: Value) -> Result<String, Error> {
|
|
368
|
+
let klass: RClass = value.class();
|
|
369
|
+
let name: Value = klass.funcall("name", ())?;
|
|
370
|
+
TryConvert::try_convert(name)
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
fn optional_string(value: Value) -> Result<Option<String>, Error> {
|
|
374
|
+
if value.is_nil() {
|
|
375
|
+
Ok(None)
|
|
376
|
+
} else {
|
|
377
|
+
Ok(Some(TryConvert::try_convert(value)?))
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
fn ascii_lower(s: String) -> String {
|
|
382
|
+
if s.chars().any(|c| c.is_ascii_uppercase()) {
|
|
383
|
+
s.to_ascii_lowercase()
|
|
384
|
+
} else {
|
|
385
|
+
s
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
pub fn unsupported(msg: &str) -> Error {
|
|
390
|
+
let ruby = Ruby::get().expect("must be on Ruby thread");
|
|
391
|
+
let class = ruby
|
|
392
|
+
.eval::<Value>("CSS::Native::Unsupported")
|
|
393
|
+
.ok()
|
|
394
|
+
.and_then(|v| ExceptionClass::from_value(v))
|
|
395
|
+
.unwrap_or_else(|| ruby.exception_runtime_error());
|
|
396
|
+
|
|
397
|
+
Error::new(class, msg.to_string())
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
pub fn init(ruby: &Ruby) -> Result<(), Error> {
|
|
401
|
+
let css = ruby.define_module("CSS")?;
|
|
402
|
+
let native = css.define_module("Native")?;
|
|
403
|
+
let std_err = ruby.exception_standard_error();
|
|
404
|
+
|
|
405
|
+
native.define_error("Unsupported", std_err)?;
|
|
406
|
+
|
|
407
|
+
let class = native.define_class("Selector", ruby.class_object())?;
|
|
408
|
+
class.define_singleton_method("compile", magnus::function!(Selector::compile, 1))?;
|
|
409
|
+
|
|
410
|
+
Ok(())
|
|
411
|
+
}
|