parselly 1.1.0 → 1.3.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.
data/lib/parselly/node.rb CHANGED
@@ -2,70 +2,183 @@
2
2
 
3
3
  module Parselly
4
4
  # Represents a node in the Abstract Syntax Tree (AST) for CSS selectors.
5
- #
6
- # Each Node represents a parsed CSS selector component (e.g., type selector,
7
- # class selector, combinator, or selector list) with its type, optional value,
8
- # child nodes, parent reference, and source position.
9
- #
10
- # @example Creating a simple AST node
11
- # node = Parselly::Node.new(:type_selector, 'div', { line: 1, column: 1 })
12
- # node.add_child(Parselly::Node.new(:class_selector, 'container'))
13
- #
14
- # @example Traversing the AST
15
- # node.ancestors # Returns array of ancestor nodes
16
- # node.descendants # Returns array of all descendant nodes
17
- # node.siblings # Returns array of sibling nodes
18
5
  class Node
19
- attr_accessor :type, :value, :children, :parent, :position
20
-
21
- # Creates a new AST node.
22
- #
23
- # @param type [Symbol] the type of the node (e.g., :type_selector, :class_selector)
24
- # @param value [String, nil] optional value associated with the node
25
- # @param position [Hash] source position with :line and :column keys
26
- def initialize(type, value = nil, position = {})
6
+ include Enumerable
7
+
8
+ SIMPLE_SELECTOR_TYPES = Set[
9
+ :type_selector,
10
+ :universal_selector,
11
+ :id_selector,
12
+ :class_selector,
13
+ :attribute_selector,
14
+ :pseudo_class,
15
+ :pseudo_function,
16
+ :pseudo_element,
17
+ :pseudo_element_function
18
+ ].freeze
19
+
20
+ COMBINATOR_TYPES = {
21
+ child_combinator: '>',
22
+ adjacent_combinator: '+',
23
+ sibling_combinator: '~',
24
+ descendant_combinator: ' ',
25
+ column_combinator: '||'
26
+ }.freeze
27
+
28
+ SPECIFICITY_ZERO_PSEUDO_FUNCTIONS = Set['where'].freeze
29
+ SPECIFICITY_MAX_ARGUMENT_PSEUDO_FUNCTIONS = Set['is', 'not', 'has'].freeze
30
+ NTH_PSEUDO_FUNCTIONS = Set['nth-child', 'nth-last-child', 'nth-of-type', 'nth-last-of-type', 'nth-col', 'nth-last-col'].freeze
31
+
32
+ class ChildList < Array
33
+ def initialize(owner)
34
+ @owner = owner
35
+ super()
36
+ end
37
+
38
+ def <<(node)
39
+ return self if node.nil?
40
+
41
+ @owner.__send__(:adopt_child, node)
42
+ super(node)
43
+ @owner.__send__(:invalidate_cache)
44
+ self
45
+ end
46
+
47
+ def push(*nodes)
48
+ nodes.each { |node| self << node }
49
+ self
50
+ end
51
+
52
+ def concat(nodes)
53
+ nodes.each { |node| self << node }
54
+ self
55
+ end
56
+
57
+ def []=(index, node)
58
+ old_node = self[index]
59
+ @owner.__send__(:detach_child, old_node) if old_node
60
+ @owner.__send__(:adopt_child, node)
61
+ super
62
+ @owner.__send__(:invalidate_cache)
63
+ node
64
+ end
65
+
66
+ def insert(index, *nodes)
67
+ nodes.each { |node| @owner.__send__(:adopt_child, node) }
68
+ result = super
69
+ @owner.__send__(:invalidate_cache)
70
+ result
71
+ end
72
+
73
+ def delete_at(index)
74
+ node = super
75
+ @owner.__send__(:detach_child, node) if node
76
+ @owner.__send__(:invalidate_cache)
77
+ node
78
+ end
79
+
80
+ def delete(node)
81
+ deleted = super
82
+ @owner.__send__(:detach_child, deleted) if deleted
83
+ @owner.__send__(:invalidate_cache) if deleted
84
+ deleted
85
+ end
86
+
87
+ def clear
88
+ each { |node| @owner.__send__(:detach_child, node) }
89
+ result = super
90
+ @owner.__send__(:invalidate_cache)
91
+ result
92
+ end
93
+
94
+ private
95
+ end
96
+
97
+ attr_accessor :type, :value, :raw_value, :parent, :position, :namespace, :quote, :modifier, :prefix
98
+ attr_reader :children
99
+
100
+ def initialize(type, value = nil, position = {}, raw_value: nil, line: nil, column: nil, offset: nil,
101
+ start_line: nil, start_column: nil, start_offset: nil,
102
+ end_line: nil, end_column: nil, end_offset: nil,
103
+ namespace: nil, quote: nil, modifier: nil, prefix: nil)
27
104
  @type = type
28
105
  @value = value
29
- @children = []
106
+ @raw_value = raw_value.nil? ? value : raw_value
107
+ @children = ChildList.new(self)
30
108
  @parent = nil
31
- @position = position
109
+ @namespace = namespace
110
+ @quote = quote
111
+ @modifier = modifier
112
+ @prefix = prefix
113
+ unless position.nil? || position.is_a?(Hash)
114
+ raise ArgumentError, 'position must be a Hash'
115
+ end
116
+
117
+ resolved_position = position ? position.dup : {}
118
+ resolved_position[:line] = line unless line.nil?
119
+ resolved_position[:column] = column unless column.nil?
120
+ resolved_position[:offset] = offset unless offset.nil?
121
+ resolved_position[:start_line] = start_line unless start_line.nil?
122
+ resolved_position[:start_column] = start_column unless start_column.nil?
123
+ resolved_position[:start_offset] = start_offset unless start_offset.nil?
124
+ resolved_position[:end_line] = end_line unless end_line.nil?
125
+ resolved_position[:end_column] = end_column unless end_column.nil?
126
+ resolved_position[:end_offset] = end_offset unless end_offset.nil?
127
+ @position = resolved_position
32
128
  @descendants_cache = nil
33
129
  end
34
130
 
35
- # Adds a child node to this node.
36
- #
37
- # @param node [Node, nil] the child node to add
38
- # @return [Node, nil] the added node, or nil if the input was nil
131
+ def children=(nodes)
132
+ @children.clear
133
+ Array(nodes).each { |node| add_child(node) }
134
+ end
135
+
39
136
  def add_child(node)
40
137
  return nil if node.nil?
41
138
 
42
- node.parent = self
43
139
  @children << node
44
- invalidate_cache
45
140
  node
46
141
  end
47
142
 
48
- # Replaces a child node at the specified index.
49
- #
50
- # @param index [Integer] the index of the child to replace
51
- # @param new_node [Node] the new child node
52
- # @return [Node, nil] the new node, or nil if invalid parameters
53
143
  def replace_child(index, new_node)
54
144
  return nil if new_node.nil?
55
- return nil if index < 0 || index >= @children.size
56
-
57
- old_node = @children[index]
58
- old_node.parent = nil if old_node
145
+ return nil if index.negative? || index >= @children.size
59
146
 
60
147
  @children[index] = new_node
61
- new_node.parent = self
62
- invalidate_cache
63
- new_node
64
148
  end
65
149
 
66
- # Returns an array of all ancestor nodes from parent to root.
67
- #
68
- # @return [Array<Node>] array of ancestor nodes
150
+ def insert_child(index, node)
151
+ return nil if node.nil?
152
+ return nil if index.negative? || index > @children.size
153
+
154
+ @children.insert(index, node)
155
+ node
156
+ end
157
+
158
+ def remove_child(node_or_index)
159
+ if node_or_index.is_a?(Integer)
160
+ return nil if node_or_index.negative? || node_or_index >= @children.size
161
+
162
+ return @children.delete_at(node_or_index)
163
+ end
164
+
165
+ @children.delete(node_or_index)
166
+ end
167
+
168
+ def insert_before(reference_child, new_child)
169
+ index = @children.index(reference_child)
170
+ return nil unless index
171
+
172
+ insert_child(index, new_child)
173
+ end
174
+
175
+ def insert_after(reference_child, new_child)
176
+ index = @children.index(reference_child)
177
+ return nil unless index
178
+
179
+ insert_child(index + 1, new_child)
180
+ end
181
+
69
182
  def ancestors
70
183
  result = []
71
184
  node = parent
@@ -76,46 +189,51 @@ module Parselly
76
189
  result
77
190
  end
78
191
 
79
- # Returns an array of all descendant nodes (children, grandchildren, etc.).
80
- #
81
- # @return [Array<Node>] array of all descendant nodes
82
192
  def descendants
83
193
  return @descendants_cache if @descendants_cache
84
194
 
85
195
  @descendants_cache = []
86
- queue = @children.dup
87
- until queue.empty?
88
- node = queue.shift
196
+ queue = @children.to_a
197
+ index = 0
198
+ while index < queue.length
199
+ node = queue[index]
89
200
  @descendants_cache << node
90
201
  queue.concat(node.children) unless node.children.empty?
202
+ index += 1
91
203
  end
92
204
  @descendants_cache
93
205
  end
94
206
 
95
- # Returns an array of sibling nodes (excluding self).
96
- #
97
- # @return [Array<Node>] array of sibling nodes, or empty array if no parent
207
+ def each
208
+ return enum_for(:each) unless block_given?
209
+
210
+ stack = [self]
211
+ until stack.empty?
212
+ node = stack.pop
213
+ yield node
214
+ stack.concat(node.children.reverse) unless node.children.empty?
215
+ end
216
+
217
+ self
218
+ end
219
+
220
+ def find_all(type)
221
+ each.select { |node| node.type == type }
222
+ end
223
+
98
224
  def siblings
99
225
  return [] unless parent
100
226
 
101
227
  parent.children.reject { |child| child == self }
102
228
  end
103
229
 
104
- # Returns a tree representation of this node and its descendants.
105
- #
106
- # @param indent [Integer] indentation level for the tree display
107
- # @return [String] formatted tree string
108
230
  def to_tree(indent = 0)
109
231
  lines = []
110
232
  prefix = ' ' * indent
111
233
  pos_info = position.empty? ? '' : " [#{position[:line]}:#{position[:column]}]"
112
234
 
113
235
  lines << "#{prefix}#{type}#{"(#{value.inspect})" if value}#{pos_info}"
114
-
115
- children.each do |child|
116
- lines << child.to_tree(indent + 1)
117
- end
118
-
236
+ children.each { |child| lines << child.to_tree(indent + 1) }
119
237
  lines.join("\n")
120
238
  end
121
239
 
@@ -123,33 +241,32 @@ module Parselly
123
241
  "#<#{self.class.name} type=#{type} value=#{value.inspect} children=#{children.size}>"
124
242
  end
125
243
 
126
- # Converts the AST node back to a CSS selector string.
127
- #
128
- # @return [String] the CSS selector string representation of this node
129
- def to_selector
244
+ def to_selector(mode: :normalized)
245
+ validate_selector_mode!(mode)
246
+
130
247
  case type
131
248
  when :selector_list
132
- children.map(&:to_selector).join(', ')
249
+ children.map { |child| child.to_selector(mode: mode) }.join(', ')
133
250
  when :selector
134
- children.map(&:to_selector).join
251
+ build_selector(mode)
135
252
  when :simple_selector_sequence
136
- children.map(&:to_selector).join
137
- when :type_selector
138
- value
139
- when :universal_selector
140
- value
253
+ children.map { |child| child.to_selector(mode: mode) }.join
254
+ when :type_selector, :universal_selector
255
+ selector_name(mode)
141
256
  when :id_selector
142
- "##{value}"
257
+ "##{selector_identifier(mode)}"
143
258
  when :class_selector
144
- ".#{value}"
259
+ ".#{selector_identifier(mode)}"
145
260
  when :attribute_selector
146
- build_attribute_selector
261
+ build_attribute_selector(mode)
147
262
  when :pseudo_class
148
- ":#{value}"
263
+ "#{selector_prefix(mode, ':')}#{selector_identifier(mode)}"
149
264
  when :pseudo_element
150
- "::#{value}"
265
+ "#{selector_prefix(mode, '::')}#{selector_identifier(mode)}"
151
266
  when :pseudo_function
152
- ":#{value}(#{children.map(&:to_selector).join})"
267
+ "#{selector_prefix(mode, ':')}#{selector_identifier(mode)}(#{children.map { |child| child.to_selector(mode: mode) }.join})"
268
+ when :pseudo_element_function
269
+ "#{selector_prefix(mode, '::')}#{selector_identifier(mode)}(#{children.map { |child| child.to_selector(mode: mode) }.join})"
153
270
  when :child_combinator
154
271
  ' > '
155
272
  when :adjacent_combinator
@@ -158,127 +275,198 @@ module Parselly
158
275
  ' ~ '
159
276
  when :descendant_combinator
160
277
  ' '
161
- when :an_plus_b, :argument
162
- value
163
- when :attribute, :value
164
- value
165
- when :equal_operator, :includes_operator, :dashmatch_operator,
278
+ when :column_combinator
279
+ ' || '
280
+ when :nth_selector_argument
281
+ "#{children[0].to_selector(mode: mode)} of #{children[1].to_selector(mode: mode)}"
282
+ when :an_plus_b
283
+ value.to_s
284
+ when :argument
285
+ argument_selector(mode)
286
+ when :attribute, :value,
287
+ :equal_operator, :includes_operator, :dashmatch_operator,
166
288
  :prefixmatch_operator, :suffixmatch_operator, :substringmatch_operator
167
- value
289
+ value.to_s
168
290
  else
169
- children.map(&:to_selector).join
291
+ children.map { |child| child.to_selector(mode: mode) }.join
170
292
  end
171
293
  end
172
294
 
173
- # Checks if this node or any descendant contains an ID selector.
174
- #
175
- # @return [Boolean] true if an ID selector is present
176
295
  def id?
177
- return true if type == :id_selector
178
- descendants.any? { |node| node.type == :id_selector }
296
+ any? { |node| node.type == :id_selector }
179
297
  end
180
298
 
181
- # Extracts the ID value from this node or its descendants.
182
- #
183
- # @return [String, nil] the ID value without the '#' prefix, or nil if no ID selector is found
184
299
  def id
185
- return value if type == :id_selector
300
+ ids.first
301
+ end
186
302
 
187
- descendants.each do |node|
188
- return node.value if node.type == :id_selector
189
- end
190
- nil
303
+ def ids
304
+ each.with_object([]) { |node, result| result << node.value if node.type == :id_selector }
191
305
  end
192
306
 
193
- # Extracts all class names from this node and its descendants.
194
- #
195
- # @return [Array<String>] array of class names without the '.' prefix
196
307
  def classes
197
- result = []
198
- result << value if type == :class_selector
199
- descendants.each do |node|
200
- result << node.value if node.type == :class_selector
201
- end
202
- result
308
+ each.with_object([]) { |node, result| result << node.value if node.type == :class_selector }
203
309
  end
204
310
 
205
- # Checks if this node or any descendant contains an attribute selector.
206
- #
207
- # @return [Boolean] true if an attribute selector is present
208
311
  def attribute?
209
- return true if type == :attribute_selector
210
- descendants.any? { |node| node.type == :attribute_selector }
312
+ any? { |node| node.type == :attribute_selector }
211
313
  end
212
314
 
213
- # Extracts all attribute selectors from this node and its descendants.
214
- #
215
- # @return [Array<Hash>] array of attribute information hashes
216
- # Each hash contains :name, :operator (optional), and :value (optional) keys
217
315
  def attributes
218
- result = []
316
+ attribute_selector_nodes.map { |node| extract_attribute_info(node) }
317
+ end
219
318
 
220
- if type == :attribute_selector
221
- result << extract_attribute_info(self)
222
- end
319
+ def attribute_selectors
320
+ attribute_selector_nodes.map { |node| extract_attribute_node(node) }
321
+ end
223
322
 
224
- descendants.each do |node|
225
- if node.type == :attribute_selector
226
- result << extract_attribute_info(node)
323
+ def pseudo_classes
324
+ each.with_object([]) do |node, result|
325
+ if [:pseudo_class, :pseudo_element, :pseudo_function, :pseudo_element_function].include?(node.type)
326
+ result << node.value
227
327
  end
228
328
  end
329
+ end
229
330
 
230
- result
331
+ def pseudo_class_names
332
+ each.with_object([]) { |node, result| result << node.value if node.type == :pseudo_class }
231
333
  end
232
334
 
233
- # Extracts all pseudo-classes and pseudo-elements from this node and its descendants.
234
- #
235
- # @return [Array<String>] array of pseudo-class and pseudo-element names
236
- def pseudo_classes
237
- result = []
335
+ def pseudo_element_names
336
+ each.with_object([]) do |node, result|
337
+ result << node.value if [:pseudo_element, :pseudo_element_function].include?(node.type)
338
+ end
339
+ end
340
+
341
+ def pseudo_function_names
342
+ each.with_object([]) { |node, result| result << node.value if node.type == :pseudo_function }
343
+ end
344
+
345
+ def type_selector?
346
+ any? { |node| node.type == :type_selector }
347
+ end
348
+
349
+ def type_names
350
+ each.with_object([]) { |node, result| result << node.value if node.type == :type_selector }
351
+ end
238
352
 
239
- if [:pseudo_class, :pseudo_element, :pseudo_function].include?(type)
240
- result << value
353
+ def type_selectors
354
+ each.with_object([]) do |node, result|
355
+ next unless node.type == :type_selector
356
+
357
+ detail = { name: node.value, raw_name: node.raw_value, position: node.position }
358
+ detail[:namespace] = node.namespace unless node.namespace.nil?
359
+ result << detail
241
360
  end
361
+ end
242
362
 
243
- descendants.each do |node|
244
- if [:pseudo_class, :pseudo_element, :pseudo_function].include?(node.type)
245
- result << node.value
246
- end
363
+ def combinators
364
+ each.with_object([]) do |node, result|
365
+ next unless COMBINATOR_TYPES.key?(node.type)
366
+
367
+ result << { type: node.type, value: node.value, position: node.position }
247
368
  end
369
+ end
248
370
 
249
- result
371
+ def selector_list?
372
+ type == :selector_list
373
+ end
374
+
375
+ def complex_selector?
376
+ type == :selector || any? { |node| COMBINATOR_TYPES.key?(node.type) }
250
377
  end
251
378
 
252
- # Checks if this selector is a compound selector, as defined by CSS.
253
- # A compound selector combines multiple simple selectors (type, class, id,
254
- # attribute, pseudo-class) without combinators (e.g., `div.class#id[attr]:hover`).
255
- # Returns true if more than one simple selector type is present.
256
- #
257
- # @return [Boolean] true if this node represents a compound selector
258
379
  def compound_selector?
259
- types = []
380
+ case type
381
+ when :selector_list
382
+ children.size == 1 && children.first.compound_selector?
383
+ when :simple_selector_sequence
384
+ children.count { |child| SIMPLE_SELECTOR_TYPES.include?(child.type) } > 1
385
+ else
386
+ false
387
+ end
388
+ end
260
389
 
261
- types << :id if id?
262
- types << :class unless classes.empty?
263
- types << :attribute if attribute?
264
- types << :pseudo unless pseudo_classes.empty?
265
- types << :type if type_selector?
390
+ def specificity
391
+ case type
392
+ when :selector_list
393
+ children.map(&:specificity).max || [0, 0, 0]
394
+ when :selector, :simple_selector_sequence
395
+ children.reduce([0, 0, 0]) { |sum, child| add_specificity(sum, child.specificity) }
396
+ when :id_selector
397
+ [1, 0, 0]
398
+ when :class_selector, :attribute_selector, :pseudo_class
399
+ [0, 1, 0]
400
+ when :type_selector, :pseudo_element, :pseudo_element_function
401
+ [0, 0, 1]
402
+ when :pseudo_function
403
+ pseudo_function_specificity
404
+ else
405
+ [0, 0, 0]
406
+ end
407
+ end
266
408
 
267
- types.size > 1
409
+ def to_h
410
+ hash = {
411
+ type: type,
412
+ value: value,
413
+ raw_value: raw_value,
414
+ namespace: namespace,
415
+ quote: quote,
416
+ modifier: modifier,
417
+ prefix: prefix,
418
+ position: position,
419
+ children: children.map(&:to_h)
420
+ }
421
+ hash.delete_if { |key, val| key != :children && (val.nil? || val == {}) }
268
422
  end
269
423
 
270
- # Checks if this node or any descendant contains a type selector.
271
- #
272
- # @return [Boolean] true if a type selector is present
273
- def type_selector?
274
- return true if type == :type_selector
275
- descendants.any? { |node| node.type == :type_selector }
424
+ def as_json(*)
425
+ to_h
276
426
  end
277
427
 
428
+ def deconstruct_keys(keys)
429
+ hash = to_h
430
+ return hash if keys.nil?
431
+
432
+ keys.each_with_object({}) { |key, result| result[key] = hash[key] if hash.key?(key) }
433
+ end
434
+
435
+ def freeze_tree
436
+ children.each(&:freeze_tree)
437
+ children.freeze
438
+ freeze
439
+ end
440
+
441
+ def dup_tree
442
+ duplicate = self.class.new(
443
+ type,
444
+ value,
445
+ position.dup,
446
+ raw_value: raw_value,
447
+ namespace: namespace,
448
+ quote: quote,
449
+ modifier: modifier,
450
+ prefix: prefix
451
+ )
452
+ children.each { |child| duplicate.add_child(child.dup_tree) }
453
+ duplicate
454
+ end
455
+ alias deep_dup dup_tree
456
+
278
457
  private
279
458
 
280
- # Invalidates the descendants cache for this node and all ancestors.
281
- # This ensures that cached descendants are cleared when the tree structure changes.
459
+ def adopt_child(node)
460
+ raise ArgumentError, 'child must be a Parselly::Node' unless node.is_a?(Node)
461
+
462
+ node.parent.remove_child(node) if node.parent && node.parent != self
463
+ node.parent = self
464
+ end
465
+
466
+ def detach_child(node)
467
+ node.parent = nil if node&.parent == self
468
+ end
469
+
282
470
  def invalidate_cache
283
471
  node = self
284
472
  while node
@@ -287,20 +475,18 @@ module Parselly
287
475
  end
288
476
  end
289
477
 
290
- # Helper method to extract attribute information from an attribute_selector node.
291
- #
292
- # @param node [Node] an attribute_selector node
293
- # @return [Hash] attribute information hash
478
+ def attribute_selector_nodes
479
+ each.select { |node| node.type == :attribute_selector }
480
+ end
481
+
294
482
  def extract_attribute_info(node)
295
483
  info = {}
296
484
 
297
- # Simple attribute selector like [disabled]
298
485
  if node.value
299
486
  info[:name] = node.value
300
487
  return info
301
488
  end
302
489
 
303
- # Attribute selector with operator and value like [type="text"]
304
490
  node.children.each do |child|
305
491
  case child.type
306
492
  when :attribute
@@ -313,17 +499,47 @@ module Parselly
313
499
  end
314
500
  end
315
501
 
502
+ info[:modifier] = node.modifier if node.modifier
503
+ info
504
+ end
505
+
506
+ def extract_attribute_node(node)
507
+ info = {}
508
+
509
+ if node.value
510
+ info[:name] = node.value
511
+ info[:raw_name] = node.raw_value
512
+ info[:namespace] = node.namespace unless node.namespace.nil?
513
+ info[:position] = node.position unless node.position.empty?
514
+ return info
515
+ end
516
+
517
+ info[:modifier] = node.modifier if node.modifier
518
+ node.children.each do |child|
519
+ case child.type
520
+ when :attribute
521
+ info[:name] = child.value
522
+ info[:raw_name] = child.raw_value
523
+ info[:namespace] = child.namespace unless child.namespace.nil?
524
+ info[:position] = child.position unless child.position.empty?
525
+ when :equal_operator, :includes_operator, :dashmatch_operator,
526
+ :prefixmatch_operator, :suffixmatch_operator, :substringmatch_operator
527
+ info[:operator] = child.value
528
+ when :value
529
+ info[:value] = child.value
530
+ info[:raw_value] = child.raw_value
531
+ info[:quote] = child.quote if child.quote
532
+ end
533
+ end
534
+
316
535
  info
317
536
  end
318
537
 
319
- # Helper method to build an attribute selector string.
320
- #
321
- # @return [String] the attribute selector string
322
- def build_attribute_selector
323
- # Simple attribute selector like [disabled]
324
- return "[#{value}]" if value
538
+ def build_attribute_selector(mode)
539
+ if value
540
+ return "[#{attribute_name_for(self, mode)}]"
541
+ end
325
542
 
326
- # Attribute selector with operator and value like [type="text"]
327
543
  attr_name = nil
328
544
  operator = nil
329
545
  attr_value = nil
@@ -331,20 +547,118 @@ module Parselly
331
547
  children.each do |child|
332
548
  case child.type
333
549
  when :attribute
334
- attr_name = child.value
550
+ attr_name = attribute_name_for(child, mode)
335
551
  when :equal_operator, :includes_operator, :dashmatch_operator,
336
552
  :prefixmatch_operator, :suffixmatch_operator, :substringmatch_operator
337
553
  operator = child.value
338
554
  when :value
339
- attr_value = child.value
555
+ attr_value = attribute_value_for(child, mode)
340
556
  end
341
557
  end
342
558
 
343
- if operator && attr_value
344
- "[#{attr_name}#{operator}\"#{attr_value}\"]"
345
- else
346
- "[#{attr_name}]"
559
+ modifier_part = modifier ? " #{modifier}" : ''
560
+ operator && attr_value ? "[#{attr_name}#{operator}#{attr_value}#{modifier_part}]" : "[#{attr_name}]"
561
+ end
562
+
563
+ def attribute_name_for(node, mode)
564
+ return node.raw_value.to_s if mode == :preserve && node.raw_value
565
+
566
+ local = Parselly.sanitize(node.value.to_s)
567
+ return local if node.namespace.nil?
568
+
569
+ prefix = node.namespace == '*' ? '*' : Parselly.sanitize(node.namespace.to_s)
570
+ "#{prefix}|#{local}"
571
+ end
572
+
573
+ def attribute_value_for(node, mode)
574
+ if mode == :preserve
575
+ value = node.raw_value.to_s
576
+ return "#{node.quote}#{value}#{node.quote}" if node.quote
577
+
578
+ return value
347
579
  end
580
+
581
+ "\"#{escape_string(node.value.to_s)}\""
582
+ end
583
+
584
+ def selector_name(mode)
585
+ return raw_value.to_s if mode == :preserve && raw_value
586
+
587
+ local = value == '*' ? '*' : Parselly.sanitize(value.to_s)
588
+ return local if namespace.nil?
589
+
590
+ prefix = namespace == '*' ? '*' : Parselly.sanitize(namespace.to_s)
591
+ "#{prefix}|#{local}"
592
+ end
593
+
594
+ def selector_identifier(mode)
595
+ return raw_value.to_s if mode == :preserve && raw_value
596
+
597
+ Parselly.sanitize(value.to_s)
598
+ end
599
+
600
+ def build_selector(mode)
601
+ parts = children.map { |child| child.to_selector(mode: mode) }
602
+ parts[0] = parts[0].lstrip if children.first && COMBINATOR_TYPES.key?(children.first.type)
603
+ parts.join
604
+ end
605
+
606
+ def selector_prefix(mode, normalized_prefix)
607
+ mode == :preserve && prefix ? prefix : normalized_prefix
608
+ end
609
+
610
+ def argument_selector(mode)
611
+ if quote
612
+ value = mode == :preserve ? raw_value.to_s : escape_string(value.to_s)
613
+ return "#{quote}#{value}#{quote}"
614
+ end
615
+
616
+ mode == :preserve && raw_value ? raw_value.to_s : value.to_s
617
+ end
618
+
619
+ def escape_string(string)
620
+ string.each_char.with_object(+'') do |char, result|
621
+ case char
622
+ when '"', '\\'
623
+ result << "\\#{char}"
624
+ when "\n"
625
+ result << '\\a '
626
+ when "\r"
627
+ result << '\\d '
628
+ when "\f"
629
+ result << '\\c '
630
+ else
631
+ result << char
632
+ end
633
+ end
634
+ end
635
+
636
+ def validate_selector_mode!(mode)
637
+ return if [:normalized, :preserve].include?(mode)
638
+
639
+ raise ArgumentError, "unknown selector serialization mode: #{mode.inspect}"
640
+ end
641
+
642
+ def pseudo_function_specificity
643
+ name = value.to_s.downcase
644
+ return [0, 0, 0] if SPECIFICITY_ZERO_PSEUDO_FUNCTIONS.include?(name)
645
+
646
+ if SPECIFICITY_MAX_ARGUMENT_PSEUDO_FUNCTIONS.include?(name)
647
+ child = children.first
648
+ return child ? child.specificity : [0, 0, 0]
649
+ end
650
+
651
+ if NTH_PSEUDO_FUNCTIONS.include?(name)
652
+ nth_argument = children.first
653
+ selector_specificity = nth_argument&.type == :nth_selector_argument ? nth_argument.children[1].specificity : [0, 0, 0]
654
+ return add_specificity([0, 1, 0], selector_specificity)
655
+ end
656
+
657
+ [0, 1, 0]
658
+ end
659
+
660
+ def add_specificity(left, right)
661
+ [left[0] + right[0], left[1] + right[1], left[2] + right[2]]
348
662
  end
349
663
  end
350
664
  end