cataract 0.1.4 → 0.2.0

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.
@@ -1,9 +1,15 @@
1
1
  #include "cataract.h"
2
2
 
3
- // Cache frequently used symbol IDs (initialized in init_merge_constants)
4
- static ID id_value = 0;
5
- static ID id_specificity = 0;
6
- static ID id_important = 0;
3
+ // NOTE: This file was previously called merge.c and the functions were named cataract_merge_*
4
+ // The terminology was changed to "flatten" to better represent CSS cascade behavior.
5
+
6
+ // Array indices for property metadata: [source_order, specificity, important, value]
7
+ #define PROP_SOURCE_ORDER 0
8
+ #define PROP_SPECIFICITY 1
9
+ #define PROP_IMPORTANT 2
10
+ #define PROP_VALUE 3
11
+
12
+ // Cache frequently used symbol IDs (initialized in init_flatten_constants)
7
13
  static ID id_all = 0;
8
14
 
9
15
  // Cached ivar IDs for Stylesheet
@@ -13,8 +19,40 @@ static ID id_ivar_media_index = 0;
13
19
  // Cached "merged" selector string
14
20
  static VALUE str_merged_selector = Qnil;
15
21
 
22
+ /*
23
+ * Shorthand recreation mapping: defines how to recreate shorthands from longhand properties
24
+ *
25
+ * We cache VALUE objects for property names to avoid repeated string allocations during
26
+ * hash lookups. These are initialized once in init_flatten_constants().
27
+ */
28
+ struct shorthand_mapping {
29
+ const char *shorthand_name; // e.g., "border-width"
30
+ size_t shorthand_name_len; // Pre-computed strlen(shorthand_name)
31
+ VALUE shorthand_name_val; // Cached Ruby string (initialized at load time)
32
+ const char *prop_top; // e.g., "border-top-width"
33
+ VALUE prop_top_val; // Cached Ruby string
34
+ const char *prop_right; // e.g., "border-right-width"
35
+ VALUE prop_right_val; // Cached Ruby string
36
+ const char *prop_bottom; // e.g., "border-bottom-width"
37
+ VALUE prop_bottom_val; // Cached Ruby string
38
+ const char *prop_left; // e.g., "border-left-width"
39
+ VALUE prop_left_val; // Cached Ruby string
40
+ VALUE (*creator_func)(VALUE, VALUE); // Function pointer to shorthand creator
41
+ };
42
+
43
+ // Static mapping table for all 4-sided shorthand properties
44
+ // The _val fields are initialized to Qnil here and populated in init_flatten_constants()
45
+ static struct shorthand_mapping SHORTHAND_MAPPINGS[] = {
46
+ {"margin", 6, Qnil, "margin-top", Qnil, "margin-right", Qnil, "margin-bottom", Qnil, "margin-left", Qnil, cataract_create_margin_shorthand},
47
+ {"padding", 7, Qnil, "padding-top", Qnil, "padding-right", Qnil, "padding-bottom", Qnil, "padding-left", Qnil, cataract_create_padding_shorthand},
48
+ {"border-width", 12, Qnil, "border-top-width", Qnil, "border-right-width", Qnil, "border-bottom-width", Qnil, "border-left-width", Qnil, cataract_create_border_width_shorthand},
49
+ {"border-style", 12, Qnil, "border-top-style", Qnil, "border-right-style", Qnil, "border-bottom-style", Qnil, "border-left-style", Qnil, cataract_create_border_style_shorthand},
50
+ {"border-color", 12, Qnil, "border-top-color", Qnil, "border-right-color", Qnil, "border-bottom-color", Qnil, "border-left-color", Qnil, cataract_create_border_color_shorthand},
51
+ {NULL, 0, Qnil, NULL, Qnil, NULL, Qnil, NULL, Qnil, NULL, Qnil, NULL} // Sentinel to mark end of array
52
+ };
53
+
16
54
  // Cached property name strings (frozen, never GC'd)
17
- // Initialized in init_merge_constants() at module load time
55
+ // Initialized in init_flatten_constants() at module load time
18
56
  static VALUE str_margin = Qnil;
19
57
  static VALUE str_margin_top = Qnil;
20
58
  static VALUE str_margin_right = Qnil;
@@ -62,12 +100,13 @@ static VALUE str_background_position = Qnil;
62
100
  // Context for expanded property iteration
63
101
  struct expand_context {
64
102
  VALUE properties_hash;
103
+ long source_order;
65
104
  int specificity;
66
105
  VALUE important;
67
106
  };
68
107
 
69
108
  // Callback for rb_hash_foreach - process expanded properties and apply cascade
70
- static int merge_expanded_callback(VALUE exp_prop, VALUE exp_value, VALUE ctx_val) {
109
+ static int flatten_expanded_callback(VALUE exp_prop, VALUE exp_value, VALUE ctx_val) {
71
110
  struct expand_context *ctx = (struct expand_context *)ctx_val;
72
111
 
73
112
  // Expanded properties from shorthand expanders are already lowercase
@@ -78,34 +117,40 @@ static int merge_expanded_callback(VALUE exp_prop, VALUE exp_value, VALUE ctx_va
78
117
  VALUE existing = rb_hash_aref(ctx->properties_hash, exp_prop);
79
118
 
80
119
  if (NIL_P(existing)) {
81
- VALUE prop_data = rb_hash_new();
82
- rb_hash_aset(prop_data, ID2SYM(id_value), exp_value);
83
- rb_hash_aset(prop_data, ID2SYM(id_specificity), INT2NUM(ctx->specificity));
84
- rb_hash_aset(prop_data, ID2SYM(id_important), ctx->important);
85
- // Note: declaration_struct not stored - use global cDeclaration instead
120
+ // Create array: [source_order, specificity, important, value]
121
+ VALUE prop_data = rb_ary_new_capa(4);
122
+ rb_ary_push(prop_data, LONG2NUM(ctx->source_order));
123
+ rb_ary_push(prop_data, INT2NUM(ctx->specificity));
124
+ rb_ary_push(prop_data, ctx->important);
125
+ rb_ary_push(prop_data, exp_value);
86
126
  rb_hash_aset(ctx->properties_hash, exp_prop, prop_data);
87
127
  } else {
88
- VALUE existing_spec = rb_hash_aref(existing, ID2SYM(id_specificity));
89
- VALUE existing_important = rb_hash_aref(existing, ID2SYM(id_important));
90
-
91
- int existing_spec_int = NUM2INT(existing_spec);
128
+ // Access array elements directly
129
+ long existing_order = NUM2LONG(RARRAY_AREF(existing, PROP_SOURCE_ORDER));
130
+ int existing_spec_int = NUM2INT(RARRAY_AREF(existing, PROP_SPECIFICITY));
131
+ VALUE existing_important = RARRAY_AREF(existing, PROP_IMPORTANT);
92
132
  int existing_is_important = RTEST(existing_important);
93
133
 
94
134
  int should_replace = 0;
95
135
  if (is_important) {
96
- if (!existing_is_important || existing_spec_int <= ctx->specificity) {
136
+ if (!existing_is_important || existing_spec_int < ctx->specificity ||
137
+ (existing_spec_int == ctx->specificity && existing_order <= ctx->source_order)) {
97
138
  should_replace = 1;
98
139
  }
99
140
  } else {
100
- if (!existing_is_important && existing_spec_int <= ctx->specificity) {
141
+ if (!existing_is_important &&
142
+ (existing_spec_int < ctx->specificity ||
143
+ (existing_spec_int == ctx->specificity && existing_order <= ctx->source_order))) {
101
144
  should_replace = 1;
102
145
  }
103
146
  }
104
147
 
105
148
  if (should_replace) {
106
- rb_hash_aset(existing, ID2SYM(id_value), exp_value);
107
- rb_hash_aset(existing, ID2SYM(id_specificity), INT2NUM(ctx->specificity));
108
- rb_hash_aset(existing, ID2SYM(id_important), ctx->important);
149
+ // Update array elements
150
+ RARRAY_ASET(existing, PROP_SOURCE_ORDER, LONG2NUM(ctx->source_order));
151
+ RARRAY_ASET(existing, PROP_SPECIFICITY, INT2NUM(ctx->specificity));
152
+ RARRAY_ASET(existing, PROP_IMPORTANT, ctx->important);
153
+ RARRAY_ASET(existing, PROP_VALUE, exp_value);
109
154
  }
110
155
  }
111
156
 
@@ -115,10 +160,10 @@ static int merge_expanded_callback(VALUE exp_prop, VALUE exp_value, VALUE ctx_va
115
160
  }
116
161
 
117
162
  // Callback for rb_hash_foreach - builds result array from properties hash
118
- static int merge_build_result_callback(VALUE property, VALUE prop_data, VALUE result_ary) {
119
- // Extract value and important flag from prop_data
120
- VALUE value = rb_hash_aref(prop_data, ID2SYM(id_value));
121
- VALUE important = rb_hash_aref(prop_data, ID2SYM(id_important));
163
+ static int flatten_build_result_callback(VALUE property, VALUE prop_data, VALUE result_ary) {
164
+ // Extract value and important flag from array: [source_order, specificity, important, value]
165
+ VALUE value = RARRAY_AREF(prop_data, PROP_VALUE);
166
+ VALUE important = RARRAY_AREF(prop_data, PROP_IMPORTANT);
122
167
 
123
168
  // Create Declaration struct (use global cDeclaration)
124
169
  VALUE decl_struct = rb_struct_new(cDeclaration, property, value, important);
@@ -128,11 +173,8 @@ static int merge_build_result_callback(VALUE property, VALUE prop_data, VALUE re
128
173
  }
129
174
 
130
175
  // Initialize cached property strings (called once at module init)
131
- void init_merge_constants(void) {
176
+ void init_flatten_constants(void) {
132
177
  // Initialize symbol IDs
133
- id_value = rb_intern("value");
134
- id_specificity = rb_intern("specificity");
135
- id_important = rb_intern("important");
136
178
  id_all = rb_intern("all");
137
179
 
138
180
  // Initialize ivar IDs for Stylesheet
@@ -248,13 +290,45 @@ void init_merge_constants(void) {
248
290
  // Cached "merged" selector string
249
291
  str_merged_selector = rb_str_freeze(USASCII_STR("merged"));
250
292
  rb_gc_register_mark_object(str_merged_selector);
293
+
294
+ // Populate the shorthand mapping table with cached string VALUEs
295
+ // This avoids allocating new strings on every hash lookup
296
+ SHORTHAND_MAPPINGS[0].shorthand_name_val = str_margin;
297
+ SHORTHAND_MAPPINGS[0].prop_top_val = str_margin_top;
298
+ SHORTHAND_MAPPINGS[0].prop_right_val = str_margin_right;
299
+ SHORTHAND_MAPPINGS[0].prop_bottom_val = str_margin_bottom;
300
+ SHORTHAND_MAPPINGS[0].prop_left_val = str_margin_left;
301
+
302
+ SHORTHAND_MAPPINGS[1].shorthand_name_val = str_padding;
303
+ SHORTHAND_MAPPINGS[1].prop_top_val = str_padding_top;
304
+ SHORTHAND_MAPPINGS[1].prop_right_val = str_padding_right;
305
+ SHORTHAND_MAPPINGS[1].prop_bottom_val = str_padding_bottom;
306
+ SHORTHAND_MAPPINGS[1].prop_left_val = str_padding_left;
307
+
308
+ SHORTHAND_MAPPINGS[2].shorthand_name_val = str_border_width;
309
+ SHORTHAND_MAPPINGS[2].prop_top_val = str_border_top_width;
310
+ SHORTHAND_MAPPINGS[2].prop_right_val = str_border_right_width;
311
+ SHORTHAND_MAPPINGS[2].prop_bottom_val = str_border_bottom_width;
312
+ SHORTHAND_MAPPINGS[2].prop_left_val = str_border_left_width;
313
+
314
+ SHORTHAND_MAPPINGS[3].shorthand_name_val = str_border_style;
315
+ SHORTHAND_MAPPINGS[3].prop_top_val = str_border_top_style;
316
+ SHORTHAND_MAPPINGS[3].prop_right_val = str_border_right_style;
317
+ SHORTHAND_MAPPINGS[3].prop_bottom_val = str_border_bottom_style;
318
+ SHORTHAND_MAPPINGS[3].prop_left_val = str_border_left_style;
319
+
320
+ SHORTHAND_MAPPINGS[4].shorthand_name_val = str_border_color;
321
+ SHORTHAND_MAPPINGS[4].prop_top_val = str_border_top_color;
322
+ SHORTHAND_MAPPINGS[4].prop_right_val = str_border_right_color;
323
+ SHORTHAND_MAPPINGS[4].prop_bottom_val = str_border_bottom_color;
324
+ SHORTHAND_MAPPINGS[4].prop_left_val = str_border_left_color;
251
325
  }
252
326
 
253
327
  // Helper macros to extract property data from properties_hash
254
- // Note: These use id_value, id_specificity, id_important which are initialized in cataract_merge
328
+ // Properties are stored as arrays: [source_order, specificity, important, value]
255
329
  #define GET_PROP_VALUE(hash, prop_name) \
256
330
  ({ VALUE pd = rb_hash_aref(hash, USASCII_STR(prop_name)); \
257
- NIL_P(pd) ? Qnil : rb_hash_aref(pd, ID2SYM(id_value)); })
331
+ NIL_P(pd) ? Qnil : RARRAY_AREF(pd, PROP_VALUE); })
258
332
 
259
333
  #define GET_PROP_DATA(hash, prop_name) \
260
334
  rb_hash_aref(hash, USASCII_STR(prop_name))
@@ -262,7 +336,7 @@ void init_merge_constants(void) {
262
336
  // Versions that accept cached VALUE strings instead of string literals
263
337
  #define GET_PROP_VALUE_STR(hash, str_prop) \
264
338
  ({ VALUE pd = rb_hash_aref(hash, str_prop); \
265
- NIL_P(pd) ? Qnil : rb_hash_aref(pd, ID2SYM(id_value)); })
339
+ NIL_P(pd) ? Qnil : RARRAY_AREF(pd, PROP_VALUE); })
266
340
 
267
341
  #define GET_PROP_DATA_STR(hash, str_prop) \
268
342
  rb_hash_aref(hash, str_prop)
@@ -270,7 +344,7 @@ void init_merge_constants(void) {
270
344
  // Helper macro to check if a property's !important flag matches a reference
271
345
  #define CHECK_IMPORTANT_MATCH(hash, str_prop, ref_important) \
272
346
  ({ VALUE _pd = GET_PROP_DATA_STR(hash, str_prop); \
273
- NIL_P(_pd) ? 1 : (RTEST(rb_hash_aref(_pd, ID2SYM(id_important))) == (ref_important)); })
347
+ NIL_P(_pd) ? 1 : (RTEST(RARRAY_AREF(_pd, PROP_IMPORTANT)) == (ref_important)); })
274
348
 
275
349
  // Macro to create shorthand from 4-sided properties (margin, padding, border-width/style/color)
276
350
  // Reduces repetitive code by encapsulating the common pattern:
@@ -293,10 +367,10 @@ void init_merge_constants(void) {
293
367
  VALUE _bottom_data = GET_PROP_DATA_STR(hash, str_bottom); \
294
368
  VALUE _left_data = GET_PROP_DATA_STR(hash, str_left); \
295
369
  \
296
- VALUE _top_imp = rb_hash_aref(_top_data, ID2SYM(id_important)); \
297
- VALUE _right_imp = rb_hash_aref(_right_data, ID2SYM(id_important)); \
298
- VALUE _bottom_imp = rb_hash_aref(_bottom_data, ID2SYM(id_important)); \
299
- VALUE _left_imp = rb_hash_aref(_left_data, ID2SYM(id_important)); \
370
+ VALUE _top_imp = RARRAY_AREF(_top_data, PROP_IMPORTANT); \
371
+ VALUE _right_imp = RARRAY_AREF(_right_data, PROP_IMPORTANT); \
372
+ VALUE _bottom_imp = RARRAY_AREF(_bottom_data, PROP_IMPORTANT); \
373
+ VALUE _left_imp = RARRAY_AREF(_left_data, PROP_IMPORTANT); \
300
374
  \
301
375
  int _top_is_imp = RTEST(_top_imp); \
302
376
  int _right_is_imp = RTEST(_right_imp); \
@@ -313,12 +387,14 @@ void init_merge_constants(void) {
313
387
  \
314
388
  VALUE _shorthand_value = creator_func(Qnil, _props); \
315
389
  if (!NIL_P(_shorthand_value)) { \
316
- int _specificity = NUM2INT(rb_hash_aref(_top_data, ID2SYM(id_specificity))); \
390
+ long _source_order = NUM2LONG(RARRAY_AREF(_top_data, PROP_SOURCE_ORDER)); \
391
+ int _specificity = NUM2INT(RARRAY_AREF(_top_data, PROP_SPECIFICITY)); \
317
392
  \
318
- VALUE _shorthand_data = rb_hash_new(); \
319
- rb_hash_aset(_shorthand_data, ID2SYM(id_value), _shorthand_value); \
320
- rb_hash_aset(_shorthand_data, ID2SYM(id_specificity), INT2NUM(_specificity)); \
321
- rb_hash_aset(_shorthand_data, ID2SYM(id_important), _top_imp); \
393
+ VALUE _shorthand_data = rb_ary_new_capa(4); \
394
+ rb_ary_push(_shorthand_data, LONG2NUM(_source_order)); \
395
+ rb_ary_push(_shorthand_data, INT2NUM(_specificity)); \
396
+ rb_ary_push(_shorthand_data, _top_imp); \
397
+ rb_ary_push(_shorthand_data, _shorthand_value); \
322
398
  rb_hash_aset(hash, str_shorthand, _shorthand_data); \
323
399
  \
324
400
  rb_hash_delete(hash, str_top); \
@@ -349,24 +425,25 @@ void init_merge_constants(void) {
349
425
  VALUE _left_data = rb_hash_aref(hash, STR_NEW_CSTR(_left_name)); \
350
426
  \
351
427
  if (!NIL_P(_top_data) && !NIL_P(_right_data) && !NIL_P(_bottom_data) && !NIL_P(_left_data)) { \
352
- VALUE _top_imp = rb_hash_aref(_top_data, ID2SYM(id_important)); \
353
- VALUE _right_imp = rb_hash_aref(_right_data, ID2SYM(id_important)); \
354
- VALUE _bottom_imp = rb_hash_aref(_bottom_data, ID2SYM(id_important)); \
355
- VALUE _left_imp = rb_hash_aref(_left_data, ID2SYM(id_important)); \
428
+ VALUE _top_imp = RARRAY_AREF(_top_data, PROP_IMPORTANT); \
429
+ VALUE _right_imp = RARRAY_AREF(_right_data, PROP_IMPORTANT); \
430
+ VALUE _bottom_imp = RARRAY_AREF(_bottom_data, PROP_IMPORTANT); \
431
+ VALUE _left_imp = RARRAY_AREF(_left_data, PROP_IMPORTANT); \
356
432
  \
357
433
  if (RTEST(_top_imp) == RTEST(_right_imp) && RTEST(_top_imp) == RTEST(_bottom_imp) && RTEST(_top_imp) == RTEST(_left_imp)) { \
358
434
  VALUE _props = rb_hash_new(); \
359
- rb_hash_aset(_props, STR_NEW_CSTR(_top_name), rb_hash_aref(_top_data, ID2SYM(id_value))); \
360
- rb_hash_aset(_props, STR_NEW_CSTR(_right_name), rb_hash_aref(_right_data, ID2SYM(id_value))); \
361
- rb_hash_aset(_props, STR_NEW_CSTR(_bottom_name), rb_hash_aref(_bottom_data, ID2SYM(id_value))); \
362
- rb_hash_aset(_props, STR_NEW_CSTR(_left_name), rb_hash_aref(_left_data, ID2SYM(id_value))); \
435
+ rb_hash_aset(_props, STR_NEW_CSTR(_top_name), RARRAY_AREF(_top_data, PROP_VALUE)); \
436
+ rb_hash_aset(_props, STR_NEW_CSTR(_right_name), RARRAY_AREF(_right_data, PROP_VALUE)); \
437
+ rb_hash_aset(_props, STR_NEW_CSTR(_bottom_name), RARRAY_AREF(_bottom_data, PROP_VALUE)); \
438
+ rb_hash_aset(_props, STR_NEW_CSTR(_left_name), RARRAY_AREF(_left_data, PROP_VALUE)); \
363
439
  \
364
440
  VALUE _shorthand_value = creator_func(Qnil, _props); \
365
441
  if (!NIL_P(_shorthand_value)) { \
366
- VALUE _shorthand_data = rb_hash_new(); \
367
- rb_hash_aset(_shorthand_data, ID2SYM(id_value), _shorthand_value); \
368
- rb_hash_aset(_shorthand_data, ID2SYM(id_specificity), rb_hash_aref(_top_data, ID2SYM(id_specificity))); \
369
- rb_hash_aset(_shorthand_data, ID2SYM(id_important), _top_imp); \
442
+ VALUE _shorthand_data = rb_ary_new_capa(4); \
443
+ rb_ary_push(_shorthand_data, RARRAY_AREF(_top_data, PROP_SOURCE_ORDER)); \
444
+ rb_ary_push(_shorthand_data, RARRAY_AREF(_top_data, PROP_SPECIFICITY)); \
445
+ rb_ary_push(_shorthand_data, _top_imp); \
446
+ rb_ary_push(_shorthand_data, _shorthand_value); \
370
447
  rb_hash_aset(hash, rb_usascii_str_new(prefix, strlen(prefix)), _shorthand_data); \
371
448
  \
372
449
  rb_hash_delete(hash, STR_NEW_CSTR(_top_name)); \
@@ -379,23 +456,83 @@ void init_merge_constants(void) {
379
456
  } \
380
457
  } while(0)
381
458
 
459
+ // Helper function: Try to recreate a shorthand property from its longhand components
460
+ // Uses cached VALUE objects for property names to avoid repeated string allocations
461
+ static inline void try_recreate_shorthand(VALUE properties_hash, const struct shorthand_mapping *mapping) {
462
+ VALUE top_data = rb_hash_aref(properties_hash, mapping->prop_top_val);
463
+ VALUE right_data = rb_hash_aref(properties_hash, mapping->prop_right_val);
464
+ VALUE bottom_data = rb_hash_aref(properties_hash, mapping->prop_bottom_val);
465
+ VALUE left_data = rb_hash_aref(properties_hash, mapping->prop_left_val);
466
+
467
+ // All four sides must be present
468
+ if (NIL_P(top_data) || NIL_P(right_data) || NIL_P(bottom_data) || NIL_P(left_data)) {
469
+ return;
470
+ }
471
+
472
+ // All four sides must have the same !important flag
473
+ VALUE top_imp = RARRAY_AREF(top_data, PROP_IMPORTANT);
474
+ VALUE right_imp = RARRAY_AREF(right_data, PROP_IMPORTANT);
475
+ VALUE bottom_imp = RARRAY_AREF(bottom_data, PROP_IMPORTANT);
476
+ VALUE left_imp = RARRAY_AREF(left_data, PROP_IMPORTANT);
477
+
478
+ if (RTEST(top_imp) != RTEST(right_imp) ||
479
+ RTEST(top_imp) != RTEST(bottom_imp) ||
480
+ RTEST(top_imp) != RTEST(left_imp)) {
481
+ return;
482
+ }
483
+
484
+ // Build a hash of property values for the creator function
485
+ VALUE props = rb_hash_new();
486
+ rb_hash_aset(props, mapping->prop_top_val, RARRAY_AREF(top_data, PROP_VALUE));
487
+ rb_hash_aset(props, mapping->prop_right_val, RARRAY_AREF(right_data, PROP_VALUE));
488
+ rb_hash_aset(props, mapping->prop_bottom_val, RARRAY_AREF(bottom_data, PROP_VALUE));
489
+ rb_hash_aset(props, mapping->prop_left_val, RARRAY_AREF(left_data, PROP_VALUE));
490
+
491
+ // Call the creator function
492
+ VALUE shorthand_value = mapping->creator_func(Qnil, props);
493
+ if (NIL_P(shorthand_value)) {
494
+ return; // Creator decided not to create shorthand
495
+ }
496
+
497
+ // Create the shorthand property data array
498
+ VALUE shorthand_data = rb_ary_new_capa(4);
499
+ rb_ary_push(shorthand_data, RARRAY_AREF(top_data, PROP_SOURCE_ORDER));
500
+ rb_ary_push(shorthand_data, RARRAY_AREF(top_data, PROP_SPECIFICITY));
501
+ rb_ary_push(shorthand_data, top_imp);
502
+ rb_ary_push(shorthand_data, shorthand_value);
503
+
504
+ // Add shorthand and remove longhand properties
505
+ rb_hash_aset(properties_hash, mapping->shorthand_name_val, shorthand_data);
506
+ rb_hash_delete(properties_hash, mapping->prop_top_val);
507
+ rb_hash_delete(properties_hash, mapping->prop_right_val);
508
+ rb_hash_delete(properties_hash, mapping->prop_bottom_val);
509
+ rb_hash_delete(properties_hash, mapping->prop_left_val);
510
+
511
+ DEBUG_PRINTF(" -> Recreated %s shorthand\n", mapping->shorthand_name);
512
+ }
513
+
382
514
  /*
383
515
  * Helper struct: For processing expanded properties during merge
384
516
  */
385
517
  struct expand_property_data {
386
518
  VALUE properties_hash; // Target hash to store properties
387
- int specificity; // Specificity of the selector
519
+ VALUE selector; // Selector string (for lazy specificity calculation)
520
+ int specificity; // Cached specificity (-1 if not yet calculated)
388
521
  int is_important; // Whether the original declaration was !important
389
522
  long source_order; // Source order of the original declaration
390
523
  };
391
524
 
392
525
  /*
393
526
  * Callback: Process each expanded property and apply cascade rules
527
+ *
528
+ * Optimization: Specificity is calculated lazily only when needed for cascade comparison.
529
+ * This avoids expensive specificity calculation when:
530
+ * - Property doesn't exist yet (no comparison needed)
531
+ * - Importance levels differ (!important always wins, regardless of specificity)
394
532
  */
395
533
  static int process_expanded_property(VALUE prop_name, VALUE prop_value, VALUE arg) {
396
534
  struct expand_property_data *data = (struct expand_property_data *)arg;
397
535
  VALUE properties_hash = data->properties_hash;
398
- int specificity = data->specificity;
399
536
  int is_important = data->is_important;
400
537
  long source_order = data->source_order;
401
538
 
@@ -407,55 +544,113 @@ static int process_expanded_property(VALUE prop_name, VALUE prop_value, VALUE ar
407
544
  VALUE existing = rb_hash_aref(properties_hash, prop_name);
408
545
  if (NIL_P(existing)) {
409
546
  DEBUG_PRINTF(" -> NEW property\n");
410
- VALUE prop_data = rb_hash_new();
411
- rb_hash_aset(prop_data, ID2SYM(id_value), prop_value);
412
- rb_hash_aset(prop_data, ID2SYM(id_specificity), INT2NUM(specificity));
413
- rb_hash_aset(prop_data, ID2SYM(id_important), is_important ? Qtrue : Qfalse);
414
- rb_hash_aset(prop_data, ID2SYM(rb_intern("source_order")), LONG2NUM(source_order));
547
+ // Calculate specificity on first use (lazy initialization)
548
+ if (data->specificity == -1) {
549
+ data->specificity = NUM2INT(calculate_specificity(Qnil, data->selector));
550
+ }
551
+ // Create array: [source_order, specificity, important, value]
552
+ VALUE prop_data = rb_ary_new_capa(4);
553
+ rb_ary_push(prop_data, LONG2NUM(source_order));
554
+ rb_ary_push(prop_data, INT2NUM(data->specificity));
555
+ rb_ary_push(prop_data, is_important ? Qtrue : Qfalse);
556
+ rb_ary_push(prop_data, prop_value);
415
557
  rb_hash_aset(properties_hash, prop_name, prop_data);
416
558
  } else {
417
559
  // Property exists - apply CSS cascade rules
418
- VALUE existing_important = rb_hash_aref(existing, ID2SYM(id_important));
419
- VALUE existing_source_order_val = rb_hash_aref(existing, ID2SYM(rb_intern("source_order")));
420
-
560
+ long existing_source_order = NUM2LONG(RARRAY_AREF(existing, PROP_SOURCE_ORDER));
561
+ int existing_spec = NUM2INT(RARRAY_AREF(existing, PROP_SPECIFICITY));
562
+ VALUE existing_important = RARRAY_AREF(existing, PROP_IMPORTANT);
421
563
  int existing_is_important = RTEST(existing_important);
422
- long existing_source_order = NUM2LONG(existing_source_order_val);
423
-
424
- DEBUG_PRINTF(" -> COLLISION: existing important=%d source_order=%ld, new important=%d source_order=%ld\n",
425
- existing_is_important, existing_source_order, is_important, source_order);
426
564
 
427
565
  int should_replace = 0;
428
566
 
429
567
  // Apply CSS cascade rules:
430
- // 1. !important always wins over non-!important
431
- // 2. Higher specificity wins (same selector = same specificity, skip)
568
+ // 1. !important always wins over non-!important (no specificity check needed)
569
+ // 2. Higher specificity wins (only check when importance is same)
432
570
  // 3. Later source order wins
433
571
  if (is_important && !existing_is_important) {
434
- // New declaration is !important, existing is not - replace
572
+ // New declaration is !important, existing is not - replace (no specificity needed)
435
573
  should_replace = 1;
436
574
  DEBUG_PRINTF(" -> REPLACE (new is !important, existing is not)\n");
437
575
  } else if (!is_important && existing_is_important) {
438
- // Existing declaration is !important, new is not - keep existing
576
+ // Existing declaration is !important, new is not - keep existing (no specificity needed)
439
577
  should_replace = 0;
440
578
  DEBUG_PRINTF(" -> KEEP (existing is !important, new is not)\n");
441
579
  } else {
442
- // Same importance level - later source order wins
443
- should_replace = source_order > existing_source_order;
444
- DEBUG_PRINTF(" -> %s (same importance, %s source order)\n",
580
+ // Same importance level - NOW we need specificity
581
+ // Calculate specificity on first use (lazy initialization)
582
+ if (data->specificity == -1) {
583
+ data->specificity = NUM2INT(calculate_specificity(Qnil, data->selector));
584
+ }
585
+
586
+ DEBUG_PRINTF(" -> COLLISION: existing spec=%d important=%d source_order=%ld, new spec=%d important=%d source_order=%ld\n",
587
+ existing_spec, existing_is_important, existing_source_order,
588
+ data->specificity, is_important, source_order);
589
+
590
+ // Same importance level - check specificity then source order
591
+ if (data->specificity > existing_spec) {
592
+ should_replace = 1;
593
+ } else if (data->specificity == existing_spec) {
594
+ should_replace = source_order > existing_source_order;
595
+ }
596
+ DEBUG_PRINTF(" -> %s (same importance, spec=%d vs %d, order=%ld vs %ld)\n",
445
597
  should_replace ? "REPLACE" : "KEEP",
446
- should_replace ? "later" : "earlier");
598
+ data->specificity, existing_spec, source_order, existing_source_order);
447
599
  }
448
600
 
449
601
  if (should_replace) {
450
- rb_hash_aset(existing, ID2SYM(id_value), prop_value);
451
- rb_hash_aset(existing, ID2SYM(id_important), is_important ? Qtrue : Qfalse);
452
- rb_hash_aset(existing, ID2SYM(rb_intern("source_order")), LONG2NUM(source_order));
602
+ // Calculate specificity if we haven't yet (edge case: importance differs but we're replacing)
603
+ if (data->specificity == -1) {
604
+ data->specificity = NUM2INT(calculate_specificity(Qnil, data->selector));
605
+ }
606
+ RARRAY_ASET(existing, PROP_SOURCE_ORDER, LONG2NUM(source_order));
607
+ RARRAY_ASET(existing, PROP_SPECIFICITY, INT2NUM(data->specificity));
608
+ RARRAY_ASET(existing, PROP_IMPORTANT, is_important ? Qtrue : Qfalse);
609
+ RARRAY_ASET(existing, PROP_VALUE, prop_value);
453
610
  }
454
611
  }
455
612
 
456
613
  return ST_CONTINUE;
457
614
  }
458
615
 
616
+ // Context for flatten_selector_group_callback
617
+ struct flatten_selectors_context {
618
+ VALUE merged_rules;
619
+ VALUE rules_array;
620
+ int *rule_id_counter;
621
+ long selector_index;
622
+ long total_selectors;
623
+ };
624
+
625
+ // Forward declaration
626
+ static VALUE flatten_rules_for_selector(VALUE rules_array, VALUE rule_indices, VALUE selector);
627
+
628
+ // Callback for rb_hash_foreach when merging selector groups
629
+ static int flatten_selector_group_callback(VALUE selector, VALUE group_indices, VALUE arg) {
630
+ struct flatten_selectors_context *ctx = (struct flatten_selectors_context *)arg;
631
+ ctx->selector_index++;
632
+
633
+ DEBUG_PRINTF("\n[Selector %ld/%ld] '%s' - %ld rules in group\n",
634
+ ctx->selector_index, ctx->total_selectors,
635
+ RSTRING_PTR(selector), RARRAY_LEN(group_indices));
636
+
637
+ // Merge all rules in this selector group
638
+ VALUE merged_decls = flatten_rules_for_selector(ctx->rules_array, group_indices, selector);
639
+
640
+ // Create new rule with this selector and merged declarations
641
+ VALUE new_rule = rb_struct_new(cRule,
642
+ INT2FIX((*ctx->rule_id_counter)++),
643
+ selector,
644
+ merged_decls,
645
+ Qnil, // specificity
646
+ Qnil, // parent_rule_id
647
+ Qnil // nesting_style
648
+ );
649
+ rb_ary_push(ctx->merged_rules, new_rule);
650
+
651
+ return ST_CONTINUE;
652
+ }
653
+
459
654
  /*
460
655
  * Helper function: Merge multiple rules with the same selector
461
656
  *
@@ -464,17 +659,13 @@ static int process_expanded_property(VALUE prop_name, VALUE prop_value, VALUE ar
464
659
  *
465
660
  * Returns: Array of merged Declaration structs
466
661
  */
467
- static VALUE merge_rules_for_selector(VALUE rules_array, VALUE rule_indices, VALUE selector) {
662
+ static VALUE flatten_rules_for_selector(VALUE rules_array, VALUE rule_indices, VALUE selector) {
468
663
  long num_rules_in_group = RARRAY_LEN(rule_indices);
469
664
  VALUE properties_hash = rb_hash_new();
470
665
 
471
- DEBUG_PRINTF(" [merge_rules_for_selector] Merging %ld rules for selector '%s'\n",
666
+ DEBUG_PRINTF(" [flatten_rules_for_selector] Merging %ld rules for selector '%s'\n",
472
667
  num_rules_in_group, RSTRING_PTR(selector));
473
668
 
474
- // Calculate specificity once for this selector
475
- VALUE specificity_val = calculate_specificity(Qnil, selector);
476
- int specificity = NUM2INT(specificity_val);
477
-
478
669
  // Process each rule in this selector group
479
670
  for (long g = 0; g < num_rules_in_group; g++) {
480
671
  long rule_idx = FIX2LONG(rb_ary_entry(rule_indices, g));
@@ -512,66 +703,81 @@ static VALUE merge_rules_for_selector(VALUE rules_array, VALUE rule_indices, VAL
512
703
  is_important ? " !important" : "", source_order);
513
704
 
514
705
  // Expand shorthands (margin, padding, background, font, etc.)
515
- // The expand functions return a hash of {property => value}
706
+ // The expand functions return an array of Declaration structs
516
707
  const char *prop_cstr = RSTRING_PTR(property);
517
708
  VALUE expanded = Qnil;
518
709
 
519
- if (strcmp(prop_cstr, "margin") == 0) {
520
- expanded = cataract_expand_margin(Qnil, value);
521
- DEBUG_PRINTF(" -> Expanding margin shorthand (%ld longhands)\n", RHASH_SIZE(expanded));
522
- } else if (strcmp(prop_cstr, "padding") == 0) {
523
- expanded = cataract_expand_padding(Qnil, value);
524
- DEBUG_PRINTF(" -> Expanding padding shorthand (%ld longhands)\n", RHASH_SIZE(expanded));
525
- } else if (strcmp(prop_cstr, "background") == 0) {
526
- expanded = cataract_expand_background(Qnil, value);
527
- DEBUG_PRINTF(" -> Expanding background shorthand (%ld longhands)\n", RHASH_SIZE(expanded));
528
- } else if (strcmp(prop_cstr, "font") == 0) {
529
- expanded = cataract_expand_font(Qnil, value);
530
- DEBUG_PRINTF(" -> Expanding font shorthand (%ld longhands)\n", RHASH_SIZE(expanded));
531
- } else if (strcmp(prop_cstr, "border") == 0) {
532
- expanded = cataract_expand_border(Qnil, value);
533
- DEBUG_PRINTF(" -> Expanding border shorthand (%ld longhands)\n", RHASH_SIZE(expanded));
534
- } else if (strcmp(prop_cstr, "border-color") == 0) {
535
- expanded = cataract_expand_border_color(Qnil, value);
536
- DEBUG_PRINTF(" -> Expanding border-color shorthand (%ld longhands)\n", RHASH_SIZE(expanded));
537
- } else if (strcmp(prop_cstr, "border-style") == 0) {
538
- expanded = cataract_expand_border_style(Qnil, value);
539
- DEBUG_PRINTF(" -> Expanding border-style shorthand (%ld longhands)\n", RHASH_SIZE(expanded));
540
- } else if (strcmp(prop_cstr, "border-width") == 0) {
541
- expanded = cataract_expand_border_width(Qnil, value);
542
- DEBUG_PRINTF(" -> Expanding border-width shorthand (%ld longhands)\n", RHASH_SIZE(expanded));
543
- } else if (strcmp(prop_cstr, "list-style") == 0) {
544
- expanded = cataract_expand_list_style(Qnil, value);
545
- DEBUG_PRINTF(" -> Expanding list-style shorthand (%ld longhands)\n", RHASH_SIZE(expanded));
546
- } else if (strcmp(prop_cstr, "border-top") == 0) {
547
- expanded = cataract_expand_border_side(Qnil, STR_NEW_CSTR("top"), value);
548
- DEBUG_PRINTF(" -> Expanding border-top shorthand (%ld longhands)\n", RHASH_SIZE(expanded));
549
- } else if (strcmp(prop_cstr, "border-right") == 0) {
550
- expanded = cataract_expand_border_side(Qnil, STR_NEW_CSTR("right"), value);
551
- DEBUG_PRINTF(" -> Expanding border-right shorthand (%ld longhands)\n", RHASH_SIZE(expanded));
552
- } else if (strcmp(prop_cstr, "border-bottom") == 0) {
553
- expanded = cataract_expand_border_side(Qnil, STR_NEW_CSTR("bottom"), value);
554
- DEBUG_PRINTF(" -> Expanding border-bottom shorthand (%ld longhands)\n", RHASH_SIZE(expanded));
555
- } else if (strcmp(prop_cstr, "border-left") == 0) {
556
- expanded = cataract_expand_border_side(Qnil, STR_NEW_CSTR("left"), value);
557
- DEBUG_PRINTF(" -> Expanding border-left shorthand (%ld longhands)\n", RHASH_SIZE(expanded));
710
+ // Early exit: shorthand properties only start with m, p, b, f, or l
711
+ char first_char = prop_cstr[0];
712
+ if (first_char == 'm' || first_char == 'p' || first_char == 'b' ||
713
+ first_char == 'f' || first_char == 'l') {
714
+ // Potentially a shorthand - check specific property names
715
+ if (strcmp(prop_cstr, "margin") == 0) {
716
+ expanded = cataract_expand_margin(Qnil, value);
717
+ DEBUG_PRINTF(" -> Expanding margin shorthand (%ld longhands)\n", RARRAY_LEN(expanded));
718
+ } else if (strcmp(prop_cstr, "padding") == 0) {
719
+ expanded = cataract_expand_padding(Qnil, value);
720
+ DEBUG_PRINTF(" -> Expanding padding shorthand (%ld longhands)\n", RARRAY_LEN(expanded));
721
+ } else if (strcmp(prop_cstr, "background") == 0) {
722
+ expanded = cataract_expand_background(Qnil, value);
723
+ DEBUG_PRINTF(" -> Expanding background shorthand (%ld longhands)\n", RARRAY_LEN(expanded));
724
+ } else if (strcmp(prop_cstr, "font") == 0) {
725
+ expanded = cataract_expand_font(Qnil, value);
726
+ DEBUG_PRINTF(" -> Expanding font shorthand (%ld longhands)\n", RARRAY_LEN(expanded));
727
+ } else if (strcmp(prop_cstr, "border") == 0) {
728
+ expanded = cataract_expand_border(Qnil, value);
729
+ DEBUG_PRINTF(" -> Expanding border shorthand (%ld longhands)\n", RARRAY_LEN(expanded));
730
+ } else if (strcmp(prop_cstr, "border-color") == 0) {
731
+ expanded = cataract_expand_border_color(Qnil, value);
732
+ DEBUG_PRINTF(" -> Expanding border-color shorthand (%ld longhands)\n", RARRAY_LEN(expanded));
733
+ } else if (strcmp(prop_cstr, "border-style") == 0) {
734
+ expanded = cataract_expand_border_style(Qnil, value);
735
+ DEBUG_PRINTF(" -> Expanding border-style shorthand (%ld longhands)\n", RARRAY_LEN(expanded));
736
+ } else if (strcmp(prop_cstr, "border-width") == 0) {
737
+ expanded = cataract_expand_border_width(Qnil, value);
738
+ DEBUG_PRINTF(" -> Expanding border-width shorthand (%ld longhands)\n", RARRAY_LEN(expanded));
739
+ } else if (strcmp(prop_cstr, "list-style") == 0) {
740
+ expanded = cataract_expand_list_style(Qnil, value);
741
+ DEBUG_PRINTF(" -> Expanding list-style shorthand (%ld longhands)\n", RARRAY_LEN(expanded));
742
+ } else if (strcmp(prop_cstr, "border-top") == 0) {
743
+ expanded = cataract_expand_border_side(Qnil, STR_NEW_CSTR("top"), value);
744
+ DEBUG_PRINTF(" -> Expanding border-top shorthand (%ld longhands)\n", RARRAY_LEN(expanded));
745
+ } else if (strcmp(prop_cstr, "border-right") == 0) {
746
+ expanded = cataract_expand_border_side(Qnil, STR_NEW_CSTR("right"), value);
747
+ DEBUG_PRINTF(" -> Expanding border-right shorthand (%ld longhands)\n", RARRAY_LEN(expanded));
748
+ } else if (strcmp(prop_cstr, "border-bottom") == 0) {
749
+ expanded = cataract_expand_border_side(Qnil, STR_NEW_CSTR("bottom"), value);
750
+ DEBUG_PRINTF(" -> Expanding border-bottom shorthand (%ld longhands)\n", RARRAY_LEN(expanded));
751
+ } else if (strcmp(prop_cstr, "border-left") == 0) {
752
+ expanded = cataract_expand_border_side(Qnil, STR_NEW_CSTR("left"), value);
753
+ DEBUG_PRINTF(" -> Expanding border-left shorthand (%ld longhands)\n", RARRAY_LEN(expanded));
754
+ }
558
755
  }
756
+ // If first_char doesn't match, expanded stays Qnil and we skip to processing original property
559
757
 
560
758
  // Process expanded properties or the original property
561
- if (!NIL_P(expanded) && RHASH_SIZE(expanded) > 0) {
562
- // Use rb_hash_foreach to iterate over expanded properties
759
+ if (!NIL_P(expanded) && RARRAY_LEN(expanded) > 0) {
760
+ // Iterate over expanded Declaration array
563
761
  struct expand_property_data expand_data = {
564
762
  .properties_hash = properties_hash,
565
- .specificity = specificity,
763
+ .selector = selector,
764
+ .specificity = -1, // Lazy: calculated only when needed
566
765
  .is_important = is_important,
567
766
  .source_order = source_order
568
767
  };
569
- rb_hash_foreach(expanded, process_expanded_property, (VALUE)&expand_data);
768
+ long expanded_len = RARRAY_LEN(expanded);
769
+ for (long i = 0; i < expanded_len; i++) {
770
+ VALUE decl = rb_ary_entry(expanded, i);
771
+ VALUE prop = rb_struct_aref(decl, INT2FIX(DECL_PROPERTY));
772
+ VALUE val = rb_struct_aref(decl, INT2FIX(DECL_VALUE));
773
+ process_expanded_property(prop, val, (VALUE)&expand_data);
774
+ }
570
775
  } else {
571
776
  // No expansion - process the original property directly
572
777
  struct expand_property_data expand_data = {
573
778
  .properties_hash = properties_hash,
574
- .specificity = specificity,
779
+ .selector = selector,
780
+ .specificity = -1, // Lazy: calculated only when needed
575
781
  .is_important = is_important,
576
782
  .source_order = source_order
577
783
  };
@@ -586,164 +792,11 @@ static VALUE merge_rules_for_selector(VALUE rules_array, VALUE rule_indices, VAL
586
792
  }
587
793
 
588
794
  // Recreate shorthands where possible (reduces output size)
589
- DEBUG_PRINTF(" [merge_rules_for_selector] Recreating shorthands...\n");
590
-
591
- // Try to recreate margin shorthand (if all 4 sides present)
592
- RECREATE_DIMENSION_SHORTHAND(properties_hash, "margin", cataract_create_margin_shorthand);
593
-
594
- // Try to recreate padding shorthand (if all 4 sides present)
595
- RECREATE_DIMENSION_SHORTHAND(properties_hash, "padding", cataract_create_padding_shorthand);
596
-
597
- // Try to recreate border-width shorthand (if all 4 sides present)
598
- {
599
- VALUE top = rb_hash_aref(properties_hash, STR_NEW_CSTR("border-top-width"));
600
- VALUE right = rb_hash_aref(properties_hash, STR_NEW_CSTR("border-right-width"));
601
- VALUE bottom = rb_hash_aref(properties_hash, STR_NEW_CSTR("border-bottom-width"));
602
- VALUE left = rb_hash_aref(properties_hash, STR_NEW_CSTR("border-left-width"));
603
-
604
- if (!NIL_P(top) && !NIL_P(right) && !NIL_P(bottom) && !NIL_P(left)) {
605
- VALUE top_imp = rb_hash_aref(top, ID2SYM(id_important));
606
- VALUE right_imp = rb_hash_aref(right, ID2SYM(id_important));
607
- VALUE bottom_imp = rb_hash_aref(bottom, ID2SYM(id_important));
608
- VALUE left_imp = rb_hash_aref(left, ID2SYM(id_important));
609
-
610
- if (RTEST(top_imp) == RTEST(right_imp) && RTEST(top_imp) == RTEST(bottom_imp) && RTEST(top_imp) == RTEST(left_imp)) {
611
- VALUE props = rb_hash_new();
612
- rb_hash_aset(props, STR_NEW_CSTR("border-top-width"), rb_hash_aref(top, ID2SYM(id_value)));
613
- rb_hash_aset(props, STR_NEW_CSTR("border-right-width"), rb_hash_aref(right, ID2SYM(id_value)));
614
- rb_hash_aset(props, STR_NEW_CSTR("border-bottom-width"), rb_hash_aref(bottom, ID2SYM(id_value)));
615
- rb_hash_aset(props, STR_NEW_CSTR("border-left-width"), rb_hash_aref(left, ID2SYM(id_value)));
616
-
617
- VALUE shorthand_value = cataract_create_border_width_shorthand(Qnil, props);
618
- if (!NIL_P(shorthand_value)) {
619
- VALUE shorthand_data = rb_hash_new();
620
- rb_hash_aset(shorthand_data, ID2SYM(id_value), shorthand_value);
621
- rb_hash_aset(shorthand_data, ID2SYM(id_specificity), rb_hash_aref(top, ID2SYM(id_specificity)));
622
- rb_hash_aset(shorthand_data, ID2SYM(id_important), top_imp);
623
- rb_hash_aset(properties_hash, USASCII_STR("border-width"), shorthand_data);
624
-
625
- rb_hash_delete(properties_hash, STR_NEW_CSTR("border-top-width"));
626
- rb_hash_delete(properties_hash, STR_NEW_CSTR("border-right-width"));
627
- rb_hash_delete(properties_hash, STR_NEW_CSTR("border-bottom-width"));
628
- rb_hash_delete(properties_hash, STR_NEW_CSTR("border-left-width"));
629
- DEBUG_PRINTF(" -> Recreated border-width shorthand\n");
630
- }
631
- }
632
- }
633
- }
634
-
635
- // Try to recreate border-style shorthand (if all 4 sides present)
636
- {
637
- VALUE top = rb_hash_aref(properties_hash, STR_NEW_CSTR("border-top-style"));
638
- VALUE right = rb_hash_aref(properties_hash, STR_NEW_CSTR("border-right-style"));
639
- VALUE bottom = rb_hash_aref(properties_hash, STR_NEW_CSTR("border-bottom-style"));
640
- VALUE left = rb_hash_aref(properties_hash, STR_NEW_CSTR("border-left-style"));
641
-
642
- if (!NIL_P(top) && !NIL_P(right) && !NIL_P(bottom) && !NIL_P(left)) {
643
- VALUE top_imp = rb_hash_aref(top, ID2SYM(id_important));
644
- VALUE right_imp = rb_hash_aref(right, ID2SYM(id_important));
645
- VALUE bottom_imp = rb_hash_aref(bottom, ID2SYM(id_important));
646
- VALUE left_imp = rb_hash_aref(left, ID2SYM(id_important));
647
-
648
- if (RTEST(top_imp) == RTEST(right_imp) && RTEST(top_imp) == RTEST(bottom_imp) && RTEST(top_imp) == RTEST(left_imp)) {
649
- VALUE props = rb_hash_new();
650
- rb_hash_aset(props, STR_NEW_CSTR("border-top-style"), rb_hash_aref(top, ID2SYM(id_value)));
651
- rb_hash_aset(props, STR_NEW_CSTR("border-right-style"), rb_hash_aref(right, ID2SYM(id_value)));
652
- rb_hash_aset(props, STR_NEW_CSTR("border-bottom-style"), rb_hash_aref(bottom, ID2SYM(id_value)));
653
- rb_hash_aset(props, STR_NEW_CSTR("border-left-style"), rb_hash_aref(left, ID2SYM(id_value)));
795
+ DEBUG_PRINTF(" [flatten_rules_for_selector] Recreating shorthands...\n");
654
796
 
655
- VALUE shorthand_value = cataract_create_border_style_shorthand(Qnil, props);
656
- if (!NIL_P(shorthand_value)) {
657
- VALUE shorthand_data = rb_hash_new();
658
- rb_hash_aset(shorthand_data, ID2SYM(id_value), shorthand_value);
659
- rb_hash_aset(shorthand_data, ID2SYM(id_specificity), rb_hash_aref(top, ID2SYM(id_specificity)));
660
- rb_hash_aset(shorthand_data, ID2SYM(id_important), top_imp);
661
- rb_hash_aset(properties_hash, USASCII_STR("border-style"), shorthand_data);
662
-
663
- rb_hash_delete(properties_hash, STR_NEW_CSTR("border-top-style"));
664
- rb_hash_delete(properties_hash, STR_NEW_CSTR("border-right-style"));
665
- rb_hash_delete(properties_hash, STR_NEW_CSTR("border-bottom-style"));
666
- rb_hash_delete(properties_hash, STR_NEW_CSTR("border-left-style"));
667
- DEBUG_PRINTF(" -> Recreated border-style shorthand\n");
668
- }
669
- }
670
- }
671
- }
672
-
673
- // Try to recreate border-color shorthand (if all 4 sides present)
674
- {
675
- VALUE top = rb_hash_aref(properties_hash, STR_NEW_CSTR("border-top-color"));
676
- VALUE right = rb_hash_aref(properties_hash, STR_NEW_CSTR("border-right-color"));
677
- VALUE bottom = rb_hash_aref(properties_hash, STR_NEW_CSTR("border-bottom-color"));
678
- VALUE left = rb_hash_aref(properties_hash, STR_NEW_CSTR("border-left-color"));
679
-
680
- if (!NIL_P(top) && !NIL_P(right) && !NIL_P(bottom) && !NIL_P(left)) {
681
- VALUE top_imp = rb_hash_aref(top, ID2SYM(id_important));
682
- VALUE right_imp = rb_hash_aref(right, ID2SYM(id_important));
683
- VALUE bottom_imp = rb_hash_aref(bottom, ID2SYM(id_important));
684
- VALUE left_imp = rb_hash_aref(left, ID2SYM(id_important));
685
-
686
- if (RTEST(top_imp) == RTEST(right_imp) && RTEST(top_imp) == RTEST(bottom_imp) && RTEST(top_imp) == RTEST(left_imp)) {
687
- VALUE props = rb_hash_new();
688
- rb_hash_aset(props, STR_NEW_CSTR("border-top-color"), rb_hash_aref(top, ID2SYM(id_value)));
689
- rb_hash_aset(props, STR_NEW_CSTR("border-right-color"), rb_hash_aref(right, ID2SYM(id_value)));
690
- rb_hash_aset(props, STR_NEW_CSTR("border-bottom-color"), rb_hash_aref(bottom, ID2SYM(id_value)));
691
- rb_hash_aset(props, STR_NEW_CSTR("border-left-color"), rb_hash_aref(left, ID2SYM(id_value)));
692
-
693
- VALUE shorthand_value = cataract_create_border_color_shorthand(Qnil, props);
694
- if (!NIL_P(shorthand_value)) {
695
- VALUE shorthand_data = rb_hash_new();
696
- rb_hash_aset(shorthand_data, ID2SYM(id_value), shorthand_value);
697
- rb_hash_aset(shorthand_data, ID2SYM(id_specificity), rb_hash_aref(top, ID2SYM(id_specificity)));
698
- rb_hash_aset(shorthand_data, ID2SYM(id_important), top_imp);
699
- rb_hash_aset(properties_hash, USASCII_STR("border-color"), shorthand_data);
700
-
701
- rb_hash_delete(properties_hash, STR_NEW_CSTR("border-top-color"));
702
- rb_hash_delete(properties_hash, STR_NEW_CSTR("border-right-color"));
703
- rb_hash_delete(properties_hash, STR_NEW_CSTR("border-bottom-color"));
704
- rb_hash_delete(properties_hash, STR_NEW_CSTR("border-left-color"));
705
- DEBUG_PRINTF(" -> Recreated border-color shorthand\n");
706
- }
707
- }
708
- }
709
- }
710
-
711
- // Try to recreate border-style shorthand (if all 4 sides present)
712
- {
713
- VALUE top = rb_hash_aref(properties_hash, STR_NEW_CSTR("border-top-style"));
714
- VALUE right = rb_hash_aref(properties_hash, STR_NEW_CSTR("border-right-style"));
715
- VALUE bottom = rb_hash_aref(properties_hash, STR_NEW_CSTR("border-bottom-style"));
716
- VALUE left = rb_hash_aref(properties_hash, STR_NEW_CSTR("border-left-style"));
717
-
718
- if (!NIL_P(top) && !NIL_P(right) && !NIL_P(bottom) && !NIL_P(left)) {
719
- VALUE top_imp = rb_hash_aref(top, ID2SYM(id_important));
720
- VALUE right_imp = rb_hash_aref(right, ID2SYM(id_important));
721
- VALUE bottom_imp = rb_hash_aref(bottom, ID2SYM(id_important));
722
- VALUE left_imp = rb_hash_aref(left, ID2SYM(id_important));
723
-
724
- if (RTEST(top_imp) == RTEST(right_imp) && RTEST(top_imp) == RTEST(bottom_imp) && RTEST(top_imp) == RTEST(left_imp)) {
725
- VALUE props = rb_hash_new();
726
- rb_hash_aset(props, STR_NEW_CSTR("border-top-style"), rb_hash_aref(top, ID2SYM(id_value)));
727
- rb_hash_aset(props, STR_NEW_CSTR("border-right-style"), rb_hash_aref(right, ID2SYM(id_value)));
728
- rb_hash_aset(props, STR_NEW_CSTR("border-bottom-style"), rb_hash_aref(bottom, ID2SYM(id_value)));
729
- rb_hash_aset(props, STR_NEW_CSTR("border-left-style"), rb_hash_aref(left, ID2SYM(id_value)));
730
-
731
- VALUE shorthand_value = cataract_create_border_style_shorthand(Qnil, props);
732
- if (!NIL_P(shorthand_value)) {
733
- VALUE shorthand_data = rb_hash_new();
734
- rb_hash_aset(shorthand_data, ID2SYM(id_value), shorthand_value);
735
- rb_hash_aset(shorthand_data, ID2SYM(id_specificity), rb_hash_aref(top, ID2SYM(id_specificity)));
736
- rb_hash_aset(shorthand_data, ID2SYM(id_important), top_imp);
737
- rb_hash_aset(properties_hash, USASCII_STR("border-style"), shorthand_data);
738
-
739
- rb_hash_delete(properties_hash, STR_NEW_CSTR("border-top-style"));
740
- rb_hash_delete(properties_hash, STR_NEW_CSTR("border-right-style"));
741
- rb_hash_delete(properties_hash, STR_NEW_CSTR("border-bottom-style"));
742
- rb_hash_delete(properties_hash, STR_NEW_CSTR("border-left-style"));
743
- DEBUG_PRINTF(" -> Recreated border-style shorthand\n");
744
- }
745
- }
746
- }
797
+ // Try to recreate all 4-sided shorthands using the mapping table
798
+ for (const struct shorthand_mapping *mapping = SHORTHAND_MAPPINGS; mapping->shorthand_name != NULL; mapping++) {
799
+ try_recreate_shorthand(properties_hash, mapping);
747
800
  }
748
801
 
749
802
  // Try to recreate full border shorthand (if border-width, border-style, border-color present)
@@ -755,23 +808,24 @@ static VALUE merge_rules_for_selector(VALUE rules_array, VALUE rule_indices, VAL
755
808
  // Need at least style (border shorthand requires style)
756
809
  if (!NIL_P(style)) {
757
810
  // Check all have same !important flag
758
- VALUE style_imp = rb_hash_aref(style, ID2SYM(id_important));
811
+ VALUE style_imp = RARRAY_AREF(style, PROP_IMPORTANT);
759
812
  int same_importance = 1;
760
- if (!NIL_P(width)) same_importance = same_importance && (RTEST(style_imp) == RTEST(rb_hash_aref(width, ID2SYM(id_important))));
761
- if (!NIL_P(color)) same_importance = same_importance && (RTEST(style_imp) == RTEST(rb_hash_aref(color, ID2SYM(id_important))));
813
+ if (!NIL_P(width)) same_importance = same_importance && (RTEST(style_imp) == RTEST(RARRAY_AREF(width, PROP_IMPORTANT)));
814
+ if (!NIL_P(color)) same_importance = same_importance && (RTEST(style_imp) == RTEST(RARRAY_AREF(color, PROP_IMPORTANT)));
762
815
 
763
816
  if (same_importance) {
764
817
  VALUE props = rb_hash_new();
765
- if (!NIL_P(width)) rb_hash_aset(props, STR_NEW_CSTR("border-width"), rb_hash_aref(width, ID2SYM(id_value)));
766
- rb_hash_aset(props, STR_NEW_CSTR("border-style"), rb_hash_aref(style, ID2SYM(id_value)));
767
- if (!NIL_P(color)) rb_hash_aset(props, STR_NEW_CSTR("border-color"), rb_hash_aref(color, ID2SYM(id_value)));
818
+ if (!NIL_P(width)) rb_hash_aset(props, STR_NEW_CSTR("border-width"), RARRAY_AREF(width, PROP_VALUE));
819
+ rb_hash_aset(props, STR_NEW_CSTR("border-style"), RARRAY_AREF(style, PROP_VALUE));
820
+ if (!NIL_P(color)) rb_hash_aset(props, STR_NEW_CSTR("border-color"), RARRAY_AREF(color, PROP_VALUE));
768
821
 
769
822
  VALUE shorthand_value = cataract_create_border_shorthand(Qnil, props);
770
823
  if (!NIL_P(shorthand_value)) {
771
- VALUE shorthand_data = rb_hash_new();
772
- rb_hash_aset(shorthand_data, ID2SYM(id_value), shorthand_value);
773
- rb_hash_aset(shorthand_data, ID2SYM(id_specificity), rb_hash_aref(style, ID2SYM(id_specificity)));
774
- rb_hash_aset(shorthand_data, ID2SYM(id_important), style_imp);
824
+ VALUE shorthand_data = rb_ary_new_capa(4);
825
+ rb_ary_push(shorthand_data, RARRAY_AREF(style, PROP_SOURCE_ORDER));
826
+ rb_ary_push(shorthand_data, RARRAY_AREF(style, PROP_SPECIFICITY));
827
+ rb_ary_push(shorthand_data, style_imp);
828
+ rb_ary_push(shorthand_data, shorthand_value);
775
829
  rb_hash_aset(properties_hash, USASCII_STR("border"), shorthand_data);
776
830
 
777
831
  rb_hash_delete(properties_hash, STR_NEW_CSTR("border-width"));
@@ -799,28 +853,29 @@ static VALUE merge_rules_for_selector(VALUE rules_array, VALUE rule_indices, VAL
799
853
  if (list_count >= 2) {
800
854
  // Check all have same !important flag
801
855
  VALUE first_imp = Qnil;
802
- if (!NIL_P(type)) first_imp = rb_hash_aref(type, ID2SYM(id_important));
803
- else if (!NIL_P(position)) first_imp = rb_hash_aref(position, ID2SYM(id_important));
804
- else if (!NIL_P(image)) first_imp = rb_hash_aref(image, ID2SYM(id_important));
856
+ if (!NIL_P(type)) first_imp = RARRAY_AREF(type, PROP_IMPORTANT);
857
+ else if (!NIL_P(position)) first_imp = RARRAY_AREF(position, PROP_IMPORTANT);
858
+ else if (!NIL_P(image)) first_imp = RARRAY_AREF(image, PROP_IMPORTANT);
805
859
 
806
860
  int same_importance = 1;
807
- if (!NIL_P(type)) same_importance = same_importance && (RTEST(first_imp) == RTEST(rb_hash_aref(type, ID2SYM(id_important))));
808
- if (!NIL_P(position)) same_importance = same_importance && (RTEST(first_imp) == RTEST(rb_hash_aref(position, ID2SYM(id_important))));
809
- if (!NIL_P(image)) same_importance = same_importance && (RTEST(first_imp) == RTEST(rb_hash_aref(image, ID2SYM(id_important))));
861
+ if (!NIL_P(type)) same_importance = same_importance && (RTEST(first_imp) == RTEST(RARRAY_AREF(type, PROP_IMPORTANT)));
862
+ if (!NIL_P(position)) same_importance = same_importance && (RTEST(first_imp) == RTEST(RARRAY_AREF(position, PROP_IMPORTANT)));
863
+ if (!NIL_P(image)) same_importance = same_importance && (RTEST(first_imp) == RTEST(RARRAY_AREF(image, PROP_IMPORTANT)));
810
864
 
811
865
  if (same_importance) {
812
866
  VALUE props = rb_hash_new();
813
- if (!NIL_P(type)) rb_hash_aset(props, STR_NEW_CSTR("list-style-type"), rb_hash_aref(type, ID2SYM(id_value)));
814
- if (!NIL_P(position)) rb_hash_aset(props, STR_NEW_CSTR("list-style-position"), rb_hash_aref(position, ID2SYM(id_value)));
815
- if (!NIL_P(image)) rb_hash_aset(props, STR_NEW_CSTR("list-style-image"), rb_hash_aref(image, ID2SYM(id_value)));
867
+ if (!NIL_P(type)) rb_hash_aset(props, STR_NEW_CSTR("list-style-type"), RARRAY_AREF(type, PROP_VALUE));
868
+ if (!NIL_P(position)) rb_hash_aset(props, STR_NEW_CSTR("list-style-position"), RARRAY_AREF(position, PROP_VALUE));
869
+ if (!NIL_P(image)) rb_hash_aset(props, STR_NEW_CSTR("list-style-image"), RARRAY_AREF(image, PROP_VALUE));
816
870
 
817
871
  VALUE shorthand_value = cataract_create_list_style_shorthand(Qnil, props);
818
872
  if (!NIL_P(shorthand_value)) {
819
- VALUE shorthand_data = rb_hash_new();
820
- rb_hash_aset(shorthand_data, ID2SYM(id_value), shorthand_value);
821
873
  VALUE first_prop = !NIL_P(type) ? type : (!NIL_P(position) ? position : image);
822
- rb_hash_aset(shorthand_data, ID2SYM(id_specificity), rb_hash_aref(first_prop, ID2SYM(id_specificity)));
823
- rb_hash_aset(shorthand_data, ID2SYM(id_important), first_imp);
874
+ VALUE shorthand_data = rb_ary_new_capa(4);
875
+ rb_ary_push(shorthand_data, RARRAY_AREF(first_prop, PROP_SOURCE_ORDER));
876
+ rb_ary_push(shorthand_data, RARRAY_AREF(first_prop, PROP_SPECIFICITY));
877
+ rb_ary_push(shorthand_data, first_imp);
878
+ rb_ary_push(shorthand_data, shorthand_value);
824
879
  rb_hash_aset(properties_hash, USASCII_STR("list-style"), shorthand_data);
825
880
 
826
881
  rb_hash_delete(properties_hash, STR_NEW_CSTR("list-style-type"));
@@ -844,30 +899,31 @@ static VALUE merge_rules_for_selector(VALUE rules_array, VALUE rule_indices, VAL
844
899
  VALUE line_height = rb_hash_aref(properties_hash, STR_NEW_CSTR("line-height"));
845
900
 
846
901
  // Check all font properties have same !important flag
847
- VALUE size_imp = rb_hash_aref(size, ID2SYM(id_important));
848
- VALUE family_imp = rb_hash_aref(family, ID2SYM(id_important));
902
+ VALUE size_imp = RARRAY_AREF(size, PROP_IMPORTANT);
903
+ VALUE family_imp = RARRAY_AREF(family, PROP_IMPORTANT);
849
904
 
850
905
  int same_importance = (RTEST(size_imp) == RTEST(family_imp));
851
- if (!NIL_P(style)) same_importance = same_importance && (RTEST(size_imp) == RTEST(rb_hash_aref(style, ID2SYM(id_important))));
852
- if (!NIL_P(variant)) same_importance = same_importance && (RTEST(size_imp) == RTEST(rb_hash_aref(variant, ID2SYM(id_important))));
853
- if (!NIL_P(weight)) same_importance = same_importance && (RTEST(size_imp) == RTEST(rb_hash_aref(weight, ID2SYM(id_important))));
854
- if (!NIL_P(line_height)) same_importance = same_importance && (RTEST(size_imp) == RTEST(rb_hash_aref(line_height, ID2SYM(id_important))));
906
+ if (!NIL_P(style)) same_importance = same_importance && (RTEST(size_imp) == RTEST(RARRAY_AREF(style, PROP_IMPORTANT)));
907
+ if (!NIL_P(variant)) same_importance = same_importance && (RTEST(size_imp) == RTEST(RARRAY_AREF(variant, PROP_IMPORTANT)));
908
+ if (!NIL_P(weight)) same_importance = same_importance && (RTEST(size_imp) == RTEST(RARRAY_AREF(weight, PROP_IMPORTANT)));
909
+ if (!NIL_P(line_height)) same_importance = same_importance && (RTEST(size_imp) == RTEST(RARRAY_AREF(line_height, PROP_IMPORTANT)));
855
910
 
856
911
  if (same_importance) {
857
912
  VALUE props = rb_hash_new();
858
- rb_hash_aset(props, STR_NEW_CSTR("font-size"), rb_hash_aref(size, ID2SYM(id_value)));
859
- rb_hash_aset(props, STR_NEW_CSTR("font-family"), rb_hash_aref(family, ID2SYM(id_value)));
860
- if (!NIL_P(style)) rb_hash_aset(props, STR_NEW_CSTR("font-style"), rb_hash_aref(style, ID2SYM(id_value)));
861
- if (!NIL_P(variant)) rb_hash_aset(props, STR_NEW_CSTR("font-variant"), rb_hash_aref(variant, ID2SYM(id_value)));
862
- if (!NIL_P(weight)) rb_hash_aset(props, STR_NEW_CSTR("font-weight"), rb_hash_aref(weight, ID2SYM(id_value)));
863
- if (!NIL_P(line_height)) rb_hash_aset(props, STR_NEW_CSTR("line-height"), rb_hash_aref(line_height, ID2SYM(id_value)));
913
+ rb_hash_aset(props, STR_NEW_CSTR("font-size"), RARRAY_AREF(size, PROP_VALUE));
914
+ rb_hash_aset(props, STR_NEW_CSTR("font-family"), RARRAY_AREF(family, PROP_VALUE));
915
+ if (!NIL_P(style)) rb_hash_aset(props, STR_NEW_CSTR("font-style"), RARRAY_AREF(style, PROP_VALUE));
916
+ if (!NIL_P(variant)) rb_hash_aset(props, STR_NEW_CSTR("font-variant"), RARRAY_AREF(variant, PROP_VALUE));
917
+ if (!NIL_P(weight)) rb_hash_aset(props, STR_NEW_CSTR("font-weight"), RARRAY_AREF(weight, PROP_VALUE));
918
+ if (!NIL_P(line_height)) rb_hash_aset(props, STR_NEW_CSTR("line-height"), RARRAY_AREF(line_height, PROP_VALUE));
864
919
 
865
920
  VALUE shorthand_value = cataract_create_font_shorthand(Qnil, props);
866
921
  if (!NIL_P(shorthand_value)) {
867
- VALUE shorthand_data = rb_hash_new();
868
- rb_hash_aset(shorthand_data, ID2SYM(id_value), shorthand_value);
869
- rb_hash_aset(shorthand_data, ID2SYM(id_specificity), rb_hash_aref(size, ID2SYM(id_specificity)));
870
- rb_hash_aset(shorthand_data, ID2SYM(id_important), size_imp);
922
+ VALUE shorthand_data = rb_ary_new_capa(4);
923
+ rb_ary_push(shorthand_data, RARRAY_AREF(size, PROP_SOURCE_ORDER));
924
+ rb_ary_push(shorthand_data, RARRAY_AREF(size, PROP_SPECIFICITY));
925
+ rb_ary_push(shorthand_data, size_imp);
926
+ rb_ary_push(shorthand_data, shorthand_value);
871
927
  rb_hash_aset(properties_hash, USASCII_STR("font"), shorthand_data);
872
928
 
873
929
  rb_hash_delete(properties_hash, STR_NEW_CSTR("font-size"));
@@ -901,34 +957,35 @@ static VALUE merge_rules_for_selector(VALUE rules_array, VALUE rule_indices, VAL
901
957
  if (bg_count >= 2) {
902
958
  // Check all have same !important flag
903
959
  VALUE first_imp = Qnil;
904
- if (!NIL_P(color)) first_imp = rb_hash_aref(color, ID2SYM(id_important));
905
- else if (!NIL_P(image)) first_imp = rb_hash_aref(image, ID2SYM(id_important));
906
- else if (!NIL_P(repeat)) first_imp = rb_hash_aref(repeat, ID2SYM(id_important));
907
- else if (!NIL_P(position)) first_imp = rb_hash_aref(position, ID2SYM(id_important));
908
- else if (!NIL_P(attachment)) first_imp = rb_hash_aref(attachment, ID2SYM(id_important));
960
+ if (!NIL_P(color)) first_imp = RARRAY_AREF(color, PROP_IMPORTANT);
961
+ else if (!NIL_P(image)) first_imp = RARRAY_AREF(image, PROP_IMPORTANT);
962
+ else if (!NIL_P(repeat)) first_imp = RARRAY_AREF(repeat, PROP_IMPORTANT);
963
+ else if (!NIL_P(position)) first_imp = RARRAY_AREF(position, PROP_IMPORTANT);
964
+ else if (!NIL_P(attachment)) first_imp = RARRAY_AREF(attachment, PROP_IMPORTANT);
909
965
 
910
966
  int same_importance = 1;
911
- if (!NIL_P(color)) same_importance = same_importance && (RTEST(first_imp) == RTEST(rb_hash_aref(color, ID2SYM(id_important))));
912
- if (!NIL_P(image)) same_importance = same_importance && (RTEST(first_imp) == RTEST(rb_hash_aref(image, ID2SYM(id_important))));
913
- if (!NIL_P(repeat)) same_importance = same_importance && (RTEST(first_imp) == RTEST(rb_hash_aref(repeat, ID2SYM(id_important))));
914
- if (!NIL_P(position)) same_importance = same_importance && (RTEST(first_imp) == RTEST(rb_hash_aref(position, ID2SYM(id_important))));
915
- if (!NIL_P(attachment)) same_importance = same_importance && (RTEST(first_imp) == RTEST(rb_hash_aref(attachment, ID2SYM(id_important))));
967
+ if (!NIL_P(color)) same_importance = same_importance && (RTEST(first_imp) == RTEST(RARRAY_AREF(color, PROP_IMPORTANT)));
968
+ if (!NIL_P(image)) same_importance = same_importance && (RTEST(first_imp) == RTEST(RARRAY_AREF(image, PROP_IMPORTANT)));
969
+ if (!NIL_P(repeat)) same_importance = same_importance && (RTEST(first_imp) == RTEST(RARRAY_AREF(repeat, PROP_IMPORTANT)));
970
+ if (!NIL_P(position)) same_importance = same_importance && (RTEST(first_imp) == RTEST(RARRAY_AREF(position, PROP_IMPORTANT)));
971
+ if (!NIL_P(attachment)) same_importance = same_importance && (RTEST(first_imp) == RTEST(RARRAY_AREF(attachment, PROP_IMPORTANT)));
916
972
 
917
973
  if (same_importance) {
918
974
  VALUE props = rb_hash_new();
919
- if (!NIL_P(color)) rb_hash_aset(props, STR_NEW_CSTR("background-color"), rb_hash_aref(color, ID2SYM(id_value)));
920
- if (!NIL_P(image)) rb_hash_aset(props, STR_NEW_CSTR("background-image"), rb_hash_aref(image, ID2SYM(id_value)));
921
- if (!NIL_P(repeat)) rb_hash_aset(props, STR_NEW_CSTR("background-repeat"), rb_hash_aref(repeat, ID2SYM(id_value)));
922
- if (!NIL_P(position)) rb_hash_aset(props, STR_NEW_CSTR("background-position"), rb_hash_aref(position, ID2SYM(id_value)));
923
- if (!NIL_P(attachment)) rb_hash_aset(props, STR_NEW_CSTR("background-attachment"), rb_hash_aref(attachment, ID2SYM(id_value)));
975
+ if (!NIL_P(color)) rb_hash_aset(props, STR_NEW_CSTR("background-color"), RARRAY_AREF(color, PROP_VALUE));
976
+ if (!NIL_P(image)) rb_hash_aset(props, STR_NEW_CSTR("background-image"), RARRAY_AREF(image, PROP_VALUE));
977
+ if (!NIL_P(repeat)) rb_hash_aset(props, STR_NEW_CSTR("background-repeat"), RARRAY_AREF(repeat, PROP_VALUE));
978
+ if (!NIL_P(position)) rb_hash_aset(props, STR_NEW_CSTR("background-position"), RARRAY_AREF(position, PROP_VALUE));
979
+ if (!NIL_P(attachment)) rb_hash_aset(props, STR_NEW_CSTR("background-attachment"), RARRAY_AREF(attachment, PROP_VALUE));
924
980
 
925
981
  VALUE shorthand_value = cataract_create_background_shorthand(Qnil, props);
926
982
  if (!NIL_P(shorthand_value)) {
927
- VALUE shorthand_data = rb_hash_new();
928
- rb_hash_aset(shorthand_data, ID2SYM(id_value), shorthand_value);
929
983
  VALUE first_prop = !NIL_P(color) ? color : (!NIL_P(image) ? image : (!NIL_P(repeat) ? repeat : (!NIL_P(position) ? position : attachment)));
930
- rb_hash_aset(shorthand_data, ID2SYM(id_specificity), rb_hash_aref(first_prop, ID2SYM(id_specificity)));
931
- rb_hash_aset(shorthand_data, ID2SYM(id_important), first_imp);
984
+ VALUE shorthand_data = rb_ary_new_capa(4);
985
+ rb_ary_push(shorthand_data, RARRAY_AREF(first_prop, PROP_SOURCE_ORDER));
986
+ rb_ary_push(shorthand_data, RARRAY_AREF(first_prop, PROP_SPECIFICITY));
987
+ rb_ary_push(shorthand_data, first_imp);
988
+ rb_ary_push(shorthand_data, shorthand_value);
932
989
  rb_hash_aset(properties_hash, USASCII_STR("background"), shorthand_data);
933
990
 
934
991
  rb_hash_delete(properties_hash, STR_NEW_CSTR("background-color"));
@@ -950,18 +1007,18 @@ static VALUE merge_rules_for_selector(VALUE rules_array, VALUE rule_indices, VAL
950
1007
  // The output order is roughly source order but may vary when properties are
951
1008
  // overridden by later rules with higher specificity or importance.
952
1009
  VALUE merged_decls = rb_ary_new();
953
- rb_hash_foreach(properties_hash, merge_build_result_callback, merged_decls);
1010
+ rb_hash_foreach(properties_hash, flatten_build_result_callback, merged_decls);
954
1011
 
955
- DEBUG_PRINTF(" [merge_rules_for_selector] Result: %ld merged declarations\n",
1012
+ DEBUG_PRINTF(" [flatten_rules_for_selector] Result: %ld merged declarations\n",
956
1013
  RARRAY_LEN(merged_decls));
957
1014
 
958
1015
  return merged_decls;
959
1016
  }
960
1017
 
961
- // Merge CSS rules according to cascade rules
1018
+ // Flatten CSS rules by applying cascade rules
962
1019
  // Input: Stylesheet object or CSS string
963
- // Output: Stylesheet with merged declarations
964
- VALUE cataract_merge_new(VALUE self, VALUE input) {
1020
+ // Output: Stylesheet with flattened declarations (cascade applied)
1021
+ VALUE cataract_flatten(VALUE self, VALUE input) {
965
1022
  VALUE rules_array;
966
1023
 
967
1024
  // Handle different input types
@@ -987,14 +1044,6 @@ VALUE cataract_merge_new(VALUE self, VALUE input) {
987
1044
  has_nesting = RTEST(has_nesting_ivar);
988
1045
  }
989
1046
 
990
- // Initialize cached symbol IDs on first call (thread-safe since GVL is held)
991
- // This only happens once, so unlikely
992
- if (id_value == 0) {
993
- id_value = rb_intern("value");
994
- id_specificity = rb_intern("specificity");
995
- id_important = rb_intern("important");
996
- }
997
-
998
1047
  long num_rules = RARRAY_LEN(rules_array);
999
1048
  // Empty stylesheets are rare
1000
1049
  if (num_rules == 0) {
@@ -1005,13 +1054,13 @@ VALUE cataract_merge_new(VALUE self, VALUE input) {
1005
1054
 
1006
1055
  /*
1007
1056
  * ============================================================================
1008
- * MERGE ALGORITHM - Rules and Implementation Notes
1057
+ * FLATTEN ALGORITHM - Rules and Implementation Notes
1009
1058
  * ============================================================================
1010
1059
  *
1011
- * CORE PRINCIPLE: Group rules by selector, merge declarations within each group
1060
+ * CORE PRINCIPLE: Group rules by selector, flatten declarations within each group
1012
1061
  *
1013
1062
  * Different selectors (.test vs #test) target different elements and must stay separate.
1014
- * Same selectors should merge into one rule to reduce output size.
1063
+ * Same selectors should flatten into one rule to reduce output size.
1015
1064
  *
1016
1065
  * ALGORITHM STEPS:
1017
1066
  * 1. Group rules by selector (.test, #test, etc.)
@@ -1031,7 +1080,7 @@ VALUE cataract_merge_new(VALUE self, VALUE input) {
1031
1080
  * This ensures declarations within the same rule maintain relative order.
1032
1081
  *
1033
1082
  * SHORTHAND EXPANSION:
1034
- * When merging, all shorthands must be expanded to longhands first.
1083
+ * When flattening, all shorthands must be expanded to longhands first.
1035
1084
  * Example: "background: blue" expands to:
1036
1085
  * - background-color: blue
1037
1086
  * - background-image: none
@@ -1061,9 +1110,9 @@ VALUE cataract_merge_new(VALUE self, VALUE input) {
1061
1110
  * (The "none", "repeat", etc. are just defaults from expansion)
1062
1111
  *
1063
1112
  * EDGE CASES:
1064
- * - Empty rules (no declarations): Skip during merge
1113
+ * - Empty rules (no declarations): Skip during flatten
1065
1114
  * - Nested CSS: Parent rules with children are containers only, skip their declarations
1066
- * - Mixed !important: Properties with different importance cannot merge into shorthand
1115
+ * - Mixed !important: Properties with different importance cannot flatten into shorthand
1067
1116
  * - Single property: Don't create shorthand (e.g., background-color alone stays as-is)
1068
1117
  * Reason: "background: blue" resets all other background properties to defaults,
1069
1118
  * which is semantically different from just setting background-color.
@@ -1071,16 +1120,16 @@ VALUE cataract_merge_new(VALUE self, VALUE input) {
1071
1120
  * PERFORMANCE NOTES:
1072
1121
  * - Use cached static strings (VALUE) for property names (no allocation)
1073
1122
  * - Group by selector in single pass (O(n) hash building)
1074
- * - Merge within groups (O(n*m) where m is avg declarations per rule)
1123
+ * - Flatten within groups (O(n*m) where m is avg declarations per rule)
1075
1124
  * ============================================================================
1076
1125
  */
1077
1126
 
1078
1127
  // For nested CSS: identify parent rules (rules that have children)
1079
- // These should be skipped during merge, even if they have declarations
1128
+ // These should be skipped during flatten, even if they have declarations
1080
1129
  // Use Ruby hash as a set: parent_id => true
1081
1130
  VALUE parent_ids = Qnil;
1082
1131
  if (has_nesting) {
1083
- DEBUG_PRINTF("\n=== MERGE: has_nesting=true, num_rules=%ld ===\n", num_rules);
1132
+ DEBUG_PRINTF("\n=== FLATTEN: has_nesting=true, num_rules=%ld ===\n", num_rules);
1084
1133
  parent_ids = rb_hash_new();
1085
1134
  for (long i = 0; i < num_rules; i++) {
1086
1135
  VALUE rule = RARRAY_AREF(rules_array, i);
@@ -1097,7 +1146,7 @@ VALUE cataract_merge_new(VALUE self, VALUE input) {
1097
1146
  }
1098
1147
  }
1099
1148
 
1100
- // ALWAYS build selector groups - this is the core of merge logic
1149
+ // ALWAYS build selector groups - this is the core of flatten logic
1101
1150
  // Group rules by selector: different selectors stay separate
1102
1151
  // selector => [rule indices]
1103
1152
  DEBUG_PRINTF("\n=== Building selector groups (has_nesting=%d) ===\n", has_nesting);
@@ -1181,32 +1230,18 @@ VALUE cataract_merge_new(VALUE self, VALUE input) {
1181
1230
  VALUE merged_rules = rb_ary_new();
1182
1231
  int rule_id_counter = 0;
1183
1232
 
1184
- // Iterate through each selector group
1185
- VALUE selectors = rb_funcall(selector_groups, rb_intern("keys"), 0);
1186
- long num_selectors = RARRAY_LEN(selectors);
1187
- DEBUG_PRINTF("\n=== Processing %ld selector groups ===\n", num_selectors);
1188
-
1189
- for (long s = 0; s < num_selectors; s++) {
1190
- VALUE selector = rb_ary_entry(selectors, s);
1191
- VALUE group_indices = rb_hash_aref(selector_groups, selector);
1192
-
1193
- DEBUG_PRINTF("\n[Selector %ld/%ld] '%s' - %ld rules in group\n",
1194
- s + 1, num_selectors, RSTRING_PTR(selector), RARRAY_LEN(group_indices));
1195
-
1196
- // Merge all rules in this selector group
1197
- VALUE merged_decls = merge_rules_for_selector(rules_array, group_indices, selector);
1198
-
1199
- // Create new rule with this selector and merged declarations
1200
- VALUE new_rule = rb_struct_new(cRule,
1201
- INT2FIX(rule_id_counter++),
1202
- selector,
1203
- merged_decls,
1204
- Qnil, // specificity
1205
- Qnil, // parent_rule_id
1206
- Qnil // nesting_style
1207
- );
1208
- rb_ary_push(merged_rules, new_rule);
1209
- }
1233
+ // Iterate through each selector group using rb_hash_foreach
1234
+ // to avoid rb_funcall in hot path
1235
+ struct flatten_selectors_context merge_ctx;
1236
+ merge_ctx.merged_rules = merged_rules;
1237
+ merge_ctx.rules_array = rules_array;
1238
+ merge_ctx.rule_id_counter = &rule_id_counter;
1239
+ merge_ctx.selector_index = 0;
1240
+ merge_ctx.total_selectors = RHASH_SIZE(selector_groups);
1241
+
1242
+ DEBUG_PRINTF("\n=== Processing %ld selector groups ===\n", merge_ctx.total_selectors);
1243
+
1244
+ rb_hash_foreach(selector_groups, flatten_selector_group_callback, (VALUE)&merge_ctx);
1210
1245
 
1211
1246
  // Add passthrough AtRules to output (preserve @keyframes, @font-face, etc.)
1212
1247
  long num_passthrough = RARRAY_LEN(passthrough_rules);
@@ -1246,6 +1281,9 @@ VALUE cataract_merge_new(VALUE self, VALUE input) {
1246
1281
  VALUE first_selector_value = Qnil;
1247
1282
  int all_same_selector = 1;
1248
1283
 
1284
+ // Track source order for cascade rules
1285
+ long source_order = 0;
1286
+
1249
1287
  // Iterate through each rule
1250
1288
  for (long i = 0; i < num_rules; i++) {
1251
1289
  VALUE rule = RARRAY_AREF(rules_array, i);
@@ -1317,45 +1355,59 @@ VALUE cataract_merge_new(VALUE self, VALUE input) {
1317
1355
  const char *prop_str = StringValueCStr(property);
1318
1356
  VALUE expanded = Qnil;
1319
1357
 
1320
- if (strcmp(prop_str, "margin") == 0) {
1321
- expanded = cataract_expand_margin(Qnil, value);
1322
- } else if (strcmp(prop_str, "padding") == 0) {
1323
- expanded = cataract_expand_padding(Qnil, value);
1324
- } else if (strcmp(prop_str, "border") == 0) {
1325
- expanded = cataract_expand_border(Qnil, value);
1326
- } else if (strcmp(prop_str, "border-color") == 0) {
1327
- expanded = cataract_expand_border_color(Qnil, value);
1328
- } else if (strcmp(prop_str, "border-style") == 0) {
1329
- expanded = cataract_expand_border_style(Qnil, value);
1330
- } else if (strcmp(prop_str, "border-width") == 0) {
1331
- expanded = cataract_expand_border_width(Qnil, value);
1332
- } else if (strcmp(prop_str, "border-top") == 0) {
1333
- expanded = cataract_expand_border_side(Qnil, USASCII_STR("top"), value);
1334
- } else if (strcmp(prop_str, "border-right") == 0) {
1335
- expanded = cataract_expand_border_side(Qnil, USASCII_STR("right"), value);
1336
- } else if (strcmp(prop_str, "border-bottom") == 0) {
1337
- expanded = cataract_expand_border_side(Qnil, USASCII_STR("bottom"), value);
1338
- } else if (strcmp(prop_str, "border-left") == 0) {
1339
- expanded = cataract_expand_border_side(Qnil, USASCII_STR("left"), value);
1340
- } else if (strcmp(prop_str, "font") == 0) {
1341
- expanded = cataract_expand_font(Qnil, value);
1342
- } else if (strcmp(prop_str, "list-style") == 0) {
1343
- expanded = cataract_expand_list_style(Qnil, value);
1344
- } else if (strcmp(prop_str, "background") == 0) {
1345
- expanded = cataract_expand_background(Qnil, value);
1358
+ // Early exit: shorthand properties only start with m, p, b, f, or l
1359
+ char first_char = prop_str[0];
1360
+ if (first_char == 'm' || first_char == 'p' || first_char == 'b' ||
1361
+ first_char == 'f' || first_char == 'l') {
1362
+ // Potentially a shorthand - check specific property names
1363
+ if (strcmp(prop_str, "margin") == 0) {
1364
+ expanded = cataract_expand_margin(Qnil, value);
1365
+ } else if (strcmp(prop_str, "padding") == 0) {
1366
+ expanded = cataract_expand_padding(Qnil, value);
1367
+ } else if (strcmp(prop_str, "border") == 0) {
1368
+ expanded = cataract_expand_border(Qnil, value);
1369
+ } else if (strcmp(prop_str, "border-color") == 0) {
1370
+ expanded = cataract_expand_border_color(Qnil, value);
1371
+ } else if (strcmp(prop_str, "border-style") == 0) {
1372
+ expanded = cataract_expand_border_style(Qnil, value);
1373
+ } else if (strcmp(prop_str, "border-width") == 0) {
1374
+ expanded = cataract_expand_border_width(Qnil, value);
1375
+ } else if (strcmp(prop_str, "border-top") == 0) {
1376
+ expanded = cataract_expand_border_side(Qnil, USASCII_STR("top"), value);
1377
+ } else if (strcmp(prop_str, "border-right") == 0) {
1378
+ expanded = cataract_expand_border_side(Qnil, USASCII_STR("right"), value);
1379
+ } else if (strcmp(prop_str, "border-bottom") == 0) {
1380
+ expanded = cataract_expand_border_side(Qnil, USASCII_STR("bottom"), value);
1381
+ } else if (strcmp(prop_str, "border-left") == 0) {
1382
+ expanded = cataract_expand_border_side(Qnil, USASCII_STR("left"), value);
1383
+ } else if (strcmp(prop_str, "font") == 0) {
1384
+ expanded = cataract_expand_font(Qnil, value);
1385
+ } else if (strcmp(prop_str, "list-style") == 0) {
1386
+ expanded = cataract_expand_list_style(Qnil, value);
1387
+ } else if (strcmp(prop_str, "background") == 0) {
1388
+ expanded = cataract_expand_background(Qnil, value);
1389
+ }
1346
1390
  }
1391
+ // If first_char doesn't match, expanded stays Qnil
1347
1392
 
1348
- // If property was expanded, iterate and apply cascade using rb_hash_foreach
1393
+ // If property was expanded, iterate array and apply cascade
1349
1394
  // Expansion is rare (most properties are not shorthands)
1350
1395
  if (!NIL_P(expanded)) {
1351
- Check_Type(expanded, T_HASH);
1396
+ Check_Type(expanded, T_ARRAY);
1352
1397
 
1353
1398
  struct expand_context ctx;
1354
1399
  ctx.properties_hash = properties_hash;
1400
+ ctx.source_order = source_order;
1355
1401
  ctx.specificity = specificity;
1356
1402
  ctx.important = important;
1357
1403
 
1358
- rb_hash_foreach(expanded, merge_expanded_callback, (VALUE)&ctx);
1404
+ long expanded_len = RARRAY_LEN(expanded);
1405
+ for (long i = 0; i < expanded_len; i++) {
1406
+ VALUE decl = rb_ary_entry(expanded, i);
1407
+ VALUE prop = rb_struct_aref(decl, INT2FIX(DECL_PROPERTY));
1408
+ VALUE val = rb_struct_aref(decl, INT2FIX(DECL_VALUE));
1409
+ flatten_expanded_callback(prop, val, (VALUE)&ctx);
1410
+ }
1359
1411
 
1360
1412
  RB_GC_GUARD(expanded);
1361
1413
  continue; // Skip processing the original shorthand property
@@ -1367,47 +1419,51 @@ VALUE cataract_merge_new(VALUE self, VALUE input) {
1367
1419
  // In merge scenarios, properties often collide (same property in multiple rules)
1368
1420
  // so existing property is the common case
1369
1421
  if (NIL_P(existing)) {
1370
- // New property - add it
1371
- VALUE prop_data = rb_hash_new();
1372
- rb_hash_aset(prop_data, ID2SYM(id_value), value);
1373
- rb_hash_aset(prop_data, ID2SYM(id_specificity), INT2NUM(specificity));
1374
- rb_hash_aset(prop_data, ID2SYM(id_important), important);
1375
- // Note: declaration_struct not stored - use global cDeclaration instead
1422
+ // New property - add it as array: [source_order, specificity, important, value]
1423
+ VALUE prop_data = rb_ary_new_capa(4);
1424
+ rb_ary_push(prop_data, LONG2NUM(source_order));
1425
+ rb_ary_push(prop_data, INT2NUM(specificity));
1426
+ rb_ary_push(prop_data, important);
1427
+ rb_ary_push(prop_data, value);
1376
1428
  rb_hash_aset(properties_hash, property, prop_data);
1377
1429
  } else {
1378
1430
  // Property exists - check cascade rules
1379
- VALUE existing_spec = rb_hash_aref(existing, ID2SYM(id_specificity));
1380
- VALUE existing_important = rb_hash_aref(existing, ID2SYM(id_important));
1381
-
1382
- int existing_spec_int = NUM2INT(existing_spec);
1431
+ long existing_order = NUM2LONG(RARRAY_AREF(existing, PROP_SOURCE_ORDER));
1432
+ int existing_spec_int = NUM2INT(RARRAY_AREF(existing, PROP_SPECIFICITY));
1433
+ VALUE existing_important = RARRAY_AREF(existing, PROP_IMPORTANT);
1383
1434
  int existing_is_important = RTEST(existing_important);
1384
1435
 
1385
1436
  int should_replace = 0;
1386
1437
 
1387
1438
  // Most declarations are NOT !important
1388
1439
  if (is_important) {
1389
- // New is !important - wins if existing is NOT important OR equal/higher specificity
1390
- if (!existing_is_important || existing_spec_int <= specificity) {
1440
+ // New is !important - wins if existing is NOT important OR higher specificity OR (equal specificity AND later order)
1441
+ if (!existing_is_important || existing_spec_int < specificity ||
1442
+ (existing_spec_int == specificity && existing_order <= source_order)) {
1391
1443
  should_replace = 1;
1392
1444
  }
1393
1445
  } else {
1394
- // New is NOT important - only wins if existing is also NOT important AND equal/higher specificity
1395
- if (!existing_is_important && existing_spec_int <= specificity) {
1446
+ // New is NOT important - only wins if existing is also NOT important AND (higher specificity OR equal specificity with later order)
1447
+ if (!existing_is_important &&
1448
+ (existing_spec_int < specificity ||
1449
+ (existing_spec_int == specificity && existing_order <= source_order))) {
1396
1450
  should_replace = 1;
1397
1451
  }
1398
1452
  }
1399
1453
 
1400
1454
  // Replacement is common in merge scenarios
1401
1455
  if (should_replace) {
1402
- rb_hash_aset(existing, ID2SYM(id_value), value);
1403
- rb_hash_aset(existing, ID2SYM(id_specificity), INT2NUM(specificity));
1404
- rb_hash_aset(existing, ID2SYM(id_important), important);
1456
+ RARRAY_ASET(existing, PROP_SOURCE_ORDER, LONG2NUM(source_order));
1457
+ RARRAY_ASET(existing, PROP_SPECIFICITY, INT2NUM(specificity));
1458
+ RARRAY_ASET(existing, PROP_IMPORTANT, important);
1459
+ RARRAY_ASET(existing, PROP_VALUE, value);
1405
1460
  }
1406
1461
  }
1407
1462
 
1408
1463
  RB_GC_GUARD(property);
1409
1464
  RB_GC_GUARD(value);
1410
1465
  RB_GC_GUARD(decl);
1466
+ source_order++;
1411
1467
  }
1412
1468
 
1413
1469
  RB_GC_GUARD(selector);
@@ -1453,7 +1509,7 @@ VALUE cataract_merge_new(VALUE self, VALUE input) {
1453
1509
  VALUE border_data_src = !NIL_P(border_width) ? GET_PROP_DATA_STR(properties_hash, str_border_width) :
1454
1510
  !NIL_P(border_style) ? GET_PROP_DATA_STR(properties_hash, str_border_style) :
1455
1511
  GET_PROP_DATA_STR(properties_hash, str_border_color);
1456
- VALUE border_important = rb_hash_aref(border_data_src, ID2SYM(id_important));
1512
+ VALUE border_important = RARRAY_AREF(border_data_src, PROP_IMPORTANT);
1457
1513
  int border_is_important = RTEST(border_important);
1458
1514
 
1459
1515
  // Check that all present properties have the same !important flag
@@ -1469,12 +1525,11 @@ VALUE cataract_merge_new(VALUE self, VALUE input) {
1469
1525
 
1470
1526
  VALUE border_shorthand = cataract_create_border_shorthand(Qnil, border_props);
1471
1527
  if (!NIL_P(border_shorthand)) {
1472
- int border_spec = NUM2INT(rb_hash_aref(border_data_src, ID2SYM(id_specificity)));
1473
-
1474
- VALUE border_data = rb_hash_new();
1475
- rb_hash_aset(border_data, ID2SYM(id_value), border_shorthand);
1476
- rb_hash_aset(border_data, ID2SYM(id_specificity), INT2NUM(border_spec));
1477
- rb_hash_aset(border_data, ID2SYM(id_important), border_important);
1528
+ VALUE border_data = rb_ary_new_capa(4);
1529
+ rb_ary_push(border_data, RARRAY_AREF(border_data_src, PROP_SOURCE_ORDER));
1530
+ rb_ary_push(border_data, RARRAY_AREF(border_data_src, PROP_SPECIFICITY));
1531
+ rb_ary_push(border_data, border_important);
1532
+ rb_ary_push(border_data, border_shorthand);
1478
1533
  rb_hash_aset(properties_hash, str_border, border_data);
1479
1534
 
1480
1535
  if (!NIL_P(border_width)) rb_hash_delete(properties_hash, str_border_width);
@@ -1499,7 +1554,7 @@ VALUE cataract_merge_new(VALUE self, VALUE input) {
1499
1554
 
1500
1555
  // Get metadata from font-size as reference
1501
1556
  VALUE size_data = GET_PROP_DATA_STR(properties_hash, str_font_size);
1502
- VALUE font_important = rb_hash_aref(size_data, ID2SYM(id_important));
1557
+ VALUE font_important = RARRAY_AREF(size_data, PROP_IMPORTANT);
1503
1558
  int font_is_important = RTEST(font_important);
1504
1559
 
1505
1560
  // Check that all present properties have the same !important flag
@@ -1520,12 +1575,11 @@ VALUE cataract_merge_new(VALUE self, VALUE input) {
1520
1575
 
1521
1576
  VALUE font_shorthand = cataract_create_font_shorthand(Qnil, font_props);
1522
1577
  if (!NIL_P(font_shorthand)) {
1523
- int font_spec = NUM2INT(rb_hash_aref(size_data, ID2SYM(id_specificity)));
1524
-
1525
- VALUE font_data = rb_hash_new();
1526
- rb_hash_aset(font_data, ID2SYM(id_value), font_shorthand);
1527
- rb_hash_aset(font_data, ID2SYM(id_specificity), INT2NUM(font_spec));
1528
- rb_hash_aset(font_data, ID2SYM(id_important), font_important);
1578
+ VALUE font_data = rb_ary_new_capa(4);
1579
+ rb_ary_push(font_data, RARRAY_AREF(size_data, PROP_SOURCE_ORDER));
1580
+ rb_ary_push(font_data, RARRAY_AREF(size_data, PROP_SPECIFICITY));
1581
+ rb_ary_push(font_data, font_important);
1582
+ rb_ary_push(font_data, font_shorthand);
1529
1583
  rb_hash_aset(properties_hash, str_font, font_data);
1530
1584
 
1531
1585
  // Remove longhand properties
@@ -1556,7 +1610,7 @@ VALUE cataract_merge_new(VALUE self, VALUE input) {
1556
1610
  VALUE list_style_data_src = !NIL_P(list_style_type) ? GET_PROP_DATA_STR(properties_hash, str_list_style_type) :
1557
1611
  !NIL_P(list_style_position) ? GET_PROP_DATA_STR(properties_hash, str_list_style_position) :
1558
1612
  GET_PROP_DATA_STR(properties_hash, str_list_style_image);
1559
- VALUE list_style_important = rb_hash_aref(list_style_data_src, ID2SYM(id_important));
1613
+ VALUE list_style_important = RARRAY_AREF(list_style_data_src, PROP_IMPORTANT);
1560
1614
  int list_style_is_important = RTEST(list_style_important);
1561
1615
 
1562
1616
  // Check that all present properties have the same !important flag
@@ -1572,12 +1626,11 @@ VALUE cataract_merge_new(VALUE self, VALUE input) {
1572
1626
 
1573
1627
  VALUE list_style_shorthand = cataract_create_list_style_shorthand(Qnil, list_style_props);
1574
1628
  if (!NIL_P(list_style_shorthand)) {
1575
- int list_style_spec = NUM2INT(rb_hash_aref(list_style_data_src, ID2SYM(id_specificity)));
1576
-
1577
- VALUE list_style_data = rb_hash_new();
1578
- rb_hash_aset(list_style_data, ID2SYM(id_value), list_style_shorthand);
1579
- rb_hash_aset(list_style_data, ID2SYM(id_specificity), INT2NUM(list_style_spec));
1580
- rb_hash_aset(list_style_data, ID2SYM(id_important), list_style_important);
1629
+ VALUE list_style_data = rb_ary_new_capa(4);
1630
+ rb_ary_push(list_style_data, RARRAY_AREF(list_style_data_src, PROP_SOURCE_ORDER));
1631
+ rb_ary_push(list_style_data, RARRAY_AREF(list_style_data_src, PROP_SPECIFICITY));
1632
+ rb_ary_push(list_style_data, list_style_important);
1633
+ rb_ary_push(list_style_data, list_style_shorthand);
1581
1634
  rb_hash_aset(properties_hash, str_list_style, list_style_data);
1582
1635
 
1583
1636
  // Remove longhand properties
@@ -1611,7 +1664,7 @@ VALUE cataract_merge_new(VALUE self, VALUE input) {
1611
1664
  !NIL_P(background_repeat) ? GET_PROP_DATA_STR(properties_hash, str_background_repeat) :
1612
1665
  !NIL_P(background_attachment) ? GET_PROP_DATA_STR(properties_hash, str_background_attachment) :
1613
1666
  GET_PROP_DATA_STR(properties_hash, str_background_position);
1614
- VALUE background_important = rb_hash_aref(background_data_src, ID2SYM(id_important));
1667
+ VALUE background_important = RARRAY_AREF(background_data_src, PROP_IMPORTANT);
1615
1668
  int background_is_important = RTEST(background_important);
1616
1669
 
1617
1670
  // Check that all present properties have the same !important flag
@@ -1631,12 +1684,11 @@ VALUE cataract_merge_new(VALUE self, VALUE input) {
1631
1684
 
1632
1685
  VALUE background_shorthand = cataract_create_background_shorthand(Qnil, background_props);
1633
1686
  if (!NIL_P(background_shorthand)) {
1634
- int background_spec = NUM2INT(rb_hash_aref(background_data_src, ID2SYM(id_specificity)));
1635
-
1636
- VALUE background_data = rb_hash_new();
1637
- rb_hash_aset(background_data, ID2SYM(id_value), background_shorthand);
1638
- rb_hash_aset(background_data, ID2SYM(id_specificity), INT2NUM(background_spec));
1639
- rb_hash_aset(background_data, ID2SYM(id_important), background_important);
1687
+ VALUE background_data = rb_ary_new_capa(4);
1688
+ rb_ary_push(background_data, RARRAY_AREF(background_data_src, PROP_SOURCE_ORDER));
1689
+ rb_ary_push(background_data, RARRAY_AREF(background_data_src, PROP_SPECIFICITY));
1690
+ rb_ary_push(background_data, background_important);
1691
+ rb_ary_push(background_data, background_shorthand);
1640
1692
  rb_hash_aset(properties_hash, str_background, background_data);
1641
1693
 
1642
1694
  // Remove longhand properties
@@ -1656,7 +1708,7 @@ VALUE cataract_merge_new(VALUE self, VALUE input) {
1656
1708
 
1657
1709
  // Build merged declarations array
1658
1710
  VALUE merged_declarations = rb_ary_new();
1659
- rb_hash_foreach(properties_hash, merge_build_result_callback, merged_declarations);
1711
+ rb_hash_foreach(properties_hash, flatten_build_result_callback, merged_declarations);
1660
1712
 
1661
1713
  // Determine final selector (allocate only once at the end)
1662
1714
  VALUE final_selector;