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 +4 -4
- data/lib/css/cascade.rb +119 -21
- data/lib/css/selectors/matcher.rb +75 -32
- data/lib/css/version.rb +1 -1
- data/sig/css/cascade.rbs +22 -0
- data/sig/css/media_queries.rbs +107 -0
- data/sig/css/nodes.rbs +76 -0
- data/sig/css/selectors.rbs +161 -0
- data/sig/css/token.rbs +33 -0
- data/sig/css.rbs +94 -0
- metadata +7 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 772060eec5d726253913cd8be2daa6180429680d014c1752c9b26b15618e4ba8
|
|
4
|
+
data.tar.gz: edd5e5afc5871362dc21cca89d8eb6a5b085350022f79f2b8c6f07a032f07aaa
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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,
|
|
12
|
-
# evaluated against the supplied context up-front
|
|
13
|
-
#
|
|
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
|
-
|
|
33
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
118
|
-
#
|
|
119
|
-
#
|
|
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
|
-
|
|
122
|
-
winner_matches = {}
|
|
223
|
+
best = {}
|
|
123
224
|
|
|
124
225
|
matches.each do |m|
|
|
125
226
|
name = m.declaration.name
|
|
126
|
-
incumbent =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
96
|
+
when TypeSelector then tag_of(element, cache).casecmp?(simple.name)
|
|
89
97
|
when UniversalSelector then true
|
|
90
|
-
when IdSelector then
|
|
91
|
-
when ClassSelector then
|
|
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
data/sig/css/cascade.rbs
ADDED
|
@@ -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.
|
|
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
|