cataract 0.1.3 → 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.
Files changed (58) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci-manual-rubies.yml +44 -0
  3. data/.overcommit.yml +1 -1
  4. data/.rubocop.yml +96 -4
  5. data/.rubocop_todo.yml +186 -0
  6. data/BENCHMARKS.md +62 -141
  7. data/CHANGELOG.md +20 -0
  8. data/RAGEL_MIGRATION.md +2 -2
  9. data/README.md +37 -4
  10. data/Rakefile +72 -32
  11. data/cataract.gemspec +4 -1
  12. data/ext/cataract/cataract.c +59 -50
  13. data/ext/cataract/cataract.h +5 -3
  14. data/ext/cataract/css_parser.c +173 -65
  15. data/ext/cataract/extconf.rb +2 -2
  16. data/ext/cataract/{merge.c → flatten.c} +526 -468
  17. data/ext/cataract/shorthand_expander.c +164 -115
  18. data/lib/cataract/at_rule.rb +8 -9
  19. data/lib/cataract/declaration.rb +18 -0
  20. data/lib/cataract/import_resolver.rb +63 -43
  21. data/lib/cataract/import_statement.rb +49 -0
  22. data/lib/cataract/pure/byte_constants.rb +69 -0
  23. data/lib/cataract/pure/flatten.rb +1145 -0
  24. data/lib/cataract/pure/helpers.rb +35 -0
  25. data/lib/cataract/pure/imports.rb +268 -0
  26. data/lib/cataract/pure/parser.rb +1340 -0
  27. data/lib/cataract/pure/serializer.rb +590 -0
  28. data/lib/cataract/pure/specificity.rb +206 -0
  29. data/lib/cataract/pure.rb +153 -0
  30. data/lib/cataract/rule.rb +69 -15
  31. data/lib/cataract/stylesheet.rb +356 -49
  32. data/lib/cataract/version.rb +1 -1
  33. data/lib/cataract.rb +43 -26
  34. metadata +14 -26
  35. data/benchmarks/benchmark_harness.rb +0 -193
  36. data/benchmarks/benchmark_merging.rb +0 -121
  37. data/benchmarks/benchmark_optimization_comparison.rb +0 -168
  38. data/benchmarks/benchmark_parsing.rb +0 -153
  39. data/benchmarks/benchmark_ragel_removal.rb +0 -56
  40. data/benchmarks/benchmark_runner.rb +0 -70
  41. data/benchmarks/benchmark_serialization.rb +0 -180
  42. data/benchmarks/benchmark_shorthand.rb +0 -109
  43. data/benchmarks/benchmark_shorthand_expansion.rb +0 -176
  44. data/benchmarks/benchmark_specificity.rb +0 -124
  45. data/benchmarks/benchmark_string_allocation.rb +0 -151
  46. data/benchmarks/benchmark_stylesheet_to_s.rb +0 -62
  47. data/benchmarks/benchmark_to_s_cached.rb +0 -55
  48. data/benchmarks/benchmark_value_splitter.rb +0 -54
  49. data/benchmarks/benchmark_yjit.rb +0 -158
  50. data/benchmarks/benchmark_yjit_workers.rb +0 -61
  51. data/benchmarks/profile_to_s.rb +0 -23
  52. data/benchmarks/speedup_calculator.rb +0 -83
  53. data/benchmarks/system_metadata.rb +0 -81
  54. data/benchmarks/templates/benchmarks.md.erb +0 -221
  55. data/benchmarks/yjit_tests.rb +0 -141
  56. data/scripts/fuzzer/run.rb +0 -828
  57. data/scripts/fuzzer/worker.rb +0 -99
  58. data/scripts/generate_benchmarks_md.rb +0 -155
@@ -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
- VALUE result = rb_hash_new();
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 result; // Invalid
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 key[128];
53
+ char prop_name[128];
99
54
  if (suffix) {
100
- snprintf(key, sizeof(key), "%s-%s-%s", property, side_names[i], suffix);
55
+ snprintf(prop_name, sizeof(prop_name), "%s-%s-%s", property, side_names[i], suffix);
101
56
  } else {
102
- snprintf(key, sizeof(key), "%s-%s", property, side_names[i]);
57
+ snprintf(prop_name, sizeof(prop_name), "%s-%s", property, side_names[i]);
103
58
  }
104
59
 
105
- // Append !important if needed
106
- VALUE final_value;
107
- if (is_important) {
108
- const char *val = StringValueCStr(sides[i]);
109
- char buf[256];
110
- snprintf(buf, sizeof(buf), "%s !important", val);
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 key[64];
214
- snprintf(key, sizeof(key), "border-%s-width", sides[i]);
215
- rb_hash_aset(result, STR_NEW_CSTR(key), width);
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 key[64];
219
- snprintf(key, sizeof(key), "border-%s-style", sides[i]);
220
- rb_hash_aset(result, STR_NEW_CSTR(key), style);
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 key[64];
224
- snprintf(key, sizeof(key), "border-%s-color", sides[i]);
225
- rb_hash_aset(result, STR_NEW_CSTR(key), color);
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 key[64];
273
- snprintf(key, sizeof(key), "border-%s-width", side_str);
274
- rb_hash_aset(result, STR_NEW_CSTR(key), width);
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 key[64];
278
- snprintf(key, sizeof(key), "border-%s-style", side_str);
279
- rb_hash_aset(result, STR_NEW_CSTR(key), style);
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 key[64];
283
- snprintf(key, sizeof(key), "border-%s-color", side_str);
284
- rb_hash_aset(result, STR_NEW_CSTR(key), color);
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
- rb_hash_aset(result, STR_NEW_CSTR("font-style"), style);
435
- rb_hash_aset(result, STR_NEW_CSTR("font-variant"), variant);
436
- rb_hash_aset(result, STR_NEW_CSTR("font-weight"), weight);
437
- if (size != Qnil) rb_hash_aset(result, STR_NEW_CSTR("font-size"), size);
438
- rb_hash_aset(result, STR_NEW_CSTR("line-height"), line_height);
439
- if (family != Qnil) rb_hash_aset(result, STR_NEW_CSTR("font-family"), family);
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
- if (type != Qnil) rb_hash_aset(result, STR_NEW_CSTR("list-style-type"), type);
490
- if (position != Qnil) rb_hash_aset(result, STR_NEW_CSTR("list-style-position"), position);
491
- if (image != Qnil) rb_hash_aset(result, STR_NEW_CSTR("list-style-image"), image);
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
- rb_hash_aset(result, STR_NEW_CSTR("background-color"),
620
- color != Qnil ? color : STR_NEW_CSTR("transparent"));
621
- rb_hash_aset(result, STR_NEW_CSTR("background-image"),
622
- image != Qnil ? image : STR_NEW_CSTR("none"));
623
- rb_hash_aset(result, STR_NEW_CSTR("background-repeat"),
624
- repeat != Qnil ? repeat : STR_NEW_CSTR("repeat"));
625
- rb_hash_aset(result, STR_NEW_CSTR("background-attachment"),
626
- attachment != Qnil ? attachment : STR_NEW_CSTR("scroll"));
627
- rb_hash_aset(result, STR_NEW_CSTR("background-position"),
628
- position != Qnil ? position : STR_NEW_CSTR("0% 0%"));
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
- rb_hash_aset(result, STR_NEW_CSTR("background-size"), size);
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
+ }
@@ -1,10 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Cataract
4
- # Represents a CSS at-rule like @keyframes, @font-face, @supports, etc.
5
- #
6
- # AtRule is a C struct defined as: `Struct.new(:id, :selector, :content, :specificity)`
7
- #
8
4
  # At-rules define CSS resources or control structures rather than selecting elements.
9
5
  # Unlike regular rules, they don't have CSS specificity and are filtered out when
10
6
  # using `select(&:selector?)`.
@@ -32,6 +28,8 @@ module Cataract
32
28
  # @attr [String] selector The at-rule identifier (e.g., "@keyframes fade", "@font-face")
33
29
  # @attr [Array<Rule>, Array<Declaration>] content Nested rules or declarations
34
30
  # @attr [nil] specificity Always nil for at-rules (they don't have CSS specificity)
31
+ AtRule = Struct.new(:id, :selector, :content, :specificity) unless const_defined?(:AtRule)
32
+
35
33
  class AtRule
36
34
  # Check if this is a selector-based rule (vs an at-rule like @keyframes).
37
35
  #
@@ -79,17 +77,18 @@ module Cataract
79
77
  false
80
78
  end
81
79
 
82
- # Compare at-rules by their attributes rather than object identity.
80
+ # Compare at-rules for logical equality based on CSS semantics.
83
81
  #
84
- # Two at-rules are equal if they have the same id, selector, and content.
82
+ # Two at-rules are equal if they have the same selector and content.
83
+ # Internal implementation details (id) are not considered since they
84
+ # don't affect the CSS semantics.
85
85
  #
86
86
  # @param other [Object] Object to compare with
87
- # @return [Boolean] true if at-rules have same attributes
87
+ # @return [Boolean] true if at-rules have same selector and content
88
88
  def ==(other)
89
89
  return false unless other.is_a?(AtRule)
90
90
 
91
- id == other.id &&
92
- selector == other.selector &&
91
+ selector == other.selector &&
93
92
  content == other.content
94
93
  end
95
94
  alias eql? ==
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Cataract
4
+ # Represents a CSS property declaration.
5
+ #
6
+ # Declaration is a Struct with fields: (property, value, important)
7
+ #
8
+ # @example Create a declaration
9
+ # decl = Cataract::Declaration.new('color', 'red', false)
10
+ # decl.property #=> "color"
11
+ # decl.value #=> "red"
12
+ # decl.important #=> false
13
+ #
14
+ # @attr [String] property CSS property name (lowercased)
15
+ # @attr [String] value CSS property value
16
+ # @attr [Boolean] important Whether the declaration has !important
17
+ Declaration = Struct.new(:property, :value, :important) unless const_defined?(:Declaration)
18
+ end
@@ -2,7 +2,6 @@
2
2
 
3
3
  require 'uri'
4
4
  require 'open-uri'
5
- require 'set'
6
5
 
7
6
  module Cataract
8
7
  # Error raised during import resolution
@@ -11,6 +10,52 @@ module Cataract
11
10
  # Resolves @import statements in CSS
12
11
  # Handles fetching imported files and inlining them with proper security controls
13
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
14
59
  # Default options for safe import resolution
15
60
  SAFE_DEFAULTS = {
16
61
  max_depth: 5, # Prevent infinite recursion
@@ -18,20 +63,31 @@ module Cataract
18
63
  extensions: ['css'], # Only .css files
19
64
  timeout: 10, # 10 second timeout for fetches
20
65
  follow_redirects: true, # Follow redirects
21
- base_path: nil # Base path for resolving relative imports
66
+ base_path: nil, # Base path for resolving relative imports
67
+ fetcher: nil # Custom fetcher (defaults to DefaultFetcher)
22
68
  }.freeze
23
69
 
24
70
  # Resolve @import statements in CSS
25
71
  #
26
72
  # @param css [String] CSS content with @import statements
27
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
28
81
  # @param depth [Integer] Current recursion depth (internal)
29
- # @param imported_urls [Set] Set of already imported URLs to prevent circular references
82
+ # @param imported_urls [Array] Array of already imported URLs to prevent circular references
30
83
  # @return [String] CSS with imports inlined
31
- def self.resolve(css, options = {}, depth: 0, imported_urls: Set.new)
84
+ def self.resolve(css, options = {}, depth: 0, imported_urls: [])
32
85
  # Normalize options
33
86
  opts = normalize_options(options)
34
87
 
88
+ # Get or create fetcher
89
+ fetcher = opts[:fetcher] || DefaultFetcher.new
90
+
35
91
  # Check recursion depth
36
92
  # depth starts at 0, max_depth is count of imports allowed
37
93
  # depth 0: parsing main file (counts as import 1)
@@ -61,12 +117,12 @@ module Cataract
61
117
  # Check for circular references
62
118
  raise ImportError, "Circular import detected: #{url}" if imported_urls.include?(url)
63
119
 
64
- # Fetch imported CSS
65
- imported_css = fetch_url(url, opts)
120
+ # Fetch imported CSS using the fetcher
121
+ imported_css = fetcher.call(url, opts)
66
122
 
67
123
  # Recursively resolve imports in the imported CSS
68
124
  imported_urls_copy = imported_urls.dup
69
- imported_urls_copy.add(url)
125
+ imported_urls_copy << url
70
126
  imported_css = resolve(imported_css, opts, depth: depth + 1, imported_urls: imported_urls_copy)
71
127
 
72
128
  # Wrap in @media if import had media query
@@ -170,41 +226,5 @@ module Cataract
170
226
  rescue URI::InvalidURIError => e
171
227
  raise ImportError, "Invalid import URL: #{url} (#{e.message})"
172
228
  end
173
-
174
- # Fetch content from URL
175
- def self.fetch_url(url, options)
176
- uri = normalize_url(url, options[:base_path])
177
-
178
- case uri.scheme
179
- when 'file'
180
- # Read from local filesystem
181
- File.read(uri.path)
182
- when 'http', 'https'
183
- # Fetch from network
184
- fetch_http(uri, options)
185
- else
186
- raise ImportError, "Unsupported scheme: #{uri.scheme}"
187
- end
188
- rescue Errno::ENOENT
189
- raise ImportError, "Import file not found: #{url}"
190
- rescue OpenURI::HTTPError => e
191
- raise ImportError, "HTTP error fetching import: #{url} (#{e.message})"
192
- rescue SocketError => e
193
- raise ImportError, "Network error fetching import: #{url} (#{e.message})"
194
- rescue StandardError => e
195
- raise ImportError, "Error fetching import: #{url} (#{e.class}: #{e.message})"
196
- end
197
-
198
- # Fetch content via HTTP/HTTPS
199
- def self.fetch_http(uri, options)
200
- # Use open-uri with timeout
201
- open_uri_options = {
202
- read_timeout: options[:timeout],
203
- redirect: options[:follow_redirects]
204
- }
205
-
206
- # Use uri.open instead of URI.open to avoid shell command injection
207
- uri.open(open_uri_options, &:read)
208
- end
209
229
  end
210
230
  end