p_css 0.1.5 → 0.1.7

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: 965047de25f5fe1719b2d2b4c59d2fa2d74550249e2ffa23d4175ae88dc09ba9
4
- data.tar.gz: ab8078d7209427b3f23cd774492fae69543bd8910b812a1a87fc7da38b8df89c
3
+ metadata.gz: 99fe89569a8cb686b5fa018e7403b55ba6603a46ccad4a8df4821135579b6ada
4
+ data.tar.gz: 8474073401ef004e365a6e294dcf35c92338ad6c4688168e999fccecac6a95e5
5
5
  SHA512:
6
- metadata.gz: '069d1c89de282155ee708b19aee7cc61226a68bd7ad662a869536b823bb99d58c6b8b1dae2567fa9f27d337fda392ea52f62592a718580aead69e1ded17cbf47'
7
- data.tar.gz: 2d0ec3fd003f145859790f3140ec808e5ba1d1273dc2e78e379d7ef5eec3dd44c9c031cd8e5db22f5b1b63b27d52f9973cc18f19b0f184ebd6a882d73b24ac36
6
+ metadata.gz: 7c79fcc55da6b57149893332f9d491912f66d04d8a2f6d6ffef9f57fb30872ecf22b00b4184730088fbcc92639bf7d55204c075147b78174f2f5e48d057f9ba8
7
+ data.tar.gz: efb733700697de31e950e90b35f10597d22975c3e0ae64ed8575474c0da175bd80bed92c39eedd3a3154560a0b7a604c79eeb3cb0ccec3b785429d7705918346
data/README.md CHANGED
@@ -61,7 +61,7 @@ Or:
61
61
  bundle add p_css
62
62
  ```
63
63
 
64
- Ruby 3.4+ is required. The matcher works against any object that quacks like
64
+ Ruby 3.3+ is required. The matcher works against any object that quacks like
65
65
  a DOM element (`Nokogiri::XML::Element` works out of the box); Nokogiri is not
66
66
  a hard dependency.
67
67
 
@@ -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
@@ -295,7 +350,7 @@ These are deliberate omissions; pull requests welcome:
295
350
 
296
351
  ## Compatibility
297
352
 
298
- Ruby 3.4+. Tested on the current MRI. No mandatory runtime dependencies.
353
+ Ruby 3.3+. Tested on the current MRI. No mandatory runtime dependencies.
299
354
 
300
355
  ## License
301
356
 
data/lib/css/cascade.rb CHANGED
@@ -89,11 +89,11 @@ module CSS
89
89
  end
90
90
 
91
91
  def register_qualified_rule(rule, media_chain, out)
92
- return unless media_chain.all? { MediaQueries::Evaluator.evaluate(it, @context) }
92
+ return unless media_chain.all? { MediaQueries::Evaluator.evaluate(_1, @context) }
93
93
 
94
94
  sl = Selectors::Parser.parse_selector_list(rule.prelude)
95
- pairs = sl.selectors.map { [it, Selectors::SpecificityCalculator.calculate(it)] }
96
- 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) }
97
97
 
98
98
  out << RuleEntry.new(selector_pairs: pairs, declarations: decls)
99
99
  rescue ParseError
@@ -259,9 +259,9 @@ module CSS
259
259
 
260
260
  def inline_declarations(style)
261
261
  case style
262
- when String then CSS.parse_block_contents(style).items.select { it.is_a?(Nodes::Declaration) }
263
- when Nodes::Block then style.items.select { it.is_a?(Nodes::Declaration) }
264
- 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) }
265
265
  else
266
266
  raise ArgumentError, "cannot derive inline declarations from #{style.class}"
267
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
@@ -207,8 +207,11 @@ module CSS
207
207
  AtRule.new(name:, prelude:, block:)
208
208
  end
209
209
 
210
+ # On EOF or a stop token (`}` while nested), the rule is dropped per
211
+ # §5.4.3 — but already-consumed prelude tokens are NOT put back. Rewinding
212
+ # would leave the caller's cursor at the same starting token and loop
213
+ # forever on input like `style="hidden"` (no `:` and no `{`).
210
214
  def consume_qualified_rule(nested:)
211
- saved = @pos
212
215
  prelude = []
213
216
 
214
217
  loop do
@@ -216,13 +219,9 @@ module CSS
216
219
 
217
220
  case t.type
218
221
  when :eof
219
- @pos = saved
220
222
  return nil
221
223
  when :rbrace
222
- if nested
223
- @pos = saved
224
- return nil
225
- end
224
+ return nil if nested
226
225
 
227
226
  prelude << consume
228
227
  when :semicolon
@@ -321,7 +320,7 @@ module CSS
321
320
  end
322
321
  end
323
322
 
324
- if value.any? { it.is_a?(SimpleBlock) && it.braced? }
323
+ if value.any? { _1.is_a?(SimpleBlock) && _1.braced? }
325
324
  @pos = saved
326
325
  return nil
327
326
  end
@@ -50,7 +50,7 @@ module CSS
50
50
 
51
51
  case sel
52
52
  when SelectorList
53
- sel.selectors.any? { match_complex(element, it, cache, state) }
53
+ sel.selectors.any? { match_complex(element, _1, cache, state) }
54
54
  when ComplexSelector
55
55
  match_complex(element, sel, cache, state)
56
56
  when CompoundSelector
@@ -99,7 +99,7 @@ module CSS
99
99
  end
100
100
 
101
101
  def match_compound(element, compound, cache, state)
102
- compound.components.all? { match_simple(element, it, cache, state) }
102
+ compound.components.all? { match_simple(element, _1, cache, state) }
103
103
  end
104
104
 
105
105
  def match_simple(element, simple, cache, state)
@@ -281,10 +281,10 @@ module CSS
281
281
  return nil if p.nil?
282
282
 
283
283
  siblings = element_children(p)
284
- siblings = siblings.select { tag(it).casecmp?(tag(element)) } if of_type
284
+ siblings = siblings.select { tag(_1).casecmp?(tag(element)) } if of_type
285
285
  siblings = siblings.reverse if from_end
286
286
 
287
- idx = siblings.index { same_node?(it, element) }
287
+ idx = siblings.index { same_node?(_1, element) }
288
288
  idx && idx + 1
289
289
  end
290
290
 
@@ -316,7 +316,7 @@ module CSS
316
316
  end
317
317
 
318
318
  def inside_first_legend?(element, fieldset)
319
- first_legend = element_children(fieldset).find { tag(it) == 'legend' }
319
+ first_legend = element_children(fieldset).find { tag(_1) == 'legend' }
320
320
 
321
321
  return false if first_legend.nil?
322
322
 
@@ -427,7 +427,7 @@ module CSS
427
427
  def ident_argument(argument)
428
428
  return nil unless argument.is_a?(Array)
429
429
 
430
- 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) }
431
431
  token&.value
432
432
  end
433
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.5'
2
+ VERSION = '0.1.7'
3
3
  end
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.5
4
+ version: 0.1.7
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
  - - ">="