p_css 0.1.0 → 0.1.2

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: bc053cb635310340632520bdf6ef0771e1003b5d2f8582434078667cc9aeb7b4
4
- data.tar.gz: b5e3c26f959dfb92e86126aab069b293796e762fb5388baef6be7c98b7b7499e
3
+ metadata.gz: 772060eec5d726253913cd8be2daa6180429680d014c1752c9b26b15618e4ba8
4
+ data.tar.gz: edd5e5afc5871362dc21cca89d8eb6a5b085350022f79f2b8c6f07a032f07aaa
5
5
  SHA512:
6
- metadata.gz: 6803cd828b9d6b2ffeaf7c3c82e75345e7b1165dff0e93e4390ebf9eb4ea7a74bcd45453582562f44bcdb45cc614389c41e60bfc94ba2865421c1cc2ea9399b6
7
- data.tar.gz: e6acedb22e9a43e81014dfff4d65378d88eda70f515936fd9f0962778e2c3aa4786cf608c8713fc0d3985e10dfb6919d77fcfe61cdd85b1263603b290fcbadb2
6
+ metadata.gz: 7b7e331023830f09938bbd03a29f50c49b19b40169226a9c13866c1ce2436b0dcd71987fb183f403b7b98784c68c8e8426e6043dd3757ebe5343e37862e2c228
7
+ data.tar.gz: c06594e2e861c77e56cc32dcb1980b3fba8bfe0175fa1f108e4b0d02bef929efd59274e5c53f04ecea8245b47c583f41eb8a4396a5689052a569af54e8026f5a
data/lib/css/cascade.rb CHANGED
@@ -8,9 +8,11 @@ module CSS
8
8
  # - cascade sort: `!important` > origin / inline > specificity > source order
9
9
  #
10
10
  # The Stylesheet is compiled once on construction (selectors are
11
- # pre-parsed, specificities pre-computed, and `@media` chains are
12
- # evaluated against the supplied context up-front so non-matching rules
13
- # are dropped). `resolve(element)` is then cheap to call per node.
11
+ # pre-parsed, specificities pre-computed, `@media` chains are
12
+ # evaluated against the supplied context up-front, and rules are
13
+ # indexed by the rightmost compound's strongest anchor id > class >
14
+ # tag > universal). `resolve(element)` then visits only the rules whose
15
+ # anchor could match the element.
14
16
  #
15
17
  # Cascade layers, `@scope` proximity, and Shadow DOM encapsulation are
16
18
  # not modeled — `@layer`, `@supports`, `@container`, `@scope`, and
@@ -25,15 +27,20 @@ module CSS
25
27
  def initialize(stylesheet, context: MediaQueries::Context.default)
26
28
  @context = context
27
29
  @entries = compile(stylesheet)
30
+ @index = build_index(@entries)
28
31
  end
29
32
 
30
33
  # Returns Hash<String, Declaration> of winning declarations.
31
34
  def resolve(element, inline_style: nil)
32
- order = 0
33
- matches = []
35
+ cache = {}
36
+ candidates = collect_candidate_indexes(element, cache)
37
+ order = 0
38
+ matches = []
39
+
40
+ candidates.each do |idx|
41
+ entry = @entries[idx]
42
+ spec = best_matching_specificity(element, entry.selector_pairs, cache)
34
43
 
35
- @entries.each do |entry|
36
- spec = best_matching_specificity(element, entry.selector_pairs)
37
44
  next if spec.nil?
38
45
 
39
46
  entry.declarations.each do |decl|
@@ -54,6 +61,9 @@ module CSS
54
61
 
55
62
  private
56
63
 
64
+ # Compile
65
+ # ----------------------------------------------------------------
66
+
57
67
  def compile(stylesheet)
58
68
  out = []
59
69
  walk(stylesheet.rules, [], out)
@@ -77,7 +87,7 @@ module CSS
77
87
  def register_qualified_rule(rule, media_chain, out)
78
88
  return unless media_chain.all? { MediaQueries::Evaluator.evaluate(it, @context) }
79
89
 
80
- sl = Selectors::Parser.parse_selector_list(rule.prelude)
90
+ sl = Selectors::Parser.parse_selector_list(rule.prelude)
81
91
  pairs = sl.selectors.map { [it, Selectors::SpecificityCalculator.calculate(it)] }
82
92
  decls = rule.block.items.select { it.is_a?(Nodes::Declaration) }
83
93
 
@@ -102,11 +112,103 @@ module CSS
102
112
  # remain unaffected.
103
113
  end
104
114
 
105
- def best_matching_specificity(element, selector_pairs)
115
+ # Index
116
+ # ----------------------------------------------------------------
117
+
118
+ Index = Data.define(:by_id, :by_class, :by_tag, :universal)
119
+ AnchorKey = Data.define(:kind, :name)
120
+
121
+ def build_index(entries)
122
+ by_id = {}
123
+ by_class = {}
124
+ by_tag = {}
125
+ universal = []
126
+
127
+ entries.each_with_index do |entry, idx|
128
+ seen = Set.new
129
+
130
+ entry.selector_pairs.each do |sel, _spec|
131
+ key = anchor_key(sel)
132
+
133
+ next if seen.include?(key)
134
+
135
+ seen << key
136
+
137
+ case key.kind
138
+ when :id then (by_id[key.name] ||= []) << idx
139
+ when :class then (by_class[key.name] ||= []) << idx
140
+ when :tag then (by_tag[key.name] ||= []) << idx
141
+ when :universal then universal << idx
142
+ end
143
+ end
144
+ end
145
+
146
+ Index.new(
147
+ by_id: by_id.freeze,
148
+ by_class: by_class.freeze,
149
+ by_tag: by_tag.freeze,
150
+ universal: universal.freeze
151
+ )
152
+ end
153
+
154
+ # Picks the strongest anchor in the rightmost compound: id > class >
155
+ # tag > universal. Compounds whose only simple selectors are pseudos
156
+ # (e.g. `:hover`) or attribute matchers fall through to universal —
157
+ # they will be tested against every element, but real-world
158
+ # stylesheets rarely have many such rules.
159
+ def anchor_key(complex_selector)
160
+ class_name = nil
161
+ tag_name = nil
162
+
163
+ complex_selector.compounds.last.components.each do |c|
164
+ case c
165
+ when Selectors::IdSelector then return AnchorKey.new(kind: :id, name: c.name)
166
+ when Selectors::ClassSelector then class_name ||= c.name
167
+ when Selectors::TypeSelector then tag_name ||= c.name.downcase
168
+ end
169
+ end
170
+
171
+ return AnchorKey.new(kind: :class, name: class_name) if class_name
172
+ return AnchorKey.new(kind: :tag, name: tag_name) if tag_name
173
+
174
+ AnchorKey.new(kind: :universal, name: nil)
175
+ end
176
+
177
+ # Resolve helpers
178
+ # ----------------------------------------------------------------
179
+
180
+ # Buckets are appended in source-order at compile time, so each is
181
+ # already sorted ascending and unique. Concatenating them and using
182
+ # `sort! + uniq!` is cheaper than going through a `Set`: integers
183
+ # sort in C, and `uniq!` on a sorted array only removes adjacent
184
+ # duplicates.
185
+ def collect_candidate_indexes(element, cache)
186
+ out = []
187
+
188
+ el_id = Selectors::Matcher.id_of(element, cache)
189
+ bucket = @index.by_id[el_id] if el_id
190
+
191
+ out.concat(bucket) if bucket
192
+
193
+ Selectors::Matcher.classes_of(element, cache).each do |cls|
194
+ bucket = @index.by_class[cls]
195
+ out.concat(bucket) if bucket
196
+ end
197
+
198
+ bucket = @index.by_tag[Selectors::Matcher.tag_of(element, cache)]
199
+ out.concat(bucket) if bucket
200
+
201
+ out.concat(@index.universal)
202
+ out.sort!
203
+ out.uniq!
204
+ out
205
+ end
206
+
207
+ def best_matching_specificity(element, selector_pairs, cache)
106
208
  best = nil
107
209
 
108
210
  selector_pairs.each do |sel, spec|
109
- next unless Selectors::Matcher.matches?(element, sel)
211
+ next unless Selectors::Matcher.matches?(element, sel, cache: cache)
110
212
 
111
213
  best = spec if best.nil? || spec > best
112
214
  end
@@ -114,24 +216,20 @@ module CSS
114
216
  best
115
217
  end
116
218
 
117
- # Single-pass: keep the running winner per property name. Cheaper than
118
- # group_by + max_by, and more importantly avoids allocating a fresh
119
- # comparison key per declaration.
219
+ # Single-pass running max per property name. Cheaper than group_by +
220
+ # max_by, and avoids allocating a fresh comparison key per
221
+ # declaration.
120
222
  def pick_winners(matches)
121
- winners = {}
122
- winner_matches = {}
223
+ best = {}
123
224
 
124
225
  matches.each do |m|
125
226
  name = m.declaration.name
126
- incumbent = winner_matches[name]
227
+ incumbent = best[name]
127
228
 
128
- if incumbent.nil? || better?(m, incumbent)
129
- winners[name] = m.declaration
130
- winner_matches[name] = m
131
- end
229
+ best[name] = m if incumbent.nil? || better?(m, incumbent)
132
230
  end
133
231
 
134
- winners
232
+ best.transform_values(&:declaration)
135
233
  end
136
234
 
137
235
  # `m` outranks `incumbent` when its priority class is higher, or — at
@@ -26,16 +26,24 @@ module CSS
26
26
  LINK_TAGS = %w[a area link].freeze
27
27
  RO_INPUT_TYPES = %w[hidden range color checkbox radio file submit image reset button].freeze
28
28
 
29
- def matches?(element, selector)
29
+ # Per-element cache used to avoid recomputing tag / id / class set
30
+ # for every selector in a hot loop (e.g. `Cascade#resolve` against
31
+ # hundreds of rules). Keyed by `Object#object_id`; only valid for
32
+ # the duration of a single matcher invocation.
33
+ Context = Data.define(:tag, :id, :classes)
34
+
35
+ EMPTY_CLASS_SET = Set.new.freeze
36
+
37
+ def matches?(element, selector, cache: nil)
30
38
  sel = selector.is_a?(String) ? Parser.parse_selector_list(selector) : selector
31
39
 
32
40
  case sel
33
41
  when SelectorList
34
- sel.selectors.any? { match_complex(element, it) }
42
+ sel.selectors.any? { match_complex(element, it, cache) }
35
43
  when ComplexSelector
36
- match_complex(element, sel)
44
+ match_complex(element, sel, cache)
37
45
  when CompoundSelector
38
- match_compound(element, sel)
46
+ match_compound(element, sel, cache)
39
47
  else
40
48
  raise ArgumentError, "expected a selector node or string, got #{sel.class}"
41
49
  end
@@ -46,32 +54,32 @@ module CSS
46
54
  # Walks the complex selector right-to-left starting at the rightmost
47
55
  # compound. Each combinator either succeeds against ancestors /
48
56
  # siblings of the current candidate or fails the whole match.
49
- def match_complex(element, complex)
50
- match_at(element, complex, complex.compounds.size - 1)
57
+ def match_complex(element, complex, cache)
58
+ match_at(element, complex, complex.compounds.size - 1, cache)
51
59
  end
52
60
 
53
- def match_at(element, complex, index)
61
+ def match_at(element, complex, index, cache)
54
62
  return false if element.nil?
55
- return false unless match_compound(element, complex.compounds[index])
63
+ return false unless match_compound(element, complex.compounds[index], cache)
56
64
  return true if index.zero?
57
65
 
58
66
  prev = index - 1
59
67
 
60
68
  case complex.combinators[prev]
61
- when :descendant then walk_until_match(element, complex, prev, :parent_element)
62
- when :child then match_at(parent_element(element), complex, prev)
63
- when :next_sibling then match_at(previous_element(element), complex, prev)
64
- when :subsequent_sibling then walk_until_match(element, complex, prev, :previous_element)
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)
65
73
  end
66
74
  end
67
75
 
68
76
  # Steps along the DOM via `direction` until a candidate matches the
69
77
  # remaining complex selector or the chain runs out.
70
- def walk_until_match(element, complex, index, direction)
78
+ def walk_until_match(element, complex, index, direction, cache)
71
79
  candidate = send(direction, element)
72
80
 
73
81
  while candidate
74
- return true if match_at(candidate, complex, index)
82
+ return true if match_at(candidate, complex, index, cache)
75
83
 
76
84
  candidate = send(direction, candidate)
77
85
  end
@@ -79,24 +87,63 @@ module CSS
79
87
  false
80
88
  end
81
89
 
82
- def match_compound(element, compound)
83
- compound.components.all? { match_simple(element, it) }
90
+ def match_compound(element, compound, cache)
91
+ compound.components.all? { match_simple(element, it, cache) }
84
92
  end
85
93
 
86
- def match_simple(element, simple)
94
+ def match_simple(element, simple, cache)
87
95
  case simple
88
- when TypeSelector then tag(element).casecmp?(simple.name)
96
+ when TypeSelector then tag_of(element, cache).casecmp?(simple.name)
89
97
  when UniversalSelector then true
90
- when IdSelector then attr(element, 'id') == simple.name
91
- when ClassSelector then class_list(element).include?(simple.name)
98
+ when IdSelector then id_of(element, cache) == simple.name
99
+ when ClassSelector then classes_of(element, cache).include?(simple.name)
92
100
  when AttributeSelector then match_attribute(element, simple)
93
- when PseudoClass then match_pseudo_class(element, simple)
101
+ when PseudoClass then match_pseudo_class(element, simple, cache)
94
102
  when PseudoElement then false
95
103
  when NestingSelector then false
96
104
  else false
97
105
  end
98
106
  end
99
107
 
108
+ # Public — used by `Cascade` for both rule indexing and matching;
109
+ # callers can share a `cache` Hash with `matches?(cache: cache)`
110
+ # so each element pays for its tag / id / class set at most once.
111
+ public
112
+
113
+ def tag_of(element, cache = nil)
114
+ ctx = context_for(element, cache)
115
+ ctx ? ctx.tag : tag(element)
116
+ end
117
+
118
+ def id_of(element, cache = nil)
119
+ ctx = context_for(element, cache)
120
+ ctx ? ctx.id : attr(element, 'id')
121
+ end
122
+
123
+ def classes_of(element, cache = nil)
124
+ ctx = context_for(element, cache)
125
+ ctx ? ctx.classes : build_class_set(element)
126
+ end
127
+
128
+ private
129
+
130
+ def context_for(element, cache)
131
+ return nil if cache.nil?
132
+
133
+ cache[element.object_id] ||= Context.new(
134
+ tag: tag(element),
135
+ id: attr(element, 'id'),
136
+ classes: build_class_set(element)
137
+ )
138
+ end
139
+
140
+ def build_class_set(element)
141
+ v = attr(element, 'class')
142
+ return EMPTY_CLASS_SET if v.nil? || v.empty?
143
+
144
+ v.to_s.split(/\s+/).to_set
145
+ end
146
+
100
147
  # Attribute matching ----------------------------------------------
101
148
 
102
149
  def match_attribute(element, attr_sel)
@@ -125,10 +172,10 @@ module CSS
125
172
 
126
173
  # Pseudo-class matching -------------------------------------------
127
174
 
128
- def match_pseudo_class(element, pc)
175
+ def match_pseudo_class(element, pc, cache)
129
176
  case pc.name.downcase
130
- when 'is', 'where', 'matches' then match_selector_list_arg(element, pc.argument)
131
- when 'not' then negate_selector_list_arg(element, pc.argument)
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)
132
179
  when 'has' then false
133
180
  when 'root' then parent_element(element).nil?
134
181
  when 'scope' then parent_element(element).nil?
@@ -159,12 +206,12 @@ module CSS
159
206
  end
160
207
  end
161
208
 
162
- def match_selector_list_arg(element, arg)
163
- arg.is_a?(SelectorList) && matches?(element, arg)
209
+ def match_selector_list_arg(element, arg, cache)
210
+ arg.is_a?(SelectorList) && matches?(element, arg, cache: cache)
164
211
  end
165
212
 
166
- def negate_selector_list_arg(element, arg)
167
- arg.is_a?(SelectorList) && !matches?(element, arg)
213
+ def negate_selector_list_arg(element, arg, cache)
214
+ arg.is_a?(SelectorList) && !matches?(element, arg, cache: cache)
168
215
  end
169
216
 
170
217
  def match_nth(element, anb, of_type:, from_end:)
@@ -378,10 +425,6 @@ module CSS
378
425
  element[lower]
379
426
  end
380
427
 
381
- def class_list(element)
382
- attr(element, 'class').to_s.split(/\s+/)
383
- end
384
-
385
428
  def parent_element(element)
386
429
  p = element.respond_to?(:parent) ? element.parent : nil
387
430
 
data/lib/css/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module CSS
2
- VERSION = '0.1.0'
2
+ VERSION = '0.1.2'
3
3
  end
@@ -0,0 +1,22 @@
1
+ module CSS
2
+ # See `CSS.cascade` for the typical entry point.
3
+ class Cascade
4
+ type inline_style = String | Nodes::Block | Array[Nodes::Declaration]
5
+
6
+ class Match < Data
7
+ attr_reader declaration: Nodes::Declaration
8
+ attr_reader specificity: Selectors::Specificity
9
+ attr_reader inline: bool
10
+ attr_reader order: Integer
11
+
12
+ def self.new: (declaration: Nodes::Declaration, specificity: Selectors::Specificity, inline: bool, order: Integer) -> Match
13
+ end
14
+
15
+ def initialize: (Nodes::Stylesheet stylesheet, ?context: MediaQueries::Context) -> void
16
+
17
+ # Returns Hash<String, Declaration> of winning declarations after
18
+ # !important > inline > stylesheet > specificity > source-order
19
+ # sorting.
20
+ def resolve: (untyped element, ?inline_style: inline_style?) -> Hash[String, Nodes::Declaration]
21
+ end
22
+ end
@@ -0,0 +1,107 @@
1
+ module CSS
2
+ module MediaQueries
3
+ module Node
4
+ end
5
+
6
+ type modifier = :not | :only
7
+
8
+ type comparison_op = :eq | :lt | :le | :gt | :ge
9
+
10
+ type media_condition = MediaNot | MediaAnd | MediaOr | MediaFeature | GeneralEnclosed
11
+
12
+ type feature_value = Token | Ratio
13
+
14
+ class MediaQueryList < Data
15
+ include Node
16
+
17
+ attr_reader queries: Array[MediaQuery]
18
+
19
+ def self.new: (queries: Array[MediaQuery]) -> MediaQueryList
20
+ end
21
+
22
+ class MediaQuery < Data
23
+ include Node
24
+
25
+ attr_reader modifier: modifier?
26
+ attr_reader type: String?
27
+ attr_reader condition: media_condition?
28
+
29
+ def self.new: (modifier: modifier?, type: String?, condition: media_condition?) -> MediaQuery
30
+ end
31
+
32
+ class MediaNot < Data
33
+ include Node
34
+
35
+ attr_reader operand: media_condition
36
+
37
+ def self.new: (operand: media_condition) -> MediaNot
38
+ end
39
+
40
+ class MediaAnd < Data
41
+ include Node
42
+
43
+ attr_reader operands: Array[media_condition]
44
+
45
+ def self.new: (operands: Array[media_condition]) -> MediaAnd
46
+ end
47
+
48
+ class MediaOr < Data
49
+ include Node
50
+
51
+ attr_reader operands: Array[media_condition]
52
+
53
+ def self.new: (operands: Array[media_condition]) -> MediaOr
54
+ end
55
+
56
+ # `op` is `nil` for boolean form (`(color)`), `:eq` for plain
57
+ # (`(min-width: 600px)`) and explicit `=`, otherwise a comparison.
58
+ # `value` mirrors that — `nil` for boolean form.
59
+ class MediaFeature < Data
60
+ include Node
61
+
62
+ attr_reader name: String
63
+ attr_reader op: comparison_op?
64
+ attr_reader value: feature_value?
65
+
66
+ def self.new: (name: String, op: comparison_op?, value: feature_value?) -> MediaFeature
67
+ end
68
+
69
+ class GeneralEnclosed < Data
70
+ include Node
71
+
72
+ attr_reader tokens: Array[component_value]
73
+
74
+ def self.new: (tokens: Array[component_value]) -> GeneralEnclosed
75
+ end
76
+
77
+ class Ratio < Data
78
+ include Node
79
+
80
+ attr_reader numerator: Numeric
81
+ attr_reader denominator: Numeric
82
+
83
+ def self.new: (numerator: Numeric, denominator: Numeric) -> Ratio
84
+
85
+ def to_f: () -> Float
86
+ end
87
+
88
+ # User-agent context against which a `MediaQueryList` is evaluated.
89
+ # `features` is keyed by feature name (no `min-`/`max-` prefix);
90
+ # values follow Media Queries 4 conventions (lengths in CSS px,
91
+ # resolution in `dppx`, identifiers as Strings, booleans as 1/0).
92
+ class Context < Data
93
+ DEFAULTS: Hash[String, untyped]
94
+
95
+ attr_reader features: Hash[String, untyped]
96
+
97
+ def self.new: (features: Hash[String, untyped]) -> Context
98
+
99
+ # `Context.default('width' => 1200, 'prefers-color-scheme' => 'dark')`
100
+ def self.default: (**untyped overrides) -> Context
101
+
102
+ def []: ((String | Symbol) name) -> untyped
103
+ def media_type: () -> String
104
+ def with: (**untyped overrides) -> Context
105
+ end
106
+ end
107
+ end
data/sig/css/nodes.rbs ADDED
@@ -0,0 +1,76 @@
1
+ module CSS
2
+ module Nodes
3
+ # `Token` here covers preserved comments when the stylesheet was
4
+ # parsed with `preserve_comments: true`.
5
+ type rule = AtRule | QualifiedRule | Token
6
+
7
+ type block_item = Declaration | QualifiedRule | AtRule | Token
8
+
9
+ class Stylesheet < Data
10
+ attr_reader rules: Array[rule]
11
+
12
+ def self.new: (rules: Array[rule]) -> Stylesheet
13
+ end
14
+
15
+ # `prelude` is the list of component values before `{` or `;`;
16
+ # `block` is `nil` for at-rules ending with `;` (e.g. `@charset "UTF-8";`).
17
+ class AtRule < Data
18
+ attr_reader name: String
19
+ attr_reader prelude: Array[component_value]
20
+ attr_reader block: Block?
21
+
22
+ def self.new: (name: String, prelude: Array[component_value], block: Block?) -> AtRule
23
+ end
24
+
25
+ class QualifiedRule < Data
26
+ attr_reader prelude: Array[component_value]
27
+ attr_reader block: Block
28
+
29
+ def self.new: (prelude: Array[component_value], block: Block) -> QualifiedRule
30
+ end
31
+
32
+ class Block < Data
33
+ attr_reader items: Array[block_item]
34
+
35
+ def self.new: (items: Array[block_item]) -> Block
36
+ end
37
+
38
+ class Declaration < Data
39
+ attr_reader name: String
40
+ attr_reader value: Array[component_value]
41
+ attr_reader important: bool
42
+
43
+ def self.new: (name: String, value: Array[component_value], important: bool) -> Declaration
44
+ end
45
+
46
+ class Function < Data
47
+ attr_reader name: String
48
+ attr_reader value: Array[component_value]
49
+
50
+ def self.new: (name: String, value: Array[component_value]) -> Function
51
+ end
52
+
53
+ # `open` is one of `(`, `[`, `{`.
54
+ class SimpleBlock < Data
55
+ attr_reader open: String
56
+ attr_reader value: Array[component_value]
57
+
58
+ def self.new: (open: String, value: Array[component_value]) -> SimpleBlock
59
+
60
+ def braced?: () -> bool
61
+ def bracketed?: () -> bool
62
+ def parenthesized?: () -> bool
63
+ end
64
+
65
+ # Inclusive code-point range produced by `CSS.parse_urange`.
66
+ class UnicodeRange < Data
67
+ attr_reader first: Integer
68
+ attr_reader last: Integer
69
+
70
+ def self.new: (first: Integer, last: Integer) -> UnicodeRange
71
+
72
+ def cover?: (Integer cp) -> bool
73
+ def to_s: () -> String
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,161 @@
1
+ module CSS
2
+ module Selectors
3
+ # Marker module included by every Selector AST node so callers can
4
+ # discriminate `Selectors::Node` from raw component values.
5
+ module Node
6
+ end
7
+
8
+ type combinator = :descendant | :child | :next_sibling | :subsequent_sibling
9
+
10
+ type simple_selector =
11
+ TypeSelector | UniversalSelector | NestingSelector
12
+ | IdSelector | ClassSelector | AttributeSelector
13
+ | PseudoClass | PseudoElement
14
+
15
+ type attribute_matcher = :exact | :includes | :dash | :prefix | :suffix | :substring
16
+
17
+ type case_flag = :i | :s
18
+
19
+ # `argument` is `nil` for plain `:hover` etc., a `SelectorList` for
20
+ # `:not / :is / :where / :matches`, an `AnB` for `:nth-*`, or an
21
+ # opaque component-value array for unrecognized functional pseudos
22
+ # (including `:has`, which is intentionally not parsed as a selector
23
+ # list yet).
24
+ type pseudo_argument = nil | SelectorList | AnB | Array[component_value]
25
+
26
+ class SelectorList < Data
27
+ include Node
28
+
29
+ attr_reader selectors: Array[ComplexSelector]
30
+
31
+ def self.new: (selectors: Array[ComplexSelector]) -> SelectorList
32
+
33
+ def to_s: () -> String
34
+ end
35
+
36
+ # `compounds.size == combinators.size + 1`. `combinators[i]` joins
37
+ # `compounds[i]` to `compounds[i + 1]`.
38
+ class ComplexSelector < Data
39
+ include Node
40
+
41
+ attr_reader compounds: Array[CompoundSelector]
42
+ attr_reader combinators: Array[combinator]
43
+
44
+ def self.new: (compounds: Array[CompoundSelector], combinators: Array[combinator]) -> ComplexSelector
45
+
46
+ def to_s: () -> String
47
+ end
48
+
49
+ class CompoundSelector < Data
50
+ include Node
51
+
52
+ attr_reader components: Array[simple_selector]
53
+
54
+ def self.new: (components: Array[simple_selector]) -> CompoundSelector
55
+
56
+ def to_s: () -> String
57
+ end
58
+
59
+ class TypeSelector < Data
60
+ include Node
61
+
62
+ attr_reader name: String
63
+
64
+ def self.new: (name: String) -> TypeSelector
65
+ end
66
+
67
+ class UniversalSelector < Data
68
+ include Node
69
+
70
+ def self.new: () -> UniversalSelector
71
+ end
72
+
73
+ class NestingSelector < Data
74
+ include Node
75
+
76
+ def self.new: () -> NestingSelector
77
+ end
78
+
79
+ class IdSelector < Data
80
+ include Node
81
+
82
+ attr_reader name: String
83
+
84
+ def self.new: (name: String) -> IdSelector
85
+ end
86
+
87
+ class ClassSelector < Data
88
+ include Node
89
+
90
+ attr_reader name: String
91
+
92
+ def self.new: (name: String) -> ClassSelector
93
+ end
94
+
95
+ class AttributeSelector < Data
96
+ include Node
97
+
98
+ attr_reader name: String
99
+ attr_reader matcher: attribute_matcher?
100
+ attr_reader value: String?
101
+ attr_reader case_flag: case_flag?
102
+
103
+ def self.new: (name: String, matcher: attribute_matcher?, value: String?, case_flag: case_flag?) -> AttributeSelector
104
+ end
105
+
106
+ class PseudoClass < Data
107
+ include Node
108
+
109
+ attr_reader name: String
110
+ attr_reader argument: pseudo_argument
111
+
112
+ def self.new: (name: String, argument: pseudo_argument) -> PseudoClass
113
+ end
114
+
115
+ class PseudoElement < Data
116
+ include Node
117
+
118
+ attr_reader name: String
119
+ attr_reader argument: pseudo_argument
120
+
121
+ def self.new: (name: String, argument: pseudo_argument) -> PseudoElement
122
+ end
123
+
124
+ class AnB < Data
125
+ include Node
126
+
127
+ attr_reader step: Integer
128
+ attr_reader offset: Integer
129
+
130
+ def self.new: (step: Integer, offset: Integer) -> AnB
131
+
132
+ def to_s: () -> String
133
+ end
134
+
135
+ # Selectors §16 specificity tuple `(a, b, c)` — id / class+attr+pseudo /
136
+ # type+pseudo-element counts.
137
+ class Specificity < Data
138
+ include Comparable
139
+
140
+ ZERO: Specificity
141
+
142
+ attr_reader a: Integer
143
+ attr_reader b: Integer
144
+ attr_reader c: Integer
145
+
146
+ def self.new: (a: Integer, b: Integer, c: Integer) -> Specificity
147
+
148
+ def <=>: (untyped other) -> Integer?
149
+ def +: (Specificity other) -> Specificity
150
+ def to_s: () -> String
151
+ end
152
+
153
+ module Matcher
154
+ def self.matches?: (untyped element, untyped selector, ?cache: Hash[Integer, untyped]) -> bool
155
+
156
+ def self.tag_of: (untyped element, ?Hash[Integer, untyped]?) -> String
157
+ def self.id_of: (untyped element, ?Hash[Integer, untyped]?) -> String?
158
+ def self.classes_of: (untyped element, ?Hash[Integer, untyped]?) -> Set[String]
159
+ end
160
+ end
161
+ end
data/sig/css/token.rbs ADDED
@@ -0,0 +1,33 @@
1
+ module CSS
2
+ class Token
3
+ TYPES: Array[Symbol]
4
+
5
+ type token_type =
6
+ :ident | :function | :at_keyword | :hash | :string | :bad_string
7
+ | :url | :bad_url | :delim | :number | :percentage | :dimension
8
+ | :whitespace | :cdo | :cdc | :comment
9
+ | :colon | :semicolon | :comma
10
+ | :lbracket | :rbracket | :lparen | :rparen | :lbrace | :rbrace
11
+ | :eof
12
+
13
+ attr_reader type: token_type
14
+ attr_reader value: untyped
15
+ attr_reader flag: Symbol?
16
+ attr_reader unit: String?
17
+ attr_reader position: Position?
18
+
19
+ def initialize: (token_type type, ?untyped value, ?flag: Symbol?, ?unit: String?, ?position: Position?) -> void
20
+
21
+ def whitespace?: () -> bool
22
+ def comment?: () -> bool
23
+ def trivia?: () -> bool
24
+
25
+ def assign_position!: (Position pos) -> self
26
+
27
+ def ==: (untyped other) -> bool
28
+ def eql?: (untyped other) -> bool
29
+ def hash: () -> Integer
30
+
31
+ def inspect: () -> String
32
+ end
33
+ end
data/sig/css.rbs ADDED
@@ -0,0 +1,94 @@
1
+ # CSS Syntax Level 4 toolkit — top-level entry points and shared types.
2
+ #
3
+ # Public API surface lives directly on `CSS`; AST nodes live under
4
+ # `CSS::Nodes`, `CSS::Selectors`, and `CSS::MediaQueries`. Per-token
5
+ # source positions are `CSS::Position`.
6
+ module CSS
7
+ # Inputs that carry CSS source. `String` is tokenized internally; an
8
+ # array of component values (as produced by `CSS.parse_component_values`
9
+ # or by reading a rule's `prelude`) is consumed structurally.
10
+ type input = String | Array[component_value]
11
+
12
+ # The output of `CSS.parse_component_value` and the element type of a
13
+ # rule's `prelude` / declaration's `value`.
14
+ type component_value = Token | Nodes::Function | Nodes::SimpleBlock
15
+
16
+ # Bracket-pair information used by both the parser (open token type →
17
+ # open char, close token type) and the serializer.
18
+ BRACKET_OPEN_CHAR: Hash[Symbol, String]
19
+ BRACKET_CLOSE_TYPE: Hash[Symbol, Symbol]
20
+ BRACKET_PAIRS: Hash[String, String]
21
+
22
+ class ParseError < StandardError
23
+ attr_reader position: Position?
24
+
25
+ def initialize: (String message, ?position: Position?) -> void
26
+ end
27
+
28
+ # 1-based line / column with 0-based character offsets into the
29
+ # preprocessed input.
30
+ class Position < Data
31
+ attr_reader line: Integer
32
+ attr_reader column: Integer
33
+ attr_reader offset: Integer
34
+ attr_reader end_offset: Integer
35
+
36
+ def self.new: (line: Integer, column: Integer, offset: Integer, end_offset: Integer) -> Position
37
+
38
+ def to_s: () -> String
39
+ end
40
+
41
+ # Tokenization (Syntax 4 §4)
42
+ # ---------------------------------------------------------------
43
+
44
+ def self.tokenize: (String input, ?preserve_comments: bool) -> Array[Token]
45
+
46
+ # Parsing (Syntax 4 §5.3)
47
+ # ---------------------------------------------------------------
48
+
49
+ def self.parse_stylesheet: (input input, ?preserve_comments: bool) -> Nodes::Stylesheet
50
+ def self.parse: (input input, ?preserve_comments: bool) -> Nodes::Stylesheet
51
+
52
+ def self.parse_rule: (input input, ?preserve_comments: bool) -> (Nodes::AtRule | Nodes::QualifiedRule)
53
+ def self.parse_declaration: (input input, ?preserve_comments: bool) -> Nodes::Declaration
54
+ def self.parse_block_contents: (input input, ?preserve_comments: bool) -> Nodes::Block
55
+
56
+ def self.parse_component_value: (input input, ?preserve_comments: bool) -> component_value
57
+ def self.parse_component_values: (input input, ?preserve_comments: bool) -> Array[component_value]
58
+ def self.parse_comma_separated_values: (input input, ?preserve_comments: bool) -> Array[Array[component_value]]
59
+
60
+ # Selectors (Selectors 4)
61
+ # ---------------------------------------------------------------
62
+
63
+ def self.parse_selector_list: (String | Array[component_value] input) -> Selectors::SelectorList
64
+ def self.parse_selector: (String | Array[component_value] input) -> Selectors::ComplexSelector
65
+ def self.parse_anb: (String | Array[Token] input) -> Selectors::AnB
66
+
67
+ def self.specificity: (Selectors::Node selector) -> Selectors::Specificity
68
+
69
+ type selector = Selectors::SelectorList | Selectors::ComplexSelector | Selectors::CompoundSelector
70
+
71
+ def self.matches?: (untyped element, String | selector selector, ?cache: Hash[Integer, untyped]) -> bool
72
+
73
+ # Media queries (Media Queries 4)
74
+ # ---------------------------------------------------------------
75
+
76
+ def self.parse_media_query_list: (String | Array[component_value] input) -> MediaQueries::MediaQueryList
77
+
78
+ type media_context_input = MediaQueries::Context | Hash[String, untyped]
79
+
80
+ def self.media_matches?: (String | MediaQueries::MediaQueryList query_list, media_context_input context) -> bool
81
+
82
+ # Other transformations
83
+ # ---------------------------------------------------------------
84
+
85
+ def self.parse_urange: (_ToS input) -> Nodes::UnicodeRange
86
+
87
+ def self.desugar: (Nodes::Stylesheet stylesheet) -> Nodes::Stylesheet
88
+
89
+ def self.cascade: (Nodes::Stylesheet stylesheet, ?context: MediaQueries::Context) -> Cascade
90
+
91
+ def self.serialize: (untyped node) -> String
92
+
93
+ VERSION: String
94
+ 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.0
4
+ version: 0.1.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Keita Urashima
@@ -45,6 +45,12 @@ files:
45
45
  - lib/css/urange.rb
46
46
  - lib/css/version.rb
47
47
  - lib/p_css.rb
48
+ - sig/css.rbs
49
+ - sig/css/cascade.rbs
50
+ - sig/css/media_queries.rbs
51
+ - sig/css/nodes.rbs
52
+ - sig/css/selectors.rbs
53
+ - sig/css/token.rbs
48
54
  homepage: https://github.com/ursm/p_css
49
55
  licenses:
50
56
  - MIT