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.
@@ -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
- rule.declarations.replace(rule.declarations.flat_map { |decl| _expand_shorthand(decl) })
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
- grouped = regular_rules.group_by(&:selector)
164
- grouped.each do |selector, rules|
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
- # Update rule IDs
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 # No nesting style after flattening
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