cataract 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.clang-tidy +30 -0
- data/.github/workflows/ci-macos.yml +12 -0
- data/.github/workflows/ci.yml +77 -0
- data/.github/workflows/test.yml +76 -0
- data/.gitignore +45 -0
- data/.overcommit.yml +38 -0
- data/.rubocop.yml +83 -0
- data/BENCHMARKS.md +201 -0
- data/CHANGELOG.md +1 -0
- data/Gemfile +27 -0
- data/LICENSE +21 -0
- data/RAGEL_MIGRATION.md +60 -0
- data/README.md +292 -0
- data/Rakefile +209 -0
- data/benchmarks/benchmark_harness.rb +193 -0
- data/benchmarks/benchmark_merging.rb +121 -0
- data/benchmarks/benchmark_optimization_comparison.rb +168 -0
- data/benchmarks/benchmark_parsing.rb +153 -0
- data/benchmarks/benchmark_ragel_removal.rb +56 -0
- data/benchmarks/benchmark_runner.rb +70 -0
- data/benchmarks/benchmark_serialization.rb +180 -0
- data/benchmarks/benchmark_shorthand.rb +109 -0
- data/benchmarks/benchmark_shorthand_expansion.rb +176 -0
- data/benchmarks/benchmark_specificity.rb +124 -0
- data/benchmarks/benchmark_string_allocation.rb +151 -0
- data/benchmarks/benchmark_stylesheet_to_s.rb +62 -0
- data/benchmarks/benchmark_to_s_cached.rb +55 -0
- data/benchmarks/benchmark_value_splitter.rb +54 -0
- data/benchmarks/benchmark_yjit.rb +158 -0
- data/benchmarks/benchmark_yjit_workers.rb +61 -0
- data/benchmarks/profile_to_s.rb +23 -0
- data/benchmarks/speedup_calculator.rb +83 -0
- data/benchmarks/system_metadata.rb +81 -0
- data/benchmarks/templates/benchmarks.md.erb +221 -0
- data/benchmarks/yjit_tests.rb +141 -0
- data/cataract.gemspec +34 -0
- data/cliff.toml +92 -0
- data/examples/color_conversion_visual_test/color_conversion_test.html +3603 -0
- data/examples/color_conversion_visual_test/generate.rb +202 -0
- data/examples/color_conversion_visual_test/template.html.erb +259 -0
- data/examples/css_analyzer/analyzer.rb +164 -0
- data/examples/css_analyzer/analyzers/base.rb +33 -0
- data/examples/css_analyzer/analyzers/colors.rb +133 -0
- data/examples/css_analyzer/analyzers/important.rb +88 -0
- data/examples/css_analyzer/analyzers/properties.rb +61 -0
- data/examples/css_analyzer/analyzers/specificity.rb +68 -0
- data/examples/css_analyzer/templates/report.html.erb +575 -0
- data/examples/css_analyzer.rb +69 -0
- data/examples/github_analysis.html +5343 -0
- data/ext/cataract/cataract.c +1086 -0
- data/ext/cataract/cataract.h +174 -0
- data/ext/cataract/css_parser.c +1435 -0
- data/ext/cataract/extconf.rb +48 -0
- data/ext/cataract/import_scanner.c +174 -0
- data/ext/cataract/merge.c +973 -0
- data/ext/cataract/shorthand_expander.c +902 -0
- data/ext/cataract/specificity.c +213 -0
- data/ext/cataract/value_splitter.c +116 -0
- data/ext/cataract_color/cataract_color.c +16 -0
- data/ext/cataract_color/color_conversion.c +1687 -0
- data/ext/cataract_color/color_conversion.h +136 -0
- data/ext/cataract_color/color_conversion_lab.c +571 -0
- data/ext/cataract_color/color_conversion_named.c +259 -0
- data/ext/cataract_color/color_conversion_oklab.c +547 -0
- data/ext/cataract_color/extconf.rb +23 -0
- data/ext/cataract_old/cataract.c +393 -0
- data/ext/cataract_old/cataract.h +250 -0
- data/ext/cataract_old/css_parser.c +933 -0
- data/ext/cataract_old/extconf.rb +67 -0
- data/ext/cataract_old/import_scanner.c +174 -0
- data/ext/cataract_old/merge.c +776 -0
- data/ext/cataract_old/shorthand_expander.c +902 -0
- data/ext/cataract_old/specificity.c +213 -0
- data/ext/cataract_old/stylesheet.c +290 -0
- data/ext/cataract_old/value_splitter.c +116 -0
- data/lib/cataract/at_rule.rb +97 -0
- data/lib/cataract/color_conversion.rb +18 -0
- data/lib/cataract/declarations.rb +332 -0
- data/lib/cataract/import_resolver.rb +210 -0
- data/lib/cataract/rule.rb +131 -0
- data/lib/cataract/stylesheet.rb +716 -0
- data/lib/cataract/stylesheet_scope.rb +257 -0
- data/lib/cataract/version.rb +5 -0
- data/lib/cataract.rb +107 -0
- data/lib/tasks/gem.rake +158 -0
- data/scripts/fuzzer/run.rb +828 -0
- data/scripts/fuzzer/worker.rb +99 -0
- data/scripts/generate_benchmarks_md.rb +155 -0
- metadata +135 -0
|
@@ -0,0 +1,1687 @@
|
|
|
1
|
+
// color_conversion.c - CSS color format conversion
|
|
2
|
+
//
|
|
3
|
+
// HOW TO ADD A NEW COLOR FORMAT
|
|
4
|
+
// ===============================
|
|
5
|
+
//
|
|
6
|
+
// This module uses an intermediate representation (IR) to convert between color formats.
|
|
7
|
+
// All conversions go through: Source Format → IR → Target Format
|
|
8
|
+
//
|
|
9
|
+
// STEP 1: Define the Intermediate Representation (IR)
|
|
10
|
+
// ---------------------------------------------------
|
|
11
|
+
// The IR is defined in color_conversion.h as `struct color_ir`:
|
|
12
|
+
// - red, green, blue (int 0-255): sRGB values (always populated)
|
|
13
|
+
// - alpha (double 0.0-1.0 or -1.0): alpha channel (-1.0 = no alpha)
|
|
14
|
+
// - has_linear_rgb (int): flag indicating if linear RGB is available
|
|
15
|
+
// - linear_r, linear_g, linear_b (double 0.0-1.0): high-precision linear RGB
|
|
16
|
+
//
|
|
17
|
+
// Use INIT_COLOR_IR(color) to initialize the struct with default values.
|
|
18
|
+
//
|
|
19
|
+
// High-precision formats (oklab, oklch) should set has_linear_rgb = 1 and populate
|
|
20
|
+
// the linear_* fields to preserve precision and avoid quantization loss.
|
|
21
|
+
//
|
|
22
|
+
// STEP 2: Implement Parser Function
|
|
23
|
+
// ----------------------------------
|
|
24
|
+
// Signature: static struct color_ir parse_FORMAT(VALUE format_value)
|
|
25
|
+
//
|
|
26
|
+
// Example:
|
|
27
|
+
// static struct color_ir parse_hsl(VALUE hsl_value) {
|
|
28
|
+
// struct color_ir color;
|
|
29
|
+
// INIT_COLOR_IR(color);
|
|
30
|
+
//
|
|
31
|
+
// // Parse the CSS string (e.g., "hsl(120, 100%, 50%)")
|
|
32
|
+
// const char *str = StringValueCStr(hsl_value);
|
|
33
|
+
// // ... parse logic ...
|
|
34
|
+
//
|
|
35
|
+
// // Convert to sRGB (0-255) and store in color.red, color.green, color.blue
|
|
36
|
+
// // Optionally store linear RGB for high precision
|
|
37
|
+
//
|
|
38
|
+
// RB_GC_GUARD(hsl_value); // Protect from GC
|
|
39
|
+
// return color;
|
|
40
|
+
// }
|
|
41
|
+
//
|
|
42
|
+
// Tips:
|
|
43
|
+
// - Use macros from color_conversion.h: SKIP_WHITESPACE, PARSE_INT, etc.
|
|
44
|
+
// - Validate input and raise rb_eArgError for invalid syntax
|
|
45
|
+
// - Always add RB_GC_GUARD before returning
|
|
46
|
+
//
|
|
47
|
+
// STEP 3: Implement Formatter Function
|
|
48
|
+
// -------------------------------------
|
|
49
|
+
// Signature: static VALUE format_FORMAT(struct color_ir color, int use_modern_syntax)
|
|
50
|
+
//
|
|
51
|
+
// Example:
|
|
52
|
+
// static VALUE format_hsl(struct color_ir color, int use_modern_syntax) {
|
|
53
|
+
// // Convert from sRGB (or linear RGB if available) to target format
|
|
54
|
+
// // Use macros from color_conversion.h: FORMAT_HSL, FORMAT_HSLA
|
|
55
|
+
// char buf[64];
|
|
56
|
+
// if (color.alpha >= 0.0) {
|
|
57
|
+
// FORMAT_HSLA(buf, h, s, l, color.alpha);
|
|
58
|
+
// } else {
|
|
59
|
+
// FORMAT_HSL(buf, h, s, l);
|
|
60
|
+
// }
|
|
61
|
+
// return rb_str_new_cstr(buf);
|
|
62
|
+
// }
|
|
63
|
+
//
|
|
64
|
+
// Tips:
|
|
65
|
+
// - Check color.has_linear_rgb to use high-precision values when available
|
|
66
|
+
// - Define FORMAT_* macros in color_conversion.h for consistent output
|
|
67
|
+
//
|
|
68
|
+
// STEP 4: Add Detection Macros (color_conversion.h)
|
|
69
|
+
// --------------------------------------------------
|
|
70
|
+
// Add a STARTS_WITH_FORMAT macro to detect the format in CSS strings:
|
|
71
|
+
//
|
|
72
|
+
// #define STARTS_WITH_HSL(p, remaining)
|
|
73
|
+
// ((remaining) >= 4 && (p)[0] == 'h' && (p)[1] == 's' && (p)[2] == 'l' &&
|
|
74
|
+
// ((p)[3] == '(' || (p)[3] == 'a'))
|
|
75
|
+
//
|
|
76
|
+
// STEP 5: Register in Dispatchers
|
|
77
|
+
// --------------------------------
|
|
78
|
+
// Add to get_parser() function:
|
|
79
|
+
// ID format_id = rb_intern("format");
|
|
80
|
+
// if (format_id == format_id) {
|
|
81
|
+
// return parse_format;
|
|
82
|
+
// }
|
|
83
|
+
//
|
|
84
|
+
// Add to get_formatter() function:
|
|
85
|
+
// ID format_id = rb_intern("format");
|
|
86
|
+
// if (format_id == format_id) {
|
|
87
|
+
// return format_format;
|
|
88
|
+
// }
|
|
89
|
+
//
|
|
90
|
+
// STEP 6: Integrate into Converter
|
|
91
|
+
// ---------------------------------
|
|
92
|
+
// Add to convert_value_with_colors() function:
|
|
93
|
+
// if (STARTS_WITH_FORMAT(p, remaining) && (parser == NULL || parser == parse_format)) {
|
|
94
|
+
// const char *end;
|
|
95
|
+
// FIND_CLOSING_PAREN(p, end);
|
|
96
|
+
// color_len = end - p;
|
|
97
|
+
// VALUE format_str = rb_str_new(p, color_len);
|
|
98
|
+
// struct color_ir color = parse_format(format_str);
|
|
99
|
+
// VALUE converted = formatter(color, use_modern_syntax);
|
|
100
|
+
// rb_str_buf_append(result, converted);
|
|
101
|
+
// pos += color_len;
|
|
102
|
+
// found_color = 1;
|
|
103
|
+
// continue;
|
|
104
|
+
// }
|
|
105
|
+
//
|
|
106
|
+
// Add to detect_color_format() function:
|
|
107
|
+
// if (STARTS_WITH_FORMAT(p, remaining)) {
|
|
108
|
+
// return parse_format;
|
|
109
|
+
// }
|
|
110
|
+
//
|
|
111
|
+
// Add to matches_color_format() function:
|
|
112
|
+
// ID format_id = rb_intern("format");
|
|
113
|
+
// if (format_id == format_id) {
|
|
114
|
+
// return STARTS_WITH_FORMAT(p, remaining);
|
|
115
|
+
// }
|
|
116
|
+
//
|
|
117
|
+
// STEP 7: Add Tests
|
|
118
|
+
// -----------------
|
|
119
|
+
// Create test/test_color_conversion_FORMAT.rb with comprehensive test coverage:
|
|
120
|
+
// - Parsing valid values
|
|
121
|
+
// - Conversions to/from other formats
|
|
122
|
+
// - Edge cases (invalid input, boundary values)
|
|
123
|
+
// - Alpha channel support
|
|
124
|
+
// - Round-trip conversions
|
|
125
|
+
//
|
|
126
|
+
// EXAMPLE: See oklab/oklch implementations for reference
|
|
127
|
+
//
|
|
128
|
+
// ============================================================================
|
|
129
|
+
|
|
130
|
+
#include "cataract.h"
|
|
131
|
+
#include "color_conversion.h"
|
|
132
|
+
#include <ctype.h>
|
|
133
|
+
#include <stdio.h>
|
|
134
|
+
#include <math.h>
|
|
135
|
+
|
|
136
|
+
// Forward declarations
|
|
137
|
+
static int is_hex_digit(char c);
|
|
138
|
+
static int hex_char_to_int(char c);
|
|
139
|
+
static VALUE expand_property_if_needed(VALUE property_name, VALUE value);
|
|
140
|
+
VALUE rb_stylesheet_convert_colors(int argc, VALUE *argv, VALUE self);
|
|
141
|
+
|
|
142
|
+
// Parser function signature: format → IR
|
|
143
|
+
typedef struct color_ir (*color_parser_fn)(VALUE color_value);
|
|
144
|
+
|
|
145
|
+
// Formatter function signature: IR → format
|
|
146
|
+
typedef VALUE (*color_formatter_fn)(struct color_ir color, int use_modern_syntax);
|
|
147
|
+
|
|
148
|
+
// Parser functions
|
|
149
|
+
static struct color_ir parse_hex(VALUE hex_value);
|
|
150
|
+
static struct color_ir parse_rgb(VALUE rgb_value);
|
|
151
|
+
static struct color_ir parse_rgb_percent(VALUE rgb_value);
|
|
152
|
+
static struct color_ir parse_hsl(VALUE hsl_value);
|
|
153
|
+
static struct color_ir parse_hwb(VALUE hwb_value);
|
|
154
|
+
|
|
155
|
+
// Formatter functions
|
|
156
|
+
static VALUE format_hex(struct color_ir color, int use_modern_syntax);
|
|
157
|
+
static VALUE format_rgb(struct color_ir color, int use_modern_syntax);
|
|
158
|
+
static VALUE format_hsl(struct color_ir color, int use_modern_syntax);
|
|
159
|
+
static VALUE format_hwb(struct color_ir color, int use_modern_syntax);
|
|
160
|
+
|
|
161
|
+
// Oklab functions (defined in color_conversion_oklab.c)
|
|
162
|
+
extern struct color_ir parse_oklab(VALUE oklab_value);
|
|
163
|
+
extern VALUE format_oklab(struct color_ir color, int use_modern_syntax);
|
|
164
|
+
|
|
165
|
+
// OKLCh functions (defined in color_conversion_oklab.c)
|
|
166
|
+
extern struct color_ir parse_oklch(VALUE oklch_value);
|
|
167
|
+
extern VALUE format_oklch(struct color_ir color, int use_modern_syntax);
|
|
168
|
+
|
|
169
|
+
// Named color functions (defined in color_conversion_named.c)
|
|
170
|
+
extern struct color_ir parse_named(VALUE named_value);
|
|
171
|
+
|
|
172
|
+
// Lab functions (defined in color_conversion_lab.c)
|
|
173
|
+
extern struct color_ir parse_lab(VALUE lab_value);
|
|
174
|
+
extern VALUE format_lab(struct color_ir color, int use_modern_syntax);
|
|
175
|
+
extern struct color_ir parse_lch(VALUE lch_value);
|
|
176
|
+
extern VALUE format_lch(struct color_ir color, int use_modern_syntax);
|
|
177
|
+
|
|
178
|
+
// Dispatchers
|
|
179
|
+
static color_parser_fn get_parser(VALUE format);
|
|
180
|
+
static color_formatter_fn get_formatter(VALUE format);
|
|
181
|
+
|
|
182
|
+
// Exception class
|
|
183
|
+
static VALUE rb_eColorConversionError = Qnil;
|
|
184
|
+
|
|
185
|
+
// Initialize color conversion module
|
|
186
|
+
void Init_color_conversion(VALUE mCataract) {
|
|
187
|
+
// Define ColorConversionError exception class
|
|
188
|
+
rb_eColorConversionError = rb_define_class_under(mCataract, "ColorConversionError", rb_eStandardError);
|
|
189
|
+
|
|
190
|
+
// Get the Stylesheet class (bootstrapped in C, defined fully in Ruby)
|
|
191
|
+
VALUE cStylesheet = rb_const_get(mCataract, rb_intern("Stylesheet"));
|
|
192
|
+
|
|
193
|
+
// Add convert_colors! instance method to Stylesheet
|
|
194
|
+
rb_define_method(cStylesheet, "convert_colors!", rb_stylesheet_convert_colors, -1);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Check if a character is a valid hex digit (0-9, a-f, A-F)
|
|
198
|
+
static int is_hex_digit(char c) {
|
|
199
|
+
return (c >= '0' && c <= '9') ||
|
|
200
|
+
(c >= 'a' && c <= 'f') ||
|
|
201
|
+
(c >= 'A' && c <= 'F');
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Convert hex character to integer value
|
|
205
|
+
static int hex_char_to_int(char c) {
|
|
206
|
+
if (c >= '0' && c <= '9') return c - '0';
|
|
207
|
+
if (c >= 'a' && c <= 'f') return c - 'a' + 10;
|
|
208
|
+
if (c >= 'A' && c <= 'F') return c - 'A' + 10;
|
|
209
|
+
return -1;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Macro to check if a string contains unparseable content and preserve it if found
|
|
213
|
+
// Sets has_unparseable to 1 if content should be preserved, 0 otherwise
|
|
214
|
+
#define HAS_UNPARSEABLE_CONTENT(str, len, has_unparseable) do { \
|
|
215
|
+
(has_unparseable) = 0; \
|
|
216
|
+
if ((str) != NULL && (len) >= 4) { \
|
|
217
|
+
static const char *unparseable[] = { \
|
|
218
|
+
"calc(", "min(", "max(", "clamp(", "var(", \
|
|
219
|
+
"none", "infinity", "-infinity", "NaN", \
|
|
220
|
+
"from ", \
|
|
221
|
+
NULL \
|
|
222
|
+
}; \
|
|
223
|
+
for (int _i = 0; unparseable[_i] != NULL; _i++) { \
|
|
224
|
+
const char *_kw = unparseable[_i]; \
|
|
225
|
+
size_t _kw_len = strlen(_kw); \
|
|
226
|
+
for (long _j = 0; _j <= (len) - (long)_kw_len; _j++) { \
|
|
227
|
+
if (strncmp((str) + _j, _kw, _kw_len) == 0) { \
|
|
228
|
+
(has_unparseable) = 1; \
|
|
229
|
+
break; \
|
|
230
|
+
} \
|
|
231
|
+
} \
|
|
232
|
+
if ((has_unparseable)) break; \
|
|
233
|
+
} \
|
|
234
|
+
} \
|
|
235
|
+
} while(0)
|
|
236
|
+
|
|
237
|
+
// Parse hex color string to intermediate representation
|
|
238
|
+
// hex_value: Ruby string like "#fff", "#ffffff", or "#ff000080"
|
|
239
|
+
// Returns: color_ir struct with RGB values (0-255) and optional alpha (0.0-1.0)
|
|
240
|
+
static struct color_ir parse_hex(VALUE hex_value) {
|
|
241
|
+
Check_Type(hex_value, T_STRING);
|
|
242
|
+
|
|
243
|
+
const char *hex_str = RSTRING_PTR(hex_value);
|
|
244
|
+
long hex_len = RSTRING_LEN(hex_value);
|
|
245
|
+
|
|
246
|
+
// Must start with '#'
|
|
247
|
+
if (hex_len < 2 || hex_str[0] != '#') {
|
|
248
|
+
rb_raise(rb_eColorConversionError, "Invalid hex color: must start with '#', got '%s'", hex_str);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// Skip the '#' character
|
|
252
|
+
hex_str++;
|
|
253
|
+
hex_len--;
|
|
254
|
+
|
|
255
|
+
// Validate length (3, 6, or 8 digits)
|
|
256
|
+
if (hex_len != 3 && hex_len != 6 && hex_len != 8) {
|
|
257
|
+
// hex_str points past '#', so show original with '#'
|
|
258
|
+
rb_raise(rb_eColorConversionError,
|
|
259
|
+
"Invalid hex color: expected 3, 6, or 8 digits, got %ld in '%s'",
|
|
260
|
+
hex_len, RSTRING_PTR(hex_value));
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// Validate all characters are hex digits
|
|
264
|
+
for (long i = 0; i < hex_len; i++) {
|
|
265
|
+
if (!is_hex_digit(hex_str[i])) {
|
|
266
|
+
rb_raise(rb_eColorConversionError,
|
|
267
|
+
"Invalid hex color: contains non-hex character '%c'",
|
|
268
|
+
hex_str[i]);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
struct color_ir color;
|
|
273
|
+
INIT_COLOR_IR(color);
|
|
274
|
+
|
|
275
|
+
if (hex_len == 3) {
|
|
276
|
+
// 3-digit hex: #RGB -> each digit is duplicated
|
|
277
|
+
color.red = hex_char_to_int(hex_str[0]) * 17;
|
|
278
|
+
color.green = hex_char_to_int(hex_str[1]) * 17;
|
|
279
|
+
color.blue = hex_char_to_int(hex_str[2]) * 17;
|
|
280
|
+
} else {
|
|
281
|
+
// 6 or 8-digit hex: #RRGGBB or #RRGGBBAA
|
|
282
|
+
color.red = (hex_char_to_int(hex_str[0]) << 4) | hex_char_to_int(hex_str[1]);
|
|
283
|
+
color.green = (hex_char_to_int(hex_str[2]) << 4) | hex_char_to_int(hex_str[3]);
|
|
284
|
+
color.blue = (hex_char_to_int(hex_str[4]) << 4) | hex_char_to_int(hex_str[5]);
|
|
285
|
+
|
|
286
|
+
if (hex_len == 8) {
|
|
287
|
+
int alpha_int = (hex_char_to_int(hex_str[6]) << 4) | hex_char_to_int(hex_str[7]);
|
|
288
|
+
color.alpha = alpha_int / 255.0;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
return color;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// Format intermediate representation to RGB string
|
|
296
|
+
// color: color_ir struct with RGB (0-255) and optional alpha (0.0-1.0)
|
|
297
|
+
// use_modern_syntax: 1 for "rgb(255 0 0)", 0 for "rgb(255, 0, 0)"
|
|
298
|
+
// Returns: Ruby string with RGB/RGBA value
|
|
299
|
+
static VALUE format_rgb(struct color_ir color, int use_modern_syntax) {
|
|
300
|
+
char rgb_buf[128];
|
|
301
|
+
|
|
302
|
+
// Use high-precision linear RGB if available (preserves precision from oklab/oklch)
|
|
303
|
+
if (color.has_linear_rgb) {
|
|
304
|
+
// Convert linear RGB to sRGB percentages
|
|
305
|
+
double lr = color.linear_r, lg = color.linear_g, lb = color.linear_b;
|
|
306
|
+
|
|
307
|
+
// Clamp to [0.0, 1.0]
|
|
308
|
+
if (lr < 0.0) lr = 0.0;
|
|
309
|
+
if (lr > 1.0) lr = 1.0;
|
|
310
|
+
if (lg < 0.0) lg = 0.0;
|
|
311
|
+
if (lg > 1.0) lg = 1.0;
|
|
312
|
+
if (lb < 0.0) lb = 0.0;
|
|
313
|
+
if (lb > 1.0) lb = 1.0;
|
|
314
|
+
|
|
315
|
+
// Apply sRGB gamma correction
|
|
316
|
+
double rs = (lr <= 0.0031308) ? lr * 12.92 : 1.055 * pow(lr, 1.0/2.4) - 0.055;
|
|
317
|
+
double gs = (lg <= 0.0031308) ? lg * 12.92 : 1.055 * pow(lg, 1.0/2.4) - 0.055;
|
|
318
|
+
double bs = (lb <= 0.0031308) ? lb * 12.92 : 1.055 * pow(lb, 1.0/2.4) - 0.055;
|
|
319
|
+
|
|
320
|
+
// Convert to percentages
|
|
321
|
+
double r_pct = rs * 100.0;
|
|
322
|
+
double g_pct = gs * 100.0;
|
|
323
|
+
double b_pct = bs * 100.0;
|
|
324
|
+
|
|
325
|
+
if (color.alpha >= 0.0) {
|
|
326
|
+
FORMAT_RGB_PERCENT_ALPHA(rgb_buf, r_pct, g_pct, b_pct, color.alpha);
|
|
327
|
+
} else {
|
|
328
|
+
FORMAT_RGB_PERCENT(rgb_buf, r_pct, g_pct, b_pct);
|
|
329
|
+
}
|
|
330
|
+
} else {
|
|
331
|
+
// Use integer sRGB values (0-255)
|
|
332
|
+
if (color.alpha >= 0.0) {
|
|
333
|
+
// Has alpha channel
|
|
334
|
+
if (use_modern_syntax) {
|
|
335
|
+
FORMAT_RGBA_MODERN(rgb_buf, color.red, color.green, color.blue, color.alpha);
|
|
336
|
+
} else {
|
|
337
|
+
FORMAT_RGBA_LEGACY(rgb_buf, color.red, color.green, color.blue, color.alpha);
|
|
338
|
+
}
|
|
339
|
+
} else {
|
|
340
|
+
// No alpha channel
|
|
341
|
+
if (use_modern_syntax) {
|
|
342
|
+
FORMAT_RGB_MODERN(rgb_buf, color.red, color.green, color.blue);
|
|
343
|
+
} else {
|
|
344
|
+
FORMAT_RGB_LEGACY(rgb_buf, color.red, color.green, color.blue);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
return rb_str_new_cstr(rgb_buf);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// Parse RGB color string to intermediate representation
|
|
353
|
+
// rgb_value: Ruby string like "rgb(255, 0, 0)" or "rgb(255 0 0 / 0.5)"
|
|
354
|
+
// Returns: color_ir struct with RGB values (0-255) and optional alpha (0.0-1.0)
|
|
355
|
+
static struct color_ir parse_rgb(VALUE rgb_value) {
|
|
356
|
+
Check_Type(rgb_value, T_STRING);
|
|
357
|
+
|
|
358
|
+
const char *rgb_str = RSTRING_PTR(rgb_value);
|
|
359
|
+
long rgb_len = RSTRING_LEN(rgb_value);
|
|
360
|
+
|
|
361
|
+
if (rgb_len < 10 || (strncmp(rgb_str, "rgb(", 4) != 0 && strncmp(rgb_str, "rgba(", 5) != 0)) {
|
|
362
|
+
rb_raise(rb_eColorConversionError, "Invalid RGB color: must start with 'rgb(' or 'rgba(', got '%s'", rgb_str);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// Skip "rgb(" or "rgba("
|
|
366
|
+
const char *p = rgb_str; // "rgb(255, 128, 64, 0.5)"
|
|
367
|
+
if (*p == 'r' && *(p+1) == 'g' && *(p+2) == 'b') { // "rgb"
|
|
368
|
+
p += 3; // "(255, 128, 64, 0.5)"
|
|
369
|
+
if (*p == 'a') p++; // skip 'a' if rgba
|
|
370
|
+
if (*p == '(') p++; // "255, 128, 64, 0.5)"
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
struct color_ir color;
|
|
374
|
+
INIT_COLOR_IR(color);
|
|
375
|
+
|
|
376
|
+
// Check if this is percentage format by looking for '%' before first separator
|
|
377
|
+
const char *check = p;
|
|
378
|
+
int is_percent = 0;
|
|
379
|
+
while (*check && *check != ')' && *check != ',' && *check != '/') {
|
|
380
|
+
if (*check == '%') {
|
|
381
|
+
is_percent = 1;
|
|
382
|
+
break;
|
|
383
|
+
}
|
|
384
|
+
check++;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
if (is_percent) {
|
|
388
|
+
// Delegate to percentage parser
|
|
389
|
+
return parse_rgb_percent(rgb_value);
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
SKIP_WHITESPACE(p); // "255, 128, 64, 0.5)"
|
|
393
|
+
PARSE_INT(p, color.red); // red=255, p=", 128, 64, 0.5)"
|
|
394
|
+
|
|
395
|
+
SKIP_SEPARATOR(p); // "128, 64, 0.5)"
|
|
396
|
+
PARSE_INT(p, color.green); // green=128, p=", 64, 0.5)"
|
|
397
|
+
|
|
398
|
+
SKIP_SEPARATOR(p); // "64, 0.5)"
|
|
399
|
+
PARSE_INT(p, color.blue); // blue=64, p=", 0.5)"
|
|
400
|
+
|
|
401
|
+
// Check for alpha (either ",alpha" or "/ alpha")
|
|
402
|
+
SKIP_SEPARATOR(p); // "0.5)" or ", 0.5)" or ", / 0.5)"
|
|
403
|
+
if (*p == '/') { // '/'
|
|
404
|
+
p++; // " 0.5)"
|
|
405
|
+
SKIP_WHITESPACE(p); // "0.5)"
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
if (*p >= '0' && *p <= '9') { // '0'
|
|
409
|
+
color.alpha = 0.0;
|
|
410
|
+
while (*p >= '0' && *p <= '9') { // '0' (integer part)
|
|
411
|
+
color.alpha = color.alpha * 10.0 + (*p - '0'); // alpha=0.0
|
|
412
|
+
p++; // ".5)"
|
|
413
|
+
}
|
|
414
|
+
if (*p == '.') { // '.'
|
|
415
|
+
p++; // "5)"
|
|
416
|
+
double decimal = 0.1;
|
|
417
|
+
while (*p >= '0' && *p <= '9') { // '5'
|
|
418
|
+
color.alpha += (*p - '0') * decimal; // alpha=0.5
|
|
419
|
+
decimal /= 10.0;
|
|
420
|
+
p++; // ")"
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
// Validate ranges
|
|
426
|
+
if (color.red < 0 || color.red > 255 || color.green < 0 || color.green > 255 || color.blue < 0 || color.blue > 255) {
|
|
427
|
+
rb_raise(rb_eColorConversionError,
|
|
428
|
+
"Invalid RGB values: must be 0-255, got red=%d green=%d blue=%d", color.red, color.green, color.blue);
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
if (color.alpha >= 0.0 && (color.alpha < 0.0 || color.alpha > 1.0)) {
|
|
432
|
+
rb_raise(rb_eColorConversionError,
|
|
433
|
+
"Invalid alpha value: must be 0.0-1.0, got %.10g", color.alpha);
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
return color;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
// Parse RGB percentage color string to intermediate representation
|
|
440
|
+
// rgb_value: Ruby string like "rgb(70.492% 2.351% 37.073%)"
|
|
441
|
+
// Returns: color_ir struct with high-precision linear RGB
|
|
442
|
+
static struct color_ir parse_rgb_percent(VALUE rgb_value) {
|
|
443
|
+
Check_Type(rgb_value, T_STRING);
|
|
444
|
+
|
|
445
|
+
const char *rgb_str = RSTRING_PTR(rgb_value);
|
|
446
|
+
long rgb_len = RSTRING_LEN(rgb_value);
|
|
447
|
+
|
|
448
|
+
if (rgb_len < 10 || (strncmp(rgb_str, "rgb(", 4) != 0 && strncmp(rgb_str, "rgba(", 5) != 0)) {
|
|
449
|
+
rb_raise(rb_eColorConversionError, "Invalid RGB color: must start with 'rgb(' or 'rgba(', got '%s'", rgb_str);
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
// Skip "rgb(" or "rgba("
|
|
453
|
+
const char *p = rgb_str;
|
|
454
|
+
if (*p == 'r' && *(p+1) == 'g' && *(p+2) == 'b') {
|
|
455
|
+
p += 3;
|
|
456
|
+
if (*p == 'a') p++;
|
|
457
|
+
if (*p == '(') p++;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
struct color_ir color;
|
|
461
|
+
INIT_COLOR_IR(color);
|
|
462
|
+
|
|
463
|
+
SKIP_WHITESPACE(p);
|
|
464
|
+
|
|
465
|
+
// Parse R%
|
|
466
|
+
double r_pct = 0.0;
|
|
467
|
+
while (*p >= '0' && *p <= '9') {
|
|
468
|
+
r_pct = r_pct * 10.0 + (*p - '0');
|
|
469
|
+
p++;
|
|
470
|
+
}
|
|
471
|
+
if (*p == '.') {
|
|
472
|
+
p++;
|
|
473
|
+
double frac = 0.1;
|
|
474
|
+
while (*p >= '0' && *p <= '9') {
|
|
475
|
+
r_pct += (*p - '0') * frac;
|
|
476
|
+
frac *= 0.1;
|
|
477
|
+
p++;
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
if (*p == '%') p++;
|
|
481
|
+
|
|
482
|
+
SKIP_SEPARATOR(p);
|
|
483
|
+
|
|
484
|
+
// Parse G%
|
|
485
|
+
double g_pct = 0.0;
|
|
486
|
+
while (*p >= '0' && *p <= '9') {
|
|
487
|
+
g_pct = g_pct * 10.0 + (*p - '0');
|
|
488
|
+
p++;
|
|
489
|
+
}
|
|
490
|
+
if (*p == '.') {
|
|
491
|
+
p++;
|
|
492
|
+
double frac = 0.1;
|
|
493
|
+
while (*p >= '0' && *p <= '9') {
|
|
494
|
+
g_pct += (*p - '0') * frac;
|
|
495
|
+
frac *= 0.1;
|
|
496
|
+
p++;
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
if (*p == '%') p++;
|
|
500
|
+
|
|
501
|
+
SKIP_SEPARATOR(p);
|
|
502
|
+
|
|
503
|
+
// Parse B%
|
|
504
|
+
double b_pct = 0.0;
|
|
505
|
+
while (*p >= '0' && *p <= '9') {
|
|
506
|
+
b_pct = b_pct * 10.0 + (*p - '0');
|
|
507
|
+
p++;
|
|
508
|
+
}
|
|
509
|
+
if (*p == '.') {
|
|
510
|
+
p++;
|
|
511
|
+
double frac = 0.1;
|
|
512
|
+
while (*p >= '0' && *p <= '9') {
|
|
513
|
+
b_pct += (*p - '0') * frac;
|
|
514
|
+
frac *= 0.1;
|
|
515
|
+
p++;
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
if (*p == '%') p++;
|
|
519
|
+
|
|
520
|
+
// Check for alpha
|
|
521
|
+
SKIP_SEPARATOR(p);
|
|
522
|
+
if (*p == '/') {
|
|
523
|
+
p++;
|
|
524
|
+
SKIP_WHITESPACE(p);
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
if (*p >= '0' && *p <= '9') {
|
|
528
|
+
color.alpha = 0.0;
|
|
529
|
+
while (*p >= '0' && *p <= '9') {
|
|
530
|
+
color.alpha = color.alpha * 10.0 + (*p - '0');
|
|
531
|
+
p++;
|
|
532
|
+
}
|
|
533
|
+
if (*p == '.') {
|
|
534
|
+
p++;
|
|
535
|
+
double decimal = 0.1;
|
|
536
|
+
while (*p >= '0' && *p <= '9') {
|
|
537
|
+
color.alpha += (*p - '0') * decimal;
|
|
538
|
+
decimal /= 10.0;
|
|
539
|
+
p++;
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
// Convert percentages to sRGB (0-1.0)
|
|
545
|
+
double rs = r_pct / 100.0;
|
|
546
|
+
double gs = g_pct / 100.0;
|
|
547
|
+
double bs = b_pct / 100.0;
|
|
548
|
+
|
|
549
|
+
// Clamp to [0, 1]
|
|
550
|
+
if (rs < 0.0) rs = 0.0;
|
|
551
|
+
if (rs > 1.0) rs = 1.0;
|
|
552
|
+
if (gs < 0.0) gs = 0.0;
|
|
553
|
+
if (gs > 1.0) gs = 1.0;
|
|
554
|
+
if (bs < 0.0) bs = 0.0;
|
|
555
|
+
if (bs > 1.0) bs = 1.0;
|
|
556
|
+
|
|
557
|
+
// Apply inverse gamma to get linear RGB for precision
|
|
558
|
+
double lr = (rs <= 0.04045) ? rs / 12.92 : pow((rs + 0.055) / 1.055, 2.4);
|
|
559
|
+
double lg = (gs <= 0.04045) ? gs / 12.92 : pow((gs + 0.055) / 1.055, 2.4);
|
|
560
|
+
double lb = (bs <= 0.04045) ? bs / 12.92 : pow((bs + 0.055) / 1.055, 2.4);
|
|
561
|
+
|
|
562
|
+
// Store high-precision linear RGB
|
|
563
|
+
color.has_linear_rgb = 1;
|
|
564
|
+
color.linear_r = lr;
|
|
565
|
+
color.linear_g = lg;
|
|
566
|
+
color.linear_b = lb;
|
|
567
|
+
|
|
568
|
+
// Also store integer sRGB for compatibility
|
|
569
|
+
color.red = (int)(rs * 255.0 + 0.5);
|
|
570
|
+
color.green = (int)(gs * 255.0 + 0.5);
|
|
571
|
+
color.blue = (int)(bs * 255.0 + 0.5);
|
|
572
|
+
|
|
573
|
+
RB_GC_GUARD(rgb_value);
|
|
574
|
+
return color;
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
// Format intermediate representation to hex string
|
|
578
|
+
// color: color_ir struct with RGB (0-255) and optional alpha (0.0-1.0)
|
|
579
|
+
// use_modern_syntax: unused for hex format (hex format is always the same)
|
|
580
|
+
// Returns: Ruby string with hex value like "#ff0000" or "#ff000080"
|
|
581
|
+
static VALUE format_hex(struct color_ir color, int use_modern_syntax) {
|
|
582
|
+
(void)use_modern_syntax; // Unused - hex format doesn't have variants
|
|
583
|
+
|
|
584
|
+
char hex_buf[10];
|
|
585
|
+
if (color.alpha >= 0.0) {
|
|
586
|
+
int alpha_int = (int)(color.alpha * 255.0 + 0.5);
|
|
587
|
+
FORMAT_HEX_ALPHA(hex_buf, color.red, color.green, color.blue, alpha_int);
|
|
588
|
+
} else {
|
|
589
|
+
FORMAT_HEX(hex_buf, color.red, color.green, color.blue);
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
return rb_str_new_cstr(hex_buf);
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
// Parse HSL color string to intermediate representation
|
|
596
|
+
// hsl_value: Ruby string like "hsl(0, 100%, 50%)" or "hsl(0, 100%, 50%, 0.5)"
|
|
597
|
+
// Returns: color_ir struct with RGB values (0-255) and optional alpha (0.0-1.0)
|
|
598
|
+
static struct color_ir parse_hsl(VALUE hsl_value) {
|
|
599
|
+
Check_Type(hsl_value, T_STRING);
|
|
600
|
+
|
|
601
|
+
const char *hsl_str = RSTRING_PTR(hsl_value);
|
|
602
|
+
long hsl_len = RSTRING_LEN(hsl_value);
|
|
603
|
+
|
|
604
|
+
if (hsl_len < 10 || (strncmp(hsl_str, "hsl(", 4) != 0 && strncmp(hsl_str, "hsla(", 5) != 0)) {
|
|
605
|
+
rb_raise(rb_eColorConversionError, "Invalid HSL color: must start with 'hsl(' or 'hsla(', got '%s'", hsl_str);
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
// Skip "hsl(" or "hsla("
|
|
609
|
+
const char *p = hsl_str; // "hsl(120, 100%, 50%, 0.75)"
|
|
610
|
+
if (*p == 'h' && *(p+1) == 's' && *(p+2) == 'l') { // "hsl"
|
|
611
|
+
p += 3; // "(120, 100%, 50%, 0.75)"
|
|
612
|
+
if (*p == 'a') p++; // skip 'a' if hsla
|
|
613
|
+
if (*p == '(') p++; // "120, 100%, 50%, 0.75)"
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
int hue, sat_int, light_int;
|
|
617
|
+
double saturation, lightness;
|
|
618
|
+
double alpha = -1.0;
|
|
619
|
+
|
|
620
|
+
// Parse hue (0-360)
|
|
621
|
+
SKIP_WHITESPACE(p); // "120, 100%, 50%, 0.75)"
|
|
622
|
+
PARSE_INT(p, hue); // hue=120, p=", 100%, 50%, 0.75)"
|
|
623
|
+
|
|
624
|
+
// Parse saturation (0-100%)
|
|
625
|
+
SKIP_SEPARATOR(p); // "100%, 50%, 0.75)"
|
|
626
|
+
PARSE_INT(p, sat_int); // sat_int=100, p="%, 50%, 0.75)"
|
|
627
|
+
saturation = sat_int;
|
|
628
|
+
if (*p == '%') p++; // " 50%, 0.75)"
|
|
629
|
+
|
|
630
|
+
// Parse lightness (0-100%)
|
|
631
|
+
SKIP_SEPARATOR(p); // "50%, 0.75)"
|
|
632
|
+
PARSE_INT(p, light_int); // light_int=50, p="%, 0.75)"
|
|
633
|
+
lightness = light_int;
|
|
634
|
+
if (*p == '%') p++; // " 0.75)"
|
|
635
|
+
|
|
636
|
+
// Check for alpha
|
|
637
|
+
SKIP_SEPARATOR(p); // "0.75)" or ", 0.75)" or ", / 0.75)"
|
|
638
|
+
if (*p == '/') { // '/'
|
|
639
|
+
p++; // " 0.75)"
|
|
640
|
+
SKIP_WHITESPACE(p); // "0.75)"
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
if (*p >= '0' && *p <= '9') { // '0'
|
|
644
|
+
alpha = 0.0;
|
|
645
|
+
while (*p >= '0' && *p <= '9') { // '0' (integer part)
|
|
646
|
+
alpha = alpha * 10.0 + (*p - '0'); // alpha=0.0
|
|
647
|
+
p++; // ".75)"
|
|
648
|
+
}
|
|
649
|
+
if (*p == '.') { // '.'
|
|
650
|
+
p++; // "75)"
|
|
651
|
+
double decimal = 0.1;
|
|
652
|
+
while (*p >= '0' && *p <= '9') { // '7', then '5'
|
|
653
|
+
alpha += (*p - '0') * decimal; // alpha=0.75
|
|
654
|
+
decimal /= 10.0; // decimal=0.01
|
|
655
|
+
p++; // "5)" then ")"
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
// Convert HSL to RGB
|
|
661
|
+
// Normalize saturation and lightness to 0.0-1.0
|
|
662
|
+
saturation /= 100.0;
|
|
663
|
+
lightness /= 100.0;
|
|
664
|
+
|
|
665
|
+
// Normalize hue to 0-360 range
|
|
666
|
+
hue = hue % 360;
|
|
667
|
+
if (hue < 0) hue += 360;
|
|
668
|
+
|
|
669
|
+
struct color_ir color;
|
|
670
|
+
INIT_COLOR_IR(color);
|
|
671
|
+
color.alpha = alpha;
|
|
672
|
+
|
|
673
|
+
double c = (1.0 - fabs(2.0 * lightness - 1.0)) * saturation; // TODO: Document
|
|
674
|
+
double x = c * (1.0 - fabs(fmod(hue / 60.0, 2.0) - 1.0));
|
|
675
|
+
double m = lightness - c / 2.0;
|
|
676
|
+
|
|
677
|
+
double red_prime, green_prime, blue_prime;
|
|
678
|
+
|
|
679
|
+
if (hue >= 0 && hue < 60) {
|
|
680
|
+
red_prime = c; green_prime = x; blue_prime = 0;
|
|
681
|
+
} else if (hue >= 60 && hue < 120) {
|
|
682
|
+
red_prime = x; green_prime = c; blue_prime = 0;
|
|
683
|
+
} else if (hue >= 120 && hue < 180) {
|
|
684
|
+
red_prime = 0; green_prime = c; blue_prime = x;
|
|
685
|
+
} else if (hue >= 180 && hue < 240) {
|
|
686
|
+
red_prime = 0; green_prime = x; blue_prime = c;
|
|
687
|
+
} else if (hue >= 240 && hue < 300) {
|
|
688
|
+
red_prime = x; green_prime = 0; blue_prime = c;
|
|
689
|
+
} else {
|
|
690
|
+
red_prime = c; green_prime = 0; blue_prime = x;
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
color.red = (int)((red_prime + m) * 255.0 + 0.5);
|
|
694
|
+
color.green = (int)((green_prime + m) * 255.0 + 0.5);
|
|
695
|
+
color.blue = (int)((blue_prime + m) * 255.0 + 0.5);
|
|
696
|
+
|
|
697
|
+
return color;
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
// Format intermediate representation to HSL string
|
|
701
|
+
// color: color_ir struct with RGB (0-255) and optional alpha (0.0-1.0)
|
|
702
|
+
// use_modern_syntax: unused for HSL format (HSL format doesn't have variants like RGB)
|
|
703
|
+
// Returns: Ruby string with HSL value like "hsl(0, 100%, 50%)"
|
|
704
|
+
static VALUE format_hsl(struct color_ir color, int use_modern_syntax) {
|
|
705
|
+
(void)use_modern_syntax; // Unused - HSL format doesn't have variants
|
|
706
|
+
|
|
707
|
+
// Convert RGB to HSL
|
|
708
|
+
double red = color.red / 255.0;
|
|
709
|
+
double green = color.green / 255.0;
|
|
710
|
+
double blue = color.blue / 255.0;
|
|
711
|
+
|
|
712
|
+
double max = red > green ? (red > blue ? red : blue) : (green > blue ? green : blue);
|
|
713
|
+
double min = red < green ? (red < blue ? red : blue) : (green < blue ? green : blue);
|
|
714
|
+
double delta = max - min;
|
|
715
|
+
|
|
716
|
+
double hue = 0.0;
|
|
717
|
+
double saturation = 0.0;
|
|
718
|
+
double lightness = (max + min) / 2.0;
|
|
719
|
+
|
|
720
|
+
if (delta > 0.0001) { // Not grayscale
|
|
721
|
+
saturation = lightness > 0.5 ? delta / (2.0 - max - min) : delta / (max + min);
|
|
722
|
+
|
|
723
|
+
if (max == red) {
|
|
724
|
+
hue = 60.0 * fmod((green - blue) / delta, 6.0);
|
|
725
|
+
} else if (max == green) {
|
|
726
|
+
hue = 60.0 * ((blue - red) / delta + 2.0);
|
|
727
|
+
} else {
|
|
728
|
+
hue = 60.0 * ((red - green) / delta + 4.0);
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
if (hue < 0) hue += 360.0;
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
int hue_int = (int)(hue + 0.5);
|
|
735
|
+
int sat_int = (int)(saturation * 100.0 + 0.5);
|
|
736
|
+
int light_int = (int)(lightness * 100.0 + 0.5);
|
|
737
|
+
|
|
738
|
+
char hsl_buf[64];
|
|
739
|
+
if (color.alpha >= 0.0) {
|
|
740
|
+
FORMAT_HSLA(hsl_buf, hue_int, sat_int, light_int, color.alpha);
|
|
741
|
+
} else {
|
|
742
|
+
FORMAT_HSL(hsl_buf, hue_int, sat_int, light_int);
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
return rb_str_new_cstr(hsl_buf);
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
// Parse HWB color string to intermediate representation
|
|
749
|
+
// hwb_value: Ruby string like "hwb(0 0% 0%)" or "hwb(120 30% 20% / 0.5)"
|
|
750
|
+
// Returns: color_ir struct with RGB (0-255) and optional alpha (0.0-1.0)
|
|
751
|
+
static struct color_ir parse_hwb(VALUE hwb_value) {
|
|
752
|
+
Check_Type(hwb_value, T_STRING);
|
|
753
|
+
|
|
754
|
+
const char *hwb_str = RSTRING_PTR(hwb_value);
|
|
755
|
+
long hwb_len = RSTRING_LEN(hwb_value);
|
|
756
|
+
|
|
757
|
+
if (hwb_len < 10 || (strncmp(hwb_str, "hwb(", 4) != 0 && strncmp(hwb_str, "hwba(", 5) != 0)) {
|
|
758
|
+
rb_raise(rb_eColorConversionError, "Invalid HWB color: must start with 'hwb(' or 'hwba(', got '%s'", hwb_str);
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
// Skip "hwb(" or "hwba("
|
|
762
|
+
const char *p = hwb_str; // "hwb(120 30% 20% / 0.5)"
|
|
763
|
+
if (*p == 'h' && *(p+1) == 'w' && *(p+2) == 'b') { // "hwb"
|
|
764
|
+
p += 3; // "(120 30% 20% / 0.5)"
|
|
765
|
+
if (*p == 'a') p++; // skip 'a' if hwba
|
|
766
|
+
if (*p == '(') p++; // "120 30% 20% / 0.5)"
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
int hue, white_int, black_int;
|
|
770
|
+
double whiteness, blackness;
|
|
771
|
+
double alpha = -1.0;
|
|
772
|
+
|
|
773
|
+
// Parse hue (0-360)
|
|
774
|
+
SKIP_WHITESPACE(p); // "120 30% 20% / 0.5)"
|
|
775
|
+
PARSE_INT(p, hue); // hue=120, p=" 30% 20% / 0.5)"
|
|
776
|
+
|
|
777
|
+
// Parse whiteness (0-100%)
|
|
778
|
+
SKIP_SEPARATOR(p); // "30% 20% / 0.5)"
|
|
779
|
+
PARSE_INT(p, white_int); // white_int=30, p="% 20% / 0.5)"
|
|
780
|
+
whiteness = white_int / 100.0;
|
|
781
|
+
if (*p == '%') p++; // " 20% / 0.5)"
|
|
782
|
+
|
|
783
|
+
// Parse blackness (0-100%)
|
|
784
|
+
SKIP_SEPARATOR(p); // "20% / 0.5)"
|
|
785
|
+
PARSE_INT(p, black_int); // black_int=20, p="% / 0.5)"
|
|
786
|
+
blackness = black_int / 100.0;
|
|
787
|
+
if (*p == '%') p++; // " / 0.5)"
|
|
788
|
+
|
|
789
|
+
// Check for alpha
|
|
790
|
+
SKIP_SEPARATOR(p); // "/ 0.5)" or "0.5)" or ")"
|
|
791
|
+
if (*p == '/') { // '/'
|
|
792
|
+
p++; // " 0.5)"
|
|
793
|
+
SKIP_WHITESPACE(p); // "0.5)"
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
if (*p >= '0' && *p <= '9') { // '0'
|
|
797
|
+
alpha = 0.0;
|
|
798
|
+
while (*p >= '0' && *p <= '9') { // '0' (integer part)
|
|
799
|
+
alpha = alpha * 10.0 + (*p - '0'); // alpha=0.0
|
|
800
|
+
p++; // ".5)"
|
|
801
|
+
}
|
|
802
|
+
if (*p == '.') { // '.'
|
|
803
|
+
p++; // "5)"
|
|
804
|
+
double decimal = 0.1;
|
|
805
|
+
while (*p >= '0' && *p <= '9') { // '5'
|
|
806
|
+
alpha += (*p - '0') * decimal; // alpha=0.5
|
|
807
|
+
decimal /= 10.0; // decimal=0.01
|
|
808
|
+
p++; // ")"
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
// Normalize W+B if > 100%
|
|
814
|
+
double wb_sum = whiteness + blackness;
|
|
815
|
+
if (wb_sum > 1.0) {
|
|
816
|
+
whiteness /= wb_sum;
|
|
817
|
+
blackness /= wb_sum;
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
// Convert HWB to RGB via HSL intermediate
|
|
821
|
+
// First convert hue to RGB with S=100%, L=50% (fully saturated color)
|
|
822
|
+
hue = hue % 360;
|
|
823
|
+
if (hue < 0) hue += 360;
|
|
824
|
+
|
|
825
|
+
// Use HSL→RGB conversion with saturation=1.0, lightness=0.5
|
|
826
|
+
double c = 1.0; // chroma at full saturation and 50% lightness
|
|
827
|
+
double x = c * (1.0 - fabs(fmod(hue / 60.0, 2.0) - 1.0));
|
|
828
|
+
double m = 0.0; // no adjustment needed for L=0.5
|
|
829
|
+
|
|
830
|
+
double red_prime, green_prime, blue_prime;
|
|
831
|
+
|
|
832
|
+
if (hue >= 0 && hue < 60) {
|
|
833
|
+
red_prime = c; green_prime = x; blue_prime = 0;
|
|
834
|
+
} else if (hue >= 60 && hue < 120) {
|
|
835
|
+
red_prime = x; green_prime = c; blue_prime = 0;
|
|
836
|
+
} else if (hue >= 120 && hue < 180) {
|
|
837
|
+
red_prime = 0; green_prime = c; blue_prime = x;
|
|
838
|
+
} else if (hue >= 180 && hue < 240) {
|
|
839
|
+
red_prime = 0; green_prime = x; blue_prime = c;
|
|
840
|
+
} else if (hue >= 240 && hue < 300) {
|
|
841
|
+
red_prime = x; green_prime = 0; blue_prime = c;
|
|
842
|
+
} else {
|
|
843
|
+
red_prime = c; green_prime = 0; blue_prime = x;
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
// Apply HWB transformation: rgb = rgb * (1 - W - B) + W
|
|
847
|
+
struct color_ir color;
|
|
848
|
+
INIT_COLOR_IR(color);
|
|
849
|
+
color.red = (int)(((red_prime + m) * (1.0 - whiteness - blackness) + whiteness) * 255.0 + 0.5);
|
|
850
|
+
color.green = (int)(((green_prime + m) * (1.0 - whiteness - blackness) + whiteness) * 255.0 + 0.5);
|
|
851
|
+
color.blue = (int)(((blue_prime + m) * (1.0 - whiteness - blackness) + whiteness) * 255.0 + 0.5);
|
|
852
|
+
color.alpha = alpha;
|
|
853
|
+
|
|
854
|
+
return color;
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
// Format intermediate representation to HWB string
|
|
858
|
+
// color: color_ir struct with RGB (0-255) and optional alpha (0.0-1.0)
|
|
859
|
+
// use_modern_syntax: unused for HWB format
|
|
860
|
+
// Returns: Ruby string with HWB value like "hwb(0 0% 0%)"
|
|
861
|
+
static VALUE format_hwb(struct color_ir color, int use_modern_syntax) {
|
|
862
|
+
(void)use_modern_syntax; // Unused - HWB format doesn't have variants
|
|
863
|
+
|
|
864
|
+
// Convert RGB to HWB
|
|
865
|
+
double red = color.red / 255.0;
|
|
866
|
+
double green = color.green / 255.0;
|
|
867
|
+
double blue = color.blue / 255.0;
|
|
868
|
+
|
|
869
|
+
double max = red > green ? (red > blue ? red : blue) : (green > blue ? green : blue);
|
|
870
|
+
double min = red < green ? (red < blue ? red : blue) : (green < blue ? green : blue);
|
|
871
|
+
double delta = max - min;
|
|
872
|
+
|
|
873
|
+
// Calculate hue (same as HSL)
|
|
874
|
+
double hue = 0.0;
|
|
875
|
+
if (delta > 0.0001) { // Not grayscale
|
|
876
|
+
if (max == red) {
|
|
877
|
+
hue = 60.0 * fmod((green - blue) / delta, 6.0);
|
|
878
|
+
} else if (max == green) {
|
|
879
|
+
hue = 60.0 * ((blue - red) / delta + 2.0);
|
|
880
|
+
} else {
|
|
881
|
+
hue = 60.0 * ((red - green) / delta + 4.0);
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
if (hue < 0) hue += 360.0;
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
// Whiteness = min component, Blackness = 1 - max component
|
|
888
|
+
double whiteness = min;
|
|
889
|
+
double blackness = 1.0 - max;
|
|
890
|
+
|
|
891
|
+
// Per W3C spec: if white + black >= 1, the color is achromatic (hue is undefined)
|
|
892
|
+
// https://www.w3.org/TR/css-color-4/#the-hwb-notation
|
|
893
|
+
// Use epsilon to account for floating-point rounding errors
|
|
894
|
+
double epsilon = 1.0 / 100000.0;
|
|
895
|
+
if (whiteness + blackness >= 1.0 - epsilon) {
|
|
896
|
+
hue = 0.0; // CSS serializes NaN as 0
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
int hue_int = (int)(hue + 0.5);
|
|
900
|
+
int white_int = (int)(whiteness * 100.0 + 0.5);
|
|
901
|
+
int black_int = (int)(blackness * 100.0 + 0.5);
|
|
902
|
+
|
|
903
|
+
char hwb_buf[64];
|
|
904
|
+
if (color.alpha >= 0.0) {
|
|
905
|
+
FORMAT_HWBA(hwb_buf, hue_int, white_int, black_int, color.alpha);
|
|
906
|
+
} else {
|
|
907
|
+
FORMAT_HWB(hwb_buf, hue_int, white_int, black_int);
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
return rb_str_new_cstr(hwb_buf);
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
// Get parser function for a given format
|
|
914
|
+
static color_parser_fn get_parser(VALUE format) {
|
|
915
|
+
ID format_id = SYM2ID(format);
|
|
916
|
+
ID hex_id = rb_intern("hex");
|
|
917
|
+
ID rgb_id = rb_intern("rgb");
|
|
918
|
+
ID hsl_id = rb_intern("hsl");
|
|
919
|
+
ID hwb_id = rb_intern("hwb");
|
|
920
|
+
ID oklab_id = rb_intern("oklab");
|
|
921
|
+
ID oklch_id = rb_intern("oklch");
|
|
922
|
+
ID lab_id = rb_intern("lab");
|
|
923
|
+
ID lch_id = rb_intern("lch");
|
|
924
|
+
ID named_id = rb_intern("named");
|
|
925
|
+
|
|
926
|
+
if (format_id == hex_id) {
|
|
927
|
+
return parse_hex;
|
|
928
|
+
}
|
|
929
|
+
if (format_id == rgb_id) {
|
|
930
|
+
return parse_rgb;
|
|
931
|
+
}
|
|
932
|
+
if (format_id == hsl_id) {
|
|
933
|
+
return parse_hsl;
|
|
934
|
+
}
|
|
935
|
+
if (format_id == hwb_id) {
|
|
936
|
+
return parse_hwb;
|
|
937
|
+
}
|
|
938
|
+
if (format_id == oklab_id) {
|
|
939
|
+
return parse_oklab;
|
|
940
|
+
}
|
|
941
|
+
if (format_id == oklch_id) {
|
|
942
|
+
return parse_oklch;
|
|
943
|
+
}
|
|
944
|
+
if (format_id == lab_id) {
|
|
945
|
+
return parse_lab;
|
|
946
|
+
}
|
|
947
|
+
if (format_id == lch_id) {
|
|
948
|
+
return parse_lch;
|
|
949
|
+
}
|
|
950
|
+
if (format_id == named_id) {
|
|
951
|
+
return parse_named;
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
return NULL;
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
// Get formatter function for a given format
|
|
958
|
+
static color_formatter_fn get_formatter(VALUE format) {
|
|
959
|
+
ID format_id = SYM2ID(format);
|
|
960
|
+
ID hex_id = rb_intern("hex");
|
|
961
|
+
ID rgb_id = rb_intern("rgb");
|
|
962
|
+
ID rgba_id = rb_intern("rgba");
|
|
963
|
+
ID hsl_id = rb_intern("hsl");
|
|
964
|
+
ID hsla_id = rb_intern("hsla");
|
|
965
|
+
ID hwb_id = rb_intern("hwb");
|
|
966
|
+
ID hwba_id = rb_intern("hwba");
|
|
967
|
+
ID oklab_id = rb_intern("oklab");
|
|
968
|
+
ID oklch_id = rb_intern("oklch");
|
|
969
|
+
ID lab_id = rb_intern("lab");
|
|
970
|
+
ID lch_id = rb_intern("lch");
|
|
971
|
+
|
|
972
|
+
if (format_id == hex_id) {
|
|
973
|
+
return format_hex;
|
|
974
|
+
}
|
|
975
|
+
if (format_id == rgb_id || format_id == rgba_id) {
|
|
976
|
+
return format_rgb;
|
|
977
|
+
}
|
|
978
|
+
if (format_id == hsl_id || format_id == hsla_id) {
|
|
979
|
+
return format_hsl;
|
|
980
|
+
}
|
|
981
|
+
if (format_id == hwb_id || format_id == hwba_id) {
|
|
982
|
+
return format_hwb;
|
|
983
|
+
}
|
|
984
|
+
if (format_id == oklab_id) {
|
|
985
|
+
return format_oklab;
|
|
986
|
+
}
|
|
987
|
+
if (format_id == oklch_id) {
|
|
988
|
+
return format_oklch;
|
|
989
|
+
}
|
|
990
|
+
if (format_id == lab_id) {
|
|
991
|
+
return format_lab;
|
|
992
|
+
}
|
|
993
|
+
if (format_id == lch_id) {
|
|
994
|
+
return format_lch;
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
return NULL;
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
// Convert a value that may contain multiple colors or colors mixed with other values
|
|
1001
|
+
// (e.g., "border-color: #fff #000 #ccc" or "box-shadow: 0 0 10px #ff0000")
|
|
1002
|
+
// parser: specific parser to use (e.g., parse_hex), or NULL for auto-detect all formats
|
|
1003
|
+
// Returns a new Ruby string with all colors converted, or Qnil if no colors found
|
|
1004
|
+
static VALUE convert_value_with_colors(VALUE value, color_parser_fn parser, color_formatter_fn formatter, int use_modern_syntax) {
|
|
1005
|
+
if (NIL_P(value) || TYPE(value) != T_STRING) {
|
|
1006
|
+
return Qnil;
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
const char *input = RSTRING_PTR(value);
|
|
1010
|
+
long input_len = RSTRING_LEN(value);
|
|
1011
|
+
|
|
1012
|
+
DEBUG_PRINTF("convert_value_with_colors: input='%.*s' parser=%p formatter=%p\n", (int)input_len, input, (void*)parser, (void*)formatter);
|
|
1013
|
+
|
|
1014
|
+
// Build output string with converted colors
|
|
1015
|
+
VALUE result = rb_str_buf_new(input_len * 2); // Allocate generous space
|
|
1016
|
+
long pos = 0;
|
|
1017
|
+
int found_color = 0;
|
|
1018
|
+
int in_url = 0; // Track if we're inside url()
|
|
1019
|
+
int url_paren_depth = 0; // Track paren depth inside url()
|
|
1020
|
+
|
|
1021
|
+
while (pos < input_len) {
|
|
1022
|
+
const char *p = input + pos;
|
|
1023
|
+
long remaining = input_len - pos;
|
|
1024
|
+
long color_len = 0;
|
|
1025
|
+
|
|
1026
|
+
// Check for url( to skip content inside URLs
|
|
1027
|
+
if (!in_url && remaining >= 4 && p[0] == 'u' && p[1] == 'r' && p[2] == 'l' && p[3] == '(') {
|
|
1028
|
+
in_url = 1;
|
|
1029
|
+
url_paren_depth = 1; // Start counting from the url's opening paren
|
|
1030
|
+
// Copy "url("
|
|
1031
|
+
rb_str_buf_cat(result, p, 4);
|
|
1032
|
+
pos += 4;
|
|
1033
|
+
continue;
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
// If we're inside url(), track parens and copy as-is
|
|
1037
|
+
if (in_url) {
|
|
1038
|
+
if (*p == '(') {
|
|
1039
|
+
url_paren_depth++;
|
|
1040
|
+
} else if (*p == ')') {
|
|
1041
|
+
url_paren_depth--;
|
|
1042
|
+
if (url_paren_depth == 0) {
|
|
1043
|
+
in_url = 0; // Exiting url()
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
1046
|
+
rb_str_buf_cat(result, &input[pos], 1);
|
|
1047
|
+
pos++;
|
|
1048
|
+
continue;
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
// Skip whitespace and preserve it
|
|
1052
|
+
while (pos < input_len && (input[pos] == ' ' || input[pos] == '\t')) {
|
|
1053
|
+
rb_str_buf_cat(result, &input[pos], 1);
|
|
1054
|
+
pos++;
|
|
1055
|
+
p = input + pos;
|
|
1056
|
+
remaining = input_len - pos;
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
if (pos >= input_len) break;
|
|
1060
|
+
|
|
1061
|
+
// Check for hex color
|
|
1062
|
+
if (*p == '#' && (parser == NULL || parser == parse_hex)) {
|
|
1063
|
+
// Find the end of the hex color (next space, comma, or delimiter)
|
|
1064
|
+
const char *end = p + 1;
|
|
1065
|
+
while (*end && *end != ' ' && *end != ',' && *end != ';' && *end != ')' && *end != '\n') {
|
|
1066
|
+
end++;
|
|
1067
|
+
}
|
|
1068
|
+
color_len = end - p;
|
|
1069
|
+
|
|
1070
|
+
// Parse and convert hex color
|
|
1071
|
+
VALUE hex_str = rb_str_new(p, color_len);
|
|
1072
|
+
struct color_ir color = parse_hex(hex_str);
|
|
1073
|
+
VALUE converted = formatter(color, use_modern_syntax);
|
|
1074
|
+
rb_str_buf_append(result, converted);
|
|
1075
|
+
pos += color_len;
|
|
1076
|
+
found_color = 1;
|
|
1077
|
+
continue;
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
// Check for rgb/rgba
|
|
1081
|
+
if (STARTS_WITH_RGB(p, remaining) && (parser == NULL || parser == parse_rgb)) {
|
|
1082
|
+
const char *end;
|
|
1083
|
+
FIND_CLOSING_PAREN(p, end);
|
|
1084
|
+
color_len = end - p;
|
|
1085
|
+
|
|
1086
|
+
// Skip if contains unparseable content (calc, none, etc.)
|
|
1087
|
+
int skip;
|
|
1088
|
+
HAS_UNPARSEABLE_CONTENT(p, color_len, skip);
|
|
1089
|
+
if (skip) {
|
|
1090
|
+
rb_str_buf_append(result, rb_str_new(p, color_len));
|
|
1091
|
+
pos += color_len;
|
|
1092
|
+
found_color = 1;
|
|
1093
|
+
continue;
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
VALUE rgb_str = rb_str_new(p, color_len);
|
|
1097
|
+
struct color_ir color = parse_rgb(rgb_str);
|
|
1098
|
+
VALUE converted = formatter(color, use_modern_syntax);
|
|
1099
|
+
rb_str_buf_append(result, converted);
|
|
1100
|
+
pos += color_len;
|
|
1101
|
+
found_color = 1;
|
|
1102
|
+
continue;
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
// Check for hsl/hsla
|
|
1106
|
+
if (STARTS_WITH_HSL(p, remaining) && (parser == NULL || parser == parse_hsl)) {
|
|
1107
|
+
const char *end;
|
|
1108
|
+
FIND_CLOSING_PAREN(p, end);
|
|
1109
|
+
color_len = end - p;
|
|
1110
|
+
|
|
1111
|
+
// Skip if contains unparseable content (calc, none, etc.)
|
|
1112
|
+
int skip;
|
|
1113
|
+
HAS_UNPARSEABLE_CONTENT(p, color_len, skip);
|
|
1114
|
+
if (skip) {
|
|
1115
|
+
rb_str_buf_append(result, rb_str_new(p, color_len));
|
|
1116
|
+
pos += color_len;
|
|
1117
|
+
found_color = 1;
|
|
1118
|
+
continue;
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1121
|
+
VALUE hsl_str = rb_str_new(p, color_len);
|
|
1122
|
+
struct color_ir color = parse_hsl(hsl_str);
|
|
1123
|
+
VALUE converted = formatter(color, use_modern_syntax);
|
|
1124
|
+
rb_str_buf_append(result, converted);
|
|
1125
|
+
pos += color_len;
|
|
1126
|
+
found_color = 1;
|
|
1127
|
+
continue;
|
|
1128
|
+
}
|
|
1129
|
+
|
|
1130
|
+
// Check for hwb/hwba
|
|
1131
|
+
if (STARTS_WITH_HWB(p, remaining) && (parser == NULL || parser == parse_hwb)) {
|
|
1132
|
+
const char *end;
|
|
1133
|
+
FIND_CLOSING_PAREN(p, end);
|
|
1134
|
+
color_len = end - p;
|
|
1135
|
+
|
|
1136
|
+
// Skip if contains unparseable content (calc, none, etc.)
|
|
1137
|
+
int skip;
|
|
1138
|
+
HAS_UNPARSEABLE_CONTENT(p, color_len, skip);
|
|
1139
|
+
if (skip) {
|
|
1140
|
+
rb_str_buf_append(result, rb_str_new(p, color_len));
|
|
1141
|
+
pos += color_len;
|
|
1142
|
+
found_color = 1;
|
|
1143
|
+
continue;
|
|
1144
|
+
}
|
|
1145
|
+
|
|
1146
|
+
VALUE hwb_str = rb_str_new(p, color_len);
|
|
1147
|
+
struct color_ir color = parse_hwb(hwb_str);
|
|
1148
|
+
VALUE converted = formatter(color, use_modern_syntax);
|
|
1149
|
+
rb_str_buf_append(result, converted);
|
|
1150
|
+
pos += color_len;
|
|
1151
|
+
found_color = 1;
|
|
1152
|
+
continue;
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1155
|
+
// Check for oklab
|
|
1156
|
+
if (STARTS_WITH_OKLAB(p, remaining) && (parser == NULL || parser == parse_oklab)) {
|
|
1157
|
+
const char *end;
|
|
1158
|
+
FIND_CLOSING_PAREN(p, end);
|
|
1159
|
+
color_len = end - p;
|
|
1160
|
+
|
|
1161
|
+
// Skip if contains unparseable content (calc, none, etc.)
|
|
1162
|
+
int skip;
|
|
1163
|
+
HAS_UNPARSEABLE_CONTENT(p, color_len, skip);
|
|
1164
|
+
if (skip) {
|
|
1165
|
+
rb_str_buf_append(result, rb_str_new(p, color_len));
|
|
1166
|
+
pos += color_len;
|
|
1167
|
+
found_color = 1;
|
|
1168
|
+
continue;
|
|
1169
|
+
}
|
|
1170
|
+
|
|
1171
|
+
VALUE oklab_str = rb_str_new(p, color_len);
|
|
1172
|
+
struct color_ir color = parse_oklab(oklab_str);
|
|
1173
|
+
VALUE converted = formatter(color, use_modern_syntax);
|
|
1174
|
+
rb_str_buf_append(result, converted);
|
|
1175
|
+
pos += color_len;
|
|
1176
|
+
found_color = 1;
|
|
1177
|
+
continue;
|
|
1178
|
+
}
|
|
1179
|
+
|
|
1180
|
+
// Check for oklch
|
|
1181
|
+
if (STARTS_WITH_OKLCH(p, remaining) && (parser == NULL || parser == parse_oklch)) {
|
|
1182
|
+
const char *end;
|
|
1183
|
+
FIND_CLOSING_PAREN(p, end);
|
|
1184
|
+
color_len = end - p;
|
|
1185
|
+
|
|
1186
|
+
// Skip if contains unparseable content (calc, none, etc.)
|
|
1187
|
+
int skip;
|
|
1188
|
+
HAS_UNPARSEABLE_CONTENT(p, color_len, skip);
|
|
1189
|
+
if (skip) {
|
|
1190
|
+
rb_str_buf_append(result, rb_str_new(p, color_len));
|
|
1191
|
+
pos += color_len;
|
|
1192
|
+
found_color = 1;
|
|
1193
|
+
continue;
|
|
1194
|
+
}
|
|
1195
|
+
|
|
1196
|
+
VALUE oklch_str = rb_str_new(p, color_len);
|
|
1197
|
+
struct color_ir color = parse_oklch(oklch_str);
|
|
1198
|
+
VALUE converted = formatter(color, use_modern_syntax);
|
|
1199
|
+
rb_str_buf_append(result, converted);
|
|
1200
|
+
pos += color_len;
|
|
1201
|
+
found_color = 1;
|
|
1202
|
+
continue;
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1205
|
+
// Check for lch (must check before lab since both start with 'l')
|
|
1206
|
+
if (STARTS_WITH_LCH(p, remaining) && (parser == NULL || parser == parse_lch)) {
|
|
1207
|
+
const char *end;
|
|
1208
|
+
FIND_CLOSING_PAREN(p, end);
|
|
1209
|
+
color_len = end - p;
|
|
1210
|
+
|
|
1211
|
+
// Skip if contains unparseable content (calc, none, etc.)
|
|
1212
|
+
int skip;
|
|
1213
|
+
HAS_UNPARSEABLE_CONTENT(p, color_len, skip);
|
|
1214
|
+
if (skip) {
|
|
1215
|
+
rb_str_buf_append(result, rb_str_new(p, color_len));
|
|
1216
|
+
pos += color_len;
|
|
1217
|
+
found_color = 1;
|
|
1218
|
+
continue;
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1221
|
+
VALUE lch_str = rb_str_new(p, color_len);
|
|
1222
|
+
struct color_ir color = parse_lch(lch_str);
|
|
1223
|
+
VALUE converted = formatter(color, use_modern_syntax);
|
|
1224
|
+
rb_str_buf_append(result, converted);
|
|
1225
|
+
pos += color_len;
|
|
1226
|
+
found_color = 1;
|
|
1227
|
+
continue;
|
|
1228
|
+
}
|
|
1229
|
+
|
|
1230
|
+
// Check for lab
|
|
1231
|
+
if (STARTS_WITH_LAB(p, remaining) && (parser == NULL || parser == parse_lab)) {
|
|
1232
|
+
const char *end;
|
|
1233
|
+
FIND_CLOSING_PAREN(p, end);
|
|
1234
|
+
color_len = end - p;
|
|
1235
|
+
|
|
1236
|
+
// Skip if contains unparseable content (calc, none, etc.)
|
|
1237
|
+
int skip;
|
|
1238
|
+
HAS_UNPARSEABLE_CONTENT(p, color_len, skip);
|
|
1239
|
+
if (skip) {
|
|
1240
|
+
rb_str_buf_append(result, rb_str_new(p, color_len));
|
|
1241
|
+
pos += color_len;
|
|
1242
|
+
found_color = 1;
|
|
1243
|
+
continue;
|
|
1244
|
+
}
|
|
1245
|
+
|
|
1246
|
+
VALUE lab_str = rb_str_new(p, color_len);
|
|
1247
|
+
struct color_ir color = parse_lab(lab_str);
|
|
1248
|
+
VALUE converted = formatter(color, use_modern_syntax);
|
|
1249
|
+
rb_str_buf_append(result, converted);
|
|
1250
|
+
pos += color_len;
|
|
1251
|
+
found_color = 1;
|
|
1252
|
+
continue;
|
|
1253
|
+
}
|
|
1254
|
+
|
|
1255
|
+
// Check for named colors (alphabetic, length 3-20)
|
|
1256
|
+
// Try early to catch valid color names, skip word if not found
|
|
1257
|
+
if (((*p >= 'a' && *p <= 'z') || (*p >= 'A' && *p <= 'Z')) && (parser == NULL || parser == parse_named)) {
|
|
1258
|
+
DEBUG_PRINTF("Checking alphabetic word at pos %ld, parser=%p, parse_named=%p\n", pos, (void*)parser, (void*)parse_named);
|
|
1259
|
+
// Find the end of the alphabetic word
|
|
1260
|
+
const char *end = p + 1;
|
|
1261
|
+
while (*end && ((*end >= 'a' && *end <= 'z') || (*end >= 'A' && *end <= 'Z'))) {
|
|
1262
|
+
end++;
|
|
1263
|
+
}
|
|
1264
|
+
color_len = end - p;
|
|
1265
|
+
DEBUG_PRINTF("Word length = %ld\n", color_len);
|
|
1266
|
+
|
|
1267
|
+
// Named colors are 3-20 characters (red to lightgoldenrodyellow)
|
|
1268
|
+
if (color_len >= 3 && color_len <= 20) {
|
|
1269
|
+
VALUE named_str = rb_str_new(p, color_len);
|
|
1270
|
+
DEBUG_PRINTF("Trying to parse: '%s'\n", StringValueCStr(named_str));
|
|
1271
|
+
struct color_ir color = parse_named(named_str);
|
|
1272
|
+
DEBUG_PRINTF("parse_named returned red=%d\n", color.red);
|
|
1273
|
+
|
|
1274
|
+
// Check if valid (red >= 0 means found)
|
|
1275
|
+
if (color.red >= 0) {
|
|
1276
|
+
DEBUG_PRINTF("Valid color! Converting...\n");
|
|
1277
|
+
VALUE converted = formatter(color, use_modern_syntax);
|
|
1278
|
+
rb_str_buf_append(result, converted);
|
|
1279
|
+
pos += color_len;
|
|
1280
|
+
found_color = 1;
|
|
1281
|
+
continue;
|
|
1282
|
+
}
|
|
1283
|
+
|
|
1284
|
+
DEBUG_PRINTF("Not a valid color, copying word\n");
|
|
1285
|
+
// Not a valid color - copy the whole word as-is and continue
|
|
1286
|
+
rb_str_buf_cat(result, p, color_len);
|
|
1287
|
+
pos += color_len;
|
|
1288
|
+
continue;
|
|
1289
|
+
}
|
|
1290
|
+
}
|
|
1291
|
+
|
|
1292
|
+
// Not a color - copy character as-is
|
|
1293
|
+
rb_str_buf_cat(result, &input[pos], 1);
|
|
1294
|
+
pos++;
|
|
1295
|
+
}
|
|
1296
|
+
|
|
1297
|
+
RB_GC_GUARD(value); // Prevent GC of value while we hold input pointer
|
|
1298
|
+
|
|
1299
|
+
DEBUG_PRINTF("Returning %s\n", found_color ? "result" : "Qnil (no colors found)");
|
|
1300
|
+
|
|
1301
|
+
return found_color ? result : Qnil;
|
|
1302
|
+
}
|
|
1303
|
+
|
|
1304
|
+
// Auto-detect the color format of a value string
|
|
1305
|
+
// Returns the appropriate parser function, or NULL if not recognized
|
|
1306
|
+
static color_parser_fn detect_color_format(VALUE value) {
|
|
1307
|
+
if (NIL_P(value) || TYPE(value) != T_STRING) {
|
|
1308
|
+
return NULL;
|
|
1309
|
+
}
|
|
1310
|
+
|
|
1311
|
+
const char *val_str = RSTRING_PTR(value);
|
|
1312
|
+
long val_len = RSTRING_LEN(value);
|
|
1313
|
+
|
|
1314
|
+
if (val_len == 0) {
|
|
1315
|
+
return NULL;
|
|
1316
|
+
}
|
|
1317
|
+
|
|
1318
|
+
// Skip leading whitespace
|
|
1319
|
+
const char *p = val_str;
|
|
1320
|
+
while (*p == ' ' || *p == '\t') {
|
|
1321
|
+
p++;
|
|
1322
|
+
}
|
|
1323
|
+
|
|
1324
|
+
long remaining = val_len - (p - val_str);
|
|
1325
|
+
|
|
1326
|
+
// Check for hex (starts with #)
|
|
1327
|
+
if (*p == '#') {
|
|
1328
|
+
return parse_hex;
|
|
1329
|
+
}
|
|
1330
|
+
|
|
1331
|
+
// Check for rgb (starts with 'rgb')
|
|
1332
|
+
if (STARTS_WITH_RGB(p, remaining)) {
|
|
1333
|
+
return parse_rgb;
|
|
1334
|
+
}
|
|
1335
|
+
|
|
1336
|
+
// Check for hwb (starts with 'hwb')
|
|
1337
|
+
if (STARTS_WITH_HWB(p, remaining)) {
|
|
1338
|
+
return parse_hwb;
|
|
1339
|
+
}
|
|
1340
|
+
|
|
1341
|
+
// Check for hsl (starts with 'hsl')
|
|
1342
|
+
if (STARTS_WITH_HSL(p, remaining)) {
|
|
1343
|
+
return parse_hsl;
|
|
1344
|
+
}
|
|
1345
|
+
|
|
1346
|
+
// Check for oklab (starts with 'oklab')
|
|
1347
|
+
if (STARTS_WITH_OKLAB(p, remaining)) {
|
|
1348
|
+
return parse_oklab;
|
|
1349
|
+
}
|
|
1350
|
+
|
|
1351
|
+
// Check for oklch (starts with 'oklch')
|
|
1352
|
+
if (STARTS_WITH_OKLCH(p, remaining)) {
|
|
1353
|
+
return parse_oklch;
|
|
1354
|
+
}
|
|
1355
|
+
|
|
1356
|
+
// Check for lch (starts with 'lch(') - must check before lab
|
|
1357
|
+
if (STARTS_WITH_LCH(p, remaining)) {
|
|
1358
|
+
return parse_lch;
|
|
1359
|
+
}
|
|
1360
|
+
|
|
1361
|
+
// Check for lab (starts with 'lab(')
|
|
1362
|
+
if (STARTS_WITH_LAB(p, remaining)) {
|
|
1363
|
+
return parse_lab;
|
|
1364
|
+
}
|
|
1365
|
+
|
|
1366
|
+
// Check for named colors (fallback - alphabetic characters)
|
|
1367
|
+
// Named colors must start with a letter
|
|
1368
|
+
if ((*p >= 'a' && *p <= 'z') || (*p >= 'A' && *p <= 'Z')) {
|
|
1369
|
+
return parse_named;
|
|
1370
|
+
}
|
|
1371
|
+
|
|
1372
|
+
return NULL;
|
|
1373
|
+
}
|
|
1374
|
+
|
|
1375
|
+
// Check if a value matches a given color format
|
|
1376
|
+
// Returns 1 if it matches, 0 otherwise
|
|
1377
|
+
static int matches_color_format(VALUE value, VALUE format) {
|
|
1378
|
+
if (NIL_P(value) || TYPE(value) != T_STRING) {
|
|
1379
|
+
return 0;
|
|
1380
|
+
}
|
|
1381
|
+
|
|
1382
|
+
const char *val_str = RSTRING_PTR(value);
|
|
1383
|
+
long val_len = RSTRING_LEN(value);
|
|
1384
|
+
|
|
1385
|
+
if (val_len == 0) {
|
|
1386
|
+
return 0;
|
|
1387
|
+
}
|
|
1388
|
+
|
|
1389
|
+
// Skip leading whitespace
|
|
1390
|
+
const char *p = val_str;
|
|
1391
|
+
while (*p == ' ' || *p == '\t') {
|
|
1392
|
+
p++;
|
|
1393
|
+
}
|
|
1394
|
+
|
|
1395
|
+
long remaining = val_len - (p - val_str);
|
|
1396
|
+
|
|
1397
|
+
ID format_id = SYM2ID(format);
|
|
1398
|
+
ID hex_id = rb_intern("hex");
|
|
1399
|
+
ID rgb_id = rb_intern("rgb");
|
|
1400
|
+
ID hsl_id = rb_intern("hsl");
|
|
1401
|
+
ID hwb_id = rb_intern("hwb");
|
|
1402
|
+
ID oklab_id = rb_intern("oklab");
|
|
1403
|
+
ID oklch_id = rb_intern("oklch");
|
|
1404
|
+
ID lab_id = rb_intern("lab");
|
|
1405
|
+
ID lch_id = rb_intern("lch");
|
|
1406
|
+
ID named_id = rb_intern("named");
|
|
1407
|
+
|
|
1408
|
+
if (format_id == hex_id) {
|
|
1409
|
+
return *p == '#';
|
|
1410
|
+
}
|
|
1411
|
+
if (format_id == rgb_id) {
|
|
1412
|
+
return STARTS_WITH_RGB(p, remaining);
|
|
1413
|
+
}
|
|
1414
|
+
if (format_id == hwb_id) {
|
|
1415
|
+
return STARTS_WITH_HWB(p, remaining);
|
|
1416
|
+
}
|
|
1417
|
+
if (format_id == hsl_id) {
|
|
1418
|
+
return STARTS_WITH_HSL(p, remaining);
|
|
1419
|
+
}
|
|
1420
|
+
if (format_id == oklab_id) {
|
|
1421
|
+
return STARTS_WITH_OKLAB(p, remaining);
|
|
1422
|
+
}
|
|
1423
|
+
if (format_id == oklch_id) {
|
|
1424
|
+
return STARTS_WITH_OKLCH(p, remaining);
|
|
1425
|
+
}
|
|
1426
|
+
if (format_id == lab_id) {
|
|
1427
|
+
return STARTS_WITH_LAB(p, remaining);
|
|
1428
|
+
}
|
|
1429
|
+
if (format_id == lch_id) {
|
|
1430
|
+
return STARTS_WITH_LCH(p, remaining);
|
|
1431
|
+
}
|
|
1432
|
+
if (format_id == named_id) {
|
|
1433
|
+
// Named colors are alphabetic (possibly with whitespace)
|
|
1434
|
+
// Just check that it starts with a letter
|
|
1435
|
+
return (*p >= 'a' && *p <= 'z') || (*p >= 'A' && *p <= 'Z');
|
|
1436
|
+
}
|
|
1437
|
+
|
|
1438
|
+
return 0;
|
|
1439
|
+
}
|
|
1440
|
+
|
|
1441
|
+
// Expand shorthand properties if needed (e.g., background → background-color, background-image, etc.)
|
|
1442
|
+
// Returns hash of expanded properties, or nil if no expansion needed
|
|
1443
|
+
static VALUE expand_property_if_needed(VALUE property_name, VALUE value) {
|
|
1444
|
+
Check_Type(property_name, T_STRING);
|
|
1445
|
+
Check_Type(value, T_STRING);
|
|
1446
|
+
|
|
1447
|
+
const char *prop = RSTRING_PTR(property_name);
|
|
1448
|
+
|
|
1449
|
+
// Check if this is a shorthand property that needs expansion
|
|
1450
|
+
if (strcmp(prop, "background") == 0) {
|
|
1451
|
+
return cataract_expand_background(Qnil, value);
|
|
1452
|
+
}
|
|
1453
|
+
// Add other shorthands if needed (margin, padding, border, font, list-style)
|
|
1454
|
+
|
|
1455
|
+
return Qnil; // No expansion needed
|
|
1456
|
+
}
|
|
1457
|
+
|
|
1458
|
+
// Context struct for hash iteration
|
|
1459
|
+
struct convert_colors_context {
|
|
1460
|
+
color_parser_fn parser; // NULL if auto-detect mode
|
|
1461
|
+
color_formatter_fn formatter;
|
|
1462
|
+
int use_modern_syntax;
|
|
1463
|
+
VALUE from_format; // :any for auto-detect
|
|
1464
|
+
};
|
|
1465
|
+
|
|
1466
|
+
// Context for expanding and converting shorthand properties
|
|
1467
|
+
struct expand_convert_context {
|
|
1468
|
+
color_parser_fn parser; // NULL if auto-detect mode
|
|
1469
|
+
color_formatter_fn formatter;
|
|
1470
|
+
int use_modern_syntax;
|
|
1471
|
+
VALUE from_format; // :any for auto-detect
|
|
1472
|
+
VALUE new_declarations;
|
|
1473
|
+
VALUE important;
|
|
1474
|
+
};
|
|
1475
|
+
|
|
1476
|
+
// Callback for iterating expanded properties, converting colors, and creating declaration structs
|
|
1477
|
+
static int convert_expanded_property_callback(VALUE prop_name, VALUE prop_value, VALUE arg) {
|
|
1478
|
+
struct expand_convert_context *ctx = (struct expand_convert_context *)arg;
|
|
1479
|
+
|
|
1480
|
+
if (!NIL_P(prop_value) && TYPE(prop_value) == T_STRING) {
|
|
1481
|
+
color_parser_fn parser;
|
|
1482
|
+
|
|
1483
|
+
// Auto-detect or use specified format
|
|
1484
|
+
if (ctx->parser == NULL) {
|
|
1485
|
+
parser = detect_color_format(prop_value);
|
|
1486
|
+
} else if (matches_color_format(prop_value, ctx->from_format)) {
|
|
1487
|
+
parser = ctx->parser;
|
|
1488
|
+
} else {
|
|
1489
|
+
parser = NULL;
|
|
1490
|
+
}
|
|
1491
|
+
|
|
1492
|
+
if (parser != NULL) {
|
|
1493
|
+
// Parse → IR → Format
|
|
1494
|
+
struct color_ir color = parser(prop_value);
|
|
1495
|
+
prop_value = ctx->formatter(color, ctx->use_modern_syntax);
|
|
1496
|
+
}
|
|
1497
|
+
|
|
1498
|
+
VALUE new_decl = rb_struct_new(cDeclaration, prop_name, prop_value, ctx->important, NULL);
|
|
1499
|
+
rb_ary_push(ctx->new_declarations, new_decl);
|
|
1500
|
+
}
|
|
1501
|
+
|
|
1502
|
+
return ST_CONTINUE;
|
|
1503
|
+
}
|
|
1504
|
+
|
|
1505
|
+
|
|
1506
|
+
// Ruby method: stylesheet.convert_colors!(from: :hex, to: :rgb, variant: :modern)
|
|
1507
|
+
// Returns self for method chaining
|
|
1508
|
+
VALUE rb_stylesheet_convert_colors(int argc, VALUE *argv, VALUE self) {
|
|
1509
|
+
VALUE kwargs;
|
|
1510
|
+
rb_scan_args(argc, argv, ":", &kwargs);
|
|
1511
|
+
|
|
1512
|
+
// Handle case where no kwargs provided
|
|
1513
|
+
if (NIL_P(kwargs)) {
|
|
1514
|
+
kwargs = rb_hash_new();
|
|
1515
|
+
}
|
|
1516
|
+
|
|
1517
|
+
// Extract keyword arguments
|
|
1518
|
+
VALUE from_format = rb_hash_aref(kwargs, ID2SYM(rb_intern("from")));
|
|
1519
|
+
VALUE to_format = rb_hash_aref(kwargs, ID2SYM(rb_intern("to")));
|
|
1520
|
+
VALUE variant = rb_hash_aref(kwargs, ID2SYM(rb_intern("variant")));
|
|
1521
|
+
|
|
1522
|
+
// Default from_format to :any (auto-detect)
|
|
1523
|
+
if (NIL_P(from_format)) {
|
|
1524
|
+
from_format = ID2SYM(rb_intern("any"));
|
|
1525
|
+
}
|
|
1526
|
+
|
|
1527
|
+
// Validate required arguments
|
|
1528
|
+
if (NIL_P(to_format)) {
|
|
1529
|
+
rb_raise(rb_eArgError, "missing keyword: :to");
|
|
1530
|
+
}
|
|
1531
|
+
|
|
1532
|
+
// Auto-detect variant based on to_format if not explicitly set
|
|
1533
|
+
// :rgba, :hsla, :hwba imply legacy syntax (rgba(), hsla(), hwba())
|
|
1534
|
+
// :rgb, :hsl, :hwb, :hex default to modern syntax
|
|
1535
|
+
if (NIL_P(variant)) {
|
|
1536
|
+
ID to_id = SYM2ID(to_format);
|
|
1537
|
+
ID rgba_id = rb_intern("rgba");
|
|
1538
|
+
ID hsla_id = rb_intern("hsla");
|
|
1539
|
+
ID hwba_id = rb_intern("hwba");
|
|
1540
|
+
|
|
1541
|
+
if (to_id == rgba_id || to_id == hsla_id || to_id == hwba_id) {
|
|
1542
|
+
variant = ID2SYM(rb_intern("legacy"));
|
|
1543
|
+
} else {
|
|
1544
|
+
variant = ID2SYM(rb_intern("modern"));
|
|
1545
|
+
}
|
|
1546
|
+
}
|
|
1547
|
+
|
|
1548
|
+
// Get the appropriate parser and formatter functions
|
|
1549
|
+
color_parser_fn parser = NULL;
|
|
1550
|
+
ID from_id = SYM2ID(from_format);
|
|
1551
|
+
ID any_id = rb_intern("any");
|
|
1552
|
+
|
|
1553
|
+
// If from is :any, we'll auto-detect per value (parser = NULL)
|
|
1554
|
+
if (from_id != any_id) {
|
|
1555
|
+
parser = get_parser(from_format);
|
|
1556
|
+
if (parser == NULL) {
|
|
1557
|
+
const char *from_str = rb_id2name(from_id);
|
|
1558
|
+
rb_raise(rb_eArgError, "Unsupported source format: %s", from_str);
|
|
1559
|
+
}
|
|
1560
|
+
}
|
|
1561
|
+
|
|
1562
|
+
color_formatter_fn formatter = get_formatter(to_format);
|
|
1563
|
+
if (formatter == NULL) {
|
|
1564
|
+
const char *to_str = rb_id2name(SYM2ID(to_format));
|
|
1565
|
+
rb_raise(rb_eArgError, "Unsupported target format: %s", to_str);
|
|
1566
|
+
}
|
|
1567
|
+
|
|
1568
|
+
// Determine syntax variant
|
|
1569
|
+
ID variant_id = SYM2ID(variant);
|
|
1570
|
+
ID modern_id = rb_intern("modern");
|
|
1571
|
+
int use_modern_syntax = (variant_id == modern_id) ? 1 : 0;
|
|
1572
|
+
|
|
1573
|
+
// Get the @rules array from the stylesheet
|
|
1574
|
+
// @rules is an Array of Rule structs
|
|
1575
|
+
VALUE rules = rb_ivar_get(self, rb_intern("@rules"));
|
|
1576
|
+
|
|
1577
|
+
if (NIL_P(rules)) {
|
|
1578
|
+
return self; // No rules, nothing to convert
|
|
1579
|
+
}
|
|
1580
|
+
|
|
1581
|
+
if (TYPE(rules) != T_ARRAY) {
|
|
1582
|
+
rb_raise(rb_eTypeError, "Stylesheet @rules must be an Array, got %s",
|
|
1583
|
+
rb_obj_classname(rules));
|
|
1584
|
+
}
|
|
1585
|
+
|
|
1586
|
+
// Iterate through each rule
|
|
1587
|
+
long rules_count = RARRAY_LEN(rules);
|
|
1588
|
+
|
|
1589
|
+
for (long i = 0; i < rules_count; i++) {
|
|
1590
|
+
VALUE rule = rb_ary_entry(rules, i);
|
|
1591
|
+
|
|
1592
|
+
// Get declarations array from the rule struct
|
|
1593
|
+
// Rule = Struct.new(:id, :selector, :declarations, :specificity)
|
|
1594
|
+
// where declarations is an Array of Declaration structs
|
|
1595
|
+
VALUE declarations = rb_struct_aref(rule, INT2FIX(RULE_DECLARATIONS));
|
|
1596
|
+
|
|
1597
|
+
if (NIL_P(declarations) || TYPE(declarations) != T_ARRAY) {
|
|
1598
|
+
continue;
|
|
1599
|
+
}
|
|
1600
|
+
|
|
1601
|
+
// Iterate through each Declaration struct in the array
|
|
1602
|
+
long decl_count = RARRAY_LEN(declarations);
|
|
1603
|
+
|
|
1604
|
+
// Build new declarations array with expanded and converted values
|
|
1605
|
+
VALUE new_declarations = rb_ary_new();
|
|
1606
|
+
|
|
1607
|
+
for (long j = 0; j < decl_count; j++) {
|
|
1608
|
+
VALUE decl_struct = rb_ary_entry(declarations, j);
|
|
1609
|
+
|
|
1610
|
+
// Declaration = Struct.new(:property, :value, :important)
|
|
1611
|
+
VALUE property = rb_struct_aref(decl_struct, INT2FIX(DECL_PROPERTY));
|
|
1612
|
+
VALUE value = rb_struct_aref(decl_struct, INT2FIX(DECL_VALUE));
|
|
1613
|
+
VALUE important = rb_struct_aref(decl_struct, INT2FIX(DECL_IMPORTANT));
|
|
1614
|
+
|
|
1615
|
+
if (NIL_P(value) || TYPE(value) != T_STRING) {
|
|
1616
|
+
rb_ary_push(new_declarations, decl_struct);
|
|
1617
|
+
continue;
|
|
1618
|
+
}
|
|
1619
|
+
|
|
1620
|
+
// Check if this property needs expansion (e.g., background shorthand)
|
|
1621
|
+
VALUE expanded = expand_property_if_needed(property, value);
|
|
1622
|
+
|
|
1623
|
+
if (!NIL_P(expanded) && TYPE(expanded) == T_HASH && RHASH_SIZE(expanded) > 0) {
|
|
1624
|
+
// Expand and convert each sub-property
|
|
1625
|
+
struct expand_convert_context exp_ctx = {
|
|
1626
|
+
.parser = parser,
|
|
1627
|
+
.formatter = formatter,
|
|
1628
|
+
.use_modern_syntax = use_modern_syntax,
|
|
1629
|
+
.from_format = from_format,
|
|
1630
|
+
.new_declarations = new_declarations,
|
|
1631
|
+
.important = important
|
|
1632
|
+
};
|
|
1633
|
+
rb_hash_foreach(expanded, convert_expanded_property_callback, (VALUE)&exp_ctx);
|
|
1634
|
+
continue;
|
|
1635
|
+
}
|
|
1636
|
+
// If expansion returned empty hash, keep original declaration (e.g., gradients)
|
|
1637
|
+
|
|
1638
|
+
// Try to convert as a value with potentially multiple colors
|
|
1639
|
+
// (e.g., "border-color: #fff #000 #ccc" or "box-shadow: 0 0 10px #ff0000")
|
|
1640
|
+
VALUE converted_multi = convert_value_with_colors(value, parser, formatter, use_modern_syntax);
|
|
1641
|
+
|
|
1642
|
+
if (!NIL_P(converted_multi)) {
|
|
1643
|
+
DEBUG_PRINTF("Creating new decl with property='%s' value='%s'\n",
|
|
1644
|
+
StringValueCStr(property), StringValueCStr(converted_multi));
|
|
1645
|
+
// Successfully converted multi-value property
|
|
1646
|
+
VALUE new_decl = rb_struct_new(cDeclaration, property, converted_multi, important, NULL);
|
|
1647
|
+
rb_ary_push(new_declarations, new_decl);
|
|
1648
|
+
DEBUG_PRINTF("Pushed new_decl to new_declarations\n");
|
|
1649
|
+
continue;
|
|
1650
|
+
}
|
|
1651
|
+
|
|
1652
|
+
// Try single-value color conversion
|
|
1653
|
+
color_parser_fn single_parser;
|
|
1654
|
+
|
|
1655
|
+
// Auto-detect or use specified format
|
|
1656
|
+
if (parser == NULL) {
|
|
1657
|
+
single_parser = detect_color_format(value);
|
|
1658
|
+
} else if (matches_color_format(value, from_format)) {
|
|
1659
|
+
single_parser = parser;
|
|
1660
|
+
} else {
|
|
1661
|
+
single_parser = NULL;
|
|
1662
|
+
}
|
|
1663
|
+
|
|
1664
|
+
if (single_parser != NULL) {
|
|
1665
|
+
// Parse → IR → Format
|
|
1666
|
+
struct color_ir color = single_parser(value);
|
|
1667
|
+
|
|
1668
|
+
// Check if parse was successful (named colors can fail lookup)
|
|
1669
|
+
if (color.red < 0) {
|
|
1670
|
+
// Invalid color - keep original declaration
|
|
1671
|
+
rb_ary_push(new_declarations, decl_struct);
|
|
1672
|
+
} else {
|
|
1673
|
+
VALUE new_value = formatter(color, use_modern_syntax);
|
|
1674
|
+
VALUE new_decl = rb_struct_new(cDeclaration, property, new_value, important, NULL);
|
|
1675
|
+
rb_ary_push(new_declarations, new_decl);
|
|
1676
|
+
}
|
|
1677
|
+
} else {
|
|
1678
|
+
rb_ary_push(new_declarations, decl_struct);
|
|
1679
|
+
}
|
|
1680
|
+
}
|
|
1681
|
+
|
|
1682
|
+
// Replace rule's declarations array
|
|
1683
|
+
rb_struct_aset(rule, INT2FIX(RULE_DECLARATIONS), new_declarations);
|
|
1684
|
+
}
|
|
1685
|
+
|
|
1686
|
+
return self; // Return self for chaining
|
|
1687
|
+
}
|