cataract 0.1.4 → 0.2.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/ci-manual-rubies.yml +18 -1
- data/.rubocop.yml +36 -6
- data/.rubocop_todo.yml +7 -7
- data/BENCHMARKS.md +30 -30
- data/CHANGELOG.md +10 -0
- data/RAGEL_MIGRATION.md +2 -2
- data/README.md +7 -2
- data/Rakefile +24 -11
- data/cataract.gemspec +1 -1
- data/ext/cataract/cataract.c +12 -3
- data/ext/cataract/cataract.h +5 -3
- data/ext/cataract/css_parser.c +156 -32
- data/ext/cataract/extconf.rb +2 -2
- data/ext/cataract/{merge.c → flatten.c} +520 -468
- data/ext/cataract/shorthand_expander.c +164 -115
- data/lib/cataract/import_resolver.rb +60 -39
- data/lib/cataract/import_statement.rb +49 -0
- data/lib/cataract/pure/{merge.rb → flatten.rb} +39 -40
- data/lib/cataract/pure/imports.rb +13 -0
- data/lib/cataract/pure/parser.rb +108 -4
- data/lib/cataract/pure.rb +32 -9
- data/lib/cataract/rule.rb +51 -6
- data/lib/cataract/stylesheet.rb +343 -41
- data/lib/cataract/version.rb +1 -1
- data/lib/cataract.rb +28 -24
- metadata +4 -3
|
@@ -9,42 +9,13 @@
|
|
|
9
9
|
|
|
10
10
|
#include "cataract.h"
|
|
11
11
|
|
|
12
|
-
/*
|
|
13
|
-
* Helper: Check if string ends with !important and strip it
|
|
14
|
-
* Returns 1 if important, 0 otherwise
|
|
15
|
-
* Updates len to exclude !important if present
|
|
16
|
-
*/
|
|
17
|
-
static int check_and_strip_important(const char *str, size_t *len) {
|
|
18
|
-
if (*len < 10) return 0; // Need at least "!important"
|
|
19
|
-
|
|
20
|
-
const char *p = str + *len - 1;
|
|
21
|
-
|
|
22
|
-
// Skip trailing whitespace
|
|
23
|
-
while (p > str && (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r')) {
|
|
24
|
-
p--;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
// Check if it ends with "!important" (case-insensitive would be: strncasecmp)
|
|
28
|
-
if (p - str >= 9) {
|
|
29
|
-
if (strncmp(p - 9, "!important", 10) == 0) {
|
|
30
|
-
// Found it - update length to exclude !important and trailing whitespace
|
|
31
|
-
p -= 10;
|
|
32
|
-
while (p >= str && (*p == ' ' || *p == '\t')) p--;
|
|
33
|
-
*len = (p - str) + 1;
|
|
34
|
-
return 1;
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
return 0;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
12
|
/*
|
|
41
13
|
* Helper: Expand dimension shorthand (margin, padding, border-color, etc.)
|
|
14
|
+
* Returns array of 4 Declaration structs (top, right, bottom, left)
|
|
42
15
|
*/
|
|
43
|
-
static VALUE expand_dimensions(VALUE parts, const char *property, const char *suffix) {
|
|
16
|
+
static VALUE expand_dimensions(VALUE parts, const char *property, const char *suffix, VALUE important) {
|
|
44
17
|
long len = RARRAY_LEN(parts);
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
if (len == 0) return result;
|
|
18
|
+
if (len == 0) return rb_ary_new();
|
|
48
19
|
|
|
49
20
|
// Sanity check: property and suffix should be reasonable length
|
|
50
21
|
if (strlen(property) > 32) {
|
|
@@ -54,25 +25,6 @@ static VALUE expand_dimensions(VALUE parts, const char *property, const char *su
|
|
|
54
25
|
rb_raise(rb_eArgError, "Suffix name too long (max 32 chars)");
|
|
55
26
|
}
|
|
56
27
|
|
|
57
|
-
// Check if last part has !important
|
|
58
|
-
int is_important = 0;
|
|
59
|
-
if (len > 0) {
|
|
60
|
-
VALUE last_part = rb_ary_entry(parts, len - 1);
|
|
61
|
-
const char *last_str = RSTRING_PTR(last_part);
|
|
62
|
-
size_t last_len = RSTRING_LEN(last_part);
|
|
63
|
-
|
|
64
|
-
if (check_and_strip_important(last_str, &last_len)) {
|
|
65
|
-
is_important = 1;
|
|
66
|
-
// Update the array with stripped value
|
|
67
|
-
if (last_len > 0) {
|
|
68
|
-
rb_ary_store(parts, len - 1, rb_str_new(last_str, last_len));
|
|
69
|
-
} else {
|
|
70
|
-
// The value was just "!important" - reduce array length
|
|
71
|
-
len--;
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
|
|
76
28
|
VALUE sides[4];
|
|
77
29
|
if (len == 1) {
|
|
78
30
|
VALUE v = rb_ary_entry(parts, 0);
|
|
@@ -90,30 +42,27 @@ static VALUE expand_dimensions(VALUE parts, const char *property, const char *su
|
|
|
90
42
|
sides[2] = rb_ary_entry(parts, 2);
|
|
91
43
|
sides[3] = rb_ary_entry(parts, 3);
|
|
92
44
|
} else {
|
|
93
|
-
return
|
|
45
|
+
return rb_ary_new(); // Invalid - return empty array
|
|
94
46
|
}
|
|
95
47
|
|
|
48
|
+
// Create array of 4 Declaration structs directly (no intermediate hash!)
|
|
49
|
+
VALUE result = rb_ary_new_capa(4);
|
|
96
50
|
const char *side_names[] = {"top", "right", "bottom", "left"};
|
|
51
|
+
|
|
97
52
|
for (int i = 0; i < 4; i++) {
|
|
98
|
-
char
|
|
53
|
+
char prop_name[128];
|
|
99
54
|
if (suffix) {
|
|
100
|
-
snprintf(
|
|
55
|
+
snprintf(prop_name, sizeof(prop_name), "%s-%s-%s", property, side_names[i], suffix);
|
|
101
56
|
} else {
|
|
102
|
-
snprintf(
|
|
57
|
+
snprintf(prop_name, sizeof(prop_name), "%s-%s", property, side_names[i]);
|
|
103
58
|
}
|
|
104
59
|
|
|
105
|
-
//
|
|
106
|
-
VALUE
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
final_value = STR_NEW_CSTR(buf);
|
|
112
|
-
} else {
|
|
113
|
-
final_value = sides[i];
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
rb_hash_aset(result, STR_NEW_CSTR(key), final_value);
|
|
60
|
+
// Create Declaration struct directly: Declaration.new(property, value, important)
|
|
61
|
+
VALUE decl = rb_struct_new(cDeclaration,
|
|
62
|
+
STR_NEW_CSTR(prop_name),
|
|
63
|
+
sides[i],
|
|
64
|
+
important);
|
|
65
|
+
rb_ary_push(result, decl);
|
|
117
66
|
}
|
|
118
67
|
|
|
119
68
|
return result;
|
|
@@ -121,42 +70,47 @@ static VALUE expand_dimensions(VALUE parts, const char *property, const char *su
|
|
|
121
70
|
|
|
122
71
|
/*
|
|
123
72
|
* Expand margin shorthand: "10px 20px 30px 40px"
|
|
73
|
+
* Returns array of Declaration structs
|
|
124
74
|
*/
|
|
125
75
|
VALUE cataract_expand_margin(VALUE self, VALUE value) {
|
|
126
76
|
VALUE parts = cataract_split_value(self, value);
|
|
127
|
-
return expand_dimensions(parts, "margin", NULL);
|
|
77
|
+
return expand_dimensions(parts, "margin", NULL, Qfalse);
|
|
128
78
|
}
|
|
129
79
|
|
|
130
80
|
/*
|
|
131
81
|
* Expand padding shorthand: "10px 20px 30px 40px"
|
|
82
|
+
* Returns array of Declaration structs
|
|
132
83
|
*/
|
|
133
84
|
VALUE cataract_expand_padding(VALUE self, VALUE value) {
|
|
134
85
|
VALUE parts = cataract_split_value(self, value);
|
|
135
|
-
return expand_dimensions(parts, "padding", NULL);
|
|
86
|
+
return expand_dimensions(parts, "padding", NULL, Qfalse);
|
|
136
87
|
}
|
|
137
88
|
|
|
138
89
|
/*
|
|
139
90
|
* Expand border-color shorthand: "red green blue yellow"
|
|
91
|
+
* Returns array of Declaration structs
|
|
140
92
|
*/
|
|
141
93
|
VALUE cataract_expand_border_color(VALUE self, VALUE value) {
|
|
142
94
|
VALUE parts = cataract_split_value(self, value);
|
|
143
|
-
return expand_dimensions(parts, "border", "color");
|
|
95
|
+
return expand_dimensions(parts, "border", "color", Qfalse);
|
|
144
96
|
}
|
|
145
97
|
|
|
146
98
|
/*
|
|
147
99
|
* Expand border-style shorthand: "solid dashed dotted double"
|
|
100
|
+
* Returns array of Declaration structs
|
|
148
101
|
*/
|
|
149
102
|
VALUE cataract_expand_border_style(VALUE self, VALUE value) {
|
|
150
103
|
VALUE parts = cataract_split_value(self, value);
|
|
151
|
-
return expand_dimensions(parts, "border", "style");
|
|
104
|
+
return expand_dimensions(parts, "border", "style", Qfalse);
|
|
152
105
|
}
|
|
153
106
|
|
|
154
107
|
/*
|
|
155
108
|
* Expand border-width shorthand: "1px 2px 3px 4px"
|
|
109
|
+
* Returns array of Declaration structs
|
|
156
110
|
*/
|
|
157
111
|
VALUE cataract_expand_border_width(VALUE self, VALUE value) {
|
|
158
112
|
VALUE parts = cataract_split_value(self, value);
|
|
159
|
-
return expand_dimensions(parts, "border", "width");
|
|
113
|
+
return expand_dimensions(parts, "border", "width", Qfalse);
|
|
160
114
|
}
|
|
161
115
|
|
|
162
116
|
/*
|
|
@@ -184,11 +138,11 @@ static int is_border_style(const char *str) {
|
|
|
184
138
|
|
|
185
139
|
/*
|
|
186
140
|
* Expand border shorthand: "1px solid red"
|
|
141
|
+
* Returns array of Declaration structs (up to 12: 4 sides × 3 properties)
|
|
187
142
|
*/
|
|
188
143
|
VALUE cataract_expand_border(VALUE self, VALUE value) {
|
|
189
144
|
VALUE parts = cataract_split_value(self, value);
|
|
190
145
|
long len = RARRAY_LEN(parts);
|
|
191
|
-
VALUE result = rb_hash_new();
|
|
192
146
|
|
|
193
147
|
VALUE width = Qnil;
|
|
194
148
|
VALUE style = Qnil;
|
|
@@ -207,22 +161,28 @@ VALUE cataract_expand_border(VALUE self, VALUE value) {
|
|
|
207
161
|
}
|
|
208
162
|
}
|
|
209
163
|
|
|
164
|
+
// Create array of Declaration structs
|
|
165
|
+
VALUE result = rb_ary_new_capa(12); // Max 12: 4 sides × 3 properties
|
|
210
166
|
const char *sides[] = {"top", "right", "bottom", "left"};
|
|
167
|
+
|
|
211
168
|
for (int i = 0; i < 4; i++) {
|
|
212
169
|
if (width != Qnil) {
|
|
213
|
-
char
|
|
214
|
-
snprintf(
|
|
215
|
-
|
|
170
|
+
char prop[64];
|
|
171
|
+
snprintf(prop, sizeof(prop), "border-%s-width", sides[i]);
|
|
172
|
+
VALUE decl = rb_struct_new(cDeclaration, STR_NEW_CSTR(prop), width, Qfalse);
|
|
173
|
+
rb_ary_push(result, decl);
|
|
216
174
|
}
|
|
217
175
|
if (style != Qnil) {
|
|
218
|
-
char
|
|
219
|
-
snprintf(
|
|
220
|
-
|
|
176
|
+
char prop[64];
|
|
177
|
+
snprintf(prop, sizeof(prop), "border-%s-style", sides[i]);
|
|
178
|
+
VALUE decl = rb_struct_new(cDeclaration, STR_NEW_CSTR(prop), style, Qfalse);
|
|
179
|
+
rb_ary_push(result, decl);
|
|
221
180
|
}
|
|
222
181
|
if (color != Qnil) {
|
|
223
|
-
char
|
|
224
|
-
snprintf(
|
|
225
|
-
|
|
182
|
+
char prop[64];
|
|
183
|
+
snprintf(prop, sizeof(prop), "border-%s-color", sides[i]);
|
|
184
|
+
VALUE decl = rb_struct_new(cDeclaration, STR_NEW_CSTR(prop), color, Qfalse);
|
|
185
|
+
rb_ary_push(result, decl);
|
|
226
186
|
}
|
|
227
187
|
}
|
|
228
188
|
|
|
@@ -231,11 +191,11 @@ VALUE cataract_expand_border(VALUE self, VALUE value) {
|
|
|
231
191
|
|
|
232
192
|
/*
|
|
233
193
|
* Expand border-{side} shorthand: "2px dashed blue"
|
|
194
|
+
* Returns array of Declaration structs (up to 3: width, style, color)
|
|
234
195
|
*/
|
|
235
196
|
VALUE cataract_expand_border_side(VALUE self, VALUE side, VALUE value) {
|
|
236
197
|
VALUE parts = cataract_split_value(self, value);
|
|
237
198
|
long len = RARRAY_LEN(parts);
|
|
238
|
-
VALUE result = rb_hash_new();
|
|
239
199
|
const char *side_str = StringValueCStr(side);
|
|
240
200
|
|
|
241
201
|
// Validate side is one of the valid CSS sides
|
|
@@ -268,20 +228,26 @@ VALUE cataract_expand_border_side(VALUE self, VALUE side, VALUE value) {
|
|
|
268
228
|
}
|
|
269
229
|
}
|
|
270
230
|
|
|
231
|
+
// Create array of Declaration structs
|
|
232
|
+
VALUE result = rb_ary_new_capa(3); // Max 3: width, style, color
|
|
233
|
+
|
|
271
234
|
if (width != Qnil) {
|
|
272
|
-
char
|
|
273
|
-
snprintf(
|
|
274
|
-
|
|
235
|
+
char prop[64];
|
|
236
|
+
snprintf(prop, sizeof(prop), "border-%s-width", side_str);
|
|
237
|
+
VALUE decl = rb_struct_new(cDeclaration, STR_NEW_CSTR(prop), width, Qfalse);
|
|
238
|
+
rb_ary_push(result, decl);
|
|
275
239
|
}
|
|
276
240
|
if (style != Qnil) {
|
|
277
|
-
char
|
|
278
|
-
snprintf(
|
|
279
|
-
|
|
241
|
+
char prop[64];
|
|
242
|
+
snprintf(prop, sizeof(prop), "border-%s-style", side_str);
|
|
243
|
+
VALUE decl = rb_struct_new(cDeclaration, STR_NEW_CSTR(prop), style, Qfalse);
|
|
244
|
+
rb_ary_push(result, decl);
|
|
280
245
|
}
|
|
281
246
|
if (color != Qnil) {
|
|
282
|
-
char
|
|
283
|
-
snprintf(
|
|
284
|
-
|
|
247
|
+
char prop[64];
|
|
248
|
+
snprintf(prop, sizeof(prop), "border-%s-color", side_str);
|
|
249
|
+
VALUE decl = rb_struct_new(cDeclaration, STR_NEW_CSTR(prop), color, Qfalse);
|
|
250
|
+
rb_ary_push(result, decl);
|
|
285
251
|
}
|
|
286
252
|
|
|
287
253
|
return result;
|
|
@@ -298,7 +264,6 @@ VALUE cataract_expand_font(VALUE self, VALUE value) {
|
|
|
298
264
|
const char *str = StringValueCStr(value);
|
|
299
265
|
const char *slash = strchr(str, '/');
|
|
300
266
|
|
|
301
|
-
VALUE result = rb_hash_new();
|
|
302
267
|
VALUE size_part, family_part;
|
|
303
268
|
VALUE line_height = Qnil;
|
|
304
269
|
|
|
@@ -431,12 +396,18 @@ VALUE cataract_expand_font(VALUE self, VALUE value) {
|
|
|
431
396
|
if (weight == Qnil) weight = STR_NEW_CSTR("normal");
|
|
432
397
|
if (line_height == Qnil) line_height = STR_NEW_CSTR("normal");
|
|
433
398
|
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
if (
|
|
399
|
+
// Create array of Declaration structs
|
|
400
|
+
VALUE result = rb_ary_new_capa(6);
|
|
401
|
+
rb_ary_push(result, rb_struct_new(cDeclaration, STR_NEW_CSTR("font-style"), style, Qfalse));
|
|
402
|
+
rb_ary_push(result, rb_struct_new(cDeclaration, STR_NEW_CSTR("font-variant"), variant, Qfalse));
|
|
403
|
+
rb_ary_push(result, rb_struct_new(cDeclaration, STR_NEW_CSTR("font-weight"), weight, Qfalse));
|
|
404
|
+
if (size != Qnil) {
|
|
405
|
+
rb_ary_push(result, rb_struct_new(cDeclaration, STR_NEW_CSTR("font-size"), size, Qfalse));
|
|
406
|
+
}
|
|
407
|
+
rb_ary_push(result, rb_struct_new(cDeclaration, STR_NEW_CSTR("line-height"), line_height, Qfalse));
|
|
408
|
+
if (family != Qnil) {
|
|
409
|
+
rb_ary_push(result, rb_struct_new(cDeclaration, STR_NEW_CSTR("font-family"), family, Qfalse));
|
|
410
|
+
}
|
|
440
411
|
|
|
441
412
|
return result;
|
|
442
413
|
}
|
|
@@ -447,7 +418,6 @@ VALUE cataract_expand_font(VALUE self, VALUE value) {
|
|
|
447
418
|
VALUE cataract_expand_list_style(VALUE self, VALUE value) {
|
|
448
419
|
VALUE parts = cataract_split_value(self, value);
|
|
449
420
|
long len = RARRAY_LEN(parts);
|
|
450
|
-
VALUE result = rb_hash_new();
|
|
451
421
|
|
|
452
422
|
const char *type_keywords[] = {"disc", "circle", "square", "decimal", "lower-roman", "upper-roman",
|
|
453
423
|
"lower-alpha", "upper-alpha", "none", NULL};
|
|
@@ -486,9 +456,17 @@ VALUE cataract_expand_list_style(VALUE self, VALUE value) {
|
|
|
486
456
|
}
|
|
487
457
|
}
|
|
488
458
|
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
if (
|
|
459
|
+
// Create array of Declaration structs
|
|
460
|
+
VALUE result = rb_ary_new_capa(3);
|
|
461
|
+
if (type != Qnil) {
|
|
462
|
+
rb_ary_push(result, rb_struct_new(cDeclaration, STR_NEW_CSTR("list-style-type"), type, Qfalse));
|
|
463
|
+
}
|
|
464
|
+
if (position != Qnil) {
|
|
465
|
+
rb_ary_push(result, rb_struct_new(cDeclaration, STR_NEW_CSTR("list-style-position"), position, Qfalse));
|
|
466
|
+
}
|
|
467
|
+
if (image != Qnil) {
|
|
468
|
+
rb_ary_push(result, rb_struct_new(cDeclaration, STR_NEW_CSTR("list-style-image"), image, Qfalse));
|
|
469
|
+
}
|
|
492
470
|
|
|
493
471
|
return result;
|
|
494
472
|
}
|
|
@@ -522,7 +500,6 @@ VALUE cataract_expand_background(VALUE self, VALUE value) {
|
|
|
522
500
|
|
|
523
501
|
VALUE parts = cataract_split_value(self, main_part);
|
|
524
502
|
long len = RARRAY_LEN(parts);
|
|
525
|
-
VALUE result = rb_hash_new();
|
|
526
503
|
|
|
527
504
|
// Color keywords (simplified list)
|
|
528
505
|
const char *color_keywords[] = {"red", "blue", "green", "white", "black", "yellow",
|
|
@@ -616,18 +593,20 @@ VALUE cataract_expand_background(VALUE self, VALUE value) {
|
|
|
616
593
|
|
|
617
594
|
// Background shorthand sets ALL longhand properties
|
|
618
595
|
// Unspecified values get CSS initial values (defaults)
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
596
|
+
// Create array of Declaration structs
|
|
597
|
+
VALUE result = rb_ary_new_capa(6);
|
|
598
|
+
rb_ary_push(result, rb_struct_new(cDeclaration, STR_NEW_CSTR("background-color"),
|
|
599
|
+
color != Qnil ? color : STR_NEW_CSTR("transparent"), Qfalse));
|
|
600
|
+
rb_ary_push(result, rb_struct_new(cDeclaration, STR_NEW_CSTR("background-image"),
|
|
601
|
+
image != Qnil ? image : STR_NEW_CSTR("none"), Qfalse));
|
|
602
|
+
rb_ary_push(result, rb_struct_new(cDeclaration, STR_NEW_CSTR("background-repeat"),
|
|
603
|
+
repeat != Qnil ? repeat : STR_NEW_CSTR("repeat"), Qfalse));
|
|
604
|
+
rb_ary_push(result, rb_struct_new(cDeclaration, STR_NEW_CSTR("background-attachment"),
|
|
605
|
+
attachment != Qnil ? attachment : STR_NEW_CSTR("scroll"), Qfalse));
|
|
606
|
+
rb_ary_push(result, rb_struct_new(cDeclaration, STR_NEW_CSTR("background-position"),
|
|
607
|
+
position != Qnil ? position : STR_NEW_CSTR("0% 0%"), Qfalse));
|
|
629
608
|
if (size != Qnil) {
|
|
630
|
-
|
|
609
|
+
rb_ary_push(result, rb_struct_new(cDeclaration, STR_NEW_CSTR("background-size"), size, Qfalse));
|
|
631
610
|
}
|
|
632
611
|
|
|
633
612
|
return result;
|
|
@@ -1013,3 +992,73 @@ VALUE cataract_create_list_style_shorthand(VALUE self, VALUE properties) {
|
|
|
1013
992
|
|
|
1014
993
|
return result;
|
|
1015
994
|
}
|
|
995
|
+
|
|
996
|
+
// Expand a single shorthand declaration into longhand declarations.
|
|
997
|
+
// Expand a single shorthand declaration into longhand declarations.
|
|
998
|
+
// Takes a Declaration struct, returns an array of Declaration structs.
|
|
999
|
+
// If the declaration is not a shorthand, returns array with just that declaration.
|
|
1000
|
+
VALUE cataract_expand_shorthand(VALUE self, VALUE decl) {
|
|
1001
|
+
// Extract property, value, important from Declaration struct
|
|
1002
|
+
VALUE property = rb_struct_aref(decl, INT2FIX(0)); // property
|
|
1003
|
+
VALUE value = rb_struct_aref(decl, INT2FIX(1)); // value
|
|
1004
|
+
VALUE important = rb_struct_aref(decl, INT2FIX(2)); // important
|
|
1005
|
+
|
|
1006
|
+
const char *prop = StringValueCStr(property);
|
|
1007
|
+
|
|
1008
|
+
// Early exit: shorthand properties only start with m, p, b, f, or l
|
|
1009
|
+
// margin, padding, border*, background, font, list-style
|
|
1010
|
+
char first_char = prop[0];
|
|
1011
|
+
if (first_char != 'm' && first_char != 'p' && first_char != 'b' &&
|
|
1012
|
+
first_char != 'f' && first_char != 'l') {
|
|
1013
|
+
// Not a shorthand - return array with original declaration
|
|
1014
|
+
VALUE result = rb_ary_new_capa(1);
|
|
1015
|
+
rb_ary_push(result, decl);
|
|
1016
|
+
return result;
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
VALUE expanded_hash = Qnil;
|
|
1020
|
+
|
|
1021
|
+
// Try to expand based on property name - return array of Declarations directly
|
|
1022
|
+
VALUE result = Qnil;
|
|
1023
|
+
|
|
1024
|
+
if (strcmp(prop, "margin") == 0) {
|
|
1025
|
+
VALUE parts = cataract_split_value(Qnil, value);
|
|
1026
|
+
result = expand_dimensions(parts, "margin", NULL, important);
|
|
1027
|
+
} else if (strcmp(prop, "padding") == 0) {
|
|
1028
|
+
VALUE parts = cataract_split_value(Qnil, value);
|
|
1029
|
+
result = expand_dimensions(parts, "padding", NULL, important);
|
|
1030
|
+
} else if (strcmp(prop, "border-color") == 0) {
|
|
1031
|
+
VALUE parts = cataract_split_value(Qnil, value);
|
|
1032
|
+
result = expand_dimensions(parts, "border", "color", important);
|
|
1033
|
+
} else if (strcmp(prop, "border-style") == 0) {
|
|
1034
|
+
VALUE parts = cataract_split_value(Qnil, value);
|
|
1035
|
+
result = expand_dimensions(parts, "border", "style", important);
|
|
1036
|
+
} else if (strcmp(prop, "border-width") == 0) {
|
|
1037
|
+
VALUE parts = cataract_split_value(Qnil, value);
|
|
1038
|
+
result = expand_dimensions(parts, "border", "width", important);
|
|
1039
|
+
} else if (strcmp(prop, "border") == 0) {
|
|
1040
|
+
result = cataract_expand_border(Qnil, value);
|
|
1041
|
+
} else if (strcmp(prop, "border-top") == 0) {
|
|
1042
|
+
result = cataract_expand_border_side(Qnil, STR_NEW_CSTR("top"), value);
|
|
1043
|
+
} else if (strcmp(prop, "border-right") == 0) {
|
|
1044
|
+
result = cataract_expand_border_side(Qnil, STR_NEW_CSTR("right"), value);
|
|
1045
|
+
} else if (strcmp(prop, "border-bottom") == 0) {
|
|
1046
|
+
result = cataract_expand_border_side(Qnil, STR_NEW_CSTR("bottom"), value);
|
|
1047
|
+
} else if (strcmp(prop, "border-left") == 0) {
|
|
1048
|
+
result = cataract_expand_border_side(Qnil, STR_NEW_CSTR("left"), value);
|
|
1049
|
+
} else if (strcmp(prop, "font") == 0) {
|
|
1050
|
+
result = cataract_expand_font(Qnil, value);
|
|
1051
|
+
} else if (strcmp(prop, "background") == 0) {
|
|
1052
|
+
result = cataract_expand_background(Qnil, value);
|
|
1053
|
+
} else if (strcmp(prop, "list-style") == 0) {
|
|
1054
|
+
result = cataract_expand_list_style(Qnil, value);
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
// If not a shorthand (or expansion failed), return array with original declaration
|
|
1058
|
+
if (NIL_P(result)) {
|
|
1059
|
+
result = rb_ary_new_capa(1);
|
|
1060
|
+
rb_ary_push(result, decl);
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
return result;
|
|
1064
|
+
}
|
|
@@ -10,6 +10,52 @@ module Cataract
|
|
|
10
10
|
# Resolves @import statements in CSS
|
|
11
11
|
# Handles fetching imported files and inlining them with proper security controls
|
|
12
12
|
module ImportResolver
|
|
13
|
+
# Default fetcher implementation using File I/O and Net::HTTP
|
|
14
|
+
# Can be replaced with custom fetchers for different environments (e.g., browser, caching)
|
|
15
|
+
class DefaultFetcher
|
|
16
|
+
# Fetch content from a URL
|
|
17
|
+
#
|
|
18
|
+
# @param url [String] URL to fetch
|
|
19
|
+
# @param options [Hash] Import resolution options
|
|
20
|
+
# @return [String] Fetched content
|
|
21
|
+
# @raise [ImportError] If fetching fails
|
|
22
|
+
def call(url, options)
|
|
23
|
+
uri = ImportResolver.normalize_url(url, options[:base_path])
|
|
24
|
+
|
|
25
|
+
case uri.scheme
|
|
26
|
+
when 'file'
|
|
27
|
+
# Read from local filesystem
|
|
28
|
+
File.read(uri.path)
|
|
29
|
+
when 'http', 'https'
|
|
30
|
+
# Fetch from network
|
|
31
|
+
fetch_http(uri, options)
|
|
32
|
+
else
|
|
33
|
+
raise ImportError, "Unsupported scheme: #{uri.scheme}"
|
|
34
|
+
end
|
|
35
|
+
rescue Errno::ENOENT
|
|
36
|
+
raise ImportError, "Import file not found: #{url}"
|
|
37
|
+
rescue OpenURI::HTTPError => e
|
|
38
|
+
raise ImportError, "HTTP error fetching import: #{url} (#{e.message})"
|
|
39
|
+
rescue SocketError => e
|
|
40
|
+
raise ImportError, "Network error fetching import: #{url} (#{e.message})"
|
|
41
|
+
rescue StandardError => e
|
|
42
|
+
raise ImportError, "Error fetching import: #{url} (#{e.class}: #{e.message})"
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
private
|
|
46
|
+
|
|
47
|
+
# Fetch content via HTTP/HTTPS
|
|
48
|
+
def fetch_http(uri, options)
|
|
49
|
+
# Use open-uri with timeout
|
|
50
|
+
open_uri_options = {
|
|
51
|
+
read_timeout: options[:timeout],
|
|
52
|
+
redirect: options[:follow_redirects]
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
# Use uri.open instead of URI.open to avoid shell command injection
|
|
56
|
+
uri.open(open_uri_options, &:read)
|
|
57
|
+
end
|
|
58
|
+
end
|
|
13
59
|
# Default options for safe import resolution
|
|
14
60
|
SAFE_DEFAULTS = {
|
|
15
61
|
max_depth: 5, # Prevent infinite recursion
|
|
@@ -17,13 +63,21 @@ module Cataract
|
|
|
17
63
|
extensions: ['css'], # Only .css files
|
|
18
64
|
timeout: 10, # 10 second timeout for fetches
|
|
19
65
|
follow_redirects: true, # Follow redirects
|
|
20
|
-
base_path: nil
|
|
66
|
+
base_path: nil, # Base path for resolving relative imports
|
|
67
|
+
fetcher: nil # Custom fetcher (defaults to DefaultFetcher)
|
|
21
68
|
}.freeze
|
|
22
69
|
|
|
23
70
|
# Resolve @import statements in CSS
|
|
24
71
|
#
|
|
25
72
|
# @param css [String] CSS content with @import statements
|
|
26
73
|
# @param options [Hash] Import resolution options
|
|
74
|
+
# @option options [#call] :fetcher Custom fetcher callable (receives url, options)
|
|
75
|
+
# @option options [Integer] :max_depth Maximum import nesting depth
|
|
76
|
+
# @option options [Array<String>] :allowed_schemes Allowed URL schemes
|
|
77
|
+
# @option options [Array<String>] :extensions Allowed file extensions
|
|
78
|
+
# @option options [Integer] :timeout HTTP request timeout in seconds
|
|
79
|
+
# @option options [Boolean] :follow_redirects Follow HTTP redirects
|
|
80
|
+
# @option options [String] :base_path Base path for relative imports
|
|
27
81
|
# @param depth [Integer] Current recursion depth (internal)
|
|
28
82
|
# @param imported_urls [Array] Array of already imported URLs to prevent circular references
|
|
29
83
|
# @return [String] CSS with imports inlined
|
|
@@ -31,6 +85,9 @@ module Cataract
|
|
|
31
85
|
# Normalize options
|
|
32
86
|
opts = normalize_options(options)
|
|
33
87
|
|
|
88
|
+
# Get or create fetcher
|
|
89
|
+
fetcher = opts[:fetcher] || DefaultFetcher.new
|
|
90
|
+
|
|
34
91
|
# Check recursion depth
|
|
35
92
|
# depth starts at 0, max_depth is count of imports allowed
|
|
36
93
|
# depth 0: parsing main file (counts as import 1)
|
|
@@ -60,8 +117,8 @@ module Cataract
|
|
|
60
117
|
# Check for circular references
|
|
61
118
|
raise ImportError, "Circular import detected: #{url}" if imported_urls.include?(url)
|
|
62
119
|
|
|
63
|
-
# Fetch imported CSS
|
|
64
|
-
imported_css =
|
|
120
|
+
# Fetch imported CSS using the fetcher
|
|
121
|
+
imported_css = fetcher.call(url, opts)
|
|
65
122
|
|
|
66
123
|
# Recursively resolve imports in the imported CSS
|
|
67
124
|
imported_urls_copy = imported_urls.dup
|
|
@@ -169,41 +226,5 @@ module Cataract
|
|
|
169
226
|
rescue URI::InvalidURIError => e
|
|
170
227
|
raise ImportError, "Invalid import URL: #{url} (#{e.message})"
|
|
171
228
|
end
|
|
172
|
-
|
|
173
|
-
# Fetch content from URL
|
|
174
|
-
def self.fetch_url(url, options)
|
|
175
|
-
uri = normalize_url(url, options[:base_path])
|
|
176
|
-
|
|
177
|
-
case uri.scheme
|
|
178
|
-
when 'file'
|
|
179
|
-
# Read from local filesystem
|
|
180
|
-
File.read(uri.path)
|
|
181
|
-
when 'http', 'https'
|
|
182
|
-
# Fetch from network
|
|
183
|
-
fetch_http(uri, options)
|
|
184
|
-
else
|
|
185
|
-
raise ImportError, "Unsupported scheme: #{uri.scheme}"
|
|
186
|
-
end
|
|
187
|
-
rescue Errno::ENOENT
|
|
188
|
-
raise ImportError, "Import file not found: #{url}"
|
|
189
|
-
rescue OpenURI::HTTPError => e
|
|
190
|
-
raise ImportError, "HTTP error fetching import: #{url} (#{e.message})"
|
|
191
|
-
rescue SocketError => e
|
|
192
|
-
raise ImportError, "Network error fetching import: #{url} (#{e.message})"
|
|
193
|
-
rescue StandardError => e
|
|
194
|
-
raise ImportError, "Error fetching import: #{url} (#{e.class}: #{e.message})"
|
|
195
|
-
end
|
|
196
|
-
|
|
197
|
-
# Fetch content via HTTP/HTTPS
|
|
198
|
-
def self.fetch_http(uri, options)
|
|
199
|
-
# Use open-uri with timeout
|
|
200
|
-
open_uri_options = {
|
|
201
|
-
read_timeout: options[:timeout],
|
|
202
|
-
redirect: options[:follow_redirects]
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
# Use uri.open instead of URI.open to avoid shell command injection
|
|
206
|
-
uri.open(open_uri_options, &:read)
|
|
207
|
-
end
|
|
208
229
|
end
|
|
209
230
|
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Cataract
|
|
4
|
+
# Represents a CSS @import statement
|
|
5
|
+
#
|
|
6
|
+
# @import statements are parsed and stored separately in the stylesheet's @_imports array.
|
|
7
|
+
# They can later be resolved by the ImportResolver to fetch and inline the imported CSS.
|
|
8
|
+
#
|
|
9
|
+
# Per CSS spec, @import must appear before all rules except @charset and @layer.
|
|
10
|
+
# Any @import that appears after a style rule is invalid and will be ignored with a warning.
|
|
11
|
+
#
|
|
12
|
+
# @example Basic import
|
|
13
|
+
# @import "styles.css";
|
|
14
|
+
# # => ImportStatement(url: "styles.css", media: nil)
|
|
15
|
+
#
|
|
16
|
+
# @example Import with media query
|
|
17
|
+
# @import "mobile.css" screen and (max-width: 768px);
|
|
18
|
+
# # => ImportStatement(url: "mobile.css", media: :"screen and (max-width: 768px)")
|
|
19
|
+
#
|
|
20
|
+
# @attr [Integer] id The import's position in the source (0-indexed)
|
|
21
|
+
# @attr [String] url The URL to import (without quotes or url() wrapper)
|
|
22
|
+
# @attr [Symbol, nil] media The media query as a symbol, or nil if no media query
|
|
23
|
+
# @attr [Boolean] resolved Whether this import has been resolved/processed
|
|
24
|
+
ImportStatement = Struct.new(:id, :url, :media, :resolved) unless const_defined?(:ImportStatement)
|
|
25
|
+
|
|
26
|
+
class ImportStatement
|
|
27
|
+
# Compare two ImportStatement objects for equality.
|
|
28
|
+
# Two imports are equal if they have the same URL and media query.
|
|
29
|
+
# The ID is ignored as it's an implementation detail.
|
|
30
|
+
#
|
|
31
|
+
# @param other [Object] Object to compare with
|
|
32
|
+
# @return [Boolean] true if equal, false otherwise
|
|
33
|
+
def ==(other)
|
|
34
|
+
return false unless other.is_a?(ImportStatement)
|
|
35
|
+
|
|
36
|
+
url == other.url && media == other.media
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
alias eql? ==
|
|
40
|
+
|
|
41
|
+
# Generate hash code for ImportStatement.
|
|
42
|
+
# Uses URL and media query (ignores ID).
|
|
43
|
+
#
|
|
44
|
+
# @return [Integer] Hash code
|
|
45
|
+
def hash
|
|
46
|
+
[url, media].hash
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|