cataract 0.2.1 → 0.2.3
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/.github/workflows/ci.yml +1 -1
- data/.rubocop.yml +2 -0
- data/BENCHMARKS.md +41 -38
- data/CHANGELOG.md +16 -0
- data/README.md +9 -3
- data/ext/cataract/cataract.c +273 -92
- data/ext/cataract/cataract.h +4 -3
- data/ext/cataract/css_parser.c +125 -11
- data/ext/cataract/flatten.c +271 -16
- data/lib/cataract/declaration.rb +19 -0
- data/lib/cataract/pure/flatten.rb +103 -8
- data/lib/cataract/pure/parser.rb +222 -141
- data/lib/cataract/pure/serializer.rb +217 -115
- data/lib/cataract/pure.rb +4 -2
- data/lib/cataract/rule.rb +39 -3
- data/lib/cataract/stylesheet.rb +137 -14
- data/lib/cataract/stylesheet_scope.rb +11 -4
- data/lib/cataract/version.rb +1 -1
- metadata +1 -1
|
@@ -152,16 +152,27 @@ module Cataract
|
|
|
152
152
|
end
|
|
153
153
|
|
|
154
154
|
# Expand shorthands in regular rules only (AtRules don't have declarations)
|
|
155
|
+
# NOTE: Using manual each + concat instead of .flat_map for performance.
|
|
156
|
+
# The concise form (.flat_map) is ~5-10% slower depending on number of shorthands to expand.
|
|
155
157
|
regular_rules.each do |rule|
|
|
156
|
-
|
|
158
|
+
expanded = []
|
|
159
|
+
rule.declarations.each do |decl|
|
|
160
|
+
expanded.concat(_expand_shorthand(decl))
|
|
161
|
+
end
|
|
162
|
+
rule.declarations.replace(expanded)
|
|
157
163
|
end
|
|
158
164
|
|
|
159
165
|
merged_rules = []
|
|
160
166
|
|
|
161
167
|
# Always group by selector and preserve original selectors
|
|
162
168
|
# (Nesting is flattened during parsing, so we just merge by resolved selector)
|
|
163
|
-
|
|
164
|
-
|
|
169
|
+
# NOTE: Using manual each instead of .group_by to avoid intermediate hash allocation.
|
|
170
|
+
# The concise form (.group_by) is ~10-25% slower depending on selector uniqueness.
|
|
171
|
+
by_selector = {}
|
|
172
|
+
regular_rules.each do |rule|
|
|
173
|
+
(by_selector[rule.selector] ||= []) << rule
|
|
174
|
+
end
|
|
175
|
+
by_selector.each do |selector, rules|
|
|
165
176
|
merged_rule = flatten_rules_for_selector(selector, rules)
|
|
166
177
|
merged_rules << merged_rule if merged_rule
|
|
167
178
|
end
|
|
@@ -169,16 +180,28 @@ module Cataract
|
|
|
169
180
|
# Recreate shorthands where possible
|
|
170
181
|
merged_rules.each { |rule| recreate_shorthands!(rule) }
|
|
171
182
|
|
|
183
|
+
# Assign new IDs before checking divergence (so we can build correct selector_lists hash)
|
|
184
|
+
merged_rules.each_with_index { |rule, i| rule.id = i }
|
|
185
|
+
|
|
186
|
+
# Handle selector list divergence: remove rules from selector lists if declarations no longer match
|
|
187
|
+
# This makes selector_list_id authoritative - if set, declarations MUST be identical
|
|
188
|
+
# Only process if selector_lists is enabled in the stylesheet's parser options
|
|
189
|
+
selector_lists = {}
|
|
190
|
+
parser_options = stylesheet.instance_variable_get(:@parser_options) || {}
|
|
191
|
+
if parser_options[:selector_lists]
|
|
192
|
+
update_selector_lists_for_divergence!(merged_rules, selector_lists)
|
|
193
|
+
end
|
|
194
|
+
|
|
172
195
|
# Add passthrough AtRules to output
|
|
173
196
|
merged_rules.concat(at_rules)
|
|
174
197
|
|
|
175
198
|
# Create result stylesheet
|
|
176
199
|
if mutate
|
|
177
200
|
stylesheet.instance_variable_set(:@rules, merged_rules)
|
|
178
|
-
# Update rule IDs
|
|
179
|
-
merged_rules.each_with_index { |rule, i| rule.id = i }
|
|
180
201
|
# Clear media index (no media rules after merge flattens everything)
|
|
181
202
|
stylesheet.instance_variable_set(:@media_index, {})
|
|
203
|
+
# Update selector lists with divergence tracking
|
|
204
|
+
stylesheet.instance_variable_set(:@_selector_lists, selector_lists)
|
|
182
205
|
stylesheet
|
|
183
206
|
else
|
|
184
207
|
# Create new Stylesheet with merged rules
|
|
@@ -186,8 +209,7 @@ module Cataract
|
|
|
186
209
|
result.instance_variable_set(:@rules, merged_rules)
|
|
187
210
|
result.instance_variable_set(:@media_index, {})
|
|
188
211
|
result.instance_variable_set(:@charset, stylesheet.charset)
|
|
189
|
-
|
|
190
|
-
merged_rules.each_with_index { |rule, i| rule.id = i }
|
|
212
|
+
result.instance_variable_set(:@_selector_lists, selector_lists)
|
|
191
213
|
result
|
|
192
214
|
end
|
|
193
215
|
end
|
|
@@ -267,6 +289,11 @@ module Cataract
|
|
|
267
289
|
|
|
268
290
|
return nil if declarations.empty?
|
|
269
291
|
|
|
292
|
+
# Preserve selector_list_id if all rules in group share the same one
|
|
293
|
+
selector_list_ids = rules.filter_map(&:selector_list_id)
|
|
294
|
+
selector_list_ids.uniq!
|
|
295
|
+
selector_list_id = selector_list_ids.size == 1 ? selector_list_ids.first : nil
|
|
296
|
+
|
|
270
297
|
# Create merged rule
|
|
271
298
|
Rule.new(
|
|
272
299
|
0, # ID will be updated later
|
|
@@ -274,7 +301,8 @@ module Cataract
|
|
|
274
301
|
declarations,
|
|
275
302
|
rules.first.specificity, # Use first rule's specificity
|
|
276
303
|
nil, # No parent after flattening
|
|
277
|
-
nil
|
|
304
|
+
nil, # No nesting style after flattening
|
|
305
|
+
selector_list_id # Preserve if all rules share same ID
|
|
278
306
|
)
|
|
279
307
|
end
|
|
280
308
|
|
|
@@ -1141,5 +1169,72 @@ module Cataract
|
|
|
1141
1169
|
rule.declarations.reject! { |d| LIST_STYLE_PROPERTIES.include?(d.property) }
|
|
1142
1170
|
rule.declarations << Declaration.new(PROP_LIST_STYLE, shorthand_value, important)
|
|
1143
1171
|
end
|
|
1172
|
+
|
|
1173
|
+
# Update selector lists to remove diverged rules
|
|
1174
|
+
#
|
|
1175
|
+
# After flattening/cascade, rules that were in the same selector list may have
|
|
1176
|
+
# different declarations. This method builds the selector_lists hash with only
|
|
1177
|
+
# rules that still match, and clears selector_list_id for diverged rules.
|
|
1178
|
+
#
|
|
1179
|
+
# @param merged_rules [Array<Rule>] Flattened rules (with new IDs assigned)
|
|
1180
|
+
# @param selector_lists [Hash] Empty hash to populate with list_id => Array of rule IDs
|
|
1181
|
+
def self.update_selector_lists_for_divergence!(merged_rules, selector_lists)
|
|
1182
|
+
# Group merged rules by selector_list_id (skip rules with no list)
|
|
1183
|
+
# Note: Using manual each loop instead of .select{}.group_by for performance.
|
|
1184
|
+
# The more concise form (.select + .group_by) is ~50-60% slower due to intermediate array allocation.
|
|
1185
|
+
rules_by_list = {}
|
|
1186
|
+
merged_rules.each do |r|
|
|
1187
|
+
next unless r.selector_list_id
|
|
1188
|
+
|
|
1189
|
+
(rules_by_list[r.selector_list_id] ||= []) << r
|
|
1190
|
+
end
|
|
1191
|
+
|
|
1192
|
+
# For each selector list, check if declarations still match
|
|
1193
|
+
rules_by_list.each do |list_id, rules_in_list|
|
|
1194
|
+
# Skip if only one rule in list (nothing to compare)
|
|
1195
|
+
next if rules_in_list.size <= 1
|
|
1196
|
+
|
|
1197
|
+
# Get first rule as reference
|
|
1198
|
+
reference_rule = rules_in_list.first
|
|
1199
|
+
reference_decls = reference_rule.declarations
|
|
1200
|
+
|
|
1201
|
+
# Find rules that still match (have identical declarations)
|
|
1202
|
+
matching_rules = [reference_rule]
|
|
1203
|
+
|
|
1204
|
+
rules_in_list[1..].each do |rule|
|
|
1205
|
+
if declarations_equal?(reference_decls, rule.declarations)
|
|
1206
|
+
matching_rules << rule
|
|
1207
|
+
else
|
|
1208
|
+
# Clear selector_list_id for diverged rule
|
|
1209
|
+
rule.selector_list_id = nil
|
|
1210
|
+
end
|
|
1211
|
+
end
|
|
1212
|
+
|
|
1213
|
+
# Only keep the selector list if at least 2 rules still match
|
|
1214
|
+
if matching_rules.size >= 2
|
|
1215
|
+
# Build selector_lists hash with NEW rule IDs
|
|
1216
|
+
selector_lists[list_id] = matching_rules.map(&:id)
|
|
1217
|
+
else
|
|
1218
|
+
# Clear selector_list_id for the last remaining rule too
|
|
1219
|
+
matching_rules.each { |r| r.selector_list_id = nil }
|
|
1220
|
+
end
|
|
1221
|
+
end
|
|
1222
|
+
end
|
|
1223
|
+
|
|
1224
|
+
# Check if two declaration arrays are identical
|
|
1225
|
+
#
|
|
1226
|
+
# @param decls1 [Array<Declaration>]
|
|
1227
|
+
# @param decls2 [Array<Declaration>]
|
|
1228
|
+
# @return [Boolean]
|
|
1229
|
+
def self.declarations_equal?(decls1, decls2)
|
|
1230
|
+
return false if decls1.size != decls2.size
|
|
1231
|
+
|
|
1232
|
+
# Compare each declaration (property, value, important must all match)
|
|
1233
|
+
decls1.zip(decls2).all? do |d1, d2|
|
|
1234
|
+
d1.property == d2.property &&
|
|
1235
|
+
d1.value == d2.value &&
|
|
1236
|
+
d1.important == d2.important
|
|
1237
|
+
end
|
|
1238
|
+
end
|
|
1144
1239
|
end
|
|
1145
1240
|
end
|