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.
- checksums.yaml +4 -4
- data/README.md +56 -9
- data/Rakefile +10 -0
- data/lib/parselly/lexer.rb +278 -68
- data/lib/parselly/node.rb +434 -205
- data/lib/parselly/parser.rb +799 -325
- data/lib/parselly/version.rb +1 -1
- data/lib/parselly.rb +57 -10
- data/parser.y +454 -101
- metadata +3 -3
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
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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
|
|
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
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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.
|
|
99
|
-
|
|
100
|
-
|
|
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
|
|
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.
|
|
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
|
-
|
|
164
|
-
|
|
165
|
-
|
|
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(
|
|
249
|
+
children.map { |child| child.to_selector(mode: mode) }.join(', ')
|
|
170
250
|
when :selector
|
|
171
|
-
|
|
251
|
+
build_selector(mode)
|
|
172
252
|
when :simple_selector_sequence
|
|
173
|
-
children.map(
|
|
174
|
-
when :type_selector
|
|
175
|
-
|
|
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
|
-
"##{
|
|
257
|
+
"##{selector_identifier(mode)}"
|
|
180
258
|
when :class_selector
|
|
181
|
-
".#{
|
|
259
|
+
".#{selector_identifier(mode)}"
|
|
182
260
|
when :attribute_selector
|
|
183
|
-
build_attribute_selector
|
|
261
|
+
build_attribute_selector(mode)
|
|
184
262
|
when :pseudo_class
|
|
185
|
-
"
|
|
263
|
+
"#{selector_prefix(mode, ':')}#{selector_identifier(mode)}"
|
|
186
264
|
when :pseudo_element
|
|
187
|
-
"
|
|
265
|
+
"#{selector_prefix(mode, '::')}#{selector_identifier(mode)}"
|
|
188
266
|
when :pseudo_function
|
|
189
|
-
"
|
|
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 :
|
|
199
|
-
|
|
200
|
-
when :
|
|
201
|
-
|
|
202
|
-
when :
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
300
|
+
ids.first
|
|
301
|
+
end
|
|
223
302
|
|
|
224
|
-
|
|
225
|
-
|
|
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
|
-
|
|
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
|
-
|
|
316
|
+
attribute_selector_nodes.map { |node| extract_attribute_info(node) }
|
|
317
|
+
end
|
|
256
318
|
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
319
|
+
def attribute_selectors
|
|
320
|
+
attribute_selector_nodes.map { |node| extract_attribute_node(node) }
|
|
321
|
+
end
|
|
260
322
|
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
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
|
-
|
|
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
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
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
|
-
|
|
278
|
-
|
|
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
|
-
|
|
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
|
-
|
|
371
|
+
def selector_list?
|
|
372
|
+
type == :selector_list
|
|
286
373
|
end
|
|
287
374
|
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
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
|
-
|
|
295
|
-
|
|
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
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
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
|
-
|
|
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
|
-
|
|
308
|
-
|
|
309
|
-
|
|
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
|
-
|
|
317
|
-
|
|
318
|
-
|
|
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
|
-
|
|
432
|
+
keys.each_with_object({}) { |key, result| result[key] = hash[key] if hash.key?(key) }
|
|
323
433
|
end
|
|
324
434
|
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
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
|
-
|
|
336
|
-
|
|
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
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
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
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
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
|
|
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
|
|
555
|
+
attr_value = attribute_value_for(child, mode)
|
|
425
556
|
end
|
|
426
557
|
end
|
|
427
558
|
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
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
|