p_css 0.1.0 → 0.1.1
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 +113 -21
- data/lib/css/selectors/matcher.rb +75 -28
- data/lib/css/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 9e5c2f5073fa34e7847e6ce57005acbd33a912c544b6ea6971f8c50fc6f9f416
|
|
4
|
+
data.tar.gz: 4b6de8fb7d7919fafdc4e4ba249024d022c628675c0ecc66d65cba85f6132b51
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ee478932c614e644236fe532663bf781c72397795db988ea34c92ff02a7659da0e076a2f547397f94b6500168b4fafa9071b728d0dd38b05075dfcf5589201fa
|
|
7
|
+
data.tar.gz: 9b87600e33a0500fd61c0b919e4709795a0cea47755eded8f343adb5341ee4c47a49b5197429cd443102adc88b0ebbcfdb2ee26a7e2a3133387c3650daae44fe
|
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,97 @@ 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
|
+
|
|
120
|
+
EMPTY_INDEXES = [].freeze
|
|
121
|
+
|
|
122
|
+
def build_index(entries)
|
|
123
|
+
by_id = {}
|
|
124
|
+
by_class = {}
|
|
125
|
+
by_tag = {}
|
|
126
|
+
universal = []
|
|
127
|
+
|
|
128
|
+
entries.each_with_index do |entry, idx|
|
|
129
|
+
keys = Set.new
|
|
130
|
+
|
|
131
|
+
entry.selector_pairs.each do |sel, _spec|
|
|
132
|
+
key = anchor_key(sel)
|
|
133
|
+
|
|
134
|
+
next if keys.include?(key)
|
|
135
|
+
|
|
136
|
+
keys << key
|
|
137
|
+
|
|
138
|
+
case key.first
|
|
139
|
+
when :id then (by_id[key.last] ||= []) << idx
|
|
140
|
+
when :class then (by_class[key.last] ||= []) << idx
|
|
141
|
+
when :tag then (by_tag[key.last] ||= []) << idx
|
|
142
|
+
when :universal then universal << idx
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
Index.new(
|
|
148
|
+
by_id: by_id.freeze,
|
|
149
|
+
by_class: by_class.freeze,
|
|
150
|
+
by_tag: by_tag.freeze,
|
|
151
|
+
universal: universal.freeze
|
|
152
|
+
)
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
# Picks the strongest anchor in the rightmost compound: id > class >
|
|
156
|
+
# tag > universal. Compounds whose only simple selectors are pseudos
|
|
157
|
+
# (e.g. `:hover`) or attribute matchers fall through to universal —
|
|
158
|
+
# they will be tested against every element, but real-world
|
|
159
|
+
# stylesheets rarely have many such rules.
|
|
160
|
+
def anchor_key(complex_selector)
|
|
161
|
+
compound = complex_selector.compounds.last
|
|
162
|
+
|
|
163
|
+
compound.components.each do |c|
|
|
164
|
+
return [:id, c.name] if c.is_a?(Selectors::IdSelector)
|
|
165
|
+
end
|
|
166
|
+
compound.components.each do |c|
|
|
167
|
+
return [:class, c.name] if c.is_a?(Selectors::ClassSelector)
|
|
168
|
+
end
|
|
169
|
+
compound.components.each do |c|
|
|
170
|
+
return [:tag, c.name.downcase] if c.is_a?(Selectors::TypeSelector)
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
[:universal]
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
# Resolve helpers
|
|
177
|
+
# ----------------------------------------------------------------
|
|
178
|
+
|
|
179
|
+
def collect_candidate_indexes(element, cache)
|
|
180
|
+
seen = Set.new
|
|
181
|
+
|
|
182
|
+
el_id = Selectors::Matcher.id_of(element, cache)
|
|
183
|
+
|
|
184
|
+
if el_id && (bucket = @index.by_id[el_id])
|
|
185
|
+
seen.merge(bucket)
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
Selectors::Matcher.classes_of(element, cache).each do |cls|
|
|
189
|
+
bucket = @index.by_class[cls]
|
|
190
|
+
seen.merge(bucket) if bucket
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
tag_bucket = @index.by_tag[Selectors::Matcher.tag_of(element, cache)]
|
|
194
|
+
seen.merge(tag_bucket) if tag_bucket
|
|
195
|
+
|
|
196
|
+
seen.merge(@index.universal)
|
|
197
|
+
|
|
198
|
+
seen.to_a.sort!
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
def best_matching_specificity(element, selector_pairs, cache)
|
|
106
202
|
best = nil
|
|
107
203
|
|
|
108
204
|
selector_pairs.each do |sel, spec|
|
|
109
|
-
next unless Selectors::Matcher.matches?(element, sel)
|
|
205
|
+
next unless Selectors::Matcher.matches?(element, sel, cache: cache)
|
|
110
206
|
|
|
111
207
|
best = spec if best.nil? || spec > best
|
|
112
208
|
end
|
|
@@ -114,24 +210,20 @@ module CSS
|
|
|
114
210
|
best
|
|
115
211
|
end
|
|
116
212
|
|
|
117
|
-
# Single-pass
|
|
118
|
-
#
|
|
119
|
-
#
|
|
213
|
+
# Single-pass running max per property name. Cheaper than group_by +
|
|
214
|
+
# max_by, and avoids allocating a fresh comparison key per
|
|
215
|
+
# declaration.
|
|
120
216
|
def pick_winners(matches)
|
|
121
|
-
|
|
122
|
-
winner_matches = {}
|
|
217
|
+
best = {}
|
|
123
218
|
|
|
124
219
|
matches.each do |m|
|
|
125
220
|
name = m.declaration.name
|
|
126
|
-
incumbent =
|
|
221
|
+
incumbent = best[name]
|
|
127
222
|
|
|
128
|
-
if incumbent.nil? || better?(m, incumbent)
|
|
129
|
-
winners[name] = m.declaration
|
|
130
|
-
winner_matches[name] = m
|
|
131
|
-
end
|
|
223
|
+
best[name] = m if incumbent.nil? || better?(m, incumbent)
|
|
132
224
|
end
|
|
133
225
|
|
|
134
|
-
|
|
226
|
+
best.transform_values(&:declaration)
|
|
135
227
|
end
|
|
136
228
|
|
|
137
229
|
# `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 = Struct.new(:tag, :id, :classes, keyword_init: true)
|
|
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:)
|
data/lib/css/version.rb
CHANGED