rexml-css_selector 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (48) hide show
  1. checksums.yaml +7 -0
  2. data/.editorconfig +8 -0
  3. data/.rubocop.yml +48 -0
  4. data/CODE_OF_CONDUCT.md +84 -0
  5. data/LICENSE.txt +21 -0
  6. data/README.md +74 -0
  7. data/Rakefile +27 -0
  8. data/example/prism.rb +13 -0
  9. data/lib/rexml/css_selector/adapters/prism_adapter.rb +77 -0
  10. data/lib/rexml/css_selector/adapters/rexml_adapter.rb +77 -0
  11. data/lib/rexml/css_selector/ast.rb +107 -0
  12. data/lib/rexml/css_selector/base_adapter.rb +88 -0
  13. data/lib/rexml/css_selector/compiler.rb +287 -0
  14. data/lib/rexml/css_selector/parser.rb +430 -0
  15. data/lib/rexml/css_selector/pseudo_class_def.rb +19 -0
  16. data/lib/rexml/css_selector/queries/adjacent_query.rb +18 -0
  17. data/lib/rexml/css_selector/queries/attribute_matcher_query.rb +58 -0
  18. data/lib/rexml/css_selector/queries/attribute_presence_query.rb +20 -0
  19. data/lib/rexml/css_selector/queries/checked_query.rb +18 -0
  20. data/lib/rexml/css_selector/queries/child_query.rb +18 -0
  21. data/lib/rexml/css_selector/queries/class_name_query.rb +18 -0
  22. data/lib/rexml/css_selector/queries/descendant_query.rb +41 -0
  23. data/lib/rexml/css_selector/queries/disabled_query.rb +18 -0
  24. data/lib/rexml/css_selector/queries/empty_query.rb +17 -0
  25. data/lib/rexml/css_selector/queries/has_query.rb +34 -0
  26. data/lib/rexml/css_selector/queries/id_query.rb +18 -0
  27. data/lib/rexml/css_selector/queries/nested_query.rb +18 -0
  28. data/lib/rexml/css_selector/queries/not_query.rb +18 -0
  29. data/lib/rexml/css_selector/queries/nth_child_of_query.rb +40 -0
  30. data/lib/rexml/css_selector/queries/nth_child_query.rb +32 -0
  31. data/lib/rexml/css_selector/queries/nth_last_child_of_query.rb +40 -0
  32. data/lib/rexml/css_selector/queries/nth_last_child_query.rb +33 -0
  33. data/lib/rexml/css_selector/queries/nth_last_of_type_query.rb +44 -0
  34. data/lib/rexml/css_selector/queries/nth_of_type_query.rb +44 -0
  35. data/lib/rexml/css_selector/queries/one_of_query.rb +17 -0
  36. data/lib/rexml/css_selector/queries/only_child_query.rb +23 -0
  37. data/lib/rexml/css_selector/queries/only_of_type_query.rb +34 -0
  38. data/lib/rexml/css_selector/queries/root_query.rb +17 -0
  39. data/lib/rexml/css_selector/queries/scope_query.rb +17 -0
  40. data/lib/rexml/css_selector/queries/sibling_query.rb +21 -0
  41. data/lib/rexml/css_selector/queries/tag_name_type_query.rb +30 -0
  42. data/lib/rexml/css_selector/queries/true_query.rb +15 -0
  43. data/lib/rexml/css_selector/queries/universal_type_query.rb +22 -0
  44. data/lib/rexml/css_selector/query_context.rb +25 -0
  45. data/lib/rexml/css_selector/version.rb +8 -0
  46. data/lib/rexml/css_selector.rb +197 -0
  47. data/sig/rexml/css_selector.rbs +5 -0
  48. 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