cataract 0.2.4 → 0.3.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.
Files changed (40) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/test.yml +6 -6
  3. data/.gitignore +17 -3
  4. data/.rubocop.yml +1 -0
  5. data/BENCHMARKS.md +24 -6
  6. data/CHANGELOG.md +29 -1
  7. data/Gemfile +2 -2
  8. data/examples/css_analyzer/analyzer.rb +9 -3
  9. data/examples/css_analyzer/analyzers/base.rb +1 -1
  10. data/ext/cataract/cataract.c +251 -458
  11. data/ext/cataract/cataract.h +127 -16
  12. data/ext/cataract/css_parser.c +1029 -700
  13. data/ext/cataract/extconf.rb +1 -2
  14. data/ext/cataract/flatten.c +279 -991
  15. data/ext/cataract/shorthand_expander.c +80 -102
  16. data/ext/cataract_color/color_conversion.c +1 -1
  17. data/lib/cataract/declarations.rb +13 -7
  18. data/lib/cataract/error.rb +49 -0
  19. data/lib/cataract/import_resolver.rb +36 -16
  20. data/lib/cataract/pure/byte_constants.rb +4 -1
  21. data/lib/cataract/pure/flatten.rb +143 -145
  22. data/lib/cataract/pure/parser.rb +623 -388
  23. data/lib/cataract/pure/serializer.rb +109 -104
  24. data/lib/cataract/pure/specificity.rb +112 -156
  25. data/lib/cataract/pure.rb +1 -6
  26. data/lib/cataract/stylesheet.rb +480 -410
  27. data/lib/cataract/version.rb +1 -1
  28. data/lib/cataract.rb +1 -0
  29. metadata +3 -13
  30. data/ext/cataract/import_scanner.c +0 -174
  31. data/ext/cataract_old/cataract.c +0 -393
  32. data/ext/cataract_old/cataract.h +0 -250
  33. data/ext/cataract_old/css_parser.c +0 -933
  34. data/ext/cataract_old/extconf.rb +0 -67
  35. data/ext/cataract_old/import_scanner.c +0 -174
  36. data/ext/cataract_old/merge.c +0 -776
  37. data/ext/cataract_old/shorthand_expander.c +0 -902
  38. data/ext/cataract_old/specificity.c +0 -213
  39. data/ext/cataract_old/stylesheet.c +0 -290
  40. data/ext/cataract_old/value_splitter.c +0 -116
@@ -51,6 +51,49 @@ static struct shorthand_mapping SHORTHAND_MAPPINGS[] = {
51
51
  {NULL, 0, Qnil, NULL, Qnil, NULL, Qnil, NULL, Qnil, NULL, Qnil, NULL} // Sentinel to mark end of array
52
52
  };
53
53
 
54
+ /*
55
+ * Family shorthand mapping: for shorthands whose longhand components aren't
56
+ * "all N required" like the 4-sided shorthands above, but a mix of required
57
+ * and optional candidates - border only requires border-style (width/color
58
+ * optional); font requires font-size and font-family (4 others optional);
59
+ * list-style and background don't require any specific candidate, just a
60
+ * minimum count of any 2 present.
61
+ *
62
+ * Candidates must be listed with any REQUIRED ones first: the recreation
63
+ * logic uses "first present candidate in this order" as the source of
64
+ * source_order/specificity/importance for the resulting shorthand, which is
65
+ * always the first required candidate when there is one (border-style for
66
+ * border, font-size for font) or the first present optional candidate
67
+ * otherwise (matching what list-style/background use) - see
68
+ * try_recreate_shorthand_family.
69
+ */
70
+ #define MAX_SHORTHAND_FAMILY_CANDIDATES 6
71
+
72
+ struct shorthand_family_mapping {
73
+ VALUE shorthand_name_val; // Cached Ruby string for the shorthand property name
74
+ VALUE candidate_vals[MAX_SHORTHAND_FAMILY_CANDIDATES]; // Cached candidate longhand property names, populated in init_flatten_constants()
75
+ int num_candidates;
76
+ int num_required; // First `num_required` candidates are mandatory; 0 means "optional-only" (see min_present)
77
+ int min_present; // Only checked when num_required == 0: minimum count of ANY candidates that must be present
78
+ VALUE (*creator_func)(VALUE, VALUE);
79
+ };
80
+
81
+ // Order matches the original inline recreation blocks (border, list-style,
82
+ // font, background) - properties_hash is a Ruby Hash, whose iteration order
83
+ // (and therefore the final declaration output order) follows insertion
84
+ // order, so this table's order is observable behavior, not just style.
85
+ static struct shorthand_family_mapping SHORTHAND_FAMILY_MAPPINGS[] = {
86
+ // border: only border-style is required; border-width/border-color are optional
87
+ { Qnil, { Qnil, Qnil, Qnil, Qnil, Qnil, Qnil }, 3, 1, 0, cataract_create_border_shorthand },
88
+ // list-style: none individually required, but at least 2 of the 3 must be present
89
+ { Qnil, { Qnil, Qnil, Qnil, Qnil, Qnil, Qnil }, 3, 0, 2, cataract_create_list_style_shorthand },
90
+ // font: font-size and font-family are both required; the rest are optional
91
+ { Qnil, { Qnil, Qnil, Qnil, Qnil, Qnil, Qnil }, 6, 2, 0, cataract_create_font_shorthand },
92
+ // background: none individually required, but at least 2 of the 5 must be present
93
+ { Qnil, { Qnil, Qnil, Qnil, Qnil, Qnil, Qnil }, 5, 0, 2, cataract_create_background_shorthand },
94
+ };
95
+ #define NUM_SHORTHAND_FAMILIES (sizeof(SHORTHAND_FAMILY_MAPPINGS) / sizeof(SHORTHAND_FAMILY_MAPPINGS[0]))
96
+
54
97
  // Cached property name strings (frozen, never GC'd)
55
98
  // Initialized in init_flatten_constants() at module load time
56
99
  static VALUE str_margin = Qnil;
@@ -97,68 +140,6 @@ static VALUE str_background_repeat = Qnil;
97
140
  static VALUE str_background_attachment = Qnil;
98
141
  static VALUE str_background_position = Qnil;
99
142
 
100
- // Context for expanded property iteration
101
- struct expand_context {
102
- VALUE properties_hash;
103
- long source_order;
104
- int specificity;
105
- VALUE important;
106
- };
107
-
108
- // Callback for rb_hash_foreach - process expanded properties and apply cascade
109
- static int flatten_expanded_callback(VALUE exp_prop, VALUE exp_value, VALUE ctx_val) {
110
- struct expand_context *ctx = (struct expand_context *)ctx_val;
111
-
112
- // Expanded properties from shorthand expanders are already lowercase
113
- // No need to lowercase again
114
- int is_important = RTEST(ctx->important);
115
-
116
- // Apply cascade rules for expanded property
117
- VALUE existing = rb_hash_aref(ctx->properties_hash, exp_prop);
118
-
119
- if (NIL_P(existing)) {
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);
126
- rb_hash_aset(ctx->properties_hash, exp_prop, prop_data);
127
- } else {
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);
132
- int existing_is_important = RTEST(existing_important);
133
-
134
- int should_replace = 0;
135
- if (is_important) {
136
- if (!existing_is_important || existing_spec_int < ctx->specificity ||
137
- (existing_spec_int == ctx->specificity && existing_order <= ctx->source_order)) {
138
- should_replace = 1;
139
- }
140
- } else {
141
- if (!existing_is_important &&
142
- (existing_spec_int < ctx->specificity ||
143
- (existing_spec_int == ctx->specificity && existing_order <= ctx->source_order))) {
144
- should_replace = 1;
145
- }
146
- }
147
-
148
- if (should_replace) {
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);
154
- }
155
- }
156
-
157
- RB_GC_GUARD(exp_prop);
158
- RB_GC_GUARD(exp_value);
159
- return ST_CONTINUE;
160
- }
161
-
162
143
  // Callback for rb_hash_foreach - builds result array from properties hash
163
144
  static int flatten_build_result_callback(VALUE property, VALUE prop_data, VALUE result_ary) {
164
145
  // Extract value and important flag from array: [source_order, specificity, important, value]
@@ -322,139 +303,34 @@ void init_flatten_constants(void) {
322
303
  SHORTHAND_MAPPINGS[4].prop_right_val = str_border_right_color;
323
304
  SHORTHAND_MAPPINGS[4].prop_bottom_val = str_border_bottom_color;
324
305
  SHORTHAND_MAPPINGS[4].prop_left_val = str_border_left_color;
325
- }
326
306
 
327
- // Helper macros to extract property data from properties_hash
328
- // Properties are stored as arrays: [source_order, specificity, important, value]
329
- #define GET_PROP_VALUE(hash, prop_name) \
330
- ({ VALUE pd = rb_hash_aref(hash, USASCII_STR(prop_name)); \
331
- NIL_P(pd) ? Qnil : RARRAY_AREF(pd, PROP_VALUE); })
332
-
333
- #define GET_PROP_DATA(hash, prop_name) \
334
- rb_hash_aref(hash, USASCII_STR(prop_name))
335
-
336
- // Versions that accept cached VALUE strings instead of string literals
337
- #define GET_PROP_VALUE_STR(hash, str_prop) \
338
- ({ VALUE pd = rb_hash_aref(hash, str_prop); \
339
- NIL_P(pd) ? Qnil : RARRAY_AREF(pd, PROP_VALUE); })
340
-
341
- #define GET_PROP_DATA_STR(hash, str_prop) \
342
- rb_hash_aref(hash, str_prop)
343
-
344
- // Helper macro to check if a property's !important flag matches a reference
345
- #define CHECK_IMPORTANT_MATCH(hash, str_prop, ref_important) \
346
- ({ VALUE _pd = GET_PROP_DATA_STR(hash, str_prop); \
347
- NIL_P(_pd) ? 1 : (RTEST(RARRAY_AREF(_pd, PROP_IMPORTANT)) == (ref_important)); })
348
-
349
- // Macro to create shorthand from 4-sided properties (margin, padding, border-width/style/color)
350
- // Reduces repetitive code by encapsulating the common pattern:
351
- // 1. Get 4 longhand values (top, right, bottom, left)
352
- // 2. Check if all 4 exist
353
- // 3. Call shorthand creator function
354
- // 4. Add shorthand to properties_hash and remove longhands
355
- // Note: Uses cached static strings (VALUE) for property names - no runtime allocation
356
- #define TRY_CREATE_FOUR_SIDED_SHORTHAND(hash, str_top, str_right, str_bottom, str_left, str_shorthand, creator_func) \
357
- do { \
358
- VALUE _top = GET_PROP_VALUE_STR(hash, str_top); \
359
- VALUE _right = GET_PROP_VALUE_STR(hash, str_right); \
360
- VALUE _bottom = GET_PROP_VALUE_STR(hash, str_bottom); \
361
- VALUE _left = GET_PROP_VALUE_STR(hash, str_left); \
362
- \
363
- if (!NIL_P(_top) && !NIL_P(_right) && !NIL_P(_bottom) && !NIL_P(_left)) { \
364
- /* Check that all properties have the same !important flag */ \
365
- VALUE _top_data = GET_PROP_DATA_STR(hash, str_top); \
366
- VALUE _right_data = GET_PROP_DATA_STR(hash, str_right); \
367
- VALUE _bottom_data = GET_PROP_DATA_STR(hash, str_bottom); \
368
- VALUE _left_data = GET_PROP_DATA_STR(hash, str_left); \
369
- \
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); \
374
- \
375
- int _top_is_imp = RTEST(_top_imp); \
376
- int _right_is_imp = RTEST(_right_imp); \
377
- int _bottom_is_imp = RTEST(_bottom_imp); \
378
- int _left_is_imp = RTEST(_left_imp); \
379
- \
380
- /* Only create shorthand if all have same !important flag */ \
381
- if (_top_is_imp == _right_is_imp && _top_is_imp == _bottom_is_imp && _top_is_imp == _left_is_imp) { \
382
- VALUE _props = rb_hash_new(); \
383
- rb_hash_aset(_props, str_top, _top); \
384
- rb_hash_aset(_props, str_right, _right); \
385
- rb_hash_aset(_props, str_bottom, _bottom); \
386
- rb_hash_aset(_props, str_left, _left); \
387
- \
388
- VALUE _shorthand_value = creator_func(Qnil, _props); \
389
- if (!NIL_P(_shorthand_value)) { \
390
- long _source_order = NUM2LONG(RARRAY_AREF(_top_data, PROP_SOURCE_ORDER)); \
391
- int _specificity = NUM2INT(RARRAY_AREF(_top_data, PROP_SPECIFICITY)); \
392
- \
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); \
398
- rb_hash_aset(hash, str_shorthand, _shorthand_data); \
399
- \
400
- rb_hash_delete(hash, str_top); \
401
- rb_hash_delete(hash, str_right); \
402
- rb_hash_delete(hash, str_bottom); \
403
- rb_hash_delete(hash, str_left); \
404
- \
405
- RB_GC_GUARD(_shorthand_value); \
406
- } \
407
- RB_GC_GUARD(_props); \
408
- } \
409
- } \
410
- } while(0)
411
-
412
- // Helper macro: Recreate dimension shorthand (margin, padding, border-width)
413
- // Takes a property prefix like "margin" and creates "margin" from margin-top/right/bottom/left
414
- #define RECREATE_DIMENSION_SHORTHAND(hash, prefix, creator_func) \
415
- do { \
416
- char _top_name[64], _right_name[64], _bottom_name[64], _left_name[64]; \
417
- snprintf(_top_name, sizeof(_top_name), "%s-top", prefix); \
418
- snprintf(_right_name, sizeof(_right_name), "%s-right", prefix); \
419
- snprintf(_bottom_name, sizeof(_bottom_name), "%s-bottom", prefix); \
420
- snprintf(_left_name, sizeof(_left_name), "%s-left", prefix); \
421
- \
422
- VALUE _top_data = rb_hash_aref(hash, STR_NEW_CSTR(_top_name)); \
423
- VALUE _right_data = rb_hash_aref(hash, STR_NEW_CSTR(_right_name)); \
424
- VALUE _bottom_data = rb_hash_aref(hash, STR_NEW_CSTR(_bottom_name)); \
425
- VALUE _left_data = rb_hash_aref(hash, STR_NEW_CSTR(_left_name)); \
426
- \
427
- if (!NIL_P(_top_data) && !NIL_P(_right_data) && !NIL_P(_bottom_data) && !NIL_P(_left_data)) { \
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); \
432
- \
433
- if (RTEST(_top_imp) == RTEST(_right_imp) && RTEST(_top_imp) == RTEST(_bottom_imp) && RTEST(_top_imp) == RTEST(_left_imp)) { \
434
- VALUE _props = rb_hash_new(); \
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)); \
439
- \
440
- VALUE _shorthand_value = creator_func(Qnil, _props); \
441
- if (!NIL_P(_shorthand_value)) { \
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); \
447
- rb_hash_aset(hash, rb_usascii_str_new(prefix, strlen(prefix)), _shorthand_data); \
448
- \
449
- rb_hash_delete(hash, STR_NEW_CSTR(_top_name)); \
450
- rb_hash_delete(hash, STR_NEW_CSTR(_right_name)); \
451
- rb_hash_delete(hash, STR_NEW_CSTR(_bottom_name)); \
452
- rb_hash_delete(hash, STR_NEW_CSTR(_left_name)); \
453
- DEBUG_PRINTF(" -> Recreated %s shorthand\n", prefix); \
454
- } \
455
- } \
456
- } \
457
- } while(0)
307
+ // Populate the family shorthand mapping table. Candidate order matches
308
+ // what each original inline block checked (required candidates first).
309
+ SHORTHAND_FAMILY_MAPPINGS[0].shorthand_name_val = str_border;
310
+ SHORTHAND_FAMILY_MAPPINGS[0].candidate_vals[0] = str_border_style;
311
+ SHORTHAND_FAMILY_MAPPINGS[0].candidate_vals[1] = str_border_width;
312
+ SHORTHAND_FAMILY_MAPPINGS[0].candidate_vals[2] = str_border_color;
313
+
314
+ SHORTHAND_FAMILY_MAPPINGS[1].shorthand_name_val = str_list_style;
315
+ SHORTHAND_FAMILY_MAPPINGS[1].candidate_vals[0] = str_list_style_type;
316
+ SHORTHAND_FAMILY_MAPPINGS[1].candidate_vals[1] = str_list_style_position;
317
+ SHORTHAND_FAMILY_MAPPINGS[1].candidate_vals[2] = str_list_style_image;
318
+
319
+ SHORTHAND_FAMILY_MAPPINGS[2].shorthand_name_val = str_font;
320
+ SHORTHAND_FAMILY_MAPPINGS[2].candidate_vals[0] = str_font_size;
321
+ SHORTHAND_FAMILY_MAPPINGS[2].candidate_vals[1] = str_font_family;
322
+ SHORTHAND_FAMILY_MAPPINGS[2].candidate_vals[2] = str_font_style;
323
+ SHORTHAND_FAMILY_MAPPINGS[2].candidate_vals[3] = str_font_variant;
324
+ SHORTHAND_FAMILY_MAPPINGS[2].candidate_vals[4] = str_font_weight;
325
+ SHORTHAND_FAMILY_MAPPINGS[2].candidate_vals[5] = str_line_height;
326
+
327
+ SHORTHAND_FAMILY_MAPPINGS[3].shorthand_name_val = str_background;
328
+ SHORTHAND_FAMILY_MAPPINGS[3].candidate_vals[0] = str_background_color;
329
+ SHORTHAND_FAMILY_MAPPINGS[3].candidate_vals[1] = str_background_image;
330
+ SHORTHAND_FAMILY_MAPPINGS[3].candidate_vals[2] = str_background_repeat;
331
+ SHORTHAND_FAMILY_MAPPINGS[3].candidate_vals[3] = str_background_position;
332
+ SHORTHAND_FAMILY_MAPPINGS[3].candidate_vals[4] = str_background_attachment;
333
+ }
458
334
 
459
335
  // Helper function: Try to recreate a shorthand property from its longhand components
460
336
  // Uses cached VALUE objects for property names to avoid repeated string allocations
@@ -511,6 +387,92 @@ static inline void try_recreate_shorthand(VALUE properties_hash, const struct sh
511
387
  DEBUG_PRINTF(" -> Recreated %s shorthand\n", mapping->shorthand_name);
512
388
  }
513
389
 
390
+ // Helper function: Try to recreate a "family" shorthand (border, font,
391
+ // list-style, background) from its longhand components. Unlike
392
+ // try_recreate_shorthand above (always exactly 4 required sides), these
393
+ // allow a mix of required and optional candidates - see
394
+ // shorthand_family_mapping for the exact rules.
395
+ static inline void try_recreate_shorthand_family(VALUE properties_hash, const struct shorthand_family_mapping *family) {
396
+ // Zero-initialize all slots (not just the first num_candidates) so every
397
+ // read below is well-defined regardless of a given family's arity.
398
+ VALUE datas[MAX_SHORTHAND_FAMILY_CANDIDATES] = { Qnil, Qnil, Qnil, Qnil, Qnil, Qnil };
399
+ int present_count = 0;
400
+
401
+ for (int i = 0; i < family->num_candidates; i++) {
402
+ datas[i] = rb_hash_aref(properties_hash, family->candidate_vals[i]);
403
+ if (!NIL_P(datas[i])) present_count++;
404
+ }
405
+
406
+ // Required candidates (if any) must all be present
407
+ for (int i = 0; i < family->num_required; i++) {
408
+ if (NIL_P(datas[i])) return;
409
+ }
410
+
411
+ // If nothing is individually required, need a minimum count of ANY present
412
+ if (family->num_required == 0 && present_count < family->min_present) {
413
+ return;
414
+ }
415
+
416
+ // All present candidates must share the same !important flag
417
+ VALUE reference_imp = Qnil;
418
+ int have_reference = 0;
419
+ for (int i = 0; i < family->num_candidates; i++) {
420
+ if (NIL_P(datas[i])) continue;
421
+ VALUE imp = RARRAY_AREF(datas[i], PROP_IMPORTANT);
422
+ if (!have_reference) {
423
+ reference_imp = imp;
424
+ have_reference = 1;
425
+ } else if (RTEST(reference_imp) != RTEST(imp)) {
426
+ return;
427
+ }
428
+ }
429
+
430
+ // Build a hash of present property values for the creator function
431
+ VALUE props = rb_hash_new();
432
+ for (int i = 0; i < family->num_candidates; i++) {
433
+ if (!NIL_P(datas[i])) {
434
+ rb_hash_aset(props, family->candidate_vals[i], RARRAY_AREF(datas[i], PROP_VALUE));
435
+ }
436
+ }
437
+
438
+ VALUE shorthand_value = family->creator_func(Qnil, props);
439
+ if (NIL_P(shorthand_value)) {
440
+ return; // Creator decided not to create shorthand
441
+ }
442
+
443
+ // First present candidate (in declared order) supplies source_order/
444
+ // specificity/importance for the shorthand - always the first required
445
+ // candidate when there is one (border-style for border, font-size for
446
+ // font), otherwise the first present optional candidate, matching what
447
+ // each original inline block hardcoded.
448
+ int primary_idx = -1;
449
+ for (int i = 0; i < family->num_candidates; i++) {
450
+ if (!NIL_P(datas[i])) {
451
+ primary_idx = i;
452
+ break;
453
+ }
454
+ }
455
+ if (primary_idx < 0) {
456
+ return; // Nothing present to build a shorthand from
457
+ }
458
+
459
+ VALUE shorthand_data = rb_ary_new_capa(4);
460
+ rb_ary_push(shorthand_data, RARRAY_AREF(datas[primary_idx], PROP_SOURCE_ORDER));
461
+ rb_ary_push(shorthand_data, RARRAY_AREF(datas[primary_idx], PROP_SPECIFICITY));
462
+ rb_ary_push(shorthand_data, reference_imp);
463
+ rb_ary_push(shorthand_data, shorthand_value);
464
+
465
+ // Add shorthand and remove longhand properties
466
+ rb_hash_aset(properties_hash, family->shorthand_name_val, shorthand_data);
467
+ for (int i = 0; i < family->num_candidates; i++) {
468
+ if (!NIL_P(datas[i])) {
469
+ rb_hash_delete(properties_hash, family->candidate_vals[i]);
470
+ }
471
+ }
472
+
473
+ DEBUG_PRINTF(" -> Recreated family shorthand\n");
474
+ }
475
+
514
476
  /*
515
477
  * Helper struct: For processing expanded properties during merge
516
478
  */
@@ -829,44 +791,49 @@ static VALUE flatten_rules_for_selector(VALUE rules_array, VALUE rule_indices, V
829
791
  if (first_char == 'm' || first_char == 'p' || first_char == 'b' ||
830
792
  first_char == 'f' || first_char == 'l') {
831
793
  // Potentially a shorthand - check specific property names
794
+ //
795
+ // Importance is always passed as Qfalse here: the expanded
796
+ // declarations' own `important` field is never read below
797
+ // (see expand_data.is_important) - actual importance comes
798
+ // from the original, un-expanded declaration instead.
832
799
  if (strcmp(prop_cstr, "margin") == 0) {
833
- expanded = cataract_expand_margin(Qnil, value);
800
+ expanded = cataract_expand_margin(Qnil, value, Qfalse);
834
801
  DEBUG_PRINTF(" -> Expanding margin shorthand (%ld longhands)\n", RARRAY_LEN(expanded));
835
802
  } else if (strcmp(prop_cstr, "padding") == 0) {
836
- expanded = cataract_expand_padding(Qnil, value);
803
+ expanded = cataract_expand_padding(Qnil, value, Qfalse);
837
804
  DEBUG_PRINTF(" -> Expanding padding shorthand (%ld longhands)\n", RARRAY_LEN(expanded));
838
805
  } else if (strcmp(prop_cstr, "background") == 0) {
839
- expanded = cataract_expand_background(Qnil, value);
806
+ expanded = cataract_expand_background(Qnil, value, Qfalse);
840
807
  DEBUG_PRINTF(" -> Expanding background shorthand (%ld longhands)\n", RARRAY_LEN(expanded));
841
808
  } else if (strcmp(prop_cstr, "font") == 0) {
842
- expanded = cataract_expand_font(Qnil, value);
809
+ expanded = cataract_expand_font(Qnil, value, Qfalse);
843
810
  DEBUG_PRINTF(" -> Expanding font shorthand (%ld longhands)\n", RARRAY_LEN(expanded));
844
811
  } else if (strcmp(prop_cstr, "border") == 0) {
845
- expanded = cataract_expand_border(Qnil, value);
812
+ expanded = cataract_expand_border(Qnil, value, Qfalse);
846
813
  DEBUG_PRINTF(" -> Expanding border shorthand (%ld longhands)\n", RARRAY_LEN(expanded));
847
814
  } else if (strcmp(prop_cstr, "border-color") == 0) {
848
- expanded = cataract_expand_border_color(Qnil, value);
815
+ expanded = cataract_expand_border_color(Qnil, value, Qfalse);
849
816
  DEBUG_PRINTF(" -> Expanding border-color shorthand (%ld longhands)\n", RARRAY_LEN(expanded));
850
817
  } else if (strcmp(prop_cstr, "border-style") == 0) {
851
- expanded = cataract_expand_border_style(Qnil, value);
818
+ expanded = cataract_expand_border_style(Qnil, value, Qfalse);
852
819
  DEBUG_PRINTF(" -> Expanding border-style shorthand (%ld longhands)\n", RARRAY_LEN(expanded));
853
820
  } else if (strcmp(prop_cstr, "border-width") == 0) {
854
- expanded = cataract_expand_border_width(Qnil, value);
821
+ expanded = cataract_expand_border_width(Qnil, value, Qfalse);
855
822
  DEBUG_PRINTF(" -> Expanding border-width shorthand (%ld longhands)\n", RARRAY_LEN(expanded));
856
823
  } else if (strcmp(prop_cstr, "list-style") == 0) {
857
- expanded = cataract_expand_list_style(Qnil, value);
824
+ expanded = cataract_expand_list_style(Qnil, value, Qfalse);
858
825
  DEBUG_PRINTF(" -> Expanding list-style shorthand (%ld longhands)\n", RARRAY_LEN(expanded));
859
826
  } else if (strcmp(prop_cstr, "border-top") == 0) {
860
- expanded = cataract_expand_border_side(Qnil, STR_NEW_CSTR("top"), value);
827
+ expanded = cataract_expand_border_side(Qnil, STR_NEW_CSTR("top"), value, Qfalse);
861
828
  DEBUG_PRINTF(" -> Expanding border-top shorthand (%ld longhands)\n", RARRAY_LEN(expanded));
862
829
  } else if (strcmp(prop_cstr, "border-right") == 0) {
863
- expanded = cataract_expand_border_side(Qnil, STR_NEW_CSTR("right"), value);
830
+ expanded = cataract_expand_border_side(Qnil, STR_NEW_CSTR("right"), value, Qfalse);
864
831
  DEBUG_PRINTF(" -> Expanding border-right shorthand (%ld longhands)\n", RARRAY_LEN(expanded));
865
832
  } else if (strcmp(prop_cstr, "border-bottom") == 0) {
866
- expanded = cataract_expand_border_side(Qnil, STR_NEW_CSTR("bottom"), value);
833
+ expanded = cataract_expand_border_side(Qnil, STR_NEW_CSTR("bottom"), value, Qfalse);
867
834
  DEBUG_PRINTF(" -> Expanding border-bottom shorthand (%ld longhands)\n", RARRAY_LEN(expanded));
868
835
  } else if (strcmp(prop_cstr, "border-left") == 0) {
869
- expanded = cataract_expand_border_side(Qnil, STR_NEW_CSTR("left"), value);
836
+ expanded = cataract_expand_border_side(Qnil, STR_NEW_CSTR("left"), value, Qfalse);
870
837
  DEBUG_PRINTF(" -> Expanding border-left shorthand (%ld longhands)\n", RARRAY_LEN(expanded));
871
838
  }
872
839
  }
@@ -916,204 +883,9 @@ static VALUE flatten_rules_for_selector(VALUE rules_array, VALUE rule_indices, V
916
883
  try_recreate_shorthand(properties_hash, mapping);
917
884
  }
918
885
 
919
- // Try to recreate full border shorthand (if border-width, border-style, border-color present)
920
- {
921
- VALUE width = rb_hash_aref(properties_hash, STR_NEW_CSTR("border-width"));
922
- VALUE style = rb_hash_aref(properties_hash, STR_NEW_CSTR("border-style"));
923
- VALUE color = rb_hash_aref(properties_hash, STR_NEW_CSTR("border-color"));
924
-
925
- // Need at least style (border shorthand requires style)
926
- if (!NIL_P(style)) {
927
- // Check all have same !important flag
928
- VALUE style_imp = RARRAY_AREF(style, PROP_IMPORTANT);
929
- int same_importance = 1;
930
- if (!NIL_P(width)) same_importance = same_importance && (RTEST(style_imp) == RTEST(RARRAY_AREF(width, PROP_IMPORTANT)));
931
- if (!NIL_P(color)) same_importance = same_importance && (RTEST(style_imp) == RTEST(RARRAY_AREF(color, PROP_IMPORTANT)));
932
-
933
- if (same_importance) {
934
- VALUE props = rb_hash_new();
935
- if (!NIL_P(width)) rb_hash_aset(props, STR_NEW_CSTR("border-width"), RARRAY_AREF(width, PROP_VALUE));
936
- rb_hash_aset(props, STR_NEW_CSTR("border-style"), RARRAY_AREF(style, PROP_VALUE));
937
- if (!NIL_P(color)) rb_hash_aset(props, STR_NEW_CSTR("border-color"), RARRAY_AREF(color, PROP_VALUE));
938
-
939
- VALUE shorthand_value = cataract_create_border_shorthand(Qnil, props);
940
- if (!NIL_P(shorthand_value)) {
941
- VALUE shorthand_data = rb_ary_new_capa(4);
942
- rb_ary_push(shorthand_data, RARRAY_AREF(style, PROP_SOURCE_ORDER));
943
- rb_ary_push(shorthand_data, RARRAY_AREF(style, PROP_SPECIFICITY));
944
- rb_ary_push(shorthand_data, style_imp);
945
- rb_ary_push(shorthand_data, shorthand_value);
946
- rb_hash_aset(properties_hash, USASCII_STR("border"), shorthand_data);
947
-
948
- rb_hash_delete(properties_hash, STR_NEW_CSTR("border-width"));
949
- rb_hash_delete(properties_hash, STR_NEW_CSTR("border-style"));
950
- rb_hash_delete(properties_hash, STR_NEW_CSTR("border-color"));
951
- DEBUG_PRINTF(" -> Recreated border shorthand\n");
952
- }
953
- }
954
- }
955
- }
956
-
957
- // Try to recreate list-style shorthand
958
- {
959
- VALUE type = rb_hash_aref(properties_hash, STR_NEW_CSTR("list-style-type"));
960
- VALUE position = rb_hash_aref(properties_hash, STR_NEW_CSTR("list-style-position"));
961
- VALUE image = rb_hash_aref(properties_hash, STR_NEW_CSTR("list-style-image"));
962
-
963
- // Need at least 2 properties to create shorthand
964
- // Single property should stay as longhand (semantic difference)
965
- int list_count = 0;
966
- if (!NIL_P(type)) list_count++;
967
- if (!NIL_P(position)) list_count++;
968
- if (!NIL_P(image)) list_count++;
969
-
970
- if (list_count >= 2) {
971
- // Check all have same !important flag
972
- VALUE first_imp = Qnil;
973
- if (!NIL_P(type)) first_imp = RARRAY_AREF(type, PROP_IMPORTANT);
974
- else if (!NIL_P(position)) first_imp = RARRAY_AREF(position, PROP_IMPORTANT);
975
- else if (!NIL_P(image)) first_imp = RARRAY_AREF(image, PROP_IMPORTANT);
976
-
977
- int same_importance = 1;
978
- if (!NIL_P(type)) same_importance = same_importance && (RTEST(first_imp) == RTEST(RARRAY_AREF(type, PROP_IMPORTANT)));
979
- if (!NIL_P(position)) same_importance = same_importance && (RTEST(first_imp) == RTEST(RARRAY_AREF(position, PROP_IMPORTANT)));
980
- if (!NIL_P(image)) same_importance = same_importance && (RTEST(first_imp) == RTEST(RARRAY_AREF(image, PROP_IMPORTANT)));
981
-
982
- if (same_importance) {
983
- VALUE props = rb_hash_new();
984
- if (!NIL_P(type)) rb_hash_aset(props, STR_NEW_CSTR("list-style-type"), RARRAY_AREF(type, PROP_VALUE));
985
- if (!NIL_P(position)) rb_hash_aset(props, STR_NEW_CSTR("list-style-position"), RARRAY_AREF(position, PROP_VALUE));
986
- if (!NIL_P(image)) rb_hash_aset(props, STR_NEW_CSTR("list-style-image"), RARRAY_AREF(image, PROP_VALUE));
987
-
988
- VALUE shorthand_value = cataract_create_list_style_shorthand(Qnil, props);
989
- if (!NIL_P(shorthand_value)) {
990
- VALUE first_prop = !NIL_P(type) ? type : (!NIL_P(position) ? position : image);
991
- VALUE shorthand_data = rb_ary_new_capa(4);
992
- rb_ary_push(shorthand_data, RARRAY_AREF(first_prop, PROP_SOURCE_ORDER));
993
- rb_ary_push(shorthand_data, RARRAY_AREF(first_prop, PROP_SPECIFICITY));
994
- rb_ary_push(shorthand_data, first_imp);
995
- rb_ary_push(shorthand_data, shorthand_value);
996
- rb_hash_aset(properties_hash, USASCII_STR("list-style"), shorthand_data);
997
-
998
- rb_hash_delete(properties_hash, STR_NEW_CSTR("list-style-type"));
999
- rb_hash_delete(properties_hash, STR_NEW_CSTR("list-style-position"));
1000
- rb_hash_delete(properties_hash, STR_NEW_CSTR("list-style-image"));
1001
- DEBUG_PRINTF(" -> Recreated list-style shorthand\n");
1002
- }
1003
- }
1004
- }
1005
- }
1006
-
1007
- // Try to recreate font shorthand (requires at least font-size and font-family)
1008
- {
1009
- VALUE size = rb_hash_aref(properties_hash, STR_NEW_CSTR("font-size"));
1010
- VALUE family = rb_hash_aref(properties_hash, STR_NEW_CSTR("font-family"));
1011
-
1012
- if (!NIL_P(size) && !NIL_P(family)) {
1013
- VALUE style = rb_hash_aref(properties_hash, STR_NEW_CSTR("font-style"));
1014
- VALUE variant = rb_hash_aref(properties_hash, STR_NEW_CSTR("font-variant"));
1015
- VALUE weight = rb_hash_aref(properties_hash, STR_NEW_CSTR("font-weight"));
1016
- VALUE line_height = rb_hash_aref(properties_hash, STR_NEW_CSTR("line-height"));
1017
-
1018
- // Check all font properties have same !important flag
1019
- VALUE size_imp = RARRAY_AREF(size, PROP_IMPORTANT);
1020
- VALUE family_imp = RARRAY_AREF(family, PROP_IMPORTANT);
1021
-
1022
- int same_importance = (RTEST(size_imp) == RTEST(family_imp));
1023
- if (!NIL_P(style)) same_importance = same_importance && (RTEST(size_imp) == RTEST(RARRAY_AREF(style, PROP_IMPORTANT)));
1024
- if (!NIL_P(variant)) same_importance = same_importance && (RTEST(size_imp) == RTEST(RARRAY_AREF(variant, PROP_IMPORTANT)));
1025
- if (!NIL_P(weight)) same_importance = same_importance && (RTEST(size_imp) == RTEST(RARRAY_AREF(weight, PROP_IMPORTANT)));
1026
- if (!NIL_P(line_height)) same_importance = same_importance && (RTEST(size_imp) == RTEST(RARRAY_AREF(line_height, PROP_IMPORTANT)));
1027
-
1028
- if (same_importance) {
1029
- VALUE props = rb_hash_new();
1030
- rb_hash_aset(props, STR_NEW_CSTR("font-size"), RARRAY_AREF(size, PROP_VALUE));
1031
- rb_hash_aset(props, STR_NEW_CSTR("font-family"), RARRAY_AREF(family, PROP_VALUE));
1032
- if (!NIL_P(style)) rb_hash_aset(props, STR_NEW_CSTR("font-style"), RARRAY_AREF(style, PROP_VALUE));
1033
- if (!NIL_P(variant)) rb_hash_aset(props, STR_NEW_CSTR("font-variant"), RARRAY_AREF(variant, PROP_VALUE));
1034
- if (!NIL_P(weight)) rb_hash_aset(props, STR_NEW_CSTR("font-weight"), RARRAY_AREF(weight, PROP_VALUE));
1035
- if (!NIL_P(line_height)) rb_hash_aset(props, STR_NEW_CSTR("line-height"), RARRAY_AREF(line_height, PROP_VALUE));
1036
-
1037
- VALUE shorthand_value = cataract_create_font_shorthand(Qnil, props);
1038
- if (!NIL_P(shorthand_value)) {
1039
- VALUE shorthand_data = rb_ary_new_capa(4);
1040
- rb_ary_push(shorthand_data, RARRAY_AREF(size, PROP_SOURCE_ORDER));
1041
- rb_ary_push(shorthand_data, RARRAY_AREF(size, PROP_SPECIFICITY));
1042
- rb_ary_push(shorthand_data, size_imp);
1043
- rb_ary_push(shorthand_data, shorthand_value);
1044
- rb_hash_aset(properties_hash, USASCII_STR("font"), shorthand_data);
1045
-
1046
- rb_hash_delete(properties_hash, STR_NEW_CSTR("font-size"));
1047
- rb_hash_delete(properties_hash, STR_NEW_CSTR("font-family"));
1048
- rb_hash_delete(properties_hash, STR_NEW_CSTR("font-style"));
1049
- rb_hash_delete(properties_hash, STR_NEW_CSTR("font-variant"));
1050
- rb_hash_delete(properties_hash, STR_NEW_CSTR("font-weight"));
1051
- rb_hash_delete(properties_hash, STR_NEW_CSTR("line-height"));
1052
- DEBUG_PRINTF(" -> Recreated font shorthand\n");
1053
- }
1054
- }
1055
- }
1056
- }
1057
-
1058
- // Try to recreate background shorthand (if 2+ properties present)
1059
- {
1060
- VALUE color = rb_hash_aref(properties_hash, STR_NEW_CSTR("background-color"));
1061
- VALUE image = rb_hash_aref(properties_hash, STR_NEW_CSTR("background-image"));
1062
- VALUE repeat = rb_hash_aref(properties_hash, STR_NEW_CSTR("background-repeat"));
1063
- VALUE position = rb_hash_aref(properties_hash, STR_NEW_CSTR("background-position"));
1064
- VALUE attachment = rb_hash_aref(properties_hash, STR_NEW_CSTR("background-attachment"));
1065
-
1066
- int bg_count = 0;
1067
- if (!NIL_P(color)) bg_count++;
1068
- if (!NIL_P(image)) bg_count++;
1069
- if (!NIL_P(repeat)) bg_count++;
1070
- if (!NIL_P(position)) bg_count++;
1071
- if (!NIL_P(attachment)) bg_count++;
1072
-
1073
- // Need at least 2 properties to create shorthand
1074
- if (bg_count >= 2) {
1075
- // Check all have same !important flag
1076
- VALUE first_imp = Qnil;
1077
- if (!NIL_P(color)) first_imp = RARRAY_AREF(color, PROP_IMPORTANT);
1078
- else if (!NIL_P(image)) first_imp = RARRAY_AREF(image, PROP_IMPORTANT);
1079
- else if (!NIL_P(repeat)) first_imp = RARRAY_AREF(repeat, PROP_IMPORTANT);
1080
- else if (!NIL_P(position)) first_imp = RARRAY_AREF(position, PROP_IMPORTANT);
1081
- else if (!NIL_P(attachment)) first_imp = RARRAY_AREF(attachment, PROP_IMPORTANT);
1082
-
1083
- int same_importance = 1;
1084
- if (!NIL_P(color)) same_importance = same_importance && (RTEST(first_imp) == RTEST(RARRAY_AREF(color, PROP_IMPORTANT)));
1085
- if (!NIL_P(image)) same_importance = same_importance && (RTEST(first_imp) == RTEST(RARRAY_AREF(image, PROP_IMPORTANT)));
1086
- if (!NIL_P(repeat)) same_importance = same_importance && (RTEST(first_imp) == RTEST(RARRAY_AREF(repeat, PROP_IMPORTANT)));
1087
- if (!NIL_P(position)) same_importance = same_importance && (RTEST(first_imp) == RTEST(RARRAY_AREF(position, PROP_IMPORTANT)));
1088
- if (!NIL_P(attachment)) same_importance = same_importance && (RTEST(first_imp) == RTEST(RARRAY_AREF(attachment, PROP_IMPORTANT)));
1089
-
1090
- if (same_importance) {
1091
- VALUE props = rb_hash_new();
1092
- if (!NIL_P(color)) rb_hash_aset(props, STR_NEW_CSTR("background-color"), RARRAY_AREF(color, PROP_VALUE));
1093
- if (!NIL_P(image)) rb_hash_aset(props, STR_NEW_CSTR("background-image"), RARRAY_AREF(image, PROP_VALUE));
1094
- if (!NIL_P(repeat)) rb_hash_aset(props, STR_NEW_CSTR("background-repeat"), RARRAY_AREF(repeat, PROP_VALUE));
1095
- if (!NIL_P(position)) rb_hash_aset(props, STR_NEW_CSTR("background-position"), RARRAY_AREF(position, PROP_VALUE));
1096
- if (!NIL_P(attachment)) rb_hash_aset(props, STR_NEW_CSTR("background-attachment"), RARRAY_AREF(attachment, PROP_VALUE));
1097
-
1098
- VALUE shorthand_value = cataract_create_background_shorthand(Qnil, props);
1099
- if (!NIL_P(shorthand_value)) {
1100
- VALUE first_prop = !NIL_P(color) ? color : (!NIL_P(image) ? image : (!NIL_P(repeat) ? repeat : (!NIL_P(position) ? position : attachment)));
1101
- VALUE shorthand_data = rb_ary_new_capa(4);
1102
- rb_ary_push(shorthand_data, RARRAY_AREF(first_prop, PROP_SOURCE_ORDER));
1103
- rb_ary_push(shorthand_data, RARRAY_AREF(first_prop, PROP_SPECIFICITY));
1104
- rb_ary_push(shorthand_data, first_imp);
1105
- rb_ary_push(shorthand_data, shorthand_value);
1106
- rb_hash_aset(properties_hash, USASCII_STR("background"), shorthand_data);
1107
-
1108
- rb_hash_delete(properties_hash, STR_NEW_CSTR("background-color"));
1109
- rb_hash_delete(properties_hash, STR_NEW_CSTR("background-image"));
1110
- rb_hash_delete(properties_hash, STR_NEW_CSTR("background-repeat"));
1111
- rb_hash_delete(properties_hash, STR_NEW_CSTR("background-position"));
1112
- rb_hash_delete(properties_hash, STR_NEW_CSTR("background-attachment"));
1113
- DEBUG_PRINTF(" -> Recreated background shorthand\n");
1114
- }
1115
- }
1116
- }
886
+ // Try to recreate family shorthands (border, font, list-style, background) using the mapping table
887
+ for (size_t i = 0; i < NUM_SHORTHAND_FAMILIES; i++) {
888
+ try_recreate_shorthand_family(properties_hash, &SHORTHAND_FAMILY_MAPPINGS[i]);
1117
889
  }
1118
890
 
1119
891
  // Build declarations array from properties_hash
@@ -1311,6 +1083,104 @@ static void update_selector_lists_for_divergence(VALUE merged_rules, VALUE selec
1311
1083
  DEBUG_PRINTF("\n=== End divergence tracking: %ld selector lists preserved ===\n\n", RHASH_SIZE(selector_lists));
1312
1084
  }
1313
1085
 
1086
+ // Build a map from rule_id -> media_query_id for all Rule (non-AtRule)
1087
+ // entries that have one set. Used to group rules by (selector, media)
1088
+ // instead of just selector, and later to remap the stylesheet's media_index
1089
+ // after flattening.
1090
+ //
1091
+ // @param out_input_media_index Output: the input Stylesheet's @media_index
1092
+ // ivar (Qnil if input isn't a Stylesheet), needed by the caller for
1093
+ // remap_media_index_callback
1094
+ static VALUE build_rule_media_map(VALUE input, VALUE rules_array, long num_rules, VALUE *out_input_media_index) {
1095
+ VALUE rule_media_map = rb_hash_new();
1096
+ *out_input_media_index = Qnil;
1097
+
1098
+ if (!rb_obj_is_kind_of(input, cStylesheet)) {
1099
+ return rule_media_map;
1100
+ }
1101
+
1102
+ *out_input_media_index = rb_ivar_get(input, id_ivar_media_index);
1103
+
1104
+ // Only process Rule objects, not AtRules (AtRules don't have media_query_id field at same offset)
1105
+ for (long i = 0; i < num_rules; i++) {
1106
+ VALUE rule = rb_ary_entry(rules_array, i);
1107
+ if (!rb_obj_is_kind_of(rule, cAtRule)) {
1108
+ VALUE media_query_id = rb_struct_aref(rule, INT2FIX(RULE_MEDIA_QUERY_ID));
1109
+ if (!NIL_P(media_query_id)) {
1110
+ VALUE rule_id = rb_struct_aref(rule, INT2FIX(RULE_ID));
1111
+ rb_hash_aset(rule_media_map, rule_id, media_query_id);
1112
+ }
1113
+ }
1114
+ }
1115
+
1116
+ return rule_media_map;
1117
+ }
1118
+
1119
+ // Group rules by (selector, media) - not just selector - since rules with
1120
+ // the same selector but different media contexts must NOT be merged
1121
+ // together. AtRule entries (e.g. @keyframes, @font-face) have no
1122
+ // declarations to merge and can't be grouped this way at all, so they're
1123
+ // collected separately into passthrough_rules and passed through unchanged.
1124
+ //
1125
+ // @param passthrough_rules Output: empty array to populate with AtRule entries
1126
+ // @return Hash of [selector, media_context] => Array of rule indices
1127
+ static VALUE group_rules_by_selector_and_media(VALUE rules_array, long num_rules, VALUE rule_media_map, VALUE passthrough_rules) {
1128
+ VALUE selector_groups = rb_hash_new();
1129
+
1130
+ for (long i = 0; i < num_rules; i++) {
1131
+ VALUE rule = RARRAY_AREF(rules_array, i);
1132
+
1133
+ // Handle AtRule objects (@keyframes, @font-face, etc.) - pass through unchanged
1134
+ // AtRule has 'content' (string) instead of 'declarations' (array)
1135
+ if (rb_obj_is_kind_of(rule, cAtRule)) {
1136
+ DEBUG_PRINTF(" [Rule %ld] PASSTHROUGH: AtRule (e.g., @keyframes, @font-face)\n", i);
1137
+ rb_ary_push(passthrough_rules, rule);
1138
+ continue;
1139
+ }
1140
+
1141
+ VALUE declarations = rb_struct_aref(rule, INT2FIX(RULE_DECLARATIONS));
1142
+ VALUE selector = rb_struct_aref(rule, INT2FIX(RULE_SELECTOR));
1143
+ VALUE rule_id_val = rb_struct_aref(rule, INT2FIX(RULE_ID));
1144
+
1145
+ // Skip empty rules (no declarations)
1146
+ // This handles both empty containers and rules with no properties
1147
+ if (RARRAY_LEN(declarations) == 0) {
1148
+ DEBUG_PRINTF(" [Rule %ld] SKIP: selector='%s' (empty declarations)\n",
1149
+ i, RSTRING_PTR(selector));
1150
+ continue;
1151
+ }
1152
+
1153
+ // Note: We do NOT skip parent rules that have children!
1154
+ // Per CSS spec, parent can have its own declarations AND nested rules.
1155
+ // Example: .parent { color: red; .child { color: blue; } }
1156
+ // Should output both .parent (color: red) and .parent .child (color: blue)
1157
+ // The nesting is already flattened during parsing, so they have different selectors.
1158
+
1159
+ // Get media context for this rule (nil if not in media query)
1160
+ VALUE media_context = rb_hash_aref(rule_media_map, rule_id_val);
1161
+
1162
+ // Build grouping key as [selector, media_context]
1163
+ VALUE group_key = rb_ary_new3(2, selector, media_context);
1164
+
1165
+ DEBUG_PRINTF(" [Rule %ld] ADD: selector='%s', media=%s, %ld declarations\n",
1166
+ i, RSTRING_PTR(selector),
1167
+ NIL_P(media_context) ? "nil" : RSTRING_PTR(rb_inspect(media_context)),
1168
+ RARRAY_LEN(declarations));
1169
+
1170
+ VALUE group = rb_hash_aref(selector_groups, group_key);
1171
+ if (NIL_P(group)) {
1172
+ group = rb_ary_new();
1173
+ rb_hash_aset(selector_groups, group_key, group);
1174
+ DEBUG_PRINTF(" -> Created new selector+media group for '%s' + %s\n",
1175
+ RSTRING_PTR(selector),
1176
+ NIL_P(media_context) ? "nil" : RSTRING_PTR(rb_inspect(media_context)));
1177
+ }
1178
+ rb_ary_push(group, LONG2FIX(i));
1179
+ }
1180
+
1181
+ return selector_groups;
1182
+ }
1183
+
1314
1184
  // Flatten CSS rules by applying cascade rules
1315
1185
  // Input: Stylesheet object or CSS string
1316
1186
  // Output: Stylesheet with flattened declarations (cascade applied)
@@ -1334,13 +1204,6 @@ VALUE cataract_flatten(VALUE self, VALUE input) {
1334
1204
 
1335
1205
  Check_Type(rules_array, T_ARRAY);
1336
1206
 
1337
- // Check if stylesheet has nesting (affects selector rollup)
1338
- int has_nesting = 0;
1339
- if (rb_obj_is_kind_of(input, cStylesheet)) {
1340
- VALUE has_nesting_ivar = rb_ivar_get(input, rb_intern("@_has_nesting"));
1341
- has_nesting = RTEST(has_nesting_ivar);
1342
- }
1343
-
1344
1207
  long num_rules = RARRAY_LEN(rules_array);
1345
1208
  // Empty stylesheets are rare
1346
1209
  if (num_rules == 0) {
@@ -1421,107 +1284,16 @@ VALUE cataract_flatten(VALUE self, VALUE input) {
1421
1284
  * ============================================================================
1422
1285
  */
1423
1286
 
1424
- // For nested CSS: identify parent rules (rules that have children)
1425
- // These should be skipped during flatten, even if they have declarations
1426
- // Use Ruby hash as a set: parent_id => true
1427
- VALUE parent_ids = Qnil;
1428
- if (has_nesting) {
1429
- DEBUG_PRINTF("\n=== FLATTEN: has_nesting=true, num_rules=%ld ===\n", num_rules);
1430
- parent_ids = rb_hash_new();
1431
- for (long i = 0; i < num_rules; i++) {
1432
- VALUE rule = RARRAY_AREF(rules_array, i);
1433
- VALUE parent_rule_id = rb_struct_aref(rule, INT2FIX(RULE_PARENT_RULE_ID));
1434
- DEBUG_PRINTF(" Rule %ld: selector='%s', rule_id=%d, parent_rule_id=%s\n",
1435
- i,
1436
- RSTRING_PTR(rb_struct_aref(rule, INT2FIX(RULE_SELECTOR))),
1437
- FIX2INT(rb_struct_aref(rule, INT2FIX(RULE_ID))),
1438
- NIL_P(parent_rule_id) ? "nil" : RSTRING_PTR(rb_inspect(parent_rule_id)));
1439
- if (!NIL_P(parent_rule_id)) {
1440
- // This rule has a parent, so mark that parent ID
1441
- rb_hash_aset(parent_ids, parent_rule_id, Qtrue);
1442
- }
1443
- }
1444
- }
1445
-
1446
1287
  // Build rule_media_map: rule_id => media_query_id
1447
1288
  // This is used to group rules by (selector, media) instead of just selector
1448
1289
  VALUE input_media_index = Qnil;
1449
- VALUE rule_media_map = rb_hash_new();
1450
- if (rb_obj_is_kind_of(input, cStylesheet)) {
1451
- input_media_index = rb_ivar_get(input, id_ivar_media_index);
1452
-
1453
- // Build map from rules' media_query_id field
1454
- // Only process Rule objects, not AtRules (AtRules don't have media_query_id field at same offset)
1455
- for (long i = 0; i < num_rules; i++) {
1456
- VALUE rule = rb_ary_entry(rules_array, i);
1457
- if (!rb_obj_is_kind_of(rule, cAtRule)) {
1458
- VALUE media_query_id = rb_struct_aref(rule, INT2FIX(RULE_MEDIA_QUERY_ID));
1459
- if (!NIL_P(media_query_id)) {
1460
- VALUE rule_id = rb_struct_aref(rule, INT2FIX(RULE_ID));
1461
- // Store media_query_id as the grouping key
1462
- rb_hash_aset(rule_media_map, rule_id, media_query_id);
1463
- }
1464
- }
1465
- }
1466
- }
1290
+ VALUE rule_media_map = build_rule_media_map(input, rules_array, num_rules, &input_media_index);
1467
1291
 
1468
1292
  // Group rules by (selector, media) instead of just selector
1469
1293
  // Rules with same selector but different media contexts should NOT be merged
1470
- // selector_and_media_key => [rule indices]
1471
- DEBUG_PRINTF("\n=== Building selector+media groups (has_nesting=%d) ===\n", has_nesting);
1472
- VALUE selector_groups = rb_hash_new();
1294
+ DEBUG_PRINTF("\n=== Building selector+media groups ===\n");
1473
1295
  VALUE passthrough_rules = rb_ary_new(); // AtRules to pass through unchanged
1474
-
1475
- for (long i = 0; i < num_rules; i++) {
1476
- VALUE rule = RARRAY_AREF(rules_array, i);
1477
-
1478
- // Handle AtRule objects (@keyframes, @font-face, etc.) - pass through unchanged
1479
- // AtRule has 'content' (string) instead of 'declarations' (array)
1480
- if (rb_obj_is_kind_of(rule, cAtRule)) {
1481
- DEBUG_PRINTF(" [Rule %ld] PASSTHROUGH: AtRule (e.g., @keyframes, @font-face)\n", i);
1482
- rb_ary_push(passthrough_rules, rule);
1483
- continue;
1484
- }
1485
-
1486
- VALUE declarations = rb_struct_aref(rule, INT2FIX(RULE_DECLARATIONS));
1487
- VALUE selector = rb_struct_aref(rule, INT2FIX(RULE_SELECTOR));
1488
- VALUE rule_id_val = rb_struct_aref(rule, INT2FIX(RULE_ID));
1489
-
1490
- // Skip empty rules (no declarations)
1491
- // This handles both empty containers and rules with no properties
1492
- if (RARRAY_LEN(declarations) == 0) {
1493
- DEBUG_PRINTF(" [Rule %ld] SKIP: selector='%s' (empty declarations)\n",
1494
- i, RSTRING_PTR(selector));
1495
- continue;
1496
- }
1497
-
1498
- // Note: We do NOT skip parent rules that have children!
1499
- // Per CSS spec, parent can have its own declarations AND nested rules.
1500
- // Example: .parent { color: red; .child { color: blue; } }
1501
- // Should output both .parent (color: red) and .parent .child (color: blue)
1502
- // The nesting is already flattened during parsing, so they have different selectors.
1503
-
1504
- // Get media context for this rule (nil if not in media query)
1505
- VALUE media_context = rb_hash_aref(rule_media_map, rule_id_val);
1506
-
1507
- // Build grouping key as [selector, media_context]
1508
- VALUE group_key = rb_ary_new3(2, selector, media_context);
1509
-
1510
- DEBUG_PRINTF(" [Rule %ld] ADD: selector='%s', media=%s, %ld declarations\n",
1511
- i, RSTRING_PTR(selector),
1512
- NIL_P(media_context) ? "nil" : RSTRING_PTR(rb_inspect(media_context)),
1513
- RARRAY_LEN(declarations));
1514
-
1515
- VALUE group = rb_hash_aref(selector_groups, group_key);
1516
- if (NIL_P(group)) {
1517
- group = rb_ary_new();
1518
- rb_hash_aset(selector_groups, group_key, group);
1519
- DEBUG_PRINTF(" -> Created new selector+media group for '%s' + %s\n",
1520
- RSTRING_PTR(selector),
1521
- NIL_P(media_context) ? "nil" : RSTRING_PTR(rb_inspect(media_context)));
1522
- }
1523
- rb_ary_push(group, LONG2FIX(i));
1524
- }
1296
+ VALUE selector_groups = group_rules_by_selector_and_media(rules_array, num_rules, rule_media_map, passthrough_rules);
1525
1297
  DEBUG_PRINTF("=== Total selector+media groups: %ld ===\n\n", RHASH_SIZE(selector_groups));
1526
1298
 
1527
1299
  // ALWAYS group by selector and keep them separate
@@ -1559,9 +1331,7 @@ VALUE cataract_flatten(VALUE self, VALUE input) {
1559
1331
  }
1560
1332
 
1561
1333
  return passthrough_sheet;
1562
- }
1563
-
1564
- if (RHASH_SIZE(selector_groups) > 0) {
1334
+ } else {
1565
1335
  DEBUG_PRINTF(" -> Taking SELECTOR-GROUPED path (%ld unique selectors)\n",
1566
1336
  RHASH_SIZE(selector_groups));
1567
1337
  VALUE merged_sheet = rb_class_new_instance(0, NULL, cStylesheet);
@@ -1665,486 +1435,4 @@ VALUE cataract_flatten(VALUE self, VALUE input) {
1665
1435
  return merged_sheet;
1666
1436
  }
1667
1437
 
1668
- // Single-merge path: merge all rules into one
1669
- VALUE properties_hash = rb_hash_new();
1670
-
1671
- // Track selector for rollup (minimize allocations)
1672
- // Store pointer + length to first non-parent selector
1673
- // Also keep the VALUE alive since we extract C pointer before allocations
1674
- const char *first_selector_ptr = NULL;
1675
- long first_selector_len = 0;
1676
- VALUE first_selector_value = Qnil;
1677
- int all_same_selector = 1;
1678
-
1679
- // Track source order for cascade rules
1680
- long source_order = 0;
1681
-
1682
- // Iterate through each rule
1683
- for (long i = 0; i < num_rules; i++) {
1684
- VALUE rule = RARRAY_AREF(rules_array, i);
1685
- Check_Type(rule, T_STRUCT);
1686
-
1687
- // Extract rule fields
1688
- VALUE rule_id = rb_struct_aref(rule, INT2FIX(RULE_ID));
1689
- VALUE selector = rb_struct_aref(rule, INT2FIX(RULE_SELECTOR));
1690
- VALUE declarations = rb_struct_aref(rule, INT2FIX(RULE_DECLARATIONS));
1691
-
1692
- // Skip parent rules when handling nested CSS
1693
- // Example: .button { color: black; &:hover { color: red; } }
1694
- // - Rule id=0, selector=".button", declarations=[color: black] (SKIP - has children)
1695
- // - Rule id=1, selector=".button:hover", declarations=[color: red] (PROCESS)
1696
- if (has_nesting && !NIL_P(parent_ids)) {
1697
- VALUE is_parent = rb_hash_aref(parent_ids, rule_id);
1698
- if (RTEST(is_parent)) {
1699
- continue;
1700
- }
1701
- }
1702
-
1703
- long num_decls = RARRAY_LEN(declarations);
1704
- // Skip rules with no declarations (empty parent containers)
1705
- if (num_decls == 0) {
1706
- continue;
1707
- }
1708
-
1709
- // Track selectors for rollup (delay allocation)
1710
- const char *sel_ptr = RSTRING_PTR(selector);
1711
- long sel_len = RSTRING_LEN(selector);
1712
- if (first_selector_ptr == NULL) {
1713
- first_selector_ptr = sel_ptr;
1714
- first_selector_len = sel_len;
1715
- first_selector_value = selector; // Keep VALUE alive for RB_GC_GUARD
1716
- } else if (all_same_selector) {
1717
- if (sel_len != first_selector_len || memcmp(sel_ptr, first_selector_ptr, sel_len) != 0) {
1718
- all_same_selector = 0;
1719
- }
1720
- }
1721
-
1722
- VALUE specificity_val = rb_struct_aref(rule, INT2FIX(RULE_SPECIFICITY));
1723
-
1724
- // Calculate specificity if not provided (lazy)
1725
- int specificity = 0;
1726
- if (NIL_P(specificity_val)) {
1727
- specificity_val = calculate_specificity(Qnil, selector);
1728
- // Cache the calculated value back to the struct
1729
- rb_struct_aset(rule, INT2FIX(RULE_SPECIFICITY), specificity_val);
1730
- }
1731
- specificity = NUM2INT(specificity_val);
1732
-
1733
- // Process each declaration in this rule
1734
- Check_Type(declarations, T_ARRAY);
1735
-
1736
- for (long j = 0; j < num_decls; j++) {
1737
- VALUE decl = RARRAY_AREF(declarations, j);
1738
-
1739
- // Extract property, value, important from Declaration struct
1740
- VALUE property = rb_struct_aref(decl, INT2FIX(DECL_PROPERTY));
1741
- VALUE value = rb_struct_aref(decl, INT2FIX(DECL_VALUE));
1742
- VALUE important = rb_struct_aref(decl, INT2FIX(DECL_IMPORTANT));
1743
-
1744
- // Properties are already lowercased during parsing (see cataract_new.c)
1745
- // No need to lowercase again
1746
- int is_important = RTEST(important);
1747
-
1748
- // Expand shorthand properties if needed
1749
- // Most properties are NOT shorthands, so hint compiler accordingly
1750
- const char *prop_str = StringValueCStr(property);
1751
- VALUE expanded = Qnil;
1752
-
1753
- // Early exit: shorthand properties only start with m, p, b, f, or l
1754
- char first_char = prop_str[0];
1755
- if (first_char == 'm' || first_char == 'p' || first_char == 'b' ||
1756
- first_char == 'f' || first_char == 'l') {
1757
- // Potentially a shorthand - check specific property names
1758
- if (strcmp(prop_str, "margin") == 0) {
1759
- expanded = cataract_expand_margin(Qnil, value);
1760
- } else if (strcmp(prop_str, "padding") == 0) {
1761
- expanded = cataract_expand_padding(Qnil, value);
1762
- } else if (strcmp(prop_str, "border") == 0) {
1763
- expanded = cataract_expand_border(Qnil, value);
1764
- } else if (strcmp(prop_str, "border-color") == 0) {
1765
- expanded = cataract_expand_border_color(Qnil, value);
1766
- } else if (strcmp(prop_str, "border-style") == 0) {
1767
- expanded = cataract_expand_border_style(Qnil, value);
1768
- } else if (strcmp(prop_str, "border-width") == 0) {
1769
- expanded = cataract_expand_border_width(Qnil, value);
1770
- } else if (strcmp(prop_str, "border-top") == 0) {
1771
- expanded = cataract_expand_border_side(Qnil, USASCII_STR("top"), value);
1772
- } else if (strcmp(prop_str, "border-right") == 0) {
1773
- expanded = cataract_expand_border_side(Qnil, USASCII_STR("right"), value);
1774
- } else if (strcmp(prop_str, "border-bottom") == 0) {
1775
- expanded = cataract_expand_border_side(Qnil, USASCII_STR("bottom"), value);
1776
- } else if (strcmp(prop_str, "border-left") == 0) {
1777
- expanded = cataract_expand_border_side(Qnil, USASCII_STR("left"), value);
1778
- } else if (strcmp(prop_str, "font") == 0) {
1779
- expanded = cataract_expand_font(Qnil, value);
1780
- } else if (strcmp(prop_str, "list-style") == 0) {
1781
- expanded = cataract_expand_list_style(Qnil, value);
1782
- } else if (strcmp(prop_str, "background") == 0) {
1783
- expanded = cataract_expand_background(Qnil, value);
1784
- }
1785
- }
1786
- // If first_char doesn't match, expanded stays Qnil
1787
-
1788
- // If property was expanded, iterate array and apply cascade
1789
- // Expansion is rare (most properties are not shorthands)
1790
- if (!NIL_P(expanded)) {
1791
- Check_Type(expanded, T_ARRAY);
1792
-
1793
- struct expand_context ctx;
1794
- ctx.properties_hash = properties_hash;
1795
- ctx.source_order = source_order;
1796
- ctx.specificity = specificity;
1797
- ctx.important = important;
1798
-
1799
- long expanded_len = RARRAY_LEN(expanded);
1800
- for (long i = 0; i < expanded_len; i++) {
1801
- VALUE decl = rb_ary_entry(expanded, i);
1802
- VALUE prop = rb_struct_aref(decl, INT2FIX(DECL_PROPERTY));
1803
- VALUE val = rb_struct_aref(decl, INT2FIX(DECL_VALUE));
1804
- flatten_expanded_callback(prop, val, (VALUE)&ctx);
1805
- }
1806
-
1807
- RB_GC_GUARD(expanded);
1808
- continue; // Skip processing the original shorthand property
1809
- }
1810
-
1811
- // Apply CSS cascade rules
1812
- VALUE existing = rb_hash_aref(properties_hash, property);
1813
-
1814
- // In merge scenarios, properties often collide (same property in multiple rules)
1815
- // so existing property is the common case
1816
- if (NIL_P(existing)) {
1817
- // New property - add it as array: [source_order, specificity, important, value]
1818
- VALUE prop_data = rb_ary_new_capa(4);
1819
- rb_ary_push(prop_data, LONG2NUM(source_order));
1820
- rb_ary_push(prop_data, INT2NUM(specificity));
1821
- rb_ary_push(prop_data, important);
1822
- rb_ary_push(prop_data, value);
1823
- rb_hash_aset(properties_hash, property, prop_data);
1824
- } else {
1825
- // Property exists - check cascade rules
1826
- long existing_order = NUM2LONG(RARRAY_AREF(existing, PROP_SOURCE_ORDER));
1827
- int existing_spec_int = NUM2INT(RARRAY_AREF(existing, PROP_SPECIFICITY));
1828
- VALUE existing_important = RARRAY_AREF(existing, PROP_IMPORTANT);
1829
- int existing_is_important = RTEST(existing_important);
1830
-
1831
- int should_replace = 0;
1832
-
1833
- // Most declarations are NOT !important
1834
- if (is_important) {
1835
- // New is !important - wins if existing is NOT important OR higher specificity OR (equal specificity AND later order)
1836
- if (!existing_is_important || existing_spec_int < specificity ||
1837
- (existing_spec_int == specificity && existing_order <= source_order)) {
1838
- should_replace = 1;
1839
- }
1840
- } else {
1841
- // New is NOT important - only wins if existing is also NOT important AND (higher specificity OR equal specificity with later order)
1842
- if (!existing_is_important &&
1843
- (existing_spec_int < specificity ||
1844
- (existing_spec_int == specificity && existing_order <= source_order))) {
1845
- should_replace = 1;
1846
- }
1847
- }
1848
-
1849
- // Replacement is common in merge scenarios
1850
- if (should_replace) {
1851
- RARRAY_ASET(existing, PROP_SOURCE_ORDER, LONG2NUM(source_order));
1852
- RARRAY_ASET(existing, PROP_SPECIFICITY, INT2NUM(specificity));
1853
- RARRAY_ASET(existing, PROP_IMPORTANT, important);
1854
- RARRAY_ASET(existing, PROP_VALUE, value);
1855
- }
1856
- }
1857
-
1858
- RB_GC_GUARD(property);
1859
- RB_GC_GUARD(value);
1860
- RB_GC_GUARD(decl);
1861
- source_order++;
1862
- }
1863
-
1864
- RB_GC_GUARD(selector);
1865
- RB_GC_GUARD(declarations);
1866
- RB_GC_GUARD(rule);
1867
- }
1868
-
1869
- // Create shorthand from longhand properties
1870
- // Uses cached static strings to avoid runtime allocation
1871
-
1872
- // Try to create margin shorthand
1873
- TRY_CREATE_FOUR_SIDED_SHORTHAND(properties_hash,
1874
- str_margin_top, str_margin_right, str_margin_bottom, str_margin_left,
1875
- str_margin, cataract_create_margin_shorthand);
1876
-
1877
- // Try to create padding shorthand
1878
- TRY_CREATE_FOUR_SIDED_SHORTHAND(properties_hash,
1879
- str_padding_top, str_padding_right, str_padding_bottom, str_padding_left,
1880
- str_padding, cataract_create_padding_shorthand);
1881
-
1882
- // Create border-width from individual sides
1883
- TRY_CREATE_FOUR_SIDED_SHORTHAND(properties_hash,
1884
- str_border_top_width, str_border_right_width, str_border_bottom_width, str_border_left_width,
1885
- str_border_width, cataract_create_border_width_shorthand);
1886
-
1887
- // Create border-style from individual sides
1888
- TRY_CREATE_FOUR_SIDED_SHORTHAND(properties_hash,
1889
- str_border_top_style, str_border_right_style, str_border_bottom_style, str_border_left_style,
1890
- str_border_style, cataract_create_border_style_shorthand);
1891
-
1892
- // Create border-color from individual sides
1893
- TRY_CREATE_FOUR_SIDED_SHORTHAND(properties_hash,
1894
- str_border_top_color, str_border_right_color, str_border_bottom_color, str_border_left_color,
1895
- str_border_color, cataract_create_border_color_shorthand);
1896
-
1897
- // Now create border shorthand from border-{width,style,color}
1898
- VALUE border_width = GET_PROP_VALUE_STR(properties_hash, str_border_width);
1899
- VALUE border_style = GET_PROP_VALUE_STR(properties_hash, str_border_style);
1900
- VALUE border_color = GET_PROP_VALUE_STR(properties_hash, str_border_color);
1901
-
1902
- if (!NIL_P(border_width) || !NIL_P(border_style) || !NIL_P(border_color)) {
1903
- // Use first available property's metadata as reference
1904
- VALUE border_data_src = !NIL_P(border_width) ? GET_PROP_DATA_STR(properties_hash, str_border_width) :
1905
- !NIL_P(border_style) ? GET_PROP_DATA_STR(properties_hash, str_border_style) :
1906
- GET_PROP_DATA_STR(properties_hash, str_border_color);
1907
- VALUE border_important = RARRAY_AREF(border_data_src, PROP_IMPORTANT);
1908
- int border_is_important = RTEST(border_important);
1909
-
1910
- // Check that all present properties have the same !important flag
1911
- int important_match = CHECK_IMPORTANT_MATCH(properties_hash, str_border_width, border_is_important) &&
1912
- CHECK_IMPORTANT_MATCH(properties_hash, str_border_style, border_is_important) &&
1913
- CHECK_IMPORTANT_MATCH(properties_hash, str_border_color, border_is_important);
1914
-
1915
- if (important_match) {
1916
- VALUE border_props = rb_hash_new();
1917
- if (!NIL_P(border_width)) rb_hash_aset(border_props, str_border_width, border_width);
1918
- if (!NIL_P(border_style)) rb_hash_aset(border_props, str_border_style, border_style);
1919
- if (!NIL_P(border_color)) rb_hash_aset(border_props, str_border_color, border_color);
1920
-
1921
- VALUE border_shorthand = cataract_create_border_shorthand(Qnil, border_props);
1922
- if (!NIL_P(border_shorthand)) {
1923
- VALUE border_data = rb_ary_new_capa(4);
1924
- rb_ary_push(border_data, RARRAY_AREF(border_data_src, PROP_SOURCE_ORDER));
1925
- rb_ary_push(border_data, RARRAY_AREF(border_data_src, PROP_SPECIFICITY));
1926
- rb_ary_push(border_data, border_important);
1927
- rb_ary_push(border_data, border_shorthand);
1928
- rb_hash_aset(properties_hash, str_border, border_data);
1929
-
1930
- if (!NIL_P(border_width)) rb_hash_delete(properties_hash, str_border_width);
1931
- if (!NIL_P(border_style)) rb_hash_delete(properties_hash, str_border_style);
1932
- if (!NIL_P(border_color)) rb_hash_delete(properties_hash, str_border_color);
1933
- }
1934
- RB_GC_GUARD(border_props);
1935
- RB_GC_GUARD(border_shorthand);
1936
- }
1937
- }
1938
-
1939
- // Try to create font shorthand
1940
- VALUE font_size = GET_PROP_VALUE_STR(properties_hash, str_font_size);
1941
- VALUE font_family = GET_PROP_VALUE_STR(properties_hash, str_font_family);
1942
-
1943
- // Font shorthand requires at least font-size and font-family
1944
- if (!NIL_P(font_size) && !NIL_P(font_family)) {
1945
- VALUE font_style = GET_PROP_VALUE_STR(properties_hash, str_font_style);
1946
- VALUE font_variant = GET_PROP_VALUE_STR(properties_hash, str_font_variant);
1947
- VALUE font_weight = GET_PROP_VALUE_STR(properties_hash, str_font_weight);
1948
- VALUE line_height = GET_PROP_VALUE_STR(properties_hash, str_line_height);
1949
-
1950
- // Get metadata from font-size as reference
1951
- VALUE size_data = GET_PROP_DATA_STR(properties_hash, str_font_size);
1952
- VALUE font_important = RARRAY_AREF(size_data, PROP_IMPORTANT);
1953
- int font_is_important = RTEST(font_important);
1954
-
1955
- // Check that all present properties have the same !important flag
1956
- int important_match = CHECK_IMPORTANT_MATCH(properties_hash, str_font_style, font_is_important) &&
1957
- CHECK_IMPORTANT_MATCH(properties_hash, str_font_variant, font_is_important) &&
1958
- CHECK_IMPORTANT_MATCH(properties_hash, str_font_weight, font_is_important) &&
1959
- CHECK_IMPORTANT_MATCH(properties_hash, str_line_height, font_is_important) &&
1960
- CHECK_IMPORTANT_MATCH(properties_hash, str_font_family, font_is_important);
1961
-
1962
- if (important_match) {
1963
- VALUE font_props = rb_hash_new();
1964
- if (!NIL_P(font_style)) rb_hash_aset(font_props, str_font_style, font_style);
1965
- if (!NIL_P(font_variant)) rb_hash_aset(font_props, str_font_variant, font_variant);
1966
- if (!NIL_P(font_weight)) rb_hash_aset(font_props, str_font_weight, font_weight);
1967
- rb_hash_aset(font_props, str_font_size, font_size);
1968
- if (!NIL_P(line_height)) rb_hash_aset(font_props, str_line_height, line_height);
1969
- rb_hash_aset(font_props, str_font_family, font_family);
1970
-
1971
- VALUE font_shorthand = cataract_create_font_shorthand(Qnil, font_props);
1972
- if (!NIL_P(font_shorthand)) {
1973
- VALUE font_data = rb_ary_new_capa(4);
1974
- rb_ary_push(font_data, RARRAY_AREF(size_data, PROP_SOURCE_ORDER));
1975
- rb_ary_push(font_data, RARRAY_AREF(size_data, PROP_SPECIFICITY));
1976
- rb_ary_push(font_data, font_important);
1977
- rb_ary_push(font_data, font_shorthand);
1978
- rb_hash_aset(properties_hash, str_font, font_data);
1979
-
1980
- // Remove longhand properties
1981
- if (!NIL_P(font_style)) rb_hash_delete(properties_hash, str_font_style);
1982
- if (!NIL_P(font_variant)) rb_hash_delete(properties_hash, str_font_variant);
1983
- if (!NIL_P(font_weight)) rb_hash_delete(properties_hash, str_font_weight);
1984
- rb_hash_delete(properties_hash, str_font_size);
1985
- if (!NIL_P(line_height)) rb_hash_delete(properties_hash, str_line_height);
1986
- rb_hash_delete(properties_hash, str_font_family);
1987
- }
1988
- RB_GC_GUARD(font_props);
1989
- RB_GC_GUARD(font_shorthand);
1990
- }
1991
- }
1992
-
1993
- // Try to create list-style shorthand
1994
- VALUE list_style_type = GET_PROP_VALUE_STR(properties_hash, str_list_style_type);
1995
- VALUE list_style_position = GET_PROP_VALUE_STR(properties_hash, str_list_style_position);
1996
- VALUE list_style_image = GET_PROP_VALUE_STR(properties_hash, str_list_style_image);
1997
-
1998
- // List-style shorthand requires at least 2 properties
1999
- int list_style_count = (!NIL_P(list_style_type) ? 1 : 0) +
2000
- (!NIL_P(list_style_position) ? 1 : 0) +
2001
- (!NIL_P(list_style_image) ? 1 : 0);
2002
-
2003
- if (list_style_count >= 2) {
2004
- // Use first available property's metadata as reference
2005
- VALUE list_style_data_src = !NIL_P(list_style_type) ? GET_PROP_DATA_STR(properties_hash, str_list_style_type) :
2006
- !NIL_P(list_style_position) ? GET_PROP_DATA_STR(properties_hash, str_list_style_position) :
2007
- GET_PROP_DATA_STR(properties_hash, str_list_style_image);
2008
- VALUE list_style_important = RARRAY_AREF(list_style_data_src, PROP_IMPORTANT);
2009
- int list_style_is_important = RTEST(list_style_important);
2010
-
2011
- // Check that all present properties have the same !important flag
2012
- int important_match = CHECK_IMPORTANT_MATCH(properties_hash, str_list_style_type, list_style_is_important) &&
2013
- CHECK_IMPORTANT_MATCH(properties_hash, str_list_style_position, list_style_is_important) &&
2014
- CHECK_IMPORTANT_MATCH(properties_hash, str_list_style_image, list_style_is_important);
2015
-
2016
- if (important_match) {
2017
- VALUE list_style_props = rb_hash_new();
2018
- if (!NIL_P(list_style_type)) rb_hash_aset(list_style_props, str_list_style_type, list_style_type);
2019
- if (!NIL_P(list_style_position)) rb_hash_aset(list_style_props, str_list_style_position, list_style_position);
2020
- if (!NIL_P(list_style_image)) rb_hash_aset(list_style_props, str_list_style_image, list_style_image);
2021
-
2022
- VALUE list_style_shorthand = cataract_create_list_style_shorthand(Qnil, list_style_props);
2023
- if (!NIL_P(list_style_shorthand)) {
2024
- VALUE list_style_data = rb_ary_new_capa(4);
2025
- rb_ary_push(list_style_data, RARRAY_AREF(list_style_data_src, PROP_SOURCE_ORDER));
2026
- rb_ary_push(list_style_data, RARRAY_AREF(list_style_data_src, PROP_SPECIFICITY));
2027
- rb_ary_push(list_style_data, list_style_important);
2028
- rb_ary_push(list_style_data, list_style_shorthand);
2029
- rb_hash_aset(properties_hash, str_list_style, list_style_data);
2030
-
2031
- // Remove longhand properties
2032
- if (!NIL_P(list_style_type)) rb_hash_delete(properties_hash, str_list_style_type);
2033
- if (!NIL_P(list_style_position)) rb_hash_delete(properties_hash, str_list_style_position);
2034
- if (!NIL_P(list_style_image)) rb_hash_delete(properties_hash, str_list_style_image);
2035
- }
2036
- RB_GC_GUARD(list_style_props);
2037
- RB_GC_GUARD(list_style_shorthand);
2038
- }
2039
- }
2040
-
2041
- // Try to create background shorthand
2042
- VALUE background_color = GET_PROP_VALUE_STR(properties_hash, str_background_color);
2043
- VALUE background_image = GET_PROP_VALUE_STR(properties_hash, str_background_image);
2044
- VALUE background_repeat = GET_PROP_VALUE_STR(properties_hash, str_background_repeat);
2045
- VALUE background_attachment = GET_PROP_VALUE_STR(properties_hash, str_background_attachment);
2046
- VALUE background_position = GET_PROP_VALUE_STR(properties_hash, str_background_position);
2047
-
2048
- // Background shorthand requires at least 2 properties
2049
- int background_count = (!NIL_P(background_color) ? 1 : 0) +
2050
- (!NIL_P(background_image) ? 1 : 0) +
2051
- (!NIL_P(background_repeat) ? 1 : 0) +
2052
- (!NIL_P(background_attachment) ? 1 : 0) +
2053
- (!NIL_P(background_position) ? 1 : 0);
2054
-
2055
- if (background_count >= 2) {
2056
- // Use first available property's metadata as reference
2057
- VALUE background_data_src = !NIL_P(background_color) ? GET_PROP_DATA_STR(properties_hash, str_background_color) :
2058
- !NIL_P(background_image) ? GET_PROP_DATA_STR(properties_hash, str_background_image) :
2059
- !NIL_P(background_repeat) ? GET_PROP_DATA_STR(properties_hash, str_background_repeat) :
2060
- !NIL_P(background_attachment) ? GET_PROP_DATA_STR(properties_hash, str_background_attachment) :
2061
- GET_PROP_DATA_STR(properties_hash, str_background_position);
2062
- VALUE background_important = RARRAY_AREF(background_data_src, PROP_IMPORTANT);
2063
- int background_is_important = RTEST(background_important);
2064
-
2065
- // Check that all present properties have the same !important flag
2066
- int important_match = CHECK_IMPORTANT_MATCH(properties_hash, str_background_color, background_is_important) &&
2067
- CHECK_IMPORTANT_MATCH(properties_hash, str_background_image, background_is_important) &&
2068
- CHECK_IMPORTANT_MATCH(properties_hash, str_background_repeat, background_is_important) &&
2069
- CHECK_IMPORTANT_MATCH(properties_hash, str_background_attachment, background_is_important) &&
2070
- CHECK_IMPORTANT_MATCH(properties_hash, str_background_position, background_is_important);
2071
-
2072
- if (important_match) {
2073
- VALUE background_props = rb_hash_new();
2074
- if (!NIL_P(background_color)) rb_hash_aset(background_props, str_background_color, background_color);
2075
- if (!NIL_P(background_image)) rb_hash_aset(background_props, str_background_image, background_image);
2076
- if (!NIL_P(background_repeat)) rb_hash_aset(background_props, str_background_repeat, background_repeat);
2077
- if (!NIL_P(background_attachment)) rb_hash_aset(background_props, str_background_attachment, background_attachment);
2078
- if (!NIL_P(background_position)) rb_hash_aset(background_props, str_background_position, background_position);
2079
-
2080
- VALUE background_shorthand = cataract_create_background_shorthand(Qnil, background_props);
2081
- if (!NIL_P(background_shorthand)) {
2082
- VALUE background_data = rb_ary_new_capa(4);
2083
- rb_ary_push(background_data, RARRAY_AREF(background_data_src, PROP_SOURCE_ORDER));
2084
- rb_ary_push(background_data, RARRAY_AREF(background_data_src, PROP_SPECIFICITY));
2085
- rb_ary_push(background_data, background_important);
2086
- rb_ary_push(background_data, background_shorthand);
2087
- rb_hash_aset(properties_hash, str_background, background_data);
2088
-
2089
- // Remove longhand properties
2090
- if (!NIL_P(background_color)) rb_hash_delete(properties_hash, str_background_color);
2091
- if (!NIL_P(background_image)) rb_hash_delete(properties_hash, str_background_image);
2092
- if (!NIL_P(background_repeat)) rb_hash_delete(properties_hash, str_background_repeat);
2093
- if (!NIL_P(background_attachment)) rb_hash_delete(properties_hash, str_background_attachment);
2094
- if (!NIL_P(background_position)) rb_hash_delete(properties_hash, str_background_position);
2095
- }
2096
- RB_GC_GUARD(background_props);
2097
- RB_GC_GUARD(background_shorthand);
2098
- }
2099
- }
2100
-
2101
- #undef GET_PROP_VALUE
2102
- #undef GET_PROP_DATA
2103
-
2104
- // Build merged declarations array
2105
- VALUE merged_declarations = rb_ary_new();
2106
- rb_hash_foreach(properties_hash, flatten_build_result_callback, merged_declarations);
2107
-
2108
- // Determine final selector (allocate only once at the end)
2109
- VALUE final_selector;
2110
- if (has_nesting && all_same_selector && first_selector_ptr != NULL) {
2111
- // All rules have same selector - use it for rollup
2112
- final_selector = rb_usascii_str_new(first_selector_ptr, first_selector_len);
2113
- } else {
2114
- // Mixed selectors or no nesting - use "merged"
2115
- final_selector = str_merged_selector;
2116
- }
2117
-
2118
- // Create a new Stylesheet with a single merged rule
2119
- // Use rb_class_new_instance instead of rb_funcall for better performance
2120
- VALUE merged_sheet = rb_class_new_instance(0, NULL, cStylesheet);
2121
-
2122
- // Create merged rule
2123
- VALUE merged_rule = rb_struct_new(cRule,
2124
- INT2FIX(0), // id
2125
- final_selector, // selector (rolled-up or "merged")
2126
- merged_declarations, // declarations
2127
- Qnil, // specificity (not applicable)
2128
- Qnil, // parent_rule_id (not nested)
2129
- Qnil, // nesting_style (not nested)
2130
- Qnil, // selector_list_id
2131
- Qnil // media_query_id
2132
- );
2133
-
2134
- // Set @rules array with single merged rule (use cached ID)
2135
- VALUE rules_ary = rb_ary_new_from_args(1, merged_rule);
2136
- rb_ivar_set(merged_sheet, id_ivar_rules, rules_ary);
2137
-
2138
- // Set @media_index with :all pointing to rule 0 (use cached ID)
2139
- VALUE media_idx = rb_hash_new();
2140
- VALUE all_ids = rb_ary_new_from_args(1, INT2FIX(0));
2141
- rb_hash_aset(media_idx, ID2SYM(id_all), all_ids);
2142
- rb_ivar_set(merged_sheet, id_ivar_media_index, media_idx);
2143
-
2144
- // Guard first_selector_value: C pointer extracted via RSTRING_PTR during iteration,
2145
- // then used after many allocations (hash operations, shorthand expansions) when
2146
- // creating final_selector with rb_usascii_str_new
2147
- RB_GC_GUARD(first_selector_value);
2148
-
2149
- return merged_sheet;
2150
1438
  }