p_css 0.1.4 → 0.1.6
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 +57 -2
- data/lib/css/cascade.rb +14 -10
- data/lib/css/code_points.rb +1 -1
- data/lib/css/media_queries/evaluator.rb +3 -3
- data/lib/css/nesting.rb +3 -3
- data/lib/css/parser.rb +1 -1
- data/lib/css/selectors/matcher.rb +75 -32
- data/lib/css/selectors/serializer.rb +2 -2
- data/lib/css/selectors/specificity.rb +2 -2
- data/lib/css/serializer.rb +5 -5
- data/lib/css/token.rb +1 -1
- data/lib/css/version.rb +1 -1
- data/lib/css.rb +1 -1
- data/sig/css/cascade.rbs +1 -1
- data/sig/css/selectors.rbs +4 -1
- data/sig/css.rbs +6 -1
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: c46687ed2138f367e3d83b51065e7faaeea99190ba035f47f0a9ad9ae7fabcbb
|
|
4
|
+
data.tar.gz: 773fb3861bb5f5c18bf5027327592477349747d7dde550d93b640add292c5b4b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 161f1b01423d7389446bbc32bc2df68812f35423c2e8c4ab8b94493a02e2b7e1b032c2b94c6c60c1e0b11411117326ad68dda66d2ebe1c88a6ab0a13fe1aa30a
|
|
7
|
+
data.tar.gz: 71e99867725196832e2598d4b3e0ffb8e40f5aef62b36ce95fd2785a3eacf114113253206db2731764450d8e6fde5f3940bbef01b74286429e58da26d0fa6e73
|
data/README.md
CHANGED
|
@@ -185,8 +185,9 @@ CSS.matches?(active, 'ul > li:not(:first-child)') # => true
|
|
|
185
185
|
```
|
|
186
186
|
|
|
187
187
|
Stateful pseudo-classes (`:hover`, `:focus`, `:visited`, validity API states,
|
|
188
|
-
etc.)
|
|
189
|
-
|
|
188
|
+
etc.) return `false` by default — there's no UA in the loop. Pass a `state:`
|
|
189
|
+
Hash to opt in; see [Stateful pseudo-classes](#stateful-pseudo-classes)
|
|
190
|
+
below. `:has()` is not yet implemented (its argument is kept as opaque
|
|
190
191
|
component values).
|
|
191
192
|
|
|
192
193
|
### Nesting de-sugar
|
|
@@ -270,6 +271,60 @@ source order. Cascade layers, `@scope` proximity, and Shadow DOM
|
|
|
270
271
|
encapsulation are not modeled — `@layer` / `@supports` / `@scope` /
|
|
271
272
|
`@container` / `@starting-style` blocks are descended into unconditionally.
|
|
272
273
|
|
|
274
|
+
### Stateful pseudo-classes
|
|
275
|
+
|
|
276
|
+
`:hover`, `:focus`, `:focus-within`, `:focus-visible`, `:active`, `:visited`,
|
|
277
|
+
and `:target` return `false` from the matcher by default. Pass a `state:`
|
|
278
|
+
Hash to override:
|
|
279
|
+
|
|
280
|
+
```ruby
|
|
281
|
+
state = {
|
|
282
|
+
hover: Set[hovered_element], # match these and their ancestors
|
|
283
|
+
focus: Set[focused_element], # match only this element
|
|
284
|
+
'focus-within' => Set[el], # propagates to ancestors
|
|
285
|
+
active: true # match every element
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
CSS.matches?(element, ':hover', state: state)
|
|
289
|
+
cascade.resolve(element, state: state)
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
Values:
|
|
293
|
+
|
|
294
|
+
- `Set` or `Array` of elements — matches those elements (and, for
|
|
295
|
+
`:hover`, `:active`, `:focus-within`, their ancestors per Selectors §10)
|
|
296
|
+
- `true` — matches every element
|
|
297
|
+
- falsy / missing — default behavior; never matches
|
|
298
|
+
|
|
299
|
+
Symbol and String keys are both accepted. Hyphenated names (`focus-within`,
|
|
300
|
+
`focus-visible`) read more naturally as String keys.
|
|
301
|
+
|
|
302
|
+
#### Limits of stateful matching
|
|
303
|
+
|
|
304
|
+
The API gives you the primitives but not a policy. Two patterns are
|
|
305
|
+
inherently hard:
|
|
306
|
+
|
|
307
|
+
- **`hover: true` over-reveals.** Every `:hover`-gated rule matches every
|
|
308
|
+
element, so multiple dropdowns / popovers / menus all become "visible"
|
|
309
|
+
simultaneously. Useful for "is this element *potentially* visible
|
|
310
|
+
somehow?" but not for unique-match queries.
|
|
311
|
+
|
|
312
|
+
- **Peer-row reveal patterns are unsolvable without mouse position.**
|
|
313
|
+
Stylesheets like `.row:hover .icon-copy { display: block }` reveal one
|
|
314
|
+
icon per row when its row is hovered. Per-candidate evaluation (giving
|
|
315
|
+
each candidate its own ancestor chain in the hover Set) doesn't break
|
|
316
|
+
the symmetry — every candidate sees its own `.row` ancestor as hovered
|
|
317
|
+
and reports itself visible. Real browsers disambiguate via the actual
|
|
318
|
+
mouse position; a headless analyzer can't reproduce that without the
|
|
319
|
+
test explicitly recording which element it treats as hovered (e.g. via
|
|
320
|
+
Capybara's `element.hover`).
|
|
321
|
+
|
|
322
|
+
The recommendation for tools layered on top of p CSS: track explicit hover
|
|
323
|
+
actions and pass the corresponding Set; for queries that depend on
|
|
324
|
+
hover-based uniqueness without an explicit hover, treat them as fragile
|
|
325
|
+
and disambiguate by `text:` / `id:` / data attributes instead of relying
|
|
326
|
+
on stateful CSS.
|
|
327
|
+
|
|
273
328
|
### `urange`
|
|
274
329
|
|
|
275
330
|
```ruby
|
data/lib/css/cascade.rb
CHANGED
|
@@ -31,7 +31,11 @@ module CSS
|
|
|
31
31
|
end
|
|
32
32
|
|
|
33
33
|
# Returns Hash<String, Declaration> of winning declarations.
|
|
34
|
-
|
|
34
|
+
#
|
|
35
|
+
# `state:` opts into stateful-pseudo matching — see
|
|
36
|
+
# `Selectors::Matcher#matches?` for the shape. Defaults to the
|
|
37
|
+
# stateless behavior (`:hover`, `:focus`, etc. never match).
|
|
38
|
+
def resolve(element, inline_style: nil, state: nil)
|
|
35
39
|
cache = {}
|
|
36
40
|
candidates = collect_candidate_indexes(element, cache)
|
|
37
41
|
order = 0
|
|
@@ -39,7 +43,7 @@ module CSS
|
|
|
39
43
|
|
|
40
44
|
candidates.each do |idx|
|
|
41
45
|
entry = @entries[idx]
|
|
42
|
-
spec = best_matching_specificity(element, entry.selector_pairs, cache)
|
|
46
|
+
spec = best_matching_specificity(element, entry.selector_pairs, cache, state)
|
|
43
47
|
|
|
44
48
|
next if spec.nil?
|
|
45
49
|
|
|
@@ -85,11 +89,11 @@ module CSS
|
|
|
85
89
|
end
|
|
86
90
|
|
|
87
91
|
def register_qualified_rule(rule, media_chain, out)
|
|
88
|
-
return unless media_chain.all? { MediaQueries::Evaluator.evaluate(
|
|
92
|
+
return unless media_chain.all? { MediaQueries::Evaluator.evaluate(_1, @context) }
|
|
89
93
|
|
|
90
94
|
sl = Selectors::Parser.parse_selector_list(rule.prelude)
|
|
91
|
-
pairs = sl.selectors.map { [
|
|
92
|
-
decls = rule.block.items.select {
|
|
95
|
+
pairs = sl.selectors.map { [_1, Selectors::SpecificityCalculator.calculate(_1)] }
|
|
96
|
+
decls = rule.block.items.select { _1.is_a?(Nodes::Declaration) }
|
|
93
97
|
|
|
94
98
|
out << RuleEntry.new(selector_pairs: pairs, declarations: decls)
|
|
95
99
|
rescue ParseError
|
|
@@ -204,11 +208,11 @@ module CSS
|
|
|
204
208
|
out
|
|
205
209
|
end
|
|
206
210
|
|
|
207
|
-
def best_matching_specificity(element, selector_pairs, cache)
|
|
211
|
+
def best_matching_specificity(element, selector_pairs, cache, state)
|
|
208
212
|
best = nil
|
|
209
213
|
|
|
210
214
|
selector_pairs.each do |sel, spec|
|
|
211
|
-
next unless Selectors::Matcher.matches?(element, sel, cache: cache)
|
|
215
|
+
next unless Selectors::Matcher.matches?(element, sel, cache: cache, state: state)
|
|
212
216
|
|
|
213
217
|
best = spec if best.nil? || spec > best
|
|
214
218
|
end
|
|
@@ -255,9 +259,9 @@ module CSS
|
|
|
255
259
|
|
|
256
260
|
def inline_declarations(style)
|
|
257
261
|
case style
|
|
258
|
-
when String then CSS.parse_block_contents(style).items.select {
|
|
259
|
-
when Nodes::Block then style.items.select {
|
|
260
|
-
when Array then style.select {
|
|
262
|
+
when String then CSS.parse_block_contents(style).items.select { _1.is_a?(Nodes::Declaration) }
|
|
263
|
+
when Nodes::Block then style.items.select { _1.is_a?(Nodes::Declaration) }
|
|
264
|
+
when Array then style.select { _1.is_a?(Nodes::Declaration) }
|
|
261
265
|
else
|
|
262
266
|
raise ArgumentError, "cannot derive inline declarations from #{style.class}"
|
|
263
267
|
end
|
data/lib/css/code_points.rb
CHANGED
|
@@ -38,7 +38,7 @@ module CSS
|
|
|
38
38
|
PREFIX_OP = {min: :ge, max: :le}.freeze
|
|
39
39
|
|
|
40
40
|
def evaluate(query_list, context)
|
|
41
|
-
query_list.queries.any? { evaluate_query(
|
|
41
|
+
query_list.queries.any? { evaluate_query(_1, context) }
|
|
42
42
|
end
|
|
43
43
|
|
|
44
44
|
private
|
|
@@ -65,8 +65,8 @@ module CSS
|
|
|
65
65
|
def evaluate_condition(node, context)
|
|
66
66
|
case node
|
|
67
67
|
when MediaNot then !evaluate_condition(node.operand, context)
|
|
68
|
-
when MediaAnd then node.operands.all? { evaluate_condition(
|
|
69
|
-
when MediaOr then node.operands.any? { evaluate_condition(
|
|
68
|
+
when MediaAnd then node.operands.all? { evaluate_condition(_1, context) }
|
|
69
|
+
when MediaOr then node.operands.any? { evaluate_condition(_1, context) }
|
|
70
70
|
when MediaFeature then evaluate_feature(node, context)
|
|
71
71
|
when GeneralEnclosed then false
|
|
72
72
|
else false
|
data/lib/css/nesting.rb
CHANGED
|
@@ -12,7 +12,7 @@ module CSS
|
|
|
12
12
|
extend self
|
|
13
13
|
|
|
14
14
|
def desugar(stylesheet)
|
|
15
|
-
Nodes::Stylesheet.new(rules: stylesheet.rules.flat_map { desugar_top_level(
|
|
15
|
+
Nodes::Stylesheet.new(rules: stylesheet.rules.flat_map { desugar_top_level(_1) })
|
|
16
16
|
end
|
|
17
17
|
|
|
18
18
|
private
|
|
@@ -135,7 +135,7 @@ module CSS
|
|
|
135
135
|
|
|
136
136
|
def contains_nesting?(complex_selector)
|
|
137
137
|
complex_selector.compounds.any? {|c|
|
|
138
|
-
c.components.any? {
|
|
138
|
+
c.components.any? { _1.is_a?(Selectors::NestingSelector) }
|
|
139
139
|
}
|
|
140
140
|
end
|
|
141
141
|
|
|
@@ -159,7 +159,7 @@ module CSS
|
|
|
159
159
|
replacement = Selectors::PseudoClass.new(name: 'is', argument: parent_list)
|
|
160
160
|
|
|
161
161
|
Selectors::ComplexSelector.new(
|
|
162
|
-
compounds: own_complex.compounds.map { swap_components_in_compound(
|
|
162
|
+
compounds: own_complex.compounds.map { swap_components_in_compound(_1, replacement) },
|
|
163
163
|
combinators: own_complex.combinators
|
|
164
164
|
)
|
|
165
165
|
end
|
data/lib/css/parser.rb
CHANGED
|
@@ -16,8 +16,9 @@ module CSS
|
|
|
16
16
|
# protocol out of the box.
|
|
17
17
|
#
|
|
18
18
|
# Pseudo-classes that depend on user-agent state (`:hover`, `:focus`,
|
|
19
|
-
# `:visited`,
|
|
20
|
-
#
|
|
19
|
+
# `:visited`, etc.) return false by default; pass an explicit `state:`
|
|
20
|
+
# mapping to opt into stateful matching. Validity-API and viewport-
|
|
21
|
+
# only states (`:fullscreen`, `:valid`, …) are not exposed.
|
|
21
22
|
module Matcher
|
|
22
23
|
extend self
|
|
23
24
|
|
|
@@ -26,6 +27,16 @@ module CSS
|
|
|
26
27
|
LINK_TAGS = %w[a area link].freeze
|
|
27
28
|
RO_INPUT_TYPES = %w[hidden range color checkbox radio file submit image reset button].freeze
|
|
28
29
|
|
|
30
|
+
# User-agent state pseudos. The matcher returns `false` for these
|
|
31
|
+
# unless the caller passes a `state:` Hash describing which
|
|
32
|
+
# elements (or "all") should match.
|
|
33
|
+
STATEFUL_PSEUDOS = %w[hover focus focus-within focus-visible active visited target].to_set.freeze
|
|
34
|
+
|
|
35
|
+
# Per spec these states propagate up the ancestor chain — if a
|
|
36
|
+
# descendant is hovered/active/contains-focus, the ancestors
|
|
37
|
+
# share the state for selector-matching purposes.
|
|
38
|
+
PROPAGATING_STATEFUL_PSEUDOS = %w[hover active focus-within].to_set.freeze
|
|
39
|
+
|
|
29
40
|
# Per-element cache used to avoid recomputing tag / id / class set
|
|
30
41
|
# for every selector in a hot loop (e.g. `Cascade#resolve` against
|
|
31
42
|
# hundreds of rules). Keyed by `Object#object_id`; only valid for
|
|
@@ -34,16 +45,16 @@ module CSS
|
|
|
34
45
|
|
|
35
46
|
EMPTY_CLASS_SET = Set.new.freeze
|
|
36
47
|
|
|
37
|
-
def matches?(element, selector, cache: nil)
|
|
48
|
+
def matches?(element, selector, cache: nil, state: nil)
|
|
38
49
|
sel = selector.is_a?(String) ? Parser.parse_selector_list(selector) : selector
|
|
39
50
|
|
|
40
51
|
case sel
|
|
41
52
|
when SelectorList
|
|
42
|
-
sel.selectors.any? { match_complex(element,
|
|
53
|
+
sel.selectors.any? { match_complex(element, _1, cache, state) }
|
|
43
54
|
when ComplexSelector
|
|
44
|
-
match_complex(element, sel, cache)
|
|
55
|
+
match_complex(element, sel, cache, state)
|
|
45
56
|
when CompoundSelector
|
|
46
|
-
match_compound(element, sel, cache)
|
|
57
|
+
match_compound(element, sel, cache, state)
|
|
47
58
|
else
|
|
48
59
|
raise ArgumentError, "expected a selector node or string, got #{sel.class}"
|
|
49
60
|
end
|
|
@@ -54,32 +65,32 @@ module CSS
|
|
|
54
65
|
# Walks the complex selector right-to-left starting at the rightmost
|
|
55
66
|
# compound. Each combinator either succeeds against ancestors /
|
|
56
67
|
# siblings of the current candidate or fails the whole match.
|
|
57
|
-
def match_complex(element, complex, cache)
|
|
58
|
-
match_at(element, complex, complex.compounds.size - 1, cache)
|
|
68
|
+
def match_complex(element, complex, cache, state)
|
|
69
|
+
match_at(element, complex, complex.compounds.size - 1, cache, state)
|
|
59
70
|
end
|
|
60
71
|
|
|
61
|
-
def match_at(element, complex, index, cache)
|
|
72
|
+
def match_at(element, complex, index, cache, state)
|
|
62
73
|
return false if element.nil?
|
|
63
|
-
return false unless match_compound(element, complex.compounds[index], cache)
|
|
74
|
+
return false unless match_compound(element, complex.compounds[index], cache, state)
|
|
64
75
|
return true if index.zero?
|
|
65
76
|
|
|
66
77
|
prev = index - 1
|
|
67
78
|
|
|
68
79
|
case complex.combinators[prev]
|
|
69
|
-
when :descendant then walk_until_match(element, complex, prev, :parent_element,
|
|
70
|
-
when :child then match_at(parent_element(element), complex, prev, cache)
|
|
71
|
-
when :next_sibling then match_at(previous_element(element), complex, prev, cache)
|
|
72
|
-
when :subsequent_sibling then walk_until_match(element, complex, prev, :previous_element, cache)
|
|
80
|
+
when :descendant then walk_until_match(element, complex, prev, :parent_element, cache, state)
|
|
81
|
+
when :child then match_at(parent_element(element), complex, prev, cache, state)
|
|
82
|
+
when :next_sibling then match_at(previous_element(element), complex, prev, cache, state)
|
|
83
|
+
when :subsequent_sibling then walk_until_match(element, complex, prev, :previous_element, cache, state)
|
|
73
84
|
end
|
|
74
85
|
end
|
|
75
86
|
|
|
76
87
|
# Steps along the DOM via `direction` until a candidate matches the
|
|
77
88
|
# remaining complex selector or the chain runs out.
|
|
78
|
-
def walk_until_match(element, complex, index, direction, cache)
|
|
89
|
+
def walk_until_match(element, complex, index, direction, cache, state)
|
|
79
90
|
candidate = send(direction, element)
|
|
80
91
|
|
|
81
92
|
while candidate
|
|
82
|
-
return true if match_at(candidate, complex, index, cache)
|
|
93
|
+
return true if match_at(candidate, complex, index, cache, state)
|
|
83
94
|
|
|
84
95
|
candidate = send(direction, candidate)
|
|
85
96
|
end
|
|
@@ -87,18 +98,18 @@ module CSS
|
|
|
87
98
|
false
|
|
88
99
|
end
|
|
89
100
|
|
|
90
|
-
def match_compound(element, compound, cache)
|
|
91
|
-
compound.components.all? { match_simple(element,
|
|
101
|
+
def match_compound(element, compound, cache, state)
|
|
102
|
+
compound.components.all? { match_simple(element, _1, cache, state) }
|
|
92
103
|
end
|
|
93
104
|
|
|
94
|
-
def match_simple(element, simple, cache)
|
|
105
|
+
def match_simple(element, simple, cache, state)
|
|
95
106
|
case simple
|
|
96
107
|
when TypeSelector then tag_of(element, cache).casecmp?(simple.name)
|
|
97
108
|
when UniversalSelector then true
|
|
98
109
|
when IdSelector then id_of(element, cache) == simple.name
|
|
99
110
|
when ClassSelector then classes_of(element, cache).include?(simple.name)
|
|
100
111
|
when AttributeSelector then match_attribute(element, simple)
|
|
101
|
-
when PseudoClass then match_pseudo_class(element, simple, cache)
|
|
112
|
+
when PseudoClass then match_pseudo_class(element, simple, cache, state)
|
|
102
113
|
when PseudoElement then false
|
|
103
114
|
when NestingSelector then false
|
|
104
115
|
else false
|
|
@@ -172,10 +183,14 @@ module CSS
|
|
|
172
183
|
|
|
173
184
|
# Pseudo-class matching -------------------------------------------
|
|
174
185
|
|
|
175
|
-
def match_pseudo_class(element, pc, cache)
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
186
|
+
def match_pseudo_class(element, pc, cache, state)
|
|
187
|
+
name = pc.name.downcase
|
|
188
|
+
|
|
189
|
+
return match_stateful_pseudo?(name, element, state) if STATEFUL_PSEUDOS.include?(name)
|
|
190
|
+
|
|
191
|
+
case name
|
|
192
|
+
when 'is', 'where', 'matches' then match_selector_list_arg(element, pc.argument, cache, state)
|
|
193
|
+
when 'not' then negate_selector_list_arg(element, pc.argument, cache, state)
|
|
179
194
|
when 'has' then false
|
|
180
195
|
when 'root' then parent_element(element).nil?
|
|
181
196
|
when 'scope' then parent_element(element).nil?
|
|
@@ -206,12 +221,40 @@ module CSS
|
|
|
206
221
|
end
|
|
207
222
|
end
|
|
208
223
|
|
|
209
|
-
|
|
210
|
-
|
|
224
|
+
# `:hover` / `:active` / `:focus-within` propagate up the ancestor
|
|
225
|
+
# chain per Selectors §10 — the Set members are the *source* nodes
|
|
226
|
+
# (e.g. the deepest hovered element) and any of their ancestors
|
|
227
|
+
# also matches. Other stateful pseudos match only the explicit
|
|
228
|
+
# elements in the Set.
|
|
229
|
+
def match_stateful_pseudo?(name, element, state)
|
|
230
|
+
return false if state.nil?
|
|
231
|
+
|
|
232
|
+
value = state[name.to_sym] || state[name]
|
|
233
|
+
|
|
234
|
+
return false if value.nil? || value == false
|
|
235
|
+
return true if value == true
|
|
236
|
+
|
|
237
|
+
return value.include?(element) unless PROPAGATING_STATEFUL_PSEUDOS.include?(name)
|
|
238
|
+
|
|
239
|
+
value.each do |source|
|
|
240
|
+
cur = source
|
|
241
|
+
|
|
242
|
+
while cur
|
|
243
|
+
return true if cur == element
|
|
244
|
+
|
|
245
|
+
cur = parent_element(cur)
|
|
246
|
+
end
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
false
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
def match_selector_list_arg(element, arg, cache, state)
|
|
253
|
+
arg.is_a?(SelectorList) && matches?(element, arg, cache: cache, state: state)
|
|
211
254
|
end
|
|
212
255
|
|
|
213
|
-
def negate_selector_list_arg(element, arg, cache)
|
|
214
|
-
arg.is_a?(SelectorList) && !matches?(element, arg, cache: cache)
|
|
256
|
+
def negate_selector_list_arg(element, arg, cache, state)
|
|
257
|
+
arg.is_a?(SelectorList) && !matches?(element, arg, cache: cache, state: state)
|
|
215
258
|
end
|
|
216
259
|
|
|
217
260
|
def match_nth(element, anb, of_type:, from_end:)
|
|
@@ -238,10 +281,10 @@ module CSS
|
|
|
238
281
|
return nil if p.nil?
|
|
239
282
|
|
|
240
283
|
siblings = element_children(p)
|
|
241
|
-
siblings = siblings.select { tag(
|
|
284
|
+
siblings = siblings.select { tag(_1).casecmp?(tag(element)) } if of_type
|
|
242
285
|
siblings = siblings.reverse if from_end
|
|
243
286
|
|
|
244
|
-
idx = siblings.index { same_node?(
|
|
287
|
+
idx = siblings.index { same_node?(_1, element) }
|
|
245
288
|
idx && idx + 1
|
|
246
289
|
end
|
|
247
290
|
|
|
@@ -273,7 +316,7 @@ module CSS
|
|
|
273
316
|
end
|
|
274
317
|
|
|
275
318
|
def inside_first_legend?(element, fieldset)
|
|
276
|
-
first_legend = element_children(fieldset).find { tag(
|
|
319
|
+
first_legend = element_children(fieldset).find { tag(_1) == 'legend' }
|
|
277
320
|
|
|
278
321
|
return false if first_legend.nil?
|
|
279
322
|
|
|
@@ -384,7 +427,7 @@ module CSS
|
|
|
384
427
|
def ident_argument(argument)
|
|
385
428
|
return nil unless argument.is_a?(Array)
|
|
386
429
|
|
|
387
|
-
token = argument.find {
|
|
430
|
+
token = argument.find { _1.is_a?(Token) && (_1.type == :ident || _1.type == :string) }
|
|
388
431
|
token&.value
|
|
389
432
|
end
|
|
390
433
|
|
|
@@ -21,9 +21,9 @@ module CSS
|
|
|
21
21
|
|
|
22
22
|
def serialize(node)
|
|
23
23
|
case node
|
|
24
|
-
when SelectorList then node.selectors.map { serialize(
|
|
24
|
+
when SelectorList then node.selectors.map { serialize(_1) }.join(', ')
|
|
25
25
|
when ComplexSelector then serialize_complex(node)
|
|
26
|
-
when CompoundSelector then node.components.map { serialize(
|
|
26
|
+
when CompoundSelector then node.components.map { serialize(_1) }.join
|
|
27
27
|
when TypeSelector then Escape.ident(node.name)
|
|
28
28
|
when UniversalSelector then '*'
|
|
29
29
|
when NestingSelector then '&'
|
|
@@ -41,7 +41,7 @@ module CSS
|
|
|
41
41
|
|
|
42
42
|
def calculate(node)
|
|
43
43
|
case node
|
|
44
|
-
when SelectorList then node.selectors.map { calculate(
|
|
44
|
+
when SelectorList then node.selectors.map { calculate(_1) }.max || Specificity::ZERO
|
|
45
45
|
when ComplexSelector then sum(node.compounds)
|
|
46
46
|
when CompoundSelector then sum(node.components)
|
|
47
47
|
when IdSelector then Specificity.new(a: 1, b: 0, c: 0)
|
|
@@ -59,7 +59,7 @@ module CSS
|
|
|
59
59
|
private
|
|
60
60
|
|
|
61
61
|
def sum(items)
|
|
62
|
-
items.map { calculate(
|
|
62
|
+
items.map { calculate(_1) }.reduce(Specificity::ZERO, :+)
|
|
63
63
|
end
|
|
64
64
|
|
|
65
65
|
def specificity_of_pseudo_class(node)
|
data/lib/css/serializer.rb
CHANGED
|
@@ -21,7 +21,7 @@ module CSS
|
|
|
21
21
|
when Nodes::SimpleBlock then serialize_simple_block(node)
|
|
22
22
|
when Token then serialize_token(node)
|
|
23
23
|
when Selectors::Node then Selectors::Serializer.serialize(node)
|
|
24
|
-
when Array then node.map { serialize(
|
|
24
|
+
when Array then node.map { serialize(_1) }.join
|
|
25
25
|
else
|
|
26
26
|
raise ArgumentError, "cannot serialize #{node.class}"
|
|
27
27
|
end
|
|
@@ -30,7 +30,7 @@ module CSS
|
|
|
30
30
|
private
|
|
31
31
|
|
|
32
32
|
def serialize_stylesheet(ss)
|
|
33
|
-
ss.rules.map { serialize(
|
|
33
|
+
ss.rules.map { serialize(_1) }.join("\n")
|
|
34
34
|
end
|
|
35
35
|
|
|
36
36
|
def serialize_at_rule(rule)
|
|
@@ -49,7 +49,7 @@ module CSS
|
|
|
49
49
|
def serialize_block(block)
|
|
50
50
|
return '{}' if block.items.empty?
|
|
51
51
|
|
|
52
|
-
inner = block.items.map { serialize(
|
|
52
|
+
inner = block.items.map { serialize(_1) }.join("\n")
|
|
53
53
|
"{\n#{indent(inner)}\n}"
|
|
54
54
|
end
|
|
55
55
|
|
|
@@ -159,8 +159,8 @@ module CSS
|
|
|
159
159
|
def serialize_string(s) = Escape.string(s)
|
|
160
160
|
|
|
161
161
|
def indent(str)
|
|
162
|
-
str.lines.map { "#{INDENT}#{
|
|
163
|
-
|
|
162
|
+
str.lines.map { "#{INDENT}#{_1}" }.join.then {
|
|
163
|
+
_1.end_with?("\n") ? _1.chomp : _1
|
|
164
164
|
}
|
|
165
165
|
end
|
|
166
166
|
end
|
data/lib/css/token.rb
CHANGED
|
@@ -93,7 +93,7 @@ module CSS
|
|
|
93
93
|
private
|
|
94
94
|
|
|
95
95
|
def compute_position
|
|
96
|
-
idx = @newlines.bsearch_index {
|
|
96
|
+
idx = @newlines.bsearch_index { _1 >= @start_offset } || @newlines.size
|
|
97
97
|
prev_nl = idx.zero? ? -1 : @newlines[idx - 1]
|
|
98
98
|
|
|
99
99
|
Position.new(
|
data/lib/css/version.rb
CHANGED
data/lib/css.rb
CHANGED
|
@@ -50,7 +50,7 @@ module CSS
|
|
|
50
50
|
|
|
51
51
|
def specificity(selector) = Selectors::SpecificityCalculator.calculate(selector)
|
|
52
52
|
|
|
53
|
-
def matches?(element, selector) = Selectors::Matcher.matches?(element, selector)
|
|
53
|
+
def matches?(element, selector, state: nil) = Selectors::Matcher.matches?(element, selector, state: state)
|
|
54
54
|
|
|
55
55
|
def parse_media_query_list(input) = MediaQueries::Parser.parse(input)
|
|
56
56
|
|
data/sig/css/cascade.rbs
CHANGED
|
@@ -17,6 +17,6 @@ module CSS
|
|
|
17
17
|
# Returns Hash<String, Declaration> of winning declarations after
|
|
18
18
|
# !important > inline > stylesheet > specificity > source-order
|
|
19
19
|
# sorting.
|
|
20
|
-
def resolve: (untyped element, ?inline_style: inline_style?) -> Hash[String, Nodes::Declaration]
|
|
20
|
+
def resolve: (untyped element, ?inline_style: inline_style?, ?state: matcher_state?) -> Hash[String, Nodes::Declaration]
|
|
21
21
|
end
|
|
22
22
|
end
|
data/sig/css/selectors.rbs
CHANGED
|
@@ -151,7 +151,10 @@ module CSS
|
|
|
151
151
|
end
|
|
152
152
|
|
|
153
153
|
module Matcher
|
|
154
|
-
|
|
154
|
+
STATEFUL_PSEUDOS: Set[String]
|
|
155
|
+
PROPAGATING_STATEFUL_PSEUDOS: Set[String]
|
|
156
|
+
|
|
157
|
+
def self.matches?: (untyped element, untyped selector, ?cache: Hash[Integer, untyped], ?state: matcher_state?) -> bool
|
|
155
158
|
|
|
156
159
|
def self.tag_of: (untyped element, ?Hash[Integer, untyped]?) -> String
|
|
157
160
|
def self.id_of: (untyped element, ?Hash[Integer, untyped]?) -> String?
|
data/sig/css.rbs
CHANGED
|
@@ -68,7 +68,12 @@ module CSS
|
|
|
68
68
|
|
|
69
69
|
type selector = Selectors::SelectorList | Selectors::ComplexSelector | Selectors::CompoundSelector
|
|
70
70
|
|
|
71
|
-
|
|
71
|
+
# Per-pseudo state. Keys are pseudo-class names (Symbol or String);
|
|
72
|
+
# values are `true` (match every element), a `Set` / `Array` of the
|
|
73
|
+
# specific elements in that state, or falsy (no match).
|
|
74
|
+
type matcher_state = Hash[Symbol | String, bool | Set[untyped] | Array[untyped]]
|
|
75
|
+
|
|
76
|
+
def self.matches?: (untyped element, String | selector selector, ?cache: Hash[Integer, untyped], ?state: matcher_state?) -> bool
|
|
72
77
|
|
|
73
78
|
# Media queries (Media Queries 4)
|
|
74
79
|
# ---------------------------------------------------------------
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: p_css
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.6
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Keita Urashima
|
|
@@ -66,7 +66,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
66
66
|
requirements:
|
|
67
67
|
- - ">="
|
|
68
68
|
- !ruby/object:Gem::Version
|
|
69
|
-
version: '3.
|
|
69
|
+
version: '3.3'
|
|
70
70
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
71
71
|
requirements:
|
|
72
72
|
- - ">="
|