parselly 1.2.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,36 +2,114 @@
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, offset: 0 })
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, :raw_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, :column, and :offset keys
26
- # @param line [Integer, nil] optional line number (keyword alternative)
27
- # @param column [Integer, nil] optional column number (keyword alternative)
28
- # @param offset [Integer, nil] optional offset (keyword alternative)
29
- def initialize(type, value = nil, position = {}, raw_value: nil, line: nil, column: nil, offset: nil)
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)
30
104
  @type = type
31
105
  @value = value
32
106
  @raw_value = raw_value.nil? ? value : raw_value
33
- @children = []
107
+ @children = ChildList.new(self)
34
108
  @parent = nil
109
+ @namespace = namespace
110
+ @quote = quote
111
+ @modifier = modifier
112
+ @prefix = prefix
35
113
  unless position.nil? || position.is_a?(Hash)
36
114
  raise ArgumentError, 'position must be a Hash'
37
115
  end
@@ -40,44 +118,67 @@ module Parselly
40
118
  resolved_position[:line] = line unless line.nil?
41
119
  resolved_position[:column] = column unless column.nil?
42
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?
43
127
  @position = resolved_position
44
128
  @descendants_cache = nil
45
129
  end
46
130
 
47
- # Adds a child node to this node.
48
- #
49
- # @param node [Node, nil] the child node to add
50
- # @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
+
51
136
  def add_child(node)
52
137
  return nil if node.nil?
53
138
 
54
- node.parent = self
55
139
  @children << node
56
- invalidate_cache
57
140
  node
58
141
  end
59
142
 
60
- # Replaces a child node at the specified index.
61
- #
62
- # @param index [Integer] the index of the child to replace
63
- # @param new_node [Node] the new child node
64
- # @return [Node, nil] the new node, or nil if invalid parameters
65
143
  def replace_child(index, new_node)
66
144
  return nil if new_node.nil?
67
- return nil if index < 0 || index >= @children.size
68
-
69
- old_node = @children[index]
70
- old_node.parent = nil if old_node
145
+ return nil if index.negative? || index >= @children.size
71
146
 
72
147
  @children[index] = new_node
73
- new_node.parent = self
74
- invalidate_cache
75
- new_node
76
148
  end
77
149
 
78
- # Returns an array of all ancestor nodes from parent to root.
79
- #
80
- # @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
+
81
182
  def ancestors
82
183
  result = []
83
184
  node = parent
@@ -88,25 +189,21 @@ module Parselly
88
189
  result
89
190
  end
90
191
 
91
- # Returns an array of all descendant nodes (children, grandchildren, etc.).
92
- #
93
- # @return [Array<Node>] array of all descendant nodes
94
192
  def descendants
95
193
  return @descendants_cache if @descendants_cache
96
194
 
97
195
  @descendants_cache = []
98
- queue = @children.dup
99
- until queue.empty?
100
- node = queue.shift
196
+ queue = @children.to_a
197
+ index = 0
198
+ while index < queue.length
199
+ node = queue[index]
101
200
  @descendants_cache << node
102
201
  queue.concat(node.children) unless node.children.empty?
202
+ index += 1
103
203
  end
104
204
  @descendants_cache
105
205
  end
106
206
 
107
- # Depth-first traversal of this node and its descendants.
108
- #
109
- # @return [Enumerator, Node] enumerator if no block, otherwise self
110
207
  def each
111
208
  return enum_for(:each) unless block_given?
112
209
 
@@ -114,45 +211,29 @@ module Parselly
114
211
  until stack.empty?
115
212
  node = stack.pop
116
213
  yield node
117
- children = node.children
118
- stack.concat(children.reverse) if children && !children.empty?
214
+ stack.concat(node.children.reverse) unless node.children.empty?
119
215
  end
120
216
 
121
217
  self
122
218
  end
123
219
 
124
- # Finds all nodes of a given type in this subtree.
125
- #
126
- # @param type [Symbol] the node type to match
127
- # @return [Array<Node>] array of matching nodes
128
220
  def find_all(type)
129
- each.with_object([]) { |node, acc| acc << node if node.type == type }
221
+ each.select { |node| node.type == type }
130
222
  end
131
223
 
132
- # Returns an array of sibling nodes (excluding self).
133
- #
134
- # @return [Array<Node>] array of sibling nodes, or empty array if no parent
135
224
  def siblings
136
225
  return [] unless parent
137
226
 
138
227
  parent.children.reject { |child| child == self }
139
228
  end
140
229
 
141
- # Returns a tree representation of this node and its descendants.
142
- #
143
- # @param indent [Integer] indentation level for the tree display
144
- # @return [String] formatted tree string
145
230
  def to_tree(indent = 0)
146
231
  lines = []
147
232
  prefix = ' ' * indent
148
233
  pos_info = position.empty? ? '' : " [#{position[:line]}:#{position[:column]}]"
149
234
 
150
235
  lines << "#{prefix}#{type}#{"(#{value.inspect})" if value}#{pos_info}"
151
-
152
- children.each do |child|
153
- lines << child.to_tree(indent + 1)
154
- end
155
-
236
+ children.each { |child| lines << child.to_tree(indent + 1) }
156
237
  lines.join("\n")
157
238
  end
158
239
 
@@ -160,33 +241,32 @@ module Parselly
160
241
  "#<#{self.class.name} type=#{type} value=#{value.inspect} children=#{children.size}>"
161
242
  end
162
243
 
163
- # Converts the AST node back to a CSS selector string.
164
- #
165
- # @return [String] the CSS selector string representation of this node
166
- def to_selector
244
+ def to_selector(mode: :normalized)
245
+ validate_selector_mode!(mode)
246
+
167
247
  case type
168
248
  when :selector_list
169
- children.map(&:to_selector).join(', ')
249
+ children.map { |child| child.to_selector(mode: mode) }.join(', ')
170
250
  when :selector
171
- children.map(&:to_selector).join
251
+ build_selector(mode)
172
252
  when :simple_selector_sequence
173
- children.map(&:to_selector).join
174
- when :type_selector
175
- value
176
- when :universal_selector
177
- value
253
+ children.map { |child| child.to_selector(mode: mode) }.join
254
+ when :type_selector, :universal_selector
255
+ selector_name(mode)
178
256
  when :id_selector
179
- "##{value}"
257
+ "##{selector_identifier(mode)}"
180
258
  when :class_selector
181
- ".#{value}"
259
+ ".#{selector_identifier(mode)}"
182
260
  when :attribute_selector
183
- build_attribute_selector
261
+ build_attribute_selector(mode)
184
262
  when :pseudo_class
185
- ":#{value}"
263
+ "#{selector_prefix(mode, ':')}#{selector_identifier(mode)}"
186
264
  when :pseudo_element
187
- "::#{value}"
265
+ "#{selector_prefix(mode, '::')}#{selector_identifier(mode)}"
188
266
  when :pseudo_function
189
- ":#{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})"
190
270
  when :child_combinator
191
271
  ' > '
192
272
  when :adjacent_combinator
@@ -195,145 +275,198 @@ module Parselly
195
275
  ' ~ '
196
276
  when :descendant_combinator
197
277
  ' '
198
- when :an_plus_b, :argument
199
- value
200
- when :attribute, :value
201
- value
202
- 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,
203
288
  :prefixmatch_operator, :suffixmatch_operator, :substringmatch_operator
204
- value
289
+ value.to_s
205
290
  else
206
- children.map(&:to_selector).join
291
+ children.map { |child| child.to_selector(mode: mode) }.join
207
292
  end
208
293
  end
209
294
 
210
- # Checks if this node or any descendant contains an ID selector.
211
- #
212
- # @return [Boolean] true if an ID selector is present
213
295
  def id?
214
- return true if type == :id_selector
215
- descendants.any? { |node| node.type == :id_selector }
296
+ any? { |node| node.type == :id_selector }
216
297
  end
217
298
 
218
- # Extracts the ID value from this node or its descendants.
219
- #
220
- # @return [String, nil] the ID value without the '#' prefix, or nil if no ID selector is found
221
299
  def id
222
- return value if type == :id_selector
300
+ ids.first
301
+ end
223
302
 
224
- descendants.each do |node|
225
- return node.value if node.type == :id_selector
226
- end
227
- nil
303
+ def ids
304
+ each.with_object([]) { |node, result| result << node.value if node.type == :id_selector }
228
305
  end
229
306
 
230
- # Extracts all class names from this node and its descendants.
231
- #
232
- # @return [Array<String>] array of class names without the '.' prefix
233
307
  def classes
234
- result = []
235
- result << value if type == :class_selector
236
- descendants.each do |node|
237
- result << node.value if node.type == :class_selector
238
- end
239
- result
308
+ each.with_object([]) { |node, result| result << node.value if node.type == :class_selector }
240
309
  end
241
310
 
242
- # Checks if this node or any descendant contains an attribute selector.
243
- #
244
- # @return [Boolean] true if an attribute selector is present
245
311
  def attribute?
246
- return true if type == :attribute_selector
247
- descendants.any? { |node| node.type == :attribute_selector }
312
+ any? { |node| node.type == :attribute_selector }
248
313
  end
249
314
 
250
- # Extracts all attribute selectors from this node and its descendants.
251
- #
252
- # @return [Array<Hash>] array of attribute information hashes
253
- # Each hash contains :name, :operator (optional), and :value (optional) keys
254
315
  def attributes
255
- result = []
316
+ attribute_selector_nodes.map { |node| extract_attribute_info(node) }
317
+ end
256
318
 
257
- if type == :attribute_selector
258
- result << extract_attribute_info(self)
259
- end
319
+ def attribute_selectors
320
+ attribute_selector_nodes.map { |node| extract_attribute_node(node) }
321
+ end
260
322
 
261
- descendants.each do |node|
262
- if node.type == :attribute_selector
263
- 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
264
327
  end
265
328
  end
329
+ end
266
330
 
267
- result
331
+ def pseudo_class_names
332
+ each.with_object([]) { |node, result| result << node.value if node.type == :pseudo_class }
268
333
  end
269
334
 
270
- # Extracts detailed attribute selector nodes from this node and its descendants.
271
- #
272
- # @return [Array<Hash>] array of attribute selector detail hashes
273
- # Each hash contains :name, :operator (optional), and :value (optional) keys
274
- def attribute_selectors
275
- 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
276
344
 
277
- if type == :attribute_selector
278
- result << extract_attribute_node(self)
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
352
+
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
279
360
  end
361
+ end
362
+
363
+ def combinators
364
+ each.with_object([]) do |node, result|
365
+ next unless COMBINATOR_TYPES.key?(node.type)
280
366
 
281
- descendants.each do |node|
282
- result << extract_attribute_node(node) if node.type == :attribute_selector
367
+ result << { type: node.type, value: node.value, position: node.position }
283
368
  end
369
+ end
284
370
 
285
- result
371
+ def selector_list?
372
+ type == :selector_list
286
373
  end
287
374
 
288
- # Extracts all pseudo-classes and pseudo-elements from this node and its descendants.
289
- #
290
- # @return [Array<String>] array of pseudo-class and pseudo-element names
291
- def pseudo_classes
292
- result = []
375
+ def complex_selector?
376
+ type == :selector || any? { |node| COMBINATOR_TYPES.key?(node.type) }
377
+ end
293
378
 
294
- if [:pseudo_class, :pseudo_element, :pseudo_function].include?(type)
295
- result << value
379
+ def compound_selector?
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
296
387
  end
388
+ end
297
389
 
298
- descendants.each do |node|
299
- if [:pseudo_class, :pseudo_element, :pseudo_function].include?(node.type)
300
- result << node.value
301
- end
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]
302
406
  end
407
+ end
303
408
 
304
- result
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 == {}) }
305
422
  end
306
423
 
307
- # Checks if this selector is a compound selector, as defined by CSS.
308
- # A compound selector combines multiple simple selectors (type, class, id,
309
- # attribute, pseudo-class) without combinators (e.g., `div.class#id[attr]:hover`).
310
- # Returns true if more than one simple selector type is present.
311
- #
312
- # @return [Boolean] true if this node represents a compound selector
313
- def compound_selector?
314
- types = []
424
+ def as_json(*)
425
+ to_h
426
+ end
315
427
 
316
- types << :id if id?
317
- types << :class unless classes.empty?
318
- types << :attribute if attribute?
319
- types << :pseudo unless pseudo_classes.empty?
320
- types << :type if type_selector?
428
+ def deconstruct_keys(keys)
429
+ hash = to_h
430
+ return hash if keys.nil?
321
431
 
322
- types.size > 1
432
+ keys.each_with_object({}) { |key, result| result[key] = hash[key] if hash.key?(key) }
323
433
  end
324
434
 
325
- # Checks if this node or any descendant contains a type selector.
326
- #
327
- # @return [Boolean] true if a type selector is present
328
- def type_selector?
329
- return true if type == :type_selector
330
- descendants.any? { |node| node.type == :type_selector }
435
+ def freeze_tree
436
+ children.each(&:freeze_tree)
437
+ children.freeze
438
+ freeze
331
439
  end
332
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
+
333
457
  private
334
458
 
335
- # Invalidates the descendants cache for this node and all ancestors.
336
- # 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
+
337
470
  def invalidate_cache
338
471
  node = self
339
472
  while node
@@ -342,20 +475,18 @@ module Parselly
342
475
  end
343
476
  end
344
477
 
345
- # Helper method to extract attribute information from an attribute_selector node.
346
- #
347
- # @param node [Node] an attribute_selector node
348
- # @return [Hash] attribute information hash
478
+ def attribute_selector_nodes
479
+ each.select { |node| node.type == :attribute_selector }
480
+ end
481
+
349
482
  def extract_attribute_info(node)
350
483
  info = {}
351
484
 
352
- # Simple attribute selector like [disabled]
353
485
  if node.value
354
486
  info[:name] = node.value
355
487
  return info
356
488
  end
357
489
 
358
- # Attribute selector with operator and value like [type="text"]
359
490
  node.children.each do |child|
360
491
  case child.type
361
492
  when :attribute
@@ -368,47 +499,47 @@ module Parselly
368
499
  end
369
500
  end
370
501
 
502
+ info[:modifier] = node.modifier if node.modifier
371
503
  info
372
504
  end
373
505
 
374
- # Helper method to extract detailed attribute selector data.
375
- #
376
- # @param node [Node] an attribute_selector node
377
- # @return [Hash] attribute selector detail hash
378
506
  def extract_attribute_node(node)
379
507
  info = {}
380
508
 
381
509
  if node.value
382
510
  info[:name] = node.value
383
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?
384
514
  return info
385
515
  end
386
516
 
517
+ info[:modifier] = node.modifier if node.modifier
387
518
  node.children.each do |child|
388
519
  case child.type
389
520
  when :attribute
390
521
  info[:name] = child.value
391
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?
392
525
  when :equal_operator, :includes_operator, :dashmatch_operator,
393
526
  :prefixmatch_operator, :suffixmatch_operator, :substringmatch_operator
394
527
  info[:operator] = child.value
395
528
  when :value
396
529
  info[:value] = child.value
397
530
  info[:raw_value] = child.raw_value
531
+ info[:quote] = child.quote if child.quote
398
532
  end
399
533
  end
400
534
 
401
535
  info
402
536
  end
403
537
 
404
- # Helper method to build an attribute selector string.
405
- #
406
- # @return [String] the attribute selector string
407
- def build_attribute_selector
408
- # Simple attribute selector like [disabled]
409
- return "[#{value}]" if value
538
+ def build_attribute_selector(mode)
539
+ if value
540
+ return "[#{attribute_name_for(self, mode)}]"
541
+ end
410
542
 
411
- # Attribute selector with operator and value like [type="text"]
412
543
  attr_name = nil
413
544
  operator = nil
414
545
  attr_value = nil
@@ -416,20 +547,118 @@ module Parselly
416
547
  children.each do |child|
417
548
  case child.type
418
549
  when :attribute
419
- attr_name = child.value
550
+ attr_name = attribute_name_for(child, mode)
420
551
  when :equal_operator, :includes_operator, :dashmatch_operator,
421
552
  :prefixmatch_operator, :suffixmatch_operator, :substringmatch_operator
422
553
  operator = child.value
423
554
  when :value
424
- attr_value = child.value
555
+ attr_value = attribute_value_for(child, mode)
425
556
  end
426
557
  end
427
558
 
428
- if operator && attr_value
429
- "[#{attr_name}#{operator}\"#{attr_value}\"]"
430
- else
431
- "[#{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
432
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]]
433
662
  end
434
663
  end
435
664
  end