cataract 0.2.3 → 0.2.4

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.
@@ -8,11 +8,85 @@ VALUE cDeclaration;
8
8
  VALUE cAtRule;
9
9
  VALUE cStylesheet;
10
10
  VALUE cImportStatement;
11
+ VALUE cMediaQuery;
11
12
 
12
13
  // Error class definitions (shared with main extension)
13
14
  VALUE eCataractError;
14
15
  VALUE eDepthError;
15
16
  VALUE eSizeError;
17
+ VALUE eParserError;
18
+
19
+ // ============================================================================
20
+ // Helper Functions
21
+ // ============================================================================
22
+
23
+ /*
24
+ * Build media query text from MediaQuery struct
25
+ * Implements the logic from MediaQuery#text in Ruby
26
+ */
27
+ static void append_media_query_text(VALUE result, VALUE media_query) {
28
+ DEBUG_PRINTF("[APPEND_MQ] Called with media_query=%s (class: %s)\n",
29
+ RSTRING_PTR(rb_inspect(media_query)),
30
+ rb_obj_classname(media_query));
31
+ VALUE media_type = rb_struct_aref(media_query, INT2FIX(1)); // type field
32
+ VALUE media_conditions = rb_struct_aref(media_query, INT2FIX(2)); // conditions field
33
+
34
+ if (!NIL_P(media_conditions)) {
35
+ // Has conditions
36
+ ID all_id = rb_intern("all");
37
+ if (SYM2ID(media_type) == all_id) {
38
+ // Type is :all - just output conditions (don't say "all and ...")
39
+ rb_str_append(result, media_conditions);
40
+ } else {
41
+ // Output "type and conditions"
42
+ rb_str_append(result, rb_sym2str(media_type));
43
+ rb_str_cat2(result, " and ");
44
+ rb_str_append(result, media_conditions);
45
+ }
46
+ } else {
47
+ // No conditions - just output type
48
+ rb_str_append(result, rb_sym2str(media_type));
49
+ }
50
+ }
51
+
52
+ // Build media query string from MediaQuery object, handling comma-separated lists
53
+ // Matches pure Ruby's build_media_query_string method
54
+ // @param result [String] String to append to
55
+ // @param media_query_id [VALUE] The media query ID from the rule (Fixnum)
56
+ // @param mq_id_to_list_id [Hash] Reverse map: media_query_id => list_id
57
+ // @param media_query_lists [Hash] Hash mapping list_id => array of MediaQuery IDs
58
+ // @param media_queries [Array] Array of all MediaQuery objects
59
+ static void append_media_query_string(VALUE result, VALUE media_query_id, VALUE mq_id_to_list_id, VALUE media_query_lists, VALUE media_queries) {
60
+ // Check if this media_query_id is part of a comma-separated list
61
+ VALUE list_id = rb_hash_aref(mq_id_to_list_id, media_query_id);
62
+
63
+ if (!NIL_P(list_id)) {
64
+ // Part of a list - serialize all media queries in the list with commas
65
+ VALUE mq_ids = rb_hash_aref(media_query_lists, list_id);
66
+ if (!NIL_P(mq_ids) && TYPE(mq_ids) == T_ARRAY) {
67
+ long list_len = RARRAY_LEN(mq_ids);
68
+ for (long i = 0; i < list_len; i++) {
69
+ if (i > 0) {
70
+ rb_str_cat2(result, ", ");
71
+ }
72
+ VALUE mq_id = rb_ary_entry(mq_ids, i);
73
+ int mq_id_int = FIX2INT(mq_id);
74
+ VALUE mq = rb_ary_entry(media_queries, mq_id_int);
75
+ if (!NIL_P(mq)) {
76
+ append_media_query_text(result, mq);
77
+ }
78
+ }
79
+ }
80
+ } else {
81
+ // Single media query - just append it
82
+ int mq_id_int = FIX2INT(media_query_id);
83
+ VALUE mq = rb_ary_entry(media_queries, mq_id_int);
84
+ if (!NIL_P(mq)) {
85
+ append_media_query_text(result, mq);
86
+ }
87
+ }
88
+ // No GC guards needed - we don't extract pointers from VALUEs, just pass them to functions
89
+ }
16
90
 
17
91
  // ============================================================================
18
92
  // Stubbed Implementation - Phase 1
@@ -298,11 +372,27 @@ static void serialize_rule_formatted(VALUE result, VALUE rule, const char *inden
298
372
  }
299
373
  }
300
374
 
301
- // Context for building rule_to_media map
302
- struct build_rule_map_ctx {
303
- VALUE rule_to_media;
375
+ // Context for building mq_id_to_list_id reverse map
376
+ struct build_mq_reverse_map_ctx {
377
+ VALUE mq_id_to_list_id;
304
378
  };
305
379
 
380
+ // Callback to build reverse map: media_query_id => list_id
381
+ // Iterates through media_query_lists hash: list_id => [mq_id1, mq_id2, ...]
382
+ static int build_mq_reverse_map_callback(VALUE list_id, VALUE mq_ids, VALUE arg) {
383
+ struct build_mq_reverse_map_ctx *ctx = (struct build_mq_reverse_map_ctx *)arg;
384
+
385
+ if (!NIL_P(mq_ids) && TYPE(mq_ids) == T_ARRAY) {
386
+ long num_mq_ids = RARRAY_LEN(mq_ids);
387
+ for (long i = 0; i < num_mq_ids; i++) {
388
+ VALUE mq_id = rb_ary_entry(mq_ids, i);
389
+ rb_hash_aset(ctx->mq_id_to_list_id, mq_id, list_id);
390
+ }
391
+ }
392
+
393
+ return ST_CONTINUE;
394
+ }
395
+
306
396
  // Formatting options for stylesheet serialization
307
397
  // Avoids mode flags and if/else branches - all behavior controlled by struct values
308
398
  struct format_opts {
@@ -314,37 +404,12 @@ struct format_opts {
314
404
  int add_blank_lines; // 0 (compact) vs 1 (formatted)
315
405
  };
316
406
 
317
- // Callback to build reverse map from rule_id to media_sym
318
- static int build_rule_map_callback(VALUE media_sym, VALUE rule_ids, VALUE arg) {
319
- struct build_rule_map_ctx *ctx = (struct build_rule_map_ctx *)arg;
320
-
321
- Check_Type(rule_ids, T_ARRAY);
322
- long ids_len = RARRAY_LEN(rule_ids);
323
-
324
- for (long i = 0; i < ids_len; i++) {
325
- VALUE id = rb_ary_entry(rule_ids, i);
326
- VALUE existing = rb_hash_aref(ctx->rule_to_media, id);
327
-
328
- if (NIL_P(existing)) {
329
- rb_hash_aset(ctx->rule_to_media, id, media_sym);
330
- } else {
331
- // Keep the longer/more specific media query
332
- VALUE existing_str = rb_sym2str(existing);
333
- VALUE new_str = rb_sym2str(media_sym);
334
- if (RSTRING_LEN(new_str) > RSTRING_LEN(existing_str)) {
335
- rb_hash_aset(ctx->rule_to_media, id, media_sym);
336
- }
337
- }
338
- }
339
-
340
- return ST_CONTINUE;
341
- }
342
-
343
407
  // Private shared implementation for stylesheet serialization with optional selector list grouping
344
408
  // All formatting behavior controlled by format_opts struct to avoid mode flags and if/else branches
345
409
  static VALUE serialize_stylesheet_with_grouping(
346
410
  VALUE rules_array,
347
- VALUE media_index,
411
+ VALUE media_queries,
412
+ VALUE media_query_lists,
348
413
  VALUE result,
349
414
  VALUE selector_lists,
350
415
  const struct format_opts *opts
@@ -354,10 +419,13 @@ static VALUE serialize_stylesheet_with_grouping(
354
419
  // Check if selector list grouping is enabled (non-empty hash)
355
420
  int grouping_enabled = (!NIL_P(selector_lists) && TYPE(selector_lists) == T_HASH && RHASH_SIZE(selector_lists) > 0);
356
421
 
357
- // Build a map from rule_id to media query symbol using rb_hash_foreach
358
- VALUE rule_to_media = rb_hash_new();
359
- struct build_rule_map_ctx map_ctx = { rule_to_media };
360
- rb_hash_foreach(media_index, build_rule_map_callback, (VALUE)&map_ctx);
422
+ // Build reverse map: media_query_id => list_id
423
+ // This allows us to detect when multiple rules share a comma-separated media query list
424
+ VALUE mq_id_to_list_id = rb_hash_new();
425
+ if (!NIL_P(media_query_lists) && TYPE(media_query_lists) == T_HASH) {
426
+ struct build_mq_reverse_map_ctx ctx = { mq_id_to_list_id };
427
+ rb_hash_foreach(media_query_lists, build_mq_reverse_map_callback, (VALUE)&ctx);
428
+ }
361
429
 
362
430
  // Track processed rules to avoid duplicates when grouping
363
431
  VALUE processed_rule_ids = rb_hash_new();
@@ -375,7 +443,12 @@ static VALUE serialize_stylesheet_with_grouping(
375
443
  continue;
376
444
  }
377
445
 
378
- VALUE rule_media = rb_hash_aref(rule_to_media, rule_id);
446
+ // Get media_query_id and fetch MediaQuery object (nil for AtRule or rules without media query)
447
+ VALUE rule_media_query_id = rb_obj_is_kind_of(rule, cAtRule) ? Qnil : rb_struct_aref(rule, INT2FIX(RULE_MEDIA_QUERY_ID));
448
+ VALUE rule_media = Qnil;
449
+ if (!NIL_P(rule_media_query_id)) {
450
+ rule_media = rb_ary_entry(media_queries, FIX2INT(rule_media_query_id));
451
+ }
379
452
  int is_first_rule = (i == 0);
380
453
 
381
454
  if (NIL_P(rule_media)) {
@@ -425,9 +498,9 @@ static VALUE serialize_stylesheet_with_grouping(
425
498
  VALUE other_rule = rb_ary_entry(rules_array, FIX2INT(other_rule_id));
426
499
  if (NIL_P(other_rule)) continue;
427
500
 
428
- // Check same media context (both should be nil for base rules)
429
- VALUE other_rule_media = rb_hash_aref(rule_to_media, other_rule_id);
430
- if (!rb_equal(rule_media, other_rule_media)) {
501
+ // Check same media context (compare media_query_id directly)
502
+ VALUE other_rule_media_query_id = rb_struct_aref(other_rule, INT2FIX(RULE_MEDIA_QUERY_ID));
503
+ if (!rb_equal(rule_media_query_id, other_rule_media_query_id)) {
431
504
  continue;
432
505
  }
433
506
 
@@ -482,7 +555,7 @@ static VALUE serialize_stylesheet_with_grouping(
482
555
  }
483
556
  } else {
484
557
  // This rule is in a media query
485
- // Check if media query changed from previous rule
558
+ // Check if media query changed from previous rule (compare MediaQuery objects by value)
486
559
  if (NIL_P(current_media) || !rb_equal(current_media, rule_media)) {
487
560
  // Close previous media block if open
488
561
  if (in_media_block) {
@@ -494,10 +567,15 @@ static VALUE serialize_stylesheet_with_grouping(
494
567
  rb_str_cat2(result, "\n");
495
568
  }
496
569
 
497
- // Open new media block
570
+ // Open new media block - store MediaQuery object for comparison
498
571
  current_media = rule_media;
499
572
  rb_str_cat2(result, "@media ");
500
- rb_str_append(result, rb_sym2str(rule_media));
573
+
574
+ // Serialize the media query (handles comma-separated lists)
575
+ if (!NIL_P(rule_media_query_id)) {
576
+ append_media_query_string(result, rule_media_query_id, mq_id_to_list_id, media_query_lists, media_queries);
577
+ }
578
+
501
579
  rb_str_cat2(result, " {\n");
502
580
  in_media_block = 1;
503
581
  }
@@ -528,8 +606,8 @@ static VALUE serialize_stylesheet_with_grouping(
528
606
  VALUE other_rule = rb_ary_entry(rules_array, FIX2INT(other_rule_id));
529
607
  if (NIL_P(other_rule)) continue;
530
608
 
531
- VALUE other_rule_media = rb_hash_aref(rule_to_media, other_rule_id);
532
- if (!rb_equal(rule_media, other_rule_media)) continue;
609
+ VALUE other_rule_media_query_id = rb_struct_aref(other_rule, INT2FIX(RULE_MEDIA_QUERY_ID));
610
+ if (!rb_equal(rule_media_query_id, other_rule_media_query_id)) continue;
533
611
 
534
612
  VALUE other_declarations = rb_struct_aref(other_rule, INT2FIX(RULE_DECLARATIONS));
535
613
  if (rb_equal(rule_declarations, other_declarations)) {
@@ -584,15 +662,16 @@ static VALUE serialize_stylesheet_with_grouping(
584
662
  rb_str_cat2(result, "}\n");
585
663
  }
586
664
 
587
- RB_GC_GUARD(rule_to_media);
665
+ // Guard hash objects we created and used throughout
666
+ RB_GC_GUARD(mq_id_to_list_id);
588
667
  RB_GC_GUARD(processed_rule_ids);
589
668
  return result;
590
669
  }
591
670
 
592
671
  // Original stylesheet serialization (no nesting support) - compact format
593
- static VALUE stylesheet_to_s_original(VALUE rules_array, VALUE media_index, VALUE charset, VALUE selector_lists) {
672
+ static VALUE stylesheet_to_s_without_nesting(VALUE rules_array, VALUE media_queries, VALUE media_query_lists, VALUE charset, VALUE selector_lists) {
594
673
  Check_Type(rules_array, T_ARRAY);
595
- Check_Type(media_index, T_HASH);
674
+ Check_Type(media_queries, T_ARRAY);
596
675
 
597
676
  VALUE result = rb_str_new_cstr("");
598
677
 
@@ -613,24 +692,24 @@ static VALUE stylesheet_to_s_original(VALUE rules_array, VALUE media_index, VALU
613
692
  .add_blank_lines = 0
614
693
  };
615
694
 
616
- return serialize_stylesheet_with_grouping(rules_array, media_index, result, selector_lists, &opts);
695
+ return serialize_stylesheet_with_grouping(rules_array, media_queries, media_query_lists, result, selector_lists, &opts);
617
696
  }
618
697
 
619
698
  // Forward declarations
620
699
  static void serialize_children_only(VALUE result, VALUE rules_array, long rule_idx,
621
- VALUE rule_to_media, VALUE parent_to_children, VALUE parent_selector,
622
- VALUE parent_declarations, int formatted, int indent_level);
700
+ VALUE parent_to_children, VALUE parent_selector,
701
+ VALUE parent_declarations, VALUE media_queries, int formatted, int indent_level);
623
702
  static void serialize_rule_with_children(VALUE result, VALUE rules_array, long rule_idx,
624
- VALUE rule_to_media, VALUE parent_to_children,
703
+ VALUE parent_to_children, VALUE media_queries,
625
704
  int formatted, int indent_level);
626
705
 
627
706
  // Helper: Only serialize children of a rule (not the rule itself)
628
707
  static void serialize_children_only(VALUE result, VALUE rules_array, long rule_idx,
629
- VALUE rule_to_media, VALUE parent_to_children, VALUE parent_selector,
630
- VALUE parent_declarations, int formatted, int indent_level) {
708
+ VALUE parent_to_children, VALUE parent_selector,
709
+ VALUE parent_declarations, VALUE media_queries, int formatted, int indent_level) {
631
710
  VALUE rule = rb_ary_entry(rules_array, rule_idx);
632
711
  VALUE rule_id = rb_struct_aref(rule, INT2FIX(RULE_ID));
633
- VALUE rule_media = rb_hash_aref(rule_to_media, rule_id); // Look up by rule ID, not array index
712
+ VALUE rule_media_query_id = rb_struct_aref(rule, INT2FIX(RULE_MEDIA_QUERY_ID));
634
713
  int parent_has_declarations = !NIL_P(parent_declarations) && RARRAY_LEN(parent_declarations) > 0;
635
714
 
636
715
  // Build indentation string for this level (only if formatted)
@@ -657,15 +736,14 @@ static void serialize_children_only(VALUE result, VALUE rules_array, long rule_i
657
736
  for (long i = 0; i < num_children; i++) {
658
737
  long child_idx = FIX2LONG(rb_ary_entry(children_indices, i));
659
738
  VALUE child = rb_ary_entry(rules_array, child_idx);
660
- VALUE child_id = rb_struct_aref(child, INT2FIX(RULE_ID));
661
- VALUE child_media = rb_hash_aref(rule_to_media, child_id); // Look up by rule ID
739
+ VALUE child_media_query_id = rb_struct_aref(child, INT2FIX(RULE_MEDIA_QUERY_ID));
662
740
 
663
- DEBUG_PRINTF("[SERIALIZE] Child %ld: child_media=%s, rule_media=%s\n", child_idx,
664
- NIL_P(child_media) ? "nil" : RSTRING_PTR(rb_inspect(child_media)),
665
- NIL_P(rule_media) ? "nil" : RSTRING_PTR(rb_inspect(rule_media)));
741
+ DEBUG_PRINTF("[SERIALIZE] Child %ld: child_media_query_id=%s, rule_media_query_id=%s\n", child_idx,
742
+ NIL_P(child_media_query_id) ? "nil" : RSTRING_PTR(rb_inspect(child_media_query_id)),
743
+ NIL_P(rule_media_query_id) ? "nil" : RSTRING_PTR(rb_inspect(rule_media_query_id)));
666
744
 
667
745
  // Only serialize selector-nested children here (not @media nested)
668
- if (NIL_P(child_media) || rb_equal(child_media, rule_media)) {
746
+ if (NIL_P(child_media_query_id) || rb_equal(child_media_query_id, rule_media_query_id)) {
669
747
  DEBUG_PRINTF("[SERIALIZE] -> Serializing as selector-nested child\n");
670
748
  VALUE child_selector = rb_struct_aref(child, INT2FIX(RULE_SELECTOR));
671
749
  VALUE child_nesting_style = rb_struct_aref(child, INT2FIX(RULE_NESTING_STYLE));
@@ -693,8 +771,8 @@ static void serialize_children_only(VALUE result, VALUE rules_array, long rule_i
693
771
  }
694
772
 
695
773
  // Recursively serialize grandchildren
696
- serialize_children_only(result, rules_array, child_idx, rule_to_media, parent_to_children,
697
- child_selector, child_declarations, formatted, indent_level + 1);
774
+ serialize_children_only(result, rules_array, child_idx, parent_to_children,
775
+ child_selector, child_declarations, media_queries, formatted, indent_level + 1);
698
776
 
699
777
  // Closing brace with indentation and newline
700
778
  rb_str_append(result, indent_str);
@@ -712,8 +790,8 @@ static void serialize_children_only(VALUE result, VALUE rules_array, long rule_i
712
790
  serialize_declarations(result, child_declarations);
713
791
 
714
792
  // Recursively serialize grandchildren
715
- serialize_children_only(result, rules_array, child_idx, rule_to_media, parent_to_children,
716
- child_selector, child_declarations, formatted, indent_level);
793
+ serialize_children_only(result, rules_array, child_idx, parent_to_children,
794
+ child_selector, child_declarations, media_queries, formatted, indent_level);
717
795
 
718
796
  rb_str_cat2(result, " }");
719
797
  }
@@ -724,16 +802,17 @@ static void serialize_children_only(VALUE result, VALUE rules_array, long rule_i
724
802
  for (long i = 0; i < num_children; i++) {
725
803
  long child_idx = FIX2LONG(rb_ary_entry(children_indices, i));
726
804
  VALUE child = rb_ary_entry(rules_array, child_idx);
727
- VALUE child_id = rb_struct_aref(child, INT2FIX(RULE_ID));
728
- VALUE child_media = rb_hash_aref(rule_to_media, child_id); // Look up by rule ID
805
+ VALUE child_media_query_id = rb_struct_aref(child, INT2FIX(RULE_MEDIA_QUERY_ID));
729
806
 
730
807
  // Check if this is a different media than parent
731
- if (!NIL_P(child_media) && !rb_equal(rule_media, child_media)) {
732
- // Nested @media!
808
+ if (!NIL_P(child_media_query_id) && !rb_equal(rule_media_query_id, child_media_query_id)) {
809
+ // Nested @media! Get the MediaQuery object for serialization
810
+ VALUE child_media = rb_ary_entry(media_queries, FIX2INT(child_media_query_id));
811
+
733
812
  if (formatted) {
734
813
  rb_str_append(result, indent_str);
735
814
  rb_str_cat2(result, "@media ");
736
- rb_str_append(result, rb_sym2str(child_media));
815
+ append_media_query_text(result, child_media);
737
816
  rb_str_cat2(result, " {\n");
738
817
 
739
818
  VALUE child_declarations = rb_struct_aref(child, INT2FIX(RULE_DECLARATIONS));
@@ -752,7 +831,7 @@ static void serialize_children_only(VALUE result, VALUE rules_array, long rule_i
752
831
  rb_str_cat2(result, "}\n");
753
832
  } else {
754
833
  rb_str_cat2(result, " @media ");
755
- rb_str_append(result, rb_sym2str(child_media));
834
+ append_media_query_text(result, child_media);
756
835
  rb_str_cat2(result, " { ");
757
836
 
758
837
  VALUE child_declarations = rb_struct_aref(child, INT2FIX(RULE_DECLARATIONS));
@@ -767,7 +846,7 @@ static void serialize_children_only(VALUE result, VALUE rules_array, long rule_i
767
846
 
768
847
  // Recursive serializer for a rule and its nested children
769
848
  static void serialize_rule_with_children(VALUE result, VALUE rules_array, long rule_idx,
770
- VALUE rule_to_media, VALUE parent_to_children,
849
+ VALUE parent_to_children, VALUE media_queries,
771
850
  int formatted, int indent_level) {
772
851
  VALUE rule = rb_ary_entry(rules_array, rule_idx);
773
852
  VALUE selector = rb_struct_aref(rule, INT2FIX(RULE_SELECTOR));
@@ -801,8 +880,8 @@ static void serialize_rule_with_children(VALUE result, VALUE rules_array, long r
801
880
  }
802
881
 
803
882
  // Serialize nested children
804
- serialize_children_only(result, rules_array, rule_idx, rule_to_media, parent_to_children,
805
- selector, declarations, formatted, indent_level + 1);
883
+ serialize_children_only(result, rules_array, rule_idx, parent_to_children,
884
+ selector, declarations, media_queries, formatted, indent_level + 1);
806
885
 
807
886
  rb_str_cat2(result, closing_indent);
808
887
  rb_str_cat2(result, "}\n");
@@ -815,8 +894,8 @@ static void serialize_rule_with_children(VALUE result, VALUE rules_array, long r
815
894
  serialize_declarations(result, declarations);
816
895
 
817
896
  // Serialize nested children
818
- serialize_children_only(result, rules_array, rule_idx, rule_to_media, parent_to_children,
819
- selector, declarations, formatted, indent_level);
897
+ serialize_children_only(result, rules_array, rule_idx, parent_to_children,
898
+ selector, declarations, media_queries, formatted, indent_level);
820
899
 
821
900
  rb_str_cat2(result, " }\n");
822
901
  }
@@ -826,17 +905,32 @@ static void serialize_rule_with_children(VALUE result, VALUE rules_array, long r
826
905
  }
827
906
 
828
907
  // New stylesheet serialization entry point - checks for nesting and delegates
829
- static VALUE stylesheet_to_s_new(VALUE self, VALUE rules_array, VALUE media_index, VALUE charset, VALUE has_nesting, VALUE selector_lists) {
908
+ static VALUE stylesheet_to_s(VALUE self, VALUE rules_array, VALUE charset, VALUE has_nesting, VALUE selector_lists, VALUE media_queries, VALUE media_query_lists) {
909
+ DEBUG_PRINTF("[STYLESHEET_TO_S] Called with:\n");
910
+ DEBUG_PRINTF(" rules_array length: %ld\n", RARRAY_LEN(rules_array));
911
+ DEBUG_PRINTF(" media_queries type: %s, length: %ld\n",
912
+ rb_obj_classname(media_queries),
913
+ TYPE(media_queries) == T_ARRAY ? RARRAY_LEN(media_queries) : -1);
914
+ DEBUG_PRINTF(" media_queries inspect: %s\n", RSTRING_PTR(rb_inspect(media_queries)));
915
+ DEBUG_PRINTF(" media_query_lists class: %s\n", rb_obj_classname(media_query_lists));
916
+ DEBUG_PRINTF(" selector_lists class: %s\n", rb_obj_classname(selector_lists));
917
+
918
+ DEBUG_PRINTF("[STYLESHEET_TO_S] About to Check_Type\n");
830
919
  Check_Type(rules_array, T_ARRAY);
831
- Check_Type(media_index, T_HASH);
920
+ Check_Type(media_queries, T_ARRAY);
921
+ if (!NIL_P(media_query_lists)) Check_Type(media_query_lists, T_HASH);
922
+ if (!NIL_P(selector_lists)) Check_Type(selector_lists, T_HASH);
923
+ DEBUG_PRINTF("[STYLESHEET_TO_S] Check_Type passed\n");
832
924
  // TODO: Phase 2 - use selector_lists for grouping
833
925
  (void)selector_lists; // Suppress unused parameter warning
834
926
 
835
927
  // Fast path: if no nesting, use original implementation (zero overhead)
836
928
  if (!RTEST(has_nesting)) {
837
- return stylesheet_to_s_original(rules_array, media_index, charset, selector_lists);
929
+ DEBUG_PRINTF("[STYLESHEET_TO_S] Taking fast path (no nesting)\n");
930
+ return stylesheet_to_s_without_nesting(rules_array, media_queries, media_query_lists, charset, selector_lists);
838
931
  }
839
932
 
933
+ DEBUG_PRINTF("[STYLESHEET_TO_S] Taking slow path (has nesting)\n");
840
934
  // SLOW PATH: Has nesting - use lookahead approach
841
935
  long total_rules = RARRAY_LEN(rules_array);
842
936
  VALUE result = rb_str_new_cstr("");
@@ -848,11 +942,6 @@ static VALUE stylesheet_to_s_new(VALUE self, VALUE rules_array, VALUE media_inde
848
942
  rb_str_cat2(result, "\";\n");
849
943
  }
850
944
 
851
- // Build rule_to_media map
852
- VALUE rule_to_media = rb_hash_new();
853
- struct build_rule_map_ctx map_ctx = { rule_to_media };
854
- rb_hash_foreach(media_index, build_rule_map_callback, (VALUE)&map_ctx);
855
-
856
945
  // Build parent_to_children map (parent_rule_id -> array of child indices)
857
946
  // This allows O(1) lookup of children when serializing each parent
858
947
  VALUE parent_to_children = rb_hash_new();
@@ -896,9 +985,15 @@ static VALUE stylesheet_to_s_new(VALUE self, VALUE rules_array, VALUE media_inde
896
985
  continue;
897
986
  }
898
987
 
899
- // Get media for this rule
900
- VALUE rule_id = rb_struct_aref(rule, INT2FIX(RULE_ID));
901
- VALUE rule_media = rb_hash_aref(rule_to_media, rule_id);
988
+ // Get media_query_id for this rule and fetch the MediaQuery object
989
+ VALUE rule_media_query_id = rb_obj_is_kind_of(rule, cAtRule) ? Qnil : rb_struct_aref(rule, INT2FIX(RULE_MEDIA_QUERY_ID));
990
+ VALUE rule_media = Qnil;
991
+ if (!NIL_P(rule_media_query_id)) {
992
+ rule_media = rb_ary_entry(media_queries, FIX2INT(rule_media_query_id));
993
+ }
994
+ DEBUG_PRINTF("[SERIALIZE] rule_media_query_id=%s, rule_media=%s\n",
995
+ NIL_P(rule_media_query_id) ? "nil" : RSTRING_PTR(rb_inspect(rule_media_query_id)),
996
+ NIL_P(rule_media) ? "nil" : RSTRING_PTR(rb_inspect(rule_media)));
902
997
 
903
998
  // Handle media block transitions
904
999
  if (NIL_P(rule_media)) {
@@ -909,16 +1004,16 @@ static VALUE stylesheet_to_s_new(VALUE self, VALUE rules_array, VALUE media_inde
909
1004
  current_media = Qnil;
910
1005
  }
911
1006
  } else {
912
- // In media - check if we need to open/change block
1007
+ // In media - check if we need to open/change block (compare MediaQuery objects by value)
913
1008
  if (NIL_P(current_media) || !rb_equal(current_media, rule_media)) {
914
1009
  // Close previous media block if open
915
1010
  if (in_media_block) {
916
1011
  rb_str_cat2(result, "}\n");
917
1012
  }
918
- // Open new media block
1013
+ // Open new media block - store the MediaQuery object for comparison
919
1014
  current_media = rule_media;
920
1015
  rb_str_cat2(result, "@media ");
921
- rb_str_append(result, rb_sym2str(rule_media));
1016
+ append_media_query_text(result, rule_media);
922
1017
  rb_str_cat2(result, " {\n");
923
1018
  in_media_block = 1;
924
1019
  }
@@ -932,7 +1027,7 @@ static VALUE stylesheet_to_s_new(VALUE self, VALUE rules_array, VALUE media_inde
932
1027
 
933
1028
  // Serialize rule with nested children
934
1029
  serialize_rule_with_children(
935
- result, rules_array, i, rule_to_media, parent_to_children,
1030
+ result, rules_array, i, parent_to_children, media_queries,
936
1031
  0, // formatted (compact)
937
1032
  0 // indent_level (top-level)
938
1033
  );
@@ -943,13 +1038,15 @@ static VALUE stylesheet_to_s_new(VALUE self, VALUE rules_array, VALUE media_inde
943
1038
  rb_str_cat2(result, "}\n");
944
1039
  }
945
1040
 
946
- RB_GC_GUARD(rule_to_media);
947
1041
  RB_GC_GUARD(parent_to_children);
948
1042
  return result;
949
1043
  }
950
1044
 
951
1045
  // Original formatted serialization (no nesting support)
952
- static VALUE stylesheet_to_formatted_s_original(VALUE rules_array, VALUE media_index, VALUE charset, VALUE selector_lists) {
1046
+ static VALUE stylesheet_to_formatted_s_without_nesting(VALUE rules_array, VALUE media_queries, VALUE media_query_lists, VALUE charset, VALUE selector_lists) {
1047
+ Check_Type(rules_array, T_ARRAY);
1048
+ Check_Type(media_queries, T_ARRAY);
1049
+
953
1050
  VALUE result = rb_str_new_cstr("");
954
1051
 
955
1052
  // Add charset if present
@@ -969,17 +1066,19 @@ static VALUE stylesheet_to_formatted_s_original(VALUE rules_array, VALUE media_i
969
1066
  .add_blank_lines = 1
970
1067
  };
971
1068
 
972
- return serialize_stylesheet_with_grouping(rules_array, media_index, result, selector_lists, &opts);
1069
+ return serialize_stylesheet_with_grouping(rules_array, media_queries, media_query_lists, result, selector_lists, &opts);
973
1070
  }
974
1071
 
975
1072
  // Formatted version with indentation and newlines (with nesting support)
976
- static VALUE stylesheet_to_formatted_s_new(VALUE self, VALUE rules_array, VALUE media_index, VALUE charset, VALUE has_nesting, VALUE selector_lists) {
1073
+ static VALUE stylesheet_to_formatted_s(VALUE self, VALUE rules_array, VALUE charset, VALUE has_nesting, VALUE selector_lists, VALUE media_queries, VALUE media_query_lists) {
977
1074
  Check_Type(rules_array, T_ARRAY);
978
- Check_Type(media_index, T_HASH);
1075
+ Check_Type(media_queries, T_ARRAY);
1076
+ if (!NIL_P(media_query_lists)) Check_Type(media_query_lists, T_HASH);
1077
+ if (!NIL_P(selector_lists)) Check_Type(selector_lists, T_HASH);
979
1078
 
980
1079
  // Fast path: if no nesting, use original implementation (zero overhead)
981
1080
  if (!RTEST(has_nesting)) {
982
- return stylesheet_to_formatted_s_original(rules_array, media_index, charset, selector_lists);
1081
+ return stylesheet_to_formatted_s_without_nesting(rules_array, media_queries, media_query_lists, charset, selector_lists);
983
1082
  }
984
1083
 
985
1084
  // SLOW PATH: Has nesting - use parameterized serialization with formatted=1
@@ -993,11 +1092,6 @@ static VALUE stylesheet_to_formatted_s_new(VALUE self, VALUE rules_array, VALUE
993
1092
  rb_str_cat2(result, "\";\n");
994
1093
  }
995
1094
 
996
- // Build rule_to_media map
997
- VALUE rule_to_media = rb_hash_new();
998
- struct build_rule_map_ctx map_ctx = { rule_to_media };
999
- rb_hash_foreach(media_index, build_rule_map_callback, (VALUE)&map_ctx);
1000
-
1001
1095
  // Build parent_to_children map (parent_rule_id -> array of child indices)
1002
1096
  VALUE parent_to_children = rb_hash_new();
1003
1097
  for (long i = 0; i < total_rules; i++) {
@@ -1028,9 +1122,12 @@ static VALUE stylesheet_to_formatted_s_new(VALUE self, VALUE rules_array, VALUE
1028
1122
  continue;
1029
1123
  }
1030
1124
 
1031
- // Get media for this rule
1032
- VALUE rule_id = rb_struct_aref(rule, INT2FIX(RULE_ID));
1033
- VALUE rule_media = rb_hash_aref(rule_to_media, rule_id);
1125
+ // Get media_query_id for this rule and fetch the MediaQuery object
1126
+ VALUE rule_media_query_id = rb_obj_is_kind_of(rule, cAtRule) ? Qnil : rb_struct_aref(rule, INT2FIX(RULE_MEDIA_QUERY_ID));
1127
+ VALUE rule_media = Qnil;
1128
+ if (!NIL_P(rule_media_query_id)) {
1129
+ rule_media = rb_ary_entry(media_queries, FIX2INT(rule_media_query_id));
1130
+ }
1034
1131
 
1035
1132
  // Handle media block transitions
1036
1133
  if (NIL_P(rule_media)) {
@@ -1044,7 +1141,7 @@ static VALUE stylesheet_to_formatted_s_new(VALUE self, VALUE rules_array, VALUE
1044
1141
  rb_str_cat2(result, "\n");
1045
1142
  }
1046
1143
  } else {
1047
- // In media - check if we need to open/change block
1144
+ // In media - check if we need to open/change block (compare MediaQuery objects by value)
1048
1145
  if (NIL_P(current_media) || !rb_equal(current_media, rule_media)) {
1049
1146
  // Close previous media block if open
1050
1147
  if (in_media_block) {
@@ -1053,10 +1150,10 @@ static VALUE stylesheet_to_formatted_s_new(VALUE self, VALUE rules_array, VALUE
1053
1150
  // Add blank line before new media block (except at start)
1054
1151
  rb_str_cat2(result, "\n");
1055
1152
  }
1056
- // Open new media block
1153
+ // Open new media block - store the MediaQuery object for comparison
1057
1154
  current_media = rule_media;
1058
1155
  rb_str_cat2(result, "@media ");
1059
- rb_str_append(result, rb_sym2str(rule_media));
1156
+ append_media_query_text(result, rule_media);
1060
1157
  rb_str_cat2(result, " {\n");
1061
1158
  in_media_block = 1;
1062
1159
  }
@@ -1077,7 +1174,7 @@ static VALUE stylesheet_to_formatted_s_new(VALUE self, VALUE rules_array, VALUE
1077
1174
  // Serialize rule with nested children
1078
1175
  DEBUG_PRINTF("[FORMATTED] Calling serialize_rule_with_children, in_media_block=%d\n", in_media_block);
1079
1176
  serialize_rule_with_children(
1080
- result, rules_array, i, rule_to_media, parent_to_children,
1177
+ result, rules_array, i, parent_to_children, media_queries,
1081
1178
  1, // formatted (with indentation)
1082
1179
  in_media_block ? 1 : 0 // indent_level (1 if inside media block, 0 otherwise)
1083
1180
  );
@@ -1088,7 +1185,6 @@ static VALUE stylesheet_to_formatted_s_new(VALUE self, VALUE rules_array, VALUE
1088
1185
  rb_str_cat2(result, "}\n");
1089
1186
  }
1090
1187
 
1091
- RB_GC_GUARD(rule_to_media);
1092
1188
  RB_GC_GUARD(parent_to_children);
1093
1189
  return result;
1094
1190
  }
@@ -1312,6 +1408,12 @@ void Init_native_extension(void) {
1312
1408
  eSizeError = rb_define_class_under(mCataract, "SizeError", eCataractError);
1313
1409
  }
1314
1410
 
1411
+ if (rb_const_defined(mCataract, rb_intern("ParserError"))) {
1412
+ eParserError = rb_const_get(mCataract, rb_intern("ParserError"));
1413
+ } else {
1414
+ eParserError = rb_define_class_under(mCataract, "ParserError", eCataractError);
1415
+ }
1416
+
1315
1417
  // Reuse Ruby-defined structs (they must be defined before loading this extension)
1316
1418
  // If they don't exist, someone required the extension directly instead of via lib/cataract.rb
1317
1419
  if (rb_const_defined(mCataract, rb_intern("Rule"))) {
@@ -1338,6 +1440,12 @@ void Init_native_extension(void) {
1338
1440
  rb_raise(rb_eLoadError, "Cataract::ImportStatement not defined. Do not require 'cataract/native_extension' directly, use require 'cataract'");
1339
1441
  }
1340
1442
 
1443
+ if (rb_const_defined(mCataract, rb_intern("MediaQuery"))) {
1444
+ cMediaQuery = rb_const_get(mCataract, rb_intern("MediaQuery"));
1445
+ } else {
1446
+ rb_raise(rb_eLoadError, "Cataract::MediaQuery not defined. Do not require 'cataract/native_extension' directly, use require 'cataract'");
1447
+ }
1448
+
1341
1449
  // Define Declarations class and add to_s method
1342
1450
  VALUE cDeclarations = rb_define_class_under(mCataract, "Declarations", rb_cObject);
1343
1451
  rb_define_method(cDeclarations, "to_s", new_declarations_to_s_method, 0);
@@ -1347,15 +1455,14 @@ void Init_native_extension(void) {
1347
1455
 
1348
1456
  // Define module functions
1349
1457
  rb_define_module_function(mCataract, "_parse_css", parse_css_new, -1);
1350
- rb_define_module_function(mCataract, "_stylesheet_to_s", stylesheet_to_s_new, 5);
1351
- rb_define_module_function(mCataract, "_stylesheet_to_formatted_s", stylesheet_to_formatted_s_new, 5);
1458
+ rb_define_module_function(mCataract, "stylesheet_to_s", stylesheet_to_s, 6);
1459
+ rb_define_module_function(mCataract, "stylesheet_to_formatted_s", stylesheet_to_formatted_s, 6);
1352
1460
  rb_define_module_function(mCataract, "parse_media_types", parse_media_types, 1);
1353
1461
  rb_define_module_function(mCataract, "parse_declarations", new_parse_declarations, 1);
1354
1462
  rb_define_module_function(mCataract, "flatten", cataract_flatten, 1);
1355
1463
  rb_define_module_function(mCataract, "merge", cataract_flatten, 1); // Deprecated alias for backwards compatibility
1356
- rb_define_module_function(mCataract, "extract_imports", extract_imports, 1);
1357
1464
  rb_define_module_function(mCataract, "calculate_specificity", calculate_specificity, 1);
1358
- rb_define_module_function(mCataract, "_expand_shorthand", cataract_expand_shorthand, 1);
1465
+ rb_define_module_function(mCataract, "expand_shorthand", cataract_expand_shorthand, 1);
1359
1466
 
1360
1467
  // Initialize flatten constants (cached property strings)
1361
1468
  init_flatten_constants();