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,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
+ }