cataract 0.2.4 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/test.yml +6 -6
  3. data/.gitignore +17 -3
  4. data/.rubocop.yml +1 -0
  5. data/BENCHMARKS.md +24 -6
  6. data/CHANGELOG.md +29 -1
  7. data/Gemfile +2 -2
  8. data/examples/css_analyzer/analyzer.rb +9 -3
  9. data/examples/css_analyzer/analyzers/base.rb +1 -1
  10. data/ext/cataract/cataract.c +251 -458
  11. data/ext/cataract/cataract.h +127 -16
  12. data/ext/cataract/css_parser.c +1029 -700
  13. data/ext/cataract/extconf.rb +1 -2
  14. data/ext/cataract/flatten.c +279 -991
  15. data/ext/cataract/shorthand_expander.c +80 -102
  16. data/ext/cataract_color/color_conversion.c +1 -1
  17. data/lib/cataract/declarations.rb +13 -7
  18. data/lib/cataract/error.rb +49 -0
  19. data/lib/cataract/import_resolver.rb +36 -16
  20. data/lib/cataract/pure/byte_constants.rb +4 -1
  21. data/lib/cataract/pure/flatten.rb +143 -145
  22. data/lib/cataract/pure/parser.rb +623 -388
  23. data/lib/cataract/pure/serializer.rb +109 -104
  24. data/lib/cataract/pure/specificity.rb +112 -156
  25. data/lib/cataract/pure.rb +1 -6
  26. data/lib/cataract/stylesheet.rb +480 -410
  27. data/lib/cataract/version.rb +1 -1
  28. data/lib/cataract.rb +1 -0
  29. metadata +3 -13
  30. data/ext/cataract/import_scanner.c +0 -174
  31. data/ext/cataract_old/cataract.c +0 -393
  32. data/ext/cataract_old/cataract.h +0 -250
  33. data/ext/cataract_old/css_parser.c +0 -933
  34. data/ext/cataract_old/extconf.rb +0 -67
  35. data/ext/cataract_old/import_scanner.c +0 -174
  36. data/ext/cataract_old/merge.c +0 -776
  37. data/ext/cataract_old/shorthand_expander.c +0 -902
  38. data/ext/cataract_old/specificity.c +0 -213
  39. data/ext/cataract_old/stylesheet.c +0 -290
  40. data/ext/cataract_old/value_splitter.c +0 -116
@@ -72,45 +72,45 @@ static VALUE expand_dimensions(VALUE parts, const char *property, const char *su
72
72
  * Expand margin shorthand: "10px 20px 30px 40px"
73
73
  * Returns array of Declaration structs
74
74
  */
75
- VALUE cataract_expand_margin(VALUE self, VALUE value) {
75
+ VALUE cataract_expand_margin(VALUE self, VALUE value, VALUE important) {
76
76
  VALUE parts = cataract_split_value(self, value);
77
- return expand_dimensions(parts, "margin", NULL, Qfalse);
77
+ return expand_dimensions(parts, "margin", NULL, important);
78
78
  }
79
79
 
80
80
  /*
81
81
  * Expand padding shorthand: "10px 20px 30px 40px"
82
82
  * Returns array of Declaration structs
83
83
  */
84
- VALUE cataract_expand_padding(VALUE self, VALUE value) {
84
+ VALUE cataract_expand_padding(VALUE self, VALUE value, VALUE important) {
85
85
  VALUE parts = cataract_split_value(self, value);
86
- return expand_dimensions(parts, "padding", NULL, Qfalse);
86
+ return expand_dimensions(parts, "padding", NULL, important);
87
87
  }
88
88
 
89
89
  /*
90
90
  * Expand border-color shorthand: "red green blue yellow"
91
91
  * Returns array of Declaration structs
92
92
  */
93
- VALUE cataract_expand_border_color(VALUE self, VALUE value) {
93
+ VALUE cataract_expand_border_color(VALUE self, VALUE value, VALUE important) {
94
94
  VALUE parts = cataract_split_value(self, value);
95
- return expand_dimensions(parts, "border", "color", Qfalse);
95
+ return expand_dimensions(parts, "border", "color", important);
96
96
  }
97
97
 
98
98
  /*
99
99
  * Expand border-style shorthand: "solid dashed dotted double"
100
100
  * Returns array of Declaration structs
101
101
  */
102
- VALUE cataract_expand_border_style(VALUE self, VALUE value) {
102
+ VALUE cataract_expand_border_style(VALUE self, VALUE value, VALUE important) {
103
103
  VALUE parts = cataract_split_value(self, value);
104
- return expand_dimensions(parts, "border", "style", Qfalse);
104
+ return expand_dimensions(parts, "border", "style", important);
105
105
  }
106
106
 
107
107
  /*
108
108
  * Expand border-width shorthand: "1px 2px 3px 4px"
109
109
  * Returns array of Declaration structs
110
110
  */
111
- VALUE cataract_expand_border_width(VALUE self, VALUE value) {
111
+ VALUE cataract_expand_border_width(VALUE self, VALUE value, VALUE important) {
112
112
  VALUE parts = cataract_split_value(self, value);
113
- return expand_dimensions(parts, "border", "width", Qfalse);
113
+ return expand_dimensions(parts, "border", "width", important);
114
114
  }
115
115
 
116
116
  /*
@@ -140,7 +140,7 @@ static int is_border_style(const char *str) {
140
140
  * Expand border shorthand: "1px solid red"
141
141
  * Returns array of Declaration structs (up to 12: 4 sides × 3 properties)
142
142
  */
143
- VALUE cataract_expand_border(VALUE self, VALUE value) {
143
+ VALUE cataract_expand_border(VALUE self, VALUE value, VALUE important) {
144
144
  VALUE parts = cataract_split_value(self, value);
145
145
  long len = RARRAY_LEN(parts);
146
146
 
@@ -169,19 +169,19 @@ VALUE cataract_expand_border(VALUE self, VALUE value) {
169
169
  if (width != Qnil) {
170
170
  char prop[64];
171
171
  snprintf(prop, sizeof(prop), "border-%s-width", sides[i]);
172
- VALUE decl = rb_struct_new(cDeclaration, STR_NEW_CSTR(prop), width, Qfalse);
172
+ VALUE decl = rb_struct_new(cDeclaration, STR_NEW_CSTR(prop), width, important);
173
173
  rb_ary_push(result, decl);
174
174
  }
175
175
  if (style != Qnil) {
176
176
  char prop[64];
177
177
  snprintf(prop, sizeof(prop), "border-%s-style", sides[i]);
178
- VALUE decl = rb_struct_new(cDeclaration, STR_NEW_CSTR(prop), style, Qfalse);
178
+ VALUE decl = rb_struct_new(cDeclaration, STR_NEW_CSTR(prop), style, important);
179
179
  rb_ary_push(result, decl);
180
180
  }
181
181
  if (color != Qnil) {
182
182
  char prop[64];
183
183
  snprintf(prop, sizeof(prop), "border-%s-color", sides[i]);
184
- VALUE decl = rb_struct_new(cDeclaration, STR_NEW_CSTR(prop), color, Qfalse);
184
+ VALUE decl = rb_struct_new(cDeclaration, STR_NEW_CSTR(prop), color, important);
185
185
  rb_ary_push(result, decl);
186
186
  }
187
187
  }
@@ -193,7 +193,7 @@ VALUE cataract_expand_border(VALUE self, VALUE value) {
193
193
  * Expand border-{side} shorthand: "2px dashed blue"
194
194
  * Returns array of Declaration structs (up to 3: width, style, color)
195
195
  */
196
- VALUE cataract_expand_border_side(VALUE self, VALUE side, VALUE value) {
196
+ VALUE cataract_expand_border_side(VALUE self, VALUE side, VALUE value, VALUE important) {
197
197
  VALUE parts = cataract_split_value(self, value);
198
198
  long len = RARRAY_LEN(parts);
199
199
  const char *side_str = StringValueCStr(side);
@@ -234,19 +234,19 @@ VALUE cataract_expand_border_side(VALUE self, VALUE side, VALUE value) {
234
234
  if (width != Qnil) {
235
235
  char prop[64];
236
236
  snprintf(prop, sizeof(prop), "border-%s-width", side_str);
237
- VALUE decl = rb_struct_new(cDeclaration, STR_NEW_CSTR(prop), width, Qfalse);
237
+ VALUE decl = rb_struct_new(cDeclaration, STR_NEW_CSTR(prop), width, important);
238
238
  rb_ary_push(result, decl);
239
239
  }
240
240
  if (style != Qnil) {
241
241
  char prop[64];
242
242
  snprintf(prop, sizeof(prop), "border-%s-style", side_str);
243
- VALUE decl = rb_struct_new(cDeclaration, STR_NEW_CSTR(prop), style, Qfalse);
243
+ VALUE decl = rb_struct_new(cDeclaration, STR_NEW_CSTR(prop), style, important);
244
244
  rb_ary_push(result, decl);
245
245
  }
246
246
  if (color != Qnil) {
247
247
  char prop[64];
248
248
  snprintf(prop, sizeof(prop), "border-%s-color", side_str);
249
- VALUE decl = rb_struct_new(cDeclaration, STR_NEW_CSTR(prop), color, Qfalse);
249
+ VALUE decl = rb_struct_new(cDeclaration, STR_NEW_CSTR(prop), color, important);
250
250
  rb_ary_push(result, decl);
251
251
  }
252
252
 
@@ -258,7 +258,7 @@ VALUE cataract_expand_border_side(VALUE self, VALUE side, VALUE value) {
258
258
  * Font syntax: [style] [variant] [weight] [size]/[line-height] [family]
259
259
  * Only size and family are required
260
260
  */
261
- VALUE cataract_expand_font(VALUE self, VALUE value) {
261
+ VALUE cataract_expand_font(VALUE self, VALUE value, VALUE important) {
262
262
  // Font is complex - need to handle / separator for line-height
263
263
  // Split on / first to separate size from line-height
264
264
  const char *str = StringValueCStr(value);
@@ -398,15 +398,15 @@ VALUE cataract_expand_font(VALUE self, VALUE value) {
398
398
 
399
399
  // Create array of Declaration structs
400
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));
401
+ rb_ary_push(result, rb_struct_new(cDeclaration, STR_NEW_CSTR("font-style"), style, important));
402
+ rb_ary_push(result, rb_struct_new(cDeclaration, STR_NEW_CSTR("font-variant"), variant, important));
403
+ rb_ary_push(result, rb_struct_new(cDeclaration, STR_NEW_CSTR("font-weight"), weight, important));
404
404
  if (size != Qnil) {
405
- rb_ary_push(result, rb_struct_new(cDeclaration, STR_NEW_CSTR("font-size"), size, Qfalse));
405
+ rb_ary_push(result, rb_struct_new(cDeclaration, STR_NEW_CSTR("font-size"), size, important));
406
406
  }
407
- rb_ary_push(result, rb_struct_new(cDeclaration, STR_NEW_CSTR("line-height"), line_height, Qfalse));
407
+ rb_ary_push(result, rb_struct_new(cDeclaration, STR_NEW_CSTR("line-height"), line_height, important));
408
408
  if (family != Qnil) {
409
- rb_ary_push(result, rb_struct_new(cDeclaration, STR_NEW_CSTR("font-family"), family, Qfalse));
409
+ rb_ary_push(result, rb_struct_new(cDeclaration, STR_NEW_CSTR("font-family"), family, important));
410
410
  }
411
411
 
412
412
  return result;
@@ -415,7 +415,7 @@ VALUE cataract_expand_font(VALUE self, VALUE value) {
415
415
  /*
416
416
  * Expand list-style shorthand: "square inside"
417
417
  */
418
- VALUE cataract_expand_list_style(VALUE self, VALUE value) {
418
+ VALUE cataract_expand_list_style(VALUE self, VALUE value, VALUE important) {
419
419
  VALUE parts = cataract_split_value(self, value);
420
420
  long len = RARRAY_LEN(parts);
421
421
 
@@ -459,13 +459,13 @@ VALUE cataract_expand_list_style(VALUE self, VALUE value) {
459
459
  // Create array of Declaration structs
460
460
  VALUE result = rb_ary_new_capa(3);
461
461
  if (type != Qnil) {
462
- rb_ary_push(result, rb_struct_new(cDeclaration, STR_NEW_CSTR("list-style-type"), type, Qfalse));
462
+ rb_ary_push(result, rb_struct_new(cDeclaration, STR_NEW_CSTR("list-style-type"), type, important));
463
463
  }
464
464
  if (position != Qnil) {
465
- rb_ary_push(result, rb_struct_new(cDeclaration, STR_NEW_CSTR("list-style-position"), position, Qfalse));
465
+ rb_ary_push(result, rb_struct_new(cDeclaration, STR_NEW_CSTR("list-style-position"), position, important));
466
466
  }
467
467
  if (image != Qnil) {
468
- rb_ary_push(result, rb_struct_new(cDeclaration, STR_NEW_CSTR("list-style-image"), image, Qfalse));
468
+ rb_ary_push(result, rb_struct_new(cDeclaration, STR_NEW_CSTR("list-style-image"), image, important));
469
469
  }
470
470
 
471
471
  return result;
@@ -475,7 +475,7 @@ VALUE cataract_expand_list_style(VALUE self, VALUE value) {
475
475
  * Expand background shorthand: "url(img.png) no-repeat center / cover"
476
476
  * This is complex - background has many sub-properties and / separator for size
477
477
  */
478
- VALUE cataract_expand_background(VALUE self, VALUE value) {
478
+ VALUE cataract_expand_background(VALUE self, VALUE value, VALUE important) {
479
479
  DEBUG_PRINTF("[cataract_expand_background] input value: '%s'\n", RSTRING_PTR(value));
480
480
 
481
481
  // First, check if there's a / separator for background-size
@@ -596,17 +596,17 @@ VALUE cataract_expand_background(VALUE self, VALUE value) {
596
596
  // Create array of Declaration structs
597
597
  VALUE result = rb_ary_new_capa(6);
598
598
  rb_ary_push(result, rb_struct_new(cDeclaration, STR_NEW_CSTR("background-color"),
599
- color != Qnil ? color : STR_NEW_CSTR("transparent"), Qfalse));
599
+ color != Qnil ? color : STR_NEW_CSTR("transparent"), important));
600
600
  rb_ary_push(result, rb_struct_new(cDeclaration, STR_NEW_CSTR("background-image"),
601
- image != Qnil ? image : STR_NEW_CSTR("none"), Qfalse));
601
+ image != Qnil ? image : STR_NEW_CSTR("none"), important));
602
602
  rb_ary_push(result, rb_struct_new(cDeclaration, STR_NEW_CSTR("background-repeat"),
603
- repeat != Qnil ? repeat : STR_NEW_CSTR("repeat"), Qfalse));
603
+ repeat != Qnil ? repeat : STR_NEW_CSTR("repeat"), important));
604
604
  rb_ary_push(result, rb_struct_new(cDeclaration, STR_NEW_CSTR("background-attachment"),
605
- attachment != Qnil ? attachment : STR_NEW_CSTR("scroll"), Qfalse));
605
+ attachment != Qnil ? attachment : STR_NEW_CSTR("scroll"), important));
606
606
  rb_ary_push(result, rb_struct_new(cDeclaration, STR_NEW_CSTR("background-position"),
607
- position != Qnil ? position : STR_NEW_CSTR("0% 0%"), Qfalse));
607
+ position != Qnil ? position : STR_NEW_CSTR("0% 0%"), important));
608
608
  if (size != Qnil) {
609
- rb_ary_push(result, rb_struct_new(cDeclaration, STR_NEW_CSTR("background-size"), size, Qfalse));
609
+ rb_ary_push(result, rb_struct_new(cDeclaration, STR_NEW_CSTR("background-size"), size, important));
610
610
  }
611
611
 
612
612
  return result;
@@ -616,26 +616,10 @@ VALUE cataract_expand_background(VALUE self, VALUE value) {
616
616
  // SHORTHAND CREATION (Inverse of expansion)
617
617
  // ============================================================================
618
618
 
619
- // Helper: Create dimension shorthand (margin or padding)
620
- // Input: hash with "#{base}-top", "#{base}-right", "#{base}-bottom", "#{base}-left"
621
- // Output: optimized shorthand string, or Qnil if not all sides present
622
- static VALUE create_dimension_shorthand(VALUE properties, const char *base) {
623
- char key_top[32], key_right[32], key_bottom[32], key_left[32];
624
- snprintf(key_top, sizeof(key_top), "%s-top", base);
625
- snprintf(key_right, sizeof(key_right), "%s-right", base);
626
- snprintf(key_bottom, sizeof(key_bottom), "%s-bottom", base);
627
- snprintf(key_left, sizeof(key_left), "%s-left", base);
628
-
629
- VALUE top = rb_hash_aref(properties, STR_NEW_CSTR(key_top));
630
- VALUE right = rb_hash_aref(properties, STR_NEW_CSTR(key_right));
631
- VALUE bottom = rb_hash_aref(properties, STR_NEW_CSTR(key_bottom));
632
- VALUE left = rb_hash_aref(properties, STR_NEW_CSTR(key_left));
633
-
634
- // All four sides must be present
635
- if (NIL_P(top) || NIL_P(right) || NIL_P(bottom) || NIL_P(left)) {
636
- return Qnil;
637
- }
638
-
619
+ // Helper: Collapse 4 box-model side values into optimized CSS shorthand
620
+ // syntax (1, 2, 3, or 4 values), per the standard top/right/bottom/left
621
+ // collapsing rules shared by margin/padding/border-{width,style,color}.
622
+ static VALUE collapse_box_values(VALUE top, VALUE right, VALUE bottom, VALUE left) {
639
623
  const char *top_str = StringValueCStr(top);
640
624
  const char *right_str = StringValueCStr(right);
641
625
  const char *bottom_str = StringValueCStr(bottom);
@@ -662,6 +646,29 @@ static VALUE create_dimension_shorthand(VALUE properties, const char *base) {
662
646
  return rb_sprintf("%s %s %s %s", top_str, right_str, bottom_str, left_str);
663
647
  }
664
648
 
649
+ // Helper: Create dimension shorthand (margin or padding)
650
+ // Input: hash with "#{base}-top", "#{base}-right", "#{base}-bottom", "#{base}-left"
651
+ // Output: optimized shorthand string, or Qnil if not all sides present
652
+ static VALUE create_dimension_shorthand(VALUE properties, const char *base) {
653
+ char key_top[32], key_right[32], key_bottom[32], key_left[32];
654
+ snprintf(key_top, sizeof(key_top), "%s-top", base);
655
+ snprintf(key_right, sizeof(key_right), "%s-right", base);
656
+ snprintf(key_bottom, sizeof(key_bottom), "%s-bottom", base);
657
+ snprintf(key_left, sizeof(key_left), "%s-left", base);
658
+
659
+ VALUE top = rb_hash_aref(properties, STR_NEW_CSTR(key_top));
660
+ VALUE right = rb_hash_aref(properties, STR_NEW_CSTR(key_right));
661
+ VALUE bottom = rb_hash_aref(properties, STR_NEW_CSTR(key_bottom));
662
+ VALUE left = rb_hash_aref(properties, STR_NEW_CSTR(key_left));
663
+
664
+ // All four sides must be present
665
+ if (NIL_P(top) || NIL_P(right) || NIL_P(bottom) || NIL_P(left)) {
666
+ return Qnil;
667
+ }
668
+
669
+ return collapse_box_values(top, right, bottom, left);
670
+ }
671
+
665
672
  // Create margin shorthand from longhand properties
666
673
  // Input: hash with "margin-top", "margin-right", "margin-bottom", "margin-left"
667
674
  // Output: optimized shorthand string, or Qnil if not all sides present
@@ -699,31 +706,7 @@ static VALUE create_border_dimension_shorthand(VALUE properties, const char *suf
699
706
  return Qnil;
700
707
  }
701
708
 
702
- // Extract C strings directly (no intermediate storage)
703
- const char *top_str = StringValueCStr(top);
704
- const char *right_str = StringValueCStr(right);
705
- const char *bottom_str = StringValueCStr(bottom);
706
- const char *left_str = StringValueCStr(left);
707
-
708
- // Optimize: if all same, return single value
709
- if (strcmp(top_str, right_str) == 0 &&
710
- strcmp(top_str, bottom_str) == 0 &&
711
- strcmp(top_str, left_str) == 0) {
712
- return rb_str_dup(top);
713
- }
714
-
715
- // Optimize: if top==bottom and left==right, use two values
716
- if (strcmp(top_str, bottom_str) == 0 && strcmp(left_str, right_str) == 0) {
717
- return rb_sprintf("%s %s", top_str, right_str);
718
- }
719
-
720
- // Optimize: if left==right, use three values
721
- if (strcmp(left_str, right_str) == 0) {
722
- return rb_sprintf("%s %s %s", top_str, right_str, bottom_str);
723
- }
724
-
725
- // All different: use four values
726
- return rb_sprintf("%s %s %s %s", top_str, right_str, bottom_str, left_str);
709
+ return collapse_box_values(top, right, bottom, left);
727
710
  }
728
711
 
729
712
  // Create border-width shorthand from individual sides
@@ -1023,42 +1006,37 @@ VALUE cataract_expand_shorthand(VALUE self, VALUE decl) {
1023
1006
  return result;
1024
1007
  }
1025
1008
 
1026
- VALUE expanded_hash = Qnil;
1027
-
1028
- // Try to expand based on property name - return array of Declarations directly
1009
+ // Try to expand based on property name, calling the same public
1010
+ // cataract_expand_* functions used directly by callers (and by
1011
+ // flatten.c) - return array of Declarations directly
1029
1012
  VALUE result = Qnil;
1030
1013
 
1031
1014
  if (strcmp(prop, "margin") == 0) {
1032
- VALUE parts = cataract_split_value(Qnil, value);
1033
- result = expand_dimensions(parts, "margin", NULL, important);
1015
+ result = cataract_expand_margin(Qnil, value, important);
1034
1016
  } else if (strcmp(prop, "padding") == 0) {
1035
- VALUE parts = cataract_split_value(Qnil, value);
1036
- result = expand_dimensions(parts, "padding", NULL, important);
1017
+ result = cataract_expand_padding(Qnil, value, important);
1037
1018
  } else if (strcmp(prop, "border-color") == 0) {
1038
- VALUE parts = cataract_split_value(Qnil, value);
1039
- result = expand_dimensions(parts, "border", "color", important);
1019
+ result = cataract_expand_border_color(Qnil, value, important);
1040
1020
  } else if (strcmp(prop, "border-style") == 0) {
1041
- VALUE parts = cataract_split_value(Qnil, value);
1042
- result = expand_dimensions(parts, "border", "style", important);
1021
+ result = cataract_expand_border_style(Qnil, value, important);
1043
1022
  } else if (strcmp(prop, "border-width") == 0) {
1044
- VALUE parts = cataract_split_value(Qnil, value);
1045
- result = expand_dimensions(parts, "border", "width", important);
1023
+ result = cataract_expand_border_width(Qnil, value, important);
1046
1024
  } else if (strcmp(prop, "border") == 0) {
1047
- result = cataract_expand_border(Qnil, value);
1025
+ result = cataract_expand_border(Qnil, value, important);
1048
1026
  } else if (strcmp(prop, "border-top") == 0) {
1049
- result = cataract_expand_border_side(Qnil, STR_NEW_CSTR("top"), value);
1027
+ result = cataract_expand_border_side(Qnil, STR_NEW_CSTR("top"), value, important);
1050
1028
  } else if (strcmp(prop, "border-right") == 0) {
1051
- result = cataract_expand_border_side(Qnil, STR_NEW_CSTR("right"), value);
1029
+ result = cataract_expand_border_side(Qnil, STR_NEW_CSTR("right"), value, important);
1052
1030
  } else if (strcmp(prop, "border-bottom") == 0) {
1053
- result = cataract_expand_border_side(Qnil, STR_NEW_CSTR("bottom"), value);
1031
+ result = cataract_expand_border_side(Qnil, STR_NEW_CSTR("bottom"), value, important);
1054
1032
  } else if (strcmp(prop, "border-left") == 0) {
1055
- result = cataract_expand_border_side(Qnil, STR_NEW_CSTR("left"), value);
1033
+ result = cataract_expand_border_side(Qnil, STR_NEW_CSTR("left"), value, important);
1056
1034
  } else if (strcmp(prop, "font") == 0) {
1057
- result = cataract_expand_font(Qnil, value);
1035
+ result = cataract_expand_font(Qnil, value, important);
1058
1036
  } else if (strcmp(prop, "background") == 0) {
1059
- result = cataract_expand_background(Qnil, value);
1037
+ result = cataract_expand_background(Qnil, value, important);
1060
1038
  } else if (strcmp(prop, "list-style") == 0) {
1061
- result = cataract_expand_list_style(Qnil, value);
1039
+ result = cataract_expand_list_style(Qnil, value, important);
1062
1040
  }
1063
1041
 
1064
1042
  // If not a shorthand (or expansion failed), return array with original declaration
@@ -1456,7 +1456,7 @@ static VALUE expand_property_if_needed(VALUE property_name, VALUE value) {
1456
1456
 
1457
1457
  // Check if this is a shorthand property that needs expansion
1458
1458
  if (strcmp(prop, "background") == 0) {
1459
- return cataract_expand_background(Qnil, value);
1459
+ return cataract_expand_background(Qnil, value, Qfalse);
1460
1460
  }
1461
1461
  // Add other shorthands if needed (margin, padding, border, font, list-style)
1462
1462
 
@@ -278,13 +278,19 @@ module Cataract
278
278
  dup.merge!(other)
279
279
  end
280
280
 
281
- # Create a shallow copy of this Declarations object.
282
- #
283
- # @return [Declarations] New Declarations with copied properties
284
- def dup
285
- new_decl = self.class.new
286
- each { |prop, value, important| new_decl[prop] = important ? "#{value} !important" : value }
287
- new_decl
281
+ # Ensure dup/clone create a proper independent copy, duplicating the
282
+ # internal declarations array so mutations (via []=, delete, merge!, etc.)
283
+ # on the copy don't affect the source. Declaration structs themselves are
284
+ # treated as immutable value objects throughout this class (always
285
+ # replaced wholesale, never mutated in place), so a shallow copy of the
286
+ # array is sufficient - defining this via initialize_copy (rather than
287
+ # overriding #dup directly) ensures #clone gets the same fix, since both
288
+ # go through initialize_copy.
289
+ #
290
+ # @param source [Declarations] Source Declarations being copied
291
+ def initialize_copy(source)
292
+ super
293
+ @values = source.instance_variable_get(:@values).dup
288
294
  end
289
295
 
290
296
  # Compare this Declarations with another object.
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Cataract
4
+ class Error < StandardError; end
5
+
6
+ # Error raised during import resolution
7
+ class ImportError < Error; end
8
+
9
+ # Parsing errors
10
+ class DepthError < Error; end
11
+ class SizeError < Error; end
12
+ # Internal parser consistency errors
13
+
14
+ # Error raised when invalid CSS is encountered in strict mode
15
+ class ParseError < Error
16
+ attr_reader :line, :column, :error_type
17
+
18
+ # @param message [String] Error message (without position info)
19
+ # @param css [String, nil] Full CSS string for calculating position
20
+ # @param pos [Integer, nil] Byte position in CSS where error occurred
21
+ # @param line [Integer, nil] Line number (if already calculated)
22
+ # @param column [Integer, nil] Column number (if already calculated)
23
+ # @param type [Symbol, nil] Type of parse error (:empty_value, :malformed_declaration, etc.)
24
+ def initialize(message, css: nil, pos: nil, line: nil, column: nil, type: nil)
25
+ # Calculate line/column from css and pos if provided
26
+ if css && pos
27
+ @line = css.byteslice(0, pos).count("\n") + 1
28
+ line_start = css.rindex("\n", pos - 1)
29
+ @column = line_start ? pos - line_start : pos + 1
30
+ else
31
+ @line = line
32
+ @column = column
33
+ end
34
+
35
+ @error_type = type
36
+
37
+ # Build message with position info
38
+ full_message = if @line && @column
39
+ "#{message} at line #{@line}, column #{@column}"
40
+ elsif @line
41
+ "#{message} at line #{@line}"
42
+ else
43
+ message
44
+ end
45
+
46
+ super(full_message)
47
+ end
48
+ end
49
+ end
@@ -57,7 +57,8 @@ module Cataract
57
57
  end
58
58
  end
59
59
 
60
- # Default options for safe import resolution
60
+ # Default options for safe import resolution (@import statements
61
+ # discovered while parsing a stylesheet - untrusted by default)
61
62
  SAFE_DEFAULTS = {
62
63
  max_depth: 5, # Prevent infinite recursion
63
64
  allowed_schemes: ['https'], # Only HTTPS by default
@@ -66,17 +67,33 @@ module Cataract
66
67
  follow_redirects: true, # Follow redirects
67
68
  base_path: nil, # Base path for resolving relative file imports
68
69
  base_uri: nil, # Base URI for resolving relative HTTP imports
69
- fetcher: nil # Custom fetcher (defaults to DefaultFetcher)
70
+ fetcher: nil, # Custom fetcher (defaults to DefaultFetcher)
71
+ dangerous_path_prefixes: ['/etc/', '/proc/', '/sys/', '/dev/'] # Blocked file:// path prefixes
70
72
  }.freeze
71
73
 
74
+ # Default options for an explicitly caller-invoked load (Stylesheet#load_uri
75
+ # / #load_file). The caller chose this exact URI/path themselves - a
76
+ # different trust model than @import content discovered inside a
77
+ # stylesheet - so this permits the schemes/extensions load_uri has always
78
+ # supported, while still keeping the dangerous_path_prefixes protection
79
+ # from SAFE_DEFAULTS as a default (callers can still override it).
80
+ LOAD_DEFAULTS = SAFE_DEFAULTS.merge(
81
+ allowed_schemes: %w[http https file],
82
+ extensions: :any # skip extension validation entirely - see validate_url
83
+ ).freeze
84
+
72
85
  # Normalize options with safe defaults
73
- def self.normalize_options(options)
86
+ #
87
+ # @param options [true, Hash] true for defaults as-is, or a Hash to merge over them
88
+ # @param defaults [Hash] Which default profile to merge over (SAFE_DEFAULTS
89
+ # for @import resolution, LOAD_DEFAULTS for an explicit load_uri/load_file call)
90
+ def self.normalize_options(options, defaults: SAFE_DEFAULTS)
74
91
  if options == true
75
- # imports: true -> use safe defaults
76
- SAFE_DEFAULTS.dup
92
+ # imports: true -> use defaults as-is
93
+ defaults.dup
77
94
  elsif options.is_a?(Hash)
78
- # imports: { ... } -> merge with safe defaults
79
- SAFE_DEFAULTS.merge(options)
95
+ # imports: { ... } -> merge with defaults
96
+ defaults.merge(options)
80
97
  else
81
98
  raise ArgumentError, 'imports option must be true or a Hash'
82
99
  end
@@ -127,13 +144,16 @@ module Cataract
127
144
  "Import scheme '#{uri.scheme}' not allowed. Allowed schemes: #{options[:allowed_schemes].join(', ')}"
128
145
  end
129
146
 
130
- # Check extension
131
- path = uri.path || ''
132
- ext = File.extname(path).delete_prefix('.')
147
+ # Check extension (extensions: :any skips this check entirely - used by
148
+ # LOAD_DEFAULTS, since a directly-invoked load isn't restricted to .css)
149
+ unless options[:extensions] == :any
150
+ path = uri.path || ''
151
+ ext = File.extname(path).delete_prefix('.')
133
152
 
134
- unless ext.empty? || options[:extensions].include?(ext)
135
- raise ImportError,
136
- "Import extension '.#{ext}' not allowed. Allowed extensions: #{options[:extensions].join(', ')}"
153
+ unless ext.empty? || options[:extensions].include?(ext)
154
+ raise ImportError,
155
+ "Import extension '.#{ext}' not allowed. Allowed extensions: #{options[:extensions].join(', ')}"
156
+ end
137
157
  end
138
158
 
139
159
  # Additional security checks for file:// scheme
@@ -146,9 +166,9 @@ module Cataract
146
166
  raise ImportError, "Import file not found or not readable: #{file_path}"
147
167
  end
148
168
 
149
- # Prevent reading sensitive files (basic check)
150
- dangerous_paths = ['/etc/', '/proc/', '/sys/', '/dev/']
151
- if dangerous_paths.any? { |prefix| file_path.start_with?(prefix) }
169
+ # Prevent reading sensitive files (basic check, configurable via
170
+ # options[:dangerous_path_prefixes] - pass [] to disable)
171
+ if options[:dangerous_path_prefixes]&.any? { |prefix| file_path.start_with?(prefix) }
152
172
  raise ImportError, "Import of sensitive system files not allowed: #{file_path}"
153
173
  end
154
174
  end
@@ -45,7 +45,10 @@ module Cataract
45
45
  BYTE_BACKSLASH = 92 # '\\'
46
46
  BYTE_BANG = 33 # '!'
47
47
  BYTE_PERCENT = 37 # '%'
48
- BYTE_SLASH_FWD = 47 # '/' (also defined as BYTE_SLASH above)
48
+ BYTE_EQUALS = 61 # '='
49
+ BYTE_CARET = 94 # '^'
50
+ BYTE_DOLLAR = 36 # '$'
51
+ BYTE_PIPE = 124 # '|'
49
52
 
50
53
  # Specific lowercase letters (for keyword matching)
51
54
  BYTE_LOWER_U = 117 # 'u'