cataract 0.2.1 → 0.2.2
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/ci.yml +1 -1
- data/.rubocop.yml +2 -0
- data/BENCHMARKS.md +41 -38
- data/CHANGELOG.md +13 -0
- data/README.md +9 -3
- data/ext/cataract/cataract.c +273 -92
- data/ext/cataract/cataract.h +4 -3
- data/ext/cataract/css_parser.c +125 -11
- data/ext/cataract/flatten.c +271 -16
- data/lib/cataract/declaration.rb +19 -0
- data/lib/cataract/pure/flatten.rb +103 -8
- data/lib/cataract/pure/parser.rb +203 -139
- data/lib/cataract/pure/serializer.rb +217 -115
- data/lib/cataract/pure.rb +4 -2
- data/lib/cataract/rule.rb +39 -3
- data/lib/cataract/stylesheet.rb +137 -14
- data/lib/cataract/stylesheet_scope.rb +11 -4
- data/lib/cataract/version.rb +1 -1
- metadata +1 -1
data/ext/cataract/css_parser.c
CHANGED
|
@@ -17,11 +17,14 @@
|
|
|
17
17
|
typedef struct {
|
|
18
18
|
VALUE rules_array; // Array of Rule structs
|
|
19
19
|
VALUE media_index; // Hash: Symbol => Array of rule IDs
|
|
20
|
+
VALUE selector_lists; // Hash: list_id => Array of rule IDs
|
|
20
21
|
VALUE imports_array; // Array of ImportStatement structs
|
|
21
22
|
int rule_id_counter; // Next rule ID (0-indexed)
|
|
23
|
+
int next_selector_list_id; // Next selector list ID (0-indexed)
|
|
22
24
|
int media_query_count; // Safety limit for media queries
|
|
23
25
|
st_table *media_cache; // Parse-time cache: string => parsed media types
|
|
24
26
|
int has_nesting; // Set to 1 if any nested rules are created
|
|
27
|
+
int selector_lists_enabled; // Parser option: track selector lists (1=enabled, 0=disabled)
|
|
25
28
|
int depth; // Current recursion depth (safety limit)
|
|
26
29
|
} ParserContext;
|
|
27
30
|
|
|
@@ -499,9 +502,15 @@ static VALUE parse_declarations(const char *start, const char *end) {
|
|
|
499
502
|
val_len, MAX_PROPERTY_VALUE_LENGTH);
|
|
500
503
|
}
|
|
501
504
|
|
|
502
|
-
// Create property string
|
|
503
|
-
VALUE
|
|
504
|
-
|
|
505
|
+
// Create property string - use UTF-8 to support custom properties with Unicode
|
|
506
|
+
VALUE property = rb_utf8_str_new(prop_start, prop_len);
|
|
507
|
+
// Custom properties (--foo) are case-sensitive and can contain Unicode
|
|
508
|
+
// Regular properties are ASCII-only and case-insensitive
|
|
509
|
+
if (!(prop_len >= 2 && prop_start[0] == '-' && prop_start[1] == '-')) {
|
|
510
|
+
// Regular property: force ASCII encoding and lowercase
|
|
511
|
+
rb_enc_associate(property, rb_usascii_encoding());
|
|
512
|
+
property = lowercase_property(property);
|
|
513
|
+
}
|
|
505
514
|
VALUE value = rb_utf8_str_new(val_start, val_len);
|
|
506
515
|
|
|
507
516
|
// Create Declaration struct
|
|
@@ -691,7 +700,8 @@ static VALUE parse_mixed_block(ParserContext *ctx, const char *start, const char
|
|
|
691
700
|
media_declarations,
|
|
692
701
|
Qnil, // specificity
|
|
693
702
|
parent_rule_id, // Link to parent for nested @media serialization
|
|
694
|
-
Qnil
|
|
703
|
+
Qnil, // nesting_style (nil for @media nesting)
|
|
704
|
+
Qnil // selector_list_id
|
|
695
705
|
);
|
|
696
706
|
|
|
697
707
|
// Mark that we have nesting (only set once)
|
|
@@ -773,7 +783,8 @@ static VALUE parse_mixed_block(ParserContext *ctx, const char *start, const char
|
|
|
773
783
|
nested_declarations,
|
|
774
784
|
Qnil, // specificity
|
|
775
785
|
parent_rule_id,
|
|
776
|
-
nesting_style
|
|
786
|
+
nesting_style,
|
|
787
|
+
Qnil // selector_list_id
|
|
777
788
|
);
|
|
778
789
|
|
|
779
790
|
// Mark that we have nesting (only set once)
|
|
@@ -852,8 +863,15 @@ static VALUE parse_mixed_block(ParserContext *ctx, const char *start, const char
|
|
|
852
863
|
val_len, MAX_PROPERTY_VALUE_LENGTH);
|
|
853
864
|
}
|
|
854
865
|
|
|
855
|
-
|
|
856
|
-
VALUE property =
|
|
866
|
+
// Create property string - use UTF-8 to support custom properties with Unicode
|
|
867
|
+
VALUE property = rb_utf8_str_new(prop_start, prop_len);
|
|
868
|
+
// Custom properties (--foo) are case-sensitive and can contain Unicode
|
|
869
|
+
// Regular properties are ASCII-only and case-insensitive
|
|
870
|
+
if (!(prop_len >= 2 && prop_start[0] == '-' && prop_start[1] == '-')) {
|
|
871
|
+
// Regular property: force ASCII encoding and lowercase
|
|
872
|
+
rb_enc_associate(property, rb_usascii_encoding());
|
|
873
|
+
property = lowercase_property(property);
|
|
874
|
+
}
|
|
857
875
|
VALUE value = rb_utf8_str_new(val_start, val_len);
|
|
858
876
|
|
|
859
877
|
VALUE decl = rb_struct_new(cDeclaration,
|
|
@@ -1158,10 +1176,14 @@ static void parse_css_recursive(ParserContext *ctx, const char *css, const char
|
|
|
1158
1176
|
ParserContext nested_ctx = {
|
|
1159
1177
|
.rules_array = rb_ary_new(),
|
|
1160
1178
|
.media_index = rb_hash_new(),
|
|
1179
|
+
.selector_lists = rb_hash_new(),
|
|
1180
|
+
.imports_array = rb_ary_new(),
|
|
1161
1181
|
.rule_id_counter = 0,
|
|
1182
|
+
.next_selector_list_id = 0,
|
|
1162
1183
|
.media_query_count = 0,
|
|
1163
1184
|
.media_cache = NULL,
|
|
1164
1185
|
.has_nesting = 0,
|
|
1186
|
+
.selector_lists_enabled = ctx->selector_lists_enabled,
|
|
1165
1187
|
.depth = 0
|
|
1166
1188
|
};
|
|
1167
1189
|
parse_css_recursive(&nested_ctx, block_start, block_end, NO_PARENT_MEDIA, NO_PARENT_SELECTOR, NO_PARENT_RULE_ID);
|
|
@@ -1283,6 +1305,28 @@ static void parse_css_recursive(ParserContext *ctx, const char *css, const char
|
|
|
1283
1305
|
// Example: ".a, .b, .c { color: red; }" creates 3 separate rules
|
|
1284
1306
|
// ^selector_start ^sel_end
|
|
1285
1307
|
// ^seg_start=seg (scanning for commas)
|
|
1308
|
+
|
|
1309
|
+
// Count selectors for selector list tracking
|
|
1310
|
+
int selector_count = 1;
|
|
1311
|
+
if (ctx->selector_lists_enabled) {
|
|
1312
|
+
const char *count_ptr = selector_start;
|
|
1313
|
+
while (count_ptr < sel_end) {
|
|
1314
|
+
if (*count_ptr == ',') {
|
|
1315
|
+
selector_count++;
|
|
1316
|
+
}
|
|
1317
|
+
count_ptr++;
|
|
1318
|
+
}
|
|
1319
|
+
}
|
|
1320
|
+
|
|
1321
|
+
// Create selector list if enabled and multiple selectors
|
|
1322
|
+
int list_id = -1;
|
|
1323
|
+
VALUE rule_ids_array = Qnil;
|
|
1324
|
+
if (ctx->selector_lists_enabled && selector_count > 1) {
|
|
1325
|
+
list_id = ctx->next_selector_list_id++;
|
|
1326
|
+
rule_ids_array = rb_ary_new();
|
|
1327
|
+
rb_hash_aset(ctx->selector_lists, INT2FIX(list_id), rule_ids_array);
|
|
1328
|
+
}
|
|
1329
|
+
|
|
1286
1330
|
const char *seg_start = selector_start;
|
|
1287
1331
|
const char *seg = selector_start;
|
|
1288
1332
|
|
|
@@ -1322,16 +1366,45 @@ static void parse_css_recursive(ParserContext *ctx, const char *css, const char
|
|
|
1322
1366
|
// Get rule ID and increment
|
|
1323
1367
|
int rule_id = ctx->rule_id_counter++;
|
|
1324
1368
|
|
|
1369
|
+
// Determine selector_list_id value
|
|
1370
|
+
VALUE selector_list_id_val = (list_id >= 0) ? INT2FIX(list_id) : Qnil;
|
|
1371
|
+
|
|
1372
|
+
// Deep copy declarations for selector lists to avoid shared state
|
|
1373
|
+
// (principle of least surprise - modifying one rule shouldn't affect others)
|
|
1374
|
+
VALUE rule_declarations;
|
|
1375
|
+
if (list_id >= 0) {
|
|
1376
|
+
// Deep copy: both array and Declaration structs inside
|
|
1377
|
+
long decl_count = RARRAY_LEN(declarations);
|
|
1378
|
+
rule_declarations = rb_ary_new_capa(decl_count);
|
|
1379
|
+
for (long k = 0; k < decl_count; k++) {
|
|
1380
|
+
VALUE decl = rb_ary_entry(declarations, k);
|
|
1381
|
+
VALUE new_decl = rb_struct_new(cDeclaration,
|
|
1382
|
+
rb_struct_aref(decl, INT2FIX(DECL_PROPERTY)),
|
|
1383
|
+
rb_struct_aref(decl, INT2FIX(DECL_VALUE)),
|
|
1384
|
+
rb_struct_aref(decl, INT2FIX(DECL_IMPORTANT))
|
|
1385
|
+
);
|
|
1386
|
+
rb_ary_push(rule_declarations, new_decl);
|
|
1387
|
+
}
|
|
1388
|
+
} else {
|
|
1389
|
+
rule_declarations = rb_ary_dup(declarations);
|
|
1390
|
+
}
|
|
1391
|
+
|
|
1325
1392
|
// Create Rule
|
|
1326
1393
|
VALUE rule = rb_struct_new(cRule,
|
|
1327
1394
|
INT2FIX(rule_id),
|
|
1328
1395
|
resolved_selector,
|
|
1329
|
-
|
|
1396
|
+
rule_declarations,
|
|
1330
1397
|
Qnil, // specificity
|
|
1331
1398
|
parent_id_val,
|
|
1332
|
-
nesting_style_val
|
|
1399
|
+
nesting_style_val,
|
|
1400
|
+
selector_list_id_val
|
|
1333
1401
|
);
|
|
1334
1402
|
|
|
1403
|
+
// Track rule in selector list if applicable
|
|
1404
|
+
if (list_id >= 0) {
|
|
1405
|
+
rb_ary_push(rule_ids_array, INT2FIX(rule_id));
|
|
1406
|
+
}
|
|
1407
|
+
|
|
1335
1408
|
// Mark that we have nesting (only set once)
|
|
1336
1409
|
if (!ctx->has_nesting && !NIL_P(parent_id_val)) {
|
|
1337
1410
|
ctx->has_nesting = 1;
|
|
@@ -1358,6 +1431,28 @@ static void parse_css_recursive(ParserContext *ctx, const char *css, const char
|
|
|
1358
1431
|
// - .a .child with declarations [font: 14px]
|
|
1359
1432
|
// - .b with declarations [color: red]
|
|
1360
1433
|
// - .b .child with declarations [font: 14px]
|
|
1434
|
+
|
|
1435
|
+
// Count selectors for selector list tracking
|
|
1436
|
+
int selector_count = 1;
|
|
1437
|
+
if (ctx->selector_lists_enabled) {
|
|
1438
|
+
const char *count_ptr = selector_start;
|
|
1439
|
+
while (count_ptr < sel_end) {
|
|
1440
|
+
if (*count_ptr == ',') {
|
|
1441
|
+
selector_count++;
|
|
1442
|
+
}
|
|
1443
|
+
count_ptr++;
|
|
1444
|
+
}
|
|
1445
|
+
}
|
|
1446
|
+
|
|
1447
|
+
// Create selector list if enabled and multiple selectors
|
|
1448
|
+
int list_id = -1;
|
|
1449
|
+
VALUE rule_ids_array = Qnil;
|
|
1450
|
+
if (ctx->selector_lists_enabled && selector_count > 1) {
|
|
1451
|
+
list_id = ctx->next_selector_list_id++;
|
|
1452
|
+
rule_ids_array = rb_ary_new();
|
|
1453
|
+
rb_hash_aset(ctx->selector_lists, INT2FIX(list_id), rule_ids_array);
|
|
1454
|
+
}
|
|
1455
|
+
|
|
1361
1456
|
const char *seg_start = selector_start;
|
|
1362
1457
|
const char *seg = selector_start;
|
|
1363
1458
|
|
|
@@ -1407,6 +1502,9 @@ static void parse_css_recursive(ParserContext *ctx, const char *css, const char
|
|
|
1407
1502
|
resolved_current, INT2FIX(current_rule_id), parent_media_sym);
|
|
1408
1503
|
ctx->depth--;
|
|
1409
1504
|
|
|
1505
|
+
// Determine selector_list_id value
|
|
1506
|
+
VALUE selector_list_id_val = (list_id >= 0) ? INT2FIX(list_id) : Qnil;
|
|
1507
|
+
|
|
1410
1508
|
// Create parent rule and replace placeholder
|
|
1411
1509
|
// Always create the rule (even if empty) to avoid edge cases
|
|
1412
1510
|
VALUE rule = rb_struct_new(cRule,
|
|
@@ -1415,9 +1513,15 @@ static void parse_css_recursive(ParserContext *ctx, const char *css, const char
|
|
|
1415
1513
|
parent_declarations,
|
|
1416
1514
|
Qnil, // specificity
|
|
1417
1515
|
current_parent_id,
|
|
1418
|
-
current_nesting_style
|
|
1516
|
+
current_nesting_style,
|
|
1517
|
+
selector_list_id_val
|
|
1419
1518
|
);
|
|
1420
1519
|
|
|
1520
|
+
// Track rule in selector list if applicable
|
|
1521
|
+
if (list_id >= 0) {
|
|
1522
|
+
rb_ary_push(rule_ids_array, INT2FIX(current_rule_id));
|
|
1523
|
+
}
|
|
1524
|
+
|
|
1421
1525
|
// Mark that we have nesting (only set once)
|
|
1422
1526
|
if (!ctx->has_nesting && !NIL_P(current_parent_id)) {
|
|
1423
1527
|
ctx->has_nesting = 1;
|
|
@@ -1473,12 +1577,17 @@ VALUE parse_media_types(VALUE self, VALUE media_query_sym) {
|
|
|
1473
1577
|
* Main parse entry point
|
|
1474
1578
|
* Returns: { rules: [...], media_index: {...}, charset: "..." | nil, last_rule_id: N }
|
|
1475
1579
|
*/
|
|
1476
|
-
VALUE parse_css_new_impl(VALUE css_string, int rule_id_offset) {
|
|
1580
|
+
VALUE parse_css_new_impl(VALUE css_string, VALUE parser_options, int rule_id_offset) {
|
|
1477
1581
|
Check_Type(css_string, T_STRING);
|
|
1582
|
+
Check_Type(parser_options, T_HASH);
|
|
1478
1583
|
|
|
1479
1584
|
DEBUG_PRINTF("\n[PARSE_NEW] ========== NEW PARSE CALL ==========\n");
|
|
1480
1585
|
DEBUG_PRINTF("[PARSE_NEW] Input CSS (first 100 chars): %.100s\n", RSTRING_PTR(css_string));
|
|
1481
1586
|
|
|
1587
|
+
// Read parser options
|
|
1588
|
+
VALUE selector_lists_opt = rb_hash_aref(parser_options, ID2SYM(rb_intern("selector_lists")));
|
|
1589
|
+
int selector_lists_enabled = (NIL_P(selector_lists_opt) || RTEST(selector_lists_opt)) ? 1 : 0;
|
|
1590
|
+
|
|
1482
1591
|
const char *css = RSTRING_PTR(css_string);
|
|
1483
1592
|
const char *pe = css + RSTRING_LEN(css_string);
|
|
1484
1593
|
const char *p = css;
|
|
@@ -1513,11 +1622,14 @@ VALUE parse_css_new_impl(VALUE css_string, int rule_id_offset) {
|
|
|
1513
1622
|
ParserContext ctx;
|
|
1514
1623
|
ctx.rules_array = rb_ary_new();
|
|
1515
1624
|
ctx.media_index = rb_hash_new();
|
|
1625
|
+
ctx.selector_lists = rb_hash_new();
|
|
1516
1626
|
ctx.imports_array = rb_ary_new();
|
|
1517
1627
|
ctx.rule_id_counter = rule_id_offset; // Start from offset
|
|
1628
|
+
ctx.next_selector_list_id = 0; // Start from 0
|
|
1518
1629
|
ctx.media_query_count = 0;
|
|
1519
1630
|
ctx.media_cache = NULL; // Removed - no perf benefit
|
|
1520
1631
|
ctx.has_nesting = 0; // Will be set to 1 if any nested rules are created
|
|
1632
|
+
ctx.selector_lists_enabled = selector_lists_enabled;
|
|
1521
1633
|
ctx.depth = 0; // Start at depth 0
|
|
1522
1634
|
|
|
1523
1635
|
// Parse CSS (top-level, no parent context)
|
|
@@ -1528,6 +1640,7 @@ VALUE parse_css_new_impl(VALUE css_string, int rule_id_offset) {
|
|
|
1528
1640
|
VALUE result = rb_hash_new();
|
|
1529
1641
|
rb_hash_aset(result, ID2SYM(rb_intern("rules")), ctx.rules_array);
|
|
1530
1642
|
rb_hash_aset(result, ID2SYM(rb_intern("_media_index")), ctx.media_index);
|
|
1643
|
+
rb_hash_aset(result, ID2SYM(rb_intern("_selector_lists")), ctx.selector_lists);
|
|
1531
1644
|
rb_hash_aset(result, ID2SYM(rb_intern("imports")), ctx.imports_array);
|
|
1532
1645
|
rb_hash_aset(result, ID2SYM(rb_intern("charset")), charset);
|
|
1533
1646
|
rb_hash_aset(result, ID2SYM(rb_intern("last_rule_id")), INT2FIX(ctx.rule_id_counter));
|
|
@@ -1536,6 +1649,7 @@ VALUE parse_css_new_impl(VALUE css_string, int rule_id_offset) {
|
|
|
1536
1649
|
RB_GC_GUARD(charset);
|
|
1537
1650
|
RB_GC_GUARD(ctx.rules_array);
|
|
1538
1651
|
RB_GC_GUARD(ctx.media_index);
|
|
1652
|
+
RB_GC_GUARD(ctx.selector_lists);
|
|
1539
1653
|
RB_GC_GUARD(ctx.imports_array);
|
|
1540
1654
|
RB_GC_GUARD(result);
|
|
1541
1655
|
|
data/ext/cataract/flatten.c
CHANGED
|
@@ -623,7 +623,7 @@ struct flatten_selectors_context {
|
|
|
623
623
|
};
|
|
624
624
|
|
|
625
625
|
// Forward declaration
|
|
626
|
-
static VALUE flatten_rules_for_selector(VALUE rules_array, VALUE rule_indices, VALUE selector);
|
|
626
|
+
static VALUE flatten_rules_for_selector(VALUE rules_array, VALUE rule_indices, VALUE selector, VALUE *out_selector_list_id);
|
|
627
627
|
|
|
628
628
|
// Callback for rb_hash_foreach when merging selector groups
|
|
629
629
|
static int flatten_selector_group_callback(VALUE selector, VALUE group_indices, VALUE arg) {
|
|
@@ -634,8 +634,9 @@ static int flatten_selector_group_callback(VALUE selector, VALUE group_indices,
|
|
|
634
634
|
ctx->selector_index, ctx->total_selectors,
|
|
635
635
|
RSTRING_PTR(selector), RARRAY_LEN(group_indices));
|
|
636
636
|
|
|
637
|
-
// Merge all rules in this selector group
|
|
638
|
-
VALUE
|
|
637
|
+
// Merge all rules in this selector group and preserve selector_list_id if all rules share same ID
|
|
638
|
+
VALUE selector_list_id = Qnil;
|
|
639
|
+
VALUE merged_decls = flatten_rules_for_selector(ctx->rules_array, group_indices, selector, &selector_list_id);
|
|
639
640
|
|
|
640
641
|
// Create new rule with this selector and merged declarations
|
|
641
642
|
VALUE new_rule = rb_struct_new(cRule,
|
|
@@ -644,7 +645,8 @@ static int flatten_selector_group_callback(VALUE selector, VALUE group_indices,
|
|
|
644
645
|
merged_decls,
|
|
645
646
|
Qnil, // specificity
|
|
646
647
|
Qnil, // parent_rule_id
|
|
647
|
-
Qnil
|
|
648
|
+
Qnil, // nesting_style
|
|
649
|
+
selector_list_id // Preserve selector_list_id if all rules in group share same ID
|
|
648
650
|
);
|
|
649
651
|
rb_ary_push(ctx->merged_rules, new_rule);
|
|
650
652
|
|
|
@@ -657,15 +659,66 @@ static int flatten_selector_group_callback(VALUE selector, VALUE group_indices,
|
|
|
657
659
|
* Takes an array of rule indices that all share the same selector,
|
|
658
660
|
* expands shorthands, applies cascade rules, and recreates shorthands.
|
|
659
661
|
*
|
|
662
|
+
* @param out_selector_list_id Output parameter: set to selector_list_id if all rules share same ID, else Qnil
|
|
660
663
|
* Returns: Array of merged Declaration structs
|
|
661
664
|
*/
|
|
662
|
-
static VALUE flatten_rules_for_selector(VALUE rules_array, VALUE rule_indices, VALUE selector) {
|
|
665
|
+
static VALUE flatten_rules_for_selector(VALUE rules_array, VALUE rule_indices, VALUE selector, VALUE *out_selector_list_id) {
|
|
663
666
|
long num_rules_in_group = RARRAY_LEN(rule_indices);
|
|
664
667
|
VALUE properties_hash = rb_hash_new();
|
|
665
668
|
|
|
666
669
|
DEBUG_PRINTF(" [flatten_rules_for_selector] Merging %ld rules for selector '%s'\n",
|
|
667
670
|
num_rules_in_group, RSTRING_PTR(selector));
|
|
668
671
|
|
|
672
|
+
// Extract selector_list_id from rules - preserve if all rules share same ID
|
|
673
|
+
VALUE first_selector_list_id = Qnil;
|
|
674
|
+
int all_same_selector_list_id = 1;
|
|
675
|
+
|
|
676
|
+
DEBUG_PRINTF(" Checking if rules share same selector_list_id...\n");
|
|
677
|
+
|
|
678
|
+
for (long g = 0; g < num_rules_in_group; g++) {
|
|
679
|
+
long rule_idx = FIX2LONG(rb_ary_entry(rule_indices, g));
|
|
680
|
+
VALUE rule = RARRAY_AREF(rules_array, rule_idx);
|
|
681
|
+
|
|
682
|
+
// Skip AtRule objects - they don't have selector_list_id
|
|
683
|
+
if (rb_obj_is_kind_of(rule, cAtRule)) {
|
|
684
|
+
continue;
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
VALUE selector_list_id = rb_struct_aref(rule, INT2FIX(RULE_SELECTOR_LIST_ID));
|
|
688
|
+
|
|
689
|
+
if (NIL_P(selector_list_id)) {
|
|
690
|
+
DEBUG_PRINTF(" Rule %ld: has nil selector_list_id, can't preserve\n", g);
|
|
691
|
+
// If any rule has nil selector_list_id, can't preserve
|
|
692
|
+
all_same_selector_list_id = 0;
|
|
693
|
+
break;
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
if (g == 0 || NIL_P(first_selector_list_id)) {
|
|
697
|
+
first_selector_list_id = selector_list_id;
|
|
698
|
+
DEBUG_PRINTF(" Rule %ld: first selector_list_id=%ld\n", g, NUM2LONG(first_selector_list_id));
|
|
699
|
+
} else if (!rb_equal(first_selector_list_id, selector_list_id)) {
|
|
700
|
+
DEBUG_PRINTF(" Rule %ld: different selector_list_id=%ld (vs %ld), can't preserve\n",
|
|
701
|
+
g, NUM2LONG(selector_list_id), NUM2LONG(first_selector_list_id));
|
|
702
|
+
// Different selector_list_ids - can't preserve
|
|
703
|
+
all_same_selector_list_id = 0;
|
|
704
|
+
break;
|
|
705
|
+
} else {
|
|
706
|
+
DEBUG_PRINTF(" Rule %ld: same selector_list_id=%ld\n", g, NUM2LONG(selector_list_id));
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
// Set output parameter: preserve selector_list_id only if all rules share same ID
|
|
711
|
+
if (out_selector_list_id) {
|
|
712
|
+
*out_selector_list_id = (all_same_selector_list_id && !NIL_P(first_selector_list_id)) ? first_selector_list_id : Qnil;
|
|
713
|
+
#ifdef CATARACT_DEBUG
|
|
714
|
+
if (!NIL_P(*out_selector_list_id)) {
|
|
715
|
+
DEBUG_PRINTF(" -> Preserving selector_list_id=%ld for merged rule\n", NUM2LONG(*out_selector_list_id));
|
|
716
|
+
} else {
|
|
717
|
+
DEBUG_PRINTF(" -> NOT preserving selector_list_id (not all same)\n");
|
|
718
|
+
}
|
|
719
|
+
#endif
|
|
720
|
+
}
|
|
721
|
+
|
|
669
722
|
// Process each rule in this selector group
|
|
670
723
|
for (long g = 0; g < num_rules_in_group; g++) {
|
|
671
724
|
long rule_idx = FIX2LONG(rb_ary_entry(rule_indices, g));
|
|
@@ -1015,6 +1068,178 @@ static VALUE flatten_rules_for_selector(VALUE rules_array, VALUE rule_indices, V
|
|
|
1015
1068
|
return merged_decls;
|
|
1016
1069
|
}
|
|
1017
1070
|
|
|
1071
|
+
/*
|
|
1072
|
+
* Helper function: Check if two declaration arrays are equal
|
|
1073
|
+
*
|
|
1074
|
+
* Returns: true if declarations have same properties, values, and importance
|
|
1075
|
+
*/
|
|
1076
|
+
static int declarations_equal(VALUE decls1, VALUE decls2) {
|
|
1077
|
+
long len1 = RARRAY_LEN(decls1);
|
|
1078
|
+
long len2 = RARRAY_LEN(decls2);
|
|
1079
|
+
|
|
1080
|
+
DEBUG_PRINTF(" [declarations_equal] Comparing %ld vs %ld declarations\n", len1, len2);
|
|
1081
|
+
|
|
1082
|
+
if (len1 != len2) {
|
|
1083
|
+
DEBUG_PRINTF(" -> Different lengths, NOT equal\n");
|
|
1084
|
+
return 0;
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
// Compare each declaration (property, value, important must all match)
|
|
1088
|
+
for (long i = 0; i < len1; i++) {
|
|
1089
|
+
VALUE d1 = RARRAY_AREF(decls1, i);
|
|
1090
|
+
VALUE d2 = RARRAY_AREF(decls2, i);
|
|
1091
|
+
|
|
1092
|
+
VALUE prop1 = rb_struct_aref(d1, INT2FIX(DECL_PROPERTY));
|
|
1093
|
+
VALUE prop2 = rb_struct_aref(d2, INT2FIX(DECL_PROPERTY));
|
|
1094
|
+
VALUE val1 = rb_struct_aref(d1, INT2FIX(DECL_VALUE));
|
|
1095
|
+
VALUE val2 = rb_struct_aref(d2, INT2FIX(DECL_VALUE));
|
|
1096
|
+
VALUE imp1 = rb_struct_aref(d1, INT2FIX(DECL_IMPORTANT));
|
|
1097
|
+
VALUE imp2 = rb_struct_aref(d2, INT2FIX(DECL_IMPORTANT));
|
|
1098
|
+
|
|
1099
|
+
if (!rb_equal(prop1, prop2) || !rb_equal(val1, val2) || (RTEST(imp1) != RTEST(imp2))) {
|
|
1100
|
+
DEBUG_PRINTF(" -> Decl %ld differs: %s:%s%s vs %s:%s%s\n",
|
|
1101
|
+
i,
|
|
1102
|
+
RSTRING_PTR(prop1), RSTRING_PTR(val1), RTEST(imp1) ? "!" : "",
|
|
1103
|
+
RSTRING_PTR(prop2), RSTRING_PTR(val2), RTEST(imp2) ? "!" : "");
|
|
1104
|
+
// Protect VALUEs from GC after rb_equal() calls and before RSTRING_PTR usage above
|
|
1105
|
+
RB_GC_GUARD(prop1);
|
|
1106
|
+
RB_GC_GUARD(val1);
|
|
1107
|
+
RB_GC_GUARD(prop2);
|
|
1108
|
+
RB_GC_GUARD(val2);
|
|
1109
|
+
return 0;
|
|
1110
|
+
}
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
DEBUG_PRINTF(" -> All declarations match, equal\n");
|
|
1114
|
+
return 1;
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
/*
|
|
1118
|
+
* Update selector lists to remove diverged rules
|
|
1119
|
+
*
|
|
1120
|
+
* After flattening/cascade, rules that were in the same selector list may have
|
|
1121
|
+
* different declarations. This function builds the selector_lists hash with only
|
|
1122
|
+
* rules that still match, and clears selector_list_id for diverged rules.
|
|
1123
|
+
*
|
|
1124
|
+
* @param merged_rules Array of flattened rules (with new IDs assigned)
|
|
1125
|
+
* @param selector_lists Empty hash to populate with list_id => Array of rule IDs
|
|
1126
|
+
*/
|
|
1127
|
+
static void update_selector_lists_for_divergence(VALUE merged_rules, VALUE selector_lists) {
|
|
1128
|
+
DEBUG_PRINTF("\n=== update_selector_lists_for_divergence ===\n");
|
|
1129
|
+
|
|
1130
|
+
// Group merged rules by selector_list_id (skip rules with no list)
|
|
1131
|
+
// NOTE: Using manual iteration instead of group_by to avoid Ruby method calls
|
|
1132
|
+
VALUE rules_by_list = rb_hash_new();
|
|
1133
|
+
|
|
1134
|
+
long num_rules = RARRAY_LEN(merged_rules);
|
|
1135
|
+
DEBUG_PRINTF(" Total merged rules: %ld\n", num_rules);
|
|
1136
|
+
|
|
1137
|
+
for (long i = 0; i < num_rules; i++) {
|
|
1138
|
+
VALUE rule = RARRAY_AREF(merged_rules, i);
|
|
1139
|
+
|
|
1140
|
+
// Skip AtRule objects
|
|
1141
|
+
if (rb_obj_is_kind_of(rule, cAtRule)) {
|
|
1142
|
+
continue;
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1145
|
+
VALUE selector_list_id = rb_struct_aref(rule, INT2FIX(RULE_SELECTOR_LIST_ID));
|
|
1146
|
+
#ifdef CATARACT_DEBUG
|
|
1147
|
+
VALUE selector = rb_struct_aref(rule, INT2FIX(RULE_SELECTOR));
|
|
1148
|
+
#endif
|
|
1149
|
+
|
|
1150
|
+
if (NIL_P(selector_list_id)) {
|
|
1151
|
+
DEBUG_PRINTF(" Rule %ld (%s): no selector_list_id\n", i, RSTRING_PTR(selector));
|
|
1152
|
+
continue;
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1155
|
+
DEBUG_PRINTF(" Rule %ld (%s): selector_list_id=%ld\n",
|
|
1156
|
+
i, RSTRING_PTR(selector), NUM2LONG(selector_list_id));
|
|
1157
|
+
|
|
1158
|
+
VALUE group = rb_hash_aref(rules_by_list, selector_list_id);
|
|
1159
|
+
if (NIL_P(group)) {
|
|
1160
|
+
group = rb_ary_new();
|
|
1161
|
+
rb_hash_aset(rules_by_list, selector_list_id, group);
|
|
1162
|
+
DEBUG_PRINTF(" -> Created new group for list_id=%ld\n", NUM2LONG(selector_list_id));
|
|
1163
|
+
}
|
|
1164
|
+
rb_ary_push(group, rule);
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
// For each selector list, check if declarations still match
|
|
1168
|
+
VALUE list_ids = rb_funcall(rules_by_list, rb_intern("keys"), 0);
|
|
1169
|
+
long num_lists = RARRAY_LEN(list_ids);
|
|
1170
|
+
DEBUG_PRINTF(" Found %ld selector list groups to check\n", num_lists);
|
|
1171
|
+
|
|
1172
|
+
for (long i = 0; i < num_lists; i++) {
|
|
1173
|
+
VALUE list_id = RARRAY_AREF(list_ids, i);
|
|
1174
|
+
VALUE rules_in_list = rb_hash_aref(rules_by_list, list_id);
|
|
1175
|
+
long num_in_list = RARRAY_LEN(rules_in_list);
|
|
1176
|
+
|
|
1177
|
+
DEBUG_PRINTF("\n Checking list_id=%ld: %ld rules\n", NUM2LONG(list_id), num_in_list);
|
|
1178
|
+
|
|
1179
|
+
// Skip if only one rule in list (nothing to compare)
|
|
1180
|
+
if (num_in_list <= 1) {
|
|
1181
|
+
DEBUG_PRINTF(" -> Only 1 rule, skipping\n");
|
|
1182
|
+
continue;
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1185
|
+
// Get first rule as reference
|
|
1186
|
+
VALUE reference_rule = RARRAY_AREF(rules_in_list, 0);
|
|
1187
|
+
VALUE reference_decls = rb_struct_aref(reference_rule, INT2FIX(RULE_DECLARATIONS));
|
|
1188
|
+
#ifdef CATARACT_DEBUG
|
|
1189
|
+
VALUE reference_selector = rb_struct_aref(reference_rule, INT2FIX(RULE_SELECTOR));
|
|
1190
|
+
DEBUG_PRINTF(" Reference rule: selector=%s, %ld declarations\n",
|
|
1191
|
+
RSTRING_PTR(reference_selector), RARRAY_LEN(reference_decls));
|
|
1192
|
+
#endif
|
|
1193
|
+
|
|
1194
|
+
// Find rules that still match (have identical declarations)
|
|
1195
|
+
VALUE matching_rules = rb_ary_new();
|
|
1196
|
+
rb_ary_push(matching_rules, reference_rule);
|
|
1197
|
+
|
|
1198
|
+
for (long j = 1; j < num_in_list; j++) {
|
|
1199
|
+
VALUE rule = RARRAY_AREF(rules_in_list, j);
|
|
1200
|
+
VALUE decls = rb_struct_aref(rule, INT2FIX(RULE_DECLARATIONS));
|
|
1201
|
+
#ifdef CATARACT_DEBUG
|
|
1202
|
+
VALUE selector = rb_struct_aref(rule, INT2FIX(RULE_SELECTOR));
|
|
1203
|
+
DEBUG_PRINTF(" Comparing rule %ld (selector=%s):\n", j, RSTRING_PTR(selector));
|
|
1204
|
+
#endif
|
|
1205
|
+
|
|
1206
|
+
if (declarations_equal(reference_decls, decls)) {
|
|
1207
|
+
DEBUG_PRINTF(" -> MATCHES reference, keeping in list\n");
|
|
1208
|
+
rb_ary_push(matching_rules, rule);
|
|
1209
|
+
} else {
|
|
1210
|
+
DEBUG_PRINTF(" -> DIVERGED from reference, clearing selector_list_id\n");
|
|
1211
|
+
// Clear selector_list_id for diverged rule
|
|
1212
|
+
rb_struct_aset(rule, INT2FIX(RULE_SELECTOR_LIST_ID), Qnil);
|
|
1213
|
+
}
|
|
1214
|
+
}
|
|
1215
|
+
|
|
1216
|
+
// Only keep the selector list if at least 2 rules still match
|
|
1217
|
+
long num_matching = RARRAY_LEN(matching_rules);
|
|
1218
|
+
DEBUG_PRINTF(" Result: %ld/%ld rules still match\n", num_matching, num_in_list);
|
|
1219
|
+
|
|
1220
|
+
if (num_matching >= 2) {
|
|
1221
|
+
// Build selector_lists hash with NEW rule IDs
|
|
1222
|
+
VALUE rule_ids = rb_ary_new_capa(num_matching);
|
|
1223
|
+
for (long j = 0; j < num_matching; j++) {
|
|
1224
|
+
VALUE rule = RARRAY_AREF(matching_rules, j);
|
|
1225
|
+
VALUE rule_id = rb_struct_aref(rule, INT2FIX(RULE_ID));
|
|
1226
|
+
rb_ary_push(rule_ids, rule_id);
|
|
1227
|
+
}
|
|
1228
|
+
rb_hash_aset(selector_lists, list_id, rule_ids);
|
|
1229
|
+
DEBUG_PRINTF(" -> Keeping selector list with %ld rules\n", num_matching);
|
|
1230
|
+
} else {
|
|
1231
|
+
DEBUG_PRINTF(" -> Only 1 rule left, clearing selector_list_id for it too\n");
|
|
1232
|
+
// Clear selector_list_id for the last remaining rule too
|
|
1233
|
+
for (long j = 0; j < num_matching; j++) {
|
|
1234
|
+
VALUE rule = RARRAY_AREF(matching_rules, j);
|
|
1235
|
+
rb_struct_aset(rule, INT2FIX(RULE_SELECTOR_LIST_ID), Qnil);
|
|
1236
|
+
}
|
|
1237
|
+
}
|
|
1238
|
+
}
|
|
1239
|
+
|
|
1240
|
+
DEBUG_PRINTF("\n=== End divergence tracking: %ld selector lists preserved ===\n\n", RHASH_SIZE(selector_lists));
|
|
1241
|
+
}
|
|
1242
|
+
|
|
1018
1243
|
// Flatten CSS rules by applying cascade rules
|
|
1019
1244
|
// Input: Stylesheet object or CSS string
|
|
1020
1245
|
// Output: Stylesheet with flattened declarations (cascade applied)
|
|
@@ -1025,7 +1250,8 @@ VALUE cataract_flatten(VALUE self, VALUE input) {
|
|
|
1025
1250
|
// Most calls pass Stylesheet (common case), String is rare
|
|
1026
1251
|
if (TYPE(input) == T_STRING) {
|
|
1027
1252
|
// Parse CSS string first
|
|
1028
|
-
VALUE
|
|
1253
|
+
VALUE argv[1] = { input };
|
|
1254
|
+
VALUE parsed = parse_css_new(1, argv, self);
|
|
1029
1255
|
rules_array = rb_hash_aref(parsed, ID2SYM(rb_intern("rules")));
|
|
1030
1256
|
} else if (rb_obj_is_kind_of(input, cStylesheet)) {
|
|
1031
1257
|
// Extract @rules from Stylesheet (common case)
|
|
@@ -1214,10 +1440,8 @@ VALUE cataract_flatten(VALUE self, VALUE input) {
|
|
|
1214
1440
|
VALUE passthrough_sheet = rb_class_new_instance(0, NULL, cStylesheet);
|
|
1215
1441
|
rb_ivar_set(passthrough_sheet, id_ivar_rules, passthrough_rules);
|
|
1216
1442
|
|
|
1217
|
-
// Set empty @media_index
|
|
1443
|
+
// Set empty @media_index (no media rules after flatten)
|
|
1218
1444
|
VALUE media_idx = rb_hash_new();
|
|
1219
|
-
VALUE all_ids = rb_ary_new();
|
|
1220
|
-
rb_hash_aset(media_idx, ID2SYM(id_all), all_ids);
|
|
1221
1445
|
rb_ivar_set(passthrough_sheet, id_ivar_media_index, media_idx);
|
|
1222
1446
|
|
|
1223
1447
|
return passthrough_sheet;
|
|
@@ -1258,15 +1482,45 @@ VALUE cataract_flatten(VALUE self, VALUE input) {
|
|
|
1258
1482
|
|
|
1259
1483
|
rb_ivar_set(merged_sheet, id_ivar_rules, merged_rules);
|
|
1260
1484
|
|
|
1261
|
-
//
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1485
|
+
// Handle selector list divergence: remove rules from selector lists if declarations no longer match
|
|
1486
|
+
// This makes selector_list_id authoritative - if set, declarations MUST be identical
|
|
1487
|
+
// Only process if selector_lists is enabled in the stylesheet's parser options
|
|
1488
|
+
VALUE selector_lists = rb_hash_new();
|
|
1489
|
+
int selector_lists_enabled = 0;
|
|
1490
|
+
|
|
1491
|
+
if (rb_obj_is_kind_of(input, cStylesheet)) {
|
|
1492
|
+
VALUE parser_options = rb_ivar_get(input, rb_intern("@parser_options"));
|
|
1493
|
+
|
|
1494
|
+
if (!NIL_P(parser_options)) {
|
|
1495
|
+
VALUE enabled_val = rb_hash_aref(parser_options, ID2SYM(rb_intern("selector_lists")));
|
|
1496
|
+
selector_lists_enabled = RTEST(enabled_val);
|
|
1497
|
+
|
|
1498
|
+
if (selector_lists_enabled) {
|
|
1499
|
+
update_selector_lists_for_divergence(merged_rules, selector_lists);
|
|
1500
|
+
} else {
|
|
1501
|
+
// Clear all selector_list_ids when feature is disabled
|
|
1502
|
+
for (long i = 0; i < rule_id_counter; i++) {
|
|
1503
|
+
VALUE rule = RARRAY_AREF(merged_rules, i);
|
|
1504
|
+
if (!rb_obj_is_kind_of(rule, cAtRule)) {
|
|
1505
|
+
rb_struct_aset(rule, INT2FIX(RULE_SELECTOR_LIST_ID), Qnil);
|
|
1506
|
+
}
|
|
1507
|
+
}
|
|
1508
|
+
}
|
|
1509
|
+
} else {
|
|
1510
|
+
// Default behavior when parser_options is nil: assume enabled
|
|
1511
|
+
update_selector_lists_for_divergence(merged_rules, selector_lists);
|
|
1512
|
+
}
|
|
1266
1513
|
}
|
|
1267
|
-
|
|
1514
|
+
|
|
1515
|
+
// Set @media_index to empty hash (no media rules after flatten)
|
|
1516
|
+
// NOTE: Setting to empty hash instead of { all: [ids] } to match pure Ruby behavior
|
|
1517
|
+
// and avoid wrapping output in @media all during serialization
|
|
1518
|
+
VALUE media_idx = rb_hash_new();
|
|
1268
1519
|
rb_ivar_set(merged_sheet, id_ivar_media_index, media_idx);
|
|
1269
1520
|
|
|
1521
|
+
// Set @_selector_lists with divergence tracking
|
|
1522
|
+
rb_ivar_set(merged_sheet, rb_intern("@_selector_lists"), selector_lists);
|
|
1523
|
+
|
|
1270
1524
|
return merged_sheet;
|
|
1271
1525
|
}
|
|
1272
1526
|
|
|
@@ -1731,7 +1985,8 @@ VALUE cataract_flatten(VALUE self, VALUE input) {
|
|
|
1731
1985
|
merged_declarations, // declarations
|
|
1732
1986
|
Qnil, // specificity (not applicable)
|
|
1733
1987
|
Qnil, // parent_rule_id (not nested)
|
|
1734
|
-
Qnil
|
|
1988
|
+
Qnil, // nesting_style (not nested)
|
|
1989
|
+
Qnil // selector_list_id
|
|
1735
1990
|
);
|
|
1736
1991
|
|
|
1737
1992
|
// Set @rules array with single merged rule (use cached ID)
|
data/lib/cataract/declaration.rb
CHANGED
|
@@ -15,4 +15,23 @@ module Cataract
|
|
|
15
15
|
# @attr [String] value CSS property value
|
|
16
16
|
# @attr [Boolean] important Whether the declaration has !important
|
|
17
17
|
Declaration = Struct.new(:property, :value, :important) unless const_defined?(:Declaration)
|
|
18
|
+
|
|
19
|
+
class Declaration
|
|
20
|
+
# Check if this declaration is a custom property (CSS variable).
|
|
21
|
+
#
|
|
22
|
+
# Custom properties are properties that start with the "--" prefix.
|
|
23
|
+
#
|
|
24
|
+
# @return [Boolean] true if property is a custom property
|
|
25
|
+
#
|
|
26
|
+
# @example Custom property
|
|
27
|
+
# decl = Cataract::Declaration.new('--color', 'red', false)
|
|
28
|
+
# decl.custom_property? #=> true
|
|
29
|
+
#
|
|
30
|
+
# @example Regular property
|
|
31
|
+
# decl = Cataract::Declaration.new('color', 'red', false)
|
|
32
|
+
# decl.custom_property? #=> false
|
|
33
|
+
def custom_property?
|
|
34
|
+
property.start_with?('--')
|
|
35
|
+
end
|
|
36
|
+
end
|
|
18
37
|
end
|