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.
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