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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 20afa2206ed855fdd796f19179d06a8dc8b76231c223560d59e664cb43ddf897
4
- data.tar.gz: e951f89d04ff6db6f68151ad05a414e4f0f7d05f2dfc48580b22ef31b6f949de
3
+ metadata.gz: c46687ed2138f367e3d83b51065e7faaeea99190ba035f47f0a9ad9ae7fabcbb
4
+ data.tar.gz: 773fb3861bb5f5c18bf5027327592477349747d7dde550d93b640add292c5b4b
5
5
  SHA512:
6
- metadata.gz: dc533dd2a146654d7a622b3206568168bea1e404b163ef6b24ae1c841ef4cd5ff3a4621bc6794f64178ffd5c87a0c4e6a46f70fd6c7999a52539091849ae2941
7
- data.tar.gz: 67d08837559466bc5713aa6a40d8e67030ff9389228527b4ed8b4bb5e1121965fa90a31e6cf5aae40167151e535ae83b9f6073068e72d495a5098559b331698d
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.) deliberately return `false` — this matcher is intended for stateless
189
- analysis. `:has()` is not yet implemented (its argument is kept as opaque
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
- def resolve(element, inline_style: nil)
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(it, @context) }
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 { [it, Selectors::SpecificityCalculator.calculate(it)] }
92
- decls = rule.block.items.select { it.is_a?(Nodes::Declaration) }
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 { it.is_a?(Nodes::Declaration) }
259
- when Nodes::Block then style.items.select { it.is_a?(Nodes::Declaration) }
260
- when Array then style.select { it.is_a?(Nodes::Declaration) }
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
@@ -14,7 +14,7 @@ module CSS
14
14
  def self.build_table(*ranges_or_ints)
15
15
  Array.new(128, false).tap {|a|
16
16
  ranges_or_ints.each {|r|
17
- if r.is_a?(Range) then r.each { a[it] = true }
17
+ if r.is_a?(Range) then r.each { a[_1] = true }
18
18
  else a[r] = true
19
19
  end
20
20
  }
@@ -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(it, context) }
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(it, context) }
69
- when MediaOr then node.operands.any? { evaluate_condition(it, context) }
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(it) })
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? { it.is_a?(Selectors::NestingSelector) }
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(it, replacement) },
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
@@ -321,7 +321,7 @@ module CSS
321
321
  end
322
322
  end
323
323
 
324
- if value.any? { it.is_a?(SimpleBlock) && it.braced? }
324
+ if value.any? { _1.is_a?(SimpleBlock) && _1.braced? }
325
325
  @pos = saved
326
326
  return nil
327
327
  end
@@ -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`, validity-API states, `:fullscreen`, etc.) always return
20
- # false; this matcher is intended for stateless analysis.
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, it, cache) }
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, cache)
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, it, cache) }
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
- case pc.name.downcase
177
- when 'is', 'where', 'matches' then match_selector_list_arg(element, pc.argument, cache)
178
- when 'not' then negate_selector_list_arg(element, pc.argument, cache)
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
- def match_selector_list_arg(element, arg, cache)
210
- arg.is_a?(SelectorList) && matches?(element, arg, cache: cache)
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(it).casecmp?(tag(element)) } if of_type
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?(it, element) }
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(it) == 'legend' }
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 { it.is_a?(Token) && (it.type == :ident || it.type == :string) }
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(it) }.join(', ')
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(it) }.join
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(it) }.max || Specificity::ZERO
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(it) }.reduce(Specificity::ZERO, :+)
62
+ items.map { calculate(_1) }.reduce(Specificity::ZERO, :+)
63
63
  end
64
64
 
65
65
  def specificity_of_pseudo_class(node)
@@ -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(it) }.join
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(it) }.join("\n")
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(it) }.join("\n")
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}#{it}" }.join.then {
163
- it.end_with?("\n") ? it.chomp : it
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 { it >= @start_offset } || @newlines.size
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
@@ -1,3 +1,3 @@
1
1
  module CSS
2
- VERSION = '0.1.4'
2
+ VERSION = '0.1.6'
3
3
  end
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
@@ -151,7 +151,10 @@ module CSS
151
151
  end
152
152
 
153
153
  module Matcher
154
- def self.matches?: (untyped element, untyped selector, ?cache: Hash[Integer, untyped]) -> bool
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
- def self.matches?: (untyped element, String | selector selector, ?cache: Hash[Integer, untyped]) -> bool
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
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.4'
69
+ version: '3.3'
70
70
  required_rubygems_version: !ruby/object:Gem::Requirement
71
71
  requirements:
72
72
  - - ">="