cataract 0.2.4 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.github/workflows/test.yml +6 -6
- data/.gitignore +17 -3
- data/.rubocop.yml +1 -0
- data/BENCHMARKS.md +24 -6
- data/CHANGELOG.md +29 -1
- data/Gemfile +2 -2
- data/examples/css_analyzer/analyzer.rb +9 -3
- data/examples/css_analyzer/analyzers/base.rb +1 -1
- data/ext/cataract/cataract.c +251 -458
- data/ext/cataract/cataract.h +127 -16
- data/ext/cataract/css_parser.c +1029 -700
- data/ext/cataract/extconf.rb +1 -2
- data/ext/cataract/flatten.c +279 -991
- data/ext/cataract/shorthand_expander.c +80 -102
- data/ext/cataract_color/color_conversion.c +1 -1
- data/lib/cataract/declarations.rb +13 -7
- data/lib/cataract/error.rb +49 -0
- data/lib/cataract/import_resolver.rb +36 -16
- data/lib/cataract/pure/byte_constants.rb +4 -1
- data/lib/cataract/pure/flatten.rb +143 -145
- data/lib/cataract/pure/parser.rb +623 -388
- data/lib/cataract/pure/serializer.rb +109 -104
- data/lib/cataract/pure/specificity.rb +112 -156
- data/lib/cataract/pure.rb +1 -6
- data/lib/cataract/stylesheet.rb +480 -410
- data/lib/cataract/version.rb +1 -1
- data/lib/cataract.rb +1 -0
- metadata +3 -13
- data/ext/cataract/import_scanner.c +0 -174
- data/ext/cataract_old/cataract.c +0 -393
- data/ext/cataract_old/cataract.h +0 -250
- data/ext/cataract_old/css_parser.c +0 -933
- data/ext/cataract_old/extconf.rb +0 -67
- data/ext/cataract_old/import_scanner.c +0 -174
- data/ext/cataract_old/merge.c +0 -776
- data/ext/cataract_old/shorthand_expander.c +0 -902
- data/ext/cataract_old/specificity.c +0 -213
- data/ext/cataract_old/stylesheet.c +0 -290
- data/ext/cataract_old/value_splitter.c +0 -116
data/ext/cataract/cataract.c
CHANGED
|
@@ -14,7 +14,7 @@ VALUE cMediaQuery;
|
|
|
14
14
|
VALUE eCataractError;
|
|
15
15
|
VALUE eDepthError;
|
|
16
16
|
VALUE eSizeError;
|
|
17
|
-
VALUE
|
|
17
|
+
VALUE eParseError;
|
|
18
18
|
|
|
19
19
|
// ============================================================================
|
|
20
20
|
// Helper Functions
|
|
@@ -131,8 +131,10 @@ VALUE parse_css_new(int argc, VALUE *argv, VALUE self) {
|
|
|
131
131
|
* @param charset [String, nil] Optional @charset value
|
|
132
132
|
* @return [String] CSS string
|
|
133
133
|
*/
|
|
134
|
-
// Helper to serialize a
|
|
135
|
-
|
|
134
|
+
// Helper to serialize a rule's declarations - indent == NULL means compact
|
|
135
|
+
// (semicolon+space separated, single line); non-NULL means one declaration
|
|
136
|
+
// per line, prefixed with indent.
|
|
137
|
+
static void serialize_declarations(VALUE result, VALUE declarations, const char *indent) {
|
|
136
138
|
long decl_len = RARRAY_LEN(declarations);
|
|
137
139
|
for (long j = 0; j < decl_len; j++) {
|
|
138
140
|
VALUE decl = rb_ary_entry(declarations, j);
|
|
@@ -140,6 +142,7 @@ static void serialize_declarations(VALUE result, VALUE declarations) {
|
|
|
140
142
|
VALUE value = rb_struct_aref(decl, INT2FIX(DECL_VALUE));
|
|
141
143
|
VALUE important = rb_struct_aref(decl, INT2FIX(DECL_IMPORTANT));
|
|
142
144
|
|
|
145
|
+
if (indent) rb_str_cat2(result, indent);
|
|
143
146
|
rb_str_append(result, property);
|
|
144
147
|
rb_str_cat2(result, ": ");
|
|
145
148
|
rb_str_append(result, value);
|
|
@@ -148,34 +151,15 @@ static void serialize_declarations(VALUE result, VALUE declarations) {
|
|
|
148
151
|
rb_str_cat2(result, " !important");
|
|
149
152
|
}
|
|
150
153
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
// Formatted version - each declaration on its own line with indentation
|
|
161
|
-
static void serialize_declarations_formatted(VALUE result, VALUE declarations, const char *indent) {
|
|
162
|
-
long decl_len = RARRAY_LEN(declarations);
|
|
163
|
-
for (long j = 0; j < decl_len; j++) {
|
|
164
|
-
VALUE decl = rb_ary_entry(declarations, j);
|
|
165
|
-
VALUE property = rb_struct_aref(decl, INT2FIX(DECL_PROPERTY));
|
|
166
|
-
VALUE value = rb_struct_aref(decl, INT2FIX(DECL_VALUE));
|
|
167
|
-
VALUE important = rb_struct_aref(decl, INT2FIX(DECL_IMPORTANT));
|
|
168
|
-
|
|
169
|
-
rb_str_cat2(result, indent);
|
|
170
|
-
rb_str_append(result, property);
|
|
171
|
-
rb_str_cat2(result, ": ");
|
|
172
|
-
rb_str_append(result, value);
|
|
173
|
-
|
|
174
|
-
if (RTEST(important)) {
|
|
175
|
-
rb_str_cat2(result, " !important");
|
|
154
|
+
if (indent) {
|
|
155
|
+
rb_str_cat2(result, ";\n");
|
|
156
|
+
} else {
|
|
157
|
+
rb_str_cat2(result, ";");
|
|
158
|
+
// Add space after semicolon except for last declaration
|
|
159
|
+
if (j < decl_len - 1) {
|
|
160
|
+
rb_str_cat2(result, " ");
|
|
161
|
+
}
|
|
176
162
|
}
|
|
177
|
-
|
|
178
|
-
rb_str_cat2(result, ";\n");
|
|
179
163
|
}
|
|
180
164
|
}
|
|
181
165
|
|
|
@@ -201,13 +185,13 @@ static void serialize_at_rule(VALUE result, VALUE at_rule) {
|
|
|
201
185
|
rb_str_cat2(result, " ");
|
|
202
186
|
rb_str_append(result, nested_selector);
|
|
203
187
|
rb_str_cat2(result, " { ");
|
|
204
|
-
serialize_declarations(result, nested_declarations);
|
|
188
|
+
serialize_declarations(result, nested_declarations, NULL);
|
|
205
189
|
rb_str_cat2(result, " }\n");
|
|
206
190
|
}
|
|
207
191
|
} else {
|
|
208
192
|
// Serialize as declarations (e.g., @font-face)
|
|
209
193
|
rb_str_cat2(result, " ");
|
|
210
|
-
serialize_declarations(result, content);
|
|
194
|
+
serialize_declarations(result, content, NULL);
|
|
211
195
|
rb_str_cat2(result, "\n");
|
|
212
196
|
}
|
|
213
197
|
}
|
|
@@ -284,7 +268,7 @@ static void serialize_rule(VALUE result, VALUE rule) {
|
|
|
284
268
|
|
|
285
269
|
rb_str_append(result, selector);
|
|
286
270
|
rb_str_cat2(result, " { ");
|
|
287
|
-
serialize_declarations(result, declarations);
|
|
271
|
+
serialize_declarations(result, declarations, NULL);
|
|
288
272
|
rb_str_cat2(result, " }\n");
|
|
289
273
|
}
|
|
290
274
|
|
|
@@ -318,7 +302,7 @@ static void serialize_at_rule_formatted(VALUE result, VALUE at_rule, const char
|
|
|
318
302
|
VALUE nested_indent = rb_str_new_cstr(indent);
|
|
319
303
|
rb_str_cat2(nested_indent, " ");
|
|
320
304
|
const char *nested_indent_ptr = RSTRING_PTR(nested_indent);
|
|
321
|
-
|
|
305
|
+
serialize_declarations(result, nested_declarations, nested_indent_ptr);
|
|
322
306
|
RB_GC_GUARD(nested_indent);
|
|
323
307
|
|
|
324
308
|
// Closing brace (2-space indent)
|
|
@@ -330,7 +314,7 @@ static void serialize_at_rule_formatted(VALUE result, VALUE at_rule, const char
|
|
|
330
314
|
VALUE content_indent = rb_str_new_cstr(indent);
|
|
331
315
|
rb_str_cat2(content_indent, " ");
|
|
332
316
|
const char *content_indent_ptr = RSTRING_PTR(content_indent);
|
|
333
|
-
|
|
317
|
+
serialize_declarations(result, content, content_indent_ptr);
|
|
334
318
|
RB_GC_GUARD(content_indent);
|
|
335
319
|
}
|
|
336
320
|
}
|
|
@@ -360,7 +344,7 @@ static void serialize_rule_formatted(VALUE result, VALUE rule, const char *inden
|
|
|
360
344
|
VALUE decl_indent = rb_str_new_cstr(indent);
|
|
361
345
|
rb_str_cat2(decl_indent, " ");
|
|
362
346
|
const char *decl_indent_ptr = RSTRING_PTR(decl_indent);
|
|
363
|
-
|
|
347
|
+
serialize_declarations(result, declarations, decl_indent_ptr);
|
|
364
348
|
RB_GC_GUARD(decl_indent);
|
|
365
349
|
|
|
366
350
|
// Closing brace - double newline for all except last rule
|
|
@@ -393,6 +377,23 @@ static int build_mq_reverse_map_callback(VALUE list_id, VALUE mq_ids, VALUE arg)
|
|
|
393
377
|
return ST_CONTINUE;
|
|
394
378
|
}
|
|
395
379
|
|
|
380
|
+
// Rule and AtRule don't share a member layout past their first few fields
|
|
381
|
+
// (see the AT_RULE_* comment in cataract.h), so reading parent_rule_id or
|
|
382
|
+
// media_query_id off a rule of unknown type needs to dispatch on which
|
|
383
|
+
// struct it actually is rather than always using Rule's index - otherwise,
|
|
384
|
+
// for an AtRule, RULE_PARENT_RULE_ID's index (4) actually reads its
|
|
385
|
+
// media_query_id field instead.
|
|
386
|
+
static inline VALUE rule_parent_id(VALUE rule) {
|
|
387
|
+
// AtRule instances are never nested via CSS nesting - only Rule can be.
|
|
388
|
+
return rb_obj_is_kind_of(rule, cAtRule) ? Qnil : rb_struct_aref(rule, INT2FIX(RULE_PARENT_RULE_ID));
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
static inline VALUE rule_media_query_id_of(VALUE rule) {
|
|
392
|
+
return rb_obj_is_kind_of(rule, cAtRule)
|
|
393
|
+
? rb_struct_aref(rule, INT2FIX(AT_RULE_MEDIA_QUERY_ID))
|
|
394
|
+
: rb_struct_aref(rule, INT2FIX(RULE_MEDIA_QUERY_ID));
|
|
395
|
+
}
|
|
396
|
+
|
|
396
397
|
// Formatting options for stylesheet serialization
|
|
397
398
|
// Avoids mode flags and if/else branches - all behavior controlled by struct values
|
|
398
399
|
struct format_opts {
|
|
@@ -404,6 +405,126 @@ struct format_opts {
|
|
|
404
405
|
int add_blank_lines; // 0 (compact) vs 1 (formatted)
|
|
405
406
|
};
|
|
406
407
|
|
|
408
|
+
// Serialize a single rule, compact or formatted depending on whether
|
|
409
|
+
// decl_indent is NULL - mirrors the format_opts indent-as-mode-flag
|
|
410
|
+
// convention used throughout this file.
|
|
411
|
+
static inline void emit_rule(VALUE result, VALUE rule, const char *decl_indent, const char *rule_prefix) {
|
|
412
|
+
if (decl_indent) {
|
|
413
|
+
serialize_rule_formatted(result, rule, rule_prefix, 1);
|
|
414
|
+
} else {
|
|
415
|
+
serialize_rule(result, rule);
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
// Scan rule_ids_in_list for other not-yet-processed rules that share
|
|
420
|
+
// rule_media_query_id and rule_declarations with the current rule, marking
|
|
421
|
+
// each match as processed. Returns an array of their selectors (does not
|
|
422
|
+
// include the current rule's own selector or mark it processed - the
|
|
423
|
+
// 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 rules_array, VALUE processed_rule_ids) {
|
|
425
|
+
VALUE matching_selectors = rb_ary_new();
|
|
426
|
+
long list_len = RARRAY_LEN(rule_ids_in_list);
|
|
427
|
+
|
|
428
|
+
for (long j = 0; j < list_len; j++) {
|
|
429
|
+
VALUE other_rule_id = rb_ary_entry(rule_ids_in_list, j);
|
|
430
|
+
|
|
431
|
+
// Skip if already processed
|
|
432
|
+
if (RTEST(rb_hash_aref(processed_rule_ids, other_rule_id))) {
|
|
433
|
+
continue;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
// Find the rule by ID
|
|
437
|
+
VALUE other_rule = rb_ary_entry(rules_array, FIX2INT(other_rule_id));
|
|
438
|
+
if (NIL_P(other_rule)) continue;
|
|
439
|
+
|
|
440
|
+
// Check same media context (compare media_query_id directly)
|
|
441
|
+
VALUE other_rule_media_query_id = rb_struct_aref(other_rule, INT2FIX(RULE_MEDIA_QUERY_ID));
|
|
442
|
+
if (!rb_equal(rule_media_query_id, other_rule_media_query_id)) {
|
|
443
|
+
continue;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
// Check if declarations match
|
|
447
|
+
VALUE other_declarations = rb_struct_aref(other_rule, INT2FIX(RULE_DECLARATIONS));
|
|
448
|
+
if (rb_equal(rule_declarations, other_declarations)) {
|
|
449
|
+
VALUE other_selector = rb_struct_aref(other_rule, INT2FIX(RULE_SELECTOR));
|
|
450
|
+
rb_ary_push(matching_selectors, other_selector);
|
|
451
|
+
rb_hash_aset(processed_rule_ids, other_rule_id, Qtrue);
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
return matching_selectors;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
// Emit a comma-joined group of matching selectors sharing one declaration
|
|
459
|
+
// block, or fall back to a single rule if grouping didn't find any matches.
|
|
460
|
+
// rule_prefix is written before the (possibly joined) selector line and
|
|
461
|
+
// before the closing brace - "" at the top level, opts->media_indent inside
|
|
462
|
+
// a media block (a no-op when "").
|
|
463
|
+
static void emit_grouped_rule(VALUE result, VALUE rule, VALUE matching_selectors, VALUE rule_declarations, const struct format_opts *opts, const char *decl_indent, const char *rule_prefix) {
|
|
464
|
+
if (RARRAY_LEN(matching_selectors) > 1) {
|
|
465
|
+
VALUE selector_str = rb_ary_join(matching_selectors, rb_str_new_cstr(", "));
|
|
466
|
+
rb_str_cat2(result, rule_prefix);
|
|
467
|
+
rb_str_append(result, selector_str);
|
|
468
|
+
rb_str_cat2(result, opts->opening_brace);
|
|
469
|
+
serialize_declarations(result, rule_declarations, decl_indent);
|
|
470
|
+
rb_str_cat2(result, rule_prefix);
|
|
471
|
+
rb_str_cat2(result, opts->closing_brace);
|
|
472
|
+
RB_GC_GUARD(selector_str);
|
|
473
|
+
} else {
|
|
474
|
+
emit_rule(result, rule, decl_indent, rule_prefix);
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
// Loop-invariant state shared by every serialize_rule_in_group call within
|
|
479
|
+
// one serialize_stylesheet_with_grouping invocation - populated once instead
|
|
480
|
+
// of threading 5 params through on every call.
|
|
481
|
+
struct rule_group_ctx {
|
|
482
|
+
VALUE selector_lists;
|
|
483
|
+
int grouping_enabled;
|
|
484
|
+
VALUE rules_array;
|
|
485
|
+
VALUE processed_rule_ids;
|
|
486
|
+
const struct format_opts *opts;
|
|
487
|
+
};
|
|
488
|
+
|
|
489
|
+
// Serialize one rule, applying selector-list grouping if enabled. Used
|
|
490
|
+
// identically for both the base (not-in-media) and in-media contexts -
|
|
491
|
+
// in_media selects the relevant format_opts fields ("" and
|
|
492
|
+
// opts->decl_indent_base at the base level; opts->media_indent and
|
|
493
|
+
// opts->decl_indent_media inside a media block).
|
|
494
|
+
static void serialize_rule_in_group(
|
|
495
|
+
VALUE result,
|
|
496
|
+
VALUE rule,
|
|
497
|
+
VALUE rule_id,
|
|
498
|
+
VALUE rule_media_query_id,
|
|
499
|
+
const struct rule_group_ctx *ctx,
|
|
500
|
+
int in_media
|
|
501
|
+
) {
|
|
502
|
+
const char *decl_indent = in_media ? ctx->opts->decl_indent_media : ctx->opts->decl_indent_base;
|
|
503
|
+
const char *rule_prefix = in_media ? ctx->opts->media_indent : "";
|
|
504
|
+
|
|
505
|
+
// Try to group with other rules from same selector list
|
|
506
|
+
// Check if this is a Rule (not AtRule) before accessing selector_list_id
|
|
507
|
+
if (ctx->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
|
+
// Get list of rule IDs in this selector list
|
|
511
|
+
VALUE rule_ids_in_list = rb_hash_aref(ctx->selector_lists, selector_list_id);
|
|
512
|
+
|
|
513
|
+
if (!NIL_P(rule_ids_in_list) && RARRAY_LEN(rule_ids_in_list) > 1) {
|
|
514
|
+
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->rules_array, ctx->processed_rule_ids);
|
|
516
|
+
emit_grouped_rule(result, rule, matching_selectors, rule_declarations, ctx->opts, decl_indent, rule_prefix);
|
|
517
|
+
return;
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
// No grouping applies (disabled, not a Rule, no selector_list_id, or a
|
|
523
|
+
// selector list of 1) - serialize normally
|
|
524
|
+
emit_rule(result, rule, decl_indent, rule_prefix);
|
|
525
|
+
rb_hash_aset(ctx->processed_rule_ids, rule_id, Qtrue);
|
|
526
|
+
}
|
|
527
|
+
|
|
407
528
|
// Private shared implementation for stylesheet serialization with optional selector list grouping
|
|
408
529
|
// All formatting behavior controlled by format_opts struct to avoid mode flags and if/else branches
|
|
409
530
|
static VALUE serialize_stylesheet_with_grouping(
|
|
@@ -430,6 +551,8 @@ static VALUE serialize_stylesheet_with_grouping(
|
|
|
430
551
|
// Track processed rules to avoid duplicates when grouping
|
|
431
552
|
VALUE processed_rule_ids = rb_hash_new();
|
|
432
553
|
|
|
554
|
+
struct rule_group_ctx group_ctx = { selector_lists, grouping_enabled, rules_array, processed_rule_ids, opts };
|
|
555
|
+
|
|
433
556
|
// Iterate through rules in insertion order, grouping consecutive media queries
|
|
434
557
|
VALUE current_media = Qnil;
|
|
435
558
|
int in_media_block = 0;
|
|
@@ -443,8 +566,8 @@ static VALUE serialize_stylesheet_with_grouping(
|
|
|
443
566
|
continue;
|
|
444
567
|
}
|
|
445
568
|
|
|
446
|
-
// Get media_query_id and fetch MediaQuery object (nil for
|
|
447
|
-
VALUE rule_media_query_id =
|
|
569
|
+
// Get media_query_id and fetch MediaQuery object (nil for rules without media query)
|
|
570
|
+
VALUE rule_media_query_id = rule_media_query_id_of(rule);
|
|
448
571
|
VALUE rule_media = Qnil;
|
|
449
572
|
if (!NIL_P(rule_media_query_id)) {
|
|
450
573
|
rule_media = rb_ary_entry(media_queries, FIX2INT(rule_media_query_id));
|
|
@@ -464,95 +587,7 @@ static VALUE serialize_stylesheet_with_grouping(
|
|
|
464
587
|
rb_str_cat2(result, "\n");
|
|
465
588
|
}
|
|
466
589
|
|
|
467
|
-
|
|
468
|
-
// Check if this is a Rule (not AtRule) before accessing selector_list_id
|
|
469
|
-
if (grouping_enabled && rb_obj_is_kind_of(rule, cRule)) {
|
|
470
|
-
VALUE selector_list_id = rb_struct_aref(rule, INT2FIX(RULE_SELECTOR_LIST_ID));
|
|
471
|
-
if (!NIL_P(selector_list_id)) {
|
|
472
|
-
// Get list of rule IDs in this selector list
|
|
473
|
-
VALUE rule_ids_in_list = rb_hash_aref(selector_lists, selector_list_id);
|
|
474
|
-
|
|
475
|
-
if (NIL_P(rule_ids_in_list) || RARRAY_LEN(rule_ids_in_list) <= 1) {
|
|
476
|
-
// Just this rule, serialize normally
|
|
477
|
-
if (opts->decl_indent_base) {
|
|
478
|
-
serialize_rule_formatted(result, rule, "", 1);
|
|
479
|
-
} else {
|
|
480
|
-
serialize_rule(result, rule);
|
|
481
|
-
}
|
|
482
|
-
rb_hash_aset(processed_rule_ids, rule_id, Qtrue);
|
|
483
|
-
} else {
|
|
484
|
-
// Find all rules with matching declarations and same media context
|
|
485
|
-
VALUE matching_selectors = rb_ary_new();
|
|
486
|
-
VALUE rule_declarations = rb_struct_aref(rule, INT2FIX(RULE_DECLARATIONS));
|
|
487
|
-
|
|
488
|
-
long list_len = RARRAY_LEN(rule_ids_in_list);
|
|
489
|
-
for (long j = 0; j < list_len; j++) {
|
|
490
|
-
VALUE other_rule_id = rb_ary_entry(rule_ids_in_list, j);
|
|
491
|
-
|
|
492
|
-
// Skip if already processed
|
|
493
|
-
if (RTEST(rb_hash_aref(processed_rule_ids, other_rule_id))) {
|
|
494
|
-
continue;
|
|
495
|
-
}
|
|
496
|
-
|
|
497
|
-
// Find the rule by ID
|
|
498
|
-
VALUE other_rule = rb_ary_entry(rules_array, FIX2INT(other_rule_id));
|
|
499
|
-
if (NIL_P(other_rule)) continue;
|
|
500
|
-
|
|
501
|
-
// Check same media context (compare media_query_id directly)
|
|
502
|
-
VALUE other_rule_media_query_id = rb_struct_aref(other_rule, INT2FIX(RULE_MEDIA_QUERY_ID));
|
|
503
|
-
if (!rb_equal(rule_media_query_id, other_rule_media_query_id)) {
|
|
504
|
-
continue;
|
|
505
|
-
}
|
|
506
|
-
|
|
507
|
-
// Check if declarations match
|
|
508
|
-
VALUE other_declarations = rb_struct_aref(other_rule, INT2FIX(RULE_DECLARATIONS));
|
|
509
|
-
if (rb_equal(rule_declarations, other_declarations)) {
|
|
510
|
-
VALUE other_selector = rb_struct_aref(other_rule, INT2FIX(RULE_SELECTOR));
|
|
511
|
-
rb_ary_push(matching_selectors, other_selector);
|
|
512
|
-
rb_hash_aset(processed_rule_ids, other_rule_id, Qtrue);
|
|
513
|
-
}
|
|
514
|
-
}
|
|
515
|
-
|
|
516
|
-
// Serialize grouped or single rule
|
|
517
|
-
if (RARRAY_LEN(matching_selectors) > 1) {
|
|
518
|
-
// Group selectors with comma-space separator
|
|
519
|
-
VALUE selector_str = rb_ary_join(matching_selectors, rb_str_new_cstr(", "));
|
|
520
|
-
rb_str_append(result, selector_str);
|
|
521
|
-
rb_str_cat2(result, opts->opening_brace);
|
|
522
|
-
if (opts->decl_indent_base) {
|
|
523
|
-
serialize_declarations_formatted(result, rule_declarations, opts->decl_indent_base);
|
|
524
|
-
} else {
|
|
525
|
-
serialize_declarations(result, rule_declarations);
|
|
526
|
-
}
|
|
527
|
-
rb_str_cat2(result, opts->closing_brace);
|
|
528
|
-
RB_GC_GUARD(selector_str);
|
|
529
|
-
} else {
|
|
530
|
-
// Just one rule, serialize normally
|
|
531
|
-
if (opts->decl_indent_base) {
|
|
532
|
-
serialize_rule_formatted(result, rule, "", 1);
|
|
533
|
-
} else {
|
|
534
|
-
serialize_rule(result, rule);
|
|
535
|
-
}
|
|
536
|
-
}
|
|
537
|
-
}
|
|
538
|
-
} else {
|
|
539
|
-
// No selector_list_id, serialize normally
|
|
540
|
-
if (opts->decl_indent_base) {
|
|
541
|
-
serialize_rule_formatted(result, rule, "", 1);
|
|
542
|
-
} else {
|
|
543
|
-
serialize_rule(result, rule);
|
|
544
|
-
}
|
|
545
|
-
rb_hash_aset(processed_rule_ids, rule_id, Qtrue);
|
|
546
|
-
}
|
|
547
|
-
} else {
|
|
548
|
-
// Grouping disabled, serialize normally
|
|
549
|
-
if (opts->decl_indent_base) {
|
|
550
|
-
serialize_rule_formatted(result, rule, "", 1);
|
|
551
|
-
} else {
|
|
552
|
-
serialize_rule(result, rule);
|
|
553
|
-
}
|
|
554
|
-
rb_hash_aset(processed_rule_ids, rule_id, Qtrue);
|
|
555
|
-
}
|
|
590
|
+
serialize_rule_in_group(result, rule, rule_id, rule_media_query_id, &group_ctx, 0);
|
|
556
591
|
} else {
|
|
557
592
|
// This rule is in a media query
|
|
558
593
|
// Check if media query changed from previous rule (compare MediaQuery objects by value)
|
|
@@ -580,80 +615,7 @@ static VALUE serialize_stylesheet_with_grouping(
|
|
|
580
615
|
in_media_block = 1;
|
|
581
616
|
}
|
|
582
617
|
|
|
583
|
-
|
|
584
|
-
// Check if this is a Rule (not AtRule) before accessing selector_list_id
|
|
585
|
-
if (grouping_enabled && rb_obj_is_kind_of(rule, cRule)) {
|
|
586
|
-
VALUE selector_list_id = rb_struct_aref(rule, INT2FIX(RULE_SELECTOR_LIST_ID));
|
|
587
|
-
if (!NIL_P(selector_list_id)) {
|
|
588
|
-
VALUE rule_ids_in_list = rb_hash_aref(selector_lists, selector_list_id);
|
|
589
|
-
|
|
590
|
-
if (NIL_P(rule_ids_in_list) || RARRAY_LEN(rule_ids_in_list) <= 1) {
|
|
591
|
-
if (opts->decl_indent_media) {
|
|
592
|
-
serialize_rule_formatted(result, rule, opts->media_indent, 1);
|
|
593
|
-
} else {
|
|
594
|
-
serialize_rule(result, rule);
|
|
595
|
-
}
|
|
596
|
-
rb_hash_aset(processed_rule_ids, rule_id, Qtrue);
|
|
597
|
-
} else {
|
|
598
|
-
VALUE matching_selectors = rb_ary_new();
|
|
599
|
-
VALUE rule_declarations = rb_struct_aref(rule, INT2FIX(RULE_DECLARATIONS));
|
|
600
|
-
|
|
601
|
-
long list_len = RARRAY_LEN(rule_ids_in_list);
|
|
602
|
-
for (long j = 0; j < list_len; j++) {
|
|
603
|
-
VALUE other_rule_id = rb_ary_entry(rule_ids_in_list, j);
|
|
604
|
-
if (RTEST(rb_hash_aref(processed_rule_ids, other_rule_id))) continue;
|
|
605
|
-
|
|
606
|
-
VALUE other_rule = rb_ary_entry(rules_array, FIX2INT(other_rule_id));
|
|
607
|
-
if (NIL_P(other_rule)) continue;
|
|
608
|
-
|
|
609
|
-
VALUE other_rule_media_query_id = rb_struct_aref(other_rule, INT2FIX(RULE_MEDIA_QUERY_ID));
|
|
610
|
-
if (!rb_equal(rule_media_query_id, other_rule_media_query_id)) continue;
|
|
611
|
-
|
|
612
|
-
VALUE other_declarations = rb_struct_aref(other_rule, INT2FIX(RULE_DECLARATIONS));
|
|
613
|
-
if (rb_equal(rule_declarations, other_declarations)) {
|
|
614
|
-
VALUE other_selector = rb_struct_aref(other_rule, INT2FIX(RULE_SELECTOR));
|
|
615
|
-
rb_ary_push(matching_selectors, other_selector);
|
|
616
|
-
rb_hash_aset(processed_rule_ids, other_rule_id, Qtrue);
|
|
617
|
-
}
|
|
618
|
-
}
|
|
619
|
-
|
|
620
|
-
if (RARRAY_LEN(matching_selectors) > 1) {
|
|
621
|
-
VALUE selector_str = rb_ary_join(matching_selectors, rb_str_new_cstr(", "));
|
|
622
|
-
rb_str_cat2(result, opts->media_indent);
|
|
623
|
-
rb_str_append(result, selector_str);
|
|
624
|
-
rb_str_cat2(result, opts->opening_brace);
|
|
625
|
-
if (opts->decl_indent_media) {
|
|
626
|
-
serialize_declarations_formatted(result, rule_declarations, opts->decl_indent_media);
|
|
627
|
-
} else {
|
|
628
|
-
serialize_declarations(result, rule_declarations);
|
|
629
|
-
}
|
|
630
|
-
rb_str_cat2(result, opts->media_indent);
|
|
631
|
-
rb_str_cat2(result, opts->closing_brace);
|
|
632
|
-
RB_GC_GUARD(selector_str);
|
|
633
|
-
} else {
|
|
634
|
-
if (opts->decl_indent_media) {
|
|
635
|
-
serialize_rule_formatted(result, rule, opts->media_indent, 1);
|
|
636
|
-
} else {
|
|
637
|
-
serialize_rule(result, rule);
|
|
638
|
-
}
|
|
639
|
-
}
|
|
640
|
-
}
|
|
641
|
-
} else {
|
|
642
|
-
if (opts->decl_indent_media) {
|
|
643
|
-
serialize_rule_formatted(result, rule, opts->media_indent, 1);
|
|
644
|
-
} else {
|
|
645
|
-
serialize_rule(result, rule);
|
|
646
|
-
}
|
|
647
|
-
rb_hash_aset(processed_rule_ids, rule_id, Qtrue);
|
|
648
|
-
}
|
|
649
|
-
} else {
|
|
650
|
-
if (opts->decl_indent_media) {
|
|
651
|
-
serialize_rule_formatted(result, rule, opts->media_indent, 1);
|
|
652
|
-
} else {
|
|
653
|
-
serialize_rule(result, rule);
|
|
654
|
-
}
|
|
655
|
-
rb_hash_aset(processed_rule_ids, rule_id, Qtrue);
|
|
656
|
-
}
|
|
618
|
+
serialize_rule_in_group(result, rule, rule_id, rule_media_query_id, &group_ctx, 1);
|
|
657
619
|
}
|
|
658
620
|
}
|
|
659
621
|
|
|
@@ -668,33 +630,6 @@ static VALUE serialize_stylesheet_with_grouping(
|
|
|
668
630
|
return result;
|
|
669
631
|
}
|
|
670
632
|
|
|
671
|
-
// Original stylesheet serialization (no nesting support) - compact format
|
|
672
|
-
static VALUE stylesheet_to_s_without_nesting(VALUE rules_array, VALUE media_queries, VALUE media_query_lists, VALUE charset, VALUE selector_lists) {
|
|
673
|
-
Check_Type(rules_array, T_ARRAY);
|
|
674
|
-
Check_Type(media_queries, T_ARRAY);
|
|
675
|
-
|
|
676
|
-
VALUE result = rb_str_new_cstr("");
|
|
677
|
-
|
|
678
|
-
// Add charset if present
|
|
679
|
-
if (!NIL_P(charset)) {
|
|
680
|
-
rb_str_cat2(result, "@charset \"");
|
|
681
|
-
rb_str_append(result, charset);
|
|
682
|
-
rb_str_cat2(result, "\";\n");
|
|
683
|
-
}
|
|
684
|
-
|
|
685
|
-
// Compact formatting options
|
|
686
|
-
struct format_opts opts = {
|
|
687
|
-
.opening_brace = " { ",
|
|
688
|
-
.closing_brace = " }\n",
|
|
689
|
-
.media_indent = "",
|
|
690
|
-
.decl_indent_base = NULL,
|
|
691
|
-
.decl_indent_media = NULL,
|
|
692
|
-
.add_blank_lines = 0
|
|
693
|
-
};
|
|
694
|
-
|
|
695
|
-
return serialize_stylesheet_with_grouping(rules_array, media_queries, media_query_lists, result, selector_lists, &opts);
|
|
696
|
-
}
|
|
697
|
-
|
|
698
633
|
// Forward declarations
|
|
699
634
|
static void serialize_children_only(VALUE result, VALUE rules_array, long rule_idx,
|
|
700
635
|
VALUE parent_to_children, VALUE parent_selector,
|
|
@@ -766,7 +701,7 @@ static void serialize_children_only(VALUE result, VALUE rules_array, long rule_i
|
|
|
766
701
|
rb_str_cat2(child_indent, " ");
|
|
767
702
|
}
|
|
768
703
|
const char *child_indent_ptr = RSTRING_PTR(child_indent);
|
|
769
|
-
|
|
704
|
+
serialize_declarations(result, child_declarations, child_indent_ptr);
|
|
770
705
|
RB_GC_GUARD(child_indent);
|
|
771
706
|
}
|
|
772
707
|
|
|
@@ -787,7 +722,7 @@ static void serialize_children_only(VALUE result, VALUE rules_array, long rule_i
|
|
|
787
722
|
|
|
788
723
|
// Serialize child declarations
|
|
789
724
|
VALUE child_declarations = rb_struct_aref(child, INT2FIX(RULE_DECLARATIONS));
|
|
790
|
-
serialize_declarations(result, child_declarations);
|
|
725
|
+
serialize_declarations(result, child_declarations, NULL);
|
|
791
726
|
|
|
792
727
|
// Recursively serialize grandchildren
|
|
793
728
|
serialize_children_only(result, rules_array, child_idx, parent_to_children,
|
|
@@ -823,7 +758,7 @@ static void serialize_children_only(VALUE result, VALUE rules_array, long rule_i
|
|
|
823
758
|
rb_str_cat2(child_indent, " ");
|
|
824
759
|
}
|
|
825
760
|
const char *child_indent_ptr = RSTRING_PTR(child_indent);
|
|
826
|
-
|
|
761
|
+
serialize_declarations(result, child_declarations, child_indent_ptr);
|
|
827
762
|
RB_GC_GUARD(child_indent);
|
|
828
763
|
}
|
|
829
764
|
|
|
@@ -835,7 +770,7 @@ static void serialize_children_only(VALUE result, VALUE rules_array, long rule_i
|
|
|
835
770
|
rb_str_cat2(result, " { ");
|
|
836
771
|
|
|
837
772
|
VALUE child_declarations = rb_struct_aref(child, INT2FIX(RULE_DECLARATIONS));
|
|
838
|
-
serialize_declarations(result, child_declarations);
|
|
773
|
+
serialize_declarations(result, child_declarations, NULL);
|
|
839
774
|
|
|
840
775
|
rb_str_cat2(result, " }");
|
|
841
776
|
}
|
|
@@ -876,7 +811,7 @@ static void serialize_rule_with_children(VALUE result, VALUE rules_array, long r
|
|
|
876
811
|
if (!NIL_P(declarations) && RARRAY_LEN(declarations) > 0) {
|
|
877
812
|
DEBUG_PRINTF("[SERIALIZE_RULE] Serializing %ld declarations with indent='%s' (%d spaces)\n",
|
|
878
813
|
RARRAY_LEN(declarations), decl_indent, decl_spaces);
|
|
879
|
-
|
|
814
|
+
serialize_declarations(result, declarations, decl_indent);
|
|
880
815
|
}
|
|
881
816
|
|
|
882
817
|
// Serialize nested children
|
|
@@ -891,7 +826,7 @@ static void serialize_rule_with_children(VALUE result, VALUE rules_array, long r
|
|
|
891
826
|
rb_str_cat2(result, " { ");
|
|
892
827
|
|
|
893
828
|
// Serialize own declarations
|
|
894
|
-
serialize_declarations(result, declarations);
|
|
829
|
+
serialize_declarations(result, declarations, NULL);
|
|
895
830
|
|
|
896
831
|
// Serialize nested children
|
|
897
832
|
serialize_children_only(result, rules_array, rule_idx, parent_to_children,
|
|
@@ -904,50 +839,25 @@ static void serialize_rule_with_children(VALUE result, VALUE rules_array, long r
|
|
|
904
839
|
RB_GC_GUARD(rule);
|
|
905
840
|
}
|
|
906
841
|
|
|
907
|
-
//
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
DEBUG_PRINTF("[STYLESHEET_TO_S] About to Check_Type\n");
|
|
919
|
-
Check_Type(rules_array, T_ARRAY);
|
|
920
|
-
Check_Type(media_queries, T_ARRAY);
|
|
921
|
-
if (!NIL_P(media_query_lists)) Check_Type(media_query_lists, T_HASH);
|
|
922
|
-
if (!NIL_P(selector_lists)) Check_Type(selector_lists, T_HASH);
|
|
923
|
-
DEBUG_PRINTF("[STYLESHEET_TO_S] Check_Type passed\n");
|
|
924
|
-
// TODO: Phase 2 - use selector_lists for grouping
|
|
925
|
-
(void)selector_lists; // Suppress unused parameter warning
|
|
926
|
-
|
|
927
|
-
// Fast path: if no nesting, use original implementation (zero overhead)
|
|
928
|
-
if (!RTEST(has_nesting)) {
|
|
929
|
-
DEBUG_PRINTF("[STYLESHEET_TO_S] Taking fast path (no nesting)\n");
|
|
930
|
-
return stylesheet_to_s_without_nesting(rules_array, media_queries, media_query_lists, charset, selector_lists);
|
|
931
|
-
}
|
|
932
|
-
|
|
933
|
-
DEBUG_PRINTF("[STYLESHEET_TO_S] Taking slow path (has nesting)\n");
|
|
934
|
-
// SLOW PATH: Has nesting - use lookahead approach
|
|
842
|
+
// Shared implementation for stylesheet serialization with nesting support.
|
|
843
|
+
// Handles both compact (to_s) and formatted (to_formatted_s) output via the
|
|
844
|
+
// format_opts struct, mirroring the pattern serialize_stylesheet_with_grouping
|
|
845
|
+
// uses for the no-nesting fast path.
|
|
846
|
+
static VALUE serialize_stylesheet_with_nesting(
|
|
847
|
+
VALUE rules_array,
|
|
848
|
+
VALUE media_queries,
|
|
849
|
+
VALUE result,
|
|
850
|
+
const struct format_opts *opts
|
|
851
|
+
) {
|
|
852
|
+
int formatted = (opts->decl_indent_base != NULL);
|
|
935
853
|
long total_rules = RARRAY_LEN(rules_array);
|
|
936
|
-
VALUE result = rb_str_new_cstr("");
|
|
937
|
-
|
|
938
|
-
// Add charset if present
|
|
939
|
-
if (!NIL_P(charset)) {
|
|
940
|
-
rb_str_cat2(result, "@charset \"");
|
|
941
|
-
rb_str_append(result, charset);
|
|
942
|
-
rb_str_cat2(result, "\";\n");
|
|
943
|
-
}
|
|
944
854
|
|
|
945
855
|
// Build parent_to_children map (parent_rule_id -> array of child indices)
|
|
946
856
|
// This allows O(1) lookup of children when serializing each parent
|
|
947
857
|
VALUE parent_to_children = rb_hash_new();
|
|
948
858
|
for (long i = 0; i < total_rules; i++) {
|
|
949
859
|
VALUE rule = rb_ary_entry(rules_array, i);
|
|
950
|
-
VALUE parent_id =
|
|
860
|
+
VALUE parent_id = rule_parent_id(rule);
|
|
951
861
|
|
|
952
862
|
if (!NIL_P(parent_id)) {
|
|
953
863
|
DEBUG_PRINTF("[MAP] Rule %ld has parent_id=%s, adding to map\n", i,
|
|
@@ -973,7 +883,7 @@ static VALUE stylesheet_to_s(VALUE self, VALUE rules_array, VALUE charset, VALUE
|
|
|
973
883
|
DEBUG_PRINTF("[SERIALIZE] Starting serialization, total_rules=%ld\n", total_rules);
|
|
974
884
|
for (long i = 0; i < total_rules; i++) {
|
|
975
885
|
VALUE rule = rb_ary_entry(rules_array, i);
|
|
976
|
-
VALUE parent_id =
|
|
886
|
+
VALUE parent_id = rule_parent_id(rule);
|
|
977
887
|
|
|
978
888
|
DEBUG_PRINTF("[SERIALIZE] Rule %ld: selector=%s, parent_id=%s\n", i,
|
|
979
889
|
RSTRING_PTR(rb_struct_aref(rule, INT2FIX(RULE_SELECTOR))),
|
|
@@ -986,7 +896,7 @@ static VALUE stylesheet_to_s(VALUE self, VALUE rules_array, VALUE charset, VALUE
|
|
|
986
896
|
}
|
|
987
897
|
|
|
988
898
|
// Get media_query_id for this rule and fetch the MediaQuery object
|
|
989
|
-
VALUE rule_media_query_id =
|
|
899
|
+
VALUE rule_media_query_id = rule_media_query_id_of(rule);
|
|
990
900
|
VALUE rule_media = Qnil;
|
|
991
901
|
if (!NIL_P(rule_media_query_id)) {
|
|
992
902
|
rule_media = rb_ary_entry(media_queries, FIX2INT(rule_media_query_id));
|
|
@@ -1002,6 +912,10 @@ static VALUE stylesheet_to_s(VALUE self, VALUE rules_array, VALUE charset, VALUE
|
|
|
1002
912
|
rb_str_cat2(result, "}\n");
|
|
1003
913
|
in_media_block = 0;
|
|
1004
914
|
current_media = Qnil;
|
|
915
|
+
|
|
916
|
+
if (opts->add_blank_lines) {
|
|
917
|
+
rb_str_cat2(result, "\n");
|
|
918
|
+
}
|
|
1005
919
|
}
|
|
1006
920
|
} else {
|
|
1007
921
|
// In media - check if we need to open/change block (compare MediaQuery objects by value)
|
|
@@ -1009,6 +923,9 @@ static VALUE stylesheet_to_s(VALUE self, VALUE rules_array, VALUE charset, VALUE
|
|
|
1009
923
|
// Close previous media block if open
|
|
1010
924
|
if (in_media_block) {
|
|
1011
925
|
rb_str_cat2(result, "}\n");
|
|
926
|
+
} else if (opts->add_blank_lines && RSTRING_LEN(result) > 0) {
|
|
927
|
+
// Add blank line before new media block (except at start)
|
|
928
|
+
rb_str_cat2(result, "\n");
|
|
1012
929
|
}
|
|
1013
930
|
// Open new media block - store the MediaQuery object for comparison
|
|
1014
931
|
current_media = rule_media;
|
|
@@ -1025,11 +942,18 @@ static VALUE stylesheet_to_s(VALUE self, VALUE rules_array, VALUE charset, VALUE
|
|
|
1025
942
|
continue;
|
|
1026
943
|
}
|
|
1027
944
|
|
|
945
|
+
// Add indent if inside media block (no-op in compact mode, where media_indent is "")
|
|
946
|
+
if (in_media_block) {
|
|
947
|
+
DEBUG_PRINTF("[FORMATTED] Adding base indent for media block\n");
|
|
948
|
+
rb_str_cat2(result, opts->media_indent);
|
|
949
|
+
}
|
|
950
|
+
|
|
1028
951
|
// Serialize rule with nested children
|
|
952
|
+
DEBUG_PRINTF("[FORMATTED] Calling serialize_rule_with_children, in_media_block=%d\n", in_media_block);
|
|
1029
953
|
serialize_rule_with_children(
|
|
1030
954
|
result, rules_array, i, parent_to_children, media_queries,
|
|
1031
|
-
|
|
1032
|
-
0 // indent_level (
|
|
955
|
+
formatted,
|
|
956
|
+
in_media_block ? 1 : 0 // indent_level (unused when !formatted)
|
|
1033
957
|
);
|
|
1034
958
|
}
|
|
1035
959
|
|
|
@@ -1042,47 +966,16 @@ static VALUE stylesheet_to_s(VALUE self, VALUE rules_array, VALUE charset, VALUE
|
|
|
1042
966
|
return result;
|
|
1043
967
|
}
|
|
1044
968
|
|
|
1045
|
-
//
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
VALUE result = rb_str_new_cstr("");
|
|
1051
|
-
|
|
1052
|
-
// Add charset if present
|
|
1053
|
-
if (!NIL_P(charset)) {
|
|
1054
|
-
rb_str_cat2(result, "@charset \"");
|
|
1055
|
-
rb_str_append(result, charset);
|
|
1056
|
-
rb_str_cat2(result, "\";\n");
|
|
1057
|
-
}
|
|
1058
|
-
|
|
1059
|
-
// Formatted output options
|
|
1060
|
-
struct format_opts opts = {
|
|
1061
|
-
.opening_brace = " {\n",
|
|
1062
|
-
.closing_brace = "}\n",
|
|
1063
|
-
.media_indent = " ",
|
|
1064
|
-
.decl_indent_base = " ",
|
|
1065
|
-
.decl_indent_media = " ",
|
|
1066
|
-
.add_blank_lines = 1
|
|
1067
|
-
};
|
|
1068
|
-
|
|
1069
|
-
return serialize_stylesheet_with_grouping(rules_array, media_queries, media_query_lists, result, selector_lists, &opts);
|
|
1070
|
-
}
|
|
1071
|
-
|
|
1072
|
-
// Formatted version with indentation and newlines (with nesting support)
|
|
1073
|
-
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) {
|
|
969
|
+
// Shared implementation for stylesheet_to_s/stylesheet_to_formatted_s - both wrap
|
|
970
|
+
// this, differing only in which format_opts they select. Dispatches on has_nesting
|
|
971
|
+
// to pick the selector-list-grouping serializer (no nesting - zero overhead) or the
|
|
972
|
+
// 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) {
|
|
1074
974
|
Check_Type(rules_array, T_ARRAY);
|
|
1075
975
|
Check_Type(media_queries, T_ARRAY);
|
|
1076
976
|
if (!NIL_P(media_query_lists)) Check_Type(media_query_lists, T_HASH);
|
|
1077
977
|
if (!NIL_P(selector_lists)) Check_Type(selector_lists, T_HASH);
|
|
1078
978
|
|
|
1079
|
-
// Fast path: if no nesting, use original implementation (zero overhead)
|
|
1080
|
-
if (!RTEST(has_nesting)) {
|
|
1081
|
-
return stylesheet_to_formatted_s_without_nesting(rules_array, media_queries, media_query_lists, charset, selector_lists);
|
|
1082
|
-
}
|
|
1083
|
-
|
|
1084
|
-
// SLOW PATH: Has nesting - use parameterized serialization with formatted=1
|
|
1085
|
-
long total_rules = RARRAY_LEN(rules_array);
|
|
1086
979
|
VALUE result = rb_str_new_cstr("");
|
|
1087
980
|
|
|
1088
981
|
// Add charset if present
|
|
@@ -1092,101 +985,45 @@ static VALUE stylesheet_to_formatted_s(VALUE self, VALUE rules_array, VALUE char
|
|
|
1092
985
|
rb_str_cat2(result, "\";\n");
|
|
1093
986
|
}
|
|
1094
987
|
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
988
|
+
struct format_opts opts;
|
|
989
|
+
if (formatted) {
|
|
990
|
+
opts = (struct format_opts){
|
|
991
|
+
.opening_brace = " {\n",
|
|
992
|
+
.closing_brace = "}\n",
|
|
993
|
+
.media_indent = " ",
|
|
994
|
+
.decl_indent_base = " ",
|
|
995
|
+
.decl_indent_media = " ",
|
|
996
|
+
.add_blank_lines = 1
|
|
997
|
+
};
|
|
998
|
+
} else {
|
|
999
|
+
opts = (struct format_opts){
|
|
1000
|
+
.opening_brace = " { ",
|
|
1001
|
+
.closing_brace = " }\n",
|
|
1002
|
+
.media_indent = "",
|
|
1003
|
+
.decl_indent_base = NULL,
|
|
1004
|
+
.decl_indent_media = NULL,
|
|
1005
|
+
.add_blank_lines = 0
|
|
1006
|
+
};
|
|
1109
1007
|
}
|
|
1110
1008
|
|
|
1111
|
-
//
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
// Serialize only top-level rules (parent_rule_id == nil)
|
|
1116
|
-
for (long i = 0; i < total_rules; i++) {
|
|
1117
|
-
VALUE rule = rb_ary_entry(rules_array, i);
|
|
1118
|
-
VALUE parent_id = rb_struct_aref(rule, INT2FIX(RULE_PARENT_RULE_ID));
|
|
1119
|
-
|
|
1120
|
-
// Skip child rules - they're serialized when we hit their parent
|
|
1121
|
-
if (!NIL_P(parent_id)) {
|
|
1122
|
-
continue;
|
|
1123
|
-
}
|
|
1124
|
-
|
|
1125
|
-
// Get media_query_id for this rule and fetch the MediaQuery object
|
|
1126
|
-
VALUE rule_media_query_id = rb_obj_is_kind_of(rule, cAtRule) ? Qnil : rb_struct_aref(rule, INT2FIX(RULE_MEDIA_QUERY_ID));
|
|
1127
|
-
VALUE rule_media = Qnil;
|
|
1128
|
-
if (!NIL_P(rule_media_query_id)) {
|
|
1129
|
-
rule_media = rb_ary_entry(media_queries, FIX2INT(rule_media_query_id));
|
|
1130
|
-
}
|
|
1131
|
-
|
|
1132
|
-
// Handle media block transitions
|
|
1133
|
-
if (NIL_P(rule_media)) {
|
|
1134
|
-
// Not in media - close any open media block
|
|
1135
|
-
if (in_media_block) {
|
|
1136
|
-
rb_str_cat2(result, "}\n");
|
|
1137
|
-
in_media_block = 0;
|
|
1138
|
-
current_media = Qnil;
|
|
1139
|
-
|
|
1140
|
-
// Add blank line after closing media block
|
|
1141
|
-
rb_str_cat2(result, "\n");
|
|
1142
|
-
}
|
|
1143
|
-
} else {
|
|
1144
|
-
// In media - check if we need to open/change block (compare MediaQuery objects by value)
|
|
1145
|
-
if (NIL_P(current_media) || !rb_equal(current_media, rule_media)) {
|
|
1146
|
-
// Close previous media block if open
|
|
1147
|
-
if (in_media_block) {
|
|
1148
|
-
rb_str_cat2(result, "}\n");
|
|
1149
|
-
} else if (RSTRING_LEN(result) > 0) {
|
|
1150
|
-
// Add blank line before new media block (except at start)
|
|
1151
|
-
rb_str_cat2(result, "\n");
|
|
1152
|
-
}
|
|
1153
|
-
// Open new media block - store the MediaQuery object for comparison
|
|
1154
|
-
current_media = rule_media;
|
|
1155
|
-
rb_str_cat2(result, "@media ");
|
|
1156
|
-
append_media_query_text(result, rule_media);
|
|
1157
|
-
rb_str_cat2(result, " {\n");
|
|
1158
|
-
in_media_block = 1;
|
|
1159
|
-
}
|
|
1160
|
-
}
|
|
1161
|
-
|
|
1162
|
-
// Check if this is an AtRule
|
|
1163
|
-
if (rb_obj_is_kind_of(rule, cAtRule)) {
|
|
1164
|
-
serialize_at_rule(result, rule);
|
|
1165
|
-
continue;
|
|
1166
|
-
}
|
|
1167
|
-
|
|
1168
|
-
// Add indent if inside media block
|
|
1169
|
-
if (in_media_block) {
|
|
1170
|
-
DEBUG_PRINTF("[FORMATTED] Adding base indent for media block\n");
|
|
1171
|
-
rb_str_cat2(result, " ");
|
|
1172
|
-
}
|
|
1173
|
-
|
|
1174
|
-
// Serialize rule with nested children
|
|
1175
|
-
DEBUG_PRINTF("[FORMATTED] Calling serialize_rule_with_children, in_media_block=%d\n", in_media_block);
|
|
1176
|
-
serialize_rule_with_children(
|
|
1177
|
-
result, rules_array, i, parent_to_children, media_queries,
|
|
1178
|
-
1, // formatted (with indentation)
|
|
1179
|
-
in_media_block ? 1 : 0 // indent_level (1 if inside media block, 0 otherwise)
|
|
1180
|
-
);
|
|
1009
|
+
// Fast path: if no nesting, use the selector-list-grouping serializer (zero overhead)
|
|
1010
|
+
if (!RTEST(has_nesting)) {
|
|
1011
|
+
return serialize_stylesheet_with_grouping(rules_array, media_queries, media_query_lists, result, selector_lists, &opts);
|
|
1181
1012
|
}
|
|
1182
1013
|
|
|
1183
|
-
//
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1014
|
+
// Has nesting - use the parent/child lookahead serializer
|
|
1015
|
+
// TODO: Phase 2 - use selector_lists for grouping on the nesting path too
|
|
1016
|
+
return serialize_stylesheet_with_nesting(rules_array, media_queries, result, &opts);
|
|
1017
|
+
}
|
|
1187
1018
|
|
|
1188
|
-
|
|
1189
|
-
|
|
1019
|
+
// 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);
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
// 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);
|
|
1190
1027
|
}
|
|
1191
1028
|
|
|
1192
1029
|
/*
|
|
@@ -1207,62 +1044,18 @@ static VALUE new_parse_declarations_string(const char *start, const char *end) {
|
|
|
1207
1044
|
while (pos < end && (IS_WHITESPACE(*pos) || *pos == ';')) pos++;
|
|
1208
1045
|
if (pos >= end) break;
|
|
1209
1046
|
|
|
1210
|
-
//
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
if (pos
|
|
1214
|
-
|
|
1215
|
-
const char *prop_end = pos;
|
|
1216
|
-
// Trim trailing whitespace
|
|
1217
|
-
while (prop_end > prop_start && IS_WHITESPACE(*(prop_end-1))) prop_end--;
|
|
1218
|
-
// Trim leading whitespace
|
|
1219
|
-
while (prop_start < prop_end && IS_WHITESPACE(*prop_start)) prop_start++;
|
|
1220
|
-
|
|
1221
|
-
pos++; // Skip colon
|
|
1222
|
-
// Trim leading whitespace
|
|
1223
|
-
while (pos < end && IS_WHITESPACE(*pos)) pos++;
|
|
1224
|
-
|
|
1225
|
-
// Find value (up to semicolon or end), handling parentheses
|
|
1226
|
-
const char *val_start = pos;
|
|
1227
|
-
int paren_depth = 0;
|
|
1228
|
-
while (pos < end) {
|
|
1229
|
-
if (*pos == '(') paren_depth++;
|
|
1230
|
-
else if (*pos == ')') paren_depth--;
|
|
1231
|
-
else if (*pos == ';' && paren_depth == 0) break;
|
|
1232
|
-
pos++;
|
|
1233
|
-
}
|
|
1234
|
-
const char *val_end = pos;
|
|
1235
|
-
// Trim trailing whitespace
|
|
1236
|
-
while (val_end > val_start && IS_WHITESPACE(*(val_end-1))) val_end--;
|
|
1237
|
-
|
|
1238
|
-
// Check for !important
|
|
1239
|
-
int is_important = 0;
|
|
1240
|
-
if (val_end - val_start >= 10) { // strlen("!important") = 10
|
|
1241
|
-
const char *check = val_end - 10;
|
|
1242
|
-
while (check < val_end && IS_WHITESPACE(*check)) check++;
|
|
1243
|
-
if (check < val_end && *check == '!') {
|
|
1244
|
-
check++;
|
|
1245
|
-
while (check < val_end && IS_WHITESPACE(*check)) check++;
|
|
1246
|
-
if ((val_end - check) >= 9 && strncmp(check, "important", 9) == 0) {
|
|
1247
|
-
is_important = 1;
|
|
1248
|
-
const char *important_pos = check - 1;
|
|
1249
|
-
while (important_pos > val_start && (IS_WHITESPACE(*(important_pos-1)) || *(important_pos-1) == '!')) {
|
|
1250
|
-
important_pos--;
|
|
1251
|
-
}
|
|
1252
|
-
val_end = important_pos;
|
|
1253
|
-
// Trim trailing whitespace again
|
|
1254
|
-
while (val_end > val_start && IS_WHITESPACE(*(val_end-1))) val_end--;
|
|
1255
|
-
}
|
|
1256
|
-
}
|
|
1257
|
-
}
|
|
1047
|
+
// Standalone declaration-list input never contains braces, so only
|
|
1048
|
+
// ':' terminates the property scan (stop_prop_scan_early=0).
|
|
1049
|
+
struct declaration_span span;
|
|
1050
|
+
if (!parse_one_declaration(&pos, end, 0, &span)) break; // No colon found
|
|
1258
1051
|
|
|
1259
1052
|
// Skip if value is empty
|
|
1260
|
-
if (val_end > val_start) {
|
|
1261
|
-
long prop_len = prop_end - prop_start;
|
|
1262
|
-
long val_len = val_end - val_start;
|
|
1053
|
+
if (span.val_end > span.val_start) {
|
|
1054
|
+
long prop_len = span.prop_end - span.prop_start;
|
|
1055
|
+
long val_len = span.val_end - span.val_start;
|
|
1263
1056
|
|
|
1264
1057
|
// Create property string (US-ASCII, lowercased)
|
|
1265
|
-
VALUE property = rb_usascii_str_new(prop_start, prop_len);
|
|
1058
|
+
VALUE property = rb_usascii_str_new(span.prop_start, prop_len);
|
|
1266
1059
|
// Lowercase it inline
|
|
1267
1060
|
char *prop_ptr = RSTRING_PTR(property);
|
|
1268
1061
|
for (long i = 0; i < prop_len; i++) {
|
|
@@ -1271,11 +1064,11 @@ static VALUE new_parse_declarations_string(const char *start, const char *end) {
|
|
|
1271
1064
|
}
|
|
1272
1065
|
}
|
|
1273
1066
|
|
|
1274
|
-
VALUE value = rb_utf8_str_new(val_start, val_len);
|
|
1067
|
+
VALUE value = rb_utf8_str_new(span.val_start, val_len);
|
|
1275
1068
|
|
|
1276
1069
|
// Create Declaration struct
|
|
1277
1070
|
VALUE decl = rb_struct_new(cDeclaration,
|
|
1278
|
-
property, value, is_important ? Qtrue : Qfalse);
|
|
1071
|
+
property, value, span.is_important ? Qtrue : Qfalse);
|
|
1279
1072
|
|
|
1280
1073
|
rb_ary_push(declarations, decl);
|
|
1281
1074
|
}
|
|
@@ -1408,10 +1201,10 @@ void Init_native_extension(void) {
|
|
|
1408
1201
|
eSizeError = rb_define_class_under(mCataract, "SizeError", eCataractError);
|
|
1409
1202
|
}
|
|
1410
1203
|
|
|
1411
|
-
if (rb_const_defined(mCataract, rb_intern("
|
|
1412
|
-
|
|
1204
|
+
if (rb_const_defined(mCataract, rb_intern("ParseError"))) {
|
|
1205
|
+
eParseError = rb_const_get(mCataract, rb_intern("ParseError"));
|
|
1413
1206
|
} else {
|
|
1414
|
-
|
|
1207
|
+
eParseError = rb_define_class_under(mCataract, "ParseError", eCataractError);
|
|
1415
1208
|
}
|
|
1416
1209
|
|
|
1417
1210
|
// Reuse Ruby-defined structs (they must be defined before loading this extension)
|