rexml-css_selector 0.1.0
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/.editorconfig +8 -0
- data/.rubocop.yml +48 -0
- data/CODE_OF_CONDUCT.md +84 -0
- data/LICENSE.txt +21 -0
- data/README.md +74 -0
- data/Rakefile +27 -0
- data/example/prism.rb +13 -0
- data/lib/rexml/css_selector/adapters/prism_adapter.rb +77 -0
- data/lib/rexml/css_selector/adapters/rexml_adapter.rb +77 -0
- data/lib/rexml/css_selector/ast.rb +107 -0
- data/lib/rexml/css_selector/base_adapter.rb +88 -0
- data/lib/rexml/css_selector/compiler.rb +287 -0
- data/lib/rexml/css_selector/parser.rb +430 -0
- data/lib/rexml/css_selector/pseudo_class_def.rb +19 -0
- data/lib/rexml/css_selector/queries/adjacent_query.rb +18 -0
- data/lib/rexml/css_selector/queries/attribute_matcher_query.rb +58 -0
- data/lib/rexml/css_selector/queries/attribute_presence_query.rb +20 -0
- data/lib/rexml/css_selector/queries/checked_query.rb +18 -0
- data/lib/rexml/css_selector/queries/child_query.rb +18 -0
- data/lib/rexml/css_selector/queries/class_name_query.rb +18 -0
- data/lib/rexml/css_selector/queries/descendant_query.rb +41 -0
- data/lib/rexml/css_selector/queries/disabled_query.rb +18 -0
- data/lib/rexml/css_selector/queries/empty_query.rb +17 -0
- data/lib/rexml/css_selector/queries/has_query.rb +34 -0
- data/lib/rexml/css_selector/queries/id_query.rb +18 -0
- data/lib/rexml/css_selector/queries/nested_query.rb +18 -0
- data/lib/rexml/css_selector/queries/not_query.rb +18 -0
- data/lib/rexml/css_selector/queries/nth_child_of_query.rb +40 -0
- data/lib/rexml/css_selector/queries/nth_child_query.rb +32 -0
- data/lib/rexml/css_selector/queries/nth_last_child_of_query.rb +40 -0
- data/lib/rexml/css_selector/queries/nth_last_child_query.rb +33 -0
- data/lib/rexml/css_selector/queries/nth_last_of_type_query.rb +44 -0
- data/lib/rexml/css_selector/queries/nth_of_type_query.rb +44 -0
- data/lib/rexml/css_selector/queries/one_of_query.rb +17 -0
- data/lib/rexml/css_selector/queries/only_child_query.rb +23 -0
- data/lib/rexml/css_selector/queries/only_of_type_query.rb +34 -0
- data/lib/rexml/css_selector/queries/root_query.rb +17 -0
- data/lib/rexml/css_selector/queries/scope_query.rb +17 -0
- data/lib/rexml/css_selector/queries/sibling_query.rb +21 -0
- data/lib/rexml/css_selector/queries/tag_name_type_query.rb +30 -0
- data/lib/rexml/css_selector/queries/true_query.rb +15 -0
- data/lib/rexml/css_selector/queries/universal_type_query.rb +22 -0
- data/lib/rexml/css_selector/query_context.rb +25 -0
- data/lib/rexml/css_selector/version.rb +8 -0
- data/lib/rexml/css_selector.rb +197 -0
- data/sig/rexml/css_selector.rbs +5 -0
- metadata +137 -0
@@ -0,0 +1,430 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module REXML
|
4
|
+
module CSSSelector
|
5
|
+
# ParseError is an error on parsing.
|
6
|
+
class ParseError < Error
|
7
|
+
def initialize(message, pos)
|
8
|
+
super("#{message} (at #{pos})")
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
# Parser is a CSS selector parser.
|
13
|
+
class Parser
|
14
|
+
def initialize(**config)
|
15
|
+
@config = config
|
16
|
+
@config[:pseudo_classes] ||= {}
|
17
|
+
end
|
18
|
+
|
19
|
+
def parse(source)
|
20
|
+
old_scanner = @scanner
|
21
|
+
@scanner = StringScanner.new(source)
|
22
|
+
@scanner.scan RE_WS
|
23
|
+
parse_selector_list
|
24
|
+
ensure
|
25
|
+
@scanner = old_scanner
|
26
|
+
end
|
27
|
+
|
28
|
+
# See https://www.w3.org/TR/css-syntax-3/#token-diagrams and https://www.w3.org/TR/css-syntax-3/#tokenizer-definitions.
|
29
|
+
|
30
|
+
# :stopdoc:
|
31
|
+
|
32
|
+
RE_NEWLINE = /\r\n|[\n\r\f]/
|
33
|
+
RE_WHITESPACE = /#{RE_NEWLINE}|[ \t]/
|
34
|
+
RE_WS = /(?:#{RE_WHITESPACE})*/
|
35
|
+
RE_ESCAPE = /\\(?:(?!#{RE_NEWLINE})\H|\h{1,6}(?:#{RE_WHITESPACE})?)/
|
36
|
+
RE_ESCAPE_NEWLINE = /#{RE_ESCAPE}|\\#{RE_NEWLINE}/
|
37
|
+
RE_IDENT_START = /[a-zA-Z_\P{ASCII}]|#{RE_ESCAPE}/
|
38
|
+
RE_IDENT_PART = /(?:[-a-zA-Z0-9_\P{ASCII}]|#{RE_ESCAPE})*/
|
39
|
+
RE_IDENT = /(?:-?(?:#{RE_IDENT_START})|--)(?:#{RE_IDENT_PART})*/
|
40
|
+
RE_STRING =
|
41
|
+
/
|
42
|
+
"(?:(?!#{RE_NEWLINE})[^"\\]|#{RE_ESCAPE_NEWLINE})*"
|
43
|
+
| '(?:(?!#{RE_NEWLINE})[^'\\]|#{RE_ESCAPE_NEWLINE})*'
|
44
|
+
/x
|
45
|
+
RE_SUBSTITUTE = /\$#{RE_IDENT}/
|
46
|
+
RE_VALUE = /#{RE_IDENT}|#{RE_STRING}|#{RE_SUBSTITUTE}/
|
47
|
+
|
48
|
+
def self.unescape_ident(ident)
|
49
|
+
return nil unless ident
|
50
|
+
|
51
|
+
ident.gsub(RE_ESCAPE) do |escape|
|
52
|
+
if escape[1] =~ /\H/
|
53
|
+
escape[1]
|
54
|
+
else
|
55
|
+
escape[1..].to_i(16).chr
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def self.unescape_value(value)
|
61
|
+
if value[0] == '"' || value[0] == "'"
|
62
|
+
value =
|
63
|
+
value[1...-1].gsub(RE_ESCAPE_NEWLINE) do |escape|
|
64
|
+
if escape[1] =~ /[\n\r\f]/
|
65
|
+
""
|
66
|
+
elsif escape[1] =~ /\H/
|
67
|
+
escape[1]
|
68
|
+
else
|
69
|
+
escape[1..].to_i(16).chr
|
70
|
+
end
|
71
|
+
end
|
72
|
+
String[value]
|
73
|
+
elsif value[0] == "$"
|
74
|
+
Substitution[unescape_ident(value[1..])]
|
75
|
+
else
|
76
|
+
Ident[unescape_ident(value)]
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
def self.unescape_namespace(value)
|
81
|
+
return nil unless value
|
82
|
+
|
83
|
+
if value == "*"
|
84
|
+
UniversalNamespace[]
|
85
|
+
else
|
86
|
+
Namespace[name: unescape_ident(value)]
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
private
|
91
|
+
|
92
|
+
# See https://www.w3.org/TR/selectors-4/#grammar.
|
93
|
+
#
|
94
|
+
# ```
|
95
|
+
# <selector-list> = <complex-selector-list>
|
96
|
+
# <complex-selector-list> = <complex-selector>#
|
97
|
+
# <compound-selector-list> = <compound-selector>#
|
98
|
+
# <simple-selector-list> = <simple-selector>#
|
99
|
+
# <relative-selector-list> = <relative-selector>#
|
100
|
+
#
|
101
|
+
# <complex-selector> = <compound-selector> [ <combinator>? <compound-selector> ]*
|
102
|
+
# <relative-selector> = <combinator>? <complex-selector>
|
103
|
+
# <compound-selector> = [ <type-selector>? <subclass-selector>*
|
104
|
+
# [ <pseudo-element-selector> <pseudo-class-selector>* ]* ]!
|
105
|
+
# <simple-selector> = <type-selector> | <subclass-selector>
|
106
|
+
#
|
107
|
+
# <combinator> = '>' | '+' | '~' | [ '|' '|' ]
|
108
|
+
# <type-selector> = <wq-name> | <ns-prefix>? '*'
|
109
|
+
# <ns-prefix> = [ <ident-token> | '*' ]? '|'
|
110
|
+
# <wq-name> = <ns-prefix>? <ident-token>
|
111
|
+
# <subclass-selector> = <id-selector> | <class-selector> |
|
112
|
+
# <attribute-selector> | <pseudo-class-selector>
|
113
|
+
#
|
114
|
+
# <id-selector> = <hash-token>
|
115
|
+
# <class-selector> = '.' <ident-token>
|
116
|
+
# <attribute-selector> = '[' <wq-name> ']' |
|
117
|
+
# '[' <wq-name> <attr-matcher> [ <string-token> | <ident-token> ] <attr-modifier>? ']'
|
118
|
+
# <attr-matcher> = [ '~' | '|' | '^' | '$' | '*' ]? '='
|
119
|
+
# <attr-modifier> = i | s
|
120
|
+
# <pseudo-class-selector> = ':' <ident-token> |
|
121
|
+
# ':' <function-token> <any-value> ')'
|
122
|
+
# <pseudo-element-selector> = ':' <pseudo-class-selector>
|
123
|
+
# ```
|
124
|
+
|
125
|
+
# ```
|
126
|
+
# <selector-list> = <complex-selector-list>
|
127
|
+
# <complex-selector-list> = <complex-selector>#
|
128
|
+
# <relative-selector-list> = <relative-selector>#
|
129
|
+
# ```
|
130
|
+
|
131
|
+
def parse_selector_list
|
132
|
+
selector_list = parse_complex_selector_list
|
133
|
+
@scanner.scan RE_WS
|
134
|
+
raise ParseError.new("expected end-of-string", @scanner.charpos) unless @scanner.eos?
|
135
|
+
|
136
|
+
selector_list
|
137
|
+
end
|
138
|
+
|
139
|
+
def parse_complex_selector_list
|
140
|
+
selectors = parse_comma_separated_list { parse_complex_selector }
|
141
|
+
SelectorList[selectors:]
|
142
|
+
end
|
143
|
+
|
144
|
+
def parse_relative_selector_list
|
145
|
+
selectors = parse_comma_separated_list { parse_relative_selector }
|
146
|
+
RelativeSelectorList[selectors:]
|
147
|
+
end
|
148
|
+
|
149
|
+
# ```
|
150
|
+
# <complex-selector> = <compound-selector> [ <combinator>? <compound-selector> ]*
|
151
|
+
# <relative-selector> = <combinator>? <complex-selector>
|
152
|
+
#
|
153
|
+
# <combinator> = '>' | '+' | '~' | [ '|' '|' ]
|
154
|
+
# ```
|
155
|
+
|
156
|
+
def parse_complex_selector
|
157
|
+
last = parse_compound_selector
|
158
|
+
pairs = []
|
159
|
+
|
160
|
+
while (combinator = try_parse_combinator)
|
161
|
+
pairs << [last, combinator]
|
162
|
+
last = parse_compound_selector
|
163
|
+
end
|
164
|
+
|
165
|
+
pairs.reverse_each { |(prev, combinator)| last = ComplexSelector[left: prev, combinator:, right: last] }
|
166
|
+
last
|
167
|
+
end
|
168
|
+
|
169
|
+
def parse_relative_selector
|
170
|
+
combinator = try_parse_combinator || :descendant
|
171
|
+
selector = parse_complex_selector
|
172
|
+
RelativeSelector[combinator:, right: selector]
|
173
|
+
end
|
174
|
+
|
175
|
+
RE_COMBINATOR = /#{RE_WS}(?:[>+~]|\|\|)#{RE_WS}|(?:#{RE_WHITESPACE})+(?![,)])/
|
176
|
+
|
177
|
+
def try_parse_combinator
|
178
|
+
return nil unless @scanner.scan(RE_COMBINATOR)
|
179
|
+
|
180
|
+
case @scanner[0].strip
|
181
|
+
when ">"
|
182
|
+
return :child
|
183
|
+
when "~"
|
184
|
+
return :sibling
|
185
|
+
when "+"
|
186
|
+
return :adjacent
|
187
|
+
when "||"
|
188
|
+
return :column
|
189
|
+
when ""
|
190
|
+
return :descendant
|
191
|
+
end
|
192
|
+
|
193
|
+
raise "unreachable"
|
194
|
+
end
|
195
|
+
|
196
|
+
# ```
|
197
|
+
# <compound-selector> = [ <type-selector>? <subclass-selector>*
|
198
|
+
# [ <pseudo-element-selector> <pseudo-class-selector>* ]* ]!
|
199
|
+
# ```
|
200
|
+
|
201
|
+
def parse_compound_selector
|
202
|
+
type = try_parse_type_selector
|
203
|
+
|
204
|
+
subclasses = []
|
205
|
+
while (subclass = try_parse_subclass_selector)
|
206
|
+
subclasses << subclass
|
207
|
+
end
|
208
|
+
|
209
|
+
pseudo_elements = []
|
210
|
+
while (pseudo_element = try_parse_pseudo_element_selector)
|
211
|
+
pseudo_elements << pseudo_element
|
212
|
+
end
|
213
|
+
|
214
|
+
if type.nil? && subclasses.empty? && pseudo_elements.empty?
|
215
|
+
raise ParseError.new("expected type, subclass, or pseudo element selector", @scanner.charpos)
|
216
|
+
end
|
217
|
+
|
218
|
+
CompoundSelector[type:, subclasses:, pseudo_elements:]
|
219
|
+
end
|
220
|
+
|
221
|
+
# ```
|
222
|
+
# <type-selector> = <wq-name> | <ns-prefix>? '*'
|
223
|
+
#
|
224
|
+
# <ns-prefix> = [ <ident-token> | '*' ]? '|'
|
225
|
+
# <wq-name> = <ns-prefix>? <ident-token>
|
226
|
+
# ```
|
227
|
+
|
228
|
+
RE_TYPE_SELECTOR = /(?:(?<namespace>#{RE_IDENT}|\*)\|)?(?<tag_name>#{RE_IDENT}|\*)/
|
229
|
+
|
230
|
+
def try_parse_type_selector
|
231
|
+
return nil unless @scanner.scan(RE_TYPE_SELECTOR)
|
232
|
+
|
233
|
+
namespace = Parser.unescape_namespace(@scanner[:namespace])
|
234
|
+
tag_name = @scanner[:tag_name]
|
235
|
+
|
236
|
+
return UniversalType[namespace:] if tag_name == "*"
|
237
|
+
|
238
|
+
TagNameType[namespace:, tag_name: Parser.unescape_ident(tag_name)]
|
239
|
+
end
|
240
|
+
|
241
|
+
# ```
|
242
|
+
# <subclass-selector> = <id-selector> | <class-selector> |
|
243
|
+
# <attribute-selector> | <pseudo-class-selector>
|
244
|
+
#
|
245
|
+
# <id-selector> = <hash-token>
|
246
|
+
# <class-selector> = '.' <ident-token>
|
247
|
+
# <attribute-selector> = '[' <wq-name> ']' |
|
248
|
+
# '[' <wq-name> <attr-matcher> [ <string-token> | <ident-token> ] <attr-modifier>? ']'
|
249
|
+
# <attr-matcher> = [ '~' | '|' | '^' | '$' | '*' ]? '='
|
250
|
+
# <attr-modifier> = i | s
|
251
|
+
# ```
|
252
|
+
|
253
|
+
RE_SUBCLASS_SELECTOR =
|
254
|
+
/
|
255
|
+
\#(?<id>#{RE_IDENT})
|
256
|
+
| \.(?<class>#{RE_IDENT})
|
257
|
+
| \[
|
258
|
+
#{RE_WS}
|
259
|
+
(?:(?<attr_namespace>#{RE_IDENT}|\*)\|)?
|
260
|
+
(?<attr_name>#{RE_IDENT})
|
261
|
+
#{RE_WS}
|
262
|
+
(?:
|
263
|
+
(?<attr_matcher>[~|^$*]?=)
|
264
|
+
#{RE_WS}
|
265
|
+
(?<attr_value>#{RE_VALUE})
|
266
|
+
#{RE_WS}
|
267
|
+
(?<attr_modifier>[is])?
|
268
|
+
#{RE_WS}
|
269
|
+
)?
|
270
|
+
\]
|
271
|
+
/x
|
272
|
+
|
273
|
+
def try_parse_subclass_selector
|
274
|
+
return try_parse_pseudo_class_selector unless @scanner.scan(RE_SUBCLASS_SELECTOR)
|
275
|
+
|
276
|
+
return Id[name: Parser.unescape_ident(@scanner[:id])] if @scanner[:id]
|
277
|
+
|
278
|
+
return ClassName[name: Parser.unescape_ident(@scanner[:class])] if @scanner[:class]
|
279
|
+
|
280
|
+
namespace = Parser.unescape_namespace(@scanner[:attr_namespace])
|
281
|
+
name = Parser.unescape_ident(@scanner[:attr_name])
|
282
|
+
|
283
|
+
if @scanner[:attr_matcher]
|
284
|
+
matcher = @scanner[:attr_matcher].intern
|
285
|
+
value = Parser.unescape_value(@scanner[:attr_value])
|
286
|
+
modifier = @scanner[:attr_modifier]&.intern
|
287
|
+
else
|
288
|
+
matcher = value = modifier = nil
|
289
|
+
end
|
290
|
+
|
291
|
+
Attribute[namespace:, name:, matcher:, value:, modifier:]
|
292
|
+
end
|
293
|
+
|
294
|
+
# ```
|
295
|
+
# <pseudo-class-selector> = ':' <ident-token> |
|
296
|
+
# ':' <function-token> <any-value> ')'
|
297
|
+
# <pseudo-element-selector> = ':' <pseudo-class-selector>
|
298
|
+
# ```
|
299
|
+
|
300
|
+
RE_PSEUDO_CLASS_SELECTOR = /:(?<name>#{RE_IDENT})/
|
301
|
+
RE_PSEUDO_ELEMENT_SELECTOR = /::(?<name>#{RE_IDENT})/
|
302
|
+
RE_OPEN_PAREN = /#{RE_WS}\(#{RE_WS}/
|
303
|
+
RE_CLOSE_PAREN = /#{RE_WS}\)/
|
304
|
+
RE_OF = /#{RE_WS}of#{RE_WS}/
|
305
|
+
|
306
|
+
def try_parse_pseudo_class_selector
|
307
|
+
return nil unless @scanner.scan(RE_PSEUDO_CLASS_SELECTOR)
|
308
|
+
|
309
|
+
name = Parser.unescape_ident(@scanner[:name])
|
310
|
+
|
311
|
+
if @scanner.scan(RE_OPEN_PAREN)
|
312
|
+
argument = parse_function_argument(@config[:pseudo_classes][name]&.argument_kind)
|
313
|
+
end
|
314
|
+
|
315
|
+
PseudoClass[name:, argument:]
|
316
|
+
end
|
317
|
+
|
318
|
+
def try_parse_pseudo_element_selector
|
319
|
+
return nil unless @scanner.scan(RE_PSEUDO_ELEMENT_SELECTOR)
|
320
|
+
|
321
|
+
name = Parser.unescape_ident(@scanner[:name])
|
322
|
+
argument = parse_function_argument(nil) if @scanner.scan(RE_OPEN_PAREN)
|
323
|
+
|
324
|
+
pseudo_classes = []
|
325
|
+
while (pseudo_class = try_parse_pseudo_class_selector)
|
326
|
+
pseudo_classes << pseudo_class
|
327
|
+
end
|
328
|
+
|
329
|
+
PseudoElement[name:, argument:, pseudo_classes:]
|
330
|
+
end
|
331
|
+
|
332
|
+
def parse_function_argument(argument_kind)
|
333
|
+
case argument_kind
|
334
|
+
when :selector_list
|
335
|
+
selector_list = parse_complex_selector_list
|
336
|
+
scan! RE_CLOSE_PAREN, '")"'
|
337
|
+
selector_list
|
338
|
+
when :relative_selector_list
|
339
|
+
selector_list = parse_relative_selector_list
|
340
|
+
scan! RE_CLOSE_PAREN, '")"'
|
341
|
+
selector_list
|
342
|
+
when :nth
|
343
|
+
nth = parse_nth
|
344
|
+
scan! RE_CLOSE_PAREN, '")"'
|
345
|
+
nth
|
346
|
+
when :nth_of_selector_list
|
347
|
+
nth = parse_nth
|
348
|
+
selector_list = parse_complex_selector_list if @scanner.scan(RE_OF)
|
349
|
+
scan! RE_CLOSE_PAREN, '")"'
|
350
|
+
NthOfSelectorList[nth:, selector_list:]
|
351
|
+
else
|
352
|
+
raise "BUG: unknown pseudo-function kind: #{argument_kind}" if argument_kind
|
353
|
+
|
354
|
+
value_list = parse_value_list
|
355
|
+
scan! RE_CLOSE_PAREN, '")"'
|
356
|
+
value_list
|
357
|
+
end
|
358
|
+
end
|
359
|
+
|
360
|
+
RE_NTH = /odd\b|even\b|(?:(?<a>[+-]?\d+|[+-]?)n)?(?<b>(?:(?:[+-]|(?<!n)[+-]?)\d+)?)/
|
361
|
+
|
362
|
+
def parse_nth
|
363
|
+
scan! RE_NTH, '"odd", "even", or An+B'
|
364
|
+
case @scanner[0]
|
365
|
+
when "odd"
|
366
|
+
Odd[]
|
367
|
+
when "even"
|
368
|
+
Even[]
|
369
|
+
when ""
|
370
|
+
raise ParseError.new('expected "odd", "even", or An+B', @scanner.charpos)
|
371
|
+
else
|
372
|
+
a =
|
373
|
+
case @scanner[:a]
|
374
|
+
when "+", ""
|
375
|
+
1
|
376
|
+
when "-"
|
377
|
+
-1
|
378
|
+
when nil
|
379
|
+
0
|
380
|
+
else
|
381
|
+
@scanner[:a].to_i
|
382
|
+
end
|
383
|
+
b =
|
384
|
+
case @scanner[:b]
|
385
|
+
when ""
|
386
|
+
0
|
387
|
+
else
|
388
|
+
@scanner[:b].to_i
|
389
|
+
end
|
390
|
+
Nth[a:, b:]
|
391
|
+
end
|
392
|
+
end
|
393
|
+
|
394
|
+
def parse_value_list
|
395
|
+
values = parse_comma_separated_list { parse_value }
|
396
|
+
ValueList[values:]
|
397
|
+
end
|
398
|
+
|
399
|
+
RE_BARE_VALUE = /[^,)]*/
|
400
|
+
|
401
|
+
def parse_value
|
402
|
+
if @scanner.scan RE_VALUE
|
403
|
+
Parser.unescape_value @scanner[0]
|
404
|
+
else
|
405
|
+
@scanner.scan RE_BARE_VALUE
|
406
|
+
Bare[value: @scanner[0].strip]
|
407
|
+
end
|
408
|
+
end
|
409
|
+
|
410
|
+
RE_COMMA = /#{RE_WS},#{RE_WS}/
|
411
|
+
|
412
|
+
def parse_comma_separated_list(&)
|
413
|
+
list = []
|
414
|
+
list << yield
|
415
|
+
|
416
|
+
list << yield while @scanner.scan(RE_COMMA)
|
417
|
+
|
418
|
+
list
|
419
|
+
end
|
420
|
+
|
421
|
+
def scan!(regexp, error_token)
|
422
|
+
return if @scanner.scan(regexp)
|
423
|
+
|
424
|
+
raise ParseError.new("expected #{error_token}", @scanner.charpos)
|
425
|
+
end
|
426
|
+
|
427
|
+
# :startdoc:
|
428
|
+
end
|
429
|
+
end
|
430
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module REXML
|
4
|
+
module CSSSelector
|
5
|
+
# PseudoClassDef is a definition of a pseudo class.
|
6
|
+
class PseudoClassDef
|
7
|
+
def initialize(argument_kind = nil, &compile)
|
8
|
+
@argument_kind = argument_kind
|
9
|
+
@compile = compile
|
10
|
+
end
|
11
|
+
|
12
|
+
attr_reader :argument_kind
|
13
|
+
|
14
|
+
def compile(cont, pseudo_class, compiler)
|
15
|
+
@compile.call(cont, pseudo_class, compiler)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module REXML
|
4
|
+
module CSSSelector
|
5
|
+
module Queries
|
6
|
+
class AdjacentQuery
|
7
|
+
def initialize(cont:)
|
8
|
+
@cont = cont
|
9
|
+
end
|
10
|
+
|
11
|
+
def call(node, context)
|
12
|
+
node = context.adapter.get_previous_sibling_element(node)
|
13
|
+
context.adapter.element?(node) && @cont.call(node, context)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module REXML
|
4
|
+
module CSSSelector
|
5
|
+
module Queries
|
6
|
+
class AttributeMatcherQuery
|
7
|
+
def initialize(cont:, name:, namespace:, matcher:, value:, modifier:)
|
8
|
+
@cont = cont
|
9
|
+
@name = name
|
10
|
+
@namespace = namespace
|
11
|
+
@matcher = matcher
|
12
|
+
@value = value
|
13
|
+
@modifier = modifier
|
14
|
+
end
|
15
|
+
|
16
|
+
def call(node, context)
|
17
|
+
value =
|
18
|
+
case @value
|
19
|
+
in Substitution[name:]
|
20
|
+
context.substitutions[name]
|
21
|
+
in String[value:]
|
22
|
+
value
|
23
|
+
in Ident[value:]
|
24
|
+
value
|
25
|
+
in Bare[value:]
|
26
|
+
value
|
27
|
+
end
|
28
|
+
return false unless value
|
29
|
+
|
30
|
+
name = @name
|
31
|
+
name = @name.downcase(:ascii) if context.options[:attribute_name_case] == :insensitive
|
32
|
+
actual = context.adapter.get_attribute(node, name, @namespace, context.options[:attribute_name_case])
|
33
|
+
return false unless actual
|
34
|
+
|
35
|
+
if @modifier == :i || context.options[:case_sensitive_attribute_values].include?(name)
|
36
|
+
value = value.downcase(:ascii)
|
37
|
+
actual = actual.downcase(:ascii)
|
38
|
+
end
|
39
|
+
|
40
|
+
case @matcher
|
41
|
+
in :"="
|
42
|
+
value == actual && @cont.call(node, context)
|
43
|
+
in :"~="
|
44
|
+
actual.split(/\s+/).include?(value) && @cont.call(node, context)
|
45
|
+
in :"|="
|
46
|
+
/(?:^|\|)#{value}(?:$|[|-])/.match?(actual) && @cont.call(node, context)
|
47
|
+
in :"^="
|
48
|
+
actual.start_with?(value) && @cont.call(node, context)
|
49
|
+
in :"$="
|
50
|
+
actual.end_with?(value) && @cont.call(node, context)
|
51
|
+
in :"*="
|
52
|
+
actual.include?(value) && @cont.call(node, context)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module REXML
|
4
|
+
module CSSSelector
|
5
|
+
module Queries
|
6
|
+
class AttributePresenceQuery
|
7
|
+
def initialize(cont:, name:, namespace:)
|
8
|
+
@cont = cont
|
9
|
+
@name = name
|
10
|
+
@namespace = namespace
|
11
|
+
end
|
12
|
+
|
13
|
+
def call(node, context)
|
14
|
+
context.adapter.get_attribute(node, @name, @namespace, context.options[:attribute_name_case]) &&
|
15
|
+
@cont.call(node, context)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module REXML
|
4
|
+
module CSSSelector
|
5
|
+
module Queries
|
6
|
+
class CheckedQuery
|
7
|
+
def initialize(cont:)
|
8
|
+
@cont = cont
|
9
|
+
end
|
10
|
+
|
11
|
+
def call(node, context)
|
12
|
+
(context.adapter.checked?(node) || context.options[:checked_elements].include?(node)) &&
|
13
|
+
@cont.call(node, context)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module REXML
|
4
|
+
module CSSSelector
|
5
|
+
module Queries
|
6
|
+
class ChildQuery
|
7
|
+
def initialize(cont:)
|
8
|
+
@cont = cont
|
9
|
+
end
|
10
|
+
|
11
|
+
def call(node, context)
|
12
|
+
node = context.adapter.get_parent_node(node)
|
13
|
+
context.adapter.element?(node) && @cont.call(node, context)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module REXML
|
4
|
+
module CSSSelector
|
5
|
+
module Queries
|
6
|
+
class ClassNameQuery
|
7
|
+
def initialize(cont:, name:)
|
8
|
+
@cont = cont
|
9
|
+
@name = name
|
10
|
+
end
|
11
|
+
|
12
|
+
def call(node, context)
|
13
|
+
context.adapter.get_class_names(node).include?(@name) && @cont.call(node, context)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module REXML
|
4
|
+
module CSSSelector
|
5
|
+
module Queries
|
6
|
+
class DeferredResult
|
7
|
+
def initialize
|
8
|
+
@is_match = false
|
9
|
+
end
|
10
|
+
|
11
|
+
attr_accessor :is_match
|
12
|
+
end
|
13
|
+
|
14
|
+
class DescendantQuery
|
15
|
+
def initialize(cont:)
|
16
|
+
@cont = cont
|
17
|
+
end
|
18
|
+
|
19
|
+
def call(node, context)
|
20
|
+
cache = context.cache
|
21
|
+
result = nil
|
22
|
+
|
23
|
+
while (node = context.adapter.get_parent_node(node))
|
24
|
+
cached = cache[[object_id, node.object_id]]
|
25
|
+
if cached.nil?
|
26
|
+
result ||= DeferredResult.new
|
27
|
+
result.is_match = @cont.call(node, context)
|
28
|
+
cache[[object_id, node.object_id]] = result
|
29
|
+
return true if result.is_match
|
30
|
+
else
|
31
|
+
result&.is_match = cached.is_match
|
32
|
+
return cached.is_match
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
false
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module REXML
|
4
|
+
module CSSSelector
|
5
|
+
module Queries
|
6
|
+
class DisabledQuery
|
7
|
+
def initialize(cont:)
|
8
|
+
@cont = cont
|
9
|
+
end
|
10
|
+
|
11
|
+
def call(node, context)
|
12
|
+
(context.adapter.disabled?(node) || context.options[:disabled_elements].include?(node)) &&
|
13
|
+
@cont.call(node, context)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module REXML
|
4
|
+
module CSSSelector
|
5
|
+
module Queries
|
6
|
+
class EmptyQuery
|
7
|
+
def initialize(cont:)
|
8
|
+
@cont = cont
|
9
|
+
end
|
10
|
+
|
11
|
+
def call(node, context)
|
12
|
+
context.adapter.empty?(node) && @cont.call(node, context)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|