cataract 0.1.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 +7 -0
- data/.clang-tidy +30 -0
- data/.github/workflows/ci-macos.yml +12 -0
- data/.github/workflows/ci.yml +77 -0
- data/.github/workflows/test.yml +76 -0
- data/.gitignore +45 -0
- data/.overcommit.yml +38 -0
- data/.rubocop.yml +83 -0
- data/BENCHMARKS.md +201 -0
- data/CHANGELOG.md +1 -0
- data/Gemfile +27 -0
- data/LICENSE +21 -0
- data/RAGEL_MIGRATION.md +60 -0
- data/README.md +292 -0
- data/Rakefile +209 -0
- data/benchmarks/benchmark_harness.rb +193 -0
- data/benchmarks/benchmark_merging.rb +121 -0
- data/benchmarks/benchmark_optimization_comparison.rb +168 -0
- data/benchmarks/benchmark_parsing.rb +153 -0
- data/benchmarks/benchmark_ragel_removal.rb +56 -0
- data/benchmarks/benchmark_runner.rb +70 -0
- data/benchmarks/benchmark_serialization.rb +180 -0
- data/benchmarks/benchmark_shorthand.rb +109 -0
- data/benchmarks/benchmark_shorthand_expansion.rb +176 -0
- data/benchmarks/benchmark_specificity.rb +124 -0
- data/benchmarks/benchmark_string_allocation.rb +151 -0
- data/benchmarks/benchmark_stylesheet_to_s.rb +62 -0
- data/benchmarks/benchmark_to_s_cached.rb +55 -0
- data/benchmarks/benchmark_value_splitter.rb +54 -0
- data/benchmarks/benchmark_yjit.rb +158 -0
- data/benchmarks/benchmark_yjit_workers.rb +61 -0
- data/benchmarks/profile_to_s.rb +23 -0
- data/benchmarks/speedup_calculator.rb +83 -0
- data/benchmarks/system_metadata.rb +81 -0
- data/benchmarks/templates/benchmarks.md.erb +221 -0
- data/benchmarks/yjit_tests.rb +141 -0
- data/cataract.gemspec +34 -0
- data/cliff.toml +92 -0
- data/examples/color_conversion_visual_test/color_conversion_test.html +3603 -0
- data/examples/color_conversion_visual_test/generate.rb +202 -0
- data/examples/color_conversion_visual_test/template.html.erb +259 -0
- data/examples/css_analyzer/analyzer.rb +164 -0
- data/examples/css_analyzer/analyzers/base.rb +33 -0
- data/examples/css_analyzer/analyzers/colors.rb +133 -0
- data/examples/css_analyzer/analyzers/important.rb +88 -0
- data/examples/css_analyzer/analyzers/properties.rb +61 -0
- data/examples/css_analyzer/analyzers/specificity.rb +68 -0
- data/examples/css_analyzer/templates/report.html.erb +575 -0
- data/examples/css_analyzer.rb +69 -0
- data/examples/github_analysis.html +5343 -0
- data/ext/cataract/cataract.c +1086 -0
- data/ext/cataract/cataract.h +174 -0
- data/ext/cataract/css_parser.c +1435 -0
- data/ext/cataract/extconf.rb +48 -0
- data/ext/cataract/import_scanner.c +174 -0
- data/ext/cataract/merge.c +973 -0
- data/ext/cataract/shorthand_expander.c +902 -0
- data/ext/cataract/specificity.c +213 -0
- data/ext/cataract/value_splitter.c +116 -0
- data/ext/cataract_color/cataract_color.c +16 -0
- data/ext/cataract_color/color_conversion.c +1687 -0
- data/ext/cataract_color/color_conversion.h +136 -0
- data/ext/cataract_color/color_conversion_lab.c +571 -0
- data/ext/cataract_color/color_conversion_named.c +259 -0
- data/ext/cataract_color/color_conversion_oklab.c +547 -0
- data/ext/cataract_color/extconf.rb +23 -0
- data/ext/cataract_old/cataract.c +393 -0
- data/ext/cataract_old/cataract.h +250 -0
- data/ext/cataract_old/css_parser.c +933 -0
- data/ext/cataract_old/extconf.rb +67 -0
- data/ext/cataract_old/import_scanner.c +174 -0
- data/ext/cataract_old/merge.c +776 -0
- data/ext/cataract_old/shorthand_expander.c +902 -0
- data/ext/cataract_old/specificity.c +213 -0
- data/ext/cataract_old/stylesheet.c +290 -0
- data/ext/cataract_old/value_splitter.c +116 -0
- data/lib/cataract/at_rule.rb +97 -0
- data/lib/cataract/color_conversion.rb +18 -0
- data/lib/cataract/declarations.rb +332 -0
- data/lib/cataract/import_resolver.rb +210 -0
- data/lib/cataract/rule.rb +131 -0
- data/lib/cataract/stylesheet.rb +716 -0
- data/lib/cataract/stylesheet_scope.rb +257 -0
- data/lib/cataract/version.rb +5 -0
- data/lib/cataract.rb +107 -0
- data/lib/tasks/gem.rake +158 -0
- data/scripts/fuzzer/run.rb +828 -0
- data/scripts/fuzzer/worker.rb +99 -0
- data/scripts/generate_benchmarks_md.rb +155 -0
- metadata +135 -0
|
@@ -0,0 +1,1086 @@
|
|
|
1
|
+
#include <ruby.h>
|
|
2
|
+
#include <stdio.h>
|
|
3
|
+
#include "cataract.h"
|
|
4
|
+
|
|
5
|
+
// Global struct class definitions
|
|
6
|
+
VALUE cRule;
|
|
7
|
+
VALUE cDeclaration;
|
|
8
|
+
VALUE cAtRule;
|
|
9
|
+
VALUE cStylesheet;
|
|
10
|
+
|
|
11
|
+
// Error class definitions (shared with main extension)
|
|
12
|
+
VALUE eCataractError;
|
|
13
|
+
VALUE eDepthError;
|
|
14
|
+
VALUE eSizeError;
|
|
15
|
+
|
|
16
|
+
// ============================================================================
|
|
17
|
+
// Stubbed Implementation - Phase 1
|
|
18
|
+
// ============================================================================
|
|
19
|
+
|
|
20
|
+
/*
|
|
21
|
+
* Parse CSS string into Rule structs
|
|
22
|
+
* Manages @_last_rule_id, @rules, @media_index, and @charset ivars on stylesheet_obj
|
|
23
|
+
*
|
|
24
|
+
* @param module [Module] Cataract module (unused, required for module function)
|
|
25
|
+
* @param stylesheet_obj [Stylesheet] The stylesheet instance
|
|
26
|
+
* @param css_string [String] CSS string to parse
|
|
27
|
+
* @return [VALUE] stylesheet_obj (for method chaining)
|
|
28
|
+
*/
|
|
29
|
+
/*
|
|
30
|
+
* Parse CSS and return hash with parsed data
|
|
31
|
+
* This matches the old parse_css API
|
|
32
|
+
*
|
|
33
|
+
* @param css_string [String] CSS to parse
|
|
34
|
+
* @return [Hash] { rules: [...], media_index: {...}, charset: "..." }
|
|
35
|
+
*/
|
|
36
|
+
VALUE parse_css_new(VALUE self, VALUE css_string) {
|
|
37
|
+
return parse_css_new_impl(css_string, 0);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/*
|
|
41
|
+
* Serialize rules array to CSS string
|
|
42
|
+
* Note: Media query grouping now handled in Ruby layer using @media_index
|
|
43
|
+
*
|
|
44
|
+
* @param rules_array [Array<Rule>] Flat array of rules in insertion order
|
|
45
|
+
* @param charset [String, nil] Optional @charset value
|
|
46
|
+
* @return [String] CSS string
|
|
47
|
+
*/
|
|
48
|
+
// Helper to serialize a single rule's declarations
|
|
49
|
+
static void serialize_declarations(VALUE result, VALUE declarations) {
|
|
50
|
+
long decl_len = RARRAY_LEN(declarations);
|
|
51
|
+
for (long j = 0; j < decl_len; j++) {
|
|
52
|
+
VALUE decl = rb_ary_entry(declarations, j);
|
|
53
|
+
VALUE property = rb_struct_aref(decl, INT2FIX(DECL_PROPERTY));
|
|
54
|
+
VALUE value = rb_struct_aref(decl, INT2FIX(DECL_VALUE));
|
|
55
|
+
VALUE important = rb_struct_aref(decl, INT2FIX(DECL_IMPORTANT));
|
|
56
|
+
|
|
57
|
+
rb_str_append(result, property);
|
|
58
|
+
rb_str_cat2(result, ": ");
|
|
59
|
+
rb_str_append(result, value);
|
|
60
|
+
|
|
61
|
+
if (RTEST(important)) {
|
|
62
|
+
rb_str_cat2(result, " !important");
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
rb_str_cat2(result, ";");
|
|
66
|
+
|
|
67
|
+
// Add space after semicolon except for last declaration
|
|
68
|
+
if (j < decl_len - 1) {
|
|
69
|
+
rb_str_cat2(result, " ");
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Formatted version - each declaration on its own line with indentation
|
|
75
|
+
static void serialize_declarations_formatted(VALUE result, VALUE declarations, const char *indent) {
|
|
76
|
+
long decl_len = RARRAY_LEN(declarations);
|
|
77
|
+
for (long j = 0; j < decl_len; j++) {
|
|
78
|
+
VALUE decl = rb_ary_entry(declarations, j);
|
|
79
|
+
VALUE property = rb_struct_aref(decl, INT2FIX(DECL_PROPERTY));
|
|
80
|
+
VALUE value = rb_struct_aref(decl, INT2FIX(DECL_VALUE));
|
|
81
|
+
VALUE important = rb_struct_aref(decl, INT2FIX(DECL_IMPORTANT));
|
|
82
|
+
|
|
83
|
+
rb_str_cat2(result, indent);
|
|
84
|
+
rb_str_append(result, property);
|
|
85
|
+
rb_str_cat2(result, ": ");
|
|
86
|
+
rb_str_append(result, value);
|
|
87
|
+
|
|
88
|
+
if (RTEST(important)) {
|
|
89
|
+
rb_str_cat2(result, " !important");
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
rb_str_cat2(result, ";\n");
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Helper to serialize an AtRule (@keyframes, @font-face, etc)
|
|
97
|
+
static void serialize_at_rule(VALUE result, VALUE at_rule) {
|
|
98
|
+
VALUE selector = rb_struct_aref(at_rule, INT2FIX(AT_RULE_SELECTOR));
|
|
99
|
+
VALUE content = rb_struct_aref(at_rule, INT2FIX(AT_RULE_CONTENT));
|
|
100
|
+
|
|
101
|
+
rb_str_append(result, selector);
|
|
102
|
+
rb_str_cat2(result, " {\n");
|
|
103
|
+
|
|
104
|
+
// Check if content is rules or declarations
|
|
105
|
+
if (RARRAY_LEN(content) > 0) {
|
|
106
|
+
VALUE first = rb_ary_entry(content, 0);
|
|
107
|
+
|
|
108
|
+
if (rb_obj_is_kind_of(first, cRule)) {
|
|
109
|
+
// Serialize as nested rules (e.g., @keyframes)
|
|
110
|
+
for (long i = 0; i < RARRAY_LEN(content); i++) {
|
|
111
|
+
VALUE nested_rule = rb_ary_entry(content, i);
|
|
112
|
+
VALUE nested_selector = rb_struct_aref(nested_rule, INT2FIX(RULE_SELECTOR));
|
|
113
|
+
VALUE nested_declarations = rb_struct_aref(nested_rule, INT2FIX(RULE_DECLARATIONS));
|
|
114
|
+
|
|
115
|
+
rb_str_cat2(result, " ");
|
|
116
|
+
rb_str_append(result, nested_selector);
|
|
117
|
+
rb_str_cat2(result, " { ");
|
|
118
|
+
serialize_declarations(result, nested_declarations);
|
|
119
|
+
rb_str_cat2(result, " }\n");
|
|
120
|
+
}
|
|
121
|
+
} else {
|
|
122
|
+
// Serialize as declarations (e.g., @font-face)
|
|
123
|
+
rb_str_cat2(result, " ");
|
|
124
|
+
serialize_declarations(result, content);
|
|
125
|
+
rb_str_cat2(result, "\n");
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
rb_str_cat2(result, "}\n");
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Helper to "unresolve" a child selector back to its nested form
|
|
133
|
+
// Input: parent_selector=".button", child_selector=".button:hover", nesting_style=EXPLICIT
|
|
134
|
+
// Output: "&:hover"
|
|
135
|
+
// Input: parent_selector=".parent", child_selector=".parent .child", nesting_style=IMPLICIT
|
|
136
|
+
// Output: ".child"
|
|
137
|
+
static VALUE unresolve_selector(VALUE parent_selector, VALUE child_selector, VALUE nesting_style) {
|
|
138
|
+
const char *parent = RSTRING_PTR(parent_selector);
|
|
139
|
+
long parent_len = RSTRING_LEN(parent_selector);
|
|
140
|
+
const char *child = RSTRING_PTR(child_selector);
|
|
141
|
+
long child_len = RSTRING_LEN(child_selector);
|
|
142
|
+
|
|
143
|
+
int style = NIL_P(nesting_style) ? NESTING_STYLE_IMPLICIT : FIX2INT(nesting_style);
|
|
144
|
+
|
|
145
|
+
VALUE result;
|
|
146
|
+
|
|
147
|
+
if (style == NESTING_STYLE_EXPLICIT) {
|
|
148
|
+
// Explicit nesting: replace parent with &
|
|
149
|
+
// ".button:hover" -> "&:hover"
|
|
150
|
+
// ".button.primary" -> "&.primary"
|
|
151
|
+
|
|
152
|
+
// Find where parent ends in child
|
|
153
|
+
if (strncmp(child, parent, parent_len) == 0) {
|
|
154
|
+
// Parent matches at start - replace with &
|
|
155
|
+
result = rb_str_new_cstr("&");
|
|
156
|
+
rb_str_cat(result, child + parent_len, child_len - parent_len);
|
|
157
|
+
} else {
|
|
158
|
+
// Fallback: just return child (shouldn't happen)
|
|
159
|
+
result = child_selector;
|
|
160
|
+
}
|
|
161
|
+
} else {
|
|
162
|
+
// Implicit nesting: strip parent + space from beginning
|
|
163
|
+
// ".parent .child" -> ".child"
|
|
164
|
+
|
|
165
|
+
if (strncmp(child, parent, parent_len) == 0) {
|
|
166
|
+
// Check if followed by space
|
|
167
|
+
if (child_len > parent_len && child[parent_len] == ' ') {
|
|
168
|
+
// Strip "parent " prefix
|
|
169
|
+
result = rb_str_new(child + parent_len + 1, child_len - parent_len - 1);
|
|
170
|
+
} else {
|
|
171
|
+
// Fallback: return child as-is
|
|
172
|
+
result = child_selector;
|
|
173
|
+
}
|
|
174
|
+
} else {
|
|
175
|
+
// Fallback: return child as-is
|
|
176
|
+
result = child_selector;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Guard both selectors since we extracted C pointers and did allocations
|
|
181
|
+
RB_GC_GUARD(parent_selector);
|
|
182
|
+
RB_GC_GUARD(child_selector);
|
|
183
|
+
|
|
184
|
+
return result;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Helper to serialize a single rule (dispatches to at-rule serializer if needed)
|
|
188
|
+
static void serialize_rule(VALUE result, VALUE rule) {
|
|
189
|
+
// Check if this is an AtRule
|
|
190
|
+
if (rb_obj_is_kind_of(rule, cAtRule)) {
|
|
191
|
+
serialize_at_rule(result, rule);
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Regular Rule serialization
|
|
196
|
+
VALUE selector = rb_struct_aref(rule, INT2FIX(RULE_SELECTOR));
|
|
197
|
+
VALUE declarations = rb_struct_aref(rule, INT2FIX(RULE_DECLARATIONS));
|
|
198
|
+
|
|
199
|
+
rb_str_append(result, selector);
|
|
200
|
+
rb_str_cat2(result, " { ");
|
|
201
|
+
serialize_declarations(result, declarations);
|
|
202
|
+
rb_str_cat2(result, " }\n");
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Helper to serialize an AtRule with formatting (@keyframes, @font-face, etc)
|
|
206
|
+
static void serialize_at_rule_formatted(VALUE result, VALUE at_rule, const char *indent) {
|
|
207
|
+
VALUE selector = rb_struct_aref(at_rule, INT2FIX(AT_RULE_SELECTOR));
|
|
208
|
+
VALUE content = rb_struct_aref(at_rule, INT2FIX(AT_RULE_CONTENT));
|
|
209
|
+
|
|
210
|
+
rb_str_cat2(result, indent);
|
|
211
|
+
rb_str_append(result, selector);
|
|
212
|
+
rb_str_cat2(result, " {\n");
|
|
213
|
+
|
|
214
|
+
// Check if content is rules or declarations
|
|
215
|
+
if (RARRAY_LEN(content) > 0) {
|
|
216
|
+
VALUE first = rb_ary_entry(content, 0);
|
|
217
|
+
|
|
218
|
+
if (rb_obj_is_kind_of(first, cRule)) {
|
|
219
|
+
// Serialize as nested rules (e.g., @keyframes) with formatting
|
|
220
|
+
for (long i = 0; i < RARRAY_LEN(content); i++) {
|
|
221
|
+
VALUE nested_rule = rb_ary_entry(content, i);
|
|
222
|
+
VALUE nested_selector = rb_struct_aref(nested_rule, INT2FIX(RULE_SELECTOR));
|
|
223
|
+
VALUE nested_declarations = rb_struct_aref(nested_rule, INT2FIX(RULE_DECLARATIONS));
|
|
224
|
+
|
|
225
|
+
// Nested selector with opening brace (2-space indent)
|
|
226
|
+
rb_str_cat2(result, indent);
|
|
227
|
+
rb_str_cat2(result, " ");
|
|
228
|
+
rb_str_append(result, nested_selector);
|
|
229
|
+
rb_str_cat2(result, " {\n");
|
|
230
|
+
|
|
231
|
+
// Declarations on their own line (4-space indent)
|
|
232
|
+
rb_str_cat2(result, indent);
|
|
233
|
+
rb_str_cat2(result, " ");
|
|
234
|
+
serialize_declarations(result, nested_declarations);
|
|
235
|
+
rb_str_cat2(result, "\n");
|
|
236
|
+
|
|
237
|
+
// Closing brace (2-space indent)
|
|
238
|
+
rb_str_cat2(result, indent);
|
|
239
|
+
rb_str_cat2(result, " }\n");
|
|
240
|
+
}
|
|
241
|
+
} else {
|
|
242
|
+
// Serialize as declarations (e.g., @font-face)
|
|
243
|
+
rb_str_cat2(result, indent);
|
|
244
|
+
rb_str_cat2(result, " ");
|
|
245
|
+
serialize_declarations(result, content);
|
|
246
|
+
rb_str_cat2(result, "\n");
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
rb_str_cat2(result, indent);
|
|
251
|
+
rb_str_cat2(result, "}\n");
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Helper to serialize a single rule with formatting (indented, multi-line)
|
|
255
|
+
static void serialize_rule_formatted(VALUE result, VALUE rule, const char *indent) {
|
|
256
|
+
// Check if this is an AtRule
|
|
257
|
+
if (rb_obj_is_kind_of(rule, cAtRule)) {
|
|
258
|
+
serialize_at_rule_formatted(result, rule, indent);
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// Regular Rule serialization with formatting
|
|
263
|
+
VALUE selector = rb_struct_aref(rule, INT2FIX(RULE_SELECTOR));
|
|
264
|
+
VALUE declarations = rb_struct_aref(rule, INT2FIX(RULE_DECLARATIONS));
|
|
265
|
+
|
|
266
|
+
// Selector line with opening brace
|
|
267
|
+
rb_str_cat2(result, indent);
|
|
268
|
+
rb_str_append(result, selector);
|
|
269
|
+
rb_str_cat2(result, " {\n");
|
|
270
|
+
|
|
271
|
+
// Declarations on their own line with extra indentation
|
|
272
|
+
rb_str_cat2(result, indent);
|
|
273
|
+
rb_str_cat2(result, " ");
|
|
274
|
+
serialize_declarations(result, declarations);
|
|
275
|
+
rb_str_cat2(result, "\n");
|
|
276
|
+
|
|
277
|
+
// Closing brace
|
|
278
|
+
rb_str_cat2(result, indent);
|
|
279
|
+
rb_str_cat2(result, "}\n");
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// Context for building rule_to_media map
|
|
283
|
+
struct build_rule_map_ctx {
|
|
284
|
+
VALUE rule_to_media;
|
|
285
|
+
};
|
|
286
|
+
|
|
287
|
+
// Callback to build reverse map from rule_id to media_sym
|
|
288
|
+
static int build_rule_map_callback(VALUE media_sym, VALUE rule_ids, VALUE arg) {
|
|
289
|
+
struct build_rule_map_ctx *ctx = (struct build_rule_map_ctx *)arg;
|
|
290
|
+
|
|
291
|
+
Check_Type(rule_ids, T_ARRAY);
|
|
292
|
+
long ids_len = RARRAY_LEN(rule_ids);
|
|
293
|
+
|
|
294
|
+
for (long i = 0; i < ids_len; i++) {
|
|
295
|
+
VALUE id = rb_ary_entry(rule_ids, i);
|
|
296
|
+
VALUE existing = rb_hash_aref(ctx->rule_to_media, id);
|
|
297
|
+
|
|
298
|
+
if (NIL_P(existing)) {
|
|
299
|
+
rb_hash_aset(ctx->rule_to_media, id, media_sym);
|
|
300
|
+
} else {
|
|
301
|
+
// Keep the longer/more specific media query
|
|
302
|
+
VALUE existing_str = rb_sym2str(existing);
|
|
303
|
+
VALUE new_str = rb_sym2str(media_sym);
|
|
304
|
+
if (RSTRING_LEN(new_str) > RSTRING_LEN(existing_str)) {
|
|
305
|
+
rb_hash_aset(ctx->rule_to_media, id, media_sym);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
return ST_CONTINUE;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// Original stylesheet serialization (no nesting support)
|
|
314
|
+
static VALUE stylesheet_to_s_original(VALUE rules_array, VALUE media_index, VALUE charset) {
|
|
315
|
+
Check_Type(rules_array, T_ARRAY);
|
|
316
|
+
Check_Type(media_index, T_HASH);
|
|
317
|
+
|
|
318
|
+
VALUE result = rb_str_new_cstr("");
|
|
319
|
+
|
|
320
|
+
// Add charset if present
|
|
321
|
+
if (!NIL_P(charset)) {
|
|
322
|
+
rb_str_cat2(result, "@charset \"");
|
|
323
|
+
rb_str_append(result, charset);
|
|
324
|
+
rb_str_cat2(result, "\";\n");
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
long total_rules = RARRAY_LEN(rules_array);
|
|
328
|
+
|
|
329
|
+
// Build a map from rule_id to media query symbol using rb_hash_foreach
|
|
330
|
+
VALUE rule_to_media = rb_hash_new();
|
|
331
|
+
struct build_rule_map_ctx map_ctx = { rule_to_media };
|
|
332
|
+
rb_hash_foreach(media_index, build_rule_map_callback, (VALUE)&map_ctx);
|
|
333
|
+
|
|
334
|
+
// Iterate through rules in insertion order, grouping consecutive media queries
|
|
335
|
+
VALUE current_media = Qnil;
|
|
336
|
+
int in_media_block = 0;
|
|
337
|
+
|
|
338
|
+
for (long i = 0; i < total_rules; i++) {
|
|
339
|
+
VALUE rule = rb_ary_entry(rules_array, i);
|
|
340
|
+
VALUE rule_id = rb_struct_aref(rule, INT2FIX(RULE_ID));
|
|
341
|
+
VALUE rule_media = rb_hash_aref(rule_to_media, rule_id);
|
|
342
|
+
|
|
343
|
+
if (NIL_P(rule_media)) {
|
|
344
|
+
// Not in any media query - close any open media block first
|
|
345
|
+
if (in_media_block) {
|
|
346
|
+
rb_str_cat2(result, "}\n");
|
|
347
|
+
in_media_block = 0;
|
|
348
|
+
current_media = Qnil;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// Output rule directly
|
|
352
|
+
serialize_rule(result, rule);
|
|
353
|
+
} else {
|
|
354
|
+
// This rule is in a media query
|
|
355
|
+
// Check if media query changed from previous rule
|
|
356
|
+
if (NIL_P(current_media) || !rb_equal(current_media, rule_media)) {
|
|
357
|
+
// Close previous media block if open
|
|
358
|
+
if (in_media_block) {
|
|
359
|
+
rb_str_cat2(result, "}\n");
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// Open new media block
|
|
363
|
+
current_media = rule_media;
|
|
364
|
+
rb_str_cat2(result, "@media ");
|
|
365
|
+
rb_str_append(result, rb_sym2str(rule_media));
|
|
366
|
+
rb_str_cat2(result, " {\n");
|
|
367
|
+
in_media_block = 1;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// Serialize rule inside media block
|
|
371
|
+
serialize_rule(result, rule);
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// Close final media block if still open
|
|
376
|
+
if (in_media_block) {
|
|
377
|
+
rb_str_cat2(result, "}\n");
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
return result;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// Forward declarations
|
|
384
|
+
static void serialize_children_only(VALUE result, VALUE rules_array, long rule_idx,
|
|
385
|
+
VALUE rule_to_media, VALUE parent_to_children, VALUE parent_selector,
|
|
386
|
+
VALUE parent_declarations, int formatted, int indent_level);
|
|
387
|
+
static void serialize_rule_with_children(VALUE result, VALUE rules_array, long rule_idx,
|
|
388
|
+
VALUE rule_to_media, VALUE parent_to_children,
|
|
389
|
+
int formatted, int indent_level);
|
|
390
|
+
|
|
391
|
+
// Helper: Only serialize children of a rule (not the rule itself)
|
|
392
|
+
static void serialize_children_only(VALUE result, VALUE rules_array, long rule_idx,
|
|
393
|
+
VALUE rule_to_media, VALUE parent_to_children, VALUE parent_selector,
|
|
394
|
+
VALUE parent_declarations, int formatted, int indent_level) {
|
|
395
|
+
VALUE rule = rb_ary_entry(rules_array, rule_idx);
|
|
396
|
+
VALUE rule_id = rb_struct_aref(rule, INT2FIX(RULE_ID));
|
|
397
|
+
VALUE rule_media = rb_hash_aref(rule_to_media, rule_id); // Look up by rule ID, not array index
|
|
398
|
+
int parent_has_declarations = !NIL_P(parent_declarations) && RARRAY_LEN(parent_declarations) > 0;
|
|
399
|
+
|
|
400
|
+
// Build indentation string for this level (only if formatted)
|
|
401
|
+
VALUE indent_str = Qnil;
|
|
402
|
+
if (formatted) {
|
|
403
|
+
indent_str = rb_str_new_cstr("");
|
|
404
|
+
for (int i = 0; i < indent_level; i++) {
|
|
405
|
+
rb_str_cat2(indent_str, " ");
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
// Get children of this rule using the map
|
|
410
|
+
VALUE children_indices = rb_hash_aref(parent_to_children, rule_id);
|
|
411
|
+
|
|
412
|
+
DEBUG_PRINTF("[SERIALIZE] Looking up children for rule_id=%s\n",
|
|
413
|
+
RSTRING_PTR(rb_inspect(rule_id)));
|
|
414
|
+
|
|
415
|
+
if (!NIL_P(children_indices)) {
|
|
416
|
+
long num_children = RARRAY_LEN(children_indices);
|
|
417
|
+
DEBUG_PRINTF("[SERIALIZE] Found %ld children for rule %ld (id=%s)\n",
|
|
418
|
+
num_children, rule_idx, RSTRING_PTR(rb_inspect(rule_id)));
|
|
419
|
+
|
|
420
|
+
// Serialize selector-nested children
|
|
421
|
+
for (long i = 0; i < num_children; i++) {
|
|
422
|
+
long child_idx = FIX2LONG(rb_ary_entry(children_indices, i));
|
|
423
|
+
VALUE child = rb_ary_entry(rules_array, child_idx);
|
|
424
|
+
VALUE child_id = rb_struct_aref(child, INT2FIX(RULE_ID));
|
|
425
|
+
VALUE child_media = rb_hash_aref(rule_to_media, child_id); // Look up by rule ID
|
|
426
|
+
|
|
427
|
+
DEBUG_PRINTF("[SERIALIZE] Child %ld: child_media=%s, rule_media=%s\n", child_idx,
|
|
428
|
+
NIL_P(child_media) ? "nil" : RSTRING_PTR(rb_inspect(child_media)),
|
|
429
|
+
NIL_P(rule_media) ? "nil" : RSTRING_PTR(rb_inspect(rule_media)));
|
|
430
|
+
|
|
431
|
+
// Only serialize selector-nested children here (not @media nested)
|
|
432
|
+
if (NIL_P(child_media) || rb_equal(child_media, rule_media)) {
|
|
433
|
+
DEBUG_PRINTF("[SERIALIZE] -> Serializing as selector-nested child\n");
|
|
434
|
+
VALUE child_selector = rb_struct_aref(child, INT2FIX(RULE_SELECTOR));
|
|
435
|
+
VALUE child_nesting_style = rb_struct_aref(child, INT2FIX(RULE_NESTING_STYLE));
|
|
436
|
+
|
|
437
|
+
// Unresolve selector
|
|
438
|
+
VALUE nested_selector = unresolve_selector(parent_selector, child_selector, child_nesting_style);
|
|
439
|
+
|
|
440
|
+
if (formatted) {
|
|
441
|
+
// Formatted: indent before nested selector
|
|
442
|
+
rb_str_append(result, indent_str);
|
|
443
|
+
rb_str_append(result, nested_selector);
|
|
444
|
+
rb_str_cat2(result, " {\n");
|
|
445
|
+
|
|
446
|
+
// Serialize child declarations (each on its own line)
|
|
447
|
+
VALUE child_declarations = rb_struct_aref(child, INT2FIX(RULE_DECLARATIONS));
|
|
448
|
+
if (!NIL_P(child_declarations) && RARRAY_LEN(child_declarations) > 0) {
|
|
449
|
+
// Build child indent (one level deeper than current)
|
|
450
|
+
VALUE child_indent = rb_str_new_cstr("");
|
|
451
|
+
for (int j = 0; j <= indent_level; j++) {
|
|
452
|
+
rb_str_cat2(child_indent, " ");
|
|
453
|
+
}
|
|
454
|
+
const char *child_indent_ptr = RSTRING_PTR(child_indent);
|
|
455
|
+
serialize_declarations_formatted(result, child_declarations, child_indent_ptr);
|
|
456
|
+
RB_GC_GUARD(child_indent);
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
// Recursively serialize grandchildren
|
|
460
|
+
serialize_children_only(result, rules_array, child_idx, rule_to_media, parent_to_children,
|
|
461
|
+
child_selector, child_declarations, formatted, indent_level + 1);
|
|
462
|
+
|
|
463
|
+
// Closing brace with indentation and newline
|
|
464
|
+
rb_str_append(result, indent_str);
|
|
465
|
+
rb_str_cat2(result, "}\n");
|
|
466
|
+
} else {
|
|
467
|
+
// Compact: space before nested selector only if parent has declarations
|
|
468
|
+
if (parent_has_declarations) {
|
|
469
|
+
rb_str_cat2(result, " ");
|
|
470
|
+
}
|
|
471
|
+
rb_str_append(result, nested_selector);
|
|
472
|
+
rb_str_cat2(result, " { ");
|
|
473
|
+
|
|
474
|
+
// Serialize child declarations
|
|
475
|
+
VALUE child_declarations = rb_struct_aref(child, INT2FIX(RULE_DECLARATIONS));
|
|
476
|
+
serialize_declarations(result, child_declarations);
|
|
477
|
+
|
|
478
|
+
// Recursively serialize grandchildren
|
|
479
|
+
serialize_children_only(result, rules_array, child_idx, rule_to_media, parent_to_children,
|
|
480
|
+
child_selector, child_declarations, formatted, indent_level);
|
|
481
|
+
|
|
482
|
+
rb_str_cat2(result, " }");
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
// Serialize nested @media children (different media than parent)
|
|
488
|
+
for (long i = 0; i < num_children; i++) {
|
|
489
|
+
long child_idx = FIX2LONG(rb_ary_entry(children_indices, i));
|
|
490
|
+
VALUE child = rb_ary_entry(rules_array, child_idx);
|
|
491
|
+
VALUE child_id = rb_struct_aref(child, INT2FIX(RULE_ID));
|
|
492
|
+
VALUE child_media = rb_hash_aref(rule_to_media, child_id); // Look up by rule ID
|
|
493
|
+
|
|
494
|
+
// Check if this is a different media than parent
|
|
495
|
+
if (!NIL_P(child_media) && !rb_equal(rule_media, child_media)) {
|
|
496
|
+
// Nested @media!
|
|
497
|
+
if (formatted) {
|
|
498
|
+
rb_str_append(result, indent_str);
|
|
499
|
+
rb_str_cat2(result, "@media ");
|
|
500
|
+
rb_str_append(result, rb_sym2str(child_media));
|
|
501
|
+
rb_str_cat2(result, " {\n");
|
|
502
|
+
|
|
503
|
+
VALUE child_declarations = rb_struct_aref(child, INT2FIX(RULE_DECLARATIONS));
|
|
504
|
+
if (!NIL_P(child_declarations) && RARRAY_LEN(child_declarations) > 0) {
|
|
505
|
+
// Build child indent (one level deeper than current)
|
|
506
|
+
VALUE child_indent = rb_str_new_cstr("");
|
|
507
|
+
for (int j = 0; j <= indent_level; j++) {
|
|
508
|
+
rb_str_cat2(child_indent, " ");
|
|
509
|
+
}
|
|
510
|
+
const char *child_indent_ptr = RSTRING_PTR(child_indent);
|
|
511
|
+
serialize_declarations_formatted(result, child_declarations, child_indent_ptr);
|
|
512
|
+
RB_GC_GUARD(child_indent);
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
rb_str_append(result, indent_str);
|
|
516
|
+
rb_str_cat2(result, "}\n");
|
|
517
|
+
} else {
|
|
518
|
+
rb_str_cat2(result, " @media ");
|
|
519
|
+
rb_str_append(result, rb_sym2str(child_media));
|
|
520
|
+
rb_str_cat2(result, " { ");
|
|
521
|
+
|
|
522
|
+
VALUE child_declarations = rb_struct_aref(child, INT2FIX(RULE_DECLARATIONS));
|
|
523
|
+
serialize_declarations(result, child_declarations);
|
|
524
|
+
|
|
525
|
+
rb_str_cat2(result, " }");
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
// Recursive serializer for a rule and its nested children
|
|
533
|
+
static void serialize_rule_with_children(VALUE result, VALUE rules_array, long rule_idx,
|
|
534
|
+
VALUE rule_to_media, VALUE parent_to_children,
|
|
535
|
+
int formatted, int indent_level) {
|
|
536
|
+
VALUE rule = rb_ary_entry(rules_array, rule_idx);
|
|
537
|
+
VALUE selector = rb_struct_aref(rule, INT2FIX(RULE_SELECTOR));
|
|
538
|
+
VALUE declarations = rb_struct_aref(rule, INT2FIX(RULE_DECLARATIONS));
|
|
539
|
+
|
|
540
|
+
DEBUG_PRINTF("[SERIALIZE] Rule %ld: selector=%s\n", rule_idx, RSTRING_PTR(selector));
|
|
541
|
+
|
|
542
|
+
if (formatted) {
|
|
543
|
+
// Formatted output with indentation
|
|
544
|
+
rb_str_append(result, selector);
|
|
545
|
+
rb_str_cat2(result, " {\n");
|
|
546
|
+
|
|
547
|
+
// Serialize own declarations with indentation (each on its own line)
|
|
548
|
+
if (!NIL_P(declarations) && RARRAY_LEN(declarations) > 0) {
|
|
549
|
+
serialize_declarations_formatted(result, declarations, " ");
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
// Serialize nested children
|
|
553
|
+
serialize_children_only(result, rules_array, rule_idx, rule_to_media, parent_to_children,
|
|
554
|
+
selector, declarations, formatted, indent_level + 1);
|
|
555
|
+
|
|
556
|
+
rb_str_cat2(result, "}\n");
|
|
557
|
+
} else {
|
|
558
|
+
// Compact output
|
|
559
|
+
rb_str_append(result, selector);
|
|
560
|
+
rb_str_cat2(result, " { ");
|
|
561
|
+
|
|
562
|
+
// Serialize own declarations
|
|
563
|
+
serialize_declarations(result, declarations);
|
|
564
|
+
|
|
565
|
+
// Serialize nested children
|
|
566
|
+
serialize_children_only(result, rules_array, rule_idx, rule_to_media, parent_to_children,
|
|
567
|
+
selector, declarations, formatted, indent_level);
|
|
568
|
+
|
|
569
|
+
rb_str_cat2(result, " }\n");
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
// New stylesheet serialization entry point - checks for nesting and delegates
|
|
574
|
+
static VALUE stylesheet_to_s_new(VALUE self, VALUE rules_array, VALUE media_index, VALUE charset, VALUE has_nesting) {
|
|
575
|
+
Check_Type(rules_array, T_ARRAY);
|
|
576
|
+
Check_Type(media_index, T_HASH);
|
|
577
|
+
|
|
578
|
+
// Fast path: if no nesting, use original implementation (zero overhead)
|
|
579
|
+
if (!RTEST(has_nesting)) {
|
|
580
|
+
return stylesheet_to_s_original(rules_array, media_index, charset);
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
// SLOW PATH: Has nesting - use lookahead approach
|
|
584
|
+
long total_rules = RARRAY_LEN(rules_array);
|
|
585
|
+
VALUE result = rb_str_new_cstr("");
|
|
586
|
+
|
|
587
|
+
// Add charset if present
|
|
588
|
+
if (!NIL_P(charset)) {
|
|
589
|
+
rb_str_cat2(result, "@charset \"");
|
|
590
|
+
rb_str_append(result, charset);
|
|
591
|
+
rb_str_cat2(result, "\";\n");
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
// Build rule_to_media map
|
|
595
|
+
VALUE rule_to_media = rb_hash_new();
|
|
596
|
+
struct build_rule_map_ctx map_ctx = { rule_to_media };
|
|
597
|
+
rb_hash_foreach(media_index, build_rule_map_callback, (VALUE)&map_ctx);
|
|
598
|
+
|
|
599
|
+
// Build parent_to_children map (parent_rule_id -> array of child indices)
|
|
600
|
+
// This allows O(1) lookup of children when serializing each parent
|
|
601
|
+
VALUE parent_to_children = rb_hash_new();
|
|
602
|
+
for (long i = 0; i < total_rules; i++) {
|
|
603
|
+
VALUE rule = rb_ary_entry(rules_array, i);
|
|
604
|
+
VALUE parent_id = rb_struct_aref(rule, INT2FIX(RULE_PARENT_RULE_ID));
|
|
605
|
+
|
|
606
|
+
if (!NIL_P(parent_id)) {
|
|
607
|
+
DEBUG_PRINTF("[MAP] Rule %ld has parent_id=%s, adding to map\n", i,
|
|
608
|
+
RSTRING_PTR(rb_inspect(parent_id)));
|
|
609
|
+
|
|
610
|
+
VALUE children = rb_hash_aref(parent_to_children, parent_id);
|
|
611
|
+
if (NIL_P(children)) {
|
|
612
|
+
children = rb_ary_new();
|
|
613
|
+
rb_hash_aset(parent_to_children, parent_id, children);
|
|
614
|
+
}
|
|
615
|
+
rb_ary_push(children, LONG2FIX(i));
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
DEBUG_PRINTF("[MAP] parent_to_children map: %s\n", RSTRING_PTR(rb_inspect(parent_to_children)));
|
|
620
|
+
|
|
621
|
+
// Serialize only top-level rules (parent_rule_id == nil)
|
|
622
|
+
// Children are serialized recursively
|
|
623
|
+
DEBUG_PRINTF("[SERIALIZE] Starting serialization, total_rules=%ld\n", total_rules);
|
|
624
|
+
for (long i = 0; i < total_rules; i++) {
|
|
625
|
+
VALUE rule = rb_ary_entry(rules_array, i);
|
|
626
|
+
VALUE parent_id = rb_struct_aref(rule, INT2FIX(RULE_PARENT_RULE_ID));
|
|
627
|
+
|
|
628
|
+
DEBUG_PRINTF("[SERIALIZE] Rule %ld: selector=%s, parent_id=%s\n", i,
|
|
629
|
+
RSTRING_PTR(rb_struct_aref(rule, INT2FIX(RULE_SELECTOR))),
|
|
630
|
+
NIL_P(parent_id) ? "nil" : RSTRING_PTR(rb_inspect(parent_id)));
|
|
631
|
+
|
|
632
|
+
// Skip child rules - they're serialized when we hit their parent
|
|
633
|
+
if (!NIL_P(parent_id)) {
|
|
634
|
+
DEBUG_PRINTF("[SERIALIZE] Skipping (is child)\n");
|
|
635
|
+
continue;
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
// Check if this is an AtRule
|
|
639
|
+
if (rb_obj_is_kind_of(rule, cAtRule)) {
|
|
640
|
+
serialize_at_rule(result, rule);
|
|
641
|
+
continue;
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
// Serialize rule with nested children
|
|
645
|
+
serialize_rule_with_children(
|
|
646
|
+
result, rules_array, i, rule_to_media, parent_to_children,
|
|
647
|
+
0, // formatted (compact)
|
|
648
|
+
0 // indent_level (top-level)
|
|
649
|
+
);
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
return result;
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
// Original formatted serialization (no nesting support)
|
|
656
|
+
static VALUE stylesheet_to_formatted_s_original(VALUE rules_array, VALUE media_index, VALUE charset) {
|
|
657
|
+
long total_rules = RARRAY_LEN(rules_array);
|
|
658
|
+
VALUE result = rb_str_new_cstr("");
|
|
659
|
+
|
|
660
|
+
// Add charset if present
|
|
661
|
+
if (!NIL_P(charset)) {
|
|
662
|
+
rb_str_cat2(result, "@charset \"");
|
|
663
|
+
rb_str_append(result, charset);
|
|
664
|
+
rb_str_cat2(result, "\";\n");
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
// Build a map from rule_id to media query symbol
|
|
668
|
+
VALUE rule_to_media = rb_hash_new();
|
|
669
|
+
struct build_rule_map_ctx map_ctx = { rule_to_media };
|
|
670
|
+
rb_hash_foreach(media_index, build_rule_map_callback, (VALUE)&map_ctx);
|
|
671
|
+
|
|
672
|
+
// Iterate through rules, grouping consecutive media queries
|
|
673
|
+
VALUE current_media = Qnil;
|
|
674
|
+
int in_media_block = 0;
|
|
675
|
+
|
|
676
|
+
for (long i = 0; i < total_rules; i++) {
|
|
677
|
+
VALUE rule = rb_ary_entry(rules_array, i);
|
|
678
|
+
VALUE rule_id = rb_struct_aref(rule, INT2FIX(RULE_ID));
|
|
679
|
+
VALUE rule_media = rb_hash_aref(rule_to_media, rule_id);
|
|
680
|
+
|
|
681
|
+
if (NIL_P(rule_media)) {
|
|
682
|
+
// Not in any media query - close any open media block first
|
|
683
|
+
if (in_media_block) {
|
|
684
|
+
rb_str_cat2(result, "}\n");
|
|
685
|
+
in_media_block = 0;
|
|
686
|
+
current_media = Qnil;
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
// Output rule with no indentation
|
|
690
|
+
serialize_rule_formatted(result, rule, "");
|
|
691
|
+
} else {
|
|
692
|
+
// This rule is in a media query
|
|
693
|
+
if (NIL_P(current_media) || !rb_equal(current_media, rule_media)) {
|
|
694
|
+
// Close previous media block if open
|
|
695
|
+
if (in_media_block) {
|
|
696
|
+
rb_str_cat2(result, "}\n");
|
|
697
|
+
} else {
|
|
698
|
+
// Add blank line before @media if transitioning from non-media rules
|
|
699
|
+
if (RSTRING_LEN(result) > 0) {
|
|
700
|
+
rb_str_cat2(result, "\n");
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
// Open new media block
|
|
705
|
+
current_media = rule_media;
|
|
706
|
+
rb_str_cat2(result, "@media ");
|
|
707
|
+
rb_str_append(result, rb_sym2str(rule_media));
|
|
708
|
+
rb_str_cat2(result, " {\n");
|
|
709
|
+
in_media_block = 1;
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
// Serialize rule inside media block with 2-space indentation
|
|
713
|
+
serialize_rule_formatted(result, rule, " ");
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
// Close final media block if still open
|
|
718
|
+
if (in_media_block) {
|
|
719
|
+
rb_str_cat2(result, "}\n");
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
return result;
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
// Formatted version with indentation and newlines (with nesting support)
|
|
726
|
+
static VALUE stylesheet_to_formatted_s_new(VALUE self, VALUE rules_array, VALUE media_index, VALUE charset, VALUE has_nesting) {
|
|
727
|
+
Check_Type(rules_array, T_ARRAY);
|
|
728
|
+
Check_Type(media_index, T_HASH);
|
|
729
|
+
|
|
730
|
+
// Fast path: if no nesting, use original implementation (zero overhead)
|
|
731
|
+
if (!RTEST(has_nesting)) {
|
|
732
|
+
return stylesheet_to_formatted_s_original(rules_array, media_index, charset);
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
// SLOW PATH: Has nesting - use parameterized serialization with formatted=1
|
|
736
|
+
long total_rules = RARRAY_LEN(rules_array);
|
|
737
|
+
VALUE result = rb_str_new_cstr("");
|
|
738
|
+
|
|
739
|
+
// Add charset if present
|
|
740
|
+
if (!NIL_P(charset)) {
|
|
741
|
+
rb_str_cat2(result, "@charset \"");
|
|
742
|
+
rb_str_append(result, charset);
|
|
743
|
+
rb_str_cat2(result, "\";\n");
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
// Build rule_to_media map
|
|
747
|
+
VALUE rule_to_media = rb_hash_new();
|
|
748
|
+
struct build_rule_map_ctx map_ctx = { rule_to_media };
|
|
749
|
+
rb_hash_foreach(media_index, build_rule_map_callback, (VALUE)&map_ctx);
|
|
750
|
+
|
|
751
|
+
// Build parent_to_children map (parent_rule_id -> array of child indices)
|
|
752
|
+
VALUE parent_to_children = rb_hash_new();
|
|
753
|
+
for (long i = 0; i < total_rules; i++) {
|
|
754
|
+
VALUE rule = rb_ary_entry(rules_array, i);
|
|
755
|
+
VALUE parent_id = rb_struct_aref(rule, INT2FIX(RULE_PARENT_RULE_ID));
|
|
756
|
+
|
|
757
|
+
if (!NIL_P(parent_id)) {
|
|
758
|
+
VALUE children = rb_hash_aref(parent_to_children, parent_id);
|
|
759
|
+
if (NIL_P(children)) {
|
|
760
|
+
children = rb_ary_new();
|
|
761
|
+
rb_hash_aset(parent_to_children, parent_id, children);
|
|
762
|
+
}
|
|
763
|
+
rb_ary_push(children, LONG2FIX(i));
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
// Serialize only top-level rules (parent_rule_id == nil)
|
|
768
|
+
for (long i = 0; i < total_rules; i++) {
|
|
769
|
+
VALUE rule = rb_ary_entry(rules_array, i);
|
|
770
|
+
VALUE parent_id = rb_struct_aref(rule, INT2FIX(RULE_PARENT_RULE_ID));
|
|
771
|
+
|
|
772
|
+
// Skip child rules - they're serialized when we hit their parent
|
|
773
|
+
if (!NIL_P(parent_id)) {
|
|
774
|
+
continue;
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
// Check if this is an AtRule
|
|
778
|
+
if (rb_obj_is_kind_of(rule, cAtRule)) {
|
|
779
|
+
serialize_at_rule(result, rule);
|
|
780
|
+
continue;
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
// Serialize rule with nested children
|
|
784
|
+
serialize_rule_with_children(
|
|
785
|
+
result, rules_array, i, rule_to_media, parent_to_children,
|
|
786
|
+
1, // formatted (with indentation)
|
|
787
|
+
0 // indent_level (top-level)
|
|
788
|
+
);
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
return result;
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
/*
|
|
795
|
+
* Parse declarations string into array of Declaration structs
|
|
796
|
+
*
|
|
797
|
+
* This is a copy of parse_declarations_string from css_parser.c,
|
|
798
|
+
* but creates Declaration structs instead of Declaration structs
|
|
799
|
+
*/
|
|
800
|
+
static VALUE new_parse_declarations_string(const char *start, const char *end) {
|
|
801
|
+
VALUE declarations = rb_ary_new();
|
|
802
|
+
|
|
803
|
+
// Note: Comments in declarations aren't stripped (copy_without_comments is in css_parser.c)
|
|
804
|
+
// The parser is error-tolerant, so it just continues parsing as-is.
|
|
805
|
+
|
|
806
|
+
const char *pos = start;
|
|
807
|
+
while (pos < end) {
|
|
808
|
+
// Skip whitespace and semicolons
|
|
809
|
+
while (pos < end && (IS_WHITESPACE(*pos) || *pos == ';')) pos++;
|
|
810
|
+
if (pos >= end) break;
|
|
811
|
+
|
|
812
|
+
// Find property (up to colon)
|
|
813
|
+
const char *prop_start = pos;
|
|
814
|
+
while (pos < end && *pos != ':') pos++;
|
|
815
|
+
if (pos >= end) break; // No colon found
|
|
816
|
+
|
|
817
|
+
const char *prop_end = pos;
|
|
818
|
+
// Trim trailing whitespace
|
|
819
|
+
while (prop_end > prop_start && IS_WHITESPACE(*(prop_end-1))) prop_end--;
|
|
820
|
+
// Trim leading whitespace
|
|
821
|
+
while (prop_start < prop_end && IS_WHITESPACE(*prop_start)) prop_start++;
|
|
822
|
+
|
|
823
|
+
pos++; // Skip colon
|
|
824
|
+
// Trim leading whitespace
|
|
825
|
+
while (pos < end && IS_WHITESPACE(*pos)) pos++;
|
|
826
|
+
|
|
827
|
+
// Find value (up to semicolon or end), handling parentheses
|
|
828
|
+
const char *val_start = pos;
|
|
829
|
+
int paren_depth = 0;
|
|
830
|
+
while (pos < end) {
|
|
831
|
+
if (*pos == '(') paren_depth++;
|
|
832
|
+
else if (*pos == ')') paren_depth--;
|
|
833
|
+
else if (*pos == ';' && paren_depth == 0) break;
|
|
834
|
+
pos++;
|
|
835
|
+
}
|
|
836
|
+
const char *val_end = pos;
|
|
837
|
+
// Trim trailing whitespace
|
|
838
|
+
while (val_end > val_start && IS_WHITESPACE(*(val_end-1))) val_end--;
|
|
839
|
+
|
|
840
|
+
// Check for !important
|
|
841
|
+
int is_important = 0;
|
|
842
|
+
if (val_end - val_start >= 10) { // strlen("!important") = 10
|
|
843
|
+
const char *check = val_end - 10;
|
|
844
|
+
while (check < val_end && IS_WHITESPACE(*check)) check++;
|
|
845
|
+
if (check < val_end && *check == '!') {
|
|
846
|
+
check++;
|
|
847
|
+
while (check < val_end && IS_WHITESPACE(*check)) check++;
|
|
848
|
+
if ((val_end - check) >= 9 && strncmp(check, "important", 9) == 0) {
|
|
849
|
+
is_important = 1;
|
|
850
|
+
const char *important_pos = check - 1;
|
|
851
|
+
while (important_pos > val_start && (IS_WHITESPACE(*(important_pos-1)) || *(important_pos-1) == '!')) {
|
|
852
|
+
important_pos--;
|
|
853
|
+
}
|
|
854
|
+
val_end = important_pos;
|
|
855
|
+
// Trim trailing whitespace again
|
|
856
|
+
while (val_end > val_start && IS_WHITESPACE(*(val_end-1))) val_end--;
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
// Skip if value is empty
|
|
862
|
+
if (val_end > val_start) {
|
|
863
|
+
long prop_len = prop_end - prop_start;
|
|
864
|
+
long val_len = val_end - val_start;
|
|
865
|
+
|
|
866
|
+
// Create property string (US-ASCII, lowercased)
|
|
867
|
+
VALUE property = rb_usascii_str_new(prop_start, prop_len);
|
|
868
|
+
// Lowercase it inline
|
|
869
|
+
char *prop_ptr = RSTRING_PTR(property);
|
|
870
|
+
for (long i = 0; i < prop_len; i++) {
|
|
871
|
+
if (prop_ptr[i] >= 'A' && prop_ptr[i] <= 'Z') {
|
|
872
|
+
prop_ptr[i] += 32;
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
VALUE value = rb_utf8_str_new(val_start, val_len);
|
|
877
|
+
|
|
878
|
+
// Create Declaration struct
|
|
879
|
+
VALUE decl = rb_struct_new(cDeclaration,
|
|
880
|
+
property, value, is_important ? Qtrue : Qfalse);
|
|
881
|
+
|
|
882
|
+
rb_ary_push(declarations, decl);
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
return declarations;
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
/*
|
|
890
|
+
* Convert array of Declaration structs to CSS string
|
|
891
|
+
* Format: "prop: value; prop2: value2 !important; "
|
|
892
|
+
*
|
|
893
|
+
* This is a copy of declarations_array_to_s from cataract.c,
|
|
894
|
+
* but works with Declaration structs instead of Declaration structs
|
|
895
|
+
*/
|
|
896
|
+
static VALUE new_declarations_array_to_s(VALUE declarations_array) {
|
|
897
|
+
Check_Type(declarations_array, T_ARRAY);
|
|
898
|
+
|
|
899
|
+
long len = RARRAY_LEN(declarations_array);
|
|
900
|
+
if (len == 0) {
|
|
901
|
+
return rb_str_new_cstr("");
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
// Use rb_str_buf_new for efficient string building
|
|
905
|
+
VALUE result = rb_str_buf_new(len * 32); // Estimate 32 chars per declaration
|
|
906
|
+
|
|
907
|
+
for (long i = 0; i < len; i++) {
|
|
908
|
+
VALUE decl = rb_ary_entry(declarations_array, i);
|
|
909
|
+
|
|
910
|
+
// Validate this is a Declaration struct
|
|
911
|
+
if (!RB_TYPE_P(decl, T_STRUCT) || rb_obj_class(decl) != cDeclaration) {
|
|
912
|
+
rb_raise(rb_eTypeError,
|
|
913
|
+
"Expected array of Declaration structs, got %s at index %ld",
|
|
914
|
+
rb_obj_classname(decl), i);
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
// Extract struct fields
|
|
918
|
+
VALUE property = rb_struct_aref(decl, INT2FIX(DECL_PROPERTY));
|
|
919
|
+
VALUE value = rb_struct_aref(decl, INT2FIX(DECL_VALUE));
|
|
920
|
+
VALUE important = rb_struct_aref(decl, INT2FIX(DECL_IMPORTANT));
|
|
921
|
+
|
|
922
|
+
// Append: "property: value"
|
|
923
|
+
rb_str_buf_append(result, property);
|
|
924
|
+
rb_str_buf_cat2(result, ": ");
|
|
925
|
+
rb_str_buf_append(result, value);
|
|
926
|
+
|
|
927
|
+
// Append " !important" if needed
|
|
928
|
+
if (RTEST(important)) {
|
|
929
|
+
rb_str_buf_cat2(result, " !important");
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
rb_str_buf_cat2(result, "; ");
|
|
933
|
+
|
|
934
|
+
RB_GC_GUARD(decl);
|
|
935
|
+
RB_GC_GUARD(property);
|
|
936
|
+
RB_GC_GUARD(value);
|
|
937
|
+
RB_GC_GUARD(important);
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
// Strip trailing space
|
|
941
|
+
rb_str_set_len(result, RSTRING_LEN(result) - 1);
|
|
942
|
+
|
|
943
|
+
RB_GC_GUARD(result);
|
|
944
|
+
return result;
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
/*
|
|
948
|
+
* Instance method: Declarations#to_s
|
|
949
|
+
* Converts declarations to CSS string
|
|
950
|
+
*
|
|
951
|
+
* @return [String] CSS declarations like "color: red; margin: 10px !important;"
|
|
952
|
+
*/
|
|
953
|
+
static VALUE new_declarations_to_s_method(VALUE self) {
|
|
954
|
+
// Get @values instance variable (array of Declaration structs)
|
|
955
|
+
VALUE values = rb_ivar_get(self, rb_intern("@values"));
|
|
956
|
+
|
|
957
|
+
// Call core serialization function
|
|
958
|
+
return new_declarations_array_to_s(values);
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
/*
|
|
962
|
+
* Ruby-facing wrapper for new_parse_declarations
|
|
963
|
+
*
|
|
964
|
+
* @param declarations_string [String] CSS declarations like "color: red; margin: 10px"
|
|
965
|
+
* @return [Array<Declaration>] Array of parsed declaration structs
|
|
966
|
+
*/
|
|
967
|
+
static VALUE new_parse_declarations(VALUE self, VALUE declarations_string) {
|
|
968
|
+
Check_Type(declarations_string, T_STRING);
|
|
969
|
+
|
|
970
|
+
const char *input = RSTRING_PTR(declarations_string);
|
|
971
|
+
long input_len = RSTRING_LEN(declarations_string);
|
|
972
|
+
|
|
973
|
+
// Strip outer braces and whitespace (css_parser compatibility)
|
|
974
|
+
const char *start = input;
|
|
975
|
+
const char *end = input + input_len;
|
|
976
|
+
|
|
977
|
+
while (start < end && (IS_WHITESPACE(*start) || *start == '{')) start++;
|
|
978
|
+
while (end > start && (IS_WHITESPACE(*(end-1)) || *(end-1) == '}')) end--;
|
|
979
|
+
|
|
980
|
+
VALUE result = new_parse_declarations_string(start, end);
|
|
981
|
+
|
|
982
|
+
RB_GC_GUARD(result);
|
|
983
|
+
return result;
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
// ============================================================================
|
|
987
|
+
// Ruby Module Initialization
|
|
988
|
+
// ============================================================================
|
|
989
|
+
|
|
990
|
+
void Init_cataract(void) {
|
|
991
|
+
// Get Cataract module (should be defined by main extension)
|
|
992
|
+
VALUE mCataract = rb_define_module("Cataract");
|
|
993
|
+
|
|
994
|
+
// Define error classes (reuse from main extension if possible)
|
|
995
|
+
if (rb_const_defined(mCataract, rb_intern("Error"))) {
|
|
996
|
+
eCataractError = rb_const_get(mCataract, rb_intern("Error"));
|
|
997
|
+
} else {
|
|
998
|
+
eCataractError = rb_define_class_under(mCataract, "Error", rb_eStandardError);
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
if (rb_const_defined(mCataract, rb_intern("DepthError"))) {
|
|
1002
|
+
eDepthError = rb_const_get(mCataract, rb_intern("DepthError"));
|
|
1003
|
+
} else {
|
|
1004
|
+
eDepthError = rb_define_class_under(mCataract, "DepthError", eCataractError);
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
if (rb_const_defined(mCataract, rb_intern("SizeError"))) {
|
|
1008
|
+
eSizeError = rb_const_get(mCataract, rb_intern("SizeError"));
|
|
1009
|
+
} else {
|
|
1010
|
+
eSizeError = rb_define_class_under(mCataract, "SizeError", eCataractError);
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
// Define Rule struct: (id, selector, declarations, specificity, parent_rule_id, nesting_style)
|
|
1014
|
+
cRule = rb_struct_define_under(
|
|
1015
|
+
mCataract,
|
|
1016
|
+
"Rule",
|
|
1017
|
+
"id", // Integer (0-indexed position in @rules array)
|
|
1018
|
+
"selector", // String (fully resolved/flattened selector)
|
|
1019
|
+
"declarations", // Array of Declaration
|
|
1020
|
+
"specificity", // Integer (nil = not calculated yet)
|
|
1021
|
+
"parent_rule_id", // Integer | nil (parent rule ID for nested rules)
|
|
1022
|
+
"nesting_style", // Integer | nil (0=implicit, 1=explicit, nil=not nested)
|
|
1023
|
+
NULL
|
|
1024
|
+
);
|
|
1025
|
+
|
|
1026
|
+
// Define Declaration struct: (property, value, important)
|
|
1027
|
+
cDeclaration = rb_struct_define_under(
|
|
1028
|
+
mCataract,
|
|
1029
|
+
"Declaration",
|
|
1030
|
+
"property", // String
|
|
1031
|
+
"value", // String
|
|
1032
|
+
"important", // Boolean
|
|
1033
|
+
NULL
|
|
1034
|
+
);
|
|
1035
|
+
|
|
1036
|
+
// Define AtRule struct: (id, selector, content, specificity)
|
|
1037
|
+
// Matches Rule interface for duck-typing
|
|
1038
|
+
// - For @keyframes: content is Array of Rule (keyframe blocks)
|
|
1039
|
+
// - For @font-face: content is Array of Declaration
|
|
1040
|
+
cAtRule = rb_struct_define_under(
|
|
1041
|
+
mCataract,
|
|
1042
|
+
"AtRule",
|
|
1043
|
+
"id", // Integer (0-indexed position in @rules array)
|
|
1044
|
+
"selector", // String (e.g., "@keyframes fade", "@font-face")
|
|
1045
|
+
"content", // Array of Rule or Declaration
|
|
1046
|
+
"specificity", // Always nil for at-rules
|
|
1047
|
+
NULL
|
|
1048
|
+
);
|
|
1049
|
+
|
|
1050
|
+
// Define Declarations class and add to_s method
|
|
1051
|
+
VALUE cDeclarations = rb_define_class_under(mCataract, "Declarations", rb_cObject);
|
|
1052
|
+
rb_define_method(cDeclarations, "to_s", new_declarations_to_s_method, 0);
|
|
1053
|
+
|
|
1054
|
+
// Define Stylesheet class (Ruby will add instance methods like each_selector)
|
|
1055
|
+
cStylesheet = rb_define_class_under(mCataract, "Stylesheet", rb_cObject);
|
|
1056
|
+
|
|
1057
|
+
// Define module functions
|
|
1058
|
+
rb_define_module_function(mCataract, "_parse_css", parse_css_new, 1);
|
|
1059
|
+
rb_define_module_function(mCataract, "_stylesheet_to_s", stylesheet_to_s_new, 4);
|
|
1060
|
+
rb_define_module_function(mCataract, "_stylesheet_to_formatted_s", stylesheet_to_formatted_s_new, 4);
|
|
1061
|
+
rb_define_module_function(mCataract, "parse_media_types", parse_media_types, 1);
|
|
1062
|
+
rb_define_module_function(mCataract, "parse_declarations", new_parse_declarations, 1);
|
|
1063
|
+
rb_define_module_function(mCataract, "merge", cataract_merge_new, 1);
|
|
1064
|
+
rb_define_module_function(mCataract, "extract_imports", extract_imports, 1);
|
|
1065
|
+
rb_define_module_function(mCataract, "calculate_specificity", calculate_specificity, 1);
|
|
1066
|
+
|
|
1067
|
+
// Initialize merge constants (cached property strings)
|
|
1068
|
+
init_merge_constants();
|
|
1069
|
+
|
|
1070
|
+
// Export compile-time flags as a hash for runtime introspection
|
|
1071
|
+
VALUE compile_flags = rb_hash_new();
|
|
1072
|
+
|
|
1073
|
+
#ifdef CATARACT_DEBUG
|
|
1074
|
+
rb_hash_aset(compile_flags, ID2SYM(rb_intern("debug")), Qtrue);
|
|
1075
|
+
#else
|
|
1076
|
+
rb_hash_aset(compile_flags, ID2SYM(rb_intern("debug")), Qfalse);
|
|
1077
|
+
#endif
|
|
1078
|
+
|
|
1079
|
+
#ifdef DISABLE_STR_BUF_OPTIMIZATION
|
|
1080
|
+
rb_hash_aset(compile_flags, ID2SYM(rb_intern("str_buf_optimization")), Qfalse);
|
|
1081
|
+
#else
|
|
1082
|
+
rb_hash_aset(compile_flags, ID2SYM(rb_intern("str_buf_optimization")), Qtrue);
|
|
1083
|
+
#endif
|
|
1084
|
+
|
|
1085
|
+
rb_define_const(mCataract, "COMPILE_FLAGS", compile_flags);
|
|
1086
|
+
}
|