cataract 0.2.4 → 0.3.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.
- checksums.yaml +4 -4
- data/.github/workflows/test.yml +6 -6
- data/.gitignore +17 -3
- data/.rubocop.yml +1 -0
- data/BENCHMARKS.md +24 -6
- data/CHANGELOG.md +29 -1
- data/Gemfile +2 -2
- data/examples/css_analyzer/analyzer.rb +9 -3
- data/examples/css_analyzer/analyzers/base.rb +1 -1
- data/ext/cataract/cataract.c +251 -458
- data/ext/cataract/cataract.h +127 -16
- data/ext/cataract/css_parser.c +1029 -700
- data/ext/cataract/extconf.rb +1 -2
- data/ext/cataract/flatten.c +279 -991
- data/ext/cataract/shorthand_expander.c +80 -102
- data/ext/cataract_color/color_conversion.c +1 -1
- data/lib/cataract/declarations.rb +13 -7
- data/lib/cataract/error.rb +49 -0
- data/lib/cataract/import_resolver.rb +36 -16
- data/lib/cataract/pure/byte_constants.rb +4 -1
- data/lib/cataract/pure/flatten.rb +143 -145
- data/lib/cataract/pure/parser.rb +623 -388
- data/lib/cataract/pure/serializer.rb +109 -104
- data/lib/cataract/pure/specificity.rb +112 -156
- data/lib/cataract/pure.rb +1 -6
- data/lib/cataract/stylesheet.rb +480 -410
- data/lib/cataract/version.rb +1 -1
- data/lib/cataract.rb +1 -0
- metadata +3 -13
- data/ext/cataract/import_scanner.c +0 -174
- data/ext/cataract_old/cataract.c +0 -393
- data/ext/cataract_old/cataract.h +0 -250
- data/ext/cataract_old/css_parser.c +0 -933
- data/ext/cataract_old/extconf.rb +0 -67
- data/ext/cataract_old/import_scanner.c +0 -174
- data/ext/cataract_old/merge.c +0 -776
- data/ext/cataract_old/shorthand_expander.c +0 -902
- data/ext/cataract_old/specificity.c +0 -213
- data/ext/cataract_old/stylesheet.c +0 -290
- data/ext/cataract_old/value_splitter.c +0 -116
data/ext/cataract/css_parser.c
CHANGED
|
@@ -12,6 +12,11 @@
|
|
|
12
12
|
|
|
13
13
|
#include "cataract.h"
|
|
14
14
|
#include <string.h>
|
|
15
|
+
#include <stdint.h>
|
|
16
|
+
|
|
17
|
+
// Use uint8_t for boolean flags to reduce struct size and improve cache efficiency
|
|
18
|
+
// (int is 4 bytes, uint8_t is 1 byte - saves 27 bytes across 9 flags)
|
|
19
|
+
#define BOOLEAN uint8_t
|
|
15
20
|
|
|
16
21
|
// Parser context passed through recursive calls
|
|
17
22
|
typedef struct {
|
|
@@ -26,16 +31,55 @@ typedef struct {
|
|
|
26
31
|
int media_query_id_counter; // Next MediaQuery ID (0-indexed)
|
|
27
32
|
int next_media_query_list_id; // Next media query list ID (0-indexed)
|
|
28
33
|
int media_query_count; // Safety limit for media queries
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
int depth; // Current recursion depth (safety limit)
|
|
34
|
+
BOOLEAN has_nesting; // Set to 1 if any nested rules are created
|
|
35
|
+
BOOLEAN selector_lists_enabled; // Parser option: track selector lists (1=enabled, 0=disabled)
|
|
36
|
+
BOOLEAN depth; // Current recursion depth (safety limit)
|
|
33
37
|
// URL conversion options
|
|
34
38
|
VALUE base_uri; // Base URI for resolving relative URLs (Qnil if disabled)
|
|
35
39
|
VALUE uri_resolver; // Proc to call for URL resolution (Qnil for default)
|
|
36
|
-
|
|
40
|
+
BOOLEAN absolute_paths; // Whether to convert relative URLs to absolute
|
|
41
|
+
// Parse error checking options
|
|
42
|
+
VALUE css_string; // Full CSS string for error position calculation
|
|
43
|
+
BOOLEAN check_empty_values; // Raise error on empty declaration values
|
|
44
|
+
BOOLEAN check_malformed_declarations; // Raise error on declarations without colons
|
|
45
|
+
BOOLEAN check_invalid_selectors; // Raise error on empty/malformed selectors
|
|
46
|
+
BOOLEAN check_invalid_selector_syntax; // Raise error on syntax violations (.. ## etc)
|
|
47
|
+
BOOLEAN check_malformed_at_rules; // Raise error on @media/@supports without conditions
|
|
48
|
+
BOOLEAN check_unclosed_blocks; // Raise error on missing closing braces
|
|
37
49
|
} ParserContext;
|
|
38
50
|
|
|
51
|
+
// Build a fresh ParserContext for a nested parse (e.g. @keyframes contents).
|
|
52
|
+
// The child gets its own rule/media collections, but explicitly inherits the
|
|
53
|
+
// parent's error-checking flags and URL/error-reporting fields instead of
|
|
54
|
+
// relying on zero-initialization, which previously left check_* flags
|
|
55
|
+
// disabled and css_string/base_uri NULL regardless of the parent's settings.
|
|
56
|
+
static inline ParserContext init_child_context(ParserContext *parent) {
|
|
57
|
+
ParserContext child = {0};
|
|
58
|
+
|
|
59
|
+
child.rules_array = rb_ary_new();
|
|
60
|
+
child.media_index = rb_hash_new();
|
|
61
|
+
child.selector_lists = rb_hash_new();
|
|
62
|
+
child.imports_array = rb_ary_new();
|
|
63
|
+
child.media_queries = rb_ary_new();
|
|
64
|
+
child.media_query_lists = rb_hash_new();
|
|
65
|
+
|
|
66
|
+
child.selector_lists_enabled = parent->selector_lists_enabled;
|
|
67
|
+
|
|
68
|
+
child.base_uri = parent->base_uri;
|
|
69
|
+
child.uri_resolver = parent->uri_resolver;
|
|
70
|
+
child.absolute_paths = parent->absolute_paths;
|
|
71
|
+
|
|
72
|
+
child.css_string = parent->css_string;
|
|
73
|
+
child.check_empty_values = parent->check_empty_values;
|
|
74
|
+
child.check_malformed_declarations = parent->check_malformed_declarations;
|
|
75
|
+
child.check_invalid_selectors = parent->check_invalid_selectors;
|
|
76
|
+
child.check_invalid_selector_syntax = parent->check_invalid_selector_syntax;
|
|
77
|
+
child.check_malformed_at_rules = parent->check_malformed_at_rules;
|
|
78
|
+
child.check_unclosed_blocks = parent->check_unclosed_blocks;
|
|
79
|
+
|
|
80
|
+
return child;
|
|
81
|
+
}
|
|
82
|
+
|
|
39
83
|
// Macro to skip CSS comments /* ... */
|
|
40
84
|
// Usage: SKIP_COMMENT(p, end) where p is current position, end is limit
|
|
41
85
|
// Side effect: advances p past the comment and continues to next iteration
|
|
@@ -63,6 +107,20 @@ static inline const char* find_matching_brace(const char *start, const char *end
|
|
|
63
107
|
return p;
|
|
64
108
|
}
|
|
65
109
|
|
|
110
|
+
// Find matching closing brace with strict error checking
|
|
111
|
+
// Input: start = position after opening '{', end = limit, check_unclosed = whether to raise error
|
|
112
|
+
// Returns: pointer to matching '}' (raises error if not found and check_unclosed is true)
|
|
113
|
+
static inline const char* find_matching_brace_strict(const char *start, const char *end, int check_unclosed) {
|
|
114
|
+
const char *closing_brace = find_matching_brace(start, end);
|
|
115
|
+
|
|
116
|
+
// Check if we found the closing brace
|
|
117
|
+
if (check_unclosed && closing_brace >= end) {
|
|
118
|
+
rb_raise(eParseError, "Unclosed block: missing closing brace");
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return closing_brace;
|
|
122
|
+
}
|
|
123
|
+
|
|
66
124
|
// Find matching closing paren
|
|
67
125
|
// Input: start = position after opening '(', end = limit
|
|
68
126
|
// Returns: pointer to matching ')' (or end if not found)
|
|
@@ -78,6 +136,99 @@ static inline const char* find_matching_paren(const char *start, const char *end
|
|
|
78
136
|
return p;
|
|
79
137
|
}
|
|
80
138
|
|
|
139
|
+
// Helper function to raise ParseError with automatic position calculation
|
|
140
|
+
// Does not return - raises error and exits
|
|
141
|
+
__attribute__((noreturn))
|
|
142
|
+
static void raise_parse_error_at(ParserContext *ctx, const char *error_pos, const char *message, const char *error_type) {
|
|
143
|
+
const char *css = RSTRING_PTR(ctx->css_string);
|
|
144
|
+
long pos = error_pos - css;
|
|
145
|
+
|
|
146
|
+
// Build keyword args hash
|
|
147
|
+
VALUE kwargs = rb_hash_new();
|
|
148
|
+
rb_hash_aset(kwargs, ID2SYM(rb_intern("css")), ctx->css_string);
|
|
149
|
+
rb_hash_aset(kwargs, ID2SYM(rb_intern("pos")), LONG2NUM(pos));
|
|
150
|
+
rb_hash_aset(kwargs, ID2SYM(rb_intern("type")), ID2SYM(rb_intern(error_type)));
|
|
151
|
+
|
|
152
|
+
// Create ParseError instance
|
|
153
|
+
VALUE msg_str = rb_str_new_cstr(message);
|
|
154
|
+
VALUE argv[2] = {msg_str, kwargs};
|
|
155
|
+
VALUE error = rb_funcallv_kw(eParseError, rb_intern("new"), 2, argv, RB_PASS_KEYWORDS);
|
|
156
|
+
|
|
157
|
+
// Raise the error
|
|
158
|
+
rb_exc_raise(error);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Check if a selector contains only valid CSS selector characters and sequences
|
|
162
|
+
// Returns 1 if valid, 0 if invalid
|
|
163
|
+
// Valid characters: a-z A-Z 0-9 - _ . # [ ] : * > + ~ ( ) ' " = ^ $ | \ & % / whitespace
|
|
164
|
+
static inline int is_valid_selector(const char *start, const char *end) {
|
|
165
|
+
const char *p = start;
|
|
166
|
+
while (p < end) {
|
|
167
|
+
unsigned char c = (unsigned char)*p;
|
|
168
|
+
|
|
169
|
+
// Check for invalid character sequences
|
|
170
|
+
if (p + 1 < end) {
|
|
171
|
+
// Double dot (..) is invalid
|
|
172
|
+
if (c == '.' && *(p + 1) == '.') {
|
|
173
|
+
return 0;
|
|
174
|
+
}
|
|
175
|
+
// Double hash (##) is invalid
|
|
176
|
+
if (c == '#' && *(p + 1) == '#') {
|
|
177
|
+
return 0;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Alphanumeric
|
|
182
|
+
if ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9')) {
|
|
183
|
+
p++;
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Whitespace
|
|
188
|
+
if (c == ' ' || c == '\t' || c == '\n' || c == '\r') {
|
|
189
|
+
p++;
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Valid CSS selector special characters
|
|
194
|
+
switch (c) {
|
|
195
|
+
case '-': // Hyphen (in identifiers, attribute selectors)
|
|
196
|
+
case '_': // Underscore (in identifiers)
|
|
197
|
+
case '.': // Class selector
|
|
198
|
+
case '#': // ID selector
|
|
199
|
+
case '[': // Attribute selector start
|
|
200
|
+
case ']': // Attribute selector end
|
|
201
|
+
case ':': // Pseudo-class/element (:: is valid for pseudo-elements)
|
|
202
|
+
case '*': // Universal selector, attribute operator
|
|
203
|
+
case '>': // Child combinator
|
|
204
|
+
case '+': // Adjacent sibling combinator
|
|
205
|
+
case '~': // General sibling combinator
|
|
206
|
+
case '(': // Pseudo-class function
|
|
207
|
+
case ')': // Pseudo-class function end
|
|
208
|
+
case '\'': // String in attribute selector
|
|
209
|
+
case '"': // String in attribute selector
|
|
210
|
+
case '=': // Attribute operator
|
|
211
|
+
case '^': // Attribute operator ^=
|
|
212
|
+
case '$': // Attribute operator $=
|
|
213
|
+
case '|': // Attribute operator |=, namespace separator
|
|
214
|
+
case '\\': // Escape character
|
|
215
|
+
case '&': // Nesting selector
|
|
216
|
+
case '%': // Sometimes used in selectors
|
|
217
|
+
case '/': // Sometimes used in selectors
|
|
218
|
+
case '!': // Negation (though rare)
|
|
219
|
+
case ',': // List separator (shouldn't be here after splitting, but allow it)
|
|
220
|
+
p++;
|
|
221
|
+
break;
|
|
222
|
+
|
|
223
|
+
default:
|
|
224
|
+
// Invalid character found
|
|
225
|
+
return 0;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
return 1;
|
|
230
|
+
}
|
|
231
|
+
|
|
81
232
|
// Lowercase property name (CSS property names are ASCII-only)
|
|
82
233
|
// Non-static so merge_new.c can use it
|
|
83
234
|
VALUE lowercase_property(VALUE property_str) {
|
|
@@ -177,7 +328,7 @@ static VALUE resolve_nested_selector(VALUE parent_selector, const char *nested_s
|
|
|
177
328
|
long parent_len = RSTRING_LEN(parent_selector);
|
|
178
329
|
|
|
179
330
|
// Check if nested selector contains &
|
|
180
|
-
|
|
331
|
+
BOOLEAN has_ampersand = 0;
|
|
181
332
|
for (long i = 0; i < nested_len; i++) {
|
|
182
333
|
if (nested_sel[i] == '&') {
|
|
183
334
|
has_ampersand = 1;
|
|
@@ -598,79 +749,94 @@ static VALUE parse_declarations(const char *start, const char *end, ParserContex
|
|
|
598
749
|
}
|
|
599
750
|
if (pos >= end) break;
|
|
600
751
|
|
|
601
|
-
//
|
|
602
|
-
//
|
|
603
|
-
//
|
|
752
|
+
// A property name can never legitimately contain '{' - its presence
|
|
753
|
+
// means this is actually an unsupported/invalid nested selector (e.g.
|
|
754
|
+
// a bare type selector without '&') that has_nested_selectors() didn't
|
|
755
|
+
// recognize as nesting. Treating it as malformed here (instead of
|
|
756
|
+
// scanning through the '{' looking for a colon) keeps its matching
|
|
757
|
+
// '}' from being silently swallowed later, which was corrupting
|
|
758
|
+
// output with unbalanced braces. So stop_prop_scan_early=1.
|
|
604
759
|
const char *prop_start = pos;
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
760
|
+
struct declaration_span span;
|
|
761
|
+
if (!parse_one_declaration(&pos, end, 1, &span)) {
|
|
762
|
+
// Malformed declaration - skip to next semicolon to recover
|
|
763
|
+
if (ctx->check_malformed_declarations) {
|
|
764
|
+
// Extract property text for error message
|
|
765
|
+
const char *prop_text_end = pos;
|
|
766
|
+
trim_trailing(prop_start, &prop_text_end);
|
|
767
|
+
long prop_text_len = prop_text_end - prop_start;
|
|
768
|
+
|
|
769
|
+
const char *css = RSTRING_PTR(ctx->css_string);
|
|
770
|
+
long error_pos = prop_start - css;
|
|
771
|
+
|
|
772
|
+
if (prop_text_len == 0) {
|
|
773
|
+
// Build keyword args hash
|
|
774
|
+
VALUE kwargs = rb_hash_new();
|
|
775
|
+
rb_hash_aset(kwargs, ID2SYM(rb_intern("css")), ctx->css_string);
|
|
776
|
+
rb_hash_aset(kwargs, ID2SYM(rb_intern("pos")), LONG2NUM(error_pos));
|
|
777
|
+
rb_hash_aset(kwargs, ID2SYM(rb_intern("type")), ID2SYM(rb_intern("malformed_declaration")));
|
|
778
|
+
|
|
779
|
+
VALUE msg_str = rb_str_new_cstr("Malformed declaration: missing property name");
|
|
780
|
+
VALUE argv[2] = {msg_str, kwargs};
|
|
781
|
+
VALUE error = rb_funcallv_kw(eParseError, rb_intern("new"), 2, argv, RB_PASS_KEYWORDS);
|
|
782
|
+
rb_exc_raise(error);
|
|
783
|
+
} else {
|
|
784
|
+
// Limit property name to 200 chars in error message
|
|
785
|
+
int display_len = (prop_text_len > 200) ? 200 : (int)prop_text_len;
|
|
786
|
+
char error_msg[256];
|
|
787
|
+
snprintf(error_msg, sizeof(error_msg),
|
|
788
|
+
"Malformed declaration: missing colon after '%.*s'",
|
|
789
|
+
display_len, prop_start);
|
|
790
|
+
|
|
791
|
+
// Build keyword args hash
|
|
792
|
+
VALUE kwargs = rb_hash_new();
|
|
793
|
+
rb_hash_aset(kwargs, ID2SYM(rb_intern("css")), ctx->css_string);
|
|
794
|
+
rb_hash_aset(kwargs, ID2SYM(rb_intern("pos")), LONG2NUM(error_pos));
|
|
795
|
+
rb_hash_aset(kwargs, ID2SYM(rb_intern("type")), ID2SYM(rb_intern("malformed_declaration")));
|
|
796
|
+
|
|
797
|
+
VALUE msg_str = rb_str_new_cstr(error_msg);
|
|
798
|
+
VALUE argv[2] = {msg_str, kwargs};
|
|
799
|
+
VALUE error = rb_funcallv_kw(eParseError, rb_intern("new"), 2, argv, RB_PASS_KEYWORDS);
|
|
800
|
+
rb_exc_raise(error);
|
|
801
|
+
}
|
|
802
|
+
}
|
|
609
803
|
while (pos < end && *pos != ';') pos++;
|
|
610
804
|
if (pos < end) pos++; // Skip the semicolon
|
|
611
805
|
continue;
|
|
612
806
|
}
|
|
613
807
|
|
|
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
|
-
pos++;
|
|
641
|
-
}
|
|
642
|
-
const char *val_end = pos;
|
|
643
|
-
|
|
644
|
-
// Trim trailing whitespace from value
|
|
645
|
-
trim_trailing(val_start, &val_end);
|
|
646
|
-
|
|
647
|
-
// Check for !important
|
|
648
|
-
int is_important = 0;
|
|
649
|
-
if (val_end - val_start >= 10) { // strlen("!important") = 10
|
|
650
|
-
const char *check = val_end - 10;
|
|
651
|
-
while (check < val_end && IS_WHITESPACE(*check)) check++;
|
|
652
|
-
if (check < val_end && *check == '!') {
|
|
653
|
-
check++;
|
|
654
|
-
while (check < val_end && IS_WHITESPACE(*check)) check++;
|
|
655
|
-
// strncmp safely handles remaining length check
|
|
656
|
-
if (check + 9 <= val_end && strncmp(check, "important", 9) == 0) {
|
|
657
|
-
is_important = 1;
|
|
658
|
-
const char *important_pos = check - 1;
|
|
659
|
-
while (important_pos > val_start && (IS_WHITESPACE(*(important_pos-1)) || *(important_pos-1) == '!')) {
|
|
660
|
-
important_pos--;
|
|
661
|
-
}
|
|
662
|
-
val_end = important_pos;
|
|
663
|
-
}
|
|
664
|
-
}
|
|
808
|
+
// Check for empty value
|
|
809
|
+
if (span.val_end <= span.val_start && ctx->check_empty_values) {
|
|
810
|
+
long prop_len = span.prop_end - span.prop_start;
|
|
811
|
+
const char *css = RSTRING_PTR(ctx->css_string);
|
|
812
|
+
long error_pos = span.val_start - css;
|
|
813
|
+
|
|
814
|
+
// Build error message
|
|
815
|
+
int display_len = (prop_len > 200) ? 200 : (int)prop_len;
|
|
816
|
+
char error_msg[256];
|
|
817
|
+
snprintf(error_msg, sizeof(error_msg),
|
|
818
|
+
"Empty value for property '%.*s'",
|
|
819
|
+
display_len, span.prop_start);
|
|
820
|
+
|
|
821
|
+
// Build keyword args hash
|
|
822
|
+
VALUE kwargs = rb_hash_new();
|
|
823
|
+
rb_hash_aset(kwargs, ID2SYM(rb_intern("css")), ctx->css_string);
|
|
824
|
+
rb_hash_aset(kwargs, ID2SYM(rb_intern("pos")), LONG2NUM(error_pos));
|
|
825
|
+
rb_hash_aset(kwargs, ID2SYM(rb_intern("type")), ID2SYM(rb_intern("empty_value")));
|
|
826
|
+
|
|
827
|
+
// Create ParseError instance: ParseError.new(message, **kwargs)
|
|
828
|
+
VALUE msg_str = rb_str_new_cstr(error_msg);
|
|
829
|
+
VALUE argv[2] = {msg_str, kwargs};
|
|
830
|
+
VALUE error = rb_funcallv_kw(eParseError, rb_intern("new"), 2, argv, RB_PASS_KEYWORDS);
|
|
831
|
+
|
|
832
|
+
// Raise the error
|
|
833
|
+
rb_exc_raise(error);
|
|
665
834
|
}
|
|
666
835
|
|
|
667
|
-
// Final trim
|
|
668
|
-
trim_trailing(val_start, &val_end);
|
|
669
|
-
|
|
670
836
|
// Skip if value is empty
|
|
671
|
-
if (val_end > val_start) {
|
|
672
|
-
long prop_len = prop_end - prop_start;
|
|
673
|
-
long val_len = val_end - val_start;
|
|
837
|
+
if (span.val_end > span.val_start) {
|
|
838
|
+
long prop_len = span.prop_end - span.prop_start;
|
|
839
|
+
long val_len = span.val_end - span.val_start;
|
|
674
840
|
|
|
675
841
|
// Check property name length
|
|
676
842
|
if (prop_len > MAX_PROPERTY_NAME_LENGTH) {
|
|
@@ -687,18 +853,18 @@ static VALUE parse_declarations(const char *start, const char *end, ParserContex
|
|
|
687
853
|
}
|
|
688
854
|
|
|
689
855
|
// Create property string - use UTF-8 to support custom properties with Unicode
|
|
690
|
-
VALUE property = rb_utf8_str_new(prop_start, prop_len);
|
|
856
|
+
VALUE property = rb_utf8_str_new(span.prop_start, prop_len);
|
|
691
857
|
// Custom properties (--foo) are case-sensitive and can contain Unicode
|
|
692
858
|
// Regular properties are ASCII-only and case-insensitive
|
|
693
|
-
if (!(prop_len >= 2 && prop_start[0] == '-' && prop_start[1] == '-')) {
|
|
859
|
+
if (!(prop_len >= 2 && span.prop_start[0] == '-' && span.prop_start[1] == '-')) {
|
|
694
860
|
// Regular property: force ASCII encoding and lowercase
|
|
695
861
|
rb_enc_associate(property, rb_usascii_encoding());
|
|
696
862
|
property = lowercase_property(property);
|
|
697
863
|
}
|
|
698
|
-
VALUE value = rb_utf8_str_new(val_start, val_len);
|
|
864
|
+
VALUE value = rb_utf8_str_new(span.val_start, val_len);
|
|
699
865
|
|
|
700
866
|
// Convert relative URLs to absolute if enabled
|
|
701
|
-
if (ctx
|
|
867
|
+
if (ctx->absolute_paths && !NIL_P(ctx->base_uri)) {
|
|
702
868
|
value = convert_urls_in_value(value, ctx->base_uri, ctx->uri_resolver);
|
|
703
869
|
}
|
|
704
870
|
|
|
@@ -706,13 +872,12 @@ static VALUE parse_declarations(const char *start, const char *end, ParserContex
|
|
|
706
872
|
VALUE decl = rb_struct_new(cDeclaration,
|
|
707
873
|
property,
|
|
708
874
|
value,
|
|
709
|
-
is_important ? Qtrue : Qfalse
|
|
875
|
+
span.is_important ? Qtrue : Qfalse
|
|
710
876
|
);
|
|
711
877
|
|
|
712
878
|
rb_ary_push(declarations, decl);
|
|
713
879
|
}
|
|
714
|
-
|
|
715
|
-
if (pos < end && *pos == ';') pos++; // Skip semicolon if present
|
|
880
|
+
// parse_one_declaration already advanced pos past the ';' on success
|
|
716
881
|
}
|
|
717
882
|
|
|
718
883
|
return declarations;
|
|
@@ -801,6 +966,54 @@ static VALUE intern_media_query_safe(ParserContext *ctx, const char *query_str,
|
|
|
801
966
|
return sym;
|
|
802
967
|
}
|
|
803
968
|
|
|
969
|
+
/*
|
|
970
|
+
* Parse a single media query (already split on commas, if applicable, and
|
|
971
|
+
* trimmed of surrounding whitespace) into a type symbol and an optional
|
|
972
|
+
* conditions string.
|
|
973
|
+
*
|
|
974
|
+
* Examples:
|
|
975
|
+
* "(min-width: 500px)" -> type=:all, conditions="(min-width: 500px)"
|
|
976
|
+
* "screen" -> type=:screen, conditions=nil
|
|
977
|
+
* "screen and (min-width: 500)" -> type=:screen, conditions="(min-width: 500)"
|
|
978
|
+
*
|
|
979
|
+
* Shared by @media block parsing (parse_mixed_block's nested-@media case and
|
|
980
|
+
* parse_css_recursive's top-level case) and @import's trailing media query,
|
|
981
|
+
* so all three agree on stripping the "and" keyword between type and
|
|
982
|
+
* conditions.
|
|
983
|
+
*/
|
|
984
|
+
static void parse_single_media_query(const char *query_start, const char *query_end,
|
|
985
|
+
VALUE *media_type_out, VALUE *media_conditions_out) {
|
|
986
|
+
const char *mq_ptr = query_start;
|
|
987
|
+
VALUE media_type;
|
|
988
|
+
VALUE media_conditions = Qnil;
|
|
989
|
+
|
|
990
|
+
if (*mq_ptr == '(') {
|
|
991
|
+
// Starts with '(' - just conditions, type defaults to :all
|
|
992
|
+
media_type = ID2SYM(rb_intern("all"));
|
|
993
|
+
media_conditions = rb_utf8_str_new(mq_ptr, query_end - mq_ptr);
|
|
994
|
+
} else {
|
|
995
|
+
// Extract media type (first word)
|
|
996
|
+
const char *type_start = mq_ptr;
|
|
997
|
+
while (mq_ptr < query_end && !IS_WHITESPACE(*mq_ptr) && *mq_ptr != '(') mq_ptr++;
|
|
998
|
+
VALUE type_str = rb_utf8_str_new(type_start, mq_ptr - type_start);
|
|
999
|
+
media_type = ID2SYM(rb_intern_str(type_str));
|
|
1000
|
+
|
|
1001
|
+
// Skip whitespace and "and" keyword if present
|
|
1002
|
+
while (mq_ptr < query_end && IS_WHITESPACE(*mq_ptr)) mq_ptr++;
|
|
1003
|
+
if (mq_ptr + 3 <= query_end && strncmp(mq_ptr, "and", 3) == 0) {
|
|
1004
|
+
mq_ptr += 3;
|
|
1005
|
+
while (mq_ptr < query_end && IS_WHITESPACE(*mq_ptr)) mq_ptr++;
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
if (mq_ptr < query_end) {
|
|
1009
|
+
media_conditions = rb_utf8_str_new(mq_ptr, query_end - mq_ptr);
|
|
1010
|
+
}
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
*media_type_out = media_type;
|
|
1014
|
+
*media_conditions_out = media_conditions;
|
|
1015
|
+
}
|
|
1016
|
+
|
|
804
1017
|
/*
|
|
805
1018
|
* Parse mixed declarations and nested selectors from a block
|
|
806
1019
|
* Used when a CSS rule block contains both declarations and nested rules
|
|
@@ -856,31 +1069,9 @@ static VALUE parse_mixed_block(ParserContext *ctx, const char *start, const char
|
|
|
856
1069
|
trim_trailing(media_query_start, &media_query_end_trimmed);
|
|
857
1070
|
|
|
858
1071
|
// Parse media query and create MediaQuery object
|
|
859
|
-
const char *mq_ptr = media_query_start;
|
|
860
1072
|
VALUE media_type;
|
|
861
|
-
VALUE media_conditions
|
|
862
|
-
|
|
863
|
-
if (*mq_ptr == '(') {
|
|
864
|
-
// Starts with '(' - just conditions, type defaults to :all
|
|
865
|
-
media_type = ID2SYM(rb_intern("all"));
|
|
866
|
-
media_conditions = rb_utf8_str_new(mq_ptr, media_query_end_trimmed - mq_ptr);
|
|
867
|
-
} else {
|
|
868
|
-
// Extract media type (first word)
|
|
869
|
-
const char *type_start = mq_ptr;
|
|
870
|
-
while (mq_ptr < media_query_end_trimmed && !IS_WHITESPACE(*mq_ptr) && *mq_ptr != '(') mq_ptr++;
|
|
871
|
-
VALUE type_str = rb_utf8_str_new(type_start, mq_ptr - type_start);
|
|
872
|
-
media_type = ID2SYM(rb_intern_str(type_str));
|
|
873
|
-
|
|
874
|
-
// Skip "and" keyword if present
|
|
875
|
-
while (mq_ptr < media_query_end_trimmed && IS_WHITESPACE(*mq_ptr)) mq_ptr++;
|
|
876
|
-
if (mq_ptr + 3 <= media_query_end_trimmed && strncmp(mq_ptr, "and", 3) == 0) {
|
|
877
|
-
mq_ptr += 3;
|
|
878
|
-
while (mq_ptr < media_query_end_trimmed && IS_WHITESPACE(*mq_ptr)) mq_ptr++;
|
|
879
|
-
}
|
|
880
|
-
if (mq_ptr < media_query_end_trimmed) {
|
|
881
|
-
media_conditions = rb_utf8_str_new(mq_ptr, media_query_end_trimmed - mq_ptr);
|
|
882
|
-
}
|
|
883
|
-
}
|
|
1073
|
+
VALUE media_conditions;
|
|
1074
|
+
parse_single_media_query(media_query_start, media_query_end_trimmed, &media_type, &media_conditions);
|
|
884
1075
|
|
|
885
1076
|
// Create MediaQuery object
|
|
886
1077
|
VALUE media_query = rb_struct_new(cMediaQuery,
|
|
@@ -896,7 +1087,7 @@ static VALUE parse_mixed_block(ParserContext *ctx, const char *start, const char
|
|
|
896
1087
|
|
|
897
1088
|
// Find matching closing brace
|
|
898
1089
|
const char *media_block_start = p;
|
|
899
|
-
const char *media_block_end =
|
|
1090
|
+
const char *media_block_end = find_matching_brace_strict(p, end, ctx->check_unclosed_blocks);
|
|
900
1091
|
p = media_block_end;
|
|
901
1092
|
|
|
902
1093
|
if (p < end) p++; // Skip }
|
|
@@ -909,7 +1100,7 @@ static VALUE parse_mixed_block(ParserContext *ctx, const char *start, const char
|
|
|
909
1100
|
|
|
910
1101
|
// This should never happen - parent_media_query_id should always be valid
|
|
911
1102
|
if (NIL_P(parent_mq)) {
|
|
912
|
-
rb_raise(
|
|
1103
|
+
rb_raise(eParseError,
|
|
913
1104
|
"Invalid parent_media_query_id: %d (not found in media_queries array)",
|
|
914
1105
|
parent_media_query_id);
|
|
915
1106
|
}
|
|
@@ -1022,7 +1213,7 @@ static VALUE parse_mixed_block(ParserContext *ctx, const char *start, const char
|
|
|
1022
1213
|
// Example: "& .child { font: 14px; }"
|
|
1023
1214
|
// ^nested_block_start ^nested_block_end (at })
|
|
1024
1215
|
const char *nested_block_start = p;
|
|
1025
|
-
const char *nested_block_end =
|
|
1216
|
+
const char *nested_block_end = find_matching_brace_strict(p, end, ctx->check_unclosed_blocks);
|
|
1026
1217
|
p = nested_block_end;
|
|
1027
1218
|
|
|
1028
1219
|
if (p < end) p++; // Skip }
|
|
@@ -1095,49 +1286,18 @@ static VALUE parse_mixed_block(ParserContext *ctx, const char *start, const char
|
|
|
1095
1286
|
}
|
|
1096
1287
|
|
|
1097
1288
|
// This is a declaration - parse it
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
if (p >= end || *p != ':') {
|
|
1289
|
+
struct declaration_span span;
|
|
1290
|
+
if (!parse_one_declaration(&p, end, 1, &span)) {
|
|
1101
1291
|
// Malformed - skip to semicolon
|
|
1102
1292
|
while (p < end && *p != ';') p++;
|
|
1103
1293
|
if (p < end) p++;
|
|
1104
1294
|
continue;
|
|
1105
1295
|
}
|
|
1106
1296
|
|
|
1107
|
-
const char *prop_end = p;
|
|
1108
|
-
trim_trailing(prop_start, &prop_end);
|
|
1109
|
-
|
|
1110
|
-
p++; // Skip :
|
|
1111
|
-
trim_leading(&p, end);
|
|
1112
|
-
|
|
1113
|
-
const char *val_start = p;
|
|
1114
|
-
int important = 0;
|
|
1115
|
-
|
|
1116
|
-
// Find end of value (semicolon or closing brace or end)
|
|
1117
|
-
while (p < end && *p != ';' && *p != '}') p++;
|
|
1118
|
-
const char *val_end = p;
|
|
1119
|
-
|
|
1120
|
-
// Check for !important
|
|
1121
|
-
const char *important_check = val_end - 10; // " !important"
|
|
1122
|
-
if (important_check >= val_start) {
|
|
1123
|
-
trim_trailing(val_start, &val_end);
|
|
1124
|
-
if (val_end - val_start >= 10) {
|
|
1125
|
-
if (strncmp(val_end - 10, "!important", 10) == 0) {
|
|
1126
|
-
important = 1;
|
|
1127
|
-
val_end -= 10;
|
|
1128
|
-
trim_trailing(val_start, &val_end);
|
|
1129
|
-
}
|
|
1130
|
-
}
|
|
1131
|
-
} else {
|
|
1132
|
-
trim_trailing(val_start, &val_end);
|
|
1133
|
-
}
|
|
1134
|
-
|
|
1135
|
-
if (p < end && *p == ';') p++;
|
|
1136
|
-
|
|
1137
1297
|
// Create declaration
|
|
1138
|
-
if (prop_end > prop_start && val_end > val_start) {
|
|
1139
|
-
long prop_len = prop_end - prop_start;
|
|
1140
|
-
long val_len = val_end - val_start;
|
|
1298
|
+
if (span.prop_end > span.prop_start && span.val_end > span.val_start) {
|
|
1299
|
+
long prop_len = span.prop_end - span.prop_start;
|
|
1300
|
+
long val_len = span.val_end - span.val_start;
|
|
1141
1301
|
|
|
1142
1302
|
// Check property name length
|
|
1143
1303
|
if (prop_len > MAX_PROPERTY_NAME_LENGTH) {
|
|
@@ -1154,15 +1314,15 @@ static VALUE parse_mixed_block(ParserContext *ctx, const char *start, const char
|
|
|
1154
1314
|
}
|
|
1155
1315
|
|
|
1156
1316
|
// Create property string - use UTF-8 to support custom properties with Unicode
|
|
1157
|
-
VALUE property = rb_utf8_str_new(prop_start, prop_len);
|
|
1317
|
+
VALUE property = rb_utf8_str_new(span.prop_start, prop_len);
|
|
1158
1318
|
// Custom properties (--foo) are case-sensitive and can contain Unicode
|
|
1159
1319
|
// Regular properties are ASCII-only and case-insensitive
|
|
1160
|
-
if (!(prop_len >= 2 && prop_start[0] == '-' && prop_start[1] == '-')) {
|
|
1320
|
+
if (!(prop_len >= 2 && span.prop_start[0] == '-' && span.prop_start[1] == '-')) {
|
|
1161
1321
|
// Regular property: force ASCII encoding and lowercase
|
|
1162
1322
|
rb_enc_associate(property, rb_usascii_encoding());
|
|
1163
1323
|
property = lowercase_property(property);
|
|
1164
1324
|
}
|
|
1165
|
-
VALUE value = rb_utf8_str_new(val_start, val_len);
|
|
1325
|
+
VALUE value = rb_utf8_str_new(span.val_start, val_len);
|
|
1166
1326
|
|
|
1167
1327
|
// Convert relative URLs to absolute if enabled
|
|
1168
1328
|
if (ctx->absolute_paths && !NIL_P(ctx->base_uri)) {
|
|
@@ -1172,7 +1332,7 @@ static VALUE parse_mixed_block(ParserContext *ctx, const char *start, const char
|
|
|
1172
1332
|
VALUE decl = rb_struct_new(cDeclaration,
|
|
1173
1333
|
property,
|
|
1174
1334
|
value,
|
|
1175
|
-
|
|
1335
|
+
span.is_important ? Qtrue : Qfalse
|
|
1176
1336
|
);
|
|
1177
1337
|
|
|
1178
1338
|
rb_ary_push(declarations, decl);
|
|
@@ -1283,29 +1443,9 @@ static void parse_import_statement(ParserContext *ctx, const char **p_ptr, const
|
|
|
1283
1443
|
|
|
1284
1444
|
if (query_start < query_end) {
|
|
1285
1445
|
// Parse this individual media query
|
|
1286
|
-
const char *mq_ptr = query_start;
|
|
1287
1446
|
VALUE media_type;
|
|
1288
|
-
VALUE media_conditions
|
|
1289
|
-
|
|
1290
|
-
if (*mq_ptr == '(') {
|
|
1291
|
-
// Starts with '(' - just conditions, type defaults to :all
|
|
1292
|
-
media_type = ID2SYM(rb_intern("all"));
|
|
1293
|
-
media_conditions = rb_utf8_str_new(mq_ptr, query_end - mq_ptr);
|
|
1294
|
-
} else {
|
|
1295
|
-
// Extract media type (first word)
|
|
1296
|
-
const char *type_start = mq_ptr;
|
|
1297
|
-
while (mq_ptr < query_end && !IS_WHITESPACE(*mq_ptr) && *mq_ptr != '(') mq_ptr++;
|
|
1298
|
-
VALUE type_str = rb_utf8_str_new(type_start, mq_ptr - type_start);
|
|
1299
|
-
media_type = ID2SYM(rb_intern_str(type_str));
|
|
1300
|
-
|
|
1301
|
-
// Skip whitespace
|
|
1302
|
-
while (mq_ptr < query_end && IS_WHITESPACE(*mq_ptr)) mq_ptr++;
|
|
1303
|
-
|
|
1304
|
-
// Check if there are conditions (rest of string)
|
|
1305
|
-
if (mq_ptr < query_end) {
|
|
1306
|
-
media_conditions = rb_utf8_str_new(mq_ptr, query_end - mq_ptr);
|
|
1307
|
-
}
|
|
1308
|
-
}
|
|
1447
|
+
VALUE media_conditions;
|
|
1448
|
+
parse_single_media_query(query_start, query_end, &media_type, &media_conditions);
|
|
1309
1449
|
|
|
1310
1450
|
// Create MediaQuery struct
|
|
1311
1451
|
VALUE media_query = rb_struct_new(cMediaQuery,
|
|
@@ -1362,6 +1502,665 @@ static void parse_import_statement(ParserContext *ctx, const char **p_ptr, const
|
|
|
1362
1502
|
RB_GC_GUARD(import_stmt);
|
|
1363
1503
|
}
|
|
1364
1504
|
|
|
1505
|
+
/*
|
|
1506
|
+
* Handle an @media at-rule found at brace_depth 0. Called with *p_ptr
|
|
1507
|
+
* pointing at the '@' of "@media"; advances *p_ptr past the entire
|
|
1508
|
+
* "@media ... { ... }" construct (including its closing '}', if found).
|
|
1509
|
+
*/
|
|
1510
|
+
static void handle_media_at_rule(ParserContext *ctx, const char **p_ptr, const char *pe,
|
|
1511
|
+
VALUE parent_media_sym, int parent_media_query_id) {
|
|
1512
|
+
const char *p = *p_ptr;
|
|
1513
|
+
p += 6; // Skip "@media"
|
|
1514
|
+
|
|
1515
|
+
// Skip whitespace
|
|
1516
|
+
while (p < pe && IS_WHITESPACE(*p)) p++;
|
|
1517
|
+
|
|
1518
|
+
// Find media query (up to opening brace)
|
|
1519
|
+
const char *mq_start = p;
|
|
1520
|
+
while (p < pe && *p != '{') p++;
|
|
1521
|
+
const char *mq_end = p;
|
|
1522
|
+
|
|
1523
|
+
// Trim
|
|
1524
|
+
trim_trailing(mq_start, &mq_end);
|
|
1525
|
+
|
|
1526
|
+
// Check for empty media query
|
|
1527
|
+
if (mq_end <= mq_start) {
|
|
1528
|
+
if (ctx->check_malformed_at_rules) {
|
|
1529
|
+
raise_parse_error_at(ctx, mq_start, "Malformed @media: missing media query", "malformed_at_rule");
|
|
1530
|
+
}
|
|
1531
|
+
|
|
1532
|
+
// Empty media query with check disabled - skip @media wrapper and parse contents as regular rules
|
|
1533
|
+
if (p >= pe || *p != '{') {
|
|
1534
|
+
*p_ptr = p;
|
|
1535
|
+
return; // Malformed structure
|
|
1536
|
+
}
|
|
1537
|
+
p++; // Skip opening {
|
|
1538
|
+
const char *block_start = p;
|
|
1539
|
+
const char *block_end = find_matching_brace_strict(p, pe, ctx->check_unclosed_blocks);
|
|
1540
|
+
p = block_end;
|
|
1541
|
+
|
|
1542
|
+
// Parse block contents with NO media query context
|
|
1543
|
+
ctx->depth++;
|
|
1544
|
+
parse_css_recursive(ctx, block_start, block_end, parent_media_sym, NO_PARENT_SELECTOR, NO_PARENT_RULE_ID, parent_media_query_id);
|
|
1545
|
+
ctx->depth--;
|
|
1546
|
+
|
|
1547
|
+
if (p < pe && *p == '}') p++;
|
|
1548
|
+
*p_ptr = p;
|
|
1549
|
+
return;
|
|
1550
|
+
}
|
|
1551
|
+
|
|
1552
|
+
if (p >= pe || *p != '{') {
|
|
1553
|
+
*p_ptr = p;
|
|
1554
|
+
return; // Malformed
|
|
1555
|
+
}
|
|
1556
|
+
|
|
1557
|
+
// Split comma-separated media queries (e.g., "screen, print" -> ["screen", "print"])
|
|
1558
|
+
// Per W3C spec, comma acts as logical OR - each query is independent
|
|
1559
|
+
VALUE media_query_ids = rb_ary_new();
|
|
1560
|
+
|
|
1561
|
+
const char *query_start = mq_start;
|
|
1562
|
+
for (const char *p_comma = mq_start; p_comma <= mq_end; p_comma++) {
|
|
1563
|
+
if (p_comma == mq_end || *p_comma == ',') {
|
|
1564
|
+
const char *query_end = p_comma;
|
|
1565
|
+
|
|
1566
|
+
// Trim whitespace from this query
|
|
1567
|
+
while (query_start < query_end && IS_WHITESPACE(*query_start)) query_start++;
|
|
1568
|
+
while (query_end > query_start && IS_WHITESPACE(*(query_end - 1))) query_end--;
|
|
1569
|
+
|
|
1570
|
+
if (query_start < query_end) {
|
|
1571
|
+
// Parse this individual media query
|
|
1572
|
+
VALUE media_type;
|
|
1573
|
+
VALUE media_conditions;
|
|
1574
|
+
parse_single_media_query(query_start, query_end, &media_type, &media_conditions);
|
|
1575
|
+
|
|
1576
|
+
// Create MediaQuery object for this query
|
|
1577
|
+
VALUE media_query = rb_struct_new(cMediaQuery,
|
|
1578
|
+
INT2FIX(ctx->media_query_id_counter),
|
|
1579
|
+
media_type,
|
|
1580
|
+
media_conditions
|
|
1581
|
+
);
|
|
1582
|
+
rb_ary_push(ctx->media_queries, media_query);
|
|
1583
|
+
rb_ary_push(media_query_ids, INT2FIX(ctx->media_query_id_counter));
|
|
1584
|
+
ctx->media_query_id_counter++;
|
|
1585
|
+
}
|
|
1586
|
+
|
|
1587
|
+
// Move to start of next query
|
|
1588
|
+
query_start = p_comma + 1;
|
|
1589
|
+
}
|
|
1590
|
+
}
|
|
1591
|
+
|
|
1592
|
+
// If multiple queries, track them as a list for serialization
|
|
1593
|
+
int media_query_list_id = -1;
|
|
1594
|
+
if (RARRAY_LEN(media_query_ids) > 1) {
|
|
1595
|
+
media_query_list_id = ctx->next_media_query_list_id;
|
|
1596
|
+
rb_hash_aset(ctx->media_query_lists, INT2FIX(media_query_list_id), media_query_ids);
|
|
1597
|
+
ctx->next_media_query_list_id++;
|
|
1598
|
+
}
|
|
1599
|
+
|
|
1600
|
+
// Use first query ID as the primary one for rules in this block
|
|
1601
|
+
int current_media_query_id = FIX2INT(rb_ary_entry(media_query_ids, 0));
|
|
1602
|
+
|
|
1603
|
+
// Handle nested @media by combining with parent
|
|
1604
|
+
if (parent_media_query_id >= 0) {
|
|
1605
|
+
VALUE parent_mq = rb_ary_entry(ctx->media_queries, parent_media_query_id);
|
|
1606
|
+
VALUE parent_type = rb_struct_aref(parent_mq, INT2FIX(1)); // type field
|
|
1607
|
+
VALUE parent_conditions = rb_struct_aref(parent_mq, INT2FIX(2)); // conditions field
|
|
1608
|
+
|
|
1609
|
+
// Get child media query (first one in the list)
|
|
1610
|
+
VALUE child_mq = rb_ary_entry(ctx->media_queries, current_media_query_id);
|
|
1611
|
+
VALUE child_conditions = rb_struct_aref(child_mq, INT2FIX(2)); // conditions field
|
|
1612
|
+
|
|
1613
|
+
// Combined type is parent's type (outermost wins, child type ignored)
|
|
1614
|
+
VALUE combined_type = parent_type;
|
|
1615
|
+
VALUE combined_conditions;
|
|
1616
|
+
|
|
1617
|
+
if (!NIL_P(parent_conditions) && !NIL_P(child_conditions)) {
|
|
1618
|
+
combined_conditions = rb_sprintf("%"PRIsVALUE" and %"PRIsVALUE, parent_conditions, child_conditions);
|
|
1619
|
+
} else if (!NIL_P(parent_conditions)) {
|
|
1620
|
+
combined_conditions = parent_conditions;
|
|
1621
|
+
} else {
|
|
1622
|
+
combined_conditions = child_conditions;
|
|
1623
|
+
}
|
|
1624
|
+
|
|
1625
|
+
VALUE combined_mq = rb_struct_new(cMediaQuery,
|
|
1626
|
+
INT2FIX(ctx->media_query_id_counter),
|
|
1627
|
+
combined_type,
|
|
1628
|
+
combined_conditions
|
|
1629
|
+
);
|
|
1630
|
+
rb_ary_push(ctx->media_queries, combined_mq);
|
|
1631
|
+
current_media_query_id = ctx->media_query_id_counter;
|
|
1632
|
+
ctx->media_query_id_counter++;
|
|
1633
|
+
}
|
|
1634
|
+
|
|
1635
|
+
// For backwards compat, also create symbol (will be removed later)
|
|
1636
|
+
VALUE child_media_sym = intern_media_query_safe(ctx, mq_start, mq_end - mq_start);
|
|
1637
|
+
VALUE combined_media_sym = combine_media_queries(parent_media_sym, child_media_sym);
|
|
1638
|
+
|
|
1639
|
+
p++; // Skip opening {
|
|
1640
|
+
|
|
1641
|
+
// Find matching closing brace
|
|
1642
|
+
const char *block_start = p;
|
|
1643
|
+
const char *block_end = find_matching_brace_strict(p, pe, ctx->check_unclosed_blocks);
|
|
1644
|
+
p = block_end;
|
|
1645
|
+
|
|
1646
|
+
// Recursively parse @media block with new media query context
|
|
1647
|
+
ctx->depth++;
|
|
1648
|
+
parse_css_recursive(ctx, block_start, block_end, combined_media_sym, NO_PARENT_SELECTOR, NO_PARENT_RULE_ID, current_media_query_id);
|
|
1649
|
+
ctx->depth--;
|
|
1650
|
+
|
|
1651
|
+
if (p < pe && *p == '}') p++;
|
|
1652
|
+
*p_ptr = p;
|
|
1653
|
+
}
|
|
1654
|
+
|
|
1655
|
+
/*
|
|
1656
|
+
* Handle a conditional-group at-rule (@supports/@layer/@container/@scope)
|
|
1657
|
+
* found at brace_depth 0. Behaves like @media but doesn't affect media
|
|
1658
|
+
* context, so it recurses with the same parent_media_sym/parent_selector/
|
|
1659
|
+
* parent_rule_id it was called with. Called with *p_ptr pointing at the
|
|
1660
|
+
* '@'; at_start/at_name_end/at_name_len describe the already-scanned
|
|
1661
|
+
* at-rule name. Advances *p_ptr past the entire construct (including its
|
|
1662
|
+
* closing '}', if found).
|
|
1663
|
+
*/
|
|
1664
|
+
static void handle_conditional_group_at_rule(ParserContext *ctx, const char **p_ptr, const char *pe,
|
|
1665
|
+
const char *at_start, const char *at_name_end, long at_name_len,
|
|
1666
|
+
VALUE parent_media_sym, VALUE parent_selector, VALUE parent_rule_id,
|
|
1667
|
+
int parent_media_query_id) {
|
|
1668
|
+
// Check if this rule requires a condition
|
|
1669
|
+
BOOLEAN requires_condition =
|
|
1670
|
+
(at_name_len == 8 && strncmp(at_start, "supports", 8) == 0) ||
|
|
1671
|
+
(at_name_len == 9 && strncmp(at_start, "container", 9) == 0);
|
|
1672
|
+
|
|
1673
|
+
// Extract condition (between at-rule name and opening brace)
|
|
1674
|
+
const char *cond_start = at_name_end;
|
|
1675
|
+
while (cond_start < pe && IS_WHITESPACE(*cond_start)) cond_start++;
|
|
1676
|
+
|
|
1677
|
+
// Skip to opening brace
|
|
1678
|
+
const char *p = at_name_end;
|
|
1679
|
+
while (p < pe && *p != '{') p++;
|
|
1680
|
+
|
|
1681
|
+
if (p >= pe || *p != '{') {
|
|
1682
|
+
*p_ptr = p;
|
|
1683
|
+
return; // Malformed
|
|
1684
|
+
}
|
|
1685
|
+
|
|
1686
|
+
// Trim condition
|
|
1687
|
+
const char *cond_end = p;
|
|
1688
|
+
while (cond_end > cond_start && IS_WHITESPACE(*(cond_end - 1))) cond_end--;
|
|
1689
|
+
|
|
1690
|
+
// Check for missing condition
|
|
1691
|
+
if (requires_condition && cond_end <= cond_start && ctx->check_malformed_at_rules) {
|
|
1692
|
+
char error_msg[100];
|
|
1693
|
+
snprintf(error_msg, sizeof(error_msg), "Malformed @%.*s: missing condition", (int)at_name_len, at_start);
|
|
1694
|
+
raise_parse_error_at(ctx, at_start - 1, error_msg, "malformed_at_rule");
|
|
1695
|
+
}
|
|
1696
|
+
|
|
1697
|
+
p++; // Skip opening {
|
|
1698
|
+
|
|
1699
|
+
// Find matching closing brace
|
|
1700
|
+
const char *block_start = p;
|
|
1701
|
+
const char *block_end = find_matching_brace_strict(p, pe, ctx->check_unclosed_blocks);
|
|
1702
|
+
p = block_end;
|
|
1703
|
+
|
|
1704
|
+
// Recursively parse block content (preserve parent media context)
|
|
1705
|
+
ctx->depth++;
|
|
1706
|
+
parse_css_recursive(ctx, block_start, block_end, parent_media_sym, parent_selector, parent_rule_id, parent_media_query_id);
|
|
1707
|
+
ctx->depth--;
|
|
1708
|
+
|
|
1709
|
+
if (p < pe && *p == '}') p++;
|
|
1710
|
+
*p_ptr = p;
|
|
1711
|
+
}
|
|
1712
|
+
|
|
1713
|
+
/*
|
|
1714
|
+
* Handle an @keyframes (or -webkit-/-moz- prefixed) at-rule found at
|
|
1715
|
+
* brace_depth 0. Called with *p_ptr pointing at the '@'; at_name_end marks
|
|
1716
|
+
* the end of the already-scanned at-rule name. Advances *p_ptr past the
|
|
1717
|
+
* entire construct (including its closing '}', if found).
|
|
1718
|
+
*/
|
|
1719
|
+
static void handle_keyframes_at_rule(ParserContext *ctx, const char **p_ptr, const char *pe,
|
|
1720
|
+
const char *at_name_end, VALUE parent_media_sym, int parent_media_query_id) {
|
|
1721
|
+
// Build full selector string: "@keyframes fade"
|
|
1722
|
+
const char *selector_start = *p_ptr; // Points to '@'
|
|
1723
|
+
const char *p = at_name_end;
|
|
1724
|
+
while (p < pe && *p != '{') p++;
|
|
1725
|
+
|
|
1726
|
+
if (p >= pe || *p != '{') {
|
|
1727
|
+
*p_ptr = p;
|
|
1728
|
+
return; // Malformed
|
|
1729
|
+
}
|
|
1730
|
+
|
|
1731
|
+
const char *selector_end = p;
|
|
1732
|
+
while (selector_end > selector_start && IS_WHITESPACE(*(selector_end - 1))) {
|
|
1733
|
+
selector_end--;
|
|
1734
|
+
}
|
|
1735
|
+
VALUE selector = rb_utf8_str_new(selector_start, selector_end - selector_start);
|
|
1736
|
+
|
|
1737
|
+
p++; // Skip opening {
|
|
1738
|
+
|
|
1739
|
+
// Find matching closing brace
|
|
1740
|
+
const char *block_start = p;
|
|
1741
|
+
const char *block_end = find_matching_brace_strict(p, pe, ctx->check_unclosed_blocks);
|
|
1742
|
+
p = block_end;
|
|
1743
|
+
|
|
1744
|
+
// Parse keyframe blocks as rules (from/to/0%/50% etc). Unlike @media/@supports,
|
|
1745
|
+
// which thread ctx straight through, these aren't real page selectors and must
|
|
1746
|
+
// NOT land in ctx->rules_array — they need their own array to become this
|
|
1747
|
+
// AtRule's `content` below, so parse into an isolated child context instead.
|
|
1748
|
+
ParserContext nested_ctx = init_child_context(ctx);
|
|
1749
|
+
parse_css_recursive(&nested_ctx, block_start, block_end, NO_PARENT_MEDIA, NO_PARENT_SELECTOR, NO_PARENT_RULE_ID, NO_MEDIA_QUERY_ID);
|
|
1750
|
+
|
|
1751
|
+
// Get rule ID and increment
|
|
1752
|
+
int rule_id = ctx->rule_id_counter++;
|
|
1753
|
+
|
|
1754
|
+
// Create AtRule with nested rules
|
|
1755
|
+
VALUE media_query_id_val = (parent_media_query_id >= 0) ? INT2FIX(parent_media_query_id) : Qnil;
|
|
1756
|
+
VALUE at_rule = rb_struct_new(cAtRule,
|
|
1757
|
+
INT2FIX(rule_id),
|
|
1758
|
+
selector,
|
|
1759
|
+
nested_ctx.rules_array, // Array of Rule (keyframe blocks)
|
|
1760
|
+
Qnil, // specificity
|
|
1761
|
+
media_query_id_val // media_query_id from parent context
|
|
1762
|
+
);
|
|
1763
|
+
|
|
1764
|
+
// Add to rules array
|
|
1765
|
+
rb_ary_push(ctx->rules_array, at_rule);
|
|
1766
|
+
|
|
1767
|
+
// Add to media index if in media query
|
|
1768
|
+
if (!NIL_P(parent_media_sym)) {
|
|
1769
|
+
VALUE rule_ids = rb_hash_aref(ctx->media_index, parent_media_sym);
|
|
1770
|
+
if (NIL_P(rule_ids)) {
|
|
1771
|
+
rule_ids = rb_ary_new();
|
|
1772
|
+
rb_hash_aset(ctx->media_index, parent_media_sym, rule_ids);
|
|
1773
|
+
}
|
|
1774
|
+
rb_ary_push(rule_ids, INT2FIX(rule_id));
|
|
1775
|
+
}
|
|
1776
|
+
|
|
1777
|
+
if (p < pe && *p == '}') p++;
|
|
1778
|
+
*p_ptr = p;
|
|
1779
|
+
}
|
|
1780
|
+
|
|
1781
|
+
/*
|
|
1782
|
+
* Handle an @font-face at-rule found at brace_depth 0. Called with *p_ptr
|
|
1783
|
+
* pointing at the '@'; at_name_end marks the end of the already-scanned
|
|
1784
|
+
* at-rule name. Advances *p_ptr past the entire construct (including its
|
|
1785
|
+
* closing '}', if found).
|
|
1786
|
+
*/
|
|
1787
|
+
static void handle_font_face_at_rule(ParserContext *ctx, const char **p_ptr, const char *pe,
|
|
1788
|
+
const char *at_name_end, VALUE parent_media_sym, int parent_media_query_id) {
|
|
1789
|
+
// Build selector string: "@font-face"
|
|
1790
|
+
const char *selector_start = *p_ptr; // Points to '@'
|
|
1791
|
+
const char *p = at_name_end;
|
|
1792
|
+
while (p < pe && *p != '{') p++;
|
|
1793
|
+
|
|
1794
|
+
if (p >= pe || *p != '{') {
|
|
1795
|
+
*p_ptr = p;
|
|
1796
|
+
return; // Malformed
|
|
1797
|
+
}
|
|
1798
|
+
|
|
1799
|
+
const char *selector_end = p;
|
|
1800
|
+
while (selector_end > selector_start && IS_WHITESPACE(*(selector_end - 1))) {
|
|
1801
|
+
selector_end--;
|
|
1802
|
+
}
|
|
1803
|
+
VALUE selector = rb_utf8_str_new(selector_start, selector_end - selector_start);
|
|
1804
|
+
|
|
1805
|
+
p++; // Skip opening {
|
|
1806
|
+
|
|
1807
|
+
// Find matching closing brace
|
|
1808
|
+
const char *decl_start = p;
|
|
1809
|
+
const char *decl_end = find_matching_brace_strict(p, pe, ctx->check_unclosed_blocks);
|
|
1810
|
+
p = decl_end;
|
|
1811
|
+
|
|
1812
|
+
// Parse declarations
|
|
1813
|
+
VALUE declarations = parse_declarations(decl_start, decl_end, ctx);
|
|
1814
|
+
|
|
1815
|
+
// Get rule ID and increment
|
|
1816
|
+
int rule_id = ctx->rule_id_counter++;
|
|
1817
|
+
|
|
1818
|
+
// Create AtRule with declarations
|
|
1819
|
+
VALUE media_query_id_val = (parent_media_query_id >= 0) ? INT2FIX(parent_media_query_id) : Qnil;
|
|
1820
|
+
VALUE at_rule = rb_struct_new(cAtRule,
|
|
1821
|
+
INT2FIX(rule_id),
|
|
1822
|
+
selector,
|
|
1823
|
+
declarations, // Array of Declaration
|
|
1824
|
+
Qnil, // specificity
|
|
1825
|
+
media_query_id_val // media_query_id from parent context
|
|
1826
|
+
);
|
|
1827
|
+
|
|
1828
|
+
// Add to rules array
|
|
1829
|
+
rb_ary_push(ctx->rules_array, at_rule);
|
|
1830
|
+
|
|
1831
|
+
// Add to media index if in media query
|
|
1832
|
+
if (!NIL_P(parent_media_sym)) {
|
|
1833
|
+
VALUE rule_ids = rb_hash_aref(ctx->media_index, parent_media_sym);
|
|
1834
|
+
if (NIL_P(rule_ids)) {
|
|
1835
|
+
rule_ids = rb_ary_new();
|
|
1836
|
+
rb_hash_aset(ctx->media_index, parent_media_sym, rule_ids);
|
|
1837
|
+
}
|
|
1838
|
+
rb_ary_push(rule_ids, INT2FIX(rule_id));
|
|
1839
|
+
}
|
|
1840
|
+
|
|
1841
|
+
if (p < pe && *p == '}') p++;
|
|
1842
|
+
*p_ptr = p;
|
|
1843
|
+
}
|
|
1844
|
+
|
|
1845
|
+
/*
|
|
1846
|
+
* Finish a complete top-level CSS rule block just found at its closing '}'
|
|
1847
|
+
* (p points at that '}'; [selector_start, decl_start) is the selector text
|
|
1848
|
+
* and [decl_start, p) is the body). Determines whether the body has nested
|
|
1849
|
+
* selectors and either parses it as pure declarations (fast path, possibly
|
|
1850
|
+
* split across comma-separated selectors) or recurses per comma-separated
|
|
1851
|
+
* selector via parse_mixed_block (nested path). Does not modify p or reset
|
|
1852
|
+
* selector_start/decl_start - the caller does that once this returns.
|
|
1853
|
+
*/
|
|
1854
|
+
static void finish_rule_block(ParserContext *ctx, const char *selector_start, const char *decl_start, const char *p,
|
|
1855
|
+
VALUE parent_selector, VALUE parent_rule_id, VALUE parent_media_sym, int parent_media_query_id) {
|
|
1856
|
+
// We've found a complete CSS rule block - now determine if it has nesting
|
|
1857
|
+
// Example: .parent { color: red; & .child { font-size: 14px; } }
|
|
1858
|
+
// ^selector_start ^decl_start ^p (at })
|
|
1859
|
+
BOOLEAN has_nesting = has_nested_selectors(decl_start, p);
|
|
1860
|
+
|
|
1861
|
+
// Get selector string
|
|
1862
|
+
const char *sel_end = decl_start - 1;
|
|
1863
|
+
while (sel_end > selector_start && IS_WHITESPACE(*(sel_end - 1))) {
|
|
1864
|
+
sel_end--;
|
|
1865
|
+
}
|
|
1866
|
+
|
|
1867
|
+
// Check for empty selector
|
|
1868
|
+
if (ctx->check_invalid_selectors && sel_end <= selector_start) {
|
|
1869
|
+
const char *css = RSTRING_PTR(ctx->css_string);
|
|
1870
|
+
long error_pos = selector_start - css;
|
|
1871
|
+
|
|
1872
|
+
// Build keyword args hash
|
|
1873
|
+
VALUE kwargs = rb_hash_new();
|
|
1874
|
+
rb_hash_aset(kwargs, ID2SYM(rb_intern("css")), ctx->css_string);
|
|
1875
|
+
rb_hash_aset(kwargs, ID2SYM(rb_intern("pos")), LONG2NUM(error_pos));
|
|
1876
|
+
rb_hash_aset(kwargs, ID2SYM(rb_intern("type")), ID2SYM(rb_intern("invalid_selector")));
|
|
1877
|
+
|
|
1878
|
+
VALUE msg_str = rb_str_new_cstr("Invalid selector: empty selector");
|
|
1879
|
+
VALUE argv[2] = {msg_str, kwargs};
|
|
1880
|
+
VALUE error = rb_funcallv_kw(eParseError, rb_intern("new"), 2, argv, RB_PASS_KEYWORDS);
|
|
1881
|
+
rb_exc_raise(error);
|
|
1882
|
+
}
|
|
1883
|
+
|
|
1884
|
+
if (!has_nesting) {
|
|
1885
|
+
// FAST PATH: No nesting - parse as pure declarations
|
|
1886
|
+
VALUE declarations = parse_declarations(decl_start, p, ctx);
|
|
1887
|
+
|
|
1888
|
+
// Split on commas to handle multi-selector rules
|
|
1889
|
+
// Example: ".a, .b, .c { color: red; }" creates 3 separate rules
|
|
1890
|
+
// ^selector_start ^sel_end
|
|
1891
|
+
// ^seg_start=seg (scanning for commas)
|
|
1892
|
+
|
|
1893
|
+
// Count selectors for selector list tracking
|
|
1894
|
+
int selector_count = 1;
|
|
1895
|
+
if (ctx->selector_lists_enabled) {
|
|
1896
|
+
const char *count_ptr = selector_start;
|
|
1897
|
+
while (count_ptr < sel_end) {
|
|
1898
|
+
if (*count_ptr == ',') {
|
|
1899
|
+
selector_count++;
|
|
1900
|
+
}
|
|
1901
|
+
count_ptr++;
|
|
1902
|
+
}
|
|
1903
|
+
}
|
|
1904
|
+
|
|
1905
|
+
// Create selector list if enabled and multiple selectors
|
|
1906
|
+
int list_id = -1;
|
|
1907
|
+
VALUE rule_ids_array = Qnil;
|
|
1908
|
+
if (ctx->selector_lists_enabled && selector_count > 1) {
|
|
1909
|
+
list_id = ctx->next_selector_list_id++;
|
|
1910
|
+
rule_ids_array = rb_ary_new();
|
|
1911
|
+
rb_hash_aset(ctx->selector_lists, INT2FIX(list_id), rule_ids_array);
|
|
1912
|
+
}
|
|
1913
|
+
|
|
1914
|
+
const char *seg_start = selector_start;
|
|
1915
|
+
const char *seg = selector_start;
|
|
1916
|
+
|
|
1917
|
+
while (seg <= sel_end) {
|
|
1918
|
+
if (seg == sel_end || *seg == ',') { // At: ',' or end
|
|
1919
|
+
// Trim segment
|
|
1920
|
+
while (seg_start < seg && IS_WHITESPACE(*seg_start)) {
|
|
1921
|
+
seg_start++;
|
|
1922
|
+
}
|
|
1923
|
+
|
|
1924
|
+
const char *seg_end_ptr = seg;
|
|
1925
|
+
while (seg_end_ptr > seg_start && IS_WHITESPACE(*(seg_end_ptr - 1))) {
|
|
1926
|
+
seg_end_ptr--;
|
|
1927
|
+
}
|
|
1928
|
+
|
|
1929
|
+
if (seg_end_ptr > seg_start) {
|
|
1930
|
+
// Check for invalid selectors
|
|
1931
|
+
if (ctx->check_invalid_selectors) {
|
|
1932
|
+
// Check if selector starts with combinator
|
|
1933
|
+
char first_char = *seg_start;
|
|
1934
|
+
if (first_char == '>' || first_char == '+' || first_char == '~') {
|
|
1935
|
+
const char *css = RSTRING_PTR(ctx->css_string);
|
|
1936
|
+
long error_pos = seg_start - css;
|
|
1937
|
+
|
|
1938
|
+
char error_msg[256];
|
|
1939
|
+
snprintf(error_msg, sizeof(error_msg),
|
|
1940
|
+
"Invalid selector: selector cannot start with combinator '%c'",
|
|
1941
|
+
first_char);
|
|
1942
|
+
|
|
1943
|
+
// Build keyword args hash
|
|
1944
|
+
VALUE kwargs = rb_hash_new();
|
|
1945
|
+
rb_hash_aset(kwargs, ID2SYM(rb_intern("css")), ctx->css_string);
|
|
1946
|
+
rb_hash_aset(kwargs, ID2SYM(rb_intern("pos")), LONG2NUM(error_pos));
|
|
1947
|
+
rb_hash_aset(kwargs, ID2SYM(rb_intern("type")), ID2SYM(rb_intern("invalid_selector")));
|
|
1948
|
+
|
|
1949
|
+
VALUE msg_str = rb_str_new_cstr(error_msg);
|
|
1950
|
+
VALUE argv[2] = {msg_str, kwargs};
|
|
1951
|
+
VALUE error = rb_funcallv_kw(eParseError, rb_intern("new"), 2, argv, RB_PASS_KEYWORDS);
|
|
1952
|
+
rb_exc_raise(error);
|
|
1953
|
+
}
|
|
1954
|
+
}
|
|
1955
|
+
|
|
1956
|
+
// Check for invalid selector syntax (whitelist validation)
|
|
1957
|
+
if (ctx->check_invalid_selector_syntax && !is_valid_selector(seg_start, seg_end_ptr)) {
|
|
1958
|
+
raise_parse_error_at(ctx, seg_start, "Invalid selector syntax: selector contains invalid characters", "invalid_selector_syntax");
|
|
1959
|
+
}
|
|
1960
|
+
|
|
1961
|
+
VALUE selector = rb_utf8_str_new(seg_start, seg_end_ptr - seg_start);
|
|
1962
|
+
|
|
1963
|
+
// Resolve against parent if nested
|
|
1964
|
+
VALUE resolved_selector;
|
|
1965
|
+
VALUE nesting_style_val;
|
|
1966
|
+
VALUE parent_id_val;
|
|
1967
|
+
|
|
1968
|
+
if (!NIL_P(parent_selector)) {
|
|
1969
|
+
// This is a nested rule - resolve selector
|
|
1970
|
+
VALUE result = resolve_nested_selector(parent_selector, RSTRING_PTR(selector), RSTRING_LEN(selector));
|
|
1971
|
+
resolved_selector = rb_ary_entry(result, 0);
|
|
1972
|
+
nesting_style_val = rb_ary_entry(result, 1);
|
|
1973
|
+
parent_id_val = parent_rule_id;
|
|
1974
|
+
} else {
|
|
1975
|
+
// Top-level rule
|
|
1976
|
+
resolved_selector = selector;
|
|
1977
|
+
nesting_style_val = Qnil;
|
|
1978
|
+
parent_id_val = Qnil;
|
|
1979
|
+
}
|
|
1980
|
+
|
|
1981
|
+
// Get rule ID and increment
|
|
1982
|
+
int rule_id = ctx->rule_id_counter++;
|
|
1983
|
+
|
|
1984
|
+
// Determine selector_list_id value
|
|
1985
|
+
VALUE selector_list_id_val = (list_id >= 0) ? INT2FIX(list_id) : Qnil;
|
|
1986
|
+
|
|
1987
|
+
// Deep copy declarations for selector lists to avoid shared state
|
|
1988
|
+
// (principle of least surprise - modifying one rule shouldn't affect others)
|
|
1989
|
+
VALUE rule_declarations;
|
|
1990
|
+
if (list_id >= 0) {
|
|
1991
|
+
// Deep copy: both array and Declaration structs inside
|
|
1992
|
+
long decl_count = RARRAY_LEN(declarations);
|
|
1993
|
+
rule_declarations = rb_ary_new_capa(decl_count);
|
|
1994
|
+
for (long k = 0; k < decl_count; k++) {
|
|
1995
|
+
VALUE decl = rb_ary_entry(declarations, k);
|
|
1996
|
+
VALUE new_decl = rb_struct_new(cDeclaration,
|
|
1997
|
+
rb_struct_aref(decl, INT2FIX(DECL_PROPERTY)),
|
|
1998
|
+
rb_struct_aref(decl, INT2FIX(DECL_VALUE)),
|
|
1999
|
+
rb_struct_aref(decl, INT2FIX(DECL_IMPORTANT))
|
|
2000
|
+
);
|
|
2001
|
+
rb_ary_push(rule_declarations, new_decl);
|
|
2002
|
+
}
|
|
2003
|
+
} else {
|
|
2004
|
+
rule_declarations = rb_ary_dup(declarations);
|
|
2005
|
+
}
|
|
2006
|
+
|
|
2007
|
+
// Create Rule
|
|
2008
|
+
VALUE media_query_id_val = (parent_media_query_id >= 0) ? INT2FIX(parent_media_query_id) : Qnil;
|
|
2009
|
+
VALUE rule = rb_struct_new(cRule,
|
|
2010
|
+
INT2FIX(rule_id),
|
|
2011
|
+
resolved_selector,
|
|
2012
|
+
rule_declarations,
|
|
2013
|
+
Qnil, // specificity
|
|
2014
|
+
parent_id_val,
|
|
2015
|
+
nesting_style_val,
|
|
2016
|
+
selector_list_id_val,
|
|
2017
|
+
media_query_id_val // media_query_id from parent context
|
|
2018
|
+
);
|
|
2019
|
+
|
|
2020
|
+
// Track rule in selector list if applicable
|
|
2021
|
+
if (list_id >= 0) {
|
|
2022
|
+
rb_ary_push(rule_ids_array, INT2FIX(rule_id));
|
|
2023
|
+
}
|
|
2024
|
+
|
|
2025
|
+
// Mark that we have nesting (only set once)
|
|
2026
|
+
if (!ctx->has_nesting && !NIL_P(parent_id_val)) {
|
|
2027
|
+
ctx->has_nesting = 1;
|
|
2028
|
+
}
|
|
2029
|
+
|
|
2030
|
+
rb_ary_push(ctx->rules_array, rule);
|
|
2031
|
+
|
|
2032
|
+
// Update media index
|
|
2033
|
+
update_media_index(ctx, parent_media_sym, rule_id);
|
|
2034
|
+
} else if (ctx->check_invalid_selector_syntax && selector_count > 1) {
|
|
2035
|
+
// Empty selector in comma-separated list (e.g., "h1, , h3")
|
|
2036
|
+
raise_parse_error_at(ctx, seg_start, "Invalid selector syntax: empty selector in comma-separated list", "invalid_selector_syntax");
|
|
2037
|
+
}
|
|
2038
|
+
|
|
2039
|
+
seg_start = seg + 1;
|
|
2040
|
+
}
|
|
2041
|
+
seg++;
|
|
2042
|
+
}
|
|
2043
|
+
} else {
|
|
2044
|
+
// NESTED PATH: Parse mixed declarations + nested rules
|
|
2045
|
+
// For each comma-separated parent selector, parse the block with that parent
|
|
2046
|
+
//
|
|
2047
|
+
// Example: ".a, .b { color: red; & .child { font: 14px; } }"
|
|
2048
|
+
// ^selector_start ^sel_end
|
|
2049
|
+
// Creates:
|
|
2050
|
+
// - .a with declarations [color: red]
|
|
2051
|
+
// - .a .child with declarations [font: 14px]
|
|
2052
|
+
// - .b with declarations [color: red]
|
|
2053
|
+
// - .b .child with declarations [font: 14px]
|
|
2054
|
+
|
|
2055
|
+
// Count selectors for selector list tracking
|
|
2056
|
+
int selector_count = 1;
|
|
2057
|
+
if (ctx->selector_lists_enabled) {
|
|
2058
|
+
const char *count_ptr = selector_start;
|
|
2059
|
+
while (count_ptr < sel_end) {
|
|
2060
|
+
if (*count_ptr == ',') {
|
|
2061
|
+
selector_count++;
|
|
2062
|
+
}
|
|
2063
|
+
count_ptr++;
|
|
2064
|
+
}
|
|
2065
|
+
}
|
|
2066
|
+
|
|
2067
|
+
// Create selector list if enabled and multiple selectors
|
|
2068
|
+
int list_id = -1;
|
|
2069
|
+
VALUE rule_ids_array = Qnil;
|
|
2070
|
+
if (ctx->selector_lists_enabled && selector_count > 1) {
|
|
2071
|
+
list_id = ctx->next_selector_list_id++;
|
|
2072
|
+
rule_ids_array = rb_ary_new();
|
|
2073
|
+
rb_hash_aset(ctx->selector_lists, INT2FIX(list_id), rule_ids_array);
|
|
2074
|
+
}
|
|
2075
|
+
|
|
2076
|
+
const char *seg_start = selector_start;
|
|
2077
|
+
const char *seg = selector_start;
|
|
2078
|
+
|
|
2079
|
+
while (seg <= sel_end) {
|
|
2080
|
+
if (seg == sel_end || *seg == ',') { // At: ',' or end
|
|
2081
|
+
// Trim segment
|
|
2082
|
+
while (seg_start < seg && IS_WHITESPACE(*seg_start)) {
|
|
2083
|
+
seg_start++;
|
|
2084
|
+
}
|
|
2085
|
+
|
|
2086
|
+
const char *seg_end_ptr = seg;
|
|
2087
|
+
while (seg_end_ptr > seg_start && IS_WHITESPACE(*(seg_end_ptr - 1))) {
|
|
2088
|
+
seg_end_ptr--;
|
|
2089
|
+
}
|
|
2090
|
+
|
|
2091
|
+
if (seg_end_ptr > seg_start) {
|
|
2092
|
+
VALUE current_selector = rb_utf8_str_new(seg_start, seg_end_ptr - seg_start);
|
|
2093
|
+
|
|
2094
|
+
// Resolve against parent if we're already nested
|
|
2095
|
+
VALUE resolved_current;
|
|
2096
|
+
VALUE current_nesting_style;
|
|
2097
|
+
VALUE current_parent_id;
|
|
2098
|
+
|
|
2099
|
+
if (!NIL_P(parent_selector)) {
|
|
2100
|
+
VALUE result = resolve_nested_selector(parent_selector, RSTRING_PTR(current_selector), RSTRING_LEN(current_selector));
|
|
2101
|
+
resolved_current = rb_ary_entry(result, 0);
|
|
2102
|
+
current_nesting_style = rb_ary_entry(result, 1);
|
|
2103
|
+
current_parent_id = parent_rule_id;
|
|
2104
|
+
} else {
|
|
2105
|
+
resolved_current = current_selector;
|
|
2106
|
+
current_nesting_style = Qnil;
|
|
2107
|
+
current_parent_id = Qnil;
|
|
2108
|
+
}
|
|
2109
|
+
|
|
2110
|
+
// Get rule ID for current selector (increment to reserve it)
|
|
2111
|
+
int current_rule_id = ctx->rule_id_counter++;
|
|
2112
|
+
|
|
2113
|
+
// Reserve parent's position in rules array with placeholder
|
|
2114
|
+
// This ensures parent comes before nested rules in array order (per W3C spec)
|
|
2115
|
+
long parent_position = RARRAY_LEN(ctx->rules_array);
|
|
2116
|
+
rb_ary_push(ctx->rules_array, Qnil);
|
|
2117
|
+
|
|
2118
|
+
// Parse mixed block (declarations + nested selectors)
|
|
2119
|
+
// Nested rules will be added AFTER the placeholder
|
|
2120
|
+
ctx->depth++;
|
|
2121
|
+
VALUE parent_declarations = parse_mixed_block(ctx, decl_start, p,
|
|
2122
|
+
resolved_current, INT2FIX(current_rule_id), parent_media_sym, parent_media_query_id);
|
|
2123
|
+
ctx->depth--;
|
|
2124
|
+
|
|
2125
|
+
// Determine selector_list_id value
|
|
2126
|
+
VALUE selector_list_id_val = (list_id >= 0) ? INT2FIX(list_id) : Qnil;
|
|
2127
|
+
|
|
2128
|
+
// Create parent rule and replace placeholder
|
|
2129
|
+
// Always create the rule (even if empty) to avoid edge cases
|
|
2130
|
+
VALUE media_query_id_val = (parent_media_query_id >= 0) ? INT2FIX(parent_media_query_id) : Qnil;
|
|
2131
|
+
VALUE rule = rb_struct_new(cRule,
|
|
2132
|
+
INT2FIX(current_rule_id),
|
|
2133
|
+
resolved_current,
|
|
2134
|
+
parent_declarations,
|
|
2135
|
+
Qnil, // specificity
|
|
2136
|
+
current_parent_id,
|
|
2137
|
+
current_nesting_style,
|
|
2138
|
+
selector_list_id_val,
|
|
2139
|
+
media_query_id_val // media_query_id from parent context
|
|
2140
|
+
);
|
|
2141
|
+
|
|
2142
|
+
// Track rule in selector list if applicable
|
|
2143
|
+
if (list_id >= 0) {
|
|
2144
|
+
rb_ary_push(rule_ids_array, INT2FIX(current_rule_id));
|
|
2145
|
+
}
|
|
2146
|
+
|
|
2147
|
+
// Mark that we have nesting (only set once)
|
|
2148
|
+
if (!ctx->has_nesting && !NIL_P(current_parent_id)) {
|
|
2149
|
+
ctx->has_nesting = 1;
|
|
2150
|
+
}
|
|
2151
|
+
|
|
2152
|
+
// Replace placeholder with actual rule - just pointer assignment, fast!
|
|
2153
|
+
rb_ary_store(ctx->rules_array, parent_position, rule);
|
|
2154
|
+
update_media_index(ctx, parent_media_sym, current_rule_id);
|
|
2155
|
+
}
|
|
2156
|
+
|
|
2157
|
+
seg_start = seg + 1;
|
|
2158
|
+
}
|
|
2159
|
+
seg++;
|
|
2160
|
+
}
|
|
2161
|
+
}
|
|
2162
|
+
}
|
|
2163
|
+
|
|
1365
2164
|
/*
|
|
1366
2165
|
* Parse CSS recursively with media query context and optional parent selector for nesting
|
|
1367
2166
|
*
|
|
@@ -1419,142 +2218,7 @@ static void parse_css_recursive(ParserContext *ctx, const char *css, const char
|
|
|
1419
2218
|
// Check for @media at-rule (only at depth 0)
|
|
1420
2219
|
if (RB_UNLIKELY(brace_depth == 0 && p + 6 < pe && *p == '@' &&
|
|
1421
2220
|
strncmp(p + 1, "media", 5) == 0 && IS_WHITESPACE(p[6]))) {
|
|
1422
|
-
p
|
|
1423
|
-
|
|
1424
|
-
// Skip whitespace
|
|
1425
|
-
while (p < pe && IS_WHITESPACE(*p)) p++;
|
|
1426
|
-
|
|
1427
|
-
// Find media query (up to opening brace)
|
|
1428
|
-
const char *mq_start = p;
|
|
1429
|
-
while (p < pe && *p != '{') p++;
|
|
1430
|
-
const char *mq_end = p;
|
|
1431
|
-
|
|
1432
|
-
// Trim
|
|
1433
|
-
trim_trailing(mq_start, &mq_end);
|
|
1434
|
-
|
|
1435
|
-
if (p >= pe || *p != '{') {
|
|
1436
|
-
continue; // Malformed
|
|
1437
|
-
}
|
|
1438
|
-
|
|
1439
|
-
// Split comma-separated media queries (e.g., "screen, print" -> ["screen", "print"])
|
|
1440
|
-
// Per W3C spec, comma acts as logical OR - each query is independent
|
|
1441
|
-
VALUE media_query_ids = rb_ary_new();
|
|
1442
|
-
|
|
1443
|
-
const char *query_start = mq_start;
|
|
1444
|
-
for (const char *p_comma = mq_start; p_comma <= mq_end; p_comma++) {
|
|
1445
|
-
if (p_comma == mq_end || *p_comma == ',') {
|
|
1446
|
-
const char *query_end = p_comma;
|
|
1447
|
-
|
|
1448
|
-
// Trim whitespace from this query
|
|
1449
|
-
while (query_start < query_end && IS_WHITESPACE(*query_start)) query_start++;
|
|
1450
|
-
while (query_end > query_start && IS_WHITESPACE(*(query_end - 1))) query_end--;
|
|
1451
|
-
|
|
1452
|
-
if (query_start < query_end) {
|
|
1453
|
-
// Parse this individual media query
|
|
1454
|
-
const char *mq_ptr = query_start;
|
|
1455
|
-
VALUE media_type;
|
|
1456
|
-
VALUE media_conditions = Qnil;
|
|
1457
|
-
|
|
1458
|
-
if (*mq_ptr == '(') {
|
|
1459
|
-
// Starts with '(' - just conditions, type defaults to :all
|
|
1460
|
-
media_type = ID2SYM(rb_intern("all"));
|
|
1461
|
-
media_conditions = rb_utf8_str_new(mq_ptr, query_end - mq_ptr);
|
|
1462
|
-
} else {
|
|
1463
|
-
// Extract media type (first word, stopping at whitespace, comma, or '(')
|
|
1464
|
-
const char *type_start = mq_ptr;
|
|
1465
|
-
while (mq_ptr < query_end && !IS_WHITESPACE(*mq_ptr) && *mq_ptr != '(') mq_ptr++;
|
|
1466
|
-
VALUE type_str = rb_utf8_str_new(type_start, mq_ptr - type_start);
|
|
1467
|
-
media_type = ID2SYM(rb_intern_str(type_str));
|
|
1468
|
-
|
|
1469
|
-
// Skip whitespace and "and" keyword if present
|
|
1470
|
-
while (mq_ptr < query_end && IS_WHITESPACE(*mq_ptr)) mq_ptr++;
|
|
1471
|
-
if (mq_ptr + 3 <= query_end && strncmp(mq_ptr, "and", 3) == 0) {
|
|
1472
|
-
mq_ptr += 3;
|
|
1473
|
-
while (mq_ptr < query_end && IS_WHITESPACE(*mq_ptr)) mq_ptr++;
|
|
1474
|
-
}
|
|
1475
|
-
|
|
1476
|
-
// Rest is conditions
|
|
1477
|
-
if (mq_ptr < query_end) {
|
|
1478
|
-
media_conditions = rb_utf8_str_new(mq_ptr, query_end - mq_ptr);
|
|
1479
|
-
}
|
|
1480
|
-
}
|
|
1481
|
-
|
|
1482
|
-
// Create MediaQuery object for this query
|
|
1483
|
-
VALUE media_query = rb_struct_new(cMediaQuery,
|
|
1484
|
-
INT2FIX(ctx->media_query_id_counter),
|
|
1485
|
-
media_type,
|
|
1486
|
-
media_conditions
|
|
1487
|
-
);
|
|
1488
|
-
rb_ary_push(ctx->media_queries, media_query);
|
|
1489
|
-
rb_ary_push(media_query_ids, INT2FIX(ctx->media_query_id_counter));
|
|
1490
|
-
ctx->media_query_id_counter++;
|
|
1491
|
-
}
|
|
1492
|
-
|
|
1493
|
-
// Move to start of next query
|
|
1494
|
-
query_start = p_comma + 1;
|
|
1495
|
-
}
|
|
1496
|
-
}
|
|
1497
|
-
|
|
1498
|
-
// If multiple queries, track them as a list for serialization
|
|
1499
|
-
int media_query_list_id = -1;
|
|
1500
|
-
if (RARRAY_LEN(media_query_ids) > 1) {
|
|
1501
|
-
media_query_list_id = ctx->next_media_query_list_id;
|
|
1502
|
-
rb_hash_aset(ctx->media_query_lists, INT2FIX(media_query_list_id), media_query_ids);
|
|
1503
|
-
ctx->next_media_query_list_id++;
|
|
1504
|
-
}
|
|
1505
|
-
|
|
1506
|
-
// Use first query ID as the primary one for rules in this block
|
|
1507
|
-
int current_media_query_id = FIX2INT(rb_ary_entry(media_query_ids, 0));
|
|
1508
|
-
|
|
1509
|
-
// Handle nested @media by combining with parent
|
|
1510
|
-
if (parent_media_query_id >= 0) {
|
|
1511
|
-
VALUE parent_mq = rb_ary_entry(ctx->media_queries, parent_media_query_id);
|
|
1512
|
-
VALUE parent_type = rb_struct_aref(parent_mq, INT2FIX(1)); // type field
|
|
1513
|
-
VALUE parent_conditions = rb_struct_aref(parent_mq, INT2FIX(2)); // conditions field
|
|
1514
|
-
|
|
1515
|
-
// Get child media query (first one in the list)
|
|
1516
|
-
VALUE child_mq = rb_ary_entry(ctx->media_queries, current_media_query_id);
|
|
1517
|
-
VALUE child_conditions = rb_struct_aref(child_mq, INT2FIX(2)); // conditions field
|
|
1518
|
-
|
|
1519
|
-
// Combined type is parent's type (outermost wins, child type ignored)
|
|
1520
|
-
VALUE combined_type = parent_type;
|
|
1521
|
-
VALUE combined_conditions;
|
|
1522
|
-
|
|
1523
|
-
if (!NIL_P(parent_conditions) && !NIL_P(child_conditions)) {
|
|
1524
|
-
combined_conditions = rb_sprintf("%"PRIsVALUE" and %"PRIsVALUE, parent_conditions, child_conditions);
|
|
1525
|
-
} else if (!NIL_P(parent_conditions)) {
|
|
1526
|
-
combined_conditions = parent_conditions;
|
|
1527
|
-
} else {
|
|
1528
|
-
combined_conditions = child_conditions;
|
|
1529
|
-
}
|
|
1530
|
-
|
|
1531
|
-
VALUE combined_mq = rb_struct_new(cMediaQuery,
|
|
1532
|
-
INT2FIX(ctx->media_query_id_counter),
|
|
1533
|
-
combined_type,
|
|
1534
|
-
combined_conditions
|
|
1535
|
-
);
|
|
1536
|
-
rb_ary_push(ctx->media_queries, combined_mq);
|
|
1537
|
-
current_media_query_id = ctx->media_query_id_counter;
|
|
1538
|
-
ctx->media_query_id_counter++;
|
|
1539
|
-
}
|
|
1540
|
-
|
|
1541
|
-
// For backwards compat, also create symbol (will be removed later)
|
|
1542
|
-
VALUE child_media_sym = intern_media_query_safe(ctx, mq_start, mq_end - mq_start);
|
|
1543
|
-
VALUE combined_media_sym = combine_media_queries(parent_media_sym, child_media_sym);
|
|
1544
|
-
|
|
1545
|
-
p++; // Skip opening {
|
|
1546
|
-
|
|
1547
|
-
// Find matching closing brace
|
|
1548
|
-
const char *block_start = p;
|
|
1549
|
-
const char *block_end = find_matching_brace(p, pe);
|
|
1550
|
-
p = block_end;
|
|
1551
|
-
|
|
1552
|
-
// Recursively parse @media block with new media query context
|
|
1553
|
-
ctx->depth++;
|
|
1554
|
-
parse_css_recursive(ctx, block_start, block_end, combined_media_sym, NO_PARENT_SELECTOR, NO_PARENT_RULE_ID, current_media_query_id);
|
|
1555
|
-
ctx->depth--;
|
|
1556
|
-
|
|
1557
|
-
if (p < pe && *p == '}') p++;
|
|
2221
|
+
handle_media_at_rule(ctx, &p, pe, parent_media_sym, parent_media_query_id);
|
|
1558
2222
|
continue;
|
|
1559
2223
|
}
|
|
1560
2224
|
|
|
@@ -1573,173 +2237,45 @@ static void parse_css_recursive(ParserContext *ctx, const char *css, const char
|
|
|
1573
2237
|
long at_name_len = at_name_end - at_start;
|
|
1574
2238
|
|
|
1575
2239
|
// Check if this is a conditional group rule
|
|
1576
|
-
|
|
2240
|
+
BOOLEAN is_conditional_group =
|
|
1577
2241
|
(at_name_len == 8 && strncmp(at_start, "supports", 8) == 0) ||
|
|
1578
2242
|
(at_name_len == 5 && strncmp(at_start, "layer", 5) == 0) ||
|
|
1579
2243
|
(at_name_len == 9 && strncmp(at_start, "container", 9) == 0) ||
|
|
1580
2244
|
(at_name_len == 5 && strncmp(at_start, "scope", 5) == 0);
|
|
1581
2245
|
|
|
1582
2246
|
if (is_conditional_group) {
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
while (p < pe && *p != '{') p++;
|
|
1586
|
-
|
|
1587
|
-
if (p >= pe || *p != '{') {
|
|
1588
|
-
continue; // Malformed
|
|
1589
|
-
}
|
|
1590
|
-
|
|
1591
|
-
p++; // Skip opening {
|
|
1592
|
-
|
|
1593
|
-
// Find matching closing brace
|
|
1594
|
-
const char *block_start = p;
|
|
1595
|
-
const char *block_end = find_matching_brace(p, pe);
|
|
1596
|
-
p = block_end;
|
|
1597
|
-
|
|
1598
|
-
// Recursively parse block content (preserve parent media context)
|
|
1599
|
-
ctx->depth++;
|
|
1600
|
-
parse_css_recursive(ctx, block_start, block_end, parent_media_sym, parent_selector, parent_rule_id, parent_media_query_id);
|
|
1601
|
-
ctx->depth--;
|
|
1602
|
-
|
|
1603
|
-
if (p < pe && *p == '}') p++;
|
|
2247
|
+
handle_conditional_group_at_rule(ctx, &p, pe, at_start, at_name_end, at_name_len,
|
|
2248
|
+
parent_media_sym, parent_selector, parent_rule_id, parent_media_query_id);
|
|
1604
2249
|
continue;
|
|
1605
2250
|
}
|
|
1606
2251
|
|
|
1607
2252
|
// Check for @keyframes (contains <rule-list>)
|
|
1608
2253
|
// TODO: Test perf gains by using RB_UNLIKELY(is_keyframes) wrapper
|
|
1609
|
-
|
|
2254
|
+
BOOLEAN is_keyframes =
|
|
1610
2255
|
(at_name_len == 9 && strncmp(at_start, "keyframes", 9) == 0) ||
|
|
1611
2256
|
(at_name_len == 17 && strncmp(at_start, "-webkit-keyframes", 17) == 0) ||
|
|
1612
2257
|
(at_name_len == 13 && strncmp(at_start, "-moz-keyframes", 13) == 0);
|
|
1613
2258
|
|
|
1614
2259
|
if (is_keyframes) {
|
|
1615
|
-
|
|
1616
|
-
const char *selector_start = p; // Points to '@'
|
|
1617
|
-
p = at_name_end;
|
|
1618
|
-
while (p < pe && *p != '{') p++;
|
|
1619
|
-
|
|
1620
|
-
if (p >= pe || *p != '{') {
|
|
1621
|
-
continue; // Malformed
|
|
1622
|
-
}
|
|
1623
|
-
|
|
1624
|
-
const char *selector_end = p;
|
|
1625
|
-
while (selector_end > selector_start && IS_WHITESPACE(*(selector_end - 1))) {
|
|
1626
|
-
selector_end--;
|
|
1627
|
-
}
|
|
1628
|
-
VALUE selector = rb_utf8_str_new(selector_start, selector_end - selector_start);
|
|
1629
|
-
|
|
1630
|
-
p++; // Skip opening {
|
|
1631
|
-
|
|
1632
|
-
// Find matching closing brace
|
|
1633
|
-
const char *block_start = p;
|
|
1634
|
-
const char *block_end = find_matching_brace(p, pe);
|
|
1635
|
-
p = block_end;
|
|
1636
|
-
|
|
1637
|
-
// Parse keyframe blocks as rules (from/to/0%/50% etc)
|
|
1638
|
-
ParserContext nested_ctx = {
|
|
1639
|
-
.rules_array = rb_ary_new(),
|
|
1640
|
-
.media_index = rb_hash_new(),
|
|
1641
|
-
.selector_lists = rb_hash_new(),
|
|
1642
|
-
.imports_array = rb_ary_new(),
|
|
1643
|
-
.rule_id_counter = 0,
|
|
1644
|
-
.next_selector_list_id = 0,
|
|
1645
|
-
.media_query_count = 0,
|
|
1646
|
-
.media_cache = NULL,
|
|
1647
|
-
.has_nesting = 0,
|
|
1648
|
-
.selector_lists_enabled = ctx->selector_lists_enabled,
|
|
1649
|
-
.depth = 0
|
|
1650
|
-
};
|
|
1651
|
-
parse_css_recursive(&nested_ctx, block_start, block_end, NO_PARENT_MEDIA, NO_PARENT_SELECTOR, NO_PARENT_RULE_ID, NO_MEDIA_QUERY_ID);
|
|
1652
|
-
|
|
1653
|
-
// Get rule ID and increment
|
|
1654
|
-
int rule_id = ctx->rule_id_counter++;
|
|
1655
|
-
|
|
1656
|
-
// Create AtRule with nested rules
|
|
1657
|
-
VALUE at_rule = rb_struct_new(cAtRule,
|
|
1658
|
-
INT2FIX(rule_id),
|
|
1659
|
-
selector,
|
|
1660
|
-
nested_ctx.rules_array, // Array of Rule (keyframe blocks)
|
|
1661
|
-
Qnil, // specificity
|
|
1662
|
-
Qnil // media_query_id
|
|
1663
|
-
);
|
|
1664
|
-
|
|
1665
|
-
// Add to rules array
|
|
1666
|
-
rb_ary_push(ctx->rules_array, at_rule);
|
|
1667
|
-
|
|
1668
|
-
// Add to media index if in media query
|
|
1669
|
-
if (!NIL_P(parent_media_sym)) {
|
|
1670
|
-
VALUE rule_ids = rb_hash_aref(ctx->media_index, parent_media_sym);
|
|
1671
|
-
if (NIL_P(rule_ids)) {
|
|
1672
|
-
rule_ids = rb_ary_new();
|
|
1673
|
-
rb_hash_aset(ctx->media_index, parent_media_sym, rule_ids);
|
|
1674
|
-
}
|
|
1675
|
-
rb_ary_push(rule_ids, INT2FIX(rule_id));
|
|
1676
|
-
}
|
|
1677
|
-
|
|
1678
|
-
if (p < pe && *p == '}') p++;
|
|
2260
|
+
handle_keyframes_at_rule(ctx, &p, pe, at_name_end, parent_media_sym, parent_media_query_id);
|
|
1679
2261
|
continue;
|
|
1680
2262
|
}
|
|
1681
2263
|
|
|
1682
2264
|
// Check for @font-face (contains <declaration-list>)
|
|
1683
|
-
|
|
2265
|
+
BOOLEAN is_font_face = (at_name_len == 9 && strncmp(at_start, "font-face", 9) == 0);
|
|
1684
2266
|
|
|
1685
2267
|
if (is_font_face) {
|
|
1686
|
-
|
|
1687
|
-
const char *selector_start = p; // Points to '@'
|
|
1688
|
-
p = at_name_end;
|
|
1689
|
-
while (p < pe && *p != '{') p++;
|
|
1690
|
-
|
|
1691
|
-
if (p >= pe || *p != '{') {
|
|
1692
|
-
continue; // Malformed
|
|
1693
|
-
}
|
|
1694
|
-
|
|
1695
|
-
const char *selector_end = p;
|
|
1696
|
-
while (selector_end > selector_start && IS_WHITESPACE(*(selector_end - 1))) {
|
|
1697
|
-
selector_end--;
|
|
1698
|
-
}
|
|
1699
|
-
VALUE selector = rb_utf8_str_new(selector_start, selector_end - selector_start);
|
|
1700
|
-
|
|
1701
|
-
p++; // Skip opening {
|
|
1702
|
-
|
|
1703
|
-
// Find matching closing brace
|
|
1704
|
-
const char *decl_start = p;
|
|
1705
|
-
const char *decl_end = find_matching_brace(p, pe);
|
|
1706
|
-
p = decl_end;
|
|
1707
|
-
|
|
1708
|
-
// Parse declarations
|
|
1709
|
-
VALUE declarations = parse_declarations(decl_start, decl_end, ctx);
|
|
1710
|
-
|
|
1711
|
-
// Get rule ID and increment
|
|
1712
|
-
int rule_id = ctx->rule_id_counter++;
|
|
1713
|
-
|
|
1714
|
-
// Create AtRule with declarations
|
|
1715
|
-
VALUE at_rule = rb_struct_new(cAtRule,
|
|
1716
|
-
INT2FIX(rule_id),
|
|
1717
|
-
selector,
|
|
1718
|
-
declarations, // Array of Declaration
|
|
1719
|
-
Qnil, // specificity
|
|
1720
|
-
Qnil // media_query_id
|
|
1721
|
-
);
|
|
1722
|
-
|
|
1723
|
-
// Add to rules array
|
|
1724
|
-
rb_ary_push(ctx->rules_array, at_rule);
|
|
1725
|
-
|
|
1726
|
-
// Add to media index if in media query
|
|
1727
|
-
if (!NIL_P(parent_media_sym)) {
|
|
1728
|
-
VALUE rule_ids = rb_hash_aref(ctx->media_index, parent_media_sym);
|
|
1729
|
-
if (NIL_P(rule_ids)) {
|
|
1730
|
-
rule_ids = rb_ary_new();
|
|
1731
|
-
rb_hash_aset(ctx->media_index, parent_media_sym, rule_ids);
|
|
1732
|
-
}
|
|
1733
|
-
rb_ary_push(rule_ids, INT2FIX(rule_id));
|
|
1734
|
-
}
|
|
1735
|
-
|
|
1736
|
-
if (p < pe && *p == '}') p++;
|
|
2268
|
+
handle_font_face_at_rule(ctx, &p, pe, at_name_end, parent_media_sym, parent_media_query_id);
|
|
1737
2269
|
continue;
|
|
1738
2270
|
}
|
|
1739
2271
|
}
|
|
1740
2272
|
|
|
1741
2273
|
// Opening brace
|
|
1742
2274
|
if (*p == '{') {
|
|
2275
|
+
// Check for empty selector (opening brace with no selector before it)
|
|
2276
|
+
if (ctx->check_invalid_selectors && brace_depth == 0 && selector_start == NULL) {
|
|
2277
|
+
raise_parse_error_at(ctx, p, "Invalid selector: empty selector", "invalid_selector");
|
|
2278
|
+
}
|
|
1743
2279
|
if (brace_depth == 0 && selector_start != NULL) {
|
|
1744
2280
|
decl_start = p + 1;
|
|
1745
2281
|
}
|
|
@@ -1752,262 +2288,8 @@ static void parse_css_recursive(ParserContext *ctx, const char *css, const char
|
|
|
1752
2288
|
if (*p == '}') {
|
|
1753
2289
|
brace_depth--;
|
|
1754
2290
|
if (brace_depth == 0 && selector_start != NULL && decl_start != NULL) {
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
// ^selector_start ^decl_start ^p (at })
|
|
1758
|
-
int has_nesting = has_nested_selectors(decl_start, p);
|
|
1759
|
-
|
|
1760
|
-
// Get selector string
|
|
1761
|
-
const char *sel_end = decl_start - 1;
|
|
1762
|
-
while (sel_end > selector_start && IS_WHITESPACE(*(sel_end - 1))) {
|
|
1763
|
-
sel_end--;
|
|
1764
|
-
}
|
|
1765
|
-
|
|
1766
|
-
if (!has_nesting) {
|
|
1767
|
-
// FAST PATH: No nesting - parse as pure declarations
|
|
1768
|
-
VALUE declarations = parse_declarations(decl_start, p, ctx);
|
|
1769
|
-
|
|
1770
|
-
// Split on commas to handle multi-selector rules
|
|
1771
|
-
// Example: ".a, .b, .c { color: red; }" creates 3 separate rules
|
|
1772
|
-
// ^selector_start ^sel_end
|
|
1773
|
-
// ^seg_start=seg (scanning for commas)
|
|
1774
|
-
|
|
1775
|
-
// Count selectors for selector list tracking
|
|
1776
|
-
int selector_count = 1;
|
|
1777
|
-
if (ctx->selector_lists_enabled) {
|
|
1778
|
-
const char *count_ptr = selector_start;
|
|
1779
|
-
while (count_ptr < sel_end) {
|
|
1780
|
-
if (*count_ptr == ',') {
|
|
1781
|
-
selector_count++;
|
|
1782
|
-
}
|
|
1783
|
-
count_ptr++;
|
|
1784
|
-
}
|
|
1785
|
-
}
|
|
1786
|
-
|
|
1787
|
-
// Create selector list if enabled and multiple selectors
|
|
1788
|
-
int list_id = -1;
|
|
1789
|
-
VALUE rule_ids_array = Qnil;
|
|
1790
|
-
if (ctx->selector_lists_enabled && selector_count > 1) {
|
|
1791
|
-
list_id = ctx->next_selector_list_id++;
|
|
1792
|
-
rule_ids_array = rb_ary_new();
|
|
1793
|
-
rb_hash_aset(ctx->selector_lists, INT2FIX(list_id), rule_ids_array);
|
|
1794
|
-
}
|
|
1795
|
-
|
|
1796
|
-
const char *seg_start = selector_start;
|
|
1797
|
-
const char *seg = selector_start;
|
|
1798
|
-
|
|
1799
|
-
while (seg <= sel_end) {
|
|
1800
|
-
if (seg == sel_end || *seg == ',') { // At: ',' or end
|
|
1801
|
-
// Trim segment
|
|
1802
|
-
while (seg_start < seg && IS_WHITESPACE(*seg_start)) {
|
|
1803
|
-
seg_start++;
|
|
1804
|
-
}
|
|
1805
|
-
|
|
1806
|
-
const char *seg_end_ptr = seg;
|
|
1807
|
-
while (seg_end_ptr > seg_start && IS_WHITESPACE(*(seg_end_ptr - 1))) {
|
|
1808
|
-
seg_end_ptr--;
|
|
1809
|
-
}
|
|
1810
|
-
|
|
1811
|
-
if (seg_end_ptr > seg_start) {
|
|
1812
|
-
VALUE selector = rb_utf8_str_new(seg_start, seg_end_ptr - seg_start);
|
|
1813
|
-
|
|
1814
|
-
// Resolve against parent if nested
|
|
1815
|
-
VALUE resolved_selector;
|
|
1816
|
-
VALUE nesting_style_val;
|
|
1817
|
-
VALUE parent_id_val;
|
|
1818
|
-
|
|
1819
|
-
if (!NIL_P(parent_selector)) {
|
|
1820
|
-
// This is a nested rule - resolve selector
|
|
1821
|
-
VALUE result = resolve_nested_selector(parent_selector, RSTRING_PTR(selector), RSTRING_LEN(selector));
|
|
1822
|
-
resolved_selector = rb_ary_entry(result, 0);
|
|
1823
|
-
nesting_style_val = rb_ary_entry(result, 1);
|
|
1824
|
-
parent_id_val = parent_rule_id;
|
|
1825
|
-
} else {
|
|
1826
|
-
// Top-level rule
|
|
1827
|
-
resolved_selector = selector;
|
|
1828
|
-
nesting_style_val = Qnil;
|
|
1829
|
-
parent_id_val = Qnil;
|
|
1830
|
-
}
|
|
1831
|
-
|
|
1832
|
-
// Get rule ID and increment
|
|
1833
|
-
int rule_id = ctx->rule_id_counter++;
|
|
1834
|
-
|
|
1835
|
-
// Determine selector_list_id value
|
|
1836
|
-
VALUE selector_list_id_val = (list_id >= 0) ? INT2FIX(list_id) : Qnil;
|
|
1837
|
-
|
|
1838
|
-
// Deep copy declarations for selector lists to avoid shared state
|
|
1839
|
-
// (principle of least surprise - modifying one rule shouldn't affect others)
|
|
1840
|
-
VALUE rule_declarations;
|
|
1841
|
-
if (list_id >= 0) {
|
|
1842
|
-
// Deep copy: both array and Declaration structs inside
|
|
1843
|
-
long decl_count = RARRAY_LEN(declarations);
|
|
1844
|
-
rule_declarations = rb_ary_new_capa(decl_count);
|
|
1845
|
-
for (long k = 0; k < decl_count; k++) {
|
|
1846
|
-
VALUE decl = rb_ary_entry(declarations, k);
|
|
1847
|
-
VALUE new_decl = rb_struct_new(cDeclaration,
|
|
1848
|
-
rb_struct_aref(decl, INT2FIX(DECL_PROPERTY)),
|
|
1849
|
-
rb_struct_aref(decl, INT2FIX(DECL_VALUE)),
|
|
1850
|
-
rb_struct_aref(decl, INT2FIX(DECL_IMPORTANT))
|
|
1851
|
-
);
|
|
1852
|
-
rb_ary_push(rule_declarations, new_decl);
|
|
1853
|
-
}
|
|
1854
|
-
} else {
|
|
1855
|
-
rule_declarations = rb_ary_dup(declarations);
|
|
1856
|
-
}
|
|
1857
|
-
|
|
1858
|
-
// Create Rule
|
|
1859
|
-
VALUE media_query_id_val = (parent_media_query_id >= 0) ? INT2FIX(parent_media_query_id) : Qnil;
|
|
1860
|
-
VALUE rule = rb_struct_new(cRule,
|
|
1861
|
-
INT2FIX(rule_id),
|
|
1862
|
-
resolved_selector,
|
|
1863
|
-
rule_declarations,
|
|
1864
|
-
Qnil, // specificity
|
|
1865
|
-
parent_id_val,
|
|
1866
|
-
nesting_style_val,
|
|
1867
|
-
selector_list_id_val,
|
|
1868
|
-
media_query_id_val // media_query_id from parent context
|
|
1869
|
-
);
|
|
1870
|
-
|
|
1871
|
-
// Track rule in selector list if applicable
|
|
1872
|
-
if (list_id >= 0) {
|
|
1873
|
-
rb_ary_push(rule_ids_array, INT2FIX(rule_id));
|
|
1874
|
-
}
|
|
1875
|
-
|
|
1876
|
-
// Mark that we have nesting (only set once)
|
|
1877
|
-
if (!ctx->has_nesting && !NIL_P(parent_id_val)) {
|
|
1878
|
-
ctx->has_nesting = 1;
|
|
1879
|
-
}
|
|
1880
|
-
|
|
1881
|
-
rb_ary_push(ctx->rules_array, rule);
|
|
1882
|
-
|
|
1883
|
-
// Update media index
|
|
1884
|
-
update_media_index(ctx, parent_media_sym, rule_id);
|
|
1885
|
-
}
|
|
1886
|
-
|
|
1887
|
-
seg_start = seg + 1;
|
|
1888
|
-
}
|
|
1889
|
-
seg++;
|
|
1890
|
-
}
|
|
1891
|
-
} else {
|
|
1892
|
-
// NESTED PATH: Parse mixed declarations + nested rules
|
|
1893
|
-
// For each comma-separated parent selector, parse the block with that parent
|
|
1894
|
-
//
|
|
1895
|
-
// Example: ".a, .b { color: red; & .child { font: 14px; } }"
|
|
1896
|
-
// ^selector_start ^sel_end
|
|
1897
|
-
// Creates:
|
|
1898
|
-
// - .a with declarations [color: red]
|
|
1899
|
-
// - .a .child with declarations [font: 14px]
|
|
1900
|
-
// - .b with declarations [color: red]
|
|
1901
|
-
// - .b .child with declarations [font: 14px]
|
|
1902
|
-
|
|
1903
|
-
// Count selectors for selector list tracking
|
|
1904
|
-
int selector_count = 1;
|
|
1905
|
-
if (ctx->selector_lists_enabled) {
|
|
1906
|
-
const char *count_ptr = selector_start;
|
|
1907
|
-
while (count_ptr < sel_end) {
|
|
1908
|
-
if (*count_ptr == ',') {
|
|
1909
|
-
selector_count++;
|
|
1910
|
-
}
|
|
1911
|
-
count_ptr++;
|
|
1912
|
-
}
|
|
1913
|
-
}
|
|
1914
|
-
|
|
1915
|
-
// Create selector list if enabled and multiple selectors
|
|
1916
|
-
int list_id = -1;
|
|
1917
|
-
VALUE rule_ids_array = Qnil;
|
|
1918
|
-
if (ctx->selector_lists_enabled && selector_count > 1) {
|
|
1919
|
-
list_id = ctx->next_selector_list_id++;
|
|
1920
|
-
rule_ids_array = rb_ary_new();
|
|
1921
|
-
rb_hash_aset(ctx->selector_lists, INT2FIX(list_id), rule_ids_array);
|
|
1922
|
-
}
|
|
1923
|
-
|
|
1924
|
-
const char *seg_start = selector_start;
|
|
1925
|
-
const char *seg = selector_start;
|
|
1926
|
-
|
|
1927
|
-
while (seg <= sel_end) {
|
|
1928
|
-
if (seg == sel_end || *seg == ',') { // At: ',' or end
|
|
1929
|
-
// Trim segment
|
|
1930
|
-
while (seg_start < seg && IS_WHITESPACE(*seg_start)) {
|
|
1931
|
-
seg_start++;
|
|
1932
|
-
}
|
|
1933
|
-
|
|
1934
|
-
const char *seg_end_ptr = seg;
|
|
1935
|
-
while (seg_end_ptr > seg_start && IS_WHITESPACE(*(seg_end_ptr - 1))) {
|
|
1936
|
-
seg_end_ptr--;
|
|
1937
|
-
}
|
|
1938
|
-
|
|
1939
|
-
if (seg_end_ptr > seg_start) {
|
|
1940
|
-
VALUE current_selector = rb_utf8_str_new(seg_start, seg_end_ptr - seg_start);
|
|
1941
|
-
|
|
1942
|
-
// Resolve against parent if we're already nested
|
|
1943
|
-
VALUE resolved_current;
|
|
1944
|
-
VALUE current_nesting_style;
|
|
1945
|
-
VALUE current_parent_id;
|
|
1946
|
-
|
|
1947
|
-
if (!NIL_P(parent_selector)) {
|
|
1948
|
-
VALUE result = resolve_nested_selector(parent_selector, RSTRING_PTR(current_selector), RSTRING_LEN(current_selector));
|
|
1949
|
-
resolved_current = rb_ary_entry(result, 0);
|
|
1950
|
-
current_nesting_style = rb_ary_entry(result, 1);
|
|
1951
|
-
current_parent_id = parent_rule_id;
|
|
1952
|
-
} else {
|
|
1953
|
-
resolved_current = current_selector;
|
|
1954
|
-
current_nesting_style = Qnil;
|
|
1955
|
-
current_parent_id = Qnil;
|
|
1956
|
-
}
|
|
1957
|
-
|
|
1958
|
-
// Get rule ID for current selector (increment to reserve it)
|
|
1959
|
-
int current_rule_id = ctx->rule_id_counter++;
|
|
1960
|
-
|
|
1961
|
-
// Reserve parent's position in rules array with placeholder
|
|
1962
|
-
// This ensures parent comes before nested rules in array order (per W3C spec)
|
|
1963
|
-
long parent_position = RARRAY_LEN(ctx->rules_array);
|
|
1964
|
-
rb_ary_push(ctx->rules_array, Qnil);
|
|
1965
|
-
|
|
1966
|
-
// Parse mixed block (declarations + nested selectors)
|
|
1967
|
-
// Nested rules will be added AFTER the placeholder
|
|
1968
|
-
ctx->depth++;
|
|
1969
|
-
VALUE parent_declarations = parse_mixed_block(ctx, decl_start, p,
|
|
1970
|
-
resolved_current, INT2FIX(current_rule_id), parent_media_sym, parent_media_query_id);
|
|
1971
|
-
ctx->depth--;
|
|
1972
|
-
|
|
1973
|
-
// Determine selector_list_id value
|
|
1974
|
-
VALUE selector_list_id_val = (list_id >= 0) ? INT2FIX(list_id) : Qnil;
|
|
1975
|
-
|
|
1976
|
-
// Create parent rule and replace placeholder
|
|
1977
|
-
// Always create the rule (even if empty) to avoid edge cases
|
|
1978
|
-
VALUE media_query_id_val = (parent_media_query_id >= 0) ? INT2FIX(parent_media_query_id) : Qnil;
|
|
1979
|
-
VALUE rule = rb_struct_new(cRule,
|
|
1980
|
-
INT2FIX(current_rule_id),
|
|
1981
|
-
resolved_current,
|
|
1982
|
-
parent_declarations,
|
|
1983
|
-
Qnil, // specificity
|
|
1984
|
-
current_parent_id,
|
|
1985
|
-
current_nesting_style,
|
|
1986
|
-
selector_list_id_val,
|
|
1987
|
-
media_query_id_val // media_query_id from parent context
|
|
1988
|
-
);
|
|
1989
|
-
|
|
1990
|
-
// Track rule in selector list if applicable
|
|
1991
|
-
if (list_id >= 0) {
|
|
1992
|
-
rb_ary_push(rule_ids_array, INT2FIX(current_rule_id));
|
|
1993
|
-
}
|
|
1994
|
-
|
|
1995
|
-
// Mark that we have nesting (only set once)
|
|
1996
|
-
if (!ctx->has_nesting && !NIL_P(current_parent_id)) {
|
|
1997
|
-
ctx->has_nesting = 1;
|
|
1998
|
-
}
|
|
1999
|
-
|
|
2000
|
-
// Replace placeholder with actual rule - just pointer assignment, fast!
|
|
2001
|
-
rb_ary_store(ctx->rules_array, parent_position, rule);
|
|
2002
|
-
update_media_index(ctx, parent_media_sym, current_rule_id);
|
|
2003
|
-
}
|
|
2004
|
-
|
|
2005
|
-
seg_start = seg + 1;
|
|
2006
|
-
}
|
|
2007
|
-
seg++;
|
|
2008
|
-
}
|
|
2009
|
-
}
|
|
2010
|
-
|
|
2291
|
+
finish_rule_block(ctx, selector_start, decl_start, p,
|
|
2292
|
+
parent_selector, parent_rule_id, parent_media_sym, parent_media_query_id);
|
|
2011
2293
|
selector_start = NULL;
|
|
2012
2294
|
decl_start = NULL;
|
|
2013
2295
|
}
|
|
@@ -2023,6 +2305,11 @@ static void parse_css_recursive(ParserContext *ctx, const char *css, const char
|
|
|
2023
2305
|
|
|
2024
2306
|
p++;
|
|
2025
2307
|
}
|
|
2308
|
+
|
|
2309
|
+
// Check for unclosed blocks at end of parsing
|
|
2310
|
+
if (ctx->check_unclosed_blocks && brace_depth > 0) {
|
|
2311
|
+
rb_raise(eParseError, "Unclosed block: missing closing brace");
|
|
2312
|
+
}
|
|
2026
2313
|
}
|
|
2027
2314
|
|
|
2028
2315
|
/*
|
|
@@ -2056,13 +2343,48 @@ VALUE parse_css_new_impl(VALUE css_string, VALUE parser_options, int rule_id_off
|
|
|
2056
2343
|
|
|
2057
2344
|
// Read parser options
|
|
2058
2345
|
VALUE selector_lists_opt = rb_hash_aref(parser_options, ID2SYM(rb_intern("selector_lists")));
|
|
2059
|
-
|
|
2346
|
+
BOOLEAN selector_lists_enabled = (NIL_P(selector_lists_opt) || RTEST(selector_lists_opt)) ? 1 : 0;
|
|
2060
2347
|
|
|
2061
2348
|
// URL conversion options
|
|
2062
2349
|
VALUE base_uri = rb_hash_aref(parser_options, ID2SYM(rb_intern("base_uri")));
|
|
2063
2350
|
VALUE absolute_paths_opt = rb_hash_aref(parser_options, ID2SYM(rb_intern("absolute_paths")));
|
|
2064
2351
|
VALUE uri_resolver = rb_hash_aref(parser_options, ID2SYM(rb_intern("uri_resolver")));
|
|
2065
|
-
|
|
2352
|
+
BOOLEAN absolute_paths = RTEST(absolute_paths_opt) ? 1 : 0;
|
|
2353
|
+
|
|
2354
|
+
// Parse error options
|
|
2355
|
+
VALUE raise_parse_errors_opt = rb_hash_aref(parser_options, ID2SYM(rb_intern("raise_parse_errors")));
|
|
2356
|
+
BOOLEAN check_empty_values = 0;
|
|
2357
|
+
BOOLEAN check_malformed_declarations = 0;
|
|
2358
|
+
BOOLEAN check_invalid_selectors = 0;
|
|
2359
|
+
BOOLEAN check_invalid_selector_syntax = 0;
|
|
2360
|
+
BOOLEAN check_malformed_at_rules = 0;
|
|
2361
|
+
BOOLEAN check_unclosed_blocks = 0;
|
|
2362
|
+
|
|
2363
|
+
if (RTEST(raise_parse_errors_opt)) {
|
|
2364
|
+
if (TYPE(raise_parse_errors_opt) == T_HASH) {
|
|
2365
|
+
// Hash of specific error types
|
|
2366
|
+
VALUE empty_values_opt = rb_hash_aref(raise_parse_errors_opt, ID2SYM(rb_intern("empty_values")));
|
|
2367
|
+
VALUE malformed_declarations_opt = rb_hash_aref(raise_parse_errors_opt, ID2SYM(rb_intern("malformed_declarations")));
|
|
2368
|
+
VALUE invalid_selectors_opt = rb_hash_aref(raise_parse_errors_opt, ID2SYM(rb_intern("invalid_selectors")));
|
|
2369
|
+
VALUE invalid_selector_syntax_opt = rb_hash_aref(raise_parse_errors_opt, ID2SYM(rb_intern("invalid_selector_syntax")));
|
|
2370
|
+
VALUE malformed_at_rules_opt = rb_hash_aref(raise_parse_errors_opt, ID2SYM(rb_intern("malformed_at_rules")));
|
|
2371
|
+
VALUE unclosed_blocks_opt = rb_hash_aref(raise_parse_errors_opt, ID2SYM(rb_intern("unclosed_blocks")));
|
|
2372
|
+
check_empty_values = RTEST(empty_values_opt) ? 1 : 0;
|
|
2373
|
+
check_malformed_declarations = RTEST(malformed_declarations_opt) ? 1 : 0;
|
|
2374
|
+
check_invalid_selectors = RTEST(invalid_selectors_opt) ? 1 : 0;
|
|
2375
|
+
check_invalid_selector_syntax = RTEST(invalid_selector_syntax_opt) ? 1 : 0;
|
|
2376
|
+
check_malformed_at_rules = RTEST(malformed_at_rules_opt) ? 1 : 0;
|
|
2377
|
+
check_unclosed_blocks = RTEST(unclosed_blocks_opt) ? 1 : 0;
|
|
2378
|
+
} else {
|
|
2379
|
+
// true - enable all checks
|
|
2380
|
+
check_empty_values = 1;
|
|
2381
|
+
check_malformed_declarations = 1;
|
|
2382
|
+
check_invalid_selectors = 1;
|
|
2383
|
+
check_invalid_selector_syntax = 1;
|
|
2384
|
+
check_malformed_at_rules = 1;
|
|
2385
|
+
check_unclosed_blocks = 1;
|
|
2386
|
+
}
|
|
2387
|
+
}
|
|
2066
2388
|
|
|
2067
2389
|
const char *css = RSTRING_PTR(css_string);
|
|
2068
2390
|
const char *pe = css + RSTRING_LEN(css_string);
|
|
@@ -2107,7 +2429,6 @@ VALUE parse_css_new_impl(VALUE css_string, VALUE parser_options, int rule_id_off
|
|
|
2107
2429
|
ctx.media_query_id_counter = 0; // Start from 0
|
|
2108
2430
|
ctx.next_media_query_list_id = 0; // Start from 0
|
|
2109
2431
|
ctx.media_query_count = 0;
|
|
2110
|
-
ctx.media_cache = NULL; // Removed - no perf benefit
|
|
2111
2432
|
ctx.has_nesting = 0; // Will be set to 1 if any nested rules are created
|
|
2112
2433
|
ctx.selector_lists_enabled = selector_lists_enabled;
|
|
2113
2434
|
ctx.depth = 0; // Start at depth 0
|
|
@@ -2115,6 +2436,14 @@ VALUE parse_css_new_impl(VALUE css_string, VALUE parser_options, int rule_id_off
|
|
|
2115
2436
|
ctx.base_uri = base_uri;
|
|
2116
2437
|
ctx.uri_resolver = uri_resolver;
|
|
2117
2438
|
ctx.absolute_paths = absolute_paths;
|
|
2439
|
+
// Parse error options
|
|
2440
|
+
ctx.css_string = css_string;
|
|
2441
|
+
ctx.check_empty_values = check_empty_values;
|
|
2442
|
+
ctx.check_malformed_declarations = check_malformed_declarations;
|
|
2443
|
+
ctx.check_invalid_selectors = check_invalid_selectors;
|
|
2444
|
+
ctx.check_invalid_selector_syntax = check_invalid_selector_syntax;
|
|
2445
|
+
ctx.check_malformed_at_rules = check_malformed_at_rules;
|
|
2446
|
+
ctx.check_unclosed_blocks = check_unclosed_blocks;
|
|
2118
2447
|
|
|
2119
2448
|
// Parse CSS (top-level, no parent context)
|
|
2120
2449
|
DEBUG_PRINTF("[PARSE] Starting parse_css_recursive from: %.80s\n", p);
|