cataract 0.1.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 (90) hide show
  1. checksums.yaml +7 -0
  2. data/.clang-tidy +30 -0
  3. data/.github/workflows/ci-macos.yml +12 -0
  4. data/.github/workflows/ci.yml +77 -0
  5. data/.github/workflows/test.yml +76 -0
  6. data/.gitignore +45 -0
  7. data/.overcommit.yml +38 -0
  8. data/.rubocop.yml +83 -0
  9. data/BENCHMARKS.md +201 -0
  10. data/CHANGELOG.md +1 -0
  11. data/Gemfile +27 -0
  12. data/LICENSE +21 -0
  13. data/RAGEL_MIGRATION.md +60 -0
  14. data/README.md +292 -0
  15. data/Rakefile +209 -0
  16. data/benchmarks/benchmark_harness.rb +193 -0
  17. data/benchmarks/benchmark_merging.rb +121 -0
  18. data/benchmarks/benchmark_optimization_comparison.rb +168 -0
  19. data/benchmarks/benchmark_parsing.rb +153 -0
  20. data/benchmarks/benchmark_ragel_removal.rb +56 -0
  21. data/benchmarks/benchmark_runner.rb +70 -0
  22. data/benchmarks/benchmark_serialization.rb +180 -0
  23. data/benchmarks/benchmark_shorthand.rb +109 -0
  24. data/benchmarks/benchmark_shorthand_expansion.rb +176 -0
  25. data/benchmarks/benchmark_specificity.rb +124 -0
  26. data/benchmarks/benchmark_string_allocation.rb +151 -0
  27. data/benchmarks/benchmark_stylesheet_to_s.rb +62 -0
  28. data/benchmarks/benchmark_to_s_cached.rb +55 -0
  29. data/benchmarks/benchmark_value_splitter.rb +54 -0
  30. data/benchmarks/benchmark_yjit.rb +158 -0
  31. data/benchmarks/benchmark_yjit_workers.rb +61 -0
  32. data/benchmarks/profile_to_s.rb +23 -0
  33. data/benchmarks/speedup_calculator.rb +83 -0
  34. data/benchmarks/system_metadata.rb +81 -0
  35. data/benchmarks/templates/benchmarks.md.erb +221 -0
  36. data/benchmarks/yjit_tests.rb +141 -0
  37. data/cataract.gemspec +34 -0
  38. data/cliff.toml +92 -0
  39. data/examples/color_conversion_visual_test/color_conversion_test.html +3603 -0
  40. data/examples/color_conversion_visual_test/generate.rb +202 -0
  41. data/examples/color_conversion_visual_test/template.html.erb +259 -0
  42. data/examples/css_analyzer/analyzer.rb +164 -0
  43. data/examples/css_analyzer/analyzers/base.rb +33 -0
  44. data/examples/css_analyzer/analyzers/colors.rb +133 -0
  45. data/examples/css_analyzer/analyzers/important.rb +88 -0
  46. data/examples/css_analyzer/analyzers/properties.rb +61 -0
  47. data/examples/css_analyzer/analyzers/specificity.rb +68 -0
  48. data/examples/css_analyzer/templates/report.html.erb +575 -0
  49. data/examples/css_analyzer.rb +69 -0
  50. data/examples/github_analysis.html +5343 -0
  51. data/ext/cataract/cataract.c +1086 -0
  52. data/ext/cataract/cataract.h +174 -0
  53. data/ext/cataract/css_parser.c +1435 -0
  54. data/ext/cataract/extconf.rb +48 -0
  55. data/ext/cataract/import_scanner.c +174 -0
  56. data/ext/cataract/merge.c +973 -0
  57. data/ext/cataract/shorthand_expander.c +902 -0
  58. data/ext/cataract/specificity.c +213 -0
  59. data/ext/cataract/value_splitter.c +116 -0
  60. data/ext/cataract_color/cataract_color.c +16 -0
  61. data/ext/cataract_color/color_conversion.c +1687 -0
  62. data/ext/cataract_color/color_conversion.h +136 -0
  63. data/ext/cataract_color/color_conversion_lab.c +571 -0
  64. data/ext/cataract_color/color_conversion_named.c +259 -0
  65. data/ext/cataract_color/color_conversion_oklab.c +547 -0
  66. data/ext/cataract_color/extconf.rb +23 -0
  67. data/ext/cataract_old/cataract.c +393 -0
  68. data/ext/cataract_old/cataract.h +250 -0
  69. data/ext/cataract_old/css_parser.c +933 -0
  70. data/ext/cataract_old/extconf.rb +67 -0
  71. data/ext/cataract_old/import_scanner.c +174 -0
  72. data/ext/cataract_old/merge.c +776 -0
  73. data/ext/cataract_old/shorthand_expander.c +902 -0
  74. data/ext/cataract_old/specificity.c +213 -0
  75. data/ext/cataract_old/stylesheet.c +290 -0
  76. data/ext/cataract_old/value_splitter.c +116 -0
  77. data/lib/cataract/at_rule.rb +97 -0
  78. data/lib/cataract/color_conversion.rb +18 -0
  79. data/lib/cataract/declarations.rb +332 -0
  80. data/lib/cataract/import_resolver.rb +210 -0
  81. data/lib/cataract/rule.rb +131 -0
  82. data/lib/cataract/stylesheet.rb +716 -0
  83. data/lib/cataract/stylesheet_scope.rb +257 -0
  84. data/lib/cataract/version.rb +5 -0
  85. data/lib/cataract.rb +107 -0
  86. data/lib/tasks/gem.rake +158 -0
  87. data/scripts/fuzzer/run.rb +828 -0
  88. data/scripts/fuzzer/worker.rb +99 -0
  89. data/scripts/generate_benchmarks_md.rb +155 -0
  90. metadata +135 -0
@@ -0,0 +1,973 @@
1
+ #include "cataract.h"
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;
7
+ static ID id_all = 0;
8
+
9
+ // Cached ivar IDs for Stylesheet
10
+ static ID id_ivar_rules = 0;
11
+ static ID id_ivar_media_index = 0;
12
+
13
+ // Cached "merged" selector string
14
+ static VALUE str_merged_selector = Qnil;
15
+
16
+ // Cached property name strings (frozen, never GC'd)
17
+ // Initialized in init_merge_constants() at module load time
18
+ static VALUE str_margin = Qnil;
19
+ static VALUE str_margin_top = Qnil;
20
+ static VALUE str_margin_right = Qnil;
21
+ static VALUE str_margin_bottom = Qnil;
22
+ static VALUE str_margin_left = Qnil;
23
+ static VALUE str_padding = Qnil;
24
+ static VALUE str_padding_top = Qnil;
25
+ static VALUE str_padding_right = Qnil;
26
+ static VALUE str_padding_bottom = Qnil;
27
+ static VALUE str_padding_left = Qnil;
28
+ static VALUE str_border_width = Qnil;
29
+ static VALUE str_border_top_width = Qnil;
30
+ static VALUE str_border_right_width = Qnil;
31
+ static VALUE str_border_bottom_width = Qnil;
32
+ static VALUE str_border_left_width = Qnil;
33
+ static VALUE str_border_style = Qnil;
34
+ static VALUE str_border_top_style = Qnil;
35
+ static VALUE str_border_right_style = Qnil;
36
+ static VALUE str_border_bottom_style = Qnil;
37
+ static VALUE str_border_left_style = Qnil;
38
+ static VALUE str_border_color = Qnil;
39
+ static VALUE str_border_top_color = Qnil;
40
+ static VALUE str_border_right_color = Qnil;
41
+ static VALUE str_border_bottom_color = Qnil;
42
+ static VALUE str_border_left_color = Qnil;
43
+ static VALUE str_border = Qnil;
44
+ static VALUE str_font = Qnil;
45
+ static VALUE str_font_style = Qnil;
46
+ static VALUE str_font_variant = Qnil;
47
+ static VALUE str_font_weight = Qnil;
48
+ static VALUE str_font_size = Qnil;
49
+ static VALUE str_line_height = Qnil;
50
+ static VALUE str_font_family = Qnil;
51
+ static VALUE str_list_style = Qnil;
52
+ static VALUE str_list_style_type = Qnil;
53
+ static VALUE str_list_style_position = Qnil;
54
+ static VALUE str_list_style_image = Qnil;
55
+ static VALUE str_background = Qnil;
56
+ static VALUE str_background_color = Qnil;
57
+ static VALUE str_background_image = Qnil;
58
+ static VALUE str_background_repeat = Qnil;
59
+ static VALUE str_background_attachment = Qnil;
60
+ static VALUE str_background_position = Qnil;
61
+
62
+ // Context for expanded property iteration
63
+ struct expand_context {
64
+ VALUE properties_hash;
65
+ int specificity;
66
+ VALUE important;
67
+ };
68
+
69
+ // 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) {
71
+ struct expand_context *ctx = (struct expand_context *)ctx_val;
72
+
73
+ // Expanded properties from shorthand expanders are already lowercase
74
+ // No need to lowercase again
75
+ int is_important = RTEST(ctx->important);
76
+
77
+ // Apply cascade rules for expanded property
78
+ VALUE existing = rb_hash_aref(ctx->properties_hash, exp_prop);
79
+
80
+ 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
86
+ rb_hash_aset(ctx->properties_hash, exp_prop, prop_data);
87
+ } 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);
92
+ int existing_is_important = RTEST(existing_important);
93
+
94
+ int should_replace = 0;
95
+ if (is_important) {
96
+ if (!existing_is_important || existing_spec_int <= ctx->specificity) {
97
+ should_replace = 1;
98
+ }
99
+ } else {
100
+ if (!existing_is_important && existing_spec_int <= ctx->specificity) {
101
+ should_replace = 1;
102
+ }
103
+ }
104
+
105
+ 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);
109
+ }
110
+ }
111
+
112
+ RB_GC_GUARD(exp_prop);
113
+ RB_GC_GUARD(exp_value);
114
+ return ST_CONTINUE;
115
+ }
116
+
117
+ // 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));
122
+
123
+ // Create Declaration struct (use global cDeclaration)
124
+ VALUE decl_struct = rb_struct_new(cDeclaration, property, value, important);
125
+ rb_ary_push(result_ary, decl_struct);
126
+
127
+ return ST_CONTINUE;
128
+ }
129
+
130
+ // Initialize cached property strings (called once at module init)
131
+ void init_merge_constants(void) {
132
+ // Initialize symbol IDs
133
+ id_value = rb_intern("value");
134
+ id_specificity = rb_intern("specificity");
135
+ id_important = rb_intern("important");
136
+ id_all = rb_intern("all");
137
+
138
+ // Initialize ivar IDs for Stylesheet
139
+ id_ivar_rules = rb_intern("@rules");
140
+ id_ivar_media_index = rb_intern("@_media_index");
141
+
142
+ // Margin properties
143
+ str_margin = rb_str_freeze(USASCII_STR("margin"));
144
+ str_margin_top = rb_str_freeze(USASCII_STR("margin-top"));
145
+ str_margin_right = rb_str_freeze(USASCII_STR("margin-right"));
146
+ str_margin_bottom = rb_str_freeze(USASCII_STR("margin-bottom"));
147
+ str_margin_left = rb_str_freeze(USASCII_STR("margin-left"));
148
+
149
+ // Padding properties
150
+ str_padding = rb_str_freeze(USASCII_STR("padding"));
151
+ str_padding_top = rb_str_freeze(USASCII_STR("padding-top"));
152
+ str_padding_right = rb_str_freeze(USASCII_STR("padding-right"));
153
+ str_padding_bottom = rb_str_freeze(USASCII_STR("padding-bottom"));
154
+ str_padding_left = rb_str_freeze(USASCII_STR("padding-left"));
155
+
156
+ // Border-width properties
157
+ str_border_width = rb_str_freeze(USASCII_STR("border-width"));
158
+ str_border_top_width = rb_str_freeze(USASCII_STR("border-top-width"));
159
+ str_border_right_width = rb_str_freeze(USASCII_STR("border-right-width"));
160
+ str_border_bottom_width = rb_str_freeze(USASCII_STR("border-bottom-width"));
161
+ str_border_left_width = rb_str_freeze(USASCII_STR("border-left-width"));
162
+
163
+ // Border-style properties
164
+ str_border_style = rb_str_freeze(USASCII_STR("border-style"));
165
+ str_border_top_style = rb_str_freeze(USASCII_STR("border-top-style"));
166
+ str_border_right_style = rb_str_freeze(USASCII_STR("border-right-style"));
167
+ str_border_bottom_style = rb_str_freeze(USASCII_STR("border-bottom-style"));
168
+ str_border_left_style = rb_str_freeze(USASCII_STR("border-left-style"));
169
+
170
+ // Border-color properties
171
+ str_border_color = rb_str_freeze(USASCII_STR("border-color"));
172
+ str_border_top_color = rb_str_freeze(USASCII_STR("border-top-color"));
173
+ str_border_right_color = rb_str_freeze(USASCII_STR("border-right-color"));
174
+ str_border_bottom_color = rb_str_freeze(USASCII_STR("border-bottom-color"));
175
+ str_border_left_color = rb_str_freeze(USASCII_STR("border-left-color"));
176
+
177
+ // Border shorthand
178
+ str_border = rb_str_freeze(USASCII_STR("border"));
179
+
180
+ // Font properties
181
+ str_font = rb_str_freeze(USASCII_STR("font"));
182
+ str_font_style = rb_str_freeze(USASCII_STR("font-style"));
183
+ str_font_variant = rb_str_freeze(USASCII_STR("font-variant"));
184
+ str_font_weight = rb_str_freeze(USASCII_STR("font-weight"));
185
+ str_font_size = rb_str_freeze(USASCII_STR("font-size"));
186
+ str_line_height = rb_str_freeze(USASCII_STR("line-height"));
187
+ str_font_family = rb_str_freeze(USASCII_STR("font-family"));
188
+
189
+ // List-style properties
190
+ str_list_style = rb_str_freeze(USASCII_STR("list-style"));
191
+ str_list_style_type = rb_str_freeze(USASCII_STR("list-style-type"));
192
+ str_list_style_position = rb_str_freeze(USASCII_STR("list-style-position"));
193
+ str_list_style_image = rb_str_freeze(USASCII_STR("list-style-image"));
194
+
195
+ // Background properties
196
+ str_background = rb_str_freeze(USASCII_STR("background"));
197
+ str_background_color = rb_str_freeze(USASCII_STR("background-color"));
198
+ str_background_image = rb_str_freeze(USASCII_STR("background-image"));
199
+ str_background_repeat = rb_str_freeze(USASCII_STR("background-repeat"));
200
+ str_background_attachment = rb_str_freeze(USASCII_STR("background-attachment"));
201
+ str_background_position = rb_str_freeze(USASCII_STR("background-position"));
202
+
203
+ // Register all strings with GC so they're never collected
204
+ rb_gc_register_mark_object(str_margin);
205
+ rb_gc_register_mark_object(str_margin_top);
206
+ rb_gc_register_mark_object(str_margin_right);
207
+ rb_gc_register_mark_object(str_margin_bottom);
208
+ rb_gc_register_mark_object(str_margin_left);
209
+ rb_gc_register_mark_object(str_padding);
210
+ rb_gc_register_mark_object(str_padding_top);
211
+ rb_gc_register_mark_object(str_padding_right);
212
+ rb_gc_register_mark_object(str_padding_bottom);
213
+ rb_gc_register_mark_object(str_padding_left);
214
+ rb_gc_register_mark_object(str_border_width);
215
+ rb_gc_register_mark_object(str_border_top_width);
216
+ rb_gc_register_mark_object(str_border_right_width);
217
+ rb_gc_register_mark_object(str_border_bottom_width);
218
+ rb_gc_register_mark_object(str_border_left_width);
219
+ rb_gc_register_mark_object(str_border_style);
220
+ rb_gc_register_mark_object(str_border_top_style);
221
+ rb_gc_register_mark_object(str_border_right_style);
222
+ rb_gc_register_mark_object(str_border_bottom_style);
223
+ rb_gc_register_mark_object(str_border_left_style);
224
+ rb_gc_register_mark_object(str_border_color);
225
+ rb_gc_register_mark_object(str_border_top_color);
226
+ rb_gc_register_mark_object(str_border_right_color);
227
+ rb_gc_register_mark_object(str_border_bottom_color);
228
+ rb_gc_register_mark_object(str_border_left_color);
229
+ rb_gc_register_mark_object(str_border);
230
+ rb_gc_register_mark_object(str_font);
231
+ rb_gc_register_mark_object(str_font_style);
232
+ rb_gc_register_mark_object(str_font_variant);
233
+ rb_gc_register_mark_object(str_font_weight);
234
+ rb_gc_register_mark_object(str_font_size);
235
+ rb_gc_register_mark_object(str_line_height);
236
+ rb_gc_register_mark_object(str_font_family);
237
+ rb_gc_register_mark_object(str_list_style);
238
+ rb_gc_register_mark_object(str_list_style_type);
239
+ rb_gc_register_mark_object(str_list_style_position);
240
+ rb_gc_register_mark_object(str_list_style_image);
241
+ rb_gc_register_mark_object(str_background);
242
+ rb_gc_register_mark_object(str_background_color);
243
+ rb_gc_register_mark_object(str_background_image);
244
+ rb_gc_register_mark_object(str_background_repeat);
245
+ rb_gc_register_mark_object(str_background_attachment);
246
+ rb_gc_register_mark_object(str_background_position);
247
+
248
+ // Cached "merged" selector string
249
+ str_merged_selector = rb_str_freeze(USASCII_STR("merged"));
250
+ rb_gc_register_mark_object(str_merged_selector);
251
+ }
252
+
253
+ // 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
255
+ #define GET_PROP_VALUE(hash, prop_name) \
256
+ ({ VALUE pd = rb_hash_aref(hash, USASCII_STR(prop_name)); \
257
+ NIL_P(pd) ? Qnil : rb_hash_aref(pd, ID2SYM(id_value)); })
258
+
259
+ #define GET_PROP_DATA(hash, prop_name) \
260
+ rb_hash_aref(hash, USASCII_STR(prop_name))
261
+
262
+ // Versions that accept cached VALUE strings instead of string literals
263
+ #define GET_PROP_VALUE_STR(hash, str_prop) \
264
+ ({ VALUE pd = rb_hash_aref(hash, str_prop); \
265
+ NIL_P(pd) ? Qnil : rb_hash_aref(pd, ID2SYM(id_value)); })
266
+
267
+ #define GET_PROP_DATA_STR(hash, str_prop) \
268
+ rb_hash_aref(hash, str_prop)
269
+
270
+ // Helper macro to check if a property's !important flag matches a reference
271
+ #define CHECK_IMPORTANT_MATCH(hash, str_prop, ref_important) \
272
+ ({ VALUE _pd = GET_PROP_DATA_STR(hash, str_prop); \
273
+ NIL_P(_pd) ? 1 : (RTEST(rb_hash_aref(_pd, ID2SYM(id_important))) == (ref_important)); })
274
+
275
+ // Macro to create shorthand from 4-sided properties (margin, padding, border-width/style/color)
276
+ // Reduces repetitive code by encapsulating the common pattern:
277
+ // 1. Get 4 longhand values (top, right, bottom, left)
278
+ // 2. Check if all 4 exist
279
+ // 3. Call shorthand creator function
280
+ // 4. Add shorthand to properties_hash and remove longhands
281
+ // Note: Uses cached static strings (VALUE) for property names - no runtime allocation
282
+ #define TRY_CREATE_FOUR_SIDED_SHORTHAND(hash, str_top, str_right, str_bottom, str_left, str_shorthand, creator_func) \
283
+ do { \
284
+ VALUE _top = GET_PROP_VALUE_STR(hash, str_top); \
285
+ VALUE _right = GET_PROP_VALUE_STR(hash, str_right); \
286
+ VALUE _bottom = GET_PROP_VALUE_STR(hash, str_bottom); \
287
+ VALUE _left = GET_PROP_VALUE_STR(hash, str_left); \
288
+ \
289
+ if (!NIL_P(_top) && !NIL_P(_right) && !NIL_P(_bottom) && !NIL_P(_left)) { \
290
+ /* Check that all properties have the same !important flag */ \
291
+ VALUE _top_data = GET_PROP_DATA_STR(hash, str_top); \
292
+ VALUE _right_data = GET_PROP_DATA_STR(hash, str_right); \
293
+ VALUE _bottom_data = GET_PROP_DATA_STR(hash, str_bottom); \
294
+ VALUE _left_data = GET_PROP_DATA_STR(hash, str_left); \
295
+ \
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)); \
300
+ \
301
+ int _top_is_imp = RTEST(_top_imp); \
302
+ int _right_is_imp = RTEST(_right_imp); \
303
+ int _bottom_is_imp = RTEST(_bottom_imp); \
304
+ int _left_is_imp = RTEST(_left_imp); \
305
+ \
306
+ /* Only create shorthand if all have same !important flag */ \
307
+ if (_top_is_imp == _right_is_imp && _top_is_imp == _bottom_is_imp && _top_is_imp == _left_is_imp) { \
308
+ VALUE _props = rb_hash_new(); \
309
+ rb_hash_aset(_props, str_top, _top); \
310
+ rb_hash_aset(_props, str_right, _right); \
311
+ rb_hash_aset(_props, str_bottom, _bottom); \
312
+ rb_hash_aset(_props, str_left, _left); \
313
+ \
314
+ VALUE _shorthand_value = creator_func(Qnil, _props); \
315
+ if (!NIL_P(_shorthand_value)) { \
316
+ int _specificity = NUM2INT(rb_hash_aref(_top_data, ID2SYM(id_specificity))); \
317
+ \
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); \
322
+ rb_hash_aset(hash, str_shorthand, _shorthand_data); \
323
+ \
324
+ rb_hash_delete(hash, str_top); \
325
+ rb_hash_delete(hash, str_right); \
326
+ rb_hash_delete(hash, str_bottom); \
327
+ rb_hash_delete(hash, str_left); \
328
+ \
329
+ RB_GC_GUARD(_shorthand_value); \
330
+ } \
331
+ RB_GC_GUARD(_props); \
332
+ } \
333
+ } \
334
+ } while(0)
335
+
336
+ // Merge CSS rules according to cascade rules
337
+ // Input: Stylesheet object or CSS string
338
+ // Output: Stylesheet with merged declarations
339
+ VALUE cataract_merge_new(VALUE self, VALUE input) {
340
+ VALUE rules_array;
341
+
342
+ // Handle different input types
343
+ // Most calls pass Stylesheet (common case), String is rare
344
+ if (TYPE(input) == T_STRING) {
345
+ // Parse CSS string first
346
+ VALUE parsed = parse_css_new(self, input);
347
+ rules_array = rb_hash_aref(parsed, ID2SYM(rb_intern("rules")));
348
+ } else if (rb_obj_is_kind_of(input, cStylesheet)) {
349
+ // Extract @rules from Stylesheet (common case)
350
+ rules_array = rb_ivar_get(input, id_ivar_rules);
351
+ } else {
352
+ rb_raise(rb_eTypeError, "Expected Stylesheet or String, got %s",
353
+ rb_obj_classname(input));
354
+ }
355
+
356
+ Check_Type(rules_array, T_ARRAY);
357
+
358
+ // Check if stylesheet has nesting (affects selector rollup)
359
+ int has_nesting = 0;
360
+ if (rb_obj_is_kind_of(input, cStylesheet)) {
361
+ VALUE has_nesting_ivar = rb_ivar_get(input, rb_intern("@_has_nesting"));
362
+ has_nesting = RTEST(has_nesting_ivar);
363
+ }
364
+
365
+ // Initialize cached symbol IDs on first call (thread-safe since GVL is held)
366
+ // This only happens once, so unlikely
367
+ if (id_value == 0) {
368
+ id_value = rb_intern("value");
369
+ id_specificity = rb_intern("specificity");
370
+ id_important = rb_intern("important");
371
+ }
372
+
373
+ long num_rules = RARRAY_LEN(rules_array);
374
+ // Empty stylesheets are rare
375
+ if (num_rules == 0) {
376
+ // Return empty stylesheet
377
+ VALUE empty_sheet = rb_class_new_instance(0, NULL, cStylesheet);
378
+ return empty_sheet;
379
+ }
380
+
381
+ // For nested CSS: identify parent rules (rules that have children)
382
+ // These should be skipped during merge, even if they have declarations
383
+ // Use Ruby hash as a set: parent_id => true
384
+ VALUE parent_ids = Qnil;
385
+ if (has_nesting) {
386
+ DEBUG_PRINTF("\n=== MERGE: has_nesting=true, num_rules=%ld ===\n", num_rules);
387
+ parent_ids = rb_hash_new();
388
+ for (long i = 0; i < num_rules; i++) {
389
+ VALUE rule = RARRAY_AREF(rules_array, i);
390
+ VALUE parent_rule_id = rb_struct_aref(rule, INT2FIX(RULE_PARENT_RULE_ID));
391
+ DEBUG_PRINTF(" Rule %ld: selector='%s', rule_id=%d, parent_rule_id=%s\n",
392
+ i,
393
+ RSTRING_PTR(rb_struct_aref(rule, INT2FIX(RULE_SELECTOR))),
394
+ FIX2INT(rb_struct_aref(rule, INT2FIX(RULE_ID))),
395
+ NIL_P(parent_rule_id) ? "nil" : RSTRING_PTR(rb_inspect(parent_rule_id)));
396
+ if (!NIL_P(parent_rule_id)) {
397
+ // This rule has a parent, so mark that parent ID
398
+ rb_hash_aset(parent_ids, parent_rule_id, Qtrue);
399
+ }
400
+ }
401
+ }
402
+
403
+ // For nested CSS with different selectors from SAME parent: group rules by selector
404
+ // Only split into multiple rules if ALL rules share the same parent_rule_id
405
+ // selector => [rule indices]
406
+ VALUE selector_groups = Qnil;
407
+ VALUE common_parent = Qundef; // Qundef = not set yet
408
+
409
+ if (has_nesting) {
410
+ DEBUG_PRINTF("\n=== Building selector groups ===\n");
411
+ selector_groups = rb_hash_new();
412
+ for (long i = 0; i < num_rules; i++) {
413
+ VALUE rule = RARRAY_AREF(rules_array, i);
414
+ VALUE declarations = rb_struct_aref(rule, INT2FIX(RULE_DECLARATIONS));
415
+ VALUE parent_rule_id = rb_struct_aref(rule, INT2FIX(RULE_PARENT_RULE_ID));
416
+ VALUE selector = rb_struct_aref(rule, INT2FIX(RULE_SELECTOR));
417
+
418
+ // Per W3C spec: parent and child are SEPARATE rules with different selectors
419
+ // Both should be included in merge output
420
+ // Don't skip parent rules - they have their own selector and declarations
421
+
422
+ // Skip empty rules (no declarations)
423
+ if (RARRAY_LEN(declarations) == 0) {
424
+ DEBUG_PRINTF(" Skipping rule %ld: selector='%s' (empty declarations)\n",
425
+ i, RSTRING_PTR(selector));
426
+ continue;
427
+ }
428
+
429
+ DEBUG_PRINTF(" Processing rule %ld: selector='%s', parent_rule_id=%s\n",
430
+ i, RSTRING_PTR(selector),
431
+ NIL_P(parent_rule_id) ? "nil" : RSTRING_PTR(rb_inspect(parent_rule_id)));
432
+
433
+ // Track if all rules share the same parent
434
+ if (common_parent == Qundef) {
435
+ common_parent = parent_rule_id;
436
+ DEBUG_PRINTF(" Setting common_parent=%s\n",
437
+ NIL_P(common_parent) ? "nil" : RSTRING_PTR(rb_inspect(common_parent)));
438
+ }
439
+
440
+ VALUE group = rb_hash_aref(selector_groups, selector);
441
+ if (NIL_P(group)) {
442
+ group = rb_ary_new();
443
+ rb_hash_aset(selector_groups, selector, group);
444
+ DEBUG_PRINTF(" Created new group for selector='%s'\n", RSTRING_PTR(selector));
445
+ }
446
+ rb_ary_push(group, LONG2FIX(i));
447
+ }
448
+ DEBUG_PRINTF(" Total selector groups: %ld\n", RHASH_SIZE(selector_groups));
449
+ }
450
+
451
+ // If nested CSS with multiple distinct selectors, return separate rules
452
+ // Per W3C spec: each unique selector (parent or child) is a separate rule
453
+ // Example: .parent { color: red; .child { color: blue; } } .other { color: green; }
454
+ // Should return 3 rules: .parent, .parent .child, .other
455
+ DEBUG_PRINTF("\n=== Decision point ===\n");
456
+ DEBUG_PRINTF(" has_nesting=%d\n", has_nesting);
457
+ DEBUG_PRINTF(" selector_groups is nil? %d\n", NIL_P(selector_groups));
458
+ if (!NIL_P(selector_groups)) {
459
+ DEBUG_PRINTF(" selector_groups size=%ld\n", RHASH_SIZE(selector_groups));
460
+ }
461
+ DEBUG_PRINTF(" Condition: has_nesting && !NIL_P(selector_groups) && RHASH_SIZE(selector_groups) > 1 = %d\n",
462
+ has_nesting && !NIL_P(selector_groups) && RHASH_SIZE(selector_groups) > 1);
463
+
464
+ if (has_nesting && !NIL_P(selector_groups) && RHASH_SIZE(selector_groups) > 1) {
465
+ DEBUG_PRINTF(" -> Taking MULTI-SELECTOR path (separate rules)\n");
466
+ VALUE merged_sheet = rb_class_new_instance(0, NULL, cStylesheet);
467
+ VALUE merged_rules = rb_ary_new();
468
+ int rule_id_counter = 0;
469
+
470
+ // Iterate through each selector group
471
+ VALUE selectors = rb_funcall(selector_groups, rb_intern("keys"), 0);
472
+ long num_selectors = RARRAY_LEN(selectors);
473
+
474
+ for (long s = 0; s < num_selectors; s++) {
475
+ VALUE selector = rb_ary_entry(selectors, s);
476
+ VALUE group_indices = rb_hash_aref(selector_groups, selector);
477
+
478
+ // For now, just take first rule from each group (no merging within group)
479
+ // TODO: Merge declarations within same-selector group
480
+ long first_idx = FIX2LONG(rb_ary_entry(group_indices, 0));
481
+ VALUE orig_rule = RARRAY_AREF(rules_array, first_idx);
482
+ VALUE orig_decls = rb_struct_aref(orig_rule, INT2FIX(RULE_DECLARATIONS));
483
+
484
+ // Create new rule with this selector and declarations
485
+ VALUE new_rule = rb_struct_new(cRule,
486
+ INT2FIX(rule_id_counter++),
487
+ selector,
488
+ orig_decls,
489
+ Qnil, // specificity
490
+ Qnil, // parent_rule_id
491
+ Qnil // nesting_style
492
+ );
493
+ rb_ary_push(merged_rules, new_rule);
494
+ }
495
+
496
+ rb_ivar_set(merged_sheet, id_ivar_rules, merged_rules);
497
+
498
+ // Set @media_index with :all pointing to all rule IDs
499
+ VALUE media_idx = rb_hash_new();
500
+ VALUE all_ids = rb_ary_new();
501
+ for (int i = 0; i < rule_id_counter; i++) {
502
+ rb_ary_push(all_ids, INT2FIX(i));
503
+ }
504
+ rb_hash_aset(media_idx, ID2SYM(id_all), all_ids);
505
+ rb_ivar_set(merged_sheet, id_ivar_media_index, media_idx);
506
+
507
+ return merged_sheet;
508
+ }
509
+
510
+ // Single-merge path: merge all rules into one
511
+ VALUE properties_hash = rb_hash_new();
512
+
513
+ // Track selector for rollup (minimize allocations)
514
+ // Store pointer + length to first non-parent selector
515
+ // Also keep the VALUE alive since we extract C pointer before allocations
516
+ const char *first_selector_ptr = NULL;
517
+ long first_selector_len = 0;
518
+ VALUE first_selector_value = Qnil;
519
+ int all_same_selector = 1;
520
+
521
+ // Iterate through each rule
522
+ for (long i = 0; i < num_rules; i++) {
523
+ VALUE rule = RARRAY_AREF(rules_array, i);
524
+ Check_Type(rule, T_STRUCT);
525
+
526
+ // Extract rule fields
527
+ VALUE rule_id = rb_struct_aref(rule, INT2FIX(RULE_ID));
528
+ VALUE selector = rb_struct_aref(rule, INT2FIX(RULE_SELECTOR));
529
+ VALUE declarations = rb_struct_aref(rule, INT2FIX(RULE_DECLARATIONS));
530
+
531
+ // Skip parent rules when handling nested CSS
532
+ // Example: .button { color: black; &:hover { color: red; } }
533
+ // - Rule id=0, selector=".button", declarations=[color: black] (SKIP - has children)
534
+ // - Rule id=1, selector=".button:hover", declarations=[color: red] (PROCESS)
535
+ if (has_nesting && !NIL_P(parent_ids)) {
536
+ VALUE is_parent = rb_hash_aref(parent_ids, rule_id);
537
+ if (RTEST(is_parent)) {
538
+ continue;
539
+ }
540
+ }
541
+
542
+ long num_decls = RARRAY_LEN(declarations);
543
+ // Skip rules with no declarations (empty parent containers)
544
+ if (num_decls == 0) {
545
+ continue;
546
+ }
547
+
548
+ // Track selectors for rollup (delay allocation)
549
+ const char *sel_ptr = RSTRING_PTR(selector);
550
+ long sel_len = RSTRING_LEN(selector);
551
+ if (first_selector_ptr == NULL) {
552
+ first_selector_ptr = sel_ptr;
553
+ first_selector_len = sel_len;
554
+ first_selector_value = selector; // Keep VALUE alive for RB_GC_GUARD
555
+ } else if (all_same_selector) {
556
+ if (sel_len != first_selector_len || memcmp(sel_ptr, first_selector_ptr, sel_len) != 0) {
557
+ all_same_selector = 0;
558
+ }
559
+ }
560
+
561
+ VALUE specificity_val = rb_struct_aref(rule, INT2FIX(RULE_SPECIFICITY));
562
+
563
+ // Calculate specificity if not provided (lazy)
564
+ int specificity = 0;
565
+ if (NIL_P(specificity_val)) {
566
+ specificity_val = calculate_specificity(Qnil, selector);
567
+ // Cache the calculated value back to the struct
568
+ rb_struct_aset(rule, INT2FIX(RULE_SPECIFICITY), specificity_val);
569
+ }
570
+ specificity = NUM2INT(specificity_val);
571
+
572
+ // Process each declaration in this rule
573
+ Check_Type(declarations, T_ARRAY);
574
+
575
+ for (long j = 0; j < num_decls; j++) {
576
+ VALUE decl = RARRAY_AREF(declarations, j);
577
+
578
+ // Extract property, value, important from Declaration struct
579
+ VALUE property = rb_struct_aref(decl, INT2FIX(DECL_PROPERTY));
580
+ VALUE value = rb_struct_aref(decl, INT2FIX(DECL_VALUE));
581
+ VALUE important = rb_struct_aref(decl, INT2FIX(DECL_IMPORTANT));
582
+
583
+ // Properties are already lowercased during parsing (see cataract_new.c)
584
+ // No need to lowercase again
585
+ int is_important = RTEST(important);
586
+
587
+ // Expand shorthand properties if needed
588
+ // Most properties are NOT shorthands, so hint compiler accordingly
589
+ const char *prop_str = StringValueCStr(property);
590
+ VALUE expanded = Qnil;
591
+
592
+ if (strcmp(prop_str, "margin") == 0) {
593
+ expanded = cataract_expand_margin(Qnil, value);
594
+ } else if (strcmp(prop_str, "padding") == 0) {
595
+ expanded = cataract_expand_padding(Qnil, value);
596
+ } else if (strcmp(prop_str, "border") == 0) {
597
+ expanded = cataract_expand_border(Qnil, value);
598
+ } else if (strcmp(prop_str, "border-color") == 0) {
599
+ expanded = cataract_expand_border_color(Qnil, value);
600
+ } else if (strcmp(prop_str, "border-style") == 0) {
601
+ expanded = cataract_expand_border_style(Qnil, value);
602
+ } else if (strcmp(prop_str, "border-width") == 0) {
603
+ expanded = cataract_expand_border_width(Qnil, value);
604
+ } else if (strcmp(prop_str, "border-top") == 0) {
605
+ expanded = cataract_expand_border_side(Qnil, USASCII_STR("top"), value);
606
+ } else if (strcmp(prop_str, "border-right") == 0) {
607
+ expanded = cataract_expand_border_side(Qnil, USASCII_STR("right"), value);
608
+ } else if (strcmp(prop_str, "border-bottom") == 0) {
609
+ expanded = cataract_expand_border_side(Qnil, USASCII_STR("bottom"), value);
610
+ } else if (strcmp(prop_str, "border-left") == 0) {
611
+ expanded = cataract_expand_border_side(Qnil, USASCII_STR("left"), value);
612
+ } else if (strcmp(prop_str, "font") == 0) {
613
+ expanded = cataract_expand_font(Qnil, value);
614
+ } else if (strcmp(prop_str, "list-style") == 0) {
615
+ expanded = cataract_expand_list_style(Qnil, value);
616
+ } else if (strcmp(prop_str, "background") == 0) {
617
+ expanded = cataract_expand_background(Qnil, value);
618
+ }
619
+
620
+ // If property was expanded, iterate and apply cascade using rb_hash_foreach
621
+ // Expansion is rare (most properties are not shorthands)
622
+ if (!NIL_P(expanded)) {
623
+ Check_Type(expanded, T_HASH);
624
+
625
+ struct expand_context ctx;
626
+ ctx.properties_hash = properties_hash;
627
+ ctx.specificity = specificity;
628
+ ctx.important = important;
629
+
630
+ rb_hash_foreach(expanded, merge_expanded_callback, (VALUE)&ctx);
631
+
632
+ RB_GC_GUARD(expanded);
633
+ continue; // Skip processing the original shorthand property
634
+ }
635
+
636
+ // Apply CSS cascade rules
637
+ VALUE existing = rb_hash_aref(properties_hash, property);
638
+
639
+ // In merge scenarios, properties often collide (same property in multiple rules)
640
+ // so existing property is the common case
641
+ if (NIL_P(existing)) {
642
+ // New property - add it
643
+ VALUE prop_data = rb_hash_new();
644
+ rb_hash_aset(prop_data, ID2SYM(id_value), value);
645
+ rb_hash_aset(prop_data, ID2SYM(id_specificity), INT2NUM(specificity));
646
+ rb_hash_aset(prop_data, ID2SYM(id_important), important);
647
+ // Note: declaration_struct not stored - use global cDeclaration instead
648
+ rb_hash_aset(properties_hash, property, prop_data);
649
+ } else {
650
+ // Property exists - check cascade rules
651
+ VALUE existing_spec = rb_hash_aref(existing, ID2SYM(id_specificity));
652
+ VALUE existing_important = rb_hash_aref(existing, ID2SYM(id_important));
653
+
654
+ int existing_spec_int = NUM2INT(existing_spec);
655
+ int existing_is_important = RTEST(existing_important);
656
+
657
+ int should_replace = 0;
658
+
659
+ // Most declarations are NOT !important
660
+ if (is_important) {
661
+ // New is !important - wins if existing is NOT important OR equal/higher specificity
662
+ if (!existing_is_important || existing_spec_int <= specificity) {
663
+ should_replace = 1;
664
+ }
665
+ } else {
666
+ // New is NOT important - only wins if existing is also NOT important AND equal/higher specificity
667
+ if (!existing_is_important && existing_spec_int <= specificity) {
668
+ should_replace = 1;
669
+ }
670
+ }
671
+
672
+ // Replacement is common in merge scenarios
673
+ if (should_replace) {
674
+ rb_hash_aset(existing, ID2SYM(id_value), value);
675
+ rb_hash_aset(existing, ID2SYM(id_specificity), INT2NUM(specificity));
676
+ rb_hash_aset(existing, ID2SYM(id_important), important);
677
+ }
678
+ }
679
+
680
+ RB_GC_GUARD(property);
681
+ RB_GC_GUARD(value);
682
+ RB_GC_GUARD(decl);
683
+ }
684
+
685
+ RB_GC_GUARD(selector);
686
+ RB_GC_GUARD(declarations);
687
+ RB_GC_GUARD(rule);
688
+ }
689
+
690
+ // Create shorthand from longhand properties
691
+ // Uses cached static strings to avoid runtime allocation
692
+
693
+ // Try to create margin shorthand
694
+ TRY_CREATE_FOUR_SIDED_SHORTHAND(properties_hash,
695
+ str_margin_top, str_margin_right, str_margin_bottom, str_margin_left,
696
+ str_margin, cataract_create_margin_shorthand);
697
+
698
+ // Try to create padding shorthand
699
+ TRY_CREATE_FOUR_SIDED_SHORTHAND(properties_hash,
700
+ str_padding_top, str_padding_right, str_padding_bottom, str_padding_left,
701
+ str_padding, cataract_create_padding_shorthand);
702
+
703
+ // Create border-width from individual sides
704
+ TRY_CREATE_FOUR_SIDED_SHORTHAND(properties_hash,
705
+ str_border_top_width, str_border_right_width, str_border_bottom_width, str_border_left_width,
706
+ str_border_width, cataract_create_border_width_shorthand);
707
+
708
+ // Create border-style from individual sides
709
+ TRY_CREATE_FOUR_SIDED_SHORTHAND(properties_hash,
710
+ str_border_top_style, str_border_right_style, str_border_bottom_style, str_border_left_style,
711
+ str_border_style, cataract_create_border_style_shorthand);
712
+
713
+ // Create border-color from individual sides
714
+ TRY_CREATE_FOUR_SIDED_SHORTHAND(properties_hash,
715
+ str_border_top_color, str_border_right_color, str_border_bottom_color, str_border_left_color,
716
+ str_border_color, cataract_create_border_color_shorthand);
717
+
718
+ // Now create border shorthand from border-{width,style,color}
719
+ VALUE border_width = GET_PROP_VALUE_STR(properties_hash, str_border_width);
720
+ VALUE border_style = GET_PROP_VALUE_STR(properties_hash, str_border_style);
721
+ VALUE border_color = GET_PROP_VALUE_STR(properties_hash, str_border_color);
722
+
723
+ if (!NIL_P(border_width) || !NIL_P(border_style) || !NIL_P(border_color)) {
724
+ // Use first available property's metadata as reference
725
+ VALUE border_data_src = !NIL_P(border_width) ? GET_PROP_DATA_STR(properties_hash, str_border_width) :
726
+ !NIL_P(border_style) ? GET_PROP_DATA_STR(properties_hash, str_border_style) :
727
+ GET_PROP_DATA_STR(properties_hash, str_border_color);
728
+ VALUE border_important = rb_hash_aref(border_data_src, ID2SYM(id_important));
729
+ int border_is_important = RTEST(border_important);
730
+
731
+ // Check that all present properties have the same !important flag
732
+ int important_match = CHECK_IMPORTANT_MATCH(properties_hash, str_border_width, border_is_important) &&
733
+ CHECK_IMPORTANT_MATCH(properties_hash, str_border_style, border_is_important) &&
734
+ CHECK_IMPORTANT_MATCH(properties_hash, str_border_color, border_is_important);
735
+
736
+ if (important_match) {
737
+ VALUE border_props = rb_hash_new();
738
+ if (!NIL_P(border_width)) rb_hash_aset(border_props, str_border_width, border_width);
739
+ if (!NIL_P(border_style)) rb_hash_aset(border_props, str_border_style, border_style);
740
+ if (!NIL_P(border_color)) rb_hash_aset(border_props, str_border_color, border_color);
741
+
742
+ VALUE border_shorthand = cataract_create_border_shorthand(Qnil, border_props);
743
+ if (!NIL_P(border_shorthand)) {
744
+ int border_spec = NUM2INT(rb_hash_aref(border_data_src, ID2SYM(id_specificity)));
745
+
746
+ VALUE border_data = rb_hash_new();
747
+ rb_hash_aset(border_data, ID2SYM(id_value), border_shorthand);
748
+ rb_hash_aset(border_data, ID2SYM(id_specificity), INT2NUM(border_spec));
749
+ rb_hash_aset(border_data, ID2SYM(id_important), border_important);
750
+ rb_hash_aset(properties_hash, str_border, border_data);
751
+
752
+ if (!NIL_P(border_width)) rb_hash_delete(properties_hash, str_border_width);
753
+ if (!NIL_P(border_style)) rb_hash_delete(properties_hash, str_border_style);
754
+ if (!NIL_P(border_color)) rb_hash_delete(properties_hash, str_border_color);
755
+ }
756
+ RB_GC_GUARD(border_props);
757
+ RB_GC_GUARD(border_shorthand);
758
+ }
759
+ }
760
+
761
+ // Try to create font shorthand
762
+ VALUE font_size = GET_PROP_VALUE_STR(properties_hash, str_font_size);
763
+ VALUE font_family = GET_PROP_VALUE_STR(properties_hash, str_font_family);
764
+
765
+ // Font shorthand requires at least font-size and font-family
766
+ if (!NIL_P(font_size) && !NIL_P(font_family)) {
767
+ VALUE font_style = GET_PROP_VALUE_STR(properties_hash, str_font_style);
768
+ VALUE font_variant = GET_PROP_VALUE_STR(properties_hash, str_font_variant);
769
+ VALUE font_weight = GET_PROP_VALUE_STR(properties_hash, str_font_weight);
770
+ VALUE line_height = GET_PROP_VALUE_STR(properties_hash, str_line_height);
771
+
772
+ // Get metadata from font-size as reference
773
+ VALUE size_data = GET_PROP_DATA_STR(properties_hash, str_font_size);
774
+ VALUE font_important = rb_hash_aref(size_data, ID2SYM(id_important));
775
+ int font_is_important = RTEST(font_important);
776
+
777
+ // Check that all present properties have the same !important flag
778
+ int important_match = CHECK_IMPORTANT_MATCH(properties_hash, str_font_style, font_is_important) &&
779
+ CHECK_IMPORTANT_MATCH(properties_hash, str_font_variant, font_is_important) &&
780
+ CHECK_IMPORTANT_MATCH(properties_hash, str_font_weight, font_is_important) &&
781
+ CHECK_IMPORTANT_MATCH(properties_hash, str_line_height, font_is_important) &&
782
+ CHECK_IMPORTANT_MATCH(properties_hash, str_font_family, font_is_important);
783
+
784
+ if (important_match) {
785
+ VALUE font_props = rb_hash_new();
786
+ if (!NIL_P(font_style)) rb_hash_aset(font_props, str_font_style, font_style);
787
+ if (!NIL_P(font_variant)) rb_hash_aset(font_props, str_font_variant, font_variant);
788
+ if (!NIL_P(font_weight)) rb_hash_aset(font_props, str_font_weight, font_weight);
789
+ rb_hash_aset(font_props, str_font_size, font_size);
790
+ if (!NIL_P(line_height)) rb_hash_aset(font_props, str_line_height, line_height);
791
+ rb_hash_aset(font_props, str_font_family, font_family);
792
+
793
+ VALUE font_shorthand = cataract_create_font_shorthand(Qnil, font_props);
794
+ if (!NIL_P(font_shorthand)) {
795
+ int font_spec = NUM2INT(rb_hash_aref(size_data, ID2SYM(id_specificity)));
796
+
797
+ VALUE font_data = rb_hash_new();
798
+ rb_hash_aset(font_data, ID2SYM(id_value), font_shorthand);
799
+ rb_hash_aset(font_data, ID2SYM(id_specificity), INT2NUM(font_spec));
800
+ rb_hash_aset(font_data, ID2SYM(id_important), font_important);
801
+ rb_hash_aset(properties_hash, str_font, font_data);
802
+
803
+ // Remove longhand properties
804
+ if (!NIL_P(font_style)) rb_hash_delete(properties_hash, str_font_style);
805
+ if (!NIL_P(font_variant)) rb_hash_delete(properties_hash, str_font_variant);
806
+ if (!NIL_P(font_weight)) rb_hash_delete(properties_hash, str_font_weight);
807
+ rb_hash_delete(properties_hash, str_font_size);
808
+ if (!NIL_P(line_height)) rb_hash_delete(properties_hash, str_line_height);
809
+ rb_hash_delete(properties_hash, str_font_family);
810
+ }
811
+ RB_GC_GUARD(font_props);
812
+ RB_GC_GUARD(font_shorthand);
813
+ }
814
+ }
815
+
816
+ // Try to create list-style shorthand
817
+ VALUE list_style_type = GET_PROP_VALUE_STR(properties_hash, str_list_style_type);
818
+ VALUE list_style_position = GET_PROP_VALUE_STR(properties_hash, str_list_style_position);
819
+ VALUE list_style_image = GET_PROP_VALUE_STR(properties_hash, str_list_style_image);
820
+
821
+ // List-style shorthand requires at least 2 properties
822
+ int list_style_count = (!NIL_P(list_style_type) ? 1 : 0) +
823
+ (!NIL_P(list_style_position) ? 1 : 0) +
824
+ (!NIL_P(list_style_image) ? 1 : 0);
825
+
826
+ if (list_style_count >= 2) {
827
+ // Use first available property's metadata as reference
828
+ VALUE list_style_data_src = !NIL_P(list_style_type) ? GET_PROP_DATA_STR(properties_hash, str_list_style_type) :
829
+ !NIL_P(list_style_position) ? GET_PROP_DATA_STR(properties_hash, str_list_style_position) :
830
+ GET_PROP_DATA_STR(properties_hash, str_list_style_image);
831
+ VALUE list_style_important = rb_hash_aref(list_style_data_src, ID2SYM(id_important));
832
+ int list_style_is_important = RTEST(list_style_important);
833
+
834
+ // Check that all present properties have the same !important flag
835
+ int important_match = CHECK_IMPORTANT_MATCH(properties_hash, str_list_style_type, list_style_is_important) &&
836
+ CHECK_IMPORTANT_MATCH(properties_hash, str_list_style_position, list_style_is_important) &&
837
+ CHECK_IMPORTANT_MATCH(properties_hash, str_list_style_image, list_style_is_important);
838
+
839
+ if (important_match) {
840
+ VALUE list_style_props = rb_hash_new();
841
+ if (!NIL_P(list_style_type)) rb_hash_aset(list_style_props, str_list_style_type, list_style_type);
842
+ if (!NIL_P(list_style_position)) rb_hash_aset(list_style_props, str_list_style_position, list_style_position);
843
+ if (!NIL_P(list_style_image)) rb_hash_aset(list_style_props, str_list_style_image, list_style_image);
844
+
845
+ VALUE list_style_shorthand = cataract_create_list_style_shorthand(Qnil, list_style_props);
846
+ if (!NIL_P(list_style_shorthand)) {
847
+ int list_style_spec = NUM2INT(rb_hash_aref(list_style_data_src, ID2SYM(id_specificity)));
848
+
849
+ VALUE list_style_data = rb_hash_new();
850
+ rb_hash_aset(list_style_data, ID2SYM(id_value), list_style_shorthand);
851
+ rb_hash_aset(list_style_data, ID2SYM(id_specificity), INT2NUM(list_style_spec));
852
+ rb_hash_aset(list_style_data, ID2SYM(id_important), list_style_important);
853
+ rb_hash_aset(properties_hash, str_list_style, list_style_data);
854
+
855
+ // Remove longhand properties
856
+ if (!NIL_P(list_style_type)) rb_hash_delete(properties_hash, str_list_style_type);
857
+ if (!NIL_P(list_style_position)) rb_hash_delete(properties_hash, str_list_style_position);
858
+ if (!NIL_P(list_style_image)) rb_hash_delete(properties_hash, str_list_style_image);
859
+ }
860
+ RB_GC_GUARD(list_style_props);
861
+ RB_GC_GUARD(list_style_shorthand);
862
+ }
863
+ }
864
+
865
+ // Try to create background shorthand
866
+ VALUE background_color = GET_PROP_VALUE_STR(properties_hash, str_background_color);
867
+ VALUE background_image = GET_PROP_VALUE_STR(properties_hash, str_background_image);
868
+ VALUE background_repeat = GET_PROP_VALUE_STR(properties_hash, str_background_repeat);
869
+ VALUE background_attachment = GET_PROP_VALUE_STR(properties_hash, str_background_attachment);
870
+ VALUE background_position = GET_PROP_VALUE_STR(properties_hash, str_background_position);
871
+
872
+ // Background shorthand requires at least 2 properties
873
+ int background_count = (!NIL_P(background_color) ? 1 : 0) +
874
+ (!NIL_P(background_image) ? 1 : 0) +
875
+ (!NIL_P(background_repeat) ? 1 : 0) +
876
+ (!NIL_P(background_attachment) ? 1 : 0) +
877
+ (!NIL_P(background_position) ? 1 : 0);
878
+
879
+ if (background_count >= 2) {
880
+ // Use first available property's metadata as reference
881
+ VALUE background_data_src = !NIL_P(background_color) ? GET_PROP_DATA_STR(properties_hash, str_background_color) :
882
+ !NIL_P(background_image) ? GET_PROP_DATA_STR(properties_hash, str_background_image) :
883
+ !NIL_P(background_repeat) ? GET_PROP_DATA_STR(properties_hash, str_background_repeat) :
884
+ !NIL_P(background_attachment) ? GET_PROP_DATA_STR(properties_hash, str_background_attachment) :
885
+ GET_PROP_DATA_STR(properties_hash, str_background_position);
886
+ VALUE background_important = rb_hash_aref(background_data_src, ID2SYM(id_important));
887
+ int background_is_important = RTEST(background_important);
888
+
889
+ // Check that all present properties have the same !important flag
890
+ int important_match = CHECK_IMPORTANT_MATCH(properties_hash, str_background_color, background_is_important) &&
891
+ CHECK_IMPORTANT_MATCH(properties_hash, str_background_image, background_is_important) &&
892
+ CHECK_IMPORTANT_MATCH(properties_hash, str_background_repeat, background_is_important) &&
893
+ CHECK_IMPORTANT_MATCH(properties_hash, str_background_attachment, background_is_important) &&
894
+ CHECK_IMPORTANT_MATCH(properties_hash, str_background_position, background_is_important);
895
+
896
+ if (important_match) {
897
+ VALUE background_props = rb_hash_new();
898
+ if (!NIL_P(background_color)) rb_hash_aset(background_props, str_background_color, background_color);
899
+ if (!NIL_P(background_image)) rb_hash_aset(background_props, str_background_image, background_image);
900
+ if (!NIL_P(background_repeat)) rb_hash_aset(background_props, str_background_repeat, background_repeat);
901
+ if (!NIL_P(background_attachment)) rb_hash_aset(background_props, str_background_attachment, background_attachment);
902
+ if (!NIL_P(background_position)) rb_hash_aset(background_props, str_background_position, background_position);
903
+
904
+ VALUE background_shorthand = cataract_create_background_shorthand(Qnil, background_props);
905
+ if (!NIL_P(background_shorthand)) {
906
+ int background_spec = NUM2INT(rb_hash_aref(background_data_src, ID2SYM(id_specificity)));
907
+
908
+ VALUE background_data = rb_hash_new();
909
+ rb_hash_aset(background_data, ID2SYM(id_value), background_shorthand);
910
+ rb_hash_aset(background_data, ID2SYM(id_specificity), INT2NUM(background_spec));
911
+ rb_hash_aset(background_data, ID2SYM(id_important), background_important);
912
+ rb_hash_aset(properties_hash, str_background, background_data);
913
+
914
+ // Remove longhand properties
915
+ if (!NIL_P(background_color)) rb_hash_delete(properties_hash, str_background_color);
916
+ if (!NIL_P(background_image)) rb_hash_delete(properties_hash, str_background_image);
917
+ if (!NIL_P(background_repeat)) rb_hash_delete(properties_hash, str_background_repeat);
918
+ if (!NIL_P(background_attachment)) rb_hash_delete(properties_hash, str_background_attachment);
919
+ if (!NIL_P(background_position)) rb_hash_delete(properties_hash, str_background_position);
920
+ }
921
+ RB_GC_GUARD(background_props);
922
+ RB_GC_GUARD(background_shorthand);
923
+ }
924
+ }
925
+
926
+ #undef GET_PROP_VALUE
927
+ #undef GET_PROP_DATA
928
+
929
+ // Build merged declarations array
930
+ VALUE merged_declarations = rb_ary_new();
931
+ rb_hash_foreach(properties_hash, merge_build_result_callback, merged_declarations);
932
+
933
+ // Determine final selector (allocate only once at the end)
934
+ VALUE final_selector;
935
+ if (has_nesting && all_same_selector && first_selector_ptr != NULL) {
936
+ // All rules have same selector - use it for rollup
937
+ final_selector = rb_usascii_str_new(first_selector_ptr, first_selector_len);
938
+ } else {
939
+ // Mixed selectors or no nesting - use "merged"
940
+ final_selector = str_merged_selector;
941
+ }
942
+
943
+ // Create a new Stylesheet with a single merged rule
944
+ // Use rb_class_new_instance instead of rb_funcall for better performance
945
+ VALUE merged_sheet = rb_class_new_instance(0, NULL, cStylesheet);
946
+
947
+ // Create merged rule
948
+ VALUE merged_rule = rb_struct_new(cRule,
949
+ INT2FIX(0), // id
950
+ final_selector, // selector (rolled-up or "merged")
951
+ merged_declarations, // declarations
952
+ Qnil, // specificity (not applicable)
953
+ Qnil, // parent_rule_id (not nested)
954
+ Qnil // nesting_style (not nested)
955
+ );
956
+
957
+ // Set @rules array with single merged rule (use cached ID)
958
+ VALUE rules_ary = rb_ary_new_from_args(1, merged_rule);
959
+ rb_ivar_set(merged_sheet, id_ivar_rules, rules_ary);
960
+
961
+ // Set @media_index with :all pointing to rule 0 (use cached ID)
962
+ VALUE media_idx = rb_hash_new();
963
+ VALUE all_ids = rb_ary_new_from_args(1, INT2FIX(0));
964
+ rb_hash_aset(media_idx, ID2SYM(id_all), all_ids);
965
+ rb_ivar_set(merged_sheet, id_ivar_media_index, media_idx);
966
+
967
+ // Guard first_selector_value: C pointer extracted via RSTRING_PTR during iteration,
968
+ // then used after many allocations (hash operations, shorthand expansions) when
969
+ // creating final_selector with rb_usascii_str_new
970
+ RB_GC_GUARD(first_selector_value);
971
+
972
+ return merged_sheet;
973
+ }