p_css 0.2.0.beta1-x86_64-linux

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.
Files changed (50) hide show
  1. checksums.yaml +7 -0
  2. data/Cargo.lock +282 -0
  3. data/Cargo.toml +3 -0
  4. data/LICENSE.txt +21 -0
  5. data/README.md +357 -0
  6. data/ext/css_native/Cargo.toml +12 -0
  7. data/ext/css_native/extconf.rb +4 -0
  8. data/ext/css_native/src/lib.rs +117 -0
  9. data/ext/css_native/src/matcher.rs +356 -0
  10. data/ext/css_native/src/selectors.rs +411 -0
  11. data/ext/css_native/src/snapshot.rs +370 -0
  12. data/ext/css_native/src/state.rs +174 -0
  13. data/ext/css_native/src/tokenizer.rs +596 -0
  14. data/lib/css/3.3/css_native.so +0 -0
  15. data/lib/css/3.4/css_native.so +0 -0
  16. data/lib/css/4.0/css_native.so +0 -0
  17. data/lib/css/cascade.rb +277 -0
  18. data/lib/css/code_points.rb +59 -0
  19. data/lib/css/escape.rb +82 -0
  20. data/lib/css/media_queries/context.rb +60 -0
  21. data/lib/css/media_queries/evaluator.rb +157 -0
  22. data/lib/css/media_queries/nodes.rb +41 -0
  23. data/lib/css/media_queries/parser.rb +374 -0
  24. data/lib/css/media_queries.rb +9 -0
  25. data/lib/css/native.rb +179 -0
  26. data/lib/css/nesting.rb +229 -0
  27. data/lib/css/nodes.rb +42 -0
  28. data/lib/css/parser.rb +429 -0
  29. data/lib/css/selectors/anb_parser.rb +174 -0
  30. data/lib/css/selectors/matcher.rb +545 -0
  31. data/lib/css/selectors/nodes.rb +61 -0
  32. data/lib/css/selectors/parser.rb +395 -0
  33. data/lib/css/selectors/serializer.rb +102 -0
  34. data/lib/css/selectors/specificity.rb +81 -0
  35. data/lib/css/selectors.rb +11 -0
  36. data/lib/css/serializer.rb +167 -0
  37. data/lib/css/token.rb +107 -0
  38. data/lib/css/token_cursor.rb +49 -0
  39. data/lib/css/tokenizer.rb +447 -0
  40. data/lib/css/urange.rb +45 -0
  41. data/lib/css/version.rb +3 -0
  42. data/lib/css.rb +73 -0
  43. data/lib/p_css.rb +1 -0
  44. data/sig/css/cascade.rbs +22 -0
  45. data/sig/css/media_queries.rbs +107 -0
  46. data/sig/css/nodes.rbs +76 -0
  47. data/sig/css/selectors.rbs +164 -0
  48. data/sig/css/token.rbs +33 -0
  49. data/sig/css.rbs +99 -0
  50. metadata +113 -0
@@ -0,0 +1,545 @@
1
+ module CSS
2
+ module Selectors
3
+ # Matches a Selector AST against any duck-typed element. Required
4
+ # methods on the element:
5
+ #
6
+ # - `name` (or `tag_name`) — tag name
7
+ # - `[](attr)` — attribute value or nil
8
+ # - `parent` — parent element or non-element
9
+ # - `previous_element` (or `previous_element_sibling`) — preceding
10
+ # element sibling
11
+ # - `next_element` (or `next_element_sibling`) — following
12
+ # element sibling
13
+ # - `children` (and optionally `element_children`) — child nodes
14
+ #
15
+ # `Nokogiri::XML::Element` and `Nokogiri::HTML::Element` satisfy this
16
+ # protocol out of the box.
17
+ #
18
+ # Pseudo-classes that depend on user-agent state (`:hover`, `:focus`,
19
+ # `:visited`, etc.) return false by default; pass an explicit `state:`
20
+ # mapping to opt into stateful matching. Validity-API and viewport-
21
+ # only states (`:fullscreen`, `:valid`, …) are not exposed.
22
+ module Matcher
23
+ extend self
24
+
25
+ DISABLEABLE_TAGS = %w[button input select textarea optgroup option fieldset].freeze
26
+ INPUT_TAGS = %w[input textarea select].freeze
27
+ LINK_TAGS = %w[a area link].freeze
28
+ RO_INPUT_TYPES = %w[hidden range color checkbox radio file submit image reset button].freeze
29
+
30
+ # User-agent state pseudos. The matcher returns `false` for these
31
+ # unless the caller passes a `state:` Hash describing which
32
+ # elements (or "all") should match.
33
+ STATEFUL_PSEUDOS = %w[hover focus focus-within focus-visible active visited target].to_set.freeze
34
+
35
+ # Per spec these states propagate up the ancestor chain — if a
36
+ # descendant is hovered/active/contains-focus, the ancestors
37
+ # share the state for selector-matching purposes.
38
+ PROPAGATING_STATEFUL_PSEUDOS = %w[hover active focus-within].to_set.freeze
39
+
40
+ # Per-element cache used to avoid recomputing tag / id / class set
41
+ # for every selector in a hot loop (e.g. `Cascade#resolve` against
42
+ # hundreds of rules). Keyed by `Object#object_id`; only valid for
43
+ # the duration of a single matcher invocation.
44
+ Context = Data.define(:tag, :id, :classes)
45
+
46
+ EMPTY_CLASSES = [].freeze
47
+
48
+ def matches?(element, selector, cache: nil, state: nil)
49
+ sel = selector.is_a?(String) ? Parser.parse_selector_list(selector) : selector
50
+
51
+ case sel
52
+ when SelectorList
53
+ sel.selectors.any? { match_complex(element, _1, cache, state) }
54
+ when ComplexSelector
55
+ match_complex(element, sel, cache, state)
56
+ when CompoundSelector
57
+ match_compound(element, sel, cache, state)
58
+ else
59
+ raise ArgumentError, "expected a selector node or string, got #{sel.class}"
60
+ end
61
+ end
62
+
63
+ private
64
+
65
+ # Walks the complex selector right-to-left starting at the rightmost
66
+ # compound. Each combinator either succeeds against ancestors /
67
+ # siblings of the current candidate or fails the whole match.
68
+ def match_complex(element, complex, cache, state)
69
+ match_at(element, complex, complex.compounds.size - 1, cache, state)
70
+ end
71
+
72
+ def match_at(element, complex, index, cache, state)
73
+ return false if element.nil?
74
+ return false unless match_compound(element, complex.compounds[index], cache, state)
75
+ return true if index.zero?
76
+
77
+ prev = index - 1
78
+
79
+ case complex.combinators[prev]
80
+ when :descendant then walk_until_match(element, complex, prev, :parent_element, cache, state)
81
+ when :child then match_at(parent_element(element), complex, prev, cache, state)
82
+ when :next_sibling then match_at(previous_element(element), complex, prev, cache, state)
83
+ when :subsequent_sibling then walk_until_match(element, complex, prev, :previous_element, cache, state)
84
+ end
85
+ end
86
+
87
+ # Steps along the DOM via `direction` until a candidate matches the
88
+ # remaining complex selector or the chain runs out.
89
+ def walk_until_match(element, complex, index, direction, cache, state)
90
+ candidate = send(direction, element)
91
+
92
+ while candidate
93
+ return true if match_at(candidate, complex, index, cache, state)
94
+
95
+ candidate = send(direction, candidate)
96
+ end
97
+
98
+ false
99
+ end
100
+
101
+ def match_compound(element, compound, cache, state)
102
+ compound.components.all? { match_simple(element, _1, cache, state) }
103
+ end
104
+
105
+ def match_simple(element, simple, cache, state)
106
+ case simple
107
+ when TypeSelector then tag_of(element, cache).casecmp?(simple.name)
108
+ when UniversalSelector then true
109
+ when IdSelector then id_of(element, cache) == simple.name
110
+ when ClassSelector then classes_of(element, cache).include?(simple.name)
111
+ when AttributeSelector then match_attribute(element, simple)
112
+ when PseudoClass then match_pseudo_class(element, simple, cache, state)
113
+ when PseudoElement then false
114
+ when NestingSelector then false
115
+ else false
116
+ end
117
+ end
118
+
119
+ # Public — used by `Cascade` for both rule indexing and matching;
120
+ # callers can share a `cache` Hash with `matches?(cache: cache)`
121
+ # so each element pays for its tag / id / class set at most once.
122
+ public
123
+
124
+ def tag_of(element, cache = nil)
125
+ ctx = context_for(element, cache)
126
+ ctx ? ctx.tag : tag(element)
127
+ end
128
+
129
+ def id_of(element, cache = nil)
130
+ ctx = context_for(element, cache)
131
+ ctx ? ctx.id : attr(element, 'id')
132
+ end
133
+
134
+ def classes_of(element, cache = nil)
135
+ ctx = context_for(element, cache)
136
+ ctx ? ctx.classes : build_class_set(element)
137
+ end
138
+
139
+ private
140
+
141
+ def context_for(element, cache)
142
+ return nil if cache.nil?
143
+
144
+ cache[element.object_id] ||= Context.new(
145
+ tag: tag(element),
146
+ id: attr(element, 'id'),
147
+ classes: build_class_set(element)
148
+ )
149
+ end
150
+
151
+ # Returns an Array of class names. We deliberately don't wrap in a Set:
152
+ # construction allocates two objects (Array + Set), and on the typical
153
+ # 1–5 classes per element, Array#include? is fast enough that the
154
+ # construction win dominates the lookup penalty.
155
+ def build_class_set(element)
156
+ v = attr(element, 'class')
157
+ return EMPTY_CLASSES if v.nil? || v.empty?
158
+
159
+ v.to_s.split(' ')
160
+ end
161
+
162
+ # Attribute matching ----------------------------------------------
163
+
164
+ def match_attribute(element, attr_sel)
165
+ actual = attr(element, attr_sel.name)
166
+
167
+ return false if actual.nil?
168
+ return true if attr_sel.matcher.nil?
169
+
170
+ haystack = actual.to_s
171
+ needle = attr_sel.value.to_s
172
+
173
+ if attr_sel.case_flag == :i
174
+ haystack = haystack.downcase
175
+ needle = needle.downcase
176
+ end
177
+
178
+ case attr_sel.matcher
179
+ when :exact then haystack == needle
180
+ when :includes then !needle.empty? && haystack.split(/\s+/).include?(needle)
181
+ when :dash then haystack == needle || haystack.start_with?("#{needle}-")
182
+ when :prefix then !needle.empty? && haystack.start_with?(needle)
183
+ when :suffix then !needle.empty? && haystack.end_with?(needle)
184
+ when :substring then !needle.empty? && haystack.include?(needle)
185
+ end
186
+ end
187
+
188
+ # Pseudo-class matching -------------------------------------------
189
+
190
+ def match_pseudo_class(element, pc, cache, state)
191
+ name = pc.name.downcase
192
+
193
+ return match_stateful_pseudo?(name, element, state) if STATEFUL_PSEUDOS.include?(name)
194
+
195
+ case name
196
+ when 'is', 'where', 'matches' then match_selector_list_arg(element, pc.argument, cache, state)
197
+ when 'not' then negate_selector_list_arg(element, pc.argument, cache, state)
198
+ when 'has' then false
199
+ when 'root' then parent_element(element).nil?
200
+ when 'scope' then parent_element(element).nil?
201
+ when 'first-child' then previous_element(element).nil?
202
+ when 'last-child' then next_element(element).nil?
203
+ when 'only-child' then previous_element(element).nil? && next_element(element).nil?
204
+ when 'first-of-type' then same_type_previous(element).nil?
205
+ when 'last-of-type' then same_type_next(element).nil?
206
+ when 'only-of-type' then same_type_previous(element).nil? && same_type_next(element).nil?
207
+ when 'nth-child' then match_nth(element, pc.argument, of_type: false, from_end: false)
208
+ when 'nth-last-child' then match_nth(element, pc.argument, of_type: false, from_end: true)
209
+ when 'nth-of-type' then match_nth(element, pc.argument, of_type: true, from_end: false)
210
+ when 'nth-last-of-type' then match_nth(element, pc.argument, of_type: true, from_end: true)
211
+ when 'empty' then empty?(element)
212
+ when 'link', 'any-link' then link?(element)
213
+ when 'enabled' then disableable?(element) && !disabled?(element)
214
+ when 'disabled' then disabled?(element)
215
+ when 'checked' then checked?(element)
216
+ when 'required' then required?(element)
217
+ when 'optional' then optional?(element)
218
+ when 'read-only' then read_only?(element)
219
+ when 'read-write' then read_write?(element)
220
+ when 'placeholder-shown' then placeholder_shown?(element)
221
+ when 'lang' then match_lang(element, pc.argument)
222
+ when 'dir' then match_dir(element, pc.argument)
223
+ when 'defined' then true
224
+ else false
225
+ end
226
+ end
227
+
228
+ # `:hover` / `:active` / `:focus-within` propagate up the ancestor
229
+ # chain per Selectors §10 — the Set members are the *source* nodes
230
+ # (e.g. the deepest hovered element) and any of their ancestors
231
+ # also matches. Other stateful pseudos match only the explicit
232
+ # elements in the Set.
233
+ def match_stateful_pseudo?(name, element, state)
234
+ return false if state.nil?
235
+
236
+ value = state[name.to_sym] || state[name]
237
+
238
+ return false if value.nil? || value == false
239
+ return true if value == true
240
+
241
+ return value.include?(element) unless PROPAGATING_STATEFUL_PSEUDOS.include?(name)
242
+
243
+ value.each do |source|
244
+ cur = source
245
+
246
+ while cur
247
+ return true if cur == element
248
+
249
+ cur = parent_element(cur)
250
+ end
251
+ end
252
+
253
+ false
254
+ end
255
+
256
+ def match_selector_list_arg(element, arg, cache, state)
257
+ arg.is_a?(SelectorList) && matches?(element, arg, cache: cache, state: state)
258
+ end
259
+
260
+ def negate_selector_list_arg(element, arg, cache, state)
261
+ arg.is_a?(SelectorList) && !matches?(element, arg, cache: cache, state: state)
262
+ end
263
+
264
+ def match_nth(element, anb, of_type:, from_end:)
265
+ return false unless anb.is_a?(AnB)
266
+
267
+ index = nth_index(element, of_type:, from_end:)
268
+
269
+ return false if index.nil?
270
+
271
+ step = anb.step
272
+ offset = anb.offset
273
+
274
+ if step.zero?
275
+ index == offset
276
+ else
277
+ diff = index - offset
278
+ (diff % step).zero? && (diff / step) >= 0
279
+ end
280
+ end
281
+
282
+ def nth_index(element, of_type:, from_end:)
283
+ p = parent_element(element)
284
+
285
+ return nil if p.nil?
286
+
287
+ siblings = element_children(p)
288
+ siblings = siblings.select { tag(_1).casecmp?(tag(element)) } if of_type
289
+ siblings = siblings.reverse if from_end
290
+
291
+ idx = siblings.index { same_node?(_1, element) }
292
+ idx && idx + 1
293
+ end
294
+
295
+ # Form / link state -----------------------------------------------
296
+
297
+ def link?(element)
298
+ LINK_TAGS.include?(tag(element)) && !attr(element, 'href').nil?
299
+ end
300
+
301
+ def disableable?(element)
302
+ DISABLEABLE_TAGS.include?(tag(element))
303
+ end
304
+
305
+ def disabled?(element)
306
+ return false unless disableable?(element)
307
+ return true if attr(element, 'disabled')
308
+
309
+ ancestor = parent_element(element)
310
+
311
+ while ancestor
312
+ if tag(ancestor) == 'fieldset' && attr(ancestor, 'disabled')
313
+ return true unless inside_first_legend?(element, ancestor)
314
+ end
315
+
316
+ ancestor = parent_element(ancestor)
317
+ end
318
+
319
+ false
320
+ end
321
+
322
+ def inside_first_legend?(element, fieldset)
323
+ first_legend = element_children(fieldset).find { tag(_1) == 'legend' }
324
+
325
+ return false if first_legend.nil?
326
+
327
+ ancestor = element
328
+
329
+ while ancestor
330
+ return true if same_node?(ancestor, first_legend)
331
+ break if same_node?(ancestor, fieldset)
332
+
333
+ ancestor = parent_element(ancestor)
334
+ end
335
+
336
+ false
337
+ end
338
+
339
+ def checked?(element)
340
+ case tag(element)
341
+ when 'input'
342
+ %w[checkbox radio].include?(attr(element, 'type').to_s.downcase) && !attr(element, 'checked').nil?
343
+ when 'option'
344
+ !attr(element, 'selected').nil?
345
+ else
346
+ false
347
+ end
348
+ end
349
+
350
+ def required?(element)
351
+ INPUT_TAGS.include?(tag(element)) && !attr(element, 'required').nil?
352
+ end
353
+
354
+ def optional?(element)
355
+ INPUT_TAGS.include?(tag(element)) && attr(element, 'required').nil?
356
+ end
357
+
358
+ def read_only?(element)
359
+ case tag(element)
360
+ when 'input'
361
+ type = attr(element, 'type').to_s.downcase
362
+ return true if RO_INPUT_TYPES.include?(type)
363
+
364
+ !attr(element, 'readonly').nil? || disabled?(element)
365
+ when 'textarea'
366
+ !attr(element, 'readonly').nil? || disabled?(element)
367
+ else
368
+ ce = attr(element, 'contenteditable').to_s.downcase
369
+ ce.empty? || (ce != 'true' && ce != 'plaintext-only')
370
+ end
371
+ end
372
+
373
+ def read_write?(element)
374
+ return !read_only?(element) if %w[input textarea].include?(tag(element))
375
+
376
+ ce = attr(element, 'contenteditable').to_s.downcase
377
+ ce == 'true' || ce == 'plaintext-only'
378
+ end
379
+
380
+ def placeholder_shown?(element)
381
+ return false unless %w[input textarea].include?(tag(element))
382
+ return false if attr(element, 'placeholder').nil?
383
+
384
+ v = attr(element, 'value')
385
+ v.nil? || v.empty?
386
+ end
387
+
388
+ def match_lang(element, argument)
389
+ target = ident_argument(argument)
390
+
391
+ return false if target.nil?
392
+
393
+ target = target.downcase
394
+ ancestor = element
395
+
396
+ while ancestor
397
+ actual = attr(ancestor, 'lang') || attr(ancestor, 'xml:lang')
398
+
399
+ if actual
400
+ actual = actual.to_s.downcase
401
+ return actual == target || actual.start_with?("#{target}-")
402
+ end
403
+
404
+ ancestor = parent_element(ancestor)
405
+ end
406
+
407
+ false
408
+ end
409
+
410
+ def match_dir(element, argument)
411
+ target = ident_argument(argument)
412
+
413
+ return false if target.nil?
414
+
415
+ target = target.downcase
416
+ ancestor = element
417
+
418
+ while ancestor
419
+ actual = attr(ancestor, 'dir')
420
+
421
+ if actual
422
+ return actual.to_s.downcase == target
423
+ end
424
+
425
+ ancestor = parent_element(ancestor)
426
+ end
427
+
428
+ target == 'ltr'
429
+ end
430
+
431
+ def ident_argument(argument)
432
+ return nil unless argument.is_a?(Array)
433
+
434
+ token = argument.find { _1.is_a?(Token) && (_1.type == :ident || _1.type == :string) }
435
+ token&.value
436
+ end
437
+
438
+ # CSS3 :empty semantics — element children always disqualify;
439
+ # whitespace-only text content does not. Comments / PIs / doctypes
440
+ # are ignored.
441
+ def empty?(element)
442
+ return false unless element.respond_to?(:children)
443
+
444
+ element.children.each do |child|
445
+ if child.respond_to?(:element?) && child.element?
446
+ return false
447
+ end
448
+
449
+ if child.respond_to?(:text?) && child.text?
450
+ content = child.respond_to?(:content) ? child.content : child.text
451
+ return false if content.to_s.match?(/\S/)
452
+ end
453
+ end
454
+
455
+ true
456
+ end
457
+
458
+ # Element protocol helpers ---------------------------------------
459
+
460
+ # Callers (LINK_TAGS.include?, case statements) compare against
461
+ # lowercase literals, so the result must be lowercase. But Nokogiri's
462
+ # HTML parsers already emit lowercase names — the .downcase only fires
463
+ # in XML / uppercase-tag cases. Skip the allocation when there's
464
+ # nothing to lower.
465
+ def tag(element)
466
+ name = element.respond_to?(:tag_name) ? element.tag_name : element.name
467
+ name = name.to_s
468
+ name.match?(/[A-Z]/) ? name.downcase : name
469
+ end
470
+
471
+ def attr(element, name)
472
+ v = element[name]
473
+ return v unless v.nil?
474
+
475
+ lower = name.downcase
476
+ return nil if name == lower
477
+
478
+ element[lower]
479
+ end
480
+
481
+ def parent_element(element)
482
+ p = element.respond_to?(:parent) ? element.parent : nil
483
+
484
+ return nil if p.nil?
485
+ return nil if p.respond_to?(:element?) && !p.element?
486
+
487
+ p
488
+ end
489
+
490
+ SIBLING_METHODS = {
491
+ previous: %i[previous_element previous_element_sibling previous_sibling],
492
+ next: %i[next_element next_element_sibling next_sibling]
493
+ }.freeze
494
+
495
+ def previous_element(element) = adjacent_element(element, :previous)
496
+ def next_element(element) = adjacent_element(element, :next)
497
+
498
+ def adjacent_element(element, direction)
499
+ primary, alt, fallback = SIBLING_METHODS.fetch(direction)
500
+
501
+ return element.send(primary) if element.respond_to?(primary)
502
+ return element.send(alt) if element.respond_to?(alt)
503
+
504
+ walk_sibling(element, fallback)
505
+ end
506
+
507
+ def walk_sibling(element, direction)
508
+ sib = element.respond_to?(direction) ? element.send(direction) : nil
509
+
510
+ until sib.nil?
511
+ return sib if !sib.respond_to?(:element?) || sib.element?
512
+
513
+ sib = sib.respond_to?(direction) ? sib.send(direction) : nil
514
+ end
515
+
516
+ nil
517
+ end
518
+
519
+ def element_children(element)
520
+ return element.element_children.to_a if element.respond_to?(:element_children)
521
+ return [] unless element.respond_to?(:children)
522
+
523
+ element.children.select {|c|
524
+ c.respond_to?(:element?) ? c.element? : false
525
+ }
526
+ end
527
+
528
+ def same_type_previous(element)
529
+ sib = previous_element(element)
530
+ sib = previous_element(sib) until sib.nil? || tag(sib).casecmp?(tag(element))
531
+ sib
532
+ end
533
+
534
+ def same_type_next(element)
535
+ sib = next_element(element)
536
+ sib = next_element(sib) until sib.nil? || tag(sib).casecmp?(tag(element))
537
+ sib
538
+ end
539
+
540
+ def same_node?(a, b)
541
+ a.equal?(b) || a == b
542
+ end
543
+ end
544
+ end
545
+ end
@@ -0,0 +1,61 @@
1
+ module CSS
2
+ module Selectors
3
+ # Marker module included by every selector AST data class. Used by the
4
+ # main `CSS.serialize` to dispatch into `Selectors::Serializer`.
5
+ module Node; end
6
+
7
+ # A comma-separated list of complex selectors.
8
+ SelectorList = Data.define(:selectors) do
9
+ include Node
10
+ def to_s = Selectors::Serializer.serialize(self)
11
+ end
12
+
13
+ # Compounds connected by combinators. `compounds.size == combinators.size + 1`.
14
+ # `combinators[i]` connects `compounds[i]` to `compounds[i + 1]`.
15
+ ComplexSelector = Data.define(:compounds, :combinators) do
16
+ include Node
17
+ def to_s = Selectors::Serializer.serialize(self)
18
+ end
19
+
20
+ # A run of simple selectors with no combinators between them, e.g.
21
+ # `a.foo:hover` or `[href]:not(:visited)`.
22
+ CompoundSelector = Data.define(:components) do
23
+ include Node
24
+ def to_s = Selectors::Serializer.serialize(self)
25
+ end
26
+
27
+ TypeSelector = Data.define(:name) { include Node }
28
+ UniversalSelector = Data.define { include Node }
29
+ NestingSelector = Data.define { include Node }
30
+ IdSelector = Data.define(:name) { include Node }
31
+ ClassSelector = Data.define(:name) { include Node }
32
+
33
+ # Attribute matchers:
34
+ # nil — `[name]` (presence)
35
+ # :exact — `[a=b]`
36
+ # :includes — `[a~=b]`
37
+ # :dash — `[a|=b]`
38
+ # :prefix — `[a^=b]`
39
+ # :suffix — `[a$=b]`
40
+ # :substring — `[a*=b]`
41
+ #
42
+ # `case_flag` is `nil`, `:i`, or `:s`.
43
+ AttributeSelector = Data.define(:name, :matcher, :value, :case_flag) do
44
+ include Node
45
+ end
46
+
47
+ # `argument` is `nil`, a `SelectorList` (`:not/:is/:where/:has`), an
48
+ # `AnB` (`:nth-*`), or a raw `Array<Token>` for unrecognized functional
49
+ # pseudos.
50
+ PseudoClass = Data.define(:name, :argument) { include Node }
51
+ PseudoElement = Data.define(:name, :argument) { include Node }
52
+
53
+ # `An+B` integer pair. `step` is the `n` coefficient, `offset` is the
54
+ # constant term. `even` => AnB(2, 0), `odd` => AnB(2, 1), `5` => AnB(0, 5),
55
+ # `n` => AnB(1, 0).
56
+ AnB = Data.define(:step, :offset) do
57
+ include Node
58
+ def to_s = Selectors::Serializer.serialize(self)
59
+ end
60
+ end
61
+ end