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.
- checksums.yaml +4 -4
- data/README.md +56 -9
- data/Rakefile +10 -0
- data/lib/parselly/lexer.rb +284 -67
- data/lib/parselly/node.rb +491 -177
- data/lib/parselly/parser.rb +874 -295
- data/lib/parselly/version.rb +1 -1
- data/lib/parselly.rb +65 -3
- data/parser.y +528 -77
- metadata +3 -3
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
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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
|
-
@
|
|
106
|
+
@raw_value = raw_value.nil? ? value : raw_value
|
|
107
|
+
@children = ChildList.new(self)
|
|
30
108
|
@parent = nil
|
|
31
|
-
@
|
|
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
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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
|
|
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
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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.
|
|
87
|
-
|
|
88
|
-
|
|
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
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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
|
-
|
|
127
|
-
|
|
128
|
-
|
|
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(
|
|
249
|
+
children.map { |child| child.to_selector(mode: mode) }.join(', ')
|
|
133
250
|
when :selector
|
|
134
|
-
|
|
251
|
+
build_selector(mode)
|
|
135
252
|
when :simple_selector_sequence
|
|
136
|
-
children.map(
|
|
137
|
-
when :type_selector
|
|
138
|
-
|
|
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
|
-
"##{
|
|
257
|
+
"##{selector_identifier(mode)}"
|
|
143
258
|
when :class_selector
|
|
144
|
-
".#{
|
|
259
|
+
".#{selector_identifier(mode)}"
|
|
145
260
|
when :attribute_selector
|
|
146
|
-
build_attribute_selector
|
|
261
|
+
build_attribute_selector(mode)
|
|
147
262
|
when :pseudo_class
|
|
148
|
-
"
|
|
263
|
+
"#{selector_prefix(mode, ':')}#{selector_identifier(mode)}"
|
|
149
264
|
when :pseudo_element
|
|
150
|
-
"
|
|
265
|
+
"#{selector_prefix(mode, '::')}#{selector_identifier(mode)}"
|
|
151
266
|
when :pseudo_function
|
|
152
|
-
"
|
|
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 :
|
|
162
|
-
|
|
163
|
-
when :
|
|
164
|
-
|
|
165
|
-
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,
|
|
166
288
|
:prefixmatch_operator, :suffixmatch_operator, :substringmatch_operator
|
|
167
|
-
value
|
|
289
|
+
value.to_s
|
|
168
290
|
else
|
|
169
|
-
children.map(
|
|
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
|
-
|
|
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
|
-
|
|
300
|
+
ids.first
|
|
301
|
+
end
|
|
186
302
|
|
|
187
|
-
|
|
188
|
-
|
|
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
|
-
|
|
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
|
-
|
|
316
|
+
attribute_selector_nodes.map { |node| extract_attribute_info(node) }
|
|
317
|
+
end
|
|
219
318
|
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
319
|
+
def attribute_selectors
|
|
320
|
+
attribute_selector_nodes.map { |node| extract_attribute_node(node) }
|
|
321
|
+
end
|
|
223
322
|
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
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
|
-
|
|
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
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
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
|
-
|
|
240
|
-
|
|
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
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
281
|
-
|
|
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
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
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
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
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
|
|
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
|
|
555
|
+
attr_value = attribute_value_for(child, mode)
|
|
340
556
|
end
|
|
341
557
|
end
|
|
342
558
|
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
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
|