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.
@@ -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
+ }
@@ -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 # Base path for resolving relative imports
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 = fetch_url(url, opts)
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