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,1086 @@
1
+ #include <ruby.h>
2
+ #include <stdio.h>
3
+ #include "cataract.h"
4
+
5
+ // Global struct class definitions
6
+ VALUE cRule;
7
+ VALUE cDeclaration;
8
+ VALUE cAtRule;
9
+ VALUE cStylesheet;
10
+
11
+ // Error class definitions (shared with main extension)
12
+ VALUE eCataractError;
13
+ VALUE eDepthError;
14
+ VALUE eSizeError;
15
+
16
+ // ============================================================================
17
+ // Stubbed Implementation - Phase 1
18
+ // ============================================================================
19
+
20
+ /*
21
+ * Parse CSS string into Rule structs
22
+ * Manages @_last_rule_id, @rules, @media_index, and @charset ivars on stylesheet_obj
23
+ *
24
+ * @param module [Module] Cataract module (unused, required for module function)
25
+ * @param stylesheet_obj [Stylesheet] The stylesheet instance
26
+ * @param css_string [String] CSS string to parse
27
+ * @return [VALUE] stylesheet_obj (for method chaining)
28
+ */
29
+ /*
30
+ * Parse CSS and return hash with parsed data
31
+ * This matches the old parse_css API
32
+ *
33
+ * @param css_string [String] CSS to parse
34
+ * @return [Hash] { rules: [...], media_index: {...}, charset: "..." }
35
+ */
36
+ VALUE parse_css_new(VALUE self, VALUE css_string) {
37
+ return parse_css_new_impl(css_string, 0);
38
+ }
39
+
40
+ /*
41
+ * Serialize rules array to CSS string
42
+ * Note: Media query grouping now handled in Ruby layer using @media_index
43
+ *
44
+ * @param rules_array [Array<Rule>] Flat array of rules in insertion order
45
+ * @param charset [String, nil] Optional @charset value
46
+ * @return [String] CSS string
47
+ */
48
+ // Helper to serialize a single rule's declarations
49
+ static void serialize_declarations(VALUE result, VALUE declarations) {
50
+ long decl_len = RARRAY_LEN(declarations);
51
+ for (long j = 0; j < decl_len; j++) {
52
+ VALUE decl = rb_ary_entry(declarations, j);
53
+ VALUE property = rb_struct_aref(decl, INT2FIX(DECL_PROPERTY));
54
+ VALUE value = rb_struct_aref(decl, INT2FIX(DECL_VALUE));
55
+ VALUE important = rb_struct_aref(decl, INT2FIX(DECL_IMPORTANT));
56
+
57
+ rb_str_append(result, property);
58
+ rb_str_cat2(result, ": ");
59
+ rb_str_append(result, value);
60
+
61
+ if (RTEST(important)) {
62
+ rb_str_cat2(result, " !important");
63
+ }
64
+
65
+ rb_str_cat2(result, ";");
66
+
67
+ // Add space after semicolon except for last declaration
68
+ if (j < decl_len - 1) {
69
+ rb_str_cat2(result, " ");
70
+ }
71
+ }
72
+ }
73
+
74
+ // Formatted version - each declaration on its own line with indentation
75
+ static void serialize_declarations_formatted(VALUE result, VALUE declarations, const char *indent) {
76
+ long decl_len = RARRAY_LEN(declarations);
77
+ for (long j = 0; j < decl_len; j++) {
78
+ VALUE decl = rb_ary_entry(declarations, j);
79
+ VALUE property = rb_struct_aref(decl, INT2FIX(DECL_PROPERTY));
80
+ VALUE value = rb_struct_aref(decl, INT2FIX(DECL_VALUE));
81
+ VALUE important = rb_struct_aref(decl, INT2FIX(DECL_IMPORTANT));
82
+
83
+ rb_str_cat2(result, indent);
84
+ rb_str_append(result, property);
85
+ rb_str_cat2(result, ": ");
86
+ rb_str_append(result, value);
87
+
88
+ if (RTEST(important)) {
89
+ rb_str_cat2(result, " !important");
90
+ }
91
+
92
+ rb_str_cat2(result, ";\n");
93
+ }
94
+ }
95
+
96
+ // Helper to serialize an AtRule (@keyframes, @font-face, etc)
97
+ static void serialize_at_rule(VALUE result, VALUE at_rule) {
98
+ VALUE selector = rb_struct_aref(at_rule, INT2FIX(AT_RULE_SELECTOR));
99
+ VALUE content = rb_struct_aref(at_rule, INT2FIX(AT_RULE_CONTENT));
100
+
101
+ rb_str_append(result, selector);
102
+ rb_str_cat2(result, " {\n");
103
+
104
+ // Check if content is rules or declarations
105
+ if (RARRAY_LEN(content) > 0) {
106
+ VALUE first = rb_ary_entry(content, 0);
107
+
108
+ if (rb_obj_is_kind_of(first, cRule)) {
109
+ // Serialize as nested rules (e.g., @keyframes)
110
+ for (long i = 0; i < RARRAY_LEN(content); i++) {
111
+ VALUE nested_rule = rb_ary_entry(content, i);
112
+ VALUE nested_selector = rb_struct_aref(nested_rule, INT2FIX(RULE_SELECTOR));
113
+ VALUE nested_declarations = rb_struct_aref(nested_rule, INT2FIX(RULE_DECLARATIONS));
114
+
115
+ rb_str_cat2(result, " ");
116
+ rb_str_append(result, nested_selector);
117
+ rb_str_cat2(result, " { ");
118
+ serialize_declarations(result, nested_declarations);
119
+ rb_str_cat2(result, " }\n");
120
+ }
121
+ } else {
122
+ // Serialize as declarations (e.g., @font-face)
123
+ rb_str_cat2(result, " ");
124
+ serialize_declarations(result, content);
125
+ rb_str_cat2(result, "\n");
126
+ }
127
+ }
128
+
129
+ rb_str_cat2(result, "}\n");
130
+ }
131
+
132
+ // Helper to "unresolve" a child selector back to its nested form
133
+ // Input: parent_selector=".button", child_selector=".button:hover", nesting_style=EXPLICIT
134
+ // Output: "&:hover"
135
+ // Input: parent_selector=".parent", child_selector=".parent .child", nesting_style=IMPLICIT
136
+ // Output: ".child"
137
+ static VALUE unresolve_selector(VALUE parent_selector, VALUE child_selector, VALUE nesting_style) {
138
+ const char *parent = RSTRING_PTR(parent_selector);
139
+ long parent_len = RSTRING_LEN(parent_selector);
140
+ const char *child = RSTRING_PTR(child_selector);
141
+ long child_len = RSTRING_LEN(child_selector);
142
+
143
+ int style = NIL_P(nesting_style) ? NESTING_STYLE_IMPLICIT : FIX2INT(nesting_style);
144
+
145
+ VALUE result;
146
+
147
+ if (style == NESTING_STYLE_EXPLICIT) {
148
+ // Explicit nesting: replace parent with &
149
+ // ".button:hover" -> "&:hover"
150
+ // ".button.primary" -> "&.primary"
151
+
152
+ // Find where parent ends in child
153
+ if (strncmp(child, parent, parent_len) == 0) {
154
+ // Parent matches at start - replace with &
155
+ result = rb_str_new_cstr("&");
156
+ rb_str_cat(result, child + parent_len, child_len - parent_len);
157
+ } else {
158
+ // Fallback: just return child (shouldn't happen)
159
+ result = child_selector;
160
+ }
161
+ } else {
162
+ // Implicit nesting: strip parent + space from beginning
163
+ // ".parent .child" -> ".child"
164
+
165
+ if (strncmp(child, parent, parent_len) == 0) {
166
+ // Check if followed by space
167
+ if (child_len > parent_len && child[parent_len] == ' ') {
168
+ // Strip "parent " prefix
169
+ result = rb_str_new(child + parent_len + 1, child_len - parent_len - 1);
170
+ } else {
171
+ // Fallback: return child as-is
172
+ result = child_selector;
173
+ }
174
+ } else {
175
+ // Fallback: return child as-is
176
+ result = child_selector;
177
+ }
178
+ }
179
+
180
+ // Guard both selectors since we extracted C pointers and did allocations
181
+ RB_GC_GUARD(parent_selector);
182
+ RB_GC_GUARD(child_selector);
183
+
184
+ return result;
185
+ }
186
+
187
+ // Helper to serialize a single rule (dispatches to at-rule serializer if needed)
188
+ static void serialize_rule(VALUE result, VALUE rule) {
189
+ // Check if this is an AtRule
190
+ if (rb_obj_is_kind_of(rule, cAtRule)) {
191
+ serialize_at_rule(result, rule);
192
+ return;
193
+ }
194
+
195
+ // Regular Rule serialization
196
+ VALUE selector = rb_struct_aref(rule, INT2FIX(RULE_SELECTOR));
197
+ VALUE declarations = rb_struct_aref(rule, INT2FIX(RULE_DECLARATIONS));
198
+
199
+ rb_str_append(result, selector);
200
+ rb_str_cat2(result, " { ");
201
+ serialize_declarations(result, declarations);
202
+ rb_str_cat2(result, " }\n");
203
+ }
204
+
205
+ // Helper to serialize an AtRule with formatting (@keyframes, @font-face, etc)
206
+ static void serialize_at_rule_formatted(VALUE result, VALUE at_rule, const char *indent) {
207
+ VALUE selector = rb_struct_aref(at_rule, INT2FIX(AT_RULE_SELECTOR));
208
+ VALUE content = rb_struct_aref(at_rule, INT2FIX(AT_RULE_CONTENT));
209
+
210
+ rb_str_cat2(result, indent);
211
+ rb_str_append(result, selector);
212
+ rb_str_cat2(result, " {\n");
213
+
214
+ // Check if content is rules or declarations
215
+ if (RARRAY_LEN(content) > 0) {
216
+ VALUE first = rb_ary_entry(content, 0);
217
+
218
+ if (rb_obj_is_kind_of(first, cRule)) {
219
+ // Serialize as nested rules (e.g., @keyframes) with formatting
220
+ for (long i = 0; i < RARRAY_LEN(content); i++) {
221
+ VALUE nested_rule = rb_ary_entry(content, i);
222
+ VALUE nested_selector = rb_struct_aref(nested_rule, INT2FIX(RULE_SELECTOR));
223
+ VALUE nested_declarations = rb_struct_aref(nested_rule, INT2FIX(RULE_DECLARATIONS));
224
+
225
+ // Nested selector with opening brace (2-space indent)
226
+ rb_str_cat2(result, indent);
227
+ rb_str_cat2(result, " ");
228
+ rb_str_append(result, nested_selector);
229
+ rb_str_cat2(result, " {\n");
230
+
231
+ // Declarations on their own line (4-space indent)
232
+ rb_str_cat2(result, indent);
233
+ rb_str_cat2(result, " ");
234
+ serialize_declarations(result, nested_declarations);
235
+ rb_str_cat2(result, "\n");
236
+
237
+ // Closing brace (2-space indent)
238
+ rb_str_cat2(result, indent);
239
+ rb_str_cat2(result, " }\n");
240
+ }
241
+ } else {
242
+ // Serialize as declarations (e.g., @font-face)
243
+ rb_str_cat2(result, indent);
244
+ rb_str_cat2(result, " ");
245
+ serialize_declarations(result, content);
246
+ rb_str_cat2(result, "\n");
247
+ }
248
+ }
249
+
250
+ rb_str_cat2(result, indent);
251
+ rb_str_cat2(result, "}\n");
252
+ }
253
+
254
+ // Helper to serialize a single rule with formatting (indented, multi-line)
255
+ static void serialize_rule_formatted(VALUE result, VALUE rule, const char *indent) {
256
+ // Check if this is an AtRule
257
+ if (rb_obj_is_kind_of(rule, cAtRule)) {
258
+ serialize_at_rule_formatted(result, rule, indent);
259
+ return;
260
+ }
261
+
262
+ // Regular Rule serialization with formatting
263
+ VALUE selector = rb_struct_aref(rule, INT2FIX(RULE_SELECTOR));
264
+ VALUE declarations = rb_struct_aref(rule, INT2FIX(RULE_DECLARATIONS));
265
+
266
+ // Selector line with opening brace
267
+ rb_str_cat2(result, indent);
268
+ rb_str_append(result, selector);
269
+ rb_str_cat2(result, " {\n");
270
+
271
+ // Declarations on their own line with extra indentation
272
+ rb_str_cat2(result, indent);
273
+ rb_str_cat2(result, " ");
274
+ serialize_declarations(result, declarations);
275
+ rb_str_cat2(result, "\n");
276
+
277
+ // Closing brace
278
+ rb_str_cat2(result, indent);
279
+ rb_str_cat2(result, "}\n");
280
+ }
281
+
282
+ // Context for building rule_to_media map
283
+ struct build_rule_map_ctx {
284
+ VALUE rule_to_media;
285
+ };
286
+
287
+ // Callback to build reverse map from rule_id to media_sym
288
+ static int build_rule_map_callback(VALUE media_sym, VALUE rule_ids, VALUE arg) {
289
+ struct build_rule_map_ctx *ctx = (struct build_rule_map_ctx *)arg;
290
+
291
+ Check_Type(rule_ids, T_ARRAY);
292
+ long ids_len = RARRAY_LEN(rule_ids);
293
+
294
+ for (long i = 0; i < ids_len; i++) {
295
+ VALUE id = rb_ary_entry(rule_ids, i);
296
+ VALUE existing = rb_hash_aref(ctx->rule_to_media, id);
297
+
298
+ if (NIL_P(existing)) {
299
+ rb_hash_aset(ctx->rule_to_media, id, media_sym);
300
+ } else {
301
+ // Keep the longer/more specific media query
302
+ VALUE existing_str = rb_sym2str(existing);
303
+ VALUE new_str = rb_sym2str(media_sym);
304
+ if (RSTRING_LEN(new_str) > RSTRING_LEN(existing_str)) {
305
+ rb_hash_aset(ctx->rule_to_media, id, media_sym);
306
+ }
307
+ }
308
+ }
309
+
310
+ return ST_CONTINUE;
311
+ }
312
+
313
+ // Original stylesheet serialization (no nesting support)
314
+ static VALUE stylesheet_to_s_original(VALUE rules_array, VALUE media_index, VALUE charset) {
315
+ Check_Type(rules_array, T_ARRAY);
316
+ Check_Type(media_index, T_HASH);
317
+
318
+ VALUE result = rb_str_new_cstr("");
319
+
320
+ // Add charset if present
321
+ if (!NIL_P(charset)) {
322
+ rb_str_cat2(result, "@charset \"");
323
+ rb_str_append(result, charset);
324
+ rb_str_cat2(result, "\";\n");
325
+ }
326
+
327
+ long total_rules = RARRAY_LEN(rules_array);
328
+
329
+ // Build a map from rule_id to media query symbol using rb_hash_foreach
330
+ VALUE rule_to_media = rb_hash_new();
331
+ struct build_rule_map_ctx map_ctx = { rule_to_media };
332
+ rb_hash_foreach(media_index, build_rule_map_callback, (VALUE)&map_ctx);
333
+
334
+ // Iterate through rules in insertion order, grouping consecutive media queries
335
+ VALUE current_media = Qnil;
336
+ int in_media_block = 0;
337
+
338
+ for (long i = 0; i < total_rules; i++) {
339
+ VALUE rule = rb_ary_entry(rules_array, i);
340
+ VALUE rule_id = rb_struct_aref(rule, INT2FIX(RULE_ID));
341
+ VALUE rule_media = rb_hash_aref(rule_to_media, rule_id);
342
+
343
+ if (NIL_P(rule_media)) {
344
+ // Not in any media query - close any open media block first
345
+ if (in_media_block) {
346
+ rb_str_cat2(result, "}\n");
347
+ in_media_block = 0;
348
+ current_media = Qnil;
349
+ }
350
+
351
+ // Output rule directly
352
+ serialize_rule(result, rule);
353
+ } else {
354
+ // This rule is in a media query
355
+ // Check if media query changed from previous rule
356
+ if (NIL_P(current_media) || !rb_equal(current_media, rule_media)) {
357
+ // Close previous media block if open
358
+ if (in_media_block) {
359
+ rb_str_cat2(result, "}\n");
360
+ }
361
+
362
+ // Open new media block
363
+ current_media = rule_media;
364
+ rb_str_cat2(result, "@media ");
365
+ rb_str_append(result, rb_sym2str(rule_media));
366
+ rb_str_cat2(result, " {\n");
367
+ in_media_block = 1;
368
+ }
369
+
370
+ // Serialize rule inside media block
371
+ serialize_rule(result, rule);
372
+ }
373
+ }
374
+
375
+ // Close final media block if still open
376
+ if (in_media_block) {
377
+ rb_str_cat2(result, "}\n");
378
+ }
379
+
380
+ return result;
381
+ }
382
+
383
+ // Forward declarations
384
+ static void serialize_children_only(VALUE result, VALUE rules_array, long rule_idx,
385
+ VALUE rule_to_media, VALUE parent_to_children, VALUE parent_selector,
386
+ VALUE parent_declarations, int formatted, int indent_level);
387
+ static void serialize_rule_with_children(VALUE result, VALUE rules_array, long rule_idx,
388
+ VALUE rule_to_media, VALUE parent_to_children,
389
+ int formatted, int indent_level);
390
+
391
+ // Helper: Only serialize children of a rule (not the rule itself)
392
+ static void serialize_children_only(VALUE result, VALUE rules_array, long rule_idx,
393
+ VALUE rule_to_media, VALUE parent_to_children, VALUE parent_selector,
394
+ VALUE parent_declarations, int formatted, int indent_level) {
395
+ VALUE rule = rb_ary_entry(rules_array, rule_idx);
396
+ VALUE rule_id = rb_struct_aref(rule, INT2FIX(RULE_ID));
397
+ VALUE rule_media = rb_hash_aref(rule_to_media, rule_id); // Look up by rule ID, not array index
398
+ int parent_has_declarations = !NIL_P(parent_declarations) && RARRAY_LEN(parent_declarations) > 0;
399
+
400
+ // Build indentation string for this level (only if formatted)
401
+ VALUE indent_str = Qnil;
402
+ if (formatted) {
403
+ indent_str = rb_str_new_cstr("");
404
+ for (int i = 0; i < indent_level; i++) {
405
+ rb_str_cat2(indent_str, " ");
406
+ }
407
+ }
408
+
409
+ // Get children of this rule using the map
410
+ VALUE children_indices = rb_hash_aref(parent_to_children, rule_id);
411
+
412
+ DEBUG_PRINTF("[SERIALIZE] Looking up children for rule_id=%s\n",
413
+ RSTRING_PTR(rb_inspect(rule_id)));
414
+
415
+ if (!NIL_P(children_indices)) {
416
+ long num_children = RARRAY_LEN(children_indices);
417
+ DEBUG_PRINTF("[SERIALIZE] Found %ld children for rule %ld (id=%s)\n",
418
+ num_children, rule_idx, RSTRING_PTR(rb_inspect(rule_id)));
419
+
420
+ // Serialize selector-nested children
421
+ for (long i = 0; i < num_children; i++) {
422
+ long child_idx = FIX2LONG(rb_ary_entry(children_indices, i));
423
+ VALUE child = rb_ary_entry(rules_array, child_idx);
424
+ VALUE child_id = rb_struct_aref(child, INT2FIX(RULE_ID));
425
+ VALUE child_media = rb_hash_aref(rule_to_media, child_id); // Look up by rule ID
426
+
427
+ DEBUG_PRINTF("[SERIALIZE] Child %ld: child_media=%s, rule_media=%s\n", child_idx,
428
+ NIL_P(child_media) ? "nil" : RSTRING_PTR(rb_inspect(child_media)),
429
+ NIL_P(rule_media) ? "nil" : RSTRING_PTR(rb_inspect(rule_media)));
430
+
431
+ // Only serialize selector-nested children here (not @media nested)
432
+ if (NIL_P(child_media) || rb_equal(child_media, rule_media)) {
433
+ DEBUG_PRINTF("[SERIALIZE] -> Serializing as selector-nested child\n");
434
+ VALUE child_selector = rb_struct_aref(child, INT2FIX(RULE_SELECTOR));
435
+ VALUE child_nesting_style = rb_struct_aref(child, INT2FIX(RULE_NESTING_STYLE));
436
+
437
+ // Unresolve selector
438
+ VALUE nested_selector = unresolve_selector(parent_selector, child_selector, child_nesting_style);
439
+
440
+ if (formatted) {
441
+ // Formatted: indent before nested selector
442
+ rb_str_append(result, indent_str);
443
+ rb_str_append(result, nested_selector);
444
+ rb_str_cat2(result, " {\n");
445
+
446
+ // Serialize child declarations (each on its own line)
447
+ VALUE child_declarations = rb_struct_aref(child, INT2FIX(RULE_DECLARATIONS));
448
+ if (!NIL_P(child_declarations) && RARRAY_LEN(child_declarations) > 0) {
449
+ // Build child indent (one level deeper than current)
450
+ VALUE child_indent = rb_str_new_cstr("");
451
+ for (int j = 0; j <= indent_level; j++) {
452
+ rb_str_cat2(child_indent, " ");
453
+ }
454
+ const char *child_indent_ptr = RSTRING_PTR(child_indent);
455
+ serialize_declarations_formatted(result, child_declarations, child_indent_ptr);
456
+ RB_GC_GUARD(child_indent);
457
+ }
458
+
459
+ // Recursively serialize grandchildren
460
+ serialize_children_only(result, rules_array, child_idx, rule_to_media, parent_to_children,
461
+ child_selector, child_declarations, formatted, indent_level + 1);
462
+
463
+ // Closing brace with indentation and newline
464
+ rb_str_append(result, indent_str);
465
+ rb_str_cat2(result, "}\n");
466
+ } else {
467
+ // Compact: space before nested selector only if parent has declarations
468
+ if (parent_has_declarations) {
469
+ rb_str_cat2(result, " ");
470
+ }
471
+ rb_str_append(result, nested_selector);
472
+ rb_str_cat2(result, " { ");
473
+
474
+ // Serialize child declarations
475
+ VALUE child_declarations = rb_struct_aref(child, INT2FIX(RULE_DECLARATIONS));
476
+ serialize_declarations(result, child_declarations);
477
+
478
+ // Recursively serialize grandchildren
479
+ serialize_children_only(result, rules_array, child_idx, rule_to_media, parent_to_children,
480
+ child_selector, child_declarations, formatted, indent_level);
481
+
482
+ rb_str_cat2(result, " }");
483
+ }
484
+ }
485
+ }
486
+
487
+ // Serialize nested @media children (different media than parent)
488
+ for (long i = 0; i < num_children; i++) {
489
+ long child_idx = FIX2LONG(rb_ary_entry(children_indices, i));
490
+ VALUE child = rb_ary_entry(rules_array, child_idx);
491
+ VALUE child_id = rb_struct_aref(child, INT2FIX(RULE_ID));
492
+ VALUE child_media = rb_hash_aref(rule_to_media, child_id); // Look up by rule ID
493
+
494
+ // Check if this is a different media than parent
495
+ if (!NIL_P(child_media) && !rb_equal(rule_media, child_media)) {
496
+ // Nested @media!
497
+ if (formatted) {
498
+ rb_str_append(result, indent_str);
499
+ rb_str_cat2(result, "@media ");
500
+ rb_str_append(result, rb_sym2str(child_media));
501
+ rb_str_cat2(result, " {\n");
502
+
503
+ VALUE child_declarations = rb_struct_aref(child, INT2FIX(RULE_DECLARATIONS));
504
+ if (!NIL_P(child_declarations) && RARRAY_LEN(child_declarations) > 0) {
505
+ // Build child indent (one level deeper than current)
506
+ VALUE child_indent = rb_str_new_cstr("");
507
+ for (int j = 0; j <= indent_level; j++) {
508
+ rb_str_cat2(child_indent, " ");
509
+ }
510
+ const char *child_indent_ptr = RSTRING_PTR(child_indent);
511
+ serialize_declarations_formatted(result, child_declarations, child_indent_ptr);
512
+ RB_GC_GUARD(child_indent);
513
+ }
514
+
515
+ rb_str_append(result, indent_str);
516
+ rb_str_cat2(result, "}\n");
517
+ } else {
518
+ rb_str_cat2(result, " @media ");
519
+ rb_str_append(result, rb_sym2str(child_media));
520
+ rb_str_cat2(result, " { ");
521
+
522
+ VALUE child_declarations = rb_struct_aref(child, INT2FIX(RULE_DECLARATIONS));
523
+ serialize_declarations(result, child_declarations);
524
+
525
+ rb_str_cat2(result, " }");
526
+ }
527
+ }
528
+ }
529
+ }
530
+ }
531
+
532
+ // Recursive serializer for a rule and its nested children
533
+ static void serialize_rule_with_children(VALUE result, VALUE rules_array, long rule_idx,
534
+ VALUE rule_to_media, VALUE parent_to_children,
535
+ int formatted, int indent_level) {
536
+ VALUE rule = rb_ary_entry(rules_array, rule_idx);
537
+ VALUE selector = rb_struct_aref(rule, INT2FIX(RULE_SELECTOR));
538
+ VALUE declarations = rb_struct_aref(rule, INT2FIX(RULE_DECLARATIONS));
539
+
540
+ DEBUG_PRINTF("[SERIALIZE] Rule %ld: selector=%s\n", rule_idx, RSTRING_PTR(selector));
541
+
542
+ if (formatted) {
543
+ // Formatted output with indentation
544
+ rb_str_append(result, selector);
545
+ rb_str_cat2(result, " {\n");
546
+
547
+ // Serialize own declarations with indentation (each on its own line)
548
+ if (!NIL_P(declarations) && RARRAY_LEN(declarations) > 0) {
549
+ serialize_declarations_formatted(result, declarations, " ");
550
+ }
551
+
552
+ // Serialize nested children
553
+ serialize_children_only(result, rules_array, rule_idx, rule_to_media, parent_to_children,
554
+ selector, declarations, formatted, indent_level + 1);
555
+
556
+ rb_str_cat2(result, "}\n");
557
+ } else {
558
+ // Compact output
559
+ rb_str_append(result, selector);
560
+ rb_str_cat2(result, " { ");
561
+
562
+ // Serialize own declarations
563
+ serialize_declarations(result, declarations);
564
+
565
+ // Serialize nested children
566
+ serialize_children_only(result, rules_array, rule_idx, rule_to_media, parent_to_children,
567
+ selector, declarations, formatted, indent_level);
568
+
569
+ rb_str_cat2(result, " }\n");
570
+ }
571
+ }
572
+
573
+ // New stylesheet serialization entry point - checks for nesting and delegates
574
+ static VALUE stylesheet_to_s_new(VALUE self, VALUE rules_array, VALUE media_index, VALUE charset, VALUE has_nesting) {
575
+ Check_Type(rules_array, T_ARRAY);
576
+ Check_Type(media_index, T_HASH);
577
+
578
+ // Fast path: if no nesting, use original implementation (zero overhead)
579
+ if (!RTEST(has_nesting)) {
580
+ return stylesheet_to_s_original(rules_array, media_index, charset);
581
+ }
582
+
583
+ // SLOW PATH: Has nesting - use lookahead approach
584
+ long total_rules = RARRAY_LEN(rules_array);
585
+ VALUE result = rb_str_new_cstr("");
586
+
587
+ // Add charset if present
588
+ if (!NIL_P(charset)) {
589
+ rb_str_cat2(result, "@charset \"");
590
+ rb_str_append(result, charset);
591
+ rb_str_cat2(result, "\";\n");
592
+ }
593
+
594
+ // Build rule_to_media map
595
+ VALUE rule_to_media = rb_hash_new();
596
+ struct build_rule_map_ctx map_ctx = { rule_to_media };
597
+ rb_hash_foreach(media_index, build_rule_map_callback, (VALUE)&map_ctx);
598
+
599
+ // Build parent_to_children map (parent_rule_id -> array of child indices)
600
+ // This allows O(1) lookup of children when serializing each parent
601
+ VALUE parent_to_children = rb_hash_new();
602
+ for (long i = 0; i < total_rules; i++) {
603
+ VALUE rule = rb_ary_entry(rules_array, i);
604
+ VALUE parent_id = rb_struct_aref(rule, INT2FIX(RULE_PARENT_RULE_ID));
605
+
606
+ if (!NIL_P(parent_id)) {
607
+ DEBUG_PRINTF("[MAP] Rule %ld has parent_id=%s, adding to map\n", i,
608
+ RSTRING_PTR(rb_inspect(parent_id)));
609
+
610
+ VALUE children = rb_hash_aref(parent_to_children, parent_id);
611
+ if (NIL_P(children)) {
612
+ children = rb_ary_new();
613
+ rb_hash_aset(parent_to_children, parent_id, children);
614
+ }
615
+ rb_ary_push(children, LONG2FIX(i));
616
+ }
617
+ }
618
+
619
+ DEBUG_PRINTF("[MAP] parent_to_children map: %s\n", RSTRING_PTR(rb_inspect(parent_to_children)));
620
+
621
+ // Serialize only top-level rules (parent_rule_id == nil)
622
+ // Children are serialized recursively
623
+ DEBUG_PRINTF("[SERIALIZE] Starting serialization, total_rules=%ld\n", total_rules);
624
+ for (long i = 0; i < total_rules; i++) {
625
+ VALUE rule = rb_ary_entry(rules_array, i);
626
+ VALUE parent_id = rb_struct_aref(rule, INT2FIX(RULE_PARENT_RULE_ID));
627
+
628
+ DEBUG_PRINTF("[SERIALIZE] Rule %ld: selector=%s, parent_id=%s\n", i,
629
+ RSTRING_PTR(rb_struct_aref(rule, INT2FIX(RULE_SELECTOR))),
630
+ NIL_P(parent_id) ? "nil" : RSTRING_PTR(rb_inspect(parent_id)));
631
+
632
+ // Skip child rules - they're serialized when we hit their parent
633
+ if (!NIL_P(parent_id)) {
634
+ DEBUG_PRINTF("[SERIALIZE] Skipping (is child)\n");
635
+ continue;
636
+ }
637
+
638
+ // Check if this is an AtRule
639
+ if (rb_obj_is_kind_of(rule, cAtRule)) {
640
+ serialize_at_rule(result, rule);
641
+ continue;
642
+ }
643
+
644
+ // Serialize rule with nested children
645
+ serialize_rule_with_children(
646
+ result, rules_array, i, rule_to_media, parent_to_children,
647
+ 0, // formatted (compact)
648
+ 0 // indent_level (top-level)
649
+ );
650
+ }
651
+
652
+ return result;
653
+ }
654
+
655
+ // Original formatted serialization (no nesting support)
656
+ static VALUE stylesheet_to_formatted_s_original(VALUE rules_array, VALUE media_index, VALUE charset) {
657
+ long total_rules = RARRAY_LEN(rules_array);
658
+ VALUE result = rb_str_new_cstr("");
659
+
660
+ // Add charset if present
661
+ if (!NIL_P(charset)) {
662
+ rb_str_cat2(result, "@charset \"");
663
+ rb_str_append(result, charset);
664
+ rb_str_cat2(result, "\";\n");
665
+ }
666
+
667
+ // Build a map from rule_id to media query symbol
668
+ VALUE rule_to_media = rb_hash_new();
669
+ struct build_rule_map_ctx map_ctx = { rule_to_media };
670
+ rb_hash_foreach(media_index, build_rule_map_callback, (VALUE)&map_ctx);
671
+
672
+ // Iterate through rules, grouping consecutive media queries
673
+ VALUE current_media = Qnil;
674
+ int in_media_block = 0;
675
+
676
+ for (long i = 0; i < total_rules; i++) {
677
+ VALUE rule = rb_ary_entry(rules_array, i);
678
+ VALUE rule_id = rb_struct_aref(rule, INT2FIX(RULE_ID));
679
+ VALUE rule_media = rb_hash_aref(rule_to_media, rule_id);
680
+
681
+ if (NIL_P(rule_media)) {
682
+ // Not in any media query - close any open media block first
683
+ if (in_media_block) {
684
+ rb_str_cat2(result, "}\n");
685
+ in_media_block = 0;
686
+ current_media = Qnil;
687
+ }
688
+
689
+ // Output rule with no indentation
690
+ serialize_rule_formatted(result, rule, "");
691
+ } else {
692
+ // This rule is in a media query
693
+ if (NIL_P(current_media) || !rb_equal(current_media, rule_media)) {
694
+ // Close previous media block if open
695
+ if (in_media_block) {
696
+ rb_str_cat2(result, "}\n");
697
+ } else {
698
+ // Add blank line before @media if transitioning from non-media rules
699
+ if (RSTRING_LEN(result) > 0) {
700
+ rb_str_cat2(result, "\n");
701
+ }
702
+ }
703
+
704
+ // Open new media block
705
+ current_media = rule_media;
706
+ rb_str_cat2(result, "@media ");
707
+ rb_str_append(result, rb_sym2str(rule_media));
708
+ rb_str_cat2(result, " {\n");
709
+ in_media_block = 1;
710
+ }
711
+
712
+ // Serialize rule inside media block with 2-space indentation
713
+ serialize_rule_formatted(result, rule, " ");
714
+ }
715
+ }
716
+
717
+ // Close final media block if still open
718
+ if (in_media_block) {
719
+ rb_str_cat2(result, "}\n");
720
+ }
721
+
722
+ return result;
723
+ }
724
+
725
+ // Formatted version with indentation and newlines (with nesting support)
726
+ static VALUE stylesheet_to_formatted_s_new(VALUE self, VALUE rules_array, VALUE media_index, VALUE charset, VALUE has_nesting) {
727
+ Check_Type(rules_array, T_ARRAY);
728
+ Check_Type(media_index, T_HASH);
729
+
730
+ // Fast path: if no nesting, use original implementation (zero overhead)
731
+ if (!RTEST(has_nesting)) {
732
+ return stylesheet_to_formatted_s_original(rules_array, media_index, charset);
733
+ }
734
+
735
+ // SLOW PATH: Has nesting - use parameterized serialization with formatted=1
736
+ long total_rules = RARRAY_LEN(rules_array);
737
+ VALUE result = rb_str_new_cstr("");
738
+
739
+ // Add charset if present
740
+ if (!NIL_P(charset)) {
741
+ rb_str_cat2(result, "@charset \"");
742
+ rb_str_append(result, charset);
743
+ rb_str_cat2(result, "\";\n");
744
+ }
745
+
746
+ // Build rule_to_media map
747
+ VALUE rule_to_media = rb_hash_new();
748
+ struct build_rule_map_ctx map_ctx = { rule_to_media };
749
+ rb_hash_foreach(media_index, build_rule_map_callback, (VALUE)&map_ctx);
750
+
751
+ // Build parent_to_children map (parent_rule_id -> array of child indices)
752
+ VALUE parent_to_children = rb_hash_new();
753
+ for (long i = 0; i < total_rules; i++) {
754
+ VALUE rule = rb_ary_entry(rules_array, i);
755
+ VALUE parent_id = rb_struct_aref(rule, INT2FIX(RULE_PARENT_RULE_ID));
756
+
757
+ if (!NIL_P(parent_id)) {
758
+ VALUE children = rb_hash_aref(parent_to_children, parent_id);
759
+ if (NIL_P(children)) {
760
+ children = rb_ary_new();
761
+ rb_hash_aset(parent_to_children, parent_id, children);
762
+ }
763
+ rb_ary_push(children, LONG2FIX(i));
764
+ }
765
+ }
766
+
767
+ // Serialize only top-level rules (parent_rule_id == nil)
768
+ for (long i = 0; i < total_rules; i++) {
769
+ VALUE rule = rb_ary_entry(rules_array, i);
770
+ VALUE parent_id = rb_struct_aref(rule, INT2FIX(RULE_PARENT_RULE_ID));
771
+
772
+ // Skip child rules - they're serialized when we hit their parent
773
+ if (!NIL_P(parent_id)) {
774
+ continue;
775
+ }
776
+
777
+ // Check if this is an AtRule
778
+ if (rb_obj_is_kind_of(rule, cAtRule)) {
779
+ serialize_at_rule(result, rule);
780
+ continue;
781
+ }
782
+
783
+ // Serialize rule with nested children
784
+ serialize_rule_with_children(
785
+ result, rules_array, i, rule_to_media, parent_to_children,
786
+ 1, // formatted (with indentation)
787
+ 0 // indent_level (top-level)
788
+ );
789
+ }
790
+
791
+ return result;
792
+ }
793
+
794
+ /*
795
+ * Parse declarations string into array of Declaration structs
796
+ *
797
+ * This is a copy of parse_declarations_string from css_parser.c,
798
+ * but creates Declaration structs instead of Declaration structs
799
+ */
800
+ static VALUE new_parse_declarations_string(const char *start, const char *end) {
801
+ VALUE declarations = rb_ary_new();
802
+
803
+ // Note: Comments in declarations aren't stripped (copy_without_comments is in css_parser.c)
804
+ // The parser is error-tolerant, so it just continues parsing as-is.
805
+
806
+ const char *pos = start;
807
+ while (pos < end) {
808
+ // Skip whitespace and semicolons
809
+ while (pos < end && (IS_WHITESPACE(*pos) || *pos == ';')) pos++;
810
+ if (pos >= end) break;
811
+
812
+ // Find property (up to colon)
813
+ const char *prop_start = pos;
814
+ while (pos < end && *pos != ':') pos++;
815
+ if (pos >= end) break; // No colon found
816
+
817
+ const char *prop_end = pos;
818
+ // Trim trailing whitespace
819
+ while (prop_end > prop_start && IS_WHITESPACE(*(prop_end-1))) prop_end--;
820
+ // Trim leading whitespace
821
+ while (prop_start < prop_end && IS_WHITESPACE(*prop_start)) prop_start++;
822
+
823
+ pos++; // Skip colon
824
+ // Trim leading whitespace
825
+ while (pos < end && IS_WHITESPACE(*pos)) pos++;
826
+
827
+ // Find value (up to semicolon or end), handling parentheses
828
+ const char *val_start = pos;
829
+ int paren_depth = 0;
830
+ while (pos < end) {
831
+ if (*pos == '(') paren_depth++;
832
+ else if (*pos == ')') paren_depth--;
833
+ else if (*pos == ';' && paren_depth == 0) break;
834
+ pos++;
835
+ }
836
+ const char *val_end = pos;
837
+ // Trim trailing whitespace
838
+ while (val_end > val_start && IS_WHITESPACE(*(val_end-1))) val_end--;
839
+
840
+ // Check for !important
841
+ int is_important = 0;
842
+ if (val_end - val_start >= 10) { // strlen("!important") = 10
843
+ const char *check = val_end - 10;
844
+ while (check < val_end && IS_WHITESPACE(*check)) check++;
845
+ if (check < val_end && *check == '!') {
846
+ check++;
847
+ while (check < val_end && IS_WHITESPACE(*check)) check++;
848
+ if ((val_end - check) >= 9 && strncmp(check, "important", 9) == 0) {
849
+ is_important = 1;
850
+ const char *important_pos = check - 1;
851
+ while (important_pos > val_start && (IS_WHITESPACE(*(important_pos-1)) || *(important_pos-1) == '!')) {
852
+ important_pos--;
853
+ }
854
+ val_end = important_pos;
855
+ // Trim trailing whitespace again
856
+ while (val_end > val_start && IS_WHITESPACE(*(val_end-1))) val_end--;
857
+ }
858
+ }
859
+ }
860
+
861
+ // Skip if value is empty
862
+ if (val_end > val_start) {
863
+ long prop_len = prop_end - prop_start;
864
+ long val_len = val_end - val_start;
865
+
866
+ // Create property string (US-ASCII, lowercased)
867
+ VALUE property = rb_usascii_str_new(prop_start, prop_len);
868
+ // Lowercase it inline
869
+ char *prop_ptr = RSTRING_PTR(property);
870
+ for (long i = 0; i < prop_len; i++) {
871
+ if (prop_ptr[i] >= 'A' && prop_ptr[i] <= 'Z') {
872
+ prop_ptr[i] += 32;
873
+ }
874
+ }
875
+
876
+ VALUE value = rb_utf8_str_new(val_start, val_len);
877
+
878
+ // Create Declaration struct
879
+ VALUE decl = rb_struct_new(cDeclaration,
880
+ property, value, is_important ? Qtrue : Qfalse);
881
+
882
+ rb_ary_push(declarations, decl);
883
+ }
884
+ }
885
+
886
+ return declarations;
887
+ }
888
+
889
+ /*
890
+ * Convert array of Declaration structs to CSS string
891
+ * Format: "prop: value; prop2: value2 !important; "
892
+ *
893
+ * This is a copy of declarations_array_to_s from cataract.c,
894
+ * but works with Declaration structs instead of Declaration structs
895
+ */
896
+ static VALUE new_declarations_array_to_s(VALUE declarations_array) {
897
+ Check_Type(declarations_array, T_ARRAY);
898
+
899
+ long len = RARRAY_LEN(declarations_array);
900
+ if (len == 0) {
901
+ return rb_str_new_cstr("");
902
+ }
903
+
904
+ // Use rb_str_buf_new for efficient string building
905
+ VALUE result = rb_str_buf_new(len * 32); // Estimate 32 chars per declaration
906
+
907
+ for (long i = 0; i < len; i++) {
908
+ VALUE decl = rb_ary_entry(declarations_array, i);
909
+
910
+ // Validate this is a Declaration struct
911
+ if (!RB_TYPE_P(decl, T_STRUCT) || rb_obj_class(decl) != cDeclaration) {
912
+ rb_raise(rb_eTypeError,
913
+ "Expected array of Declaration structs, got %s at index %ld",
914
+ rb_obj_classname(decl), i);
915
+ }
916
+
917
+ // Extract struct fields
918
+ VALUE property = rb_struct_aref(decl, INT2FIX(DECL_PROPERTY));
919
+ VALUE value = rb_struct_aref(decl, INT2FIX(DECL_VALUE));
920
+ VALUE important = rb_struct_aref(decl, INT2FIX(DECL_IMPORTANT));
921
+
922
+ // Append: "property: value"
923
+ rb_str_buf_append(result, property);
924
+ rb_str_buf_cat2(result, ": ");
925
+ rb_str_buf_append(result, value);
926
+
927
+ // Append " !important" if needed
928
+ if (RTEST(important)) {
929
+ rb_str_buf_cat2(result, " !important");
930
+ }
931
+
932
+ rb_str_buf_cat2(result, "; ");
933
+
934
+ RB_GC_GUARD(decl);
935
+ RB_GC_GUARD(property);
936
+ RB_GC_GUARD(value);
937
+ RB_GC_GUARD(important);
938
+ }
939
+
940
+ // Strip trailing space
941
+ rb_str_set_len(result, RSTRING_LEN(result) - 1);
942
+
943
+ RB_GC_GUARD(result);
944
+ return result;
945
+ }
946
+
947
+ /*
948
+ * Instance method: Declarations#to_s
949
+ * Converts declarations to CSS string
950
+ *
951
+ * @return [String] CSS declarations like "color: red; margin: 10px !important;"
952
+ */
953
+ static VALUE new_declarations_to_s_method(VALUE self) {
954
+ // Get @values instance variable (array of Declaration structs)
955
+ VALUE values = rb_ivar_get(self, rb_intern("@values"));
956
+
957
+ // Call core serialization function
958
+ return new_declarations_array_to_s(values);
959
+ }
960
+
961
+ /*
962
+ * Ruby-facing wrapper for new_parse_declarations
963
+ *
964
+ * @param declarations_string [String] CSS declarations like "color: red; margin: 10px"
965
+ * @return [Array<Declaration>] Array of parsed declaration structs
966
+ */
967
+ static VALUE new_parse_declarations(VALUE self, VALUE declarations_string) {
968
+ Check_Type(declarations_string, T_STRING);
969
+
970
+ const char *input = RSTRING_PTR(declarations_string);
971
+ long input_len = RSTRING_LEN(declarations_string);
972
+
973
+ // Strip outer braces and whitespace (css_parser compatibility)
974
+ const char *start = input;
975
+ const char *end = input + input_len;
976
+
977
+ while (start < end && (IS_WHITESPACE(*start) || *start == '{')) start++;
978
+ while (end > start && (IS_WHITESPACE(*(end-1)) || *(end-1) == '}')) end--;
979
+
980
+ VALUE result = new_parse_declarations_string(start, end);
981
+
982
+ RB_GC_GUARD(result);
983
+ return result;
984
+ }
985
+
986
+ // ============================================================================
987
+ // Ruby Module Initialization
988
+ // ============================================================================
989
+
990
+ void Init_cataract(void) {
991
+ // Get Cataract module (should be defined by main extension)
992
+ VALUE mCataract = rb_define_module("Cataract");
993
+
994
+ // Define error classes (reuse from main extension if possible)
995
+ if (rb_const_defined(mCataract, rb_intern("Error"))) {
996
+ eCataractError = rb_const_get(mCataract, rb_intern("Error"));
997
+ } else {
998
+ eCataractError = rb_define_class_under(mCataract, "Error", rb_eStandardError);
999
+ }
1000
+
1001
+ if (rb_const_defined(mCataract, rb_intern("DepthError"))) {
1002
+ eDepthError = rb_const_get(mCataract, rb_intern("DepthError"));
1003
+ } else {
1004
+ eDepthError = rb_define_class_under(mCataract, "DepthError", eCataractError);
1005
+ }
1006
+
1007
+ if (rb_const_defined(mCataract, rb_intern("SizeError"))) {
1008
+ eSizeError = rb_const_get(mCataract, rb_intern("SizeError"));
1009
+ } else {
1010
+ eSizeError = rb_define_class_under(mCataract, "SizeError", eCataractError);
1011
+ }
1012
+
1013
+ // Define Rule struct: (id, selector, declarations, specificity, parent_rule_id, nesting_style)
1014
+ cRule = rb_struct_define_under(
1015
+ mCataract,
1016
+ "Rule",
1017
+ "id", // Integer (0-indexed position in @rules array)
1018
+ "selector", // String (fully resolved/flattened selector)
1019
+ "declarations", // Array of Declaration
1020
+ "specificity", // Integer (nil = not calculated yet)
1021
+ "parent_rule_id", // Integer | nil (parent rule ID for nested rules)
1022
+ "nesting_style", // Integer | nil (0=implicit, 1=explicit, nil=not nested)
1023
+ NULL
1024
+ );
1025
+
1026
+ // Define Declaration struct: (property, value, important)
1027
+ cDeclaration = rb_struct_define_under(
1028
+ mCataract,
1029
+ "Declaration",
1030
+ "property", // String
1031
+ "value", // String
1032
+ "important", // Boolean
1033
+ NULL
1034
+ );
1035
+
1036
+ // Define AtRule struct: (id, selector, content, specificity)
1037
+ // Matches Rule interface for duck-typing
1038
+ // - For @keyframes: content is Array of Rule (keyframe blocks)
1039
+ // - For @font-face: content is Array of Declaration
1040
+ cAtRule = rb_struct_define_under(
1041
+ mCataract,
1042
+ "AtRule",
1043
+ "id", // Integer (0-indexed position in @rules array)
1044
+ "selector", // String (e.g., "@keyframes fade", "@font-face")
1045
+ "content", // Array of Rule or Declaration
1046
+ "specificity", // Always nil for at-rules
1047
+ NULL
1048
+ );
1049
+
1050
+ // Define Declarations class and add to_s method
1051
+ VALUE cDeclarations = rb_define_class_under(mCataract, "Declarations", rb_cObject);
1052
+ rb_define_method(cDeclarations, "to_s", new_declarations_to_s_method, 0);
1053
+
1054
+ // Define Stylesheet class (Ruby will add instance methods like each_selector)
1055
+ cStylesheet = rb_define_class_under(mCataract, "Stylesheet", rb_cObject);
1056
+
1057
+ // Define module functions
1058
+ rb_define_module_function(mCataract, "_parse_css", parse_css_new, 1);
1059
+ rb_define_module_function(mCataract, "_stylesheet_to_s", stylesheet_to_s_new, 4);
1060
+ rb_define_module_function(mCataract, "_stylesheet_to_formatted_s", stylesheet_to_formatted_s_new, 4);
1061
+ rb_define_module_function(mCataract, "parse_media_types", parse_media_types, 1);
1062
+ rb_define_module_function(mCataract, "parse_declarations", new_parse_declarations, 1);
1063
+ rb_define_module_function(mCataract, "merge", cataract_merge_new, 1);
1064
+ rb_define_module_function(mCataract, "extract_imports", extract_imports, 1);
1065
+ rb_define_module_function(mCataract, "calculate_specificity", calculate_specificity, 1);
1066
+
1067
+ // Initialize merge constants (cached property strings)
1068
+ init_merge_constants();
1069
+
1070
+ // Export compile-time flags as a hash for runtime introspection
1071
+ VALUE compile_flags = rb_hash_new();
1072
+
1073
+ #ifdef CATARACT_DEBUG
1074
+ rb_hash_aset(compile_flags, ID2SYM(rb_intern("debug")), Qtrue);
1075
+ #else
1076
+ rb_hash_aset(compile_flags, ID2SYM(rb_intern("debug")), Qfalse);
1077
+ #endif
1078
+
1079
+ #ifdef DISABLE_STR_BUF_OPTIMIZATION
1080
+ rb_hash_aset(compile_flags, ID2SYM(rb_intern("str_buf_optimization")), Qfalse);
1081
+ #else
1082
+ rb_hash_aset(compile_flags, ID2SYM(rb_intern("str_buf_optimization")), Qtrue);
1083
+ #endif
1084
+
1085
+ rb_define_const(mCataract, "COMPILE_FLAGS", compile_flags);
1086
+ }