Module: Cataract

Defined in:
lib/cataract.rb,
lib/cataract/rule.rb,
lib/cataract/at_rule.rb,
lib/cataract/version.rb,
lib/cataract/stylesheet.rb,
lib/cataract/declarations.rb,
lib/cataract/import_resolver.rb,
lib/cataract/stylesheet_scope.rb,
ext/cataract/cataract.c,
ext/cataract_old/cataract.c

Overview

Cataract is a high-performance CSS parser written in C with a Ruby interface.

It provides fast CSS parsing, rule querying, cascade merging, and serialization. Designed for performance-critical applications that need to process large amounts of CSS.

Examples:

Basic usage

require 'cataract'

# Parse CSS
sheet = Cataract.parse_css("body { color: red; } h1 { color: blue; }")

# Query rules
sheet.select(&:selector?).each { |rule| puts "#{rule.selector}: #{rule.declarations}" }

# Merge with cascade rules
merged = sheet.merge

See Also:

Defined Under Namespace

Modules: ImportResolver Classes: AtRule, ColorConversionError, Declarations, DepthError, Error, ImportError, ParseError, Rule, SizeError, Stylesheet, StylesheetScope

Constant Summary collapse

VERSION =
'0.1.0'
COMPILE_FLAGS =

compiler optimizations that affect the generated code.

compile_flags
STRING_ALLOC_MODE =
ID2SYM(rb_intern("buffer"))

Class Method Summary collapse

Class Method Details

._create_background_shorthandObject

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

._create_border_color_shorthandObject

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

._create_border_shorthandObject

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

._create_border_style_shorthandObject

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

._create_border_width_shorthandObject

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

._create_font_shorthandObject

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

._create_list_style_shorthandObject

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

._create_margin_shorthandObject

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

._create_padding_shorthandObject

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

._expand_backgroundObject

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

._expand_borderObject

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

._expand_border_colorObject

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

._expand_border_sideObject

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

._expand_border_styleObject

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

._expand_border_widthObject

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

._expand_fontObject

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

._expand_list_styleObject

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

._expand_marginObject

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

._expand_paddingObject

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

._parse_css(css_string) ⇒ Hash

Parse CSS and return hash with parsed data This matches the old parse_css API

Parameters:

  • css_string (String)

    CSS to parse

Returns:

  • (Hash)

    { rules: […], media_index: …, charset: “…” }



36
37
38
# File 'ext/cataract/cataract.c', line 36

VALUE parse_css_new(VALUE self, VALUE css_string) {
    return parse_css_new_impl(css_string, 0);
}

._rules_to_s(rules_array) ⇒ Object

Convert array of Rule structs to full CSS string Format: “selector { prop: value; }nselector2 { prop: value; }”



117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
# File 'ext/cataract_old/cataract.c', line 117

static VALUE rules_to_s(VALUE self, VALUE rules_array) {
    Check_Type(rules_array, T_ARRAY);

    long len = RARRAY_LEN(rules_array);
    if (len == 0) {
        return rb_str_new_cstr("");
    }

    // Estimate: ~100 chars per rule (selector + declarations)
    VALUE result = rb_str_buf_new(len * 100);

    for (long i = 0; i < len; i++) {
        VALUE rule = rb_ary_entry(rules_array, i);

        // Validate this is a Rule struct
        if (!RB_TYPE_P(rule, T_STRUCT)) {
            rb_raise(rb_eTypeError,
                     "Expected array of Rule structs, got %s at index %ld",
                     rb_obj_classname(rule), i);
        }

        // Extract: selector, declarations, specificity, media_query
        VALUE selector = rb_struct_aref(rule, INT2FIX(RULE_SELECTOR));
        VALUE declarations = rb_struct_aref(rule, INT2FIX(RULE_DECLARATIONS));

        // Append selector
        rb_str_buf_append(result, selector);
        rb_str_buf_cat2(result, " { ");

        // Serialize each declaration
        long decl_len = RARRAY_LEN(declarations);
        for (long j = 0; j < decl_len; j++) {
            VALUE decl = rb_ary_entry(declarations, j);

            VALUE property = rb_struct_aref(decl, INT2FIX(DECL_PROPERTY));
            VALUE value = rb_struct_aref(decl, INT2FIX(DECL_VALUE));
            VALUE important = rb_struct_aref(decl, INT2FIX(DECL_IMPORTANT));

            rb_str_buf_append(result, property);
            rb_str_buf_cat2(result, ": ");
            rb_str_buf_append(result, value);

            if (RTEST(important)) {
                rb_str_buf_cat2(result, " !important");
            }

            rb_str_buf_cat2(result, "; ");
        }

        rb_str_buf_cat2(result, "}\n");

        RB_GC_GUARD(rule);
        RB_GC_GUARD(selector);
        RB_GC_GUARD(declarations);
    }

    RB_GC_GUARD(result);
    return result;
}

._split_valueObject

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

._stylesheet_to_formatted_s(rules_array, media_index, charset, has_nesting) ⇒ Object

Formatted version with indentation and newlines (with nesting support)



726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
# File 'ext/cataract/cataract.c', line 726

static VALUE stylesheet_to_formatted_s_new(VALUE self, VALUE rules_array, VALUE media_index, VALUE charset, VALUE has_nesting) {
    Check_Type(rules_array, T_ARRAY);
    Check_Type(media_index, T_HASH);

    // Fast path: if no nesting, use original implementation (zero overhead)
    if (!RTEST(has_nesting)) {
        return stylesheet_to_formatted_s_original(rules_array, media_index, charset);
    }

    // SLOW PATH: Has nesting - use parameterized serialization with formatted=1
    long total_rules = RARRAY_LEN(rules_array);
    VALUE result = rb_str_new_cstr("");

    // Add charset if present
    if (!NIL_P(charset)) {
        rb_str_cat2(result, "@charset \"");
        rb_str_append(result, charset);
        rb_str_cat2(result, "\";\n");
    }

    // Build rule_to_media map
    VALUE rule_to_media = rb_hash_new();
    struct build_rule_map_ctx map_ctx = { rule_to_media };
    rb_hash_foreach(media_index, build_rule_map_callback, (VALUE)&map_ctx);

    // Build parent_to_children map (parent_rule_id -> array of child indices)
    VALUE parent_to_children = rb_hash_new();
    for (long i = 0; i < total_rules; i++) {
        VALUE rule = rb_ary_entry(rules_array, i);
        VALUE parent_id = rb_struct_aref(rule, INT2FIX(RULE_PARENT_RULE_ID));

        if (!NIL_P(parent_id)) {
            VALUE children = rb_hash_aref(parent_to_children, parent_id);
            if (NIL_P(children)) {
                children = rb_ary_new();
                rb_hash_aset(parent_to_children, parent_id, children);
            }
            rb_ary_push(children, LONG2FIX(i));
        }
    }

    // Serialize only top-level rules (parent_rule_id == nil)
    for (long i = 0; i < total_rules; i++) {
        VALUE rule = rb_ary_entry(rules_array, i);
        VALUE parent_id = rb_struct_aref(rule, INT2FIX(RULE_PARENT_RULE_ID));

        // Skip child rules - they're serialized when we hit their parent
        if (!NIL_P(parent_id)) {
            continue;
        }

        // Check if this is an AtRule
        if (rb_obj_is_kind_of(rule, cAtRule)) {
            serialize_at_rule(result, rule);
            continue;
        }

        // Serialize rule with nested children
        serialize_rule_with_children(
            result, rules_array, i, rule_to_media, parent_to_children,
            1,  // formatted (with indentation)
            0   // indent_level (top-level)
        );
    }

    return result;
}

._stylesheet_to_formatted_s_cObject

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

._stylesheet_to_s(rules_array, media_index, charset, has_nesting) ⇒ Object

New stylesheet serialization entry point - checks for nesting and delegates



574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
# File 'ext/cataract/cataract.c', line 574

static VALUE stylesheet_to_s_new(VALUE self, VALUE rules_array, VALUE media_index, VALUE charset, VALUE has_nesting) {
    Check_Type(rules_array, T_ARRAY);
    Check_Type(media_index, T_HASH);

    // Fast path: if no nesting, use original implementation (zero overhead)
    if (!RTEST(has_nesting)) {
        return stylesheet_to_s_original(rules_array, media_index, charset);
    }

    // SLOW PATH: Has nesting - use lookahead approach
    long total_rules = RARRAY_LEN(rules_array);
    VALUE result = rb_str_new_cstr("");

    // Add charset if present
    if (!NIL_P(charset)) {
        rb_str_cat2(result, "@charset \"");
        rb_str_append(result, charset);
        rb_str_cat2(result, "\";\n");
    }

    // Build rule_to_media map
    VALUE rule_to_media = rb_hash_new();
    struct build_rule_map_ctx map_ctx = { rule_to_media };
    rb_hash_foreach(media_index, build_rule_map_callback, (VALUE)&map_ctx);

    // Build parent_to_children map (parent_rule_id -> array of child indices)
    // This allows O(1) lookup of children when serializing each parent
    VALUE parent_to_children = rb_hash_new();
    for (long i = 0; i < total_rules; i++) {
        VALUE rule = rb_ary_entry(rules_array, i);
        VALUE parent_id = rb_struct_aref(rule, INT2FIX(RULE_PARENT_RULE_ID));

        if (!NIL_P(parent_id)) {
            DEBUG_PRINTF("[MAP] Rule %ld has parent_id=%s, adding to map\n", i,
                        RSTRING_PTR(rb_inspect(parent_id)));

            VALUE children = rb_hash_aref(parent_to_children, parent_id);
            if (NIL_P(children)) {
                children = rb_ary_new();
                rb_hash_aset(parent_to_children, parent_id, children);
            }
            rb_ary_push(children, LONG2FIX(i));
        }
    }

    DEBUG_PRINTF("[MAP] parent_to_children map: %s\n", RSTRING_PTR(rb_inspect(parent_to_children)));

    // Serialize only top-level rules (parent_rule_id == nil)
    // Children are serialized recursively
    DEBUG_PRINTF("[SERIALIZE] Starting serialization, total_rules=%ld\n", total_rules);
    for (long i = 0; i < total_rules; i++) {
        VALUE rule = rb_ary_entry(rules_array, i);
        VALUE parent_id = rb_struct_aref(rule, INT2FIX(RULE_PARENT_RULE_ID));

        DEBUG_PRINTF("[SERIALIZE] Rule %ld: selector=%s, parent_id=%s\n", i,
                    RSTRING_PTR(rb_struct_aref(rule, INT2FIX(RULE_SELECTOR))),
                    NIL_P(parent_id) ? "nil" : RSTRING_PTR(rb_inspect(parent_id)));

        // Skip child rules - they're serialized when we hit their parent
        if (!NIL_P(parent_id)) {
            DEBUG_PRINTF("[SERIALIZE]   Skipping (is child)\n");
            continue;
        }

        // Check if this is an AtRule
        if (rb_obj_is_kind_of(rule, cAtRule)) {
            serialize_at_rule(result, rule);
            continue;
        }

        // Serialize rule with nested children
        serialize_rule_with_children(
            result, rules_array, i, rule_to_media, parent_to_children,
            0,  // formatted (compact)
            0   // indent_level (top-level)
        );
    }

    return result;
}

._stylesheet_to_s_cObject

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

.apply_cascade(input) ⇒ Object

This is called from Ruby as Cataract.merge_rules



759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
# File 'ext/cataract_old/merge.c', line 759

VALUE cataract_merge_wrapper(VALUE self, VALUE input) {
    // Check if input is a hash (new structure from Stylesheet)
    if (TYPE(input) == T_HASH) {
        // Flatten hash structure to array
        VALUE rules_array = rb_ary_new();
        struct flatten_hash_ctx ctx = { rules_array };
        rb_hash_foreach(input, flatten_hash_callback, (VALUE)&ctx);

        // Call the original merge function
        VALUE result = cataract_merge(self, rules_array);

        RB_GC_GUARD(rules_array);
        return result;
    }

    // Input is already an array - call original function directly
    return cataract_merge(self, input);
}

.calculate_specificity(selector_string) ⇒ Object

Calculate specificity for a CSS selector string



22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
# File 'ext/cataract/specificity.c', line 22

VALUE calculate_specificity(VALUE self, VALUE selector_string) {
    Check_Type(selector_string, T_STRING);

    const char *p = RSTRING_PTR(selector_string);
    const char *pe = p + RSTRING_LEN(selector_string);

    // Counters for specificity components
    int id_count = 0;
    int class_count = 0;
    int attr_count = 0;
    int pseudo_class_count = 0;
    int pseudo_element_count = 0;
    int element_count = 0;

    while (p < pe) {
        char c = *p;

        // Skip whitespace and combinators
        if (IS_WHITESPACE(c) || c == '>' || c == '+' || c == '~' || c == ',') {
            p++;
            continue;
        }

        // ID selector: #id
        if (c == '#') {
            id_count++;
            p++;
            // Skip the identifier
            while (p < pe && ((*p >= 'a' && *p <= 'z') || (*p >= 'A' && *p <= 'Z') ||
                              (*p >= '0' && *p <= '9') || *p == '-' || *p == '_')) {
                p++;
            }
            continue;
        }

        // Class selector: .class
        if (c == '.') {
            class_count++;
            p++;
            // Skip the identifier
            while (p < pe && ((*p >= 'a' && *p <= 'z') || (*p >= 'A' && *p <= 'Z') ||
                              (*p >= '0' && *p <= '9') || *p == '-' || *p == '_')) {
                p++;
            }
            continue;
        }

        // Attribute selector: [attr] or [attr=value]
        if (c == '[') {
            attr_count++;
            p++;
            // Skip to closing bracket
            int bracket_depth = 1;
            while (p < pe && bracket_depth > 0) {
                if (*p == '[') bracket_depth++;
                else if (*p == ']') bracket_depth--;
                p++;
            }
            continue;
        }

        // Pseudo-element (::) or pseudo-class (:)
        if (c == ':') {
            p++;
            int is_pseudo_element = 0;

            // Check for double colon (::)
            if (p < pe && *p == ':') {
                is_pseudo_element = 1;
                p++;
            }

            // Extract pseudo name
            const char *pseudo_start = p;
            while (p < pe && ((*p >= 'a' && *p <= 'z') || (*p >= 'A' && *p <= 'Z') ||
                              (*p >= '0' && *p <= '9') || *p == '-')) {
                p++;
            }
            long pseudo_len = p - pseudo_start;

            // Check for legacy pseudo-elements (single colon but should be double)
            // :before, :after, :first-line, :first-letter, :selection
            int is_legacy_pseudo_element = 0;
            if (!is_pseudo_element && pseudo_len > 0) {
                is_legacy_pseudo_element =
                    (pseudo_len == 6 && strncmp(pseudo_start, "before", 6) == 0) ||
                    (pseudo_len == 5 && strncmp(pseudo_start, "after", 5) == 0) ||
                    (pseudo_len == 10 && strncmp(pseudo_start, "first-line", 10) == 0) ||
                    (pseudo_len == 12 && strncmp(pseudo_start, "first-letter", 12) == 0) ||
                    (pseudo_len == 9 && strncmp(pseudo_start, "selection", 9) == 0);
            }

            // Check for :not() - it doesn't count itself, but its content does
            int is_not = (pseudo_len == 3 && strncmp(pseudo_start, "not", 3) == 0);

            // Skip function arguments if present
            if (p < pe && *p == '(') {
                p++;
                int paren_depth = 1;

                // If it's :not(), we need to calculate specificity of the content
                if (is_not) {
                    const char *not_content_start = p;

                    // Find closing paren
                    while (p < pe && paren_depth > 0) {
                        if (*p == '(') paren_depth++;
                        else if (*p == ')') paren_depth--;
                        if (paren_depth > 0) p++;
                    }

                    const char *not_content_end = p;
                    long not_content_len = not_content_end - not_content_start;

                    // Recursively calculate specificity of :not() content
                    if (not_content_len > 0) {
                        VALUE not_content = rb_str_new(not_content_start, not_content_len);
                        VALUE not_spec = calculate_specificity(self, not_content);
                        int not_specificity = NUM2INT(not_spec);

                        // Add :not() content's specificity to our counts
                        int additional_a = not_specificity / 100;
                        int additional_b = (not_specificity % 100) / 10;
                        int additional_c = not_specificity % 10;

                        id_count += additional_a;
                        class_count += additional_b;
                        element_count += additional_c;

                        RB_GC_GUARD(not_content);
                        RB_GC_GUARD(not_spec);
                    }

                    p++;  // Skip closing paren
                } else {
                    // Skip other function arguments
                    while (p < pe && paren_depth > 0) {
                        if (*p == '(') paren_depth++;
                        else if (*p == ')') paren_depth--;
                        p++;
                    }

                    // Count the pseudo-class/element
                    if (is_pseudo_element || is_legacy_pseudo_element) {
                        pseudo_element_count++;
                    } else {
                        pseudo_class_count++;
                    }
                }
            } else {
                // No function arguments - count the pseudo-class/element
                if (is_not) {
                    // :not without parens is invalid, but don't count it
                } else if (is_pseudo_element || is_legacy_pseudo_element) {
                    pseudo_element_count++;
                } else {
                    pseudo_class_count++;
                }
            }
            continue;
        }

        // Universal selector: *
        if (c == '*') {
            // Universal selector has specificity 0, don't count
            p++;
            continue;
        }

        // Type selector (element name): div, span, etc.
        if ((*p >= 'a' && *p <= 'z') || (*p >= 'A' && *p <= 'Z')) {
            element_count++;
            // Skip the identifier
            while (p < pe && ((*p >= 'a' && *p <= 'z') || (*p >= 'A' && *p <= 'Z') ||
                              (*p >= '0' && *p <= '9') || *p == '-' || *p == '_')) {
                p++;
            }
            continue;
        }

        // Unknown character, skip it
        p++;
    }

    // Calculate specificity using W3C formula:
    // IDs * 100 + (classes + attributes + pseudo-classes) * 10 + (elements + pseudo-elements) * 1
    int specificity = (id_count * 100) +
                      ((class_count + attr_count + pseudo_class_count) * 10) +
                      ((element_count + pseudo_element_count) * 1);

    return INT2NUM(specificity);
}

.extract_importsObject

Import scanning

.merge(input) ⇒ Object

Output: Stylesheet with merged declarations



339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
# File 'ext/cataract/merge.c', line 339

VALUE cataract_merge_new(VALUE self, VALUE input) {
    VALUE rules_array;

    // Handle different input types
    // Most calls pass Stylesheet (common case), String is rare
    if (TYPE(input) == T_STRING) {
        // Parse CSS string first
        VALUE parsed = parse_css_new(self, input);
        rules_array = rb_hash_aref(parsed, ID2SYM(rb_intern("rules")));
    } else if (rb_obj_is_kind_of(input, cStylesheet)) {
        // Extract @rules from Stylesheet (common case)
        rules_array = rb_ivar_get(input, id_ivar_rules);
    } else {
        rb_raise(rb_eTypeError, "Expected Stylesheet or String, got %s",
                rb_obj_classname(input));
    }

    Check_Type(rules_array, T_ARRAY);

    // Check if stylesheet has nesting (affects selector rollup)
    int has_nesting = 0;
    if (rb_obj_is_kind_of(input, cStylesheet)) {
        VALUE has_nesting_ivar = rb_ivar_get(input, rb_intern("@_has_nesting"));
        has_nesting = RTEST(has_nesting_ivar);
    }

    // Initialize cached symbol IDs on first call (thread-safe since GVL is held)
    // This only happens once, so unlikely
    if (id_value == 0) {
        id_value = rb_intern("value");
        id_specificity = rb_intern("specificity");
        id_important = rb_intern("important");
    }

    long num_rules = RARRAY_LEN(rules_array);
    // Empty stylesheets are rare
    if (num_rules == 0) {
        // Return empty stylesheet
        VALUE empty_sheet = rb_class_new_instance(0, NULL, cStylesheet);
        return empty_sheet;
    }

    // For nested CSS: identify parent rules (rules that have children)
    // These should be skipped during merge, even if they have declarations
    // Use Ruby hash as a set: parent_id => true
    VALUE parent_ids = Qnil;
    if (has_nesting) {
        DEBUG_PRINTF("\n=== MERGE: has_nesting=true, num_rules=%ld ===\n", num_rules);
        parent_ids = rb_hash_new();
        for (long i = 0; i < num_rules; i++) {
            VALUE rule = RARRAY_AREF(rules_array, i);
            VALUE parent_rule_id = rb_struct_aref(rule, INT2FIX(RULE_PARENT_RULE_ID));
            DEBUG_PRINTF("  Rule %ld: selector='%s', rule_id=%d, parent_rule_id=%s\n",
                         i,
                         RSTRING_PTR(rb_struct_aref(rule, INT2FIX(RULE_SELECTOR))),
                         FIX2INT(rb_struct_aref(rule, INT2FIX(RULE_ID))),
                         NIL_P(parent_rule_id) ? "nil" : RSTRING_PTR(rb_inspect(parent_rule_id)));
            if (!NIL_P(parent_rule_id)) {
                // This rule has a parent, so mark that parent ID
                rb_hash_aset(parent_ids, parent_rule_id, Qtrue);
            }
        }
    }

    // For nested CSS with different selectors from SAME parent: group rules by selector
    // Only split into multiple rules if ALL rules share the same parent_rule_id
    // selector => [rule indices]
    VALUE selector_groups = Qnil;
    VALUE common_parent = Qundef;  // Qundef = not set yet

    if (has_nesting) {
        DEBUG_PRINTF("\n=== Building selector groups ===\n");
        selector_groups = rb_hash_new();
        for (long i = 0; i < num_rules; i++) {
            VALUE rule = RARRAY_AREF(rules_array, i);
            VALUE declarations = rb_struct_aref(rule, INT2FIX(RULE_DECLARATIONS));
            VALUE parent_rule_id = rb_struct_aref(rule, INT2FIX(RULE_PARENT_RULE_ID));
            VALUE selector = rb_struct_aref(rule, INT2FIX(RULE_SELECTOR));

            // Per W3C spec: parent and child are SEPARATE rules with different selectors
            // Both should be included in merge output
            // Don't skip parent rules - they have their own selector and declarations

            // Skip empty rules (no declarations)
            if (RARRAY_LEN(declarations) == 0) {
                DEBUG_PRINTF("  Skipping rule %ld: selector='%s' (empty declarations)\n",
                             i, RSTRING_PTR(selector));
                continue;
            }

            DEBUG_PRINTF("  Processing rule %ld: selector='%s', parent_rule_id=%s\n",
                         i, RSTRING_PTR(selector),
                         NIL_P(parent_rule_id) ? "nil" : RSTRING_PTR(rb_inspect(parent_rule_id)));

            // Track if all rules share the same parent
            if (common_parent == Qundef) {
                common_parent = parent_rule_id;
                DEBUG_PRINTF("    Setting common_parent=%s\n",
                             NIL_P(common_parent) ? "nil" : RSTRING_PTR(rb_inspect(common_parent)));
            }

            VALUE group = rb_hash_aref(selector_groups, selector);
            if (NIL_P(group)) {
                group = rb_ary_new();
                rb_hash_aset(selector_groups, selector, group);
                DEBUG_PRINTF("    Created new group for selector='%s'\n", RSTRING_PTR(selector));
            }
            rb_ary_push(group, LONG2FIX(i));
        }
        DEBUG_PRINTF("  Total selector groups: %ld\n", RHASH_SIZE(selector_groups));
    }

    // If nested CSS with multiple distinct selectors, return separate rules
    // Per W3C spec: each unique selector (parent or child) is a separate rule
    // Example: .parent { color: red; .child { color: blue; } } .other { color: green; }
    // Should return 3 rules: .parent, .parent .child, .other
    DEBUG_PRINTF("\n=== Decision point ===\n");
    DEBUG_PRINTF("  has_nesting=%d\n", has_nesting);
    DEBUG_PRINTF("  selector_groups is nil? %d\n", NIL_P(selector_groups));
    if (!NIL_P(selector_groups)) {
        DEBUG_PRINTF("  selector_groups size=%ld\n", RHASH_SIZE(selector_groups));
    }
    DEBUG_PRINTF("  Condition: has_nesting && !NIL_P(selector_groups) && RHASH_SIZE(selector_groups) > 1 = %d\n",
                 has_nesting && !NIL_P(selector_groups) && RHASH_SIZE(selector_groups) > 1);

    if (has_nesting && !NIL_P(selector_groups) && RHASH_SIZE(selector_groups) > 1) {
        DEBUG_PRINTF("  -> Taking MULTI-SELECTOR path (separate rules)\n");
        VALUE merged_sheet = rb_class_new_instance(0, NULL, cStylesheet);
        VALUE merged_rules = rb_ary_new();
        int rule_id_counter = 0;

        // Iterate through each selector group
        VALUE selectors = rb_funcall(selector_groups, rb_intern("keys"), 0);
        long num_selectors = RARRAY_LEN(selectors);

        for (long s = 0; s < num_selectors; s++) {
            VALUE selector = rb_ary_entry(selectors, s);
            VALUE group_indices = rb_hash_aref(selector_groups, selector);

            // For now, just take first rule from each group (no merging within group)
            // TODO: Merge declarations within same-selector group
            long first_idx = FIX2LONG(rb_ary_entry(group_indices, 0));
            VALUE orig_rule = RARRAY_AREF(rules_array, first_idx);
            VALUE orig_decls = rb_struct_aref(orig_rule, INT2FIX(RULE_DECLARATIONS));

            // Create new rule with this selector and declarations
            VALUE new_rule = rb_struct_new(cRule,
                INT2FIX(rule_id_counter++),
                selector,
                orig_decls,
                Qnil,  // specificity
                Qnil,  // parent_rule_id
                Qnil   // nesting_style
            );
            rb_ary_push(merged_rules, new_rule);
        }

        rb_ivar_set(merged_sheet, id_ivar_rules, merged_rules);

        // Set @media_index with :all pointing to all rule IDs
        VALUE media_idx = rb_hash_new();
        VALUE all_ids = rb_ary_new();
        for (int i = 0; i < rule_id_counter; i++) {
            rb_ary_push(all_ids, INT2FIX(i));
        }
        rb_hash_aset(media_idx, ID2SYM(id_all), all_ids);
        rb_ivar_set(merged_sheet, id_ivar_media_index, media_idx);

        return merged_sheet;
    }

    // Single-merge path: merge all rules into one
    VALUE properties_hash = rb_hash_new();

    // Track selector for rollup (minimize allocations)
    // Store pointer + length to first non-parent selector
    // Also keep the VALUE alive since we extract C pointer before allocations
    const char *first_selector_ptr = NULL;
    long first_selector_len = 0;
    VALUE first_selector_value = Qnil;
    int all_same_selector = 1;

    // Iterate through each rule
    for (long i = 0; i < num_rules; i++) {
        VALUE rule = RARRAY_AREF(rules_array, i);
        Check_Type(rule, T_STRUCT);

        // Extract rule fields
        VALUE rule_id = rb_struct_aref(rule, INT2FIX(RULE_ID));
        VALUE selector = rb_struct_aref(rule, INT2FIX(RULE_SELECTOR));
        VALUE declarations = rb_struct_aref(rule, INT2FIX(RULE_DECLARATIONS));

        // Skip parent rules when handling nested CSS
        // Example: .button { color: black; &:hover { color: red; } }
        //   - Rule id=0, selector=".button", declarations=[color: black] (SKIP - has children)
        //   - Rule id=1, selector=".button:hover", declarations=[color: red] (PROCESS)
        if (has_nesting && !NIL_P(parent_ids)) {
            VALUE is_parent = rb_hash_aref(parent_ids, rule_id);
            if (RTEST(is_parent)) {
                continue;
            }
        }

        long num_decls = RARRAY_LEN(declarations);
        // Skip rules with no declarations (empty parent containers)
        if (num_decls == 0) {
            continue;
        }

        // Track selectors for rollup (delay allocation)
        const char *sel_ptr = RSTRING_PTR(selector);
        long sel_len = RSTRING_LEN(selector);
        if (first_selector_ptr == NULL) {
            first_selector_ptr = sel_ptr;
            first_selector_len = sel_len;
            first_selector_value = selector;  // Keep VALUE alive for RB_GC_GUARD
        } else if (all_same_selector) {
            if (sel_len != first_selector_len || memcmp(sel_ptr, first_selector_ptr, sel_len) != 0) {
                all_same_selector = 0;
            }
        }

        VALUE specificity_val = rb_struct_aref(rule, INT2FIX(RULE_SPECIFICITY));

        // Calculate specificity if not provided (lazy)
        int specificity = 0;
        if (NIL_P(specificity_val)) {
            specificity_val = calculate_specificity(Qnil, selector);
            // Cache the calculated value back to the struct
            rb_struct_aset(rule, INT2FIX(RULE_SPECIFICITY), specificity_val);
        }
        specificity = NUM2INT(specificity_val);

        // Process each declaration in this rule
        Check_Type(declarations, T_ARRAY);

        for (long j = 0; j < num_decls; j++) {
            VALUE decl = RARRAY_AREF(declarations, j);

            // Extract property, value, important from Declaration struct
            VALUE property = rb_struct_aref(decl, INT2FIX(DECL_PROPERTY));
            VALUE value = rb_struct_aref(decl, INT2FIX(DECL_VALUE));
            VALUE important = rb_struct_aref(decl, INT2FIX(DECL_IMPORTANT));

            // Properties are already lowercased during parsing (see cataract_new.c)
            // No need to lowercase again
            int is_important = RTEST(important);

            // Expand shorthand properties if needed
            // Most properties are NOT shorthands, so hint compiler accordingly
            const char *prop_str = StringValueCStr(property);
            VALUE expanded = Qnil;

            if (strcmp(prop_str, "margin") == 0) {
                expanded = cataract_expand_margin(Qnil, value);
            } else if (strcmp(prop_str, "padding") == 0) {
                expanded = cataract_expand_padding(Qnil, value);
            } else if (strcmp(prop_str, "border") == 0) {
                expanded = cataract_expand_border(Qnil, value);
            } else if (strcmp(prop_str, "border-color") == 0) {
                expanded = cataract_expand_border_color(Qnil, value);
            } else if (strcmp(prop_str, "border-style") == 0) {
                expanded = cataract_expand_border_style(Qnil, value);
            } else if (strcmp(prop_str, "border-width") == 0) {
                expanded = cataract_expand_border_width(Qnil, value);
            } else if (strcmp(prop_str, "border-top") == 0) {
                expanded = cataract_expand_border_side(Qnil, USASCII_STR("top"), value);
            } else if (strcmp(prop_str, "border-right") == 0) {
                expanded = cataract_expand_border_side(Qnil, USASCII_STR("right"), value);
            } else if (strcmp(prop_str, "border-bottom") == 0) {
                expanded = cataract_expand_border_side(Qnil, USASCII_STR("bottom"), value);
            } else if (strcmp(prop_str, "border-left") == 0) {
                expanded = cataract_expand_border_side(Qnil, USASCII_STR("left"), value);
            } else if (strcmp(prop_str, "font") == 0) {
                expanded = cataract_expand_font(Qnil, value);
            } else if (strcmp(prop_str, "list-style") == 0) {
                expanded = cataract_expand_list_style(Qnil, value);
            } else if (strcmp(prop_str, "background") == 0) {
                expanded = cataract_expand_background(Qnil, value);
            }

            // If property was expanded, iterate and apply cascade using rb_hash_foreach
            // Expansion is rare (most properties are not shorthands)
            if (!NIL_P(expanded)) {
                Check_Type(expanded, T_HASH);

                struct expand_context ctx;
                ctx.properties_hash = properties_hash;
                ctx.specificity = specificity;
                ctx.important = important;

                rb_hash_foreach(expanded, merge_expanded_callback, (VALUE)&ctx);

                RB_GC_GUARD(expanded);
                continue; // Skip processing the original shorthand property
            }

            // Apply CSS cascade rules
            VALUE existing = rb_hash_aref(properties_hash, property);

            // In merge scenarios, properties often collide (same property in multiple rules)
            // so existing property is the common case
            if (NIL_P(existing)) {
                // New property - add it
                VALUE prop_data = rb_hash_new();
                rb_hash_aset(prop_data, ID2SYM(id_value), value);
                rb_hash_aset(prop_data, ID2SYM(id_specificity), INT2NUM(specificity));
                rb_hash_aset(prop_data, ID2SYM(id_important), important);
                // Note: declaration_struct not stored - use global cDeclaration instead
                rb_hash_aset(properties_hash, property, prop_data);
            } else {
                // Property exists - check cascade rules
                VALUE existing_spec = rb_hash_aref(existing, ID2SYM(id_specificity));
                VALUE existing_important = rb_hash_aref(existing, ID2SYM(id_important));

                int existing_spec_int = NUM2INT(existing_spec);
                int existing_is_important = RTEST(existing_important);

                int should_replace = 0;

                // Most declarations are NOT !important
                if (is_important) {
                    // New is !important - wins if existing is NOT important OR equal/higher specificity
                    if (!existing_is_important || existing_spec_int <= specificity) {
                        should_replace = 1;
                    }
                } else {
                    // New is NOT important - only wins if existing is also NOT important AND equal/higher specificity
                    if (!existing_is_important && existing_spec_int <= specificity) {
                        should_replace = 1;
                    }
                }

                // Replacement is common in merge scenarios
                if (should_replace) {
                    rb_hash_aset(existing, ID2SYM(id_value), value);
                    rb_hash_aset(existing, ID2SYM(id_specificity), INT2NUM(specificity));
                    rb_hash_aset(existing, ID2SYM(id_important), important);
                }
            }

            RB_GC_GUARD(property);
            RB_GC_GUARD(value);
            RB_GC_GUARD(decl);
        }

        RB_GC_GUARD(selector);
        RB_GC_GUARD(declarations);
        RB_GC_GUARD(rule);
    }

    // Create shorthand from longhand properties
    // Uses cached static strings to avoid runtime allocation

    // Try to create margin shorthand
    TRY_CREATE_FOUR_SIDED_SHORTHAND(properties_hash,
        str_margin_top, str_margin_right, str_margin_bottom, str_margin_left,
        str_margin, cataract_create_margin_shorthand);

    // Try to create padding shorthand
    TRY_CREATE_FOUR_SIDED_SHORTHAND(properties_hash,
        str_padding_top, str_padding_right, str_padding_bottom, str_padding_left,
        str_padding, cataract_create_padding_shorthand);

    // Create border-width from individual sides
    TRY_CREATE_FOUR_SIDED_SHORTHAND(properties_hash,
        str_border_top_width, str_border_right_width, str_border_bottom_width, str_border_left_width,
        str_border_width, cataract_create_border_width_shorthand);

    // Create border-style from individual sides
    TRY_CREATE_FOUR_SIDED_SHORTHAND(properties_hash,
        str_border_top_style, str_border_right_style, str_border_bottom_style, str_border_left_style,
        str_border_style, cataract_create_border_style_shorthand);

    // Create border-color from individual sides
    TRY_CREATE_FOUR_SIDED_SHORTHAND(properties_hash,
        str_border_top_color, str_border_right_color, str_border_bottom_color, str_border_left_color,
        str_border_color, cataract_create_border_color_shorthand);

    // Now create border shorthand from border-{width,style,color}
    VALUE border_width = GET_PROP_VALUE_STR(properties_hash, str_border_width);
    VALUE border_style = GET_PROP_VALUE_STR(properties_hash, str_border_style);
    VALUE border_color = GET_PROP_VALUE_STR(properties_hash, str_border_color);

    if (!NIL_P(border_width) || !NIL_P(border_style) || !NIL_P(border_color)) {
        // Use first available property's metadata as reference
        VALUE border_data_src = !NIL_P(border_width) ? GET_PROP_DATA_STR(properties_hash, str_border_width) :
                                !NIL_P(border_style) ? GET_PROP_DATA_STR(properties_hash, str_border_style) :
                                GET_PROP_DATA_STR(properties_hash, str_border_color);
        VALUE border_important = rb_hash_aref(border_data_src, ID2SYM(id_important));
        int border_is_important = RTEST(border_important);

        // Check that all present properties have the same !important flag
        int important_match = CHECK_IMPORTANT_MATCH(properties_hash, str_border_width, border_is_important) &&
                             CHECK_IMPORTANT_MATCH(properties_hash, str_border_style, border_is_important) &&
                             CHECK_IMPORTANT_MATCH(properties_hash, str_border_color, border_is_important);

        if (important_match) {
            VALUE border_props = rb_hash_new();
            if (!NIL_P(border_width)) rb_hash_aset(border_props, str_border_width, border_width);
            if (!NIL_P(border_style)) rb_hash_aset(border_props, str_border_style, border_style);
            if (!NIL_P(border_color)) rb_hash_aset(border_props, str_border_color, border_color);

            VALUE border_shorthand = cataract_create_border_shorthand(Qnil, border_props);
            if (!NIL_P(border_shorthand)) {
                int border_spec = NUM2INT(rb_hash_aref(border_data_src, ID2SYM(id_specificity)));

                VALUE border_data = rb_hash_new();
                rb_hash_aset(border_data, ID2SYM(id_value), border_shorthand);
                rb_hash_aset(border_data, ID2SYM(id_specificity), INT2NUM(border_spec));
                rb_hash_aset(border_data, ID2SYM(id_important), border_important);
                rb_hash_aset(properties_hash, str_border, border_data);

                if (!NIL_P(border_width)) rb_hash_delete(properties_hash, str_border_width);
                if (!NIL_P(border_style)) rb_hash_delete(properties_hash, str_border_style);
                if (!NIL_P(border_color)) rb_hash_delete(properties_hash, str_border_color);
            }
            RB_GC_GUARD(border_props);
            RB_GC_GUARD(border_shorthand);
        }
    }

    // Try to create font shorthand
    VALUE font_size = GET_PROP_VALUE_STR(properties_hash, str_font_size);
    VALUE font_family = GET_PROP_VALUE_STR(properties_hash, str_font_family);

    // Font shorthand requires at least font-size and font-family
    if (!NIL_P(font_size) && !NIL_P(font_family)) {
        VALUE font_style = GET_PROP_VALUE_STR(properties_hash, str_font_style);
        VALUE font_variant = GET_PROP_VALUE_STR(properties_hash, str_font_variant);
        VALUE font_weight = GET_PROP_VALUE_STR(properties_hash, str_font_weight);
        VALUE line_height = GET_PROP_VALUE_STR(properties_hash, str_line_height);

        // Get metadata from font-size as reference
        VALUE size_data = GET_PROP_DATA_STR(properties_hash, str_font_size);
        VALUE font_important = rb_hash_aref(size_data, ID2SYM(id_important));
        int font_is_important = RTEST(font_important);

        // Check that all present properties have the same !important flag
        int important_match = CHECK_IMPORTANT_MATCH(properties_hash, str_font_style, font_is_important) &&
                             CHECK_IMPORTANT_MATCH(properties_hash, str_font_variant, font_is_important) &&
                             CHECK_IMPORTANT_MATCH(properties_hash, str_font_weight, font_is_important) &&
                             CHECK_IMPORTANT_MATCH(properties_hash, str_line_height, font_is_important) &&
                             CHECK_IMPORTANT_MATCH(properties_hash, str_font_family, font_is_important);

        if (important_match) {
            VALUE font_props = rb_hash_new();
            if (!NIL_P(font_style)) rb_hash_aset(font_props, str_font_style, font_style);
            if (!NIL_P(font_variant)) rb_hash_aset(font_props, str_font_variant, font_variant);
            if (!NIL_P(font_weight)) rb_hash_aset(font_props, str_font_weight, font_weight);
            rb_hash_aset(font_props, str_font_size, font_size);
            if (!NIL_P(line_height)) rb_hash_aset(font_props, str_line_height, line_height);
            rb_hash_aset(font_props, str_font_family, font_family);

            VALUE font_shorthand = cataract_create_font_shorthand(Qnil, font_props);
            if (!NIL_P(font_shorthand)) {
                int font_spec = NUM2INT(rb_hash_aref(size_data, ID2SYM(id_specificity)));

                VALUE font_data = rb_hash_new();
                rb_hash_aset(font_data, ID2SYM(id_value), font_shorthand);
                rb_hash_aset(font_data, ID2SYM(id_specificity), INT2NUM(font_spec));
                rb_hash_aset(font_data, ID2SYM(id_important), font_important);
                rb_hash_aset(properties_hash, str_font, font_data);

                // Remove longhand properties
                if (!NIL_P(font_style)) rb_hash_delete(properties_hash, str_font_style);
                if (!NIL_P(font_variant)) rb_hash_delete(properties_hash, str_font_variant);
                if (!NIL_P(font_weight)) rb_hash_delete(properties_hash, str_font_weight);
                rb_hash_delete(properties_hash, str_font_size);
                if (!NIL_P(line_height)) rb_hash_delete(properties_hash, str_line_height);
                rb_hash_delete(properties_hash, str_font_family);
            }
            RB_GC_GUARD(font_props);
            RB_GC_GUARD(font_shorthand);
        }
    }

    // Try to create list-style shorthand
    VALUE list_style_type = GET_PROP_VALUE_STR(properties_hash, str_list_style_type);
    VALUE list_style_position = GET_PROP_VALUE_STR(properties_hash, str_list_style_position);
    VALUE list_style_image = GET_PROP_VALUE_STR(properties_hash, str_list_style_image);

    // List-style shorthand requires at least 2 properties
    int list_style_count = (!NIL_P(list_style_type) ? 1 : 0) +
                           (!NIL_P(list_style_position) ? 1 : 0) +
                           (!NIL_P(list_style_image) ? 1 : 0);

    if (list_style_count >= 2) {
        // Use first available property's metadata as reference
        VALUE list_style_data_src = !NIL_P(list_style_type) ? GET_PROP_DATA_STR(properties_hash, str_list_style_type) :
                                    !NIL_P(list_style_position) ? GET_PROP_DATA_STR(properties_hash, str_list_style_position) :
                                    GET_PROP_DATA_STR(properties_hash, str_list_style_image);
        VALUE list_style_important = rb_hash_aref(list_style_data_src, ID2SYM(id_important));
        int list_style_is_important = RTEST(list_style_important);

        // Check that all present properties have the same !important flag
        int important_match = CHECK_IMPORTANT_MATCH(properties_hash, str_list_style_type, list_style_is_important) &&
                             CHECK_IMPORTANT_MATCH(properties_hash, str_list_style_position, list_style_is_important) &&
                             CHECK_IMPORTANT_MATCH(properties_hash, str_list_style_image, list_style_is_important);

        if (important_match) {
            VALUE list_style_props = rb_hash_new();
            if (!NIL_P(list_style_type)) rb_hash_aset(list_style_props, str_list_style_type, list_style_type);
            if (!NIL_P(list_style_position)) rb_hash_aset(list_style_props, str_list_style_position, list_style_position);
            if (!NIL_P(list_style_image)) rb_hash_aset(list_style_props, str_list_style_image, list_style_image);

            VALUE list_style_shorthand = cataract_create_list_style_shorthand(Qnil, list_style_props);
            if (!NIL_P(list_style_shorthand)) {
                int list_style_spec = NUM2INT(rb_hash_aref(list_style_data_src, ID2SYM(id_specificity)));

                VALUE list_style_data = rb_hash_new();
                rb_hash_aset(list_style_data, ID2SYM(id_value), list_style_shorthand);
                rb_hash_aset(list_style_data, ID2SYM(id_specificity), INT2NUM(list_style_spec));
                rb_hash_aset(list_style_data, ID2SYM(id_important), list_style_important);
                rb_hash_aset(properties_hash, str_list_style, list_style_data);

                // Remove longhand properties
                if (!NIL_P(list_style_type)) rb_hash_delete(properties_hash, str_list_style_type);
                if (!NIL_P(list_style_position)) rb_hash_delete(properties_hash, str_list_style_position);
                if (!NIL_P(list_style_image)) rb_hash_delete(properties_hash, str_list_style_image);
            }
            RB_GC_GUARD(list_style_props);
            RB_GC_GUARD(list_style_shorthand);
        }
    }

    // Try to create background shorthand
    VALUE background_color = GET_PROP_VALUE_STR(properties_hash, str_background_color);
    VALUE background_image = GET_PROP_VALUE_STR(properties_hash, str_background_image);
    VALUE background_repeat = GET_PROP_VALUE_STR(properties_hash, str_background_repeat);
    VALUE background_attachment = GET_PROP_VALUE_STR(properties_hash, str_background_attachment);
    VALUE background_position = GET_PROP_VALUE_STR(properties_hash, str_background_position);

    // Background shorthand requires at least 2 properties
    int background_count = (!NIL_P(background_color) ? 1 : 0) +
                          (!NIL_P(background_image) ? 1 : 0) +
                          (!NIL_P(background_repeat) ? 1 : 0) +
                          (!NIL_P(background_attachment) ? 1 : 0) +
                          (!NIL_P(background_position) ? 1 : 0);

    if (background_count >= 2) {
        // Use first available property's metadata as reference
        VALUE background_data_src = !NIL_P(background_color) ? GET_PROP_DATA_STR(properties_hash, str_background_color) :
                                   !NIL_P(background_image) ? GET_PROP_DATA_STR(properties_hash, str_background_image) :
                                   !NIL_P(background_repeat) ? GET_PROP_DATA_STR(properties_hash, str_background_repeat) :
                                   !NIL_P(background_attachment) ? GET_PROP_DATA_STR(properties_hash, str_background_attachment) :
                                   GET_PROP_DATA_STR(properties_hash, str_background_position);
        VALUE background_important = rb_hash_aref(background_data_src, ID2SYM(id_important));
        int background_is_important = RTEST(background_important);

        // Check that all present properties have the same !important flag
        int important_match = CHECK_IMPORTANT_MATCH(properties_hash, str_background_color, background_is_important) &&
                             CHECK_IMPORTANT_MATCH(properties_hash, str_background_image, background_is_important) &&
                             CHECK_IMPORTANT_MATCH(properties_hash, str_background_repeat, background_is_important) &&
                             CHECK_IMPORTANT_MATCH(properties_hash, str_background_attachment, background_is_important) &&
                             CHECK_IMPORTANT_MATCH(properties_hash, str_background_position, background_is_important);

        if (important_match) {
            VALUE background_props = rb_hash_new();
            if (!NIL_P(background_color)) rb_hash_aset(background_props, str_background_color, background_color);
            if (!NIL_P(background_image)) rb_hash_aset(background_props, str_background_image, background_image);
            if (!NIL_P(background_repeat)) rb_hash_aset(background_props, str_background_repeat, background_repeat);
            if (!NIL_P(background_attachment)) rb_hash_aset(background_props, str_background_attachment, background_attachment);
            if (!NIL_P(background_position)) rb_hash_aset(background_props, str_background_position, background_position);

            VALUE background_shorthand = cataract_create_background_shorthand(Qnil, background_props);
            if (!NIL_P(background_shorthand)) {
                int background_spec = NUM2INT(rb_hash_aref(background_data_src, ID2SYM(id_specificity)));

                VALUE background_data = rb_hash_new();
                rb_hash_aset(background_data, ID2SYM(id_value), background_shorthand);
                rb_hash_aset(background_data, ID2SYM(id_specificity), INT2NUM(background_spec));
                rb_hash_aset(background_data, ID2SYM(id_important), background_important);
                rb_hash_aset(properties_hash, str_background, background_data);

                // Remove longhand properties
                if (!NIL_P(background_color)) rb_hash_delete(properties_hash, str_background_color);
                if (!NIL_P(background_image)) rb_hash_delete(properties_hash, str_background_image);
                if (!NIL_P(background_repeat)) rb_hash_delete(properties_hash, str_background_repeat);
                if (!NIL_P(background_attachment)) rb_hash_delete(properties_hash, str_background_attachment);
                if (!NIL_P(background_position)) rb_hash_delete(properties_hash, str_background_position);
            }
            RB_GC_GUARD(background_props);
            RB_GC_GUARD(background_shorthand);
        }
    }

    #undef GET_PROP_VALUE
    #undef GET_PROP_DATA

    // Build merged declarations array
    VALUE merged_declarations = rb_ary_new();
    rb_hash_foreach(properties_hash, merge_build_result_callback, merged_declarations);

    // Determine final selector (allocate only once at the end)
    VALUE final_selector;
    if (has_nesting && all_same_selector && first_selector_ptr != NULL) {
        // All rules have same selector - use it for rollup
        final_selector = rb_usascii_str_new(first_selector_ptr, first_selector_len);
    } else {
        // Mixed selectors or no nesting - use "merged"
        final_selector = str_merged_selector;
    }

    // Create a new Stylesheet with a single merged rule
    // Use rb_class_new_instance instead of rb_funcall for better performance
    VALUE merged_sheet = rb_class_new_instance(0, NULL, cStylesheet);

    // Create merged rule
    VALUE merged_rule = rb_struct_new(cRule,
        INT2FIX(0),              // id
        final_selector,          // selector (rolled-up or "merged")
        merged_declarations,      // declarations
        Qnil,                     // specificity (not applicable)
        Qnil,                     // parent_rule_id (not nested)
        Qnil                      // nesting_style (not nested)
    );

    // Set @rules array with single merged rule (use cached ID)
    VALUE rules_ary = rb_ary_new_from_args(1, merged_rule);
    rb_ivar_set(merged_sheet, id_ivar_rules, rules_ary);

    // Set @media_index with :all pointing to rule 0 (use cached ID)
    VALUE media_idx = rb_hash_new();
    VALUE all_ids = rb_ary_new_from_args(1, INT2FIX(0));
    rb_hash_aset(media_idx, ID2SYM(id_all), all_ids);
    rb_ivar_set(merged_sheet, id_ivar_media_index, media_idx);

    // Guard first_selector_value: C pointer extracted via RSTRING_PTR during iteration,
    // then used after many allocations (hash operations, shorthand expansions) when
    // creating final_selector with rb_usascii_str_new
    RB_GC_GUARD(first_selector_value);

    return merged_sheet;
}

.merge_rules(input) ⇒ Object

This is called from Ruby as Cataract.merge_rules



759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
# File 'ext/cataract_old/merge.c', line 759

VALUE cataract_merge_wrapper(VALUE self, VALUE input) {
    // Check if input is a hash (new structure from Stylesheet)
    if (TYPE(input) == T_HASH) {
        // Flatten hash structure to array
        VALUE rules_array = rb_ary_new();
        struct flatten_hash_ctx ctx = { rules_array };
        rb_hash_foreach(input, flatten_hash_callback, (VALUE)&ctx);

        // Call the original merge function
        VALUE result = cataract_merge(self, rules_array);

        RB_GC_GUARD(rules_array);
        return result;
    }

    // Input is already an array - call original function directly
    return cataract_merge(self, input);
}

.parse_css(css_string) ⇒ Object

Public wrapper for Ruby - starts at depth 0



63
64
65
66
67
68
# File 'lib/cataract.rb', line 63

def parse_css(css, imports: false)
  # Resolve @import statements if requested
  css = ImportResolver.resolve(css, imports) if imports

  Stylesheet.parse(css)
end

.parse_declarations(declarations_string) ⇒ Array<Declaration>

Ruby-facing wrapper for parse_declarations

Parameters:

  • declarations_string (String)

    CSS declarations like “color: red; margin: 10px”

Returns:

  • (Array<Declaration>)

    Array of parsed declaration structs



967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
# File 'ext/cataract/cataract.c', line 967

static VALUE new_parse_declarations(VALUE self, VALUE declarations_string) {
    Check_Type(declarations_string, T_STRING);

    const char *input = RSTRING_PTR(declarations_string);
    long input_len = RSTRING_LEN(declarations_string);

    // Strip outer braces and whitespace (css_parser compatibility)
    const char *start = input;
    const char *end = input + input_len;

    while (start < end && (IS_WHITESPACE(*start) || *start == '{')) start++;
    while (end > start && (IS_WHITESPACE(*(end-1)) || *(end-1) == '}')) end--;

    VALUE result = new_parse_declarations_string(start, end);

    RB_GC_GUARD(result);
    return result;
}

.parse_media_typesObject