cataract 0.2.5 → 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/CHANGELOG.md +19 -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 +247 -454
- data/ext/cataract/cataract.h +126 -15
- data/ext/cataract/css_parser.c +782 -788
- 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/import_resolver.rb +36 -16
- data/lib/cataract/pure/byte_constants.rb +0 -1
- data/lib/cataract/pure/flatten.rb +143 -145
- data/lib/cataract/pure/parser.rb +495 -422
- data/lib/cataract/pure/serializer.rb +109 -104
- data/lib/cataract/pure/specificity.rb +112 -156
- data/lib/cataract/pure.rb +0 -7
- data/lib/cataract/stylesheet.rb +460 -412
- data/lib/cataract/version.rb +1 -1
- metadata +2 -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
|
@@ -31,7 +31,6 @@ typedef struct {
|
|
|
31
31
|
int media_query_id_counter; // Next MediaQuery ID (0-indexed)
|
|
32
32
|
int next_media_query_list_id; // Next media query list ID (0-indexed)
|
|
33
33
|
int media_query_count; // Safety limit for media queries
|
|
34
|
-
st_table *media_cache; // Parse-time cache: string => parsed media types
|
|
35
34
|
BOOLEAN has_nesting; // Set to 1 if any nested rules are created
|
|
36
35
|
BOOLEAN selector_lists_enabled; // Parser option: track selector lists (1=enabled, 0=disabled)
|
|
37
36
|
BOOLEAN depth; // Current recursion depth (safety limit)
|
|
@@ -49,6 +48,38 @@ typedef struct {
|
|
|
49
48
|
BOOLEAN check_unclosed_blocks; // Raise error on missing closing braces
|
|
50
49
|
} ParserContext;
|
|
51
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
|
+
|
|
52
83
|
// Macro to skip CSS comments /* ... */
|
|
53
84
|
// Usage: SKIP_COMMENT(p, end) where p is current position, end is limit
|
|
54
85
|
// Side effect: advances p past the comment and continues to next iteration
|
|
@@ -718,14 +749,17 @@ static VALUE parse_declarations(const char *start, const char *end, ParserContex
|
|
|
718
749
|
}
|
|
719
750
|
if (pos >= end) break;
|
|
720
751
|
|
|
721
|
-
//
|
|
722
|
-
//
|
|
723
|
-
//
|
|
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.
|
|
724
759
|
const char *prop_start = pos;
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
if (pos >= end || *pos != ':') {
|
|
760
|
+
struct declaration_span span;
|
|
761
|
+
if (!parse_one_declaration(&pos, end, 1, &span)) {
|
|
762
|
+
// Malformed declaration - skip to next semicolon to recover
|
|
729
763
|
if (ctx->check_malformed_declarations) {
|
|
730
764
|
// Extract property text for error message
|
|
731
765
|
const char *prop_text_end = pos;
|
|
@@ -771,74 +805,18 @@ static VALUE parse_declarations(const char *start, const char *end, ParserContex
|
|
|
771
805
|
continue;
|
|
772
806
|
}
|
|
773
807
|
|
|
774
|
-
const char *prop_end = pos;
|
|
775
|
-
// Trim whitespace from property
|
|
776
|
-
trim_trailing(prop_start, &prop_end);
|
|
777
|
-
trim_leading(&prop_start, prop_end);
|
|
778
|
-
|
|
779
|
-
pos++; // Skip colon
|
|
780
|
-
|
|
781
|
-
// Skip whitespace after colon
|
|
782
|
-
while (pos < end && IS_WHITESPACE(*pos)) {
|
|
783
|
-
pos++;
|
|
784
|
-
}
|
|
785
|
-
|
|
786
|
-
// Find value (up to semicolon or end)
|
|
787
|
-
// Must track paren depth to avoid breaking on semicolons inside url() or rgba()
|
|
788
|
-
// Example: "url(data:image/svg+xml;base64,...); next-prop: ..."
|
|
789
|
-
// ^val_start ^pos (at ; outside parens)
|
|
790
|
-
const char *val_start = pos;
|
|
791
|
-
int paren_depth = 0;
|
|
792
|
-
while (pos < end) {
|
|
793
|
-
if (*pos == '(') { // At: '('
|
|
794
|
-
paren_depth++; // Depth: 1
|
|
795
|
-
} else if (*pos == ')') { // At: ')'
|
|
796
|
-
paren_depth--; // Depth: 0
|
|
797
|
-
} else if (*pos == ';' && paren_depth == 0) { // At: ';' (outside parens)
|
|
798
|
-
break; // Found terminating semicolon
|
|
799
|
-
}
|
|
800
|
-
pos++;
|
|
801
|
-
}
|
|
802
|
-
const char *val_end = pos;
|
|
803
|
-
|
|
804
|
-
// Trim trailing whitespace from value
|
|
805
|
-
trim_trailing(val_start, &val_end);
|
|
806
|
-
|
|
807
|
-
// Check for !important
|
|
808
|
-
BOOLEAN is_important = 0;
|
|
809
|
-
if (val_end - val_start >= 10) { // strlen("!important") = 10
|
|
810
|
-
const char *check = val_end - 10;
|
|
811
|
-
while (check < val_end && IS_WHITESPACE(*check)) check++;
|
|
812
|
-
if (check < val_end && *check == '!') {
|
|
813
|
-
check++;
|
|
814
|
-
while (check < val_end && IS_WHITESPACE(*check)) check++;
|
|
815
|
-
// strncmp safely handles remaining length check
|
|
816
|
-
if (check + 9 <= val_end && strncmp(check, "important", 9) == 0) {
|
|
817
|
-
is_important = 1;
|
|
818
|
-
const char *important_pos = check - 1;
|
|
819
|
-
while (important_pos > val_start && (IS_WHITESPACE(*(important_pos-1)) || *(important_pos-1) == '!')) {
|
|
820
|
-
important_pos--;
|
|
821
|
-
}
|
|
822
|
-
val_end = important_pos;
|
|
823
|
-
}
|
|
824
|
-
}
|
|
825
|
-
}
|
|
826
|
-
|
|
827
|
-
// Final trim
|
|
828
|
-
trim_trailing(val_start, &val_end);
|
|
829
|
-
|
|
830
808
|
// Check for empty value
|
|
831
|
-
if (val_end <= val_start && ctx->check_empty_values) {
|
|
832
|
-
long prop_len = prop_end - prop_start;
|
|
809
|
+
if (span.val_end <= span.val_start && ctx->check_empty_values) {
|
|
810
|
+
long prop_len = span.prop_end - span.prop_start;
|
|
833
811
|
const char *css = RSTRING_PTR(ctx->css_string);
|
|
834
|
-
long error_pos = val_start - css;
|
|
812
|
+
long error_pos = span.val_start - css;
|
|
835
813
|
|
|
836
814
|
// Build error message
|
|
837
815
|
int display_len = (prop_len > 200) ? 200 : (int)prop_len;
|
|
838
816
|
char error_msg[256];
|
|
839
817
|
snprintf(error_msg, sizeof(error_msg),
|
|
840
818
|
"Empty value for property '%.*s'",
|
|
841
|
-
display_len, prop_start);
|
|
819
|
+
display_len, span.prop_start);
|
|
842
820
|
|
|
843
821
|
// Build keyword args hash
|
|
844
822
|
VALUE kwargs = rb_hash_new();
|
|
@@ -856,9 +834,9 @@ static VALUE parse_declarations(const char *start, const char *end, ParserContex
|
|
|
856
834
|
}
|
|
857
835
|
|
|
858
836
|
// Skip if value is empty
|
|
859
|
-
if (val_end > val_start) {
|
|
860
|
-
long prop_len = prop_end - prop_start;
|
|
861
|
-
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;
|
|
862
840
|
|
|
863
841
|
// Check property name length
|
|
864
842
|
if (prop_len > MAX_PROPERTY_NAME_LENGTH) {
|
|
@@ -875,18 +853,18 @@ static VALUE parse_declarations(const char *start, const char *end, ParserContex
|
|
|
875
853
|
}
|
|
876
854
|
|
|
877
855
|
// Create property string - use UTF-8 to support custom properties with Unicode
|
|
878
|
-
VALUE property = rb_utf8_str_new(prop_start, prop_len);
|
|
856
|
+
VALUE property = rb_utf8_str_new(span.prop_start, prop_len);
|
|
879
857
|
// Custom properties (--foo) are case-sensitive and can contain Unicode
|
|
880
858
|
// Regular properties are ASCII-only and case-insensitive
|
|
881
|
-
if (!(prop_len >= 2 && prop_start[0] == '-' && prop_start[1] == '-')) {
|
|
859
|
+
if (!(prop_len >= 2 && span.prop_start[0] == '-' && span.prop_start[1] == '-')) {
|
|
882
860
|
// Regular property: force ASCII encoding and lowercase
|
|
883
861
|
rb_enc_associate(property, rb_usascii_encoding());
|
|
884
862
|
property = lowercase_property(property);
|
|
885
863
|
}
|
|
886
|
-
VALUE value = rb_utf8_str_new(val_start, val_len);
|
|
864
|
+
VALUE value = rb_utf8_str_new(span.val_start, val_len);
|
|
887
865
|
|
|
888
866
|
// Convert relative URLs to absolute if enabled
|
|
889
|
-
if (ctx
|
|
867
|
+
if (ctx->absolute_paths && !NIL_P(ctx->base_uri)) {
|
|
890
868
|
value = convert_urls_in_value(value, ctx->base_uri, ctx->uri_resolver);
|
|
891
869
|
}
|
|
892
870
|
|
|
@@ -894,13 +872,12 @@ static VALUE parse_declarations(const char *start, const char *end, ParserContex
|
|
|
894
872
|
VALUE decl = rb_struct_new(cDeclaration,
|
|
895
873
|
property,
|
|
896
874
|
value,
|
|
897
|
-
is_important ? Qtrue : Qfalse
|
|
875
|
+
span.is_important ? Qtrue : Qfalse
|
|
898
876
|
);
|
|
899
877
|
|
|
900
878
|
rb_ary_push(declarations, decl);
|
|
901
879
|
}
|
|
902
|
-
|
|
903
|
-
if (pos < end && *pos == ';') pos++; // Skip semicolon if present
|
|
880
|
+
// parse_one_declaration already advanced pos past the ';' on success
|
|
904
881
|
}
|
|
905
882
|
|
|
906
883
|
return declarations;
|
|
@@ -989,6 +966,54 @@ static VALUE intern_media_query_safe(ParserContext *ctx, const char *query_str,
|
|
|
989
966
|
return sym;
|
|
990
967
|
}
|
|
991
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
|
+
|
|
992
1017
|
/*
|
|
993
1018
|
* Parse mixed declarations and nested selectors from a block
|
|
994
1019
|
* Used when a CSS rule block contains both declarations and nested rules
|
|
@@ -1044,31 +1069,9 @@ static VALUE parse_mixed_block(ParserContext *ctx, const char *start, const char
|
|
|
1044
1069
|
trim_trailing(media_query_start, &media_query_end_trimmed);
|
|
1045
1070
|
|
|
1046
1071
|
// Parse media query and create MediaQuery object
|
|
1047
|
-
const char *mq_ptr = media_query_start;
|
|
1048
1072
|
VALUE media_type;
|
|
1049
|
-
VALUE media_conditions
|
|
1050
|
-
|
|
1051
|
-
if (*mq_ptr == '(') {
|
|
1052
|
-
// Starts with '(' - just conditions, type defaults to :all
|
|
1053
|
-
media_type = ID2SYM(rb_intern("all"));
|
|
1054
|
-
media_conditions = rb_utf8_str_new(mq_ptr, media_query_end_trimmed - mq_ptr);
|
|
1055
|
-
} else {
|
|
1056
|
-
// Extract media type (first word)
|
|
1057
|
-
const char *type_start = mq_ptr;
|
|
1058
|
-
while (mq_ptr < media_query_end_trimmed && !IS_WHITESPACE(*mq_ptr) && *mq_ptr != '(') mq_ptr++;
|
|
1059
|
-
VALUE type_str = rb_utf8_str_new(type_start, mq_ptr - type_start);
|
|
1060
|
-
media_type = ID2SYM(rb_intern_str(type_str));
|
|
1061
|
-
|
|
1062
|
-
// Skip "and" keyword if present
|
|
1063
|
-
while (mq_ptr < media_query_end_trimmed && IS_WHITESPACE(*mq_ptr)) mq_ptr++;
|
|
1064
|
-
if (mq_ptr + 3 <= media_query_end_trimmed && strncmp(mq_ptr, "and", 3) == 0) {
|
|
1065
|
-
mq_ptr += 3;
|
|
1066
|
-
while (mq_ptr < media_query_end_trimmed && IS_WHITESPACE(*mq_ptr)) mq_ptr++;
|
|
1067
|
-
}
|
|
1068
|
-
if (mq_ptr < media_query_end_trimmed) {
|
|
1069
|
-
media_conditions = rb_utf8_str_new(mq_ptr, media_query_end_trimmed - mq_ptr);
|
|
1070
|
-
}
|
|
1071
|
-
}
|
|
1073
|
+
VALUE media_conditions;
|
|
1074
|
+
parse_single_media_query(media_query_start, media_query_end_trimmed, &media_type, &media_conditions);
|
|
1072
1075
|
|
|
1073
1076
|
// Create MediaQuery object
|
|
1074
1077
|
VALUE media_query = rb_struct_new(cMediaQuery,
|
|
@@ -1283,49 +1286,18 @@ static VALUE parse_mixed_block(ParserContext *ctx, const char *start, const char
|
|
|
1283
1286
|
}
|
|
1284
1287
|
|
|
1285
1288
|
// This is a declaration - parse it
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
if (p >= end || *p != ':') {
|
|
1289
|
+
struct declaration_span span;
|
|
1290
|
+
if (!parse_one_declaration(&p, end, 1, &span)) {
|
|
1289
1291
|
// Malformed - skip to semicolon
|
|
1290
1292
|
while (p < end && *p != ';') p++;
|
|
1291
1293
|
if (p < end) p++;
|
|
1292
1294
|
continue;
|
|
1293
1295
|
}
|
|
1294
1296
|
|
|
1295
|
-
const char *prop_end = p;
|
|
1296
|
-
trim_trailing(prop_start, &prop_end);
|
|
1297
|
-
|
|
1298
|
-
p++; // Skip :
|
|
1299
|
-
trim_leading(&p, end);
|
|
1300
|
-
|
|
1301
|
-
const char *val_start = p;
|
|
1302
|
-
BOOLEAN important = 0;
|
|
1303
|
-
|
|
1304
|
-
// Find end of value (semicolon or closing brace or end)
|
|
1305
|
-
while (p < end && *p != ';' && *p != '}') p++;
|
|
1306
|
-
const char *val_end = p;
|
|
1307
|
-
|
|
1308
|
-
// Check for !important
|
|
1309
|
-
const char *important_check = val_end - 10; // " !important"
|
|
1310
|
-
if (important_check >= val_start) {
|
|
1311
|
-
trim_trailing(val_start, &val_end);
|
|
1312
|
-
if (val_end - val_start >= 10) {
|
|
1313
|
-
if (strncmp(val_end - 10, "!important", 10) == 0) {
|
|
1314
|
-
important = 1;
|
|
1315
|
-
val_end -= 10;
|
|
1316
|
-
trim_trailing(val_start, &val_end);
|
|
1317
|
-
}
|
|
1318
|
-
}
|
|
1319
|
-
} else {
|
|
1320
|
-
trim_trailing(val_start, &val_end);
|
|
1321
|
-
}
|
|
1322
|
-
|
|
1323
|
-
if (p < end && *p == ';') p++;
|
|
1324
|
-
|
|
1325
1297
|
// Create declaration
|
|
1326
|
-
if (prop_end > prop_start && val_end > val_start) {
|
|
1327
|
-
long prop_len = prop_end - prop_start;
|
|
1328
|
-
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;
|
|
1329
1301
|
|
|
1330
1302
|
// Check property name length
|
|
1331
1303
|
if (prop_len > MAX_PROPERTY_NAME_LENGTH) {
|
|
@@ -1342,15 +1314,15 @@ static VALUE parse_mixed_block(ParserContext *ctx, const char *start, const char
|
|
|
1342
1314
|
}
|
|
1343
1315
|
|
|
1344
1316
|
// Create property string - use UTF-8 to support custom properties with Unicode
|
|
1345
|
-
VALUE property = rb_utf8_str_new(prop_start, prop_len);
|
|
1317
|
+
VALUE property = rb_utf8_str_new(span.prop_start, prop_len);
|
|
1346
1318
|
// Custom properties (--foo) are case-sensitive and can contain Unicode
|
|
1347
1319
|
// Regular properties are ASCII-only and case-insensitive
|
|
1348
|
-
if (!(prop_len >= 2 && prop_start[0] == '-' && prop_start[1] == '-')) {
|
|
1320
|
+
if (!(prop_len >= 2 && span.prop_start[0] == '-' && span.prop_start[1] == '-')) {
|
|
1349
1321
|
// Regular property: force ASCII encoding and lowercase
|
|
1350
1322
|
rb_enc_associate(property, rb_usascii_encoding());
|
|
1351
1323
|
property = lowercase_property(property);
|
|
1352
1324
|
}
|
|
1353
|
-
VALUE value = rb_utf8_str_new(val_start, val_len);
|
|
1325
|
+
VALUE value = rb_utf8_str_new(span.val_start, val_len);
|
|
1354
1326
|
|
|
1355
1327
|
// Convert relative URLs to absolute if enabled
|
|
1356
1328
|
if (ctx->absolute_paths && !NIL_P(ctx->base_uri)) {
|
|
@@ -1360,7 +1332,7 @@ static VALUE parse_mixed_block(ParserContext *ctx, const char *start, const char
|
|
|
1360
1332
|
VALUE decl = rb_struct_new(cDeclaration,
|
|
1361
1333
|
property,
|
|
1362
1334
|
value,
|
|
1363
|
-
|
|
1335
|
+
span.is_important ? Qtrue : Qfalse
|
|
1364
1336
|
);
|
|
1365
1337
|
|
|
1366
1338
|
rb_ary_push(declarations, decl);
|
|
@@ -1471,29 +1443,9 @@ static void parse_import_statement(ParserContext *ctx, const char **p_ptr, const
|
|
|
1471
1443
|
|
|
1472
1444
|
if (query_start < query_end) {
|
|
1473
1445
|
// Parse this individual media query
|
|
1474
|
-
const char *mq_ptr = query_start;
|
|
1475
1446
|
VALUE media_type;
|
|
1476
|
-
VALUE media_conditions
|
|
1477
|
-
|
|
1478
|
-
if (*mq_ptr == '(') {
|
|
1479
|
-
// Starts with '(' - just conditions, type defaults to :all
|
|
1480
|
-
media_type = ID2SYM(rb_intern("all"));
|
|
1481
|
-
media_conditions = rb_utf8_str_new(mq_ptr, query_end - mq_ptr);
|
|
1482
|
-
} else {
|
|
1483
|
-
// Extract media type (first word)
|
|
1484
|
-
const char *type_start = mq_ptr;
|
|
1485
|
-
while (mq_ptr < query_end && !IS_WHITESPACE(*mq_ptr) && *mq_ptr != '(') mq_ptr++;
|
|
1486
|
-
VALUE type_str = rb_utf8_str_new(type_start, mq_ptr - type_start);
|
|
1487
|
-
media_type = ID2SYM(rb_intern_str(type_str));
|
|
1488
|
-
|
|
1489
|
-
// Skip whitespace
|
|
1490
|
-
while (mq_ptr < query_end && IS_WHITESPACE(*mq_ptr)) mq_ptr++;
|
|
1491
|
-
|
|
1492
|
-
// Check if there are conditions (rest of string)
|
|
1493
|
-
if (mq_ptr < query_end) {
|
|
1494
|
-
media_conditions = rb_utf8_str_new(mq_ptr, query_end - mq_ptr);
|
|
1495
|
-
}
|
|
1496
|
-
}
|
|
1447
|
+
VALUE media_conditions;
|
|
1448
|
+
parse_single_media_query(query_start, query_end, &media_type, &media_conditions);
|
|
1497
1449
|
|
|
1498
1450
|
// Create MediaQuery struct
|
|
1499
1451
|
VALUE media_query = rb_struct_new(cMediaQuery,
|
|
@@ -1550,6 +1502,665 @@ static void parse_import_statement(ParserContext *ctx, const char **p_ptr, const
|
|
|
1550
1502
|
RB_GC_GUARD(import_stmt);
|
|
1551
1503
|
}
|
|
1552
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
|
+
|
|
1553
2164
|
/*
|
|
1554
2165
|
* Parse CSS recursively with media query context and optional parent selector for nesting
|
|
1555
2166
|
*
|
|
@@ -1607,166 +2218,7 @@ static void parse_css_recursive(ParserContext *ctx, const char *css, const char
|
|
|
1607
2218
|
// Check for @media at-rule (only at depth 0)
|
|
1608
2219
|
if (RB_UNLIKELY(brace_depth == 0 && p + 6 < pe && *p == '@' &&
|
|
1609
2220
|
strncmp(p + 1, "media", 5) == 0 && IS_WHITESPACE(p[6]))) {
|
|
1610
|
-
p
|
|
1611
|
-
|
|
1612
|
-
// Skip whitespace
|
|
1613
|
-
while (p < pe && IS_WHITESPACE(*p)) p++;
|
|
1614
|
-
|
|
1615
|
-
// Find media query (up to opening brace)
|
|
1616
|
-
const char *mq_start = p;
|
|
1617
|
-
while (p < pe && *p != '{') p++;
|
|
1618
|
-
const char *mq_end = p;
|
|
1619
|
-
|
|
1620
|
-
// Trim
|
|
1621
|
-
trim_trailing(mq_start, &mq_end);
|
|
1622
|
-
|
|
1623
|
-
// Check for empty media query
|
|
1624
|
-
if (mq_end <= mq_start) {
|
|
1625
|
-
if (ctx->check_malformed_at_rules) {
|
|
1626
|
-
raise_parse_error_at(ctx, mq_start, "Malformed @media: missing media query", "malformed_at_rule");
|
|
1627
|
-
} else {
|
|
1628
|
-
// Empty media query with check disabled - skip @media wrapper and parse contents as regular rules
|
|
1629
|
-
if (p >= pe || *p != '{') {
|
|
1630
|
-
continue; // Malformed structure
|
|
1631
|
-
}
|
|
1632
|
-
p++; // Skip opening {
|
|
1633
|
-
const char *block_start = p;
|
|
1634
|
-
const char *block_end = find_matching_brace_strict(p, pe, ctx->check_unclosed_blocks);
|
|
1635
|
-
p = block_end;
|
|
1636
|
-
|
|
1637
|
-
// Parse block contents with NO media query context
|
|
1638
|
-
ctx->depth++;
|
|
1639
|
-
parse_css_recursive(ctx, block_start, block_end, parent_media_sym, NO_PARENT_SELECTOR, NO_PARENT_RULE_ID, parent_media_query_id);
|
|
1640
|
-
ctx->depth--;
|
|
1641
|
-
|
|
1642
|
-
if (p < pe && *p == '}') p++;
|
|
1643
|
-
continue;
|
|
1644
|
-
}
|
|
1645
|
-
}
|
|
1646
|
-
|
|
1647
|
-
if (p >= pe || *p != '{') {
|
|
1648
|
-
continue; // Malformed
|
|
1649
|
-
}
|
|
1650
|
-
|
|
1651
|
-
// Split comma-separated media queries (e.g., "screen, print" -> ["screen", "print"])
|
|
1652
|
-
// Per W3C spec, comma acts as logical OR - each query is independent
|
|
1653
|
-
VALUE media_query_ids = rb_ary_new();
|
|
1654
|
-
|
|
1655
|
-
const char *query_start = mq_start;
|
|
1656
|
-
for (const char *p_comma = mq_start; p_comma <= mq_end; p_comma++) {
|
|
1657
|
-
if (p_comma == mq_end || *p_comma == ',') {
|
|
1658
|
-
const char *query_end = p_comma;
|
|
1659
|
-
|
|
1660
|
-
// Trim whitespace from this query
|
|
1661
|
-
while (query_start < query_end && IS_WHITESPACE(*query_start)) query_start++;
|
|
1662
|
-
while (query_end > query_start && IS_WHITESPACE(*(query_end - 1))) query_end--;
|
|
1663
|
-
|
|
1664
|
-
if (query_start < query_end) {
|
|
1665
|
-
// Parse this individual media query
|
|
1666
|
-
const char *mq_ptr = query_start;
|
|
1667
|
-
VALUE media_type;
|
|
1668
|
-
VALUE media_conditions = Qnil;
|
|
1669
|
-
|
|
1670
|
-
if (*mq_ptr == '(') {
|
|
1671
|
-
// Starts with '(' - just conditions, type defaults to :all
|
|
1672
|
-
media_type = ID2SYM(rb_intern("all"));
|
|
1673
|
-
media_conditions = rb_utf8_str_new(mq_ptr, query_end - mq_ptr);
|
|
1674
|
-
} else {
|
|
1675
|
-
// Extract media type (first word, stopping at whitespace, comma, or '(')
|
|
1676
|
-
const char *type_start = mq_ptr;
|
|
1677
|
-
while (mq_ptr < query_end && !IS_WHITESPACE(*mq_ptr) && *mq_ptr != '(') mq_ptr++;
|
|
1678
|
-
VALUE type_str = rb_utf8_str_new(type_start, mq_ptr - type_start);
|
|
1679
|
-
media_type = ID2SYM(rb_intern_str(type_str));
|
|
1680
|
-
|
|
1681
|
-
// Skip whitespace and "and" keyword if present
|
|
1682
|
-
while (mq_ptr < query_end && IS_WHITESPACE(*mq_ptr)) mq_ptr++;
|
|
1683
|
-
if (mq_ptr + 3 <= query_end && strncmp(mq_ptr, "and", 3) == 0) {
|
|
1684
|
-
mq_ptr += 3;
|
|
1685
|
-
while (mq_ptr < query_end && IS_WHITESPACE(*mq_ptr)) mq_ptr++;
|
|
1686
|
-
}
|
|
1687
|
-
|
|
1688
|
-
// Rest is conditions
|
|
1689
|
-
if (mq_ptr < query_end) {
|
|
1690
|
-
media_conditions = rb_utf8_str_new(mq_ptr, query_end - mq_ptr);
|
|
1691
|
-
}
|
|
1692
|
-
}
|
|
1693
|
-
|
|
1694
|
-
// Create MediaQuery object for this query
|
|
1695
|
-
VALUE media_query = rb_struct_new(cMediaQuery,
|
|
1696
|
-
INT2FIX(ctx->media_query_id_counter),
|
|
1697
|
-
media_type,
|
|
1698
|
-
media_conditions
|
|
1699
|
-
);
|
|
1700
|
-
rb_ary_push(ctx->media_queries, media_query);
|
|
1701
|
-
rb_ary_push(media_query_ids, INT2FIX(ctx->media_query_id_counter));
|
|
1702
|
-
ctx->media_query_id_counter++;
|
|
1703
|
-
}
|
|
1704
|
-
|
|
1705
|
-
// Move to start of next query
|
|
1706
|
-
query_start = p_comma + 1;
|
|
1707
|
-
}
|
|
1708
|
-
}
|
|
1709
|
-
|
|
1710
|
-
// If multiple queries, track them as a list for serialization
|
|
1711
|
-
int media_query_list_id = -1;
|
|
1712
|
-
if (RARRAY_LEN(media_query_ids) > 1) {
|
|
1713
|
-
media_query_list_id = ctx->next_media_query_list_id;
|
|
1714
|
-
rb_hash_aset(ctx->media_query_lists, INT2FIX(media_query_list_id), media_query_ids);
|
|
1715
|
-
ctx->next_media_query_list_id++;
|
|
1716
|
-
}
|
|
1717
|
-
|
|
1718
|
-
// Use first query ID as the primary one for rules in this block
|
|
1719
|
-
int current_media_query_id = FIX2INT(rb_ary_entry(media_query_ids, 0));
|
|
1720
|
-
|
|
1721
|
-
// Handle nested @media by combining with parent
|
|
1722
|
-
if (parent_media_query_id >= 0) {
|
|
1723
|
-
VALUE parent_mq = rb_ary_entry(ctx->media_queries, parent_media_query_id);
|
|
1724
|
-
VALUE parent_type = rb_struct_aref(parent_mq, INT2FIX(1)); // type field
|
|
1725
|
-
VALUE parent_conditions = rb_struct_aref(parent_mq, INT2FIX(2)); // conditions field
|
|
1726
|
-
|
|
1727
|
-
// Get child media query (first one in the list)
|
|
1728
|
-
VALUE child_mq = rb_ary_entry(ctx->media_queries, current_media_query_id);
|
|
1729
|
-
VALUE child_conditions = rb_struct_aref(child_mq, INT2FIX(2)); // conditions field
|
|
1730
|
-
|
|
1731
|
-
// Combined type is parent's type (outermost wins, child type ignored)
|
|
1732
|
-
VALUE combined_type = parent_type;
|
|
1733
|
-
VALUE combined_conditions;
|
|
1734
|
-
|
|
1735
|
-
if (!NIL_P(parent_conditions) && !NIL_P(child_conditions)) {
|
|
1736
|
-
combined_conditions = rb_sprintf("%"PRIsVALUE" and %"PRIsVALUE, parent_conditions, child_conditions);
|
|
1737
|
-
} else if (!NIL_P(parent_conditions)) {
|
|
1738
|
-
combined_conditions = parent_conditions;
|
|
1739
|
-
} else {
|
|
1740
|
-
combined_conditions = child_conditions;
|
|
1741
|
-
}
|
|
1742
|
-
|
|
1743
|
-
VALUE combined_mq = rb_struct_new(cMediaQuery,
|
|
1744
|
-
INT2FIX(ctx->media_query_id_counter),
|
|
1745
|
-
combined_type,
|
|
1746
|
-
combined_conditions
|
|
1747
|
-
);
|
|
1748
|
-
rb_ary_push(ctx->media_queries, combined_mq);
|
|
1749
|
-
current_media_query_id = ctx->media_query_id_counter;
|
|
1750
|
-
ctx->media_query_id_counter++;
|
|
1751
|
-
}
|
|
1752
|
-
|
|
1753
|
-
// For backwards compat, also create symbol (will be removed later)
|
|
1754
|
-
VALUE child_media_sym = intern_media_query_safe(ctx, mq_start, mq_end - mq_start);
|
|
1755
|
-
VALUE combined_media_sym = combine_media_queries(parent_media_sym, child_media_sym);
|
|
1756
|
-
|
|
1757
|
-
p++; // Skip opening {
|
|
1758
|
-
|
|
1759
|
-
// Find matching closing brace
|
|
1760
|
-
const char *block_start = p;
|
|
1761
|
-
const char *block_end = find_matching_brace_strict(p, pe, ctx->check_unclosed_blocks);
|
|
1762
|
-
p = block_end;
|
|
1763
|
-
|
|
1764
|
-
// Recursively parse @media block with new media query context
|
|
1765
|
-
ctx->depth++;
|
|
1766
|
-
parse_css_recursive(ctx, block_start, block_end, combined_media_sym, NO_PARENT_SELECTOR, NO_PARENT_RULE_ID, current_media_query_id);
|
|
1767
|
-
ctx->depth--;
|
|
1768
|
-
|
|
1769
|
-
if (p < pe && *p == '}') p++;
|
|
2221
|
+
handle_media_at_rule(ctx, &p, pe, parent_media_sym, parent_media_query_id);
|
|
1770
2222
|
continue;
|
|
1771
2223
|
}
|
|
1772
2224
|
|
|
@@ -1792,47 +2244,8 @@ static void parse_css_recursive(ParserContext *ctx, const char *css, const char
|
|
|
1792
2244
|
(at_name_len == 5 && strncmp(at_start, "scope", 5) == 0);
|
|
1793
2245
|
|
|
1794
2246
|
if (is_conditional_group) {
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
(at_name_len == 8 && strncmp(at_start, "supports", 8) == 0) ||
|
|
1798
|
-
(at_name_len == 9 && strncmp(at_start, "container", 9) == 0);
|
|
1799
|
-
|
|
1800
|
-
// Extract condition (between at-rule name and opening brace)
|
|
1801
|
-
const char *cond_start = at_name_end;
|
|
1802
|
-
while (cond_start < pe && IS_WHITESPACE(*cond_start)) cond_start++;
|
|
1803
|
-
|
|
1804
|
-
// Skip to opening brace
|
|
1805
|
-
p = at_name_end;
|
|
1806
|
-
while (p < pe && *p != '{') p++;
|
|
1807
|
-
|
|
1808
|
-
if (p >= pe || *p != '{') {
|
|
1809
|
-
continue; // Malformed
|
|
1810
|
-
}
|
|
1811
|
-
|
|
1812
|
-
// Trim condition
|
|
1813
|
-
const char *cond_end = p;
|
|
1814
|
-
while (cond_end > cond_start && IS_WHITESPACE(*(cond_end - 1))) cond_end--;
|
|
1815
|
-
|
|
1816
|
-
// Check for missing condition
|
|
1817
|
-
if (requires_condition && cond_end <= cond_start && ctx->check_malformed_at_rules) {
|
|
1818
|
-
char error_msg[100];
|
|
1819
|
-
snprintf(error_msg, sizeof(error_msg), "Malformed @%.*s: missing condition", (int)at_name_len, at_start);
|
|
1820
|
-
raise_parse_error_at(ctx, at_start - 1, error_msg, "malformed_at_rule");
|
|
1821
|
-
}
|
|
1822
|
-
|
|
1823
|
-
p++; // Skip opening {
|
|
1824
|
-
|
|
1825
|
-
// Find matching closing brace
|
|
1826
|
-
const char *block_start = p;
|
|
1827
|
-
const char *block_end = find_matching_brace_strict(p, pe, ctx->check_unclosed_blocks);
|
|
1828
|
-
p = block_end;
|
|
1829
|
-
|
|
1830
|
-
// Recursively parse block content (preserve parent media context)
|
|
1831
|
-
ctx->depth++;
|
|
1832
|
-
parse_css_recursive(ctx, block_start, block_end, parent_media_sym, parent_selector, parent_rule_id, parent_media_query_id);
|
|
1833
|
-
ctx->depth--;
|
|
1834
|
-
|
|
1835
|
-
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);
|
|
1836
2249
|
continue;
|
|
1837
2250
|
}
|
|
1838
2251
|
|
|
@@ -1844,70 +2257,7 @@ static void parse_css_recursive(ParserContext *ctx, const char *css, const char
|
|
|
1844
2257
|
(at_name_len == 13 && strncmp(at_start, "-moz-keyframes", 13) == 0);
|
|
1845
2258
|
|
|
1846
2259
|
if (is_keyframes) {
|
|
1847
|
-
|
|
1848
|
-
const char *selector_start = p; // Points to '@'
|
|
1849
|
-
p = at_name_end;
|
|
1850
|
-
while (p < pe && *p != '{') p++;
|
|
1851
|
-
|
|
1852
|
-
if (p >= pe || *p != '{') {
|
|
1853
|
-
continue; // Malformed
|
|
1854
|
-
}
|
|
1855
|
-
|
|
1856
|
-
const char *selector_end = p;
|
|
1857
|
-
while (selector_end > selector_start && IS_WHITESPACE(*(selector_end - 1))) {
|
|
1858
|
-
selector_end--;
|
|
1859
|
-
}
|
|
1860
|
-
VALUE selector = rb_utf8_str_new(selector_start, selector_end - selector_start);
|
|
1861
|
-
|
|
1862
|
-
p++; // Skip opening {
|
|
1863
|
-
|
|
1864
|
-
// Find matching closing brace
|
|
1865
|
-
const char *block_start = p;
|
|
1866
|
-
const char *block_end = find_matching_brace_strict(p, pe, ctx->check_unclosed_blocks);
|
|
1867
|
-
p = block_end;
|
|
1868
|
-
|
|
1869
|
-
// Parse keyframe blocks as rules (from/to/0%/50% etc)
|
|
1870
|
-
ParserContext nested_ctx = {
|
|
1871
|
-
.rules_array = rb_ary_new(),
|
|
1872
|
-
.media_index = rb_hash_new(),
|
|
1873
|
-
.selector_lists = rb_hash_new(),
|
|
1874
|
-
.imports_array = rb_ary_new(),
|
|
1875
|
-
.rule_id_counter = 0,
|
|
1876
|
-
.next_selector_list_id = 0,
|
|
1877
|
-
.media_query_count = 0,
|
|
1878
|
-
.media_cache = NULL,
|
|
1879
|
-
.has_nesting = 0,
|
|
1880
|
-
.selector_lists_enabled = ctx->selector_lists_enabled,
|
|
1881
|
-
.depth = 0
|
|
1882
|
-
};
|
|
1883
|
-
parse_css_recursive(&nested_ctx, block_start, block_end, NO_PARENT_MEDIA, NO_PARENT_SELECTOR, NO_PARENT_RULE_ID, NO_MEDIA_QUERY_ID);
|
|
1884
|
-
|
|
1885
|
-
// Get rule ID and increment
|
|
1886
|
-
int rule_id = ctx->rule_id_counter++;
|
|
1887
|
-
|
|
1888
|
-
// Create AtRule with nested rules
|
|
1889
|
-
VALUE at_rule = rb_struct_new(cAtRule,
|
|
1890
|
-
INT2FIX(rule_id),
|
|
1891
|
-
selector,
|
|
1892
|
-
nested_ctx.rules_array, // Array of Rule (keyframe blocks)
|
|
1893
|
-
Qnil, // specificity
|
|
1894
|
-
Qnil // media_query_id
|
|
1895
|
-
);
|
|
1896
|
-
|
|
1897
|
-
// Add to rules array
|
|
1898
|
-
rb_ary_push(ctx->rules_array, at_rule);
|
|
1899
|
-
|
|
1900
|
-
// Add to media index if in media query
|
|
1901
|
-
if (!NIL_P(parent_media_sym)) {
|
|
1902
|
-
VALUE rule_ids = rb_hash_aref(ctx->media_index, parent_media_sym);
|
|
1903
|
-
if (NIL_P(rule_ids)) {
|
|
1904
|
-
rule_ids = rb_ary_new();
|
|
1905
|
-
rb_hash_aset(ctx->media_index, parent_media_sym, rule_ids);
|
|
1906
|
-
}
|
|
1907
|
-
rb_ary_push(rule_ids, INT2FIX(rule_id));
|
|
1908
|
-
}
|
|
1909
|
-
|
|
1910
|
-
if (p < pe && *p == '}') p++;
|
|
2260
|
+
handle_keyframes_at_rule(ctx, &p, pe, at_name_end, parent_media_sym, parent_media_query_id);
|
|
1911
2261
|
continue;
|
|
1912
2262
|
}
|
|
1913
2263
|
|
|
@@ -1915,57 +2265,7 @@ static void parse_css_recursive(ParserContext *ctx, const char *css, const char
|
|
|
1915
2265
|
BOOLEAN is_font_face = (at_name_len == 9 && strncmp(at_start, "font-face", 9) == 0);
|
|
1916
2266
|
|
|
1917
2267
|
if (is_font_face) {
|
|
1918
|
-
|
|
1919
|
-
const char *selector_start = p; // Points to '@'
|
|
1920
|
-
p = at_name_end;
|
|
1921
|
-
while (p < pe && *p != '{') p++;
|
|
1922
|
-
|
|
1923
|
-
if (p >= pe || *p != '{') {
|
|
1924
|
-
continue; // Malformed
|
|
1925
|
-
}
|
|
1926
|
-
|
|
1927
|
-
const char *selector_end = p;
|
|
1928
|
-
while (selector_end > selector_start && IS_WHITESPACE(*(selector_end - 1))) {
|
|
1929
|
-
selector_end--;
|
|
1930
|
-
}
|
|
1931
|
-
VALUE selector = rb_utf8_str_new(selector_start, selector_end - selector_start);
|
|
1932
|
-
|
|
1933
|
-
p++; // Skip opening {
|
|
1934
|
-
|
|
1935
|
-
// Find matching closing brace
|
|
1936
|
-
const char *decl_start = p;
|
|
1937
|
-
const char *decl_end = find_matching_brace_strict(p, pe, ctx->check_unclosed_blocks);
|
|
1938
|
-
p = decl_end;
|
|
1939
|
-
|
|
1940
|
-
// Parse declarations
|
|
1941
|
-
VALUE declarations = parse_declarations(decl_start, decl_end, ctx);
|
|
1942
|
-
|
|
1943
|
-
// Get rule ID and increment
|
|
1944
|
-
int rule_id = ctx->rule_id_counter++;
|
|
1945
|
-
|
|
1946
|
-
// Create AtRule with declarations
|
|
1947
|
-
VALUE at_rule = rb_struct_new(cAtRule,
|
|
1948
|
-
INT2FIX(rule_id),
|
|
1949
|
-
selector,
|
|
1950
|
-
declarations, // Array of Declaration
|
|
1951
|
-
Qnil, // specificity
|
|
1952
|
-
Qnil // media_query_id
|
|
1953
|
-
);
|
|
1954
|
-
|
|
1955
|
-
// Add to rules array
|
|
1956
|
-
rb_ary_push(ctx->rules_array, at_rule);
|
|
1957
|
-
|
|
1958
|
-
// Add to media index if in media query
|
|
1959
|
-
if (!NIL_P(parent_media_sym)) {
|
|
1960
|
-
VALUE rule_ids = rb_hash_aref(ctx->media_index, parent_media_sym);
|
|
1961
|
-
if (NIL_P(rule_ids)) {
|
|
1962
|
-
rule_ids = rb_ary_new();
|
|
1963
|
-
rb_hash_aset(ctx->media_index, parent_media_sym, rule_ids);
|
|
1964
|
-
}
|
|
1965
|
-
rb_ary_push(rule_ids, INT2FIX(rule_id));
|
|
1966
|
-
}
|
|
1967
|
-
|
|
1968
|
-
if (p < pe && *p == '}') p++;
|
|
2268
|
+
handle_font_face_at_rule(ctx, &p, pe, at_name_end, parent_media_sym, parent_media_query_id);
|
|
1969
2269
|
continue;
|
|
1970
2270
|
}
|
|
1971
2271
|
}
|
|
@@ -1988,313 +2288,8 @@ static void parse_css_recursive(ParserContext *ctx, const char *css, const char
|
|
|
1988
2288
|
if (*p == '}') {
|
|
1989
2289
|
brace_depth--;
|
|
1990
2290
|
if (brace_depth == 0 && selector_start != NULL && decl_start != NULL) {
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
// ^selector_start ^decl_start ^p (at })
|
|
1994
|
-
BOOLEAN has_nesting = has_nested_selectors(decl_start, p);
|
|
1995
|
-
|
|
1996
|
-
// Get selector string
|
|
1997
|
-
const char *sel_end = decl_start - 1;
|
|
1998
|
-
while (sel_end > selector_start && IS_WHITESPACE(*(sel_end - 1))) {
|
|
1999
|
-
sel_end--;
|
|
2000
|
-
}
|
|
2001
|
-
|
|
2002
|
-
// Check for empty selector
|
|
2003
|
-
if (ctx->check_invalid_selectors && sel_end <= selector_start) {
|
|
2004
|
-
const char *css = RSTRING_PTR(ctx->css_string);
|
|
2005
|
-
long error_pos = selector_start - css;
|
|
2006
|
-
|
|
2007
|
-
// Build keyword args hash
|
|
2008
|
-
VALUE kwargs = rb_hash_new();
|
|
2009
|
-
rb_hash_aset(kwargs, ID2SYM(rb_intern("css")), ctx->css_string);
|
|
2010
|
-
rb_hash_aset(kwargs, ID2SYM(rb_intern("pos")), LONG2NUM(error_pos));
|
|
2011
|
-
rb_hash_aset(kwargs, ID2SYM(rb_intern("type")), ID2SYM(rb_intern("invalid_selector")));
|
|
2012
|
-
|
|
2013
|
-
VALUE msg_str = rb_str_new_cstr("Invalid selector: empty selector");
|
|
2014
|
-
VALUE argv[2] = {msg_str, kwargs};
|
|
2015
|
-
VALUE error = rb_funcallv_kw(eParseError, rb_intern("new"), 2, argv, RB_PASS_KEYWORDS);
|
|
2016
|
-
rb_exc_raise(error);
|
|
2017
|
-
}
|
|
2018
|
-
|
|
2019
|
-
if (!has_nesting) {
|
|
2020
|
-
// FAST PATH: No nesting - parse as pure declarations
|
|
2021
|
-
VALUE declarations = parse_declarations(decl_start, p, ctx);
|
|
2022
|
-
|
|
2023
|
-
// Split on commas to handle multi-selector rules
|
|
2024
|
-
// Example: ".a, .b, .c { color: red; }" creates 3 separate rules
|
|
2025
|
-
// ^selector_start ^sel_end
|
|
2026
|
-
// ^seg_start=seg (scanning for commas)
|
|
2027
|
-
|
|
2028
|
-
// Count selectors for selector list tracking
|
|
2029
|
-
int selector_count = 1;
|
|
2030
|
-
if (ctx->selector_lists_enabled) {
|
|
2031
|
-
const char *count_ptr = selector_start;
|
|
2032
|
-
while (count_ptr < sel_end) {
|
|
2033
|
-
if (*count_ptr == ',') {
|
|
2034
|
-
selector_count++;
|
|
2035
|
-
}
|
|
2036
|
-
count_ptr++;
|
|
2037
|
-
}
|
|
2038
|
-
}
|
|
2039
|
-
|
|
2040
|
-
// Create selector list if enabled and multiple selectors
|
|
2041
|
-
int list_id = -1;
|
|
2042
|
-
VALUE rule_ids_array = Qnil;
|
|
2043
|
-
if (ctx->selector_lists_enabled && selector_count > 1) {
|
|
2044
|
-
list_id = ctx->next_selector_list_id++;
|
|
2045
|
-
rule_ids_array = rb_ary_new();
|
|
2046
|
-
rb_hash_aset(ctx->selector_lists, INT2FIX(list_id), rule_ids_array);
|
|
2047
|
-
}
|
|
2048
|
-
|
|
2049
|
-
const char *seg_start = selector_start;
|
|
2050
|
-
const char *seg = selector_start;
|
|
2051
|
-
|
|
2052
|
-
while (seg <= sel_end) {
|
|
2053
|
-
if (seg == sel_end || *seg == ',') { // At: ',' or end
|
|
2054
|
-
// Trim segment
|
|
2055
|
-
while (seg_start < seg && IS_WHITESPACE(*seg_start)) {
|
|
2056
|
-
seg_start++;
|
|
2057
|
-
}
|
|
2058
|
-
|
|
2059
|
-
const char *seg_end_ptr = seg;
|
|
2060
|
-
while (seg_end_ptr > seg_start && IS_WHITESPACE(*(seg_end_ptr - 1))) {
|
|
2061
|
-
seg_end_ptr--;
|
|
2062
|
-
}
|
|
2063
|
-
|
|
2064
|
-
if (seg_end_ptr > seg_start) {
|
|
2065
|
-
// Check for invalid selectors
|
|
2066
|
-
if (ctx->check_invalid_selectors) {
|
|
2067
|
-
// Check if selector starts with combinator
|
|
2068
|
-
char first_char = *seg_start;
|
|
2069
|
-
if (first_char == '>' || first_char == '+' || first_char == '~') {
|
|
2070
|
-
const char *css = RSTRING_PTR(ctx->css_string);
|
|
2071
|
-
long error_pos = seg_start - css;
|
|
2072
|
-
|
|
2073
|
-
char error_msg[256];
|
|
2074
|
-
snprintf(error_msg, sizeof(error_msg),
|
|
2075
|
-
"Invalid selector: selector cannot start with combinator '%c'",
|
|
2076
|
-
first_char);
|
|
2077
|
-
|
|
2078
|
-
// Build keyword args hash
|
|
2079
|
-
VALUE kwargs = rb_hash_new();
|
|
2080
|
-
rb_hash_aset(kwargs, ID2SYM(rb_intern("css")), ctx->css_string);
|
|
2081
|
-
rb_hash_aset(kwargs, ID2SYM(rb_intern("pos")), LONG2NUM(error_pos));
|
|
2082
|
-
rb_hash_aset(kwargs, ID2SYM(rb_intern("type")), ID2SYM(rb_intern("invalid_selector")));
|
|
2083
|
-
|
|
2084
|
-
VALUE msg_str = rb_str_new_cstr(error_msg);
|
|
2085
|
-
VALUE argv[2] = {msg_str, kwargs};
|
|
2086
|
-
VALUE error = rb_funcallv_kw(eParseError, rb_intern("new"), 2, argv, RB_PASS_KEYWORDS);
|
|
2087
|
-
rb_exc_raise(error);
|
|
2088
|
-
}
|
|
2089
|
-
}
|
|
2090
|
-
|
|
2091
|
-
// Check for invalid selector syntax (whitelist validation)
|
|
2092
|
-
if (ctx->check_invalid_selector_syntax && !is_valid_selector(seg_start, seg_end_ptr)) {
|
|
2093
|
-
raise_parse_error_at(ctx, seg_start, "Invalid selector syntax: selector contains invalid characters", "invalid_selector_syntax");
|
|
2094
|
-
}
|
|
2095
|
-
|
|
2096
|
-
VALUE selector = rb_utf8_str_new(seg_start, seg_end_ptr - seg_start);
|
|
2097
|
-
|
|
2098
|
-
// Resolve against parent if nested
|
|
2099
|
-
VALUE resolved_selector;
|
|
2100
|
-
VALUE nesting_style_val;
|
|
2101
|
-
VALUE parent_id_val;
|
|
2102
|
-
|
|
2103
|
-
if (!NIL_P(parent_selector)) {
|
|
2104
|
-
// This is a nested rule - resolve selector
|
|
2105
|
-
VALUE result = resolve_nested_selector(parent_selector, RSTRING_PTR(selector), RSTRING_LEN(selector));
|
|
2106
|
-
resolved_selector = rb_ary_entry(result, 0);
|
|
2107
|
-
nesting_style_val = rb_ary_entry(result, 1);
|
|
2108
|
-
parent_id_val = parent_rule_id;
|
|
2109
|
-
} else {
|
|
2110
|
-
// Top-level rule
|
|
2111
|
-
resolved_selector = selector;
|
|
2112
|
-
nesting_style_val = Qnil;
|
|
2113
|
-
parent_id_val = Qnil;
|
|
2114
|
-
}
|
|
2115
|
-
|
|
2116
|
-
// Get rule ID and increment
|
|
2117
|
-
int rule_id = ctx->rule_id_counter++;
|
|
2118
|
-
|
|
2119
|
-
// Determine selector_list_id value
|
|
2120
|
-
VALUE selector_list_id_val = (list_id >= 0) ? INT2FIX(list_id) : Qnil;
|
|
2121
|
-
|
|
2122
|
-
// Deep copy declarations for selector lists to avoid shared state
|
|
2123
|
-
// (principle of least surprise - modifying one rule shouldn't affect others)
|
|
2124
|
-
VALUE rule_declarations;
|
|
2125
|
-
if (list_id >= 0) {
|
|
2126
|
-
// Deep copy: both array and Declaration structs inside
|
|
2127
|
-
long decl_count = RARRAY_LEN(declarations);
|
|
2128
|
-
rule_declarations = rb_ary_new_capa(decl_count);
|
|
2129
|
-
for (long k = 0; k < decl_count; k++) {
|
|
2130
|
-
VALUE decl = rb_ary_entry(declarations, k);
|
|
2131
|
-
VALUE new_decl = rb_struct_new(cDeclaration,
|
|
2132
|
-
rb_struct_aref(decl, INT2FIX(DECL_PROPERTY)),
|
|
2133
|
-
rb_struct_aref(decl, INT2FIX(DECL_VALUE)),
|
|
2134
|
-
rb_struct_aref(decl, INT2FIX(DECL_IMPORTANT))
|
|
2135
|
-
);
|
|
2136
|
-
rb_ary_push(rule_declarations, new_decl);
|
|
2137
|
-
}
|
|
2138
|
-
} else {
|
|
2139
|
-
rule_declarations = rb_ary_dup(declarations);
|
|
2140
|
-
}
|
|
2141
|
-
|
|
2142
|
-
// Create Rule
|
|
2143
|
-
VALUE media_query_id_val = (parent_media_query_id >= 0) ? INT2FIX(parent_media_query_id) : Qnil;
|
|
2144
|
-
VALUE rule = rb_struct_new(cRule,
|
|
2145
|
-
INT2FIX(rule_id),
|
|
2146
|
-
resolved_selector,
|
|
2147
|
-
rule_declarations,
|
|
2148
|
-
Qnil, // specificity
|
|
2149
|
-
parent_id_val,
|
|
2150
|
-
nesting_style_val,
|
|
2151
|
-
selector_list_id_val,
|
|
2152
|
-
media_query_id_val // media_query_id from parent context
|
|
2153
|
-
);
|
|
2154
|
-
|
|
2155
|
-
// Track rule in selector list if applicable
|
|
2156
|
-
if (list_id >= 0) {
|
|
2157
|
-
rb_ary_push(rule_ids_array, INT2FIX(rule_id));
|
|
2158
|
-
}
|
|
2159
|
-
|
|
2160
|
-
// Mark that we have nesting (only set once)
|
|
2161
|
-
if (!ctx->has_nesting && !NIL_P(parent_id_val)) {
|
|
2162
|
-
ctx->has_nesting = 1;
|
|
2163
|
-
}
|
|
2164
|
-
|
|
2165
|
-
rb_ary_push(ctx->rules_array, rule);
|
|
2166
|
-
|
|
2167
|
-
// Update media index
|
|
2168
|
-
update_media_index(ctx, parent_media_sym, rule_id);
|
|
2169
|
-
} else if (ctx->check_invalid_selector_syntax && selector_count > 1) {
|
|
2170
|
-
// Empty selector in comma-separated list (e.g., "h1, , h3")
|
|
2171
|
-
raise_parse_error_at(ctx, seg_start, "Invalid selector syntax: empty selector in comma-separated list", "invalid_selector_syntax");
|
|
2172
|
-
}
|
|
2173
|
-
|
|
2174
|
-
seg_start = seg + 1;
|
|
2175
|
-
}
|
|
2176
|
-
seg++;
|
|
2177
|
-
}
|
|
2178
|
-
} else {
|
|
2179
|
-
// NESTED PATH: Parse mixed declarations + nested rules
|
|
2180
|
-
// For each comma-separated parent selector, parse the block with that parent
|
|
2181
|
-
//
|
|
2182
|
-
// Example: ".a, .b { color: red; & .child { font: 14px; } }"
|
|
2183
|
-
// ^selector_start ^sel_end
|
|
2184
|
-
// Creates:
|
|
2185
|
-
// - .a with declarations [color: red]
|
|
2186
|
-
// - .a .child with declarations [font: 14px]
|
|
2187
|
-
// - .b with declarations [color: red]
|
|
2188
|
-
// - .b .child with declarations [font: 14px]
|
|
2189
|
-
|
|
2190
|
-
// Count selectors for selector list tracking
|
|
2191
|
-
int selector_count = 1;
|
|
2192
|
-
if (ctx->selector_lists_enabled) {
|
|
2193
|
-
const char *count_ptr = selector_start;
|
|
2194
|
-
while (count_ptr < sel_end) {
|
|
2195
|
-
if (*count_ptr == ',') {
|
|
2196
|
-
selector_count++;
|
|
2197
|
-
}
|
|
2198
|
-
count_ptr++;
|
|
2199
|
-
}
|
|
2200
|
-
}
|
|
2201
|
-
|
|
2202
|
-
// Create selector list if enabled and multiple selectors
|
|
2203
|
-
int list_id = -1;
|
|
2204
|
-
VALUE rule_ids_array = Qnil;
|
|
2205
|
-
if (ctx->selector_lists_enabled && selector_count > 1) {
|
|
2206
|
-
list_id = ctx->next_selector_list_id++;
|
|
2207
|
-
rule_ids_array = rb_ary_new();
|
|
2208
|
-
rb_hash_aset(ctx->selector_lists, INT2FIX(list_id), rule_ids_array);
|
|
2209
|
-
}
|
|
2210
|
-
|
|
2211
|
-
const char *seg_start = selector_start;
|
|
2212
|
-
const char *seg = selector_start;
|
|
2213
|
-
|
|
2214
|
-
while (seg <= sel_end) {
|
|
2215
|
-
if (seg == sel_end || *seg == ',') { // At: ',' or end
|
|
2216
|
-
// Trim segment
|
|
2217
|
-
while (seg_start < seg && IS_WHITESPACE(*seg_start)) {
|
|
2218
|
-
seg_start++;
|
|
2219
|
-
}
|
|
2220
|
-
|
|
2221
|
-
const char *seg_end_ptr = seg;
|
|
2222
|
-
while (seg_end_ptr > seg_start && IS_WHITESPACE(*(seg_end_ptr - 1))) {
|
|
2223
|
-
seg_end_ptr--;
|
|
2224
|
-
}
|
|
2225
|
-
|
|
2226
|
-
if (seg_end_ptr > seg_start) {
|
|
2227
|
-
VALUE current_selector = rb_utf8_str_new(seg_start, seg_end_ptr - seg_start);
|
|
2228
|
-
|
|
2229
|
-
// Resolve against parent if we're already nested
|
|
2230
|
-
VALUE resolved_current;
|
|
2231
|
-
VALUE current_nesting_style;
|
|
2232
|
-
VALUE current_parent_id;
|
|
2233
|
-
|
|
2234
|
-
if (!NIL_P(parent_selector)) {
|
|
2235
|
-
VALUE result = resolve_nested_selector(parent_selector, RSTRING_PTR(current_selector), RSTRING_LEN(current_selector));
|
|
2236
|
-
resolved_current = rb_ary_entry(result, 0);
|
|
2237
|
-
current_nesting_style = rb_ary_entry(result, 1);
|
|
2238
|
-
current_parent_id = parent_rule_id;
|
|
2239
|
-
} else {
|
|
2240
|
-
resolved_current = current_selector;
|
|
2241
|
-
current_nesting_style = Qnil;
|
|
2242
|
-
current_parent_id = Qnil;
|
|
2243
|
-
}
|
|
2244
|
-
|
|
2245
|
-
// Get rule ID for current selector (increment to reserve it)
|
|
2246
|
-
int current_rule_id = ctx->rule_id_counter++;
|
|
2247
|
-
|
|
2248
|
-
// Reserve parent's position in rules array with placeholder
|
|
2249
|
-
// This ensures parent comes before nested rules in array order (per W3C spec)
|
|
2250
|
-
long parent_position = RARRAY_LEN(ctx->rules_array);
|
|
2251
|
-
rb_ary_push(ctx->rules_array, Qnil);
|
|
2252
|
-
|
|
2253
|
-
// Parse mixed block (declarations + nested selectors)
|
|
2254
|
-
// Nested rules will be added AFTER the placeholder
|
|
2255
|
-
ctx->depth++;
|
|
2256
|
-
VALUE parent_declarations = parse_mixed_block(ctx, decl_start, p,
|
|
2257
|
-
resolved_current, INT2FIX(current_rule_id), parent_media_sym, parent_media_query_id);
|
|
2258
|
-
ctx->depth--;
|
|
2259
|
-
|
|
2260
|
-
// Determine selector_list_id value
|
|
2261
|
-
VALUE selector_list_id_val = (list_id >= 0) ? INT2FIX(list_id) : Qnil;
|
|
2262
|
-
|
|
2263
|
-
// Create parent rule and replace placeholder
|
|
2264
|
-
// Always create the rule (even if empty) to avoid edge cases
|
|
2265
|
-
VALUE media_query_id_val = (parent_media_query_id >= 0) ? INT2FIX(parent_media_query_id) : Qnil;
|
|
2266
|
-
VALUE rule = rb_struct_new(cRule,
|
|
2267
|
-
INT2FIX(current_rule_id),
|
|
2268
|
-
resolved_current,
|
|
2269
|
-
parent_declarations,
|
|
2270
|
-
Qnil, // specificity
|
|
2271
|
-
current_parent_id,
|
|
2272
|
-
current_nesting_style,
|
|
2273
|
-
selector_list_id_val,
|
|
2274
|
-
media_query_id_val // media_query_id from parent context
|
|
2275
|
-
);
|
|
2276
|
-
|
|
2277
|
-
// Track rule in selector list if applicable
|
|
2278
|
-
if (list_id >= 0) {
|
|
2279
|
-
rb_ary_push(rule_ids_array, INT2FIX(current_rule_id));
|
|
2280
|
-
}
|
|
2281
|
-
|
|
2282
|
-
// Mark that we have nesting (only set once)
|
|
2283
|
-
if (!ctx->has_nesting && !NIL_P(current_parent_id)) {
|
|
2284
|
-
ctx->has_nesting = 1;
|
|
2285
|
-
}
|
|
2286
|
-
|
|
2287
|
-
// Replace placeholder with actual rule - just pointer assignment, fast!
|
|
2288
|
-
rb_ary_store(ctx->rules_array, parent_position, rule);
|
|
2289
|
-
update_media_index(ctx, parent_media_sym, current_rule_id);
|
|
2290
|
-
}
|
|
2291
|
-
|
|
2292
|
-
seg_start = seg + 1;
|
|
2293
|
-
}
|
|
2294
|
-
seg++;
|
|
2295
|
-
}
|
|
2296
|
-
}
|
|
2297
|
-
|
|
2291
|
+
finish_rule_block(ctx, selector_start, decl_start, p,
|
|
2292
|
+
parent_selector, parent_rule_id, parent_media_sym, parent_media_query_id);
|
|
2298
2293
|
selector_start = NULL;
|
|
2299
2294
|
decl_start = NULL;
|
|
2300
2295
|
}
|
|
@@ -2434,7 +2429,6 @@ VALUE parse_css_new_impl(VALUE css_string, VALUE parser_options, int rule_id_off
|
|
|
2434
2429
|
ctx.media_query_id_counter = 0; // Start from 0
|
|
2435
2430
|
ctx.next_media_query_list_id = 0; // Start from 0
|
|
2436
2431
|
ctx.media_query_count = 0;
|
|
2437
|
-
ctx.media_cache = NULL; // Removed - no perf benefit
|
|
2438
2432
|
ctx.has_nesting = 0; // Will be set to 1 if any nested rules are created
|
|
2439
2433
|
ctx.selector_lists_enabled = selector_lists_enabled;
|
|
2440
2434
|
ctx.depth = 0; // Start at depth 0
|