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