cataract 0.2.0 → 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 +17 -0
- data/README.md +9 -3
- data/ext/cataract/cataract.c +376 -89
- data/ext/cataract/cataract.h +8 -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 +220 -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/cataract.c
CHANGED
|
@@ -32,10 +32,21 @@ VALUE eSizeError;
|
|
|
32
32
|
* This matches the old parse_css API
|
|
33
33
|
*
|
|
34
34
|
* @param css_string [String] CSS to parse
|
|
35
|
+
* @param parser_options [Hash] Parser options (optional, defaults to {})
|
|
35
36
|
* @return [Hash] { rules: [...], media_index: {...}, charset: "..." }
|
|
36
37
|
*/
|
|
37
|
-
VALUE parse_css_new(VALUE
|
|
38
|
-
|
|
38
|
+
VALUE parse_css_new(int argc, VALUE *argv, VALUE self) {
|
|
39
|
+
VALUE css_string, parser_options;
|
|
40
|
+
|
|
41
|
+
// Parse arguments: required css_string, optional parser_options hash
|
|
42
|
+
rb_scan_args(argc, argv, "11", &css_string, &parser_options);
|
|
43
|
+
|
|
44
|
+
// Default to empty hash if not provided
|
|
45
|
+
if (NIL_P(parser_options)) {
|
|
46
|
+
parser_options = rb_hash_new();
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return parse_css_new_impl(css_string, parser_options, 0);
|
|
39
50
|
}
|
|
40
51
|
|
|
41
52
|
/*
|
|
@@ -292,6 +303,17 @@ struct build_rule_map_ctx {
|
|
|
292
303
|
VALUE rule_to_media;
|
|
293
304
|
};
|
|
294
305
|
|
|
306
|
+
// Formatting options for stylesheet serialization
|
|
307
|
+
// Avoids mode flags and if/else branches - all behavior controlled by struct values
|
|
308
|
+
struct format_opts {
|
|
309
|
+
const char *opening_brace; // " { " (compact) vs " {\n" (formatted)
|
|
310
|
+
const char *closing_brace; // " }\n" (compact) vs "}\n" (formatted)
|
|
311
|
+
const char *media_indent; // "" (compact) vs " " (formatted)
|
|
312
|
+
const char *decl_indent_base; // NULL (compact) vs " " (formatted base rules)
|
|
313
|
+
const char *decl_indent_media; // NULL (compact) vs " " (formatted media rules)
|
|
314
|
+
int add_blank_lines; // 0 (compact) vs 1 (formatted)
|
|
315
|
+
};
|
|
316
|
+
|
|
295
317
|
// Callback to build reverse map from rule_id to media_sym
|
|
296
318
|
static int build_rule_map_callback(VALUE media_sym, VALUE rule_ids, VALUE arg) {
|
|
297
319
|
struct build_rule_map_ctx *ctx = (struct build_rule_map_ctx *)arg;
|
|
@@ -318,27 +340,28 @@ static int build_rule_map_callback(VALUE media_sym, VALUE rule_ids, VALUE arg) {
|
|
|
318
340
|
return ST_CONTINUE;
|
|
319
341
|
}
|
|
320
342
|
|
|
321
|
-
//
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
VALUE result
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
rb_str_cat2(result, "@charset \"");
|
|
331
|
-
rb_str_append(result, charset);
|
|
332
|
-
rb_str_cat2(result, "\";\n");
|
|
333
|
-
}
|
|
334
|
-
|
|
343
|
+
// Private shared implementation for stylesheet serialization with optional selector list grouping
|
|
344
|
+
// All formatting behavior controlled by format_opts struct to avoid mode flags and if/else branches
|
|
345
|
+
static VALUE serialize_stylesheet_with_grouping(
|
|
346
|
+
VALUE rules_array,
|
|
347
|
+
VALUE media_index,
|
|
348
|
+
VALUE result,
|
|
349
|
+
VALUE selector_lists,
|
|
350
|
+
const struct format_opts *opts
|
|
351
|
+
) {
|
|
335
352
|
long total_rules = RARRAY_LEN(rules_array);
|
|
336
353
|
|
|
354
|
+
// Check if selector list grouping is enabled (non-empty hash)
|
|
355
|
+
int grouping_enabled = (!NIL_P(selector_lists) && TYPE(selector_lists) == T_HASH && RHASH_SIZE(selector_lists) > 0);
|
|
356
|
+
|
|
337
357
|
// Build a map from rule_id to media query symbol using rb_hash_foreach
|
|
338
358
|
VALUE rule_to_media = rb_hash_new();
|
|
339
359
|
struct build_rule_map_ctx map_ctx = { rule_to_media };
|
|
340
360
|
rb_hash_foreach(media_index, build_rule_map_callback, (VALUE)&map_ctx);
|
|
341
361
|
|
|
362
|
+
// Track processed rules to avoid duplicates when grouping
|
|
363
|
+
VALUE processed_rule_ids = rb_hash_new();
|
|
364
|
+
|
|
342
365
|
// Iterate through rules in insertion order, grouping consecutive media queries
|
|
343
366
|
VALUE current_media = Qnil;
|
|
344
367
|
int in_media_block = 0;
|
|
@@ -346,7 +369,14 @@ static VALUE stylesheet_to_s_original(VALUE rules_array, VALUE media_index, VALU
|
|
|
346
369
|
for (long i = 0; i < total_rules; i++) {
|
|
347
370
|
VALUE rule = rb_ary_entry(rules_array, i);
|
|
348
371
|
VALUE rule_id = rb_struct_aref(rule, INT2FIX(RULE_ID));
|
|
372
|
+
|
|
373
|
+
// Skip if already processed (when grouped)
|
|
374
|
+
if (RTEST(rb_hash_aref(processed_rule_ids, rule_id))) {
|
|
375
|
+
continue;
|
|
376
|
+
}
|
|
377
|
+
|
|
349
378
|
VALUE rule_media = rb_hash_aref(rule_to_media, rule_id);
|
|
379
|
+
int is_first_rule = (i == 0);
|
|
350
380
|
|
|
351
381
|
if (NIL_P(rule_media)) {
|
|
352
382
|
// Not in any media query - close any open media block first
|
|
@@ -356,8 +386,100 @@ static VALUE stylesheet_to_s_original(VALUE rules_array, VALUE media_index, VALU
|
|
|
356
386
|
current_media = Qnil;
|
|
357
387
|
}
|
|
358
388
|
|
|
359
|
-
//
|
|
360
|
-
|
|
389
|
+
// Add blank line prefix for non-first rules (formatted only)
|
|
390
|
+
if (opts->add_blank_lines && !is_first_rule) {
|
|
391
|
+
rb_str_cat2(result, "\n");
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// Try to group with other rules from same selector list
|
|
395
|
+
// Check if this is a Rule (not AtRule) before accessing selector_list_id
|
|
396
|
+
if (grouping_enabled && rb_obj_is_kind_of(rule, cRule)) {
|
|
397
|
+
VALUE selector_list_id = rb_struct_aref(rule, INT2FIX(RULE_SELECTOR_LIST_ID));
|
|
398
|
+
if (!NIL_P(selector_list_id)) {
|
|
399
|
+
// Get list of rule IDs in this selector list
|
|
400
|
+
VALUE rule_ids_in_list = rb_hash_aref(selector_lists, selector_list_id);
|
|
401
|
+
|
|
402
|
+
if (NIL_P(rule_ids_in_list) || RARRAY_LEN(rule_ids_in_list) <= 1) {
|
|
403
|
+
// Just this rule, serialize normally
|
|
404
|
+
if (opts->decl_indent_base) {
|
|
405
|
+
serialize_rule_formatted(result, rule, "", 1);
|
|
406
|
+
} else {
|
|
407
|
+
serialize_rule(result, rule);
|
|
408
|
+
}
|
|
409
|
+
rb_hash_aset(processed_rule_ids, rule_id, Qtrue);
|
|
410
|
+
} else {
|
|
411
|
+
// Find all rules with matching declarations and same media context
|
|
412
|
+
VALUE matching_selectors = rb_ary_new();
|
|
413
|
+
VALUE rule_declarations = rb_struct_aref(rule, INT2FIX(RULE_DECLARATIONS));
|
|
414
|
+
|
|
415
|
+
long list_len = RARRAY_LEN(rule_ids_in_list);
|
|
416
|
+
for (long j = 0; j < list_len; j++) {
|
|
417
|
+
VALUE other_rule_id = rb_ary_entry(rule_ids_in_list, j);
|
|
418
|
+
|
|
419
|
+
// Skip if already processed
|
|
420
|
+
if (RTEST(rb_hash_aref(processed_rule_ids, other_rule_id))) {
|
|
421
|
+
continue;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
// Find the rule by ID
|
|
425
|
+
VALUE other_rule = rb_ary_entry(rules_array, FIX2INT(other_rule_id));
|
|
426
|
+
if (NIL_P(other_rule)) continue;
|
|
427
|
+
|
|
428
|
+
// Check same media context (both should be nil for base rules)
|
|
429
|
+
VALUE other_rule_media = rb_hash_aref(rule_to_media, other_rule_id);
|
|
430
|
+
if (!rb_equal(rule_media, other_rule_media)) {
|
|
431
|
+
continue;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
// Check if declarations match
|
|
435
|
+
VALUE other_declarations = rb_struct_aref(other_rule, INT2FIX(RULE_DECLARATIONS));
|
|
436
|
+
if (rb_equal(rule_declarations, other_declarations)) {
|
|
437
|
+
VALUE other_selector = rb_struct_aref(other_rule, INT2FIX(RULE_SELECTOR));
|
|
438
|
+
rb_ary_push(matching_selectors, other_selector);
|
|
439
|
+
rb_hash_aset(processed_rule_ids, other_rule_id, Qtrue);
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
// Serialize grouped or single rule
|
|
444
|
+
if (RARRAY_LEN(matching_selectors) > 1) {
|
|
445
|
+
// Group selectors with comma-space separator
|
|
446
|
+
VALUE selector_str = rb_ary_join(matching_selectors, rb_str_new_cstr(", "));
|
|
447
|
+
rb_str_append(result, selector_str);
|
|
448
|
+
rb_str_cat2(result, opts->opening_brace);
|
|
449
|
+
if (opts->decl_indent_base) {
|
|
450
|
+
serialize_declarations_formatted(result, rule_declarations, opts->decl_indent_base);
|
|
451
|
+
} else {
|
|
452
|
+
serialize_declarations(result, rule_declarations);
|
|
453
|
+
}
|
|
454
|
+
rb_str_cat2(result, opts->closing_brace);
|
|
455
|
+
RB_GC_GUARD(selector_str);
|
|
456
|
+
} else {
|
|
457
|
+
// Just one rule, serialize normally
|
|
458
|
+
if (opts->decl_indent_base) {
|
|
459
|
+
serialize_rule_formatted(result, rule, "", 1);
|
|
460
|
+
} else {
|
|
461
|
+
serialize_rule(result, rule);
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
} else {
|
|
466
|
+
// No selector_list_id, serialize normally
|
|
467
|
+
if (opts->decl_indent_base) {
|
|
468
|
+
serialize_rule_formatted(result, rule, "", 1);
|
|
469
|
+
} else {
|
|
470
|
+
serialize_rule(result, rule);
|
|
471
|
+
}
|
|
472
|
+
rb_hash_aset(processed_rule_ids, rule_id, Qtrue);
|
|
473
|
+
}
|
|
474
|
+
} else {
|
|
475
|
+
// Grouping disabled, serialize normally
|
|
476
|
+
if (opts->decl_indent_base) {
|
|
477
|
+
serialize_rule_formatted(result, rule, "", 1);
|
|
478
|
+
} else {
|
|
479
|
+
serialize_rule(result, rule);
|
|
480
|
+
}
|
|
481
|
+
rb_hash_aset(processed_rule_ids, rule_id, Qtrue);
|
|
482
|
+
}
|
|
361
483
|
} else {
|
|
362
484
|
// This rule is in a media query
|
|
363
485
|
// Check if media query changed from previous rule
|
|
@@ -367,6 +489,11 @@ static VALUE stylesheet_to_s_original(VALUE rules_array, VALUE media_index, VALU
|
|
|
367
489
|
rb_str_cat2(result, "}\n");
|
|
368
490
|
}
|
|
369
491
|
|
|
492
|
+
// Add blank line prefix for non-first rules (formatted only)
|
|
493
|
+
if (opts->add_blank_lines && !is_first_rule) {
|
|
494
|
+
rb_str_cat2(result, "\n");
|
|
495
|
+
}
|
|
496
|
+
|
|
370
497
|
// Open new media block
|
|
371
498
|
current_media = rule_media;
|
|
372
499
|
rb_str_cat2(result, "@media ");
|
|
@@ -375,8 +502,80 @@ static VALUE stylesheet_to_s_original(VALUE rules_array, VALUE media_index, VALU
|
|
|
375
502
|
in_media_block = 1;
|
|
376
503
|
}
|
|
377
504
|
|
|
378
|
-
// Serialize rule inside media block
|
|
379
|
-
|
|
505
|
+
// Serialize rule inside media block (with grouping if enabled)
|
|
506
|
+
// Check if this is a Rule (not AtRule) before accessing selector_list_id
|
|
507
|
+
if (grouping_enabled && rb_obj_is_kind_of(rule, cRule)) {
|
|
508
|
+
VALUE selector_list_id = rb_struct_aref(rule, INT2FIX(RULE_SELECTOR_LIST_ID));
|
|
509
|
+
if (!NIL_P(selector_list_id)) {
|
|
510
|
+
VALUE rule_ids_in_list = rb_hash_aref(selector_lists, selector_list_id);
|
|
511
|
+
|
|
512
|
+
if (NIL_P(rule_ids_in_list) || RARRAY_LEN(rule_ids_in_list) <= 1) {
|
|
513
|
+
if (opts->decl_indent_media) {
|
|
514
|
+
serialize_rule_formatted(result, rule, opts->media_indent, 1);
|
|
515
|
+
} else {
|
|
516
|
+
serialize_rule(result, rule);
|
|
517
|
+
}
|
|
518
|
+
rb_hash_aset(processed_rule_ids, rule_id, Qtrue);
|
|
519
|
+
} else {
|
|
520
|
+
VALUE matching_selectors = rb_ary_new();
|
|
521
|
+
VALUE rule_declarations = rb_struct_aref(rule, INT2FIX(RULE_DECLARATIONS));
|
|
522
|
+
|
|
523
|
+
long list_len = RARRAY_LEN(rule_ids_in_list);
|
|
524
|
+
for (long j = 0; j < list_len; j++) {
|
|
525
|
+
VALUE other_rule_id = rb_ary_entry(rule_ids_in_list, j);
|
|
526
|
+
if (RTEST(rb_hash_aref(processed_rule_ids, other_rule_id))) continue;
|
|
527
|
+
|
|
528
|
+
VALUE other_rule = rb_ary_entry(rules_array, FIX2INT(other_rule_id));
|
|
529
|
+
if (NIL_P(other_rule)) continue;
|
|
530
|
+
|
|
531
|
+
VALUE other_rule_media = rb_hash_aref(rule_to_media, other_rule_id);
|
|
532
|
+
if (!rb_equal(rule_media, other_rule_media)) continue;
|
|
533
|
+
|
|
534
|
+
VALUE other_declarations = rb_struct_aref(other_rule, INT2FIX(RULE_DECLARATIONS));
|
|
535
|
+
if (rb_equal(rule_declarations, other_declarations)) {
|
|
536
|
+
VALUE other_selector = rb_struct_aref(other_rule, INT2FIX(RULE_SELECTOR));
|
|
537
|
+
rb_ary_push(matching_selectors, other_selector);
|
|
538
|
+
rb_hash_aset(processed_rule_ids, other_rule_id, Qtrue);
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
if (RARRAY_LEN(matching_selectors) > 1) {
|
|
543
|
+
VALUE selector_str = rb_ary_join(matching_selectors, rb_str_new_cstr(", "));
|
|
544
|
+
rb_str_cat2(result, opts->media_indent);
|
|
545
|
+
rb_str_append(result, selector_str);
|
|
546
|
+
rb_str_cat2(result, opts->opening_brace);
|
|
547
|
+
if (opts->decl_indent_media) {
|
|
548
|
+
serialize_declarations_formatted(result, rule_declarations, opts->decl_indent_media);
|
|
549
|
+
} else {
|
|
550
|
+
serialize_declarations(result, rule_declarations);
|
|
551
|
+
}
|
|
552
|
+
rb_str_cat2(result, opts->media_indent);
|
|
553
|
+
rb_str_cat2(result, opts->closing_brace);
|
|
554
|
+
RB_GC_GUARD(selector_str);
|
|
555
|
+
} else {
|
|
556
|
+
if (opts->decl_indent_media) {
|
|
557
|
+
serialize_rule_formatted(result, rule, opts->media_indent, 1);
|
|
558
|
+
} else {
|
|
559
|
+
serialize_rule(result, rule);
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
} else {
|
|
564
|
+
if (opts->decl_indent_media) {
|
|
565
|
+
serialize_rule_formatted(result, rule, opts->media_indent, 1);
|
|
566
|
+
} else {
|
|
567
|
+
serialize_rule(result, rule);
|
|
568
|
+
}
|
|
569
|
+
rb_hash_aset(processed_rule_ids, rule_id, Qtrue);
|
|
570
|
+
}
|
|
571
|
+
} else {
|
|
572
|
+
if (opts->decl_indent_media) {
|
|
573
|
+
serialize_rule_formatted(result, rule, opts->media_indent, 1);
|
|
574
|
+
} else {
|
|
575
|
+
serialize_rule(result, rule);
|
|
576
|
+
}
|
|
577
|
+
rb_hash_aset(processed_rule_ids, rule_id, Qtrue);
|
|
578
|
+
}
|
|
380
579
|
}
|
|
381
580
|
}
|
|
382
581
|
|
|
@@ -385,9 +584,38 @@ static VALUE stylesheet_to_s_original(VALUE rules_array, VALUE media_index, VALU
|
|
|
385
584
|
rb_str_cat2(result, "}\n");
|
|
386
585
|
}
|
|
387
586
|
|
|
587
|
+
RB_GC_GUARD(rule_to_media);
|
|
588
|
+
RB_GC_GUARD(processed_rule_ids);
|
|
388
589
|
return result;
|
|
389
590
|
}
|
|
390
591
|
|
|
592
|
+
// Original stylesheet serialization (no nesting support) - compact format
|
|
593
|
+
static VALUE stylesheet_to_s_original(VALUE rules_array, VALUE media_index, VALUE charset, VALUE selector_lists) {
|
|
594
|
+
Check_Type(rules_array, T_ARRAY);
|
|
595
|
+
Check_Type(media_index, T_HASH);
|
|
596
|
+
|
|
597
|
+
VALUE result = rb_str_new_cstr("");
|
|
598
|
+
|
|
599
|
+
// Add charset if present
|
|
600
|
+
if (!NIL_P(charset)) {
|
|
601
|
+
rb_str_cat2(result, "@charset \"");
|
|
602
|
+
rb_str_append(result, charset);
|
|
603
|
+
rb_str_cat2(result, "\";\n");
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
// Compact formatting options
|
|
607
|
+
struct format_opts opts = {
|
|
608
|
+
.opening_brace = " { ",
|
|
609
|
+
.closing_brace = " }\n",
|
|
610
|
+
.media_indent = "",
|
|
611
|
+
.decl_indent_base = NULL,
|
|
612
|
+
.decl_indent_media = NULL,
|
|
613
|
+
.add_blank_lines = 0
|
|
614
|
+
};
|
|
615
|
+
|
|
616
|
+
return serialize_stylesheet_with_grouping(rules_array, media_index, result, selector_lists, &opts);
|
|
617
|
+
}
|
|
618
|
+
|
|
391
619
|
// Forward declarations
|
|
392
620
|
static void serialize_children_only(VALUE result, VALUE rules_array, long rule_idx,
|
|
393
621
|
VALUE rule_to_media, VALUE parent_to_children, VALUE parent_selector,
|
|
@@ -549,18 +777,34 @@ static void serialize_rule_with_children(VALUE result, VALUE rules_array, long r
|
|
|
549
777
|
|
|
550
778
|
if (formatted) {
|
|
551
779
|
// Formatted output with indentation
|
|
780
|
+
DEBUG_PRINTF("[SERIALIZE_RULE] Formatted mode, indent_level=%d, selector=%s\n", indent_level, RSTRING_PTR(selector));
|
|
552
781
|
rb_str_append(result, selector);
|
|
553
782
|
rb_str_cat2(result, " {\n");
|
|
554
783
|
|
|
784
|
+
// Build indent strings based on indent_level
|
|
785
|
+
// Declarations are inside the rule, so add 1 level (2 spaces per level)
|
|
786
|
+
// Closing brace matches the opening selector level
|
|
787
|
+
char decl_indent[MAX_INDENT_BUFFER];
|
|
788
|
+
char closing_indent[MAX_INDENT_BUFFER];
|
|
789
|
+
int decl_spaces = (indent_level + 1) * 2;
|
|
790
|
+
int closing_spaces = indent_level * 2;
|
|
791
|
+
memset(decl_indent, ' ', decl_spaces);
|
|
792
|
+
decl_indent[decl_spaces] = '\0';
|
|
793
|
+
memset(closing_indent, ' ', closing_spaces);
|
|
794
|
+
closing_indent[closing_spaces] = '\0';
|
|
795
|
+
|
|
555
796
|
// Serialize own declarations with indentation (each on its own line)
|
|
556
797
|
if (!NIL_P(declarations) && RARRAY_LEN(declarations) > 0) {
|
|
557
|
-
|
|
798
|
+
DEBUG_PRINTF("[SERIALIZE_RULE] Serializing %ld declarations with indent='%s' (%d spaces)\n",
|
|
799
|
+
RARRAY_LEN(declarations), decl_indent, decl_spaces);
|
|
800
|
+
serialize_declarations_formatted(result, declarations, decl_indent);
|
|
558
801
|
}
|
|
559
802
|
|
|
560
803
|
// Serialize nested children
|
|
561
804
|
serialize_children_only(result, rules_array, rule_idx, rule_to_media, parent_to_children,
|
|
562
805
|
selector, declarations, formatted, indent_level + 1);
|
|
563
806
|
|
|
807
|
+
rb_str_cat2(result, closing_indent);
|
|
564
808
|
rb_str_cat2(result, "}\n");
|
|
565
809
|
} else {
|
|
566
810
|
// Compact output
|
|
@@ -576,16 +820,21 @@ static void serialize_rule_with_children(VALUE result, VALUE rules_array, long r
|
|
|
576
820
|
|
|
577
821
|
rb_str_cat2(result, " }\n");
|
|
578
822
|
}
|
|
823
|
+
|
|
824
|
+
// Prevent compiler from optimizing away 'rule' before we're done with selector/declarations
|
|
825
|
+
RB_GC_GUARD(rule);
|
|
579
826
|
}
|
|
580
827
|
|
|
581
828
|
// New stylesheet serialization entry point - checks for nesting and delegates
|
|
582
|
-
static VALUE stylesheet_to_s_new(VALUE self, VALUE rules_array, VALUE media_index, VALUE charset, VALUE has_nesting) {
|
|
829
|
+
static VALUE stylesheet_to_s_new(VALUE self, VALUE rules_array, VALUE media_index, VALUE charset, VALUE has_nesting, VALUE selector_lists) {
|
|
583
830
|
Check_Type(rules_array, T_ARRAY);
|
|
584
831
|
Check_Type(media_index, T_HASH);
|
|
832
|
+
// TODO: Phase 2 - use selector_lists for grouping
|
|
833
|
+
(void)selector_lists; // Suppress unused parameter warning
|
|
585
834
|
|
|
586
835
|
// Fast path: if no nesting, use original implementation (zero overhead)
|
|
587
836
|
if (!RTEST(has_nesting)) {
|
|
588
|
-
return stylesheet_to_s_original(rules_array, media_index, charset);
|
|
837
|
+
return stylesheet_to_s_original(rules_array, media_index, charset, selector_lists);
|
|
589
838
|
}
|
|
590
839
|
|
|
591
840
|
// SLOW PATH: Has nesting - use lookahead approach
|
|
@@ -626,6 +875,10 @@ static VALUE stylesheet_to_s_new(VALUE self, VALUE rules_array, VALUE media_inde
|
|
|
626
875
|
|
|
627
876
|
DEBUG_PRINTF("[MAP] parent_to_children map: %s\n", RSTRING_PTR(rb_inspect(parent_to_children)));
|
|
628
877
|
|
|
878
|
+
// Track media block state for proper opening/closing
|
|
879
|
+
VALUE current_media = Qnil;
|
|
880
|
+
int in_media_block = 0;
|
|
881
|
+
|
|
629
882
|
// Serialize only top-level rules (parent_rule_id == nil)
|
|
630
883
|
// Children are serialized recursively
|
|
631
884
|
DEBUG_PRINTF("[SERIALIZE] Starting serialization, total_rules=%ld\n", total_rules);
|
|
@@ -643,78 +896,25 @@ static VALUE stylesheet_to_s_new(VALUE self, VALUE rules_array, VALUE media_inde
|
|
|
643
896
|
continue;
|
|
644
897
|
}
|
|
645
898
|
|
|
646
|
-
//
|
|
647
|
-
if (rb_obj_is_kind_of(rule, cAtRule)) {
|
|
648
|
-
serialize_at_rule(result, rule);
|
|
649
|
-
continue;
|
|
650
|
-
}
|
|
651
|
-
|
|
652
|
-
// Serialize rule with nested children
|
|
653
|
-
serialize_rule_with_children(
|
|
654
|
-
result, rules_array, i, rule_to_media, parent_to_children,
|
|
655
|
-
0, // formatted (compact)
|
|
656
|
-
0 // indent_level (top-level)
|
|
657
|
-
);
|
|
658
|
-
}
|
|
659
|
-
|
|
660
|
-
return result;
|
|
661
|
-
}
|
|
662
|
-
|
|
663
|
-
// Original formatted serialization (no nesting support)
|
|
664
|
-
static VALUE stylesheet_to_formatted_s_original(VALUE rules_array, VALUE media_index, VALUE charset) {
|
|
665
|
-
long total_rules = RARRAY_LEN(rules_array);
|
|
666
|
-
VALUE result = rb_str_new_cstr("");
|
|
667
|
-
|
|
668
|
-
// Add charset if present
|
|
669
|
-
if (!NIL_P(charset)) {
|
|
670
|
-
rb_str_cat2(result, "@charset \"");
|
|
671
|
-
rb_str_append(result, charset);
|
|
672
|
-
rb_str_cat2(result, "\";\n");
|
|
673
|
-
}
|
|
674
|
-
|
|
675
|
-
// Build a map from rule_id to media query symbol
|
|
676
|
-
VALUE rule_to_media = rb_hash_new();
|
|
677
|
-
struct build_rule_map_ctx map_ctx = { rule_to_media };
|
|
678
|
-
rb_hash_foreach(media_index, build_rule_map_callback, (VALUE)&map_ctx);
|
|
679
|
-
|
|
680
|
-
// Iterate through rules, grouping consecutive media queries
|
|
681
|
-
VALUE current_media = Qnil;
|
|
682
|
-
int in_media_block = 0;
|
|
683
|
-
|
|
684
|
-
for (long i = 0; i < total_rules; i++) {
|
|
685
|
-
VALUE rule = rb_ary_entry(rules_array, i);
|
|
899
|
+
// Get media for this rule
|
|
686
900
|
VALUE rule_id = rb_struct_aref(rule, INT2FIX(RULE_ID));
|
|
687
901
|
VALUE rule_media = rb_hash_aref(rule_to_media, rule_id);
|
|
688
|
-
int is_first_rule = (i == 0);
|
|
689
902
|
|
|
903
|
+
// Handle media block transitions
|
|
690
904
|
if (NIL_P(rule_media)) {
|
|
691
|
-
// Not in
|
|
905
|
+
// Not in media - close any open media block
|
|
692
906
|
if (in_media_block) {
|
|
693
907
|
rb_str_cat2(result, "}\n");
|
|
694
908
|
in_media_block = 0;
|
|
695
909
|
current_media = Qnil;
|
|
696
910
|
}
|
|
697
|
-
|
|
698
|
-
// Add blank line prefix for non-first rules
|
|
699
|
-
if (!is_first_rule) {
|
|
700
|
-
rb_str_cat2(result, "\n");
|
|
701
|
-
}
|
|
702
|
-
|
|
703
|
-
// Output rule with no indentation (always single newline suffix)
|
|
704
|
-
serialize_rule_formatted(result, rule, "", 1);
|
|
705
911
|
} else {
|
|
706
|
-
//
|
|
912
|
+
// In media - check if we need to open/change block
|
|
707
913
|
if (NIL_P(current_media) || !rb_equal(current_media, rule_media)) {
|
|
708
914
|
// Close previous media block if open
|
|
709
915
|
if (in_media_block) {
|
|
710
916
|
rb_str_cat2(result, "}\n");
|
|
711
917
|
}
|
|
712
|
-
|
|
713
|
-
// Add blank line prefix for non-first rules
|
|
714
|
-
if (!is_first_rule) {
|
|
715
|
-
rb_str_cat2(result, "\n");
|
|
716
|
-
}
|
|
717
|
-
|
|
718
918
|
// Open new media block
|
|
719
919
|
current_media = rule_media;
|
|
720
920
|
rb_str_cat2(result, "@media ");
|
|
@@ -722,11 +922,20 @@ static VALUE stylesheet_to_formatted_s_original(VALUE rules_array, VALUE media_i
|
|
|
722
922
|
rb_str_cat2(result, " {\n");
|
|
723
923
|
in_media_block = 1;
|
|
724
924
|
}
|
|
925
|
+
}
|
|
725
926
|
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
927
|
+
// Check if this is an AtRule
|
|
928
|
+
if (rb_obj_is_kind_of(rule, cAtRule)) {
|
|
929
|
+
serialize_at_rule(result, rule);
|
|
930
|
+
continue;
|
|
729
931
|
}
|
|
932
|
+
|
|
933
|
+
// Serialize rule with nested children
|
|
934
|
+
serialize_rule_with_children(
|
|
935
|
+
result, rules_array, i, rule_to_media, parent_to_children,
|
|
936
|
+
0, // formatted (compact)
|
|
937
|
+
0 // indent_level (top-level)
|
|
938
|
+
);
|
|
730
939
|
}
|
|
731
940
|
|
|
732
941
|
// Close final media block if still open
|
|
@@ -734,17 +943,43 @@ static VALUE stylesheet_to_formatted_s_original(VALUE rules_array, VALUE media_i
|
|
|
734
943
|
rb_str_cat2(result, "}\n");
|
|
735
944
|
}
|
|
736
945
|
|
|
946
|
+
RB_GC_GUARD(rule_to_media);
|
|
947
|
+
RB_GC_GUARD(parent_to_children);
|
|
737
948
|
return result;
|
|
738
949
|
}
|
|
739
950
|
|
|
951
|
+
// Original formatted serialization (no nesting support)
|
|
952
|
+
static VALUE stylesheet_to_formatted_s_original(VALUE rules_array, VALUE media_index, VALUE charset, VALUE selector_lists) {
|
|
953
|
+
VALUE result = rb_str_new_cstr("");
|
|
954
|
+
|
|
955
|
+
// Add charset if present
|
|
956
|
+
if (!NIL_P(charset)) {
|
|
957
|
+
rb_str_cat2(result, "@charset \"");
|
|
958
|
+
rb_str_append(result, charset);
|
|
959
|
+
rb_str_cat2(result, "\";\n");
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
// Formatted output options
|
|
963
|
+
struct format_opts opts = {
|
|
964
|
+
.opening_brace = " {\n",
|
|
965
|
+
.closing_brace = "}\n",
|
|
966
|
+
.media_indent = " ",
|
|
967
|
+
.decl_indent_base = " ",
|
|
968
|
+
.decl_indent_media = " ",
|
|
969
|
+
.add_blank_lines = 1
|
|
970
|
+
};
|
|
971
|
+
|
|
972
|
+
return serialize_stylesheet_with_grouping(rules_array, media_index, result, selector_lists, &opts);
|
|
973
|
+
}
|
|
974
|
+
|
|
740
975
|
// Formatted version with indentation and newlines (with nesting support)
|
|
741
|
-
static VALUE stylesheet_to_formatted_s_new(VALUE self, VALUE rules_array, VALUE media_index, VALUE charset, VALUE has_nesting) {
|
|
976
|
+
static VALUE stylesheet_to_formatted_s_new(VALUE self, VALUE rules_array, VALUE media_index, VALUE charset, VALUE has_nesting, VALUE selector_lists) {
|
|
742
977
|
Check_Type(rules_array, T_ARRAY);
|
|
743
978
|
Check_Type(media_index, T_HASH);
|
|
744
979
|
|
|
745
980
|
// Fast path: if no nesting, use original implementation (zero overhead)
|
|
746
981
|
if (!RTEST(has_nesting)) {
|
|
747
|
-
return stylesheet_to_formatted_s_original(rules_array, media_index, charset);
|
|
982
|
+
return stylesheet_to_formatted_s_original(rules_array, media_index, charset, selector_lists);
|
|
748
983
|
}
|
|
749
984
|
|
|
750
985
|
// SLOW PATH: Has nesting - use parameterized serialization with formatted=1
|
|
@@ -779,6 +1014,10 @@ static VALUE stylesheet_to_formatted_s_new(VALUE self, VALUE rules_array, VALUE
|
|
|
779
1014
|
}
|
|
780
1015
|
}
|
|
781
1016
|
|
|
1017
|
+
// Track media block state for proper opening/closing
|
|
1018
|
+
VALUE current_media = Qnil;
|
|
1019
|
+
int in_media_block = 0;
|
|
1020
|
+
|
|
782
1021
|
// Serialize only top-level rules (parent_rule_id == nil)
|
|
783
1022
|
for (long i = 0; i < total_rules; i++) {
|
|
784
1023
|
VALUE rule = rb_ary_entry(rules_array, i);
|
|
@@ -789,20 +1028,68 @@ static VALUE stylesheet_to_formatted_s_new(VALUE self, VALUE rules_array, VALUE
|
|
|
789
1028
|
continue;
|
|
790
1029
|
}
|
|
791
1030
|
|
|
1031
|
+
// Get media for this rule
|
|
1032
|
+
VALUE rule_id = rb_struct_aref(rule, INT2FIX(RULE_ID));
|
|
1033
|
+
VALUE rule_media = rb_hash_aref(rule_to_media, rule_id);
|
|
1034
|
+
|
|
1035
|
+
// Handle media block transitions
|
|
1036
|
+
if (NIL_P(rule_media)) {
|
|
1037
|
+
// Not in media - close any open media block
|
|
1038
|
+
if (in_media_block) {
|
|
1039
|
+
rb_str_cat2(result, "}\n");
|
|
1040
|
+
in_media_block = 0;
|
|
1041
|
+
current_media = Qnil;
|
|
1042
|
+
|
|
1043
|
+
// Add blank line after closing media block
|
|
1044
|
+
rb_str_cat2(result, "\n");
|
|
1045
|
+
}
|
|
1046
|
+
} else {
|
|
1047
|
+
// In media - check if we need to open/change block
|
|
1048
|
+
if (NIL_P(current_media) || !rb_equal(current_media, rule_media)) {
|
|
1049
|
+
// Close previous media block if open
|
|
1050
|
+
if (in_media_block) {
|
|
1051
|
+
rb_str_cat2(result, "}\n");
|
|
1052
|
+
} else if (RSTRING_LEN(result) > 0) {
|
|
1053
|
+
// Add blank line before new media block (except at start)
|
|
1054
|
+
rb_str_cat2(result, "\n");
|
|
1055
|
+
}
|
|
1056
|
+
// Open new media block
|
|
1057
|
+
current_media = rule_media;
|
|
1058
|
+
rb_str_cat2(result, "@media ");
|
|
1059
|
+
rb_str_append(result, rb_sym2str(rule_media));
|
|
1060
|
+
rb_str_cat2(result, " {\n");
|
|
1061
|
+
in_media_block = 1;
|
|
1062
|
+
}
|
|
1063
|
+
}
|
|
1064
|
+
|
|
792
1065
|
// Check if this is an AtRule
|
|
793
1066
|
if (rb_obj_is_kind_of(rule, cAtRule)) {
|
|
794
1067
|
serialize_at_rule(result, rule);
|
|
795
1068
|
continue;
|
|
796
1069
|
}
|
|
797
1070
|
|
|
1071
|
+
// Add indent if inside media block
|
|
1072
|
+
if (in_media_block) {
|
|
1073
|
+
DEBUG_PRINTF("[FORMATTED] Adding base indent for media block\n");
|
|
1074
|
+
rb_str_cat2(result, " ");
|
|
1075
|
+
}
|
|
1076
|
+
|
|
798
1077
|
// Serialize rule with nested children
|
|
1078
|
+
DEBUG_PRINTF("[FORMATTED] Calling serialize_rule_with_children, in_media_block=%d\n", in_media_block);
|
|
799
1079
|
serialize_rule_with_children(
|
|
800
1080
|
result, rules_array, i, rule_to_media, parent_to_children,
|
|
801
1081
|
1, // formatted (with indentation)
|
|
802
|
-
0 // indent_level (
|
|
1082
|
+
in_media_block ? 1 : 0 // indent_level (1 if inside media block, 0 otherwise)
|
|
803
1083
|
);
|
|
804
1084
|
}
|
|
805
1085
|
|
|
1086
|
+
// Close final media block if still open
|
|
1087
|
+
if (in_media_block) {
|
|
1088
|
+
rb_str_cat2(result, "}\n");
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
RB_GC_GUARD(rule_to_media);
|
|
1092
|
+
RB_GC_GUARD(parent_to_children);
|
|
806
1093
|
return result;
|
|
807
1094
|
}
|
|
808
1095
|
|
|
@@ -1059,9 +1346,9 @@ void Init_native_extension(void) {
|
|
|
1059
1346
|
cStylesheet = rb_define_class_under(mCataract, "Stylesheet", rb_cObject);
|
|
1060
1347
|
|
|
1061
1348
|
// Define module functions
|
|
1062
|
-
rb_define_module_function(mCataract, "_parse_css", parse_css_new, 1);
|
|
1063
|
-
rb_define_module_function(mCataract, "_stylesheet_to_s", stylesheet_to_s_new,
|
|
1064
|
-
rb_define_module_function(mCataract, "_stylesheet_to_formatted_s", stylesheet_to_formatted_s_new,
|
|
1349
|
+
rb_define_module_function(mCataract, "_parse_css", parse_css_new, -1);
|
|
1350
|
+
rb_define_module_function(mCataract, "_stylesheet_to_s", stylesheet_to_s_new, 5);
|
|
1351
|
+
rb_define_module_function(mCataract, "_stylesheet_to_formatted_s", stylesheet_to_formatted_s_new, 5);
|
|
1065
1352
|
rb_define_module_function(mCataract, "parse_media_types", parse_media_types, 1);
|
|
1066
1353
|
rb_define_module_function(mCataract, "parse_declarations", new_parse_declarations, 1);
|
|
1067
1354
|
rb_define_module_function(mCataract, "flatten", cataract_flatten, 1);
|