cataract 0.3.0 → 0.5.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.
@@ -9,6 +9,7 @@ VALUE cAtRule;
9
9
  VALUE cStylesheet;
10
10
  VALUE cImportStatement;
11
11
  VALUE cMediaQuery;
12
+ VALUE cConditionalGroup;
12
13
 
13
14
  // Error class definitions (shared with main extension)
14
15
  VALUE eCataractError;
@@ -168,6 +169,13 @@ static void serialize_at_rule(VALUE result, VALUE at_rule) {
168
169
  VALUE selector = rb_struct_aref(at_rule, INT2FIX(AT_RULE_SELECTOR));
169
170
  VALUE content = rb_struct_aref(at_rule, INT2FIX(AT_RULE_CONTENT));
170
171
 
172
+ if (NIL_P(content)) {
173
+ // Statement form (e.g. `@layer a, b;`) - no block to wrap.
174
+ rb_str_append(result, selector);
175
+ rb_str_cat2(result, ";\n");
176
+ return;
177
+ }
178
+
171
179
  rb_str_append(result, selector);
172
180
  rb_str_cat2(result, " {\n");
173
181
 
@@ -277,6 +285,14 @@ static void serialize_at_rule_formatted(VALUE result, VALUE at_rule, const char
277
285
  VALUE selector = rb_struct_aref(at_rule, INT2FIX(AT_RULE_SELECTOR));
278
286
  VALUE content = rb_struct_aref(at_rule, INT2FIX(AT_RULE_CONTENT));
279
287
 
288
+ if (NIL_P(content)) {
289
+ // Statement form (e.g. `@layer a, b;`) - no block to wrap.
290
+ rb_str_cat2(result, indent);
291
+ rb_str_append(result, selector);
292
+ rb_str_cat2(result, ";\n");
293
+ return;
294
+ }
295
+
280
296
  rb_str_cat2(result, indent);
281
297
  rb_str_append(result, selector);
282
298
  rb_str_cat2(result, " {\n");
@@ -377,6 +393,108 @@ static int build_mq_reverse_map_callback(VALUE list_id, VALUE mq_ids, VALUE arg)
377
393
  return ST_CONTINUE;
378
394
  }
379
395
 
396
+ // Build the media_query_id => list_id reverse map used by both stylesheet
397
+ // serializers (grouping and nesting) to detect when a rule's media query is
398
+ // part of a comma-separated list (e.g. "@media screen, print").
399
+ static VALUE build_mq_id_to_list_id(VALUE media_query_lists) {
400
+ VALUE mq_id_to_list_id = rb_hash_new();
401
+ if (!NIL_P(media_query_lists) && TYPE(media_query_lists) == T_HASH) {
402
+ struct build_mq_reverse_map_ctx ctx = { mq_id_to_list_id };
403
+ rb_hash_foreach(media_query_lists, build_mq_reverse_map_callback, (VALUE)&ctx);
404
+ }
405
+ return mq_id_to_list_id;
406
+ }
407
+
408
+ // Build a rule_id => rule lookup. rules_array is NOT guaranteed to satisfy
409
+ // rules[i].id == i here - that invariant only holds for the full,
410
+ // freshly-parsed rules array; Stylesheet#to_s(media: ...) passes a filtered
411
+ // subset whenever the media filter isn't :all, so a plain array index would
412
+ // silently fetch the wrong rule (or even an AtRule, which doesn't share
413
+ // Rule's struct layout) once any rule has been filtered out. Both Rule and
414
+ // AtRule keep id at field index 0, so a single RULE_ID read is safe for
415
+ // either struct type.
416
+ static VALUE build_rules_by_id(VALUE rules_array) {
417
+ VALUE rules_by_id = rb_hash_new();
418
+ long len = RARRAY_LEN(rules_array);
419
+ for (long i = 0; i < len; i++) {
420
+ VALUE rule = rb_ary_entry(rules_array, i);
421
+ VALUE rule_id = rb_struct_aref(rule, INT2FIX(RULE_ID));
422
+ rb_hash_aset(rules_by_id, rule_id, rule);
423
+ }
424
+ return rules_by_id;
425
+ }
426
+
427
+ // A rule's conditional-group wrapping chain, outermost first, found by
428
+ // walking parent_id from its own conditional_group_id up to the root. Only
429
+ // @supports currently populates conditional_group_id (see the parser), but
430
+ // this walks whatever's there generically.
431
+ static VALUE conditional_group_chain(VALUE conditional_groups, VALUE conditional_group_id) {
432
+ VALUE chain = rb_ary_new();
433
+ VALUE id = conditional_group_id;
434
+ while (!NIL_P(id)) {
435
+ VALUE group = rb_ary_entry(conditional_groups, FIX2INT(id));
436
+ if (NIL_P(group)) break;
437
+
438
+ rb_ary_unshift(chain, group);
439
+ id = rb_struct_aref(group, INT2FIX(CONDITIONAL_GROUP_PARENT_ID));
440
+ }
441
+ return chain;
442
+ }
443
+
444
+ // Reconcile the currently-open conditional-group blocks with the ones a
445
+ // rule needs, like diffing two paths down a tree: close whatever's open
446
+ // past where the chains diverge (innermost first), then open whatever the
447
+ // target chain adds from that point on. Nested entirely inside whatever
448
+ // media block is currently open (conditional groups don't affect media
449
+ // context). Returns target_chain, for the caller to track as current.
450
+ static VALUE sync_conditional_group_chain(VALUE result, VALUE current_chain, VALUE target_chain, const char *indent) {
451
+ long current_len = RARRAY_LEN(current_chain);
452
+ long target_len = RARRAY_LEN(target_chain);
453
+ long common = 0;
454
+ while (common < current_len && common < target_len) {
455
+ VALUE cur_id = rb_struct_aref(rb_ary_entry(current_chain, common), INT2FIX(CONDITIONAL_GROUP_ID));
456
+ VALUE tgt_id = rb_struct_aref(rb_ary_entry(target_chain, common), INT2FIX(CONDITIONAL_GROUP_ID));
457
+ if (!rb_equal(cur_id, tgt_id)) break;
458
+
459
+ common++;
460
+ }
461
+
462
+ for (long i = current_len - 1; i >= common; i--) {
463
+ rb_str_cat2(result, indent);
464
+ rb_str_cat2(result, "}\n");
465
+ }
466
+
467
+ for (long i = common; i < target_len; i++) {
468
+ VALUE group = rb_ary_entry(target_chain, i);
469
+ VALUE name = rb_struct_aref(group, INT2FIX(CONDITIONAL_GROUP_NAME));
470
+ VALUE condition = rb_struct_aref(group, INT2FIX(CONDITIONAL_GROUP_CONDITION));
471
+
472
+ rb_str_cat2(result, indent);
473
+ rb_str_cat2(result, "@");
474
+ rb_str_append(result, rb_sym2str(rb_struct_aref(group, INT2FIX(CONDITIONAL_GROUP_TYPE))));
475
+ if (!NIL_P(name) || !NIL_P(condition)) {
476
+ rb_str_cat2(result, " ");
477
+ if (!NIL_P(name)) {
478
+ rb_str_append(result, name);
479
+ if (!NIL_P(condition)) rb_str_cat2(result, " ");
480
+ }
481
+ if (!NIL_P(condition)) {
482
+ rb_str_append(result, condition);
483
+ }
484
+ }
485
+ rb_str_cat2(result, " {\n");
486
+ }
487
+
488
+ return target_chain;
489
+ }
490
+
491
+ static void close_conditional_groups(VALUE result, long count, const char *indent) {
492
+ for (long i = 0; i < count; i++) {
493
+ rb_str_cat2(result, indent);
494
+ rb_str_cat2(result, "}\n");
495
+ }
496
+ }
497
+
380
498
  // Rule and AtRule don't share a member layout past their first few fields
381
499
  // (see the AT_RULE_* comment in cataract.h), so reading parent_rule_id or
382
500
  // media_query_id off a rule of unknown type needs to dispatch on which
@@ -394,6 +512,12 @@ static inline VALUE rule_media_query_id_of(VALUE rule) {
394
512
  : rb_struct_aref(rule, INT2FIX(RULE_MEDIA_QUERY_ID));
395
513
  }
396
514
 
515
+ static inline VALUE rule_conditional_group_id_of(VALUE rule) {
516
+ return rb_obj_is_kind_of(rule, cAtRule)
517
+ ? rb_struct_aref(rule, INT2FIX(AT_RULE_CONDITIONAL_GROUP_ID))
518
+ : rb_struct_aref(rule, INT2FIX(RULE_CONDITIONAL_GROUP_ID));
519
+ }
520
+
397
521
  // Formatting options for stylesheet serialization
398
522
  // Avoids mode flags and if/else branches - all behavior controlled by struct values
399
523
  struct format_opts {
@@ -421,7 +545,7 @@ static inline void emit_rule(VALUE result, VALUE rule, const char *decl_indent,
421
545
  // each match as processed. Returns an array of their selectors (does not
422
546
  // include the current rule's own selector or mark it processed - the
423
547
  // caller's rule_id is handled by serialize_rule_in_group).
424
- static VALUE collect_matching_selectors(VALUE rule_ids_in_list, VALUE rule_media_query_id, VALUE rule_declarations, VALUE rules_array, VALUE processed_rule_ids) {
548
+ static VALUE collect_matching_selectors(VALUE rule_ids_in_list, VALUE rule_media_query_id, VALUE rule_declarations, VALUE rules_by_id, VALUE processed_rule_ids) {
425
549
  VALUE matching_selectors = rb_ary_new();
426
550
  long list_len = RARRAY_LEN(rule_ids_in_list);
427
551
 
@@ -434,7 +558,7 @@ static VALUE collect_matching_selectors(VALUE rule_ids_in_list, VALUE rule_media
434
558
  }
435
559
 
436
560
  // Find the rule by ID
437
- VALUE other_rule = rb_ary_entry(rules_array, FIX2INT(other_rule_id));
561
+ VALUE other_rule = rb_hash_aref(rules_by_id, other_rule_id);
438
562
  if (NIL_P(other_rule)) continue;
439
563
 
440
564
  // Check same media context (compare media_query_id directly)
@@ -481,7 +605,7 @@ static void emit_grouped_rule(VALUE result, VALUE rule, VALUE matching_selectors
481
605
  struct rule_group_ctx {
482
606
  VALUE selector_lists;
483
607
  int grouping_enabled;
484
- VALUE rules_array;
608
+ VALUE rules_by_id;
485
609
  VALUE processed_rule_ids;
486
610
  const struct format_opts *opts;
487
611
  };
@@ -512,7 +636,7 @@ static void serialize_rule_in_group(
512
636
 
513
637
  if (!NIL_P(rule_ids_in_list) && RARRAY_LEN(rule_ids_in_list) > 1) {
514
638
  VALUE rule_declarations = rb_struct_aref(rule, INT2FIX(RULE_DECLARATIONS));
515
- VALUE matching_selectors = collect_matching_selectors(rule_ids_in_list, rule_media_query_id, rule_declarations, ctx->rules_array, ctx->processed_rule_ids);
639
+ VALUE matching_selectors = collect_matching_selectors(rule_ids_in_list, rule_media_query_id, rule_declarations, ctx->rules_by_id, ctx->processed_rule_ids);
516
640
  emit_grouped_rule(result, rule, matching_selectors, rule_declarations, ctx->opts, decl_indent, rule_prefix);
517
641
  return;
518
642
  }
@@ -533,6 +657,7 @@ static VALUE serialize_stylesheet_with_grouping(
533
657
  VALUE media_query_lists,
534
658
  VALUE result,
535
659
  VALUE selector_lists,
660
+ VALUE conditional_groups,
536
661
  const struct format_opts *opts
537
662
  ) {
538
663
  long total_rules = RARRAY_LEN(rules_array);
@@ -542,20 +667,23 @@ static VALUE serialize_stylesheet_with_grouping(
542
667
 
543
668
  // Build reverse map: media_query_id => list_id
544
669
  // This allows us to detect when multiple rules share a comma-separated media query list
545
- VALUE mq_id_to_list_id = rb_hash_new();
546
- if (!NIL_P(media_query_lists) && TYPE(media_query_lists) == T_HASH) {
547
- struct build_mq_reverse_map_ctx ctx = { mq_id_to_list_id };
548
- rb_hash_foreach(media_query_lists, build_mq_reverse_map_callback, (VALUE)&ctx);
549
- }
670
+ VALUE mq_id_to_list_id = build_mq_id_to_list_id(media_query_lists);
671
+
672
+ // Only needed by the grouping path - rules_array here may already be
673
+ // filtered (Stylesheet#to_s(media: ...) for anything but :all), so
674
+ // looking up "other rules in this selector list" needs a real id => rule
675
+ // map rather than indexing directly into rules_array.
676
+ VALUE rules_by_id = grouping_enabled ? build_rules_by_id(rules_array) : Qnil;
550
677
 
551
678
  // Track processed rules to avoid duplicates when grouping
552
679
  VALUE processed_rule_ids = rb_hash_new();
553
680
 
554
- struct rule_group_ctx group_ctx = { selector_lists, grouping_enabled, rules_array, processed_rule_ids, opts };
681
+ struct rule_group_ctx group_ctx = { selector_lists, grouping_enabled, rules_by_id, processed_rule_ids, opts };
555
682
 
556
683
  // Iterate through rules in insertion order, grouping consecutive media queries
557
684
  VALUE current_media = Qnil;
558
685
  int in_media_block = 0;
686
+ VALUE current_cg_chain = rb_ary_new();
559
687
 
560
688
  for (long i = 0; i < total_rules; i++) {
561
689
  VALUE rule = rb_ary_entry(rules_array, i);
@@ -573,10 +701,12 @@ static VALUE serialize_stylesheet_with_grouping(
573
701
  rule_media = rb_ary_entry(media_queries, FIX2INT(rule_media_query_id));
574
702
  }
575
703
  int is_first_rule = (i == 0);
704
+ VALUE rule_cg_chain = conditional_group_chain(conditional_groups, rule_conditional_group_id_of(rule));
576
705
 
577
706
  if (NIL_P(rule_media)) {
578
707
  // Not in any media query - close any open media block first
579
708
  if (in_media_block) {
709
+ current_cg_chain = sync_conditional_group_chain(result, current_cg_chain, rb_ary_new(), opts->media_indent);
580
710
  rb_str_cat2(result, "}\n");
581
711
  in_media_block = 0;
582
712
  current_media = Qnil;
@@ -587,11 +717,15 @@ static VALUE serialize_stylesheet_with_grouping(
587
717
  rb_str_cat2(result, "\n");
588
718
  }
589
719
 
590
- serialize_rule_in_group(result, rule, rule_id, rule_media_query_id, &group_ctx, 0);
720
+ current_cg_chain = sync_conditional_group_chain(result, current_cg_chain, rule_cg_chain, "");
721
+
722
+ serialize_rule_in_group(result, rule, rule_id, rule_media_query_id, &group_ctx, RARRAY_LEN(current_cg_chain) > 0);
591
723
  } else {
592
724
  // This rule is in a media query
593
725
  // Check if media query changed from previous rule (compare MediaQuery objects by value)
594
726
  if (NIL_P(current_media) || !rb_equal(current_media, rule_media)) {
727
+ current_cg_chain = sync_conditional_group_chain(result, current_cg_chain, rb_ary_new(), opts->media_indent);
728
+
595
729
  // Close previous media block if open
596
730
  if (in_media_block) {
597
731
  rb_str_cat2(result, "}\n");
@@ -615,18 +749,23 @@ static VALUE serialize_stylesheet_with_grouping(
615
749
  in_media_block = 1;
616
750
  }
617
751
 
752
+ current_cg_chain = sync_conditional_group_chain(result, current_cg_chain, rule_cg_chain, opts->media_indent);
753
+
618
754
  serialize_rule_in_group(result, rule, rule_id, rule_media_query_id, &group_ctx, 1);
619
755
  }
620
756
  }
621
757
 
622
- // Close final media block if still open
758
+ // Close final conditional-group and media blocks if still open
759
+ close_conditional_groups(result, RARRAY_LEN(current_cg_chain), in_media_block ? opts->media_indent : "");
623
760
  if (in_media_block) {
624
761
  rb_str_cat2(result, "}\n");
625
762
  }
626
763
 
627
- // Guard hash objects we created and used throughout
764
+ // Guard hash/array objects we created and used throughout
628
765
  RB_GC_GUARD(mq_id_to_list_id);
766
+ RB_GC_GUARD(rules_by_id);
629
767
  RB_GC_GUARD(processed_rule_ids);
768
+ RB_GC_GUARD(current_cg_chain);
630
769
  return result;
631
770
  }
632
771
 
@@ -846,12 +985,18 @@ static void serialize_rule_with_children(VALUE result, VALUE rules_array, long r
846
985
  static VALUE serialize_stylesheet_with_nesting(
847
986
  VALUE rules_array,
848
987
  VALUE media_queries,
988
+ VALUE media_query_lists,
849
989
  VALUE result,
850
990
  const struct format_opts *opts
851
991
  ) {
852
992
  int formatted = (opts->decl_indent_base != NULL);
853
993
  long total_rules = RARRAY_LEN(rules_array);
854
994
 
995
+ // Reverse map: media_query_id => list_id, so a rule whose media query is
996
+ // part of a comma-separated list (e.g. "@media screen, print") renders
997
+ // the full list, not just its own single media query.
998
+ VALUE mq_id_to_list_id = build_mq_id_to_list_id(media_query_lists);
999
+
855
1000
  // Build parent_to_children map (parent_rule_id -> array of child indices)
856
1001
  // This allows O(1) lookup of children when serializing each parent
857
1002
  VALUE parent_to_children = rb_hash_new();
@@ -930,7 +1075,7 @@ static VALUE serialize_stylesheet_with_nesting(
930
1075
  // Open new media block - store the MediaQuery object for comparison
931
1076
  current_media = rule_media;
932
1077
  rb_str_cat2(result, "@media ");
933
- append_media_query_text(result, rule_media);
1078
+ append_media_query_string(result, rule_media_query_id, mq_id_to_list_id, media_query_lists, media_queries);
934
1079
  rb_str_cat2(result, " {\n");
935
1080
  in_media_block = 1;
936
1081
  }
@@ -970,11 +1115,12 @@ static VALUE serialize_stylesheet_with_nesting(
970
1115
  // this, differing only in which format_opts they select. Dispatches on has_nesting
971
1116
  // to pick the selector-list-grouping serializer (no nesting - zero overhead) or the
972
1117
  // parent/child lookahead serializer (has nesting).
973
- static VALUE stylesheet_serialize_impl(VALUE rules_array, VALUE charset, VALUE has_nesting, VALUE selector_lists, VALUE media_queries, VALUE media_query_lists, int formatted) {
1118
+ static VALUE stylesheet_serialize_impl(VALUE rules_array, VALUE charset, VALUE has_nesting, VALUE selector_lists, VALUE media_queries, VALUE media_query_lists, VALUE conditional_groups, int formatted) {
974
1119
  Check_Type(rules_array, T_ARRAY);
975
1120
  Check_Type(media_queries, T_ARRAY);
976
1121
  if (!NIL_P(media_query_lists)) Check_Type(media_query_lists, T_HASH);
977
1122
  if (!NIL_P(selector_lists)) Check_Type(selector_lists, T_HASH);
1123
+ if (!NIL_P(conditional_groups)) Check_Type(conditional_groups, T_ARRAY);
978
1124
 
979
1125
  VALUE result = rb_str_new_cstr("");
980
1126
 
@@ -1008,22 +1154,23 @@ static VALUE stylesheet_serialize_impl(VALUE rules_array, VALUE charset, VALUE h
1008
1154
 
1009
1155
  // Fast path: if no nesting, use the selector-list-grouping serializer (zero overhead)
1010
1156
  if (!RTEST(has_nesting)) {
1011
- return serialize_stylesheet_with_grouping(rules_array, media_queries, media_query_lists, result, selector_lists, &opts);
1157
+ return serialize_stylesheet_with_grouping(rules_array, media_queries, media_query_lists, result, selector_lists, conditional_groups, &opts);
1012
1158
  }
1013
1159
 
1014
1160
  // Has nesting - use the parent/child lookahead serializer
1015
1161
  // TODO: Phase 2 - use selector_lists for grouping on the nesting path too
1016
- return serialize_stylesheet_with_nesting(rules_array, media_queries, result, &opts);
1162
+ // TODO: conditional_groups (@supports etc.) aren't reconstructed on this path yet
1163
+ return serialize_stylesheet_with_nesting(rules_array, media_queries, media_query_lists, result, &opts);
1017
1164
  }
1018
1165
 
1019
1166
  // New stylesheet serialization entry point - checks for nesting and delegates
1020
- 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) {
1021
- return stylesheet_serialize_impl(rules_array, charset, has_nesting, selector_lists, media_queries, media_query_lists, 0);
1167
+ 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, VALUE conditional_groups) {
1168
+ return stylesheet_serialize_impl(rules_array, charset, has_nesting, selector_lists, media_queries, media_query_lists, conditional_groups, 0);
1022
1169
  }
1023
1170
 
1024
1171
  // Formatted version with indentation and newlines (with nesting support)
1025
- 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) {
1026
- return stylesheet_serialize_impl(rules_array, charset, has_nesting, selector_lists, media_queries, media_query_lists, 1);
1172
+ 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, VALUE conditional_groups) {
1173
+ return stylesheet_serialize_impl(rules_array, charset, has_nesting, selector_lists, media_queries, media_query_lists, conditional_groups, 1);
1027
1174
  }
1028
1175
 
1029
1176
  /*
@@ -1077,78 +1224,6 @@ static VALUE new_parse_declarations_string(const char *start, const char *end) {
1077
1224
  return declarations;
1078
1225
  }
1079
1226
 
1080
- /*
1081
- * Convert array of Declaration structs to CSS string
1082
- * Format: "prop: value; prop2: value2 !important; "
1083
- *
1084
- * This is a copy of declarations_array_to_s from cataract.c,
1085
- * but works with Declaration structs instead of Declaration structs
1086
- */
1087
- static VALUE new_declarations_array_to_s(VALUE declarations_array) {
1088
- Check_Type(declarations_array, T_ARRAY);
1089
-
1090
- long len = RARRAY_LEN(declarations_array);
1091
- if (len == 0) {
1092
- return rb_str_new_cstr("");
1093
- }
1094
-
1095
- // Use rb_str_buf_new for efficient string building
1096
- VALUE result = rb_str_buf_new(len * 32); // Estimate 32 chars per declaration
1097
-
1098
- for (long i = 0; i < len; i++) {
1099
- VALUE decl = rb_ary_entry(declarations_array, i);
1100
-
1101
- // Validate this is a Declaration struct
1102
- if (!RB_TYPE_P(decl, T_STRUCT) || rb_obj_class(decl) != cDeclaration) {
1103
- rb_raise(rb_eTypeError,
1104
- "Expected array of Declaration structs, got %s at index %ld",
1105
- rb_obj_classname(decl), i);
1106
- }
1107
-
1108
- // Extract struct fields
1109
- VALUE property = rb_struct_aref(decl, INT2FIX(DECL_PROPERTY));
1110
- VALUE value = rb_struct_aref(decl, INT2FIX(DECL_VALUE));
1111
- VALUE important = rb_struct_aref(decl, INT2FIX(DECL_IMPORTANT));
1112
-
1113
- // Append: "property: value"
1114
- rb_str_buf_append(result, property);
1115
- rb_str_buf_cat2(result, ": ");
1116
- rb_str_buf_append(result, value);
1117
-
1118
- // Append " !important" if needed
1119
- if (RTEST(important)) {
1120
- rb_str_buf_cat2(result, " !important");
1121
- }
1122
-
1123
- rb_str_buf_cat2(result, "; ");
1124
-
1125
- RB_GC_GUARD(decl);
1126
- RB_GC_GUARD(property);
1127
- RB_GC_GUARD(value);
1128
- RB_GC_GUARD(important);
1129
- }
1130
-
1131
- // Strip trailing space
1132
- rb_str_set_len(result, RSTRING_LEN(result) - 1);
1133
-
1134
- RB_GC_GUARD(result);
1135
- return result;
1136
- }
1137
-
1138
- /*
1139
- * Instance method: Declarations#to_s
1140
- * Converts declarations to CSS string
1141
- *
1142
- * @return [String] CSS declarations like "color: red; margin: 10px !important;"
1143
- */
1144
- static VALUE new_declarations_to_s_method(VALUE self) {
1145
- // Get @values instance variable (array of Declaration structs)
1146
- VALUE values = rb_ivar_get(self, rb_intern("@values"));
1147
-
1148
- // Call core serialization function
1149
- return new_declarations_array_to_s(values);
1150
- }
1151
-
1152
1227
  /*
1153
1228
  * Ruby-facing wrapper for new_parse_declarations
1154
1229
  *
@@ -1239,23 +1314,30 @@ void Init_native_extension(void) {
1239
1314
  rb_raise(rb_eLoadError, "Cataract::MediaQuery not defined. Do not require 'cataract/native_extension' directly, use require 'cataract'");
1240
1315
  }
1241
1316
 
1242
- // Define Declarations class and add to_s method
1243
- VALUE cDeclarations = rb_define_class_under(mCataract, "Declarations", rb_cObject);
1244
- rb_define_method(cDeclarations, "to_s", new_declarations_to_s_method, 0);
1317
+ if (rb_const_defined(mCataract, rb_intern("ConditionalGroup"))) {
1318
+ cConditionalGroup = rb_const_get(mCataract, rb_intern("ConditionalGroup"));
1319
+ } else {
1320
+ rb_raise(rb_eLoadError, "Cataract::ConditionalGroup not defined. Do not require 'cataract/native_extension' directly, use require 'cataract'");
1321
+ }
1245
1322
 
1246
- // Define Stylesheet class (Ruby will add instance methods like each_selector)
1323
+ // Define Stylesheet class (Ruby will add instance methods like each_selector).
1324
+ // Declarations is left alone here - it's a plain Ruby class (declarations.rb)
1325
+ // with its own #to_s, not something this backend attaches methods to.
1247
1326
  cStylesheet = rb_define_class_under(mCataract, "Stylesheet", rb_cObject);
1248
1327
 
1249
- // Define module functions
1250
- rb_define_module_function(mCataract, "_parse_css", parse_css_new, -1);
1251
- rb_define_module_function(mCataract, "stylesheet_to_s", stylesheet_to_s, 6);
1252
- rb_define_module_function(mCataract, "stylesheet_to_formatted_s", stylesheet_to_formatted_s, 6);
1253
- rb_define_module_function(mCataract, "parse_media_types", parse_media_types, 1);
1254
- rb_define_module_function(mCataract, "parse_declarations", new_parse_declarations, 1);
1255
- rb_define_module_function(mCataract, "flatten", cataract_flatten, 1);
1256
- rb_define_module_function(mCataract, "merge", cataract_flatten, 1); // Deprecated alias for backwards compatibility
1257
- rb_define_module_function(mCataract, "calculate_specificity", calculate_specificity, 1);
1258
- rb_define_module_function(mCataract, "expand_shorthand", cataract_expand_shorthand, 1);
1328
+ // Every native-specific entry point lives under Cataract::Backends::Native,
1329
+ // never directly on Cataract itself, so the pure Ruby backend can be loaded
1330
+ // in the same process without either backend clobbering the other's methods.
1331
+ VALUE mBackends = rb_define_module_under(mCataract, "Backends");
1332
+ VALUE mNative = rb_define_module_under(mBackends, "Native");
1333
+
1334
+ rb_define_module_function(mNative, "parse", parse_css_new, -1);
1335
+ rb_define_module_function(mNative, "stylesheet_to_s", stylesheet_to_s, 7);
1336
+ rb_define_module_function(mNative, "stylesheet_to_formatted_s", stylesheet_to_formatted_s, 7);
1337
+ rb_define_module_function(mNative, "parse_declarations", new_parse_declarations, 1);
1338
+ rb_define_module_function(mNative, "flatten", cataract_flatten, 1);
1339
+ rb_define_module_function(mNative, "calculate_specificity", calculate_specificity, 1);
1340
+ rb_define_module_function(mNative, "expand_shorthand", cataract_expand_shorthand, 1);
1259
1341
 
1260
1342
  // Initialize flatten constants (cached property strings)
1261
1343
  init_flatten_constants();
@@ -1275,11 +1357,11 @@ void Init_native_extension(void) {
1275
1357
  rb_hash_aset(compile_flags, ID2SYM(rb_intern("str_buf_optimization")), Qtrue);
1276
1358
  #endif
1277
1359
 
1278
- rb_define_const(mCataract, "COMPILE_FLAGS", compile_flags);
1360
+ rb_define_const(mNative, "COMPILE_FLAGS", compile_flags);
1279
1361
 
1280
- // Flag to indicate native extension is loaded (for pure Ruby fallback detection)
1281
- rb_define_const(mCataract, "NATIVE_EXTENSION_LOADED", Qtrue);
1362
+ // Flag to indicate the native backend is loaded (for pure Ruby fallback detection)
1363
+ rb_define_const(mNative, "NATIVE_EXTENSION_LOADED", Qtrue);
1282
1364
 
1283
1365
  // Implementation type constant
1284
- rb_define_const(mCataract, "IMPLEMENTATION", ID2SYM(rb_intern("native")));
1366
+ rb_define_const(mNative, "IMPLEMENTATION", ID2SYM(rb_intern("native")));
1285
1367
  }
@@ -14,6 +14,7 @@ extern VALUE cAtRule;
14
14
  extern VALUE cStylesheet;
15
15
  extern VALUE cImportStatement;
16
16
  extern VALUE cMediaQuery;
17
+ extern VALUE cConditionalGroup;
17
18
 
18
19
  // Error class references
19
20
  extern VALUE eCataractError;
@@ -25,7 +26,7 @@ extern VALUE eParseError;
25
26
  // Struct field indices
26
27
  // ============================================================================
27
28
 
28
- // Rule struct field indices (id, selector, declarations, specificity, parent_rule_id, nesting_style, selector_list_id, media_query_id)
29
+ // Rule struct field indices (id, selector, declarations, specificity, parent_rule_id, nesting_style, selector_list_id, media_query_id, conditional_group_id)
29
30
  #define RULE_ID 0
30
31
  #define RULE_SELECTOR 1
31
32
  #define RULE_DECLARATIONS 2
@@ -34,6 +35,7 @@ extern VALUE eParseError;
34
35
  #define RULE_NESTING_STYLE 5
35
36
  #define RULE_SELECTOR_LIST_ID 6
36
37
  #define RULE_MEDIA_QUERY_ID 7
38
+ #define RULE_CONDITIONAL_GROUP_ID 8
37
39
 
38
40
  // Nesting style constants
39
41
  #define NESTING_STYLE_IMPLICIT 0 // .parent { .child { } } - no &
@@ -45,13 +47,14 @@ extern VALUE eParseError;
45
47
  #define NO_PARENT_SELECTOR Qnil
46
48
  #define NO_PARENT_RULE_ID Qnil
47
49
  #define NO_MEDIA_QUERY_ID (-1)
50
+ #define NO_CONDITIONAL_GROUP_ID (-1)
48
51
 
49
52
  // Declaration struct field indices (property, value, important)
50
53
  #define DECL_PROPERTY 0
51
54
  #define DECL_VALUE 1
52
55
  #define DECL_IMPORTANT 2
53
56
 
54
- // AtRule struct field indices (id, selector, content, specificity, media_query_id)
57
+ // AtRule struct field indices (id, selector, content, specificity, media_query_id, conditional_group_id)
55
58
  // Matches Rule interface for duck-typing, but AtRule has fewer members - its
56
59
  // indices do NOT line up with Rule's past AT_RULE_SPECIFICITY (e.g. index 4
57
60
  // is media_query_id here but parent_rule_id on Rule), so never read a field
@@ -61,6 +64,14 @@ extern VALUE eParseError;
61
64
  #define AT_RULE_CONTENT 2
62
65
  #define AT_RULE_SPECIFICITY 3
63
66
  #define AT_RULE_MEDIA_QUERY_ID 4
67
+ #define AT_RULE_CONDITIONAL_GROUP_ID 5
68
+
69
+ // ConditionalGroup struct field indices (id, type, name, condition, parent_id)
70
+ #define CONDITIONAL_GROUP_ID 0
71
+ #define CONDITIONAL_GROUP_TYPE 1
72
+ #define CONDITIONAL_GROUP_NAME 2
73
+ #define CONDITIONAL_GROUP_CONDITION 3
74
+ #define CONDITIONAL_GROUP_PARENT_ID 4
64
75
 
65
76
  // ============================================================================
66
77
  // Macros
@@ -262,7 +273,6 @@ static inline VALUE strip_string(const char *str, long len) {
262
273
  // CSS parser (css_parser_new.c)
263
274
  VALUE parse_css_new(int argc, VALUE *argv, VALUE self);
264
275
  VALUE parse_css_new_impl(VALUE css_string, VALUE parser_options, int rule_id_offset);
265
- VALUE parse_media_types(VALUE self, VALUE media_query_sym);
266
276
 
267
277
  // Flatten (flatten.c)
268
278
  VALUE cataract_flatten(VALUE self, VALUE rules_array);