p_css 0.2.0.beta1-aarch64-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.
- checksums.yaml +7 -0
- data/Cargo.lock +282 -0
- data/Cargo.toml +3 -0
- data/LICENSE.txt +21 -0
- data/README.md +357 -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/3.3/css_native.so +0 -0
- data/lib/css/3.4/css_native.so +0 -0
- data/lib/css/4.0/css_native.so +0 -0
- data/lib/css/cascade.rb +277 -0
- data/lib/css/code_points.rb +59 -0
- data/lib/css/escape.rb +82 -0
- data/lib/css/media_queries/context.rb +60 -0
- data/lib/css/media_queries/evaluator.rb +157 -0
- data/lib/css/media_queries/nodes.rb +41 -0
- data/lib/css/media_queries/parser.rb +374 -0
- data/lib/css/media_queries.rb +9 -0
- data/lib/css/native.rb +179 -0
- data/lib/css/nesting.rb +229 -0
- data/lib/css/nodes.rb +42 -0
- data/lib/css/parser.rb +429 -0
- data/lib/css/selectors/anb_parser.rb +174 -0
- data/lib/css/selectors/matcher.rb +545 -0
- data/lib/css/selectors/nodes.rb +61 -0
- data/lib/css/selectors/parser.rb +395 -0
- data/lib/css/selectors/serializer.rb +102 -0
- data/lib/css/selectors/specificity.rb +81 -0
- data/lib/css/selectors.rb +11 -0
- data/lib/css/serializer.rb +167 -0
- data/lib/css/token.rb +107 -0
- data/lib/css/token_cursor.rb +49 -0
- data/lib/css/tokenizer.rb +447 -0
- data/lib/css/urange.rb +45 -0
- data/lib/css/version.rb +3 -0
- data/lib/css.rb +73 -0
- data/lib/p_css.rb +1 -0
- data/sig/css/cascade.rbs +22 -0
- data/sig/css/media_queries.rbs +107 -0
- data/sig/css/nodes.rbs +76 -0
- data/sig/css/selectors.rbs +164 -0
- data/sig/css/token.rbs +33 -0
- data/sig/css.rbs +99 -0
- 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
|