p_css 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/LICENSE.txt +21 -0
- data/README.md +302 -0
- data/lib/css/cascade.rb +168 -0
- data/lib/css/code_points.rb +36 -0
- data/lib/css/escape.rb +82 -0
- data/lib/css/media_queries/context.rb +60 -0
- data/lib/css/media_queries/evaluator.rb +157 -0
- data/lib/css/media_queries/nodes.rb +41 -0
- data/lib/css/media_queries/parser.rb +374 -0
- data/lib/css/media_queries.rb +9 -0
- data/lib/css/nesting.rb +229 -0
- data/lib/css/nodes.rb +42 -0
- data/lib/css/parser.rb +430 -0
- data/lib/css/selectors/anb_parser.rb +174 -0
- data/lib/css/selectors/matcher.rb +449 -0
- data/lib/css/selectors/nodes.rb +61 -0
- data/lib/css/selectors/parser.rb +395 -0
- data/lib/css/selectors/serializer.rb +102 -0
- data/lib/css/selectors/specificity.rb +81 -0
- data/lib/css/selectors.rb +11 -0
- data/lib/css/serializer.rb +167 -0
- data/lib/css/token.rb +78 -0
- data/lib/css/token_cursor.rb +49 -0
- data/lib/css/tokenizer.rb +441 -0
- data/lib/css/urange.rb +45 -0
- data/lib/css/version.rb +3 -0
- data/lib/css.rb +73 -0
- data/lib/p_css.rb +1 -0
- metadata +73 -0
|
@@ -0,0 +1,395 @@
|
|
|
1
|
+
module CSS
|
|
2
|
+
module Selectors
|
|
3
|
+
# Parser for CSS Selectors Level 4. Covers compound and complex
|
|
4
|
+
# selectors, the four standard combinators (descendant, child, next-
|
|
5
|
+
# sibling, subsequent-sibling), pseudo-classes / pseudo-elements
|
|
6
|
+
# (with recursive parsing of `:not/:is/:where/:has` and AnB parsing of
|
|
7
|
+
# `:nth-*`), attribute selectors with case-insensitive `i` / `s` flags,
|
|
8
|
+
# and the `&` nesting selector.
|
|
9
|
+
#
|
|
10
|
+
# Out of scope (intermediate plan): namespace prefixes, the column
|
|
11
|
+
# combinator `||`, and forgiving vs strict selector list distinctions.
|
|
12
|
+
class Parser
|
|
13
|
+
include CSS::TokenCursor
|
|
14
|
+
|
|
15
|
+
# `:has()` is intentionally excluded — it takes a *relative* selector
|
|
16
|
+
# list (each item may start with a combinator) which would require
|
|
17
|
+
# extending the ComplexSelector AST. Falls back to opaque component
|
|
18
|
+
# values for now.
|
|
19
|
+
SELECTOR_LIST_PSEUDOS = %w[is where not matches].freeze
|
|
20
|
+
ANB_PSEUDOS = %w[nth-child nth-last-child nth-of-type nth-last-of-type].freeze
|
|
21
|
+
|
|
22
|
+
ATTR_MATCHERS = {
|
|
23
|
+
'~' => :includes,
|
|
24
|
+
'|' => :dash,
|
|
25
|
+
'^' => :prefix,
|
|
26
|
+
'$' => :suffix,
|
|
27
|
+
'*' => :substring
|
|
28
|
+
}.freeze
|
|
29
|
+
|
|
30
|
+
class << self
|
|
31
|
+
def parse_selector_list(input)
|
|
32
|
+
new(tokens_from(input)).parse_selector_list_complete
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def parse_selector(input)
|
|
36
|
+
new(tokens_from(input)).parse_selector_complete
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
private
|
|
40
|
+
|
|
41
|
+
def tokens_from(input)
|
|
42
|
+
return Tokenizer.new(input).tokenize if input.is_a?(String)
|
|
43
|
+
|
|
44
|
+
flatten_tokens(input.to_a)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Selectors are normally parsed straight from a token stream, but a
|
|
48
|
+
# caller may pass a `prelude` from the main parser, which contains
|
|
49
|
+
# `Function` and `SimpleBlock` AST nodes in place of the raw paren
|
|
50
|
+
# / bracket tokens. Flatten those back into a token sequence the
|
|
51
|
+
# selector parser can step through.
|
|
52
|
+
def flatten_tokens(items)
|
|
53
|
+
out = []
|
|
54
|
+
|
|
55
|
+
items.each {|it|
|
|
56
|
+
case it
|
|
57
|
+
when Token
|
|
58
|
+
out << it
|
|
59
|
+
when Nodes::SimpleBlock
|
|
60
|
+
out << Token.new(BRACKET_TYPE_FOR_OPEN.fetch(it.open))
|
|
61
|
+
out.concat(flatten_tokens(it.value))
|
|
62
|
+
out << Token.new(BRACKET_CLOSE_TYPE_FOR_OPEN.fetch(it.open))
|
|
63
|
+
when Nodes::Function
|
|
64
|
+
out << Token.new(:function, it.name)
|
|
65
|
+
out.concat(flatten_tokens(it.value))
|
|
66
|
+
out << Token.new(:rparen)
|
|
67
|
+
else
|
|
68
|
+
raise ArgumentError, "cannot feed #{it.class} into selector parser"
|
|
69
|
+
end
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
out
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
BRACKET_TYPE_FOR_OPEN = BRACKET_OPEN_CHAR.invert.freeze
|
|
77
|
+
BRACKET_CLOSE_TYPE_FOR_OPEN = BRACKET_OPEN_CHAR.to_h {|type, ch| [ch, BRACKET_CLOSE_TYPE.fetch(type)] }.freeze
|
|
78
|
+
|
|
79
|
+
def initialize(tokens)
|
|
80
|
+
init_cursor(tokens)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def parse_selector_list_complete
|
|
84
|
+
list = parse_selector_list
|
|
85
|
+
|
|
86
|
+
skip_whitespace
|
|
87
|
+
|
|
88
|
+
parse_error!("trailing tokens after selector list: #{peek.type}") unless peek.type == :eof
|
|
89
|
+
|
|
90
|
+
list
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def parse_selector_complete
|
|
94
|
+
skip_whitespace
|
|
95
|
+
|
|
96
|
+
cs = parse_complex_selector
|
|
97
|
+
|
|
98
|
+
skip_whitespace
|
|
99
|
+
|
|
100
|
+
parse_error!("trailing tokens after selector: #{peek.type}") unless peek.type == :eof
|
|
101
|
+
|
|
102
|
+
cs
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# A comma-separated list of complex selectors, terminated by EOF or
|
|
106
|
+
# `)` (for use inside functional pseudos like `:is(...)`).
|
|
107
|
+
def parse_selector_list
|
|
108
|
+
skip_whitespace
|
|
109
|
+
|
|
110
|
+
parse_error!('empty selector list') if list_terminator?(peek)
|
|
111
|
+
|
|
112
|
+
selectors = [parse_complex_selector]
|
|
113
|
+
|
|
114
|
+
loop do
|
|
115
|
+
skip_whitespace
|
|
116
|
+
break unless peek.type == :comma
|
|
117
|
+
|
|
118
|
+
consume
|
|
119
|
+
skip_whitespace
|
|
120
|
+
selectors << parse_complex_selector
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
SelectorList.new(selectors:)
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def parse_complex_selector
|
|
127
|
+
skip_whitespace
|
|
128
|
+
|
|
129
|
+
compounds = [parse_compound_selector]
|
|
130
|
+
combinators = []
|
|
131
|
+
|
|
132
|
+
loop do
|
|
133
|
+
combo = try_consume_combinator
|
|
134
|
+
break if combo.nil?
|
|
135
|
+
|
|
136
|
+
compounds << parse_compound_selector
|
|
137
|
+
combinators << combo
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
ComplexSelector.new(compounds:, combinators:)
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
private
|
|
144
|
+
|
|
145
|
+
def consume_whitespace_returning_bool
|
|
146
|
+
consumed = false
|
|
147
|
+
|
|
148
|
+
while peek.type == :whitespace
|
|
149
|
+
consume
|
|
150
|
+
consumed = true
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
consumed
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def list_terminator?(t)
|
|
157
|
+
t.type == :eof || t.type == :rparen
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def try_consume_combinator
|
|
161
|
+
saved = @pos
|
|
162
|
+
had_ws = consume_whitespace_returning_bool
|
|
163
|
+
|
|
164
|
+
t = peek
|
|
165
|
+
|
|
166
|
+
if t.type == :delim && (combo = combinator_for_delim(t.value))
|
|
167
|
+
consume
|
|
168
|
+
skip_whitespace
|
|
169
|
+
return combo
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
if had_ws && compound_selector_ahead?(t)
|
|
173
|
+
return :descendant
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
@pos = saved
|
|
177
|
+
nil
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
def combinator_for_delim(value)
|
|
181
|
+
case value
|
|
182
|
+
when '>' then :child
|
|
183
|
+
when '+' then :next_sibling
|
|
184
|
+
when '~' then :subsequent_sibling
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
def compound_selector_ahead?(t)
|
|
189
|
+
case t.type
|
|
190
|
+
when :ident, :hash, :lbracket, :colon
|
|
191
|
+
true
|
|
192
|
+
when :delim
|
|
193
|
+
%w[* . &].include?(t.value)
|
|
194
|
+
else
|
|
195
|
+
false
|
|
196
|
+
end
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
def parse_compound_selector
|
|
200
|
+
components = []
|
|
201
|
+
|
|
202
|
+
if (head = try_consume_type_or_universal)
|
|
203
|
+
components << head
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
loop do
|
|
207
|
+
sub = try_consume_subclass_or_pseudo
|
|
208
|
+
break if sub.nil?
|
|
209
|
+
|
|
210
|
+
components << sub
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
parse_error!('expected a compound selector') if components.empty?
|
|
214
|
+
|
|
215
|
+
CompoundSelector.new(components:)
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
def try_consume_type_or_universal
|
|
219
|
+
case peek.type
|
|
220
|
+
when :ident
|
|
221
|
+
TypeSelector.new(name: consume.value)
|
|
222
|
+
when :delim
|
|
223
|
+
case peek.value
|
|
224
|
+
when '*' then consume; UniversalSelector.new
|
|
225
|
+
when '&' then consume; NestingSelector.new
|
|
226
|
+
end
|
|
227
|
+
end
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
def try_consume_subclass_or_pseudo
|
|
231
|
+
case peek.type
|
|
232
|
+
when :hash
|
|
233
|
+
parse_id_selector
|
|
234
|
+
when :lbracket
|
|
235
|
+
parse_attribute_selector
|
|
236
|
+
when :colon
|
|
237
|
+
parse_pseudo
|
|
238
|
+
when :delim
|
|
239
|
+
case peek.value
|
|
240
|
+
when '.' then parse_class_selector
|
|
241
|
+
when '&' then consume; NestingSelector.new
|
|
242
|
+
end
|
|
243
|
+
end
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
def parse_id_selector
|
|
247
|
+
t = consume
|
|
248
|
+
|
|
249
|
+
parse_error!('id hash must be a valid identifier') unless t.flag == :id
|
|
250
|
+
|
|
251
|
+
IdSelector.new(name: t.value)
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
def parse_class_selector
|
|
255
|
+
consume # the '.'
|
|
256
|
+
|
|
257
|
+
parse_error!("expected ident after '.', got #{peek.type}") unless peek.type == :ident
|
|
258
|
+
|
|
259
|
+
ClassSelector.new(name: consume.value)
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
def parse_attribute_selector
|
|
263
|
+
consume # [
|
|
264
|
+
skip_whitespace
|
|
265
|
+
|
|
266
|
+
parse_error!('expected attribute name') unless peek.type == :ident
|
|
267
|
+
|
|
268
|
+
name = consume.value
|
|
269
|
+
|
|
270
|
+
skip_whitespace
|
|
271
|
+
|
|
272
|
+
matcher, value = parse_attr_matcher_and_value
|
|
273
|
+
case_flag = parse_attr_case_flag
|
|
274
|
+
|
|
275
|
+
skip_whitespace
|
|
276
|
+
|
|
277
|
+
parse_error!("expected ']', got #{peek.type}") unless peek.type == :rbracket
|
|
278
|
+
|
|
279
|
+
consume
|
|
280
|
+
|
|
281
|
+
AttributeSelector.new(name:, matcher:, value:, case_flag:)
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
def parse_attr_matcher_and_value
|
|
285
|
+
return [nil, nil] if peek.type == :rbracket
|
|
286
|
+
|
|
287
|
+
matcher =
|
|
288
|
+
if peek.type == :delim && peek.value == '='
|
|
289
|
+
consume
|
|
290
|
+
:exact
|
|
291
|
+
elsif peek.type == :delim && (sym = ATTR_MATCHERS[peek.value])
|
|
292
|
+
consume
|
|
293
|
+
unless peek.type == :delim && peek.value == '='
|
|
294
|
+
parse_error!("expected '=' to complete attribute matcher")
|
|
295
|
+
end
|
|
296
|
+
consume
|
|
297
|
+
sym
|
|
298
|
+
else
|
|
299
|
+
parse_error!("invalid attribute matcher: #{peek.type}")
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
skip_whitespace
|
|
303
|
+
|
|
304
|
+
unless peek.type == :ident || peek.type == :string
|
|
305
|
+
parse_error!("expected attribute value, got #{peek.type}")
|
|
306
|
+
end
|
|
307
|
+
|
|
308
|
+
[matcher, consume.value]
|
|
309
|
+
end
|
|
310
|
+
|
|
311
|
+
def parse_attr_case_flag
|
|
312
|
+
skip_whitespace
|
|
313
|
+
|
|
314
|
+
return nil unless peek.type == :ident
|
|
315
|
+
|
|
316
|
+
v = peek.value.downcase
|
|
317
|
+
return nil unless v == 'i' || v == 's'
|
|
318
|
+
|
|
319
|
+
consume
|
|
320
|
+
v.to_sym
|
|
321
|
+
end
|
|
322
|
+
|
|
323
|
+
def parse_pseudo
|
|
324
|
+
consume # first colon
|
|
325
|
+
|
|
326
|
+
if peek.type == :colon
|
|
327
|
+
consume
|
|
328
|
+
parse_pseudo_body(element: true)
|
|
329
|
+
else
|
|
330
|
+
parse_pseudo_body(element: false)
|
|
331
|
+
end
|
|
332
|
+
end
|
|
333
|
+
|
|
334
|
+
def parse_pseudo_body(element:)
|
|
335
|
+
case peek.type
|
|
336
|
+
when :ident
|
|
337
|
+
name = consume.value
|
|
338
|
+
build_pseudo(element:, name:, argument: nil)
|
|
339
|
+
when :function
|
|
340
|
+
name = consume.value
|
|
341
|
+
arg = parse_pseudo_argument(name)
|
|
342
|
+
|
|
343
|
+
parse_error!("expected ')' to close :#{name}") unless peek.type == :rparen
|
|
344
|
+
|
|
345
|
+
consume
|
|
346
|
+
build_pseudo(element:, name:, argument: arg)
|
|
347
|
+
else
|
|
348
|
+
parse_error!("expected pseudo-#{element ? 'element' : 'class'} name, got #{peek.type}")
|
|
349
|
+
end
|
|
350
|
+
end
|
|
351
|
+
|
|
352
|
+
def build_pseudo(element:, name:, argument:)
|
|
353
|
+
element ? PseudoElement.new(name:, argument:) : PseudoClass.new(name:, argument:)
|
|
354
|
+
end
|
|
355
|
+
|
|
356
|
+
def parse_pseudo_argument(name)
|
|
357
|
+
n = name.downcase
|
|
358
|
+
|
|
359
|
+
if SELECTOR_LIST_PSEUDOS.include?(n)
|
|
360
|
+
parse_selector_list
|
|
361
|
+
elsif ANB_PSEUDOS.include?(n)
|
|
362
|
+
AnBParser.parse(collect_argument_tokens)
|
|
363
|
+
else
|
|
364
|
+
collect_argument_tokens
|
|
365
|
+
end
|
|
366
|
+
end
|
|
367
|
+
|
|
368
|
+
# Collects all tokens up to the closing `)` of the current functional
|
|
369
|
+
# context, balancing nested parens / functions.
|
|
370
|
+
def collect_argument_tokens
|
|
371
|
+
inner = []
|
|
372
|
+
depth = 0
|
|
373
|
+
|
|
374
|
+
loop do
|
|
375
|
+
case peek.type
|
|
376
|
+
when :eof
|
|
377
|
+
parse_error!('unexpected EOF in pseudo argument')
|
|
378
|
+
when :function, :lparen
|
|
379
|
+
depth += 1
|
|
380
|
+
inner << consume
|
|
381
|
+
when :rparen
|
|
382
|
+
break if depth.zero?
|
|
383
|
+
|
|
384
|
+
depth -= 1
|
|
385
|
+
inner << consume
|
|
386
|
+
else
|
|
387
|
+
inner << consume
|
|
388
|
+
end
|
|
389
|
+
end
|
|
390
|
+
|
|
391
|
+
inner
|
|
392
|
+
end
|
|
393
|
+
end
|
|
394
|
+
end
|
|
395
|
+
end
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
module CSS
|
|
2
|
+
module Selectors
|
|
3
|
+
module Serializer
|
|
4
|
+
extend self
|
|
5
|
+
|
|
6
|
+
COMBINATOR_GLUE = {
|
|
7
|
+
descendant: ' ',
|
|
8
|
+
child: ' > ',
|
|
9
|
+
next_sibling: ' + ',
|
|
10
|
+
subsequent_sibling: ' ~ '
|
|
11
|
+
}.freeze
|
|
12
|
+
|
|
13
|
+
ATTR_OPS = {
|
|
14
|
+
exact: '=',
|
|
15
|
+
includes: '~=',
|
|
16
|
+
dash: '|=',
|
|
17
|
+
prefix: '^=',
|
|
18
|
+
suffix: '$=',
|
|
19
|
+
substring: '*='
|
|
20
|
+
}.freeze
|
|
21
|
+
|
|
22
|
+
def serialize(node)
|
|
23
|
+
case node
|
|
24
|
+
when SelectorList then node.selectors.map { serialize(it) }.join(', ')
|
|
25
|
+
when ComplexSelector then serialize_complex(node)
|
|
26
|
+
when CompoundSelector then node.components.map { serialize(it) }.join
|
|
27
|
+
when TypeSelector then Escape.ident(node.name)
|
|
28
|
+
when UniversalSelector then '*'
|
|
29
|
+
when NestingSelector then '&'
|
|
30
|
+
when IdSelector then "##{Escape.ident(node.name)}"
|
|
31
|
+
when ClassSelector then ".#{Escape.ident(node.name)}"
|
|
32
|
+
when AttributeSelector then serialize_attribute(node)
|
|
33
|
+
when PseudoClass then serialize_pseudo(node, '')
|
|
34
|
+
when PseudoElement then serialize_pseudo(node, ':')
|
|
35
|
+
when AnB then serialize_anb(node)
|
|
36
|
+
else
|
|
37
|
+
raise ArgumentError, "cannot serialize selector node #{node.class}"
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
private
|
|
42
|
+
|
|
43
|
+
def serialize_complex(cs)
|
|
44
|
+
out = +serialize(cs.compounds[0])
|
|
45
|
+
|
|
46
|
+
cs.combinators.each_with_index {|combo, i|
|
|
47
|
+
out << COMBINATOR_GLUE.fetch(combo) << serialize(cs.compounds[i + 1])
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
out
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def serialize_attribute(attr)
|
|
54
|
+
out = +"[#{Escape.ident(attr.name)}"
|
|
55
|
+
|
|
56
|
+
if attr.matcher
|
|
57
|
+
out << ATTR_OPS.fetch(attr.matcher) << Escape.string(attr.value.to_s)
|
|
58
|
+
out << " #{attr.case_flag}" if attr.case_flag
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
out << ']'
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def serialize_pseudo(node, extra_colon)
|
|
65
|
+
head = ":#{extra_colon}#{Escape.ident(node.name)}"
|
|
66
|
+
|
|
67
|
+
return head if node.argument.nil?
|
|
68
|
+
|
|
69
|
+
"#{head}(#{serialize_argument(node.argument)})"
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def serialize_argument(arg)
|
|
73
|
+
case arg
|
|
74
|
+
when SelectorList then serialize(arg)
|
|
75
|
+
when AnB then serialize_anb(arg)
|
|
76
|
+
when Array then CSS::Serializer.serialize(arg)
|
|
77
|
+
else
|
|
78
|
+
raise ArgumentError, "unknown pseudo argument #{arg.class}"
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def serialize_anb(anb)
|
|
83
|
+
return 'even' if anb.step == 2 && anb.offset.zero?
|
|
84
|
+
return 'odd' if anb.step == 2 && anb.offset == 1
|
|
85
|
+
|
|
86
|
+
return anb.offset.to_s if anb.step.zero?
|
|
87
|
+
|
|
88
|
+
step_str =
|
|
89
|
+
case anb.step
|
|
90
|
+
when 1 then 'n'
|
|
91
|
+
when -1 then '-n'
|
|
92
|
+
else "#{anb.step}n"
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
return step_str if anb.offset.zero?
|
|
96
|
+
|
|
97
|
+
sign = anb.offset.positive? ? '+' : '-'
|
|
98
|
+
"#{step_str}#{sign}#{anb.offset.abs}"
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
module CSS
|
|
2
|
+
module Selectors
|
|
3
|
+
# Selectors §16 specificity tuple `(a, b, c)`.
|
|
4
|
+
# a = id selectors
|
|
5
|
+
# b = class / attribute / pseudo-class selectors
|
|
6
|
+
# c = type / pseudo-element selectors
|
|
7
|
+
Specificity = Data.define(:a, :b, :c) do
|
|
8
|
+
include Comparable
|
|
9
|
+
|
|
10
|
+
# Avoids the per-call allocation of two 3-element arrays — this
|
|
11
|
+
# comparison runs many times in the cascade-sort hot path.
|
|
12
|
+
def <=>(other)
|
|
13
|
+
return nil unless other.is_a?(Specificity)
|
|
14
|
+
|
|
15
|
+
d = a - other.a
|
|
16
|
+
return d unless d.zero?
|
|
17
|
+
|
|
18
|
+
d = b - other.b
|
|
19
|
+
return d unless d.zero?
|
|
20
|
+
|
|
21
|
+
c - other.c
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def +(other)
|
|
25
|
+
Specificity.new(a: a + other.a, b: b + other.b, c: c + other.c)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def to_s = "#{a},#{b},#{c}"
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
Specificity::ZERO = Specificity.new(a: 0, b: 0, c: 0).freeze
|
|
32
|
+
|
|
33
|
+
# Computes specificity for any selector AST node.
|
|
34
|
+
#
|
|
35
|
+
# Note on the nesting selector (`&`): without parent context its
|
|
36
|
+
# specificity is conservatively reported as zero. Callers wanting
|
|
37
|
+
# accurate cascade behavior should run `CSS.desugar(stylesheet)` first
|
|
38
|
+
# so `&` is replaced by the parent's compounds.
|
|
39
|
+
module SpecificityCalculator
|
|
40
|
+
extend self
|
|
41
|
+
|
|
42
|
+
def calculate(node)
|
|
43
|
+
case node
|
|
44
|
+
when SelectorList then node.selectors.map { calculate(it) }.max || Specificity::ZERO
|
|
45
|
+
when ComplexSelector then sum(node.compounds)
|
|
46
|
+
when CompoundSelector then sum(node.components)
|
|
47
|
+
when IdSelector then Specificity.new(a: 1, b: 0, c: 0)
|
|
48
|
+
when ClassSelector,
|
|
49
|
+
AttributeSelector then Specificity.new(a: 0, b: 1, c: 0)
|
|
50
|
+
when TypeSelector then Specificity.new(a: 0, b: 0, c: 1)
|
|
51
|
+
when PseudoElement then Specificity.new(a: 0, b: 0, c: 1)
|
|
52
|
+
when PseudoClass then specificity_of_pseudo_class(node)
|
|
53
|
+
when UniversalSelector,
|
|
54
|
+
NestingSelector then Specificity::ZERO
|
|
55
|
+
else Specificity::ZERO
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
private
|
|
60
|
+
|
|
61
|
+
def sum(items)
|
|
62
|
+
items.map { calculate(it) }.reduce(Specificity::ZERO, :+)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def specificity_of_pseudo_class(node)
|
|
66
|
+
case node.name.downcase
|
|
67
|
+
when 'where'
|
|
68
|
+
Specificity::ZERO
|
|
69
|
+
when 'is', 'not', 'has', 'matches'
|
|
70
|
+
if node.argument.is_a?(SelectorList)
|
|
71
|
+
calculate(node.argument)
|
|
72
|
+
else
|
|
73
|
+
Specificity.new(a: 0, b: 1, c: 0)
|
|
74
|
+
end
|
|
75
|
+
else
|
|
76
|
+
Specificity.new(a: 0, b: 1, c: 0)
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
require_relative 'selectors/nodes'
|
|
2
|
+
require_relative 'selectors/anb_parser'
|
|
3
|
+
require_relative 'selectors/parser'
|
|
4
|
+
require_relative 'selectors/serializer'
|
|
5
|
+
require_relative 'selectors/specificity'
|
|
6
|
+
require_relative 'selectors/matcher'
|
|
7
|
+
|
|
8
|
+
module CSS
|
|
9
|
+
module Selectors
|
|
10
|
+
end
|
|
11
|
+
end
|