saturon 0.2.2 → 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.
package/dist/index.umd.js CHANGED
@@ -126,53 +126,32 @@ var Saturon = (() => {
126
126
  return product.map((x) => x[0]);
127
127
  return product;
128
128
  }
129
- function clean(color) {
130
- return color.trim().replace(/\s+/g, " ").replace(/\( /g, "(").replace(/ \)/g, ")").replace(/\s*,\s*/g, ", ").replace(/ ,/g, ",").replace(/calc\(NaN\)/g, "0").replace(/[A-Z]/g, (c) => String.fromCharCode(c.charCodeAt(0) + 32));
131
- }
132
- function extractBalancedExpression(input, start) {
133
- let i = start;
134
- let expression = "";
135
- let depth = 0;
136
- if (input[i] !== "(") {
137
- while (i < input.length && /[a-zA-Z0-9-%#]/.test(input[i])) {
138
- expression += input[i];
139
- i++;
140
- }
141
- }
142
- if (input[i] === "(") {
143
- expression += "(";
144
- i++;
145
- depth = 1;
146
- while (i < input.length && depth > 0) {
147
- const char = input[i];
148
- if (char === "(")
149
- depth++;
150
- else if (char === ")")
151
- depth--;
152
- if (depth > 0)
153
- expression += char;
154
- i++;
155
- }
156
- expression += ")";
157
- }
158
- return { expression, end: i };
129
+ function normalize(component, value) {
130
+ const [min2, max2] = Array.isArray(value) ? value : value === "hue" ? [0, 360] : [0, 100];
131
+ if (Number.isNaN(component))
132
+ return 0;
133
+ if (component === Infinity)
134
+ return max2;
135
+ if (component === -Infinity)
136
+ return min2;
137
+ return typeof component === "number" ? component : 0;
159
138
  }
160
139
  function fit(coords, model, options = {}) {
161
140
  const { method = config.defaults.fit, precision } = options;
162
141
  const { components } = colorModels[model];
163
- const componentProps = Object.values(components).reduce((arr, props) => (arr[props.index] = props, arr), []);
142
+ const defs = Object.values(components).reduce((arr, props) => (arr[props.index] = props, arr), []);
164
143
  let clipped;
165
- if (method === "none") {
144
+ if (method === "none")
166
145
  clipped = coords;
167
- } else if (method === "clip") {
168
- clipped = coords.slice(0, 3).map((v, i) => {
169
- const prop = componentProps[i];
146
+ else if (method === "clip") {
147
+ clipped = coords.map((v, i) => {
148
+ const prop = defs[i];
170
149
  if (!prop)
171
150
  throw new Error(`Missing component properties for index ${i}.`);
172
- if (prop.value === "angle")
151
+ if (prop.value === "hue")
173
152
  return (v % 360 + 360) % 360;
174
- const [min, max] = Array.isArray(prop.value) ? prop.value : [0, 100];
175
- return Math.min(max, Math.max(min, v));
153
+ const [min2, max2] = Array.isArray(prop.value) ? prop.value : [0, 100];
154
+ return Math.min(max2, Math.max(min2, v));
176
155
  });
177
156
  } else {
178
157
  const fn = fitMethods[method];
@@ -181,631 +160,26 @@ var Saturon = (() => {
181
160
  }
182
161
  clipped = fn(coords, model);
183
162
  }
184
- return clipped.slice(0, 3).map((v, i) => {
163
+ return clipped.map((v, i) => {
185
164
  let p;
186
165
  if (typeof precision === "number" || precision === null)
187
166
  p = precision;
188
167
  else if (typeof precision === "undefined")
189
- p = componentProps[i]?.precision ?? 3;
168
+ p = defs[i]?.precision ?? 3;
190
169
  else
191
170
  throw new TypeError(`Invalid precision value: ${precision}.`);
192
171
  return p === null ? v : Number(v.toFixed(p));
193
172
  });
194
173
  }
195
- function modelConverterToColorConverter(name, converter) {
196
- const evaluateComponent = (token, value, base = {}, commaSeparated = false, relative = false) => {
197
- const parsePercent = (str, min2, max2) => {
198
- const percent = parseFloat(str);
199
- if (isNaN(percent))
200
- throw new Error(`Invalid percentage value: '${str}'.`);
201
- if (value === "percentage")
202
- return percent;
203
- if (min2 < 0 && max2 > 0)
204
- return percent / 100 * (max2 - min2) / 2;
205
- return percent / 100 * (max2 - min2) + min2;
206
- };
207
- const parseAngle = (token2) => {
208
- const value2 = parseFloat(token2);
209
- if (isNaN(value2))
210
- throw new Error(`Invalid angle value: '${token2}'.`);
211
- if (token2.slice(-3) === "deg")
212
- return value2;
213
- if (token2.slice(-3) === "rad")
214
- return value2 * (180 / pi);
215
- if (token2.slice(-4) === "grad")
216
- return value2 * 0.9;
217
- if (token2.slice(-4) === "turn")
218
- return value2 * 360;
219
- return value2;
220
- };
221
- const parseCalc = (token2, _min, _max) => {
222
- const tokenize2 = (s) => {
223
- const out = [];
224
- for (let i = 0; i < s.length; ) {
225
- const c = s[i];
226
- if (/\s/.test(c)) {
227
- i++;
228
- continue;
229
- }
230
- if (s.slice(i, i + 2) === "**") {
231
- out.push({ type: "operator", value: "**" });
232
- i += 2;
233
- continue;
234
- }
235
- if (/[0-9]/.test(c) || c === "." && /[0-9]/.test(s[i + 1] || "")) {
236
- let num = "";
237
- while (i < s.length && /[0-9.]/.test(s[i]))
238
- num += s[i++];
239
- if (i < s.length && /[eE]/.test(s[i])) {
240
- num += s[i++];
241
- if (/[+-]/.test(s[i]))
242
- num += s[i++];
243
- while (i < s.length && /[0-9]/.test(s[i]))
244
- num += s[i++];
245
- }
246
- out.push({ type: "number", value: parseFloat(num) });
247
- continue;
248
- }
249
- if (/[a-zA-Z_]/.test(c)) {
250
- let id = "";
251
- while (i < s.length && /[a-zA-Z0-9_]/.test(s[i]))
252
- id += s[i++];
253
- out.push({ type: "identifier", value: id });
254
- continue;
255
- }
256
- if ("+-*/%(),".includes(c)) {
257
- out.push({ type: "operator", value: c });
258
- i++;
259
- continue;
260
- }
261
- throw new Error(`Unexpected character: ${c}`);
262
- }
263
- return out;
264
- };
265
- const parse = (tokens) => {
266
- let pos = 0;
267
- const cur = () => pos < tokens.length ? tokens[pos] : null;
268
- const nxt = () => {
269
- if (pos >= tokens.length)
270
- throw new Error("Unexpected end of input");
271
- return tokens[pos++];
272
- };
273
- const expect = (v) => {
274
- const t = cur();
275
- if (!t || t.value !== v)
276
- throw new Error(`Expected "${v}" but got "${t ? t.value : "end of input"}`);
277
- nxt();
278
- };
279
- const parsePrimary = () => {
280
- const t = cur();
281
- if (!t)
282
- throw new Error("Unexpected end of input");
283
- if (t.type === "number") {
284
- nxt();
285
- return { type: "number", value: t.value };
286
- }
287
- if (t.type === "identifier") {
288
- nxt();
289
- if (cur() && cur().value === "(") {
290
- nxt();
291
- const args = [];
292
- if (cur() && cur().value !== ")") {
293
- args.push(parseAdd());
294
- while (cur() && cur().value === ",") {
295
- nxt();
296
- args.push(parseAdd());
297
- }
298
- }
299
- expect(")");
300
- return { type: "call", func: t.value, args };
301
- }
302
- return { type: "var", name: t.value };
303
- }
304
- if (t.value === "(") {
305
- nxt();
306
- const e2 = parseAdd();
307
- expect(")");
308
- return e2;
309
- }
310
- throw new Error(`Unexpected token: ${t.value}`);
311
- };
312
- const parseUnary = () => {
313
- if (cur() && (cur().value === "+" || cur().value === "-")) {
314
- const op = nxt().value;
315
- return { type: "unary", op, arg: parseUnary() };
316
- }
317
- return parsePrimary();
318
- };
319
- const parsePower = () => {
320
- let left = parseUnary();
321
- while (cur() && cur().value === "**") {
322
- const op = nxt().value;
323
- left = { type: "binary", op, left, right: parseUnary() };
324
- }
325
- return left;
326
- };
327
- const parseMul = () => {
328
- let left = parsePower();
329
- while (cur() && ["*", "/", "%"].includes(String(cur().value))) {
330
- const op = nxt().value;
331
- left = { type: "binary", op, left, right: parsePower() };
332
- }
333
- return left;
334
- };
335
- const parseAdd = () => {
336
- let left = parseMul();
337
- while (cur() && (cur().value === "+" || cur().value === "-")) {
338
- const op = nxt().value;
339
- left = { type: "binary", op, left, right: parseMul() };
340
- }
341
- return left;
342
- };
343
- const ast = parseAdd();
344
- if (pos < tokens.length)
345
- throw new Error(`Extra tokens after expression: ${tokens.slice(pos).map((t) => t.value).join(" ")}`);
346
- return ast;
347
- };
348
- const evaluate = (ast, env) => {
349
- switch (ast.type) {
350
- case "number":
351
- return ast.value;
352
- case "var": {
353
- const v = env[ast.name];
354
- if (v === void 0)
355
- throw new Error(`Unknown variable: ${ast.name}`);
356
- if (typeof v === "function")
357
- throw new Error(`Expected variable but found function: ${ast.name}`);
358
- return v;
359
- }
360
- case "binary": {
361
- const L = evaluate(ast.left, env), R = evaluate(ast.right, env);
362
- switch (ast.op) {
363
- case "+":
364
- return L + R;
365
- case "-":
366
- return L - R;
367
- case "*":
368
- return L * R;
369
- case "/":
370
- return L / R;
371
- case "%":
372
- return L % R;
373
- case "**":
374
- return L ** R;
375
- default:
376
- throw new Error(`Unknown binary operator: ${ast.op}`);
377
- }
378
- }
379
- case "unary": {
380
- const v = evaluate(ast.arg, env);
381
- switch (ast.op) {
382
- case "+":
383
- return +v;
384
- case "-":
385
- return -v;
386
- default:
387
- throw new Error(`Unknown unary operator: ${ast.op}`);
388
- }
389
- }
390
- case "call": {
391
- const fn = env[ast.func];
392
- if (typeof fn !== "function")
393
- throw new Error(`Unknown function: ${ast.func}`);
394
- return fn(...ast.args.map((a) => evaluate(a, env)));
395
- }
396
- default: {
397
- throw new Error(`Unknown AST node type: ${ast.type}`);
398
- }
399
- }
400
- };
401
- let inner = token2.slice(5, -1).trim();
402
- if (inner === "infinity")
403
- return _max;
404
- if (inner === "-infinity")
405
- return _min;
406
- if (inner === "NaN")
407
- return 0;
408
- inner = inner.replace(/(\d+(\.\d+)?)%/g, (m) => {
409
- if (relative === true)
410
- throw new Error("<angle> and <percentage> values are converted to <number> in relative syntax.");
411
- const r = parsePercent(m, _min, _max);
412
- return r !== void 0 ? String(r) : "0";
413
- });
414
- inner = inner.replace(/(\d+(\.\d+)?)(deg|rad|grad|turn)/g, (_, num, __, unit) => {
415
- if (relative === true)
416
- throw new Error("<angle> and <percentage> values are converted to <number> in relative syntax.");
417
- return String(parseAngle(`${parseFloat(num)}${unit}`));
418
- });
419
- const caclEnv = {
420
- ...base,
421
- pi,
422
- e,
423
- tau: pi * 2,
424
- pow,
425
- sqrt,
426
- sin,
427
- cos,
428
- tan,
429
- asin,
430
- acos,
431
- atan,
432
- atan2,
433
- exp,
434
- log,
435
- log10,
436
- log2,
437
- abs,
438
- min,
439
- max,
440
- hypot,
441
- round,
442
- ceil,
443
- floor,
444
- sign,
445
- trunc,
446
- random
447
- };
448
- try {
449
- const tokens = tokenize2(inner);
450
- const ast = parse(tokens);
451
- const result = evaluate(ast, caclEnv);
452
- return result;
453
- } catch (err) {
454
- throw new Error(`Evaluation error: ${err}`);
455
- }
456
- };
457
- const evaluateAngle = () => {
458
- if (/^-?(?:\d+|\d*\.\d+)(?:deg|rad|grad|turn)$/.test(token)) {
459
- return parseAngle(token);
460
- }
461
- if (/^-?(?:\d+|\d*\.\d+)$/.test(token)) {
462
- return parseFloat(token);
463
- }
464
- const [min2, max2] = [0, 360];
465
- if (token[token.length - 1] === "%") {
466
- if (commaSeparated && supportsLegacy === true) {
467
- throw new Error("The legacy color syntax does not allow percentages for <angle> components.");
468
- }
469
- if (relative === true) {
470
- throw new Error("The relative color syntax doesn't allow percentages for <angle> components.");
471
- }
472
- return parsePercent(token, min2, max2);
473
- }
474
- if (token.slice(0, 5) === "calc(") {
475
- return parseCalc(token, min2, max2);
476
- }
477
- throw new Error(`Invalid angle value: '${token}'. Must be a number, a number with a unit (deg, rad, grad, turn), or a percentage.`);
478
- };
479
- const evaluatePercent = () => {
480
- if (/^-?(?:\d+|\d*\.\d+)$/.test(token)) {
481
- if (commaSeparated && supportsLegacy === true) {
482
- throw new Error("The legacy color syntax does not allow numbers for <percentage> components.");
483
- }
484
- return parseFloat(token);
485
- }
486
- const [min2, max2] = [0, 100];
487
- if (token[token.length - 1] === "%") {
488
- return parsePercent(token, min2, max2);
489
- }
490
- if (token.slice(0, 5) === "calc(") {
491
- return parseCalc(token, min2, max2);
492
- }
493
- throw new Error(`Invalid percentage value: '${token}'. Must be a percentage or a number.`);
494
- };
495
- const evaluateNumber = () => {
496
- if (/^-?(?:\d+|\d*\.\d+)$/.test(token)) {
497
- return parseFloat(token);
498
- }
499
- const [min2, max2] = value;
500
- if (token[token.length - 1] === "%") {
501
- return parsePercent(token, min2, max2);
502
- }
503
- if (token.slice(0, 5) === "calc(") {
504
- return parseCalc(token, min2, max2);
505
- }
506
- throw new Error(`Invalid number value: '${token}'. Must be a number${relative === false ? " or a percentage" : ""}.`);
507
- };
508
- if (token === "none")
509
- return 0;
510
- if (token in base)
511
- return base[token];
512
- if (value === "angle")
513
- return evaluateAngle();
514
- if (value === "percentage")
515
- return evaluatePercent();
516
- if (Array.isArray(value))
517
- return evaluateNumber();
518
- throw new Error(`Unable to parse component token: ${token}`);
519
- };
520
- const parseAST = (ast) => {
521
- const { fn, space, fromOrigin, c1, c2, c3, alpha, commaSeparated } = ast;
522
- const { components: components2, supportsLegacy: supportsLegacy2 } = converter;
523
- components2.alpha = { index: 3, value: [0, 1], precision: 3 };
524
- if (commaSeparated && supportsLegacy2 !== true) {
525
- throw new Error(`<${fn}()> does not support comma-separated syntax.`);
526
- }
527
- const sorted = Object.entries(components2).sort((a, b) => a[1].index - b[1].index);
528
- if (fromOrigin) {
529
- let colorSpace;
530
- if (fn === "color") {
531
- colorSpace = space;
532
- } else if (fn in colorModels) {
533
- colorSpace = fn;
534
- } else {
535
- for (const model in colorModels) {
536
- if (colorModels[model].alphaVariant === fn) {
537
- colorSpace = model;
538
- break;
539
- }
540
- }
541
- }
542
- const originComponents = Color.from(fromOrigin).in(colorSpace).toObject({ fit: "none", precision: null });
543
- const evaluatedComponents = [c1, c2, c3, alpha].map((token, i) => {
544
- const [, meta] = sorted[i];
545
- return evaluateComponent(token, meta.value, originComponents, commaSeparated, true);
546
- });
547
- return evaluatedComponents.slice(0, 4);
548
- } else {
549
- const result = [];
550
- const percentFlags = [];
551
- const tokens = [c1, c2, c3, alpha];
552
- for (let i = 0; i < sorted.length; i++) {
553
- const [, meta] = sorted[i];
554
- const token = tokens[i];
555
- if (commaSeparated && token === "none") {
556
- throw new Error(`${fn}() cannot use "none" in comma-separated syntax.`);
557
- }
558
- if (meta.index !== 3 && meta.value !== "angle" && meta.value !== "percentage" && token.slice(0, 5) !== "calc(") {
559
- percentFlags.push(token.trim()[token.length - 1] === "%");
560
- }
561
- if (token) {
562
- const value = evaluateComponent(token, meta.value, {}, commaSeparated);
563
- result[meta.index] = value;
564
- }
565
- }
566
- if (commaSeparated && percentFlags.length > 1) {
567
- const allPercent = percentFlags.every(Boolean);
568
- const nonePercent = percentFlags.every((f) => !f);
569
- if (!allPercent && !nonePercent) {
570
- throw new Error(`${fn}()'s <number> components must all be numbers or all percentages.`);
571
- }
572
- }
573
- return result.slice(0, 4);
574
- }
575
- };
576
- const getAST = (tokens) => {
577
- const getAlpha = (index, separator = "/") => {
578
- if (tokens[index] !== void 0) {
579
- if (tokens[index] === separator) {
580
- return { value: tokens[index + 1], hasAlpha: true };
581
- }
582
- throw new Error("Invalid alpha separator");
583
- }
584
- return { value: "1", hasAlpha: false };
585
- };
586
- let fn, space, fromOrigin, c1, c2, c3, alpha, commaSeparated = false, expectedLength;
587
- if (tokens[0] === "color") {
588
- fn = "color";
589
- if (tokens[1] === "from") {
590
- space = tokens[3];
591
- fromOrigin = tokens[2];
592
- c1 = tokens[4];
593
- c2 = tokens[5];
594
- c3 = tokens[6];
595
- const { value, hasAlpha } = getAlpha(7);
596
- expectedLength = hasAlpha ? 9 : 7;
597
- alpha = value;
598
- } else {
599
- space = tokens[1];
600
- fromOrigin = null;
601
- c1 = tokens[2];
602
- c2 = tokens[3];
603
- c3 = tokens[4];
604
- const { value, hasAlpha } = getAlpha(5);
605
- expectedLength = hasAlpha ? 7 : 5;
606
- alpha = value;
607
- }
608
- } else {
609
- fn = tokens[0];
610
- space = null;
611
- if (tokens[1] === "from") {
612
- fromOrigin = tokens[2];
613
- c1 = tokens[3];
614
- c2 = tokens[4];
615
- c3 = tokens[5];
616
- const { value, hasAlpha } = getAlpha(6);
617
- expectedLength = hasAlpha ? 8 : 6;
618
- alpha = value;
619
- } else {
620
- fromOrigin = null;
621
- c1 = tokens[1];
622
- if (tokens[2] === "," && tokens[4] === ",") {
623
- commaSeparated = true;
624
- c2 = tokens[3];
625
- c3 = tokens[5];
626
- const { value, hasAlpha } = getAlpha(6, ",");
627
- expectedLength = hasAlpha ? 8 : 6;
628
- if (hasAlpha && tokens[6] !== ",") {
629
- throw new Error("Comma optional syntax requires no commas at all.");
630
- }
631
- alpha = value;
632
- } else {
633
- c2 = tokens[2];
634
- c3 = tokens[3];
635
- const { value, hasAlpha } = getAlpha(4);
636
- expectedLength = hasAlpha ? 6 : 4;
637
- alpha = value;
638
- }
639
- }
640
- }
641
- if (tokens.length !== expectedLength) {
642
- throw new Error(`Invalid number of tokens for ${fn}(): expected ${expectedLength} but got ${tokens.length}.`);
643
- }
644
- return { fn, space, fromOrigin, c1, c2, c3, alpha, commaSeparated };
645
- };
646
- const tokenize = (str) => {
647
- const tokens = [];
648
- let i = 0;
649
- let funcName = "";
650
- while (i < str.length && str[i] !== "(") {
651
- funcName += str[i];
652
- i++;
653
- }
654
- funcName = funcName.trim();
655
- tokens.push(funcName);
656
- const innerStart = str.indexOf("(") + 1;
657
- const innerStr = str.slice(innerStart, -1).trim();
658
- i = 0;
659
- if (innerStr.slice(0, 5) === "from ") {
660
- tokens.push("from");
661
- i += 5;
662
- while (i < innerStr.length && innerStr[i] === " ")
663
- i++;
664
- const colorStart = i;
665
- while (i < innerStr.length && innerStr[i] !== " ")
666
- i++;
667
- const colorStr = innerStr.slice(colorStart, i);
668
- if (colorStr.includes("(")) {
669
- const { expression, end } = extractBalancedExpression(innerStr, colorStart);
670
- tokens.push(expression);
671
- i = end;
672
- } else {
673
- tokens.push(colorStr);
674
- }
675
- while (i < innerStr.length && innerStr[i] === " ")
676
- i++;
677
- }
678
- if (tokens[0] === "color" && i < innerStr.length) {
679
- const spaceStart = i;
680
- while (i < innerStr.length && innerStr[i] !== " ")
681
- i++;
682
- tokens.push(innerStr.slice(spaceStart, i));
683
- while (i < innerStr.length && innerStr[i] === " ")
684
- i++;
685
- }
686
- while (i < innerStr.length) {
687
- const char = innerStr[i];
688
- if (char === ",") {
689
- tokens.push(",");
690
- i++;
691
- if (innerStr[i] === " ")
692
- i++;
693
- } else if (char === "/") {
694
- tokens.push("/");
695
- i++;
696
- if (innerStr[i] === " ")
697
- i++;
698
- } else if (char === " ") {
699
- i++;
700
- } else if (/[a-zA-Z#]/.test(char)) {
701
- const identStart = i;
702
- let ident = "";
703
- while (i < innerStr.length && /[a-zA-Z0-9-%#]/.test(innerStr[i])) {
704
- ident += innerStr[i];
705
- i++;
706
- }
707
- if (i < innerStr.length && innerStr[i] === "(") {
708
- const { expression, end } = extractBalancedExpression(innerStr, identStart);
709
- tokens.push(expression);
710
- i = end;
711
- } else {
712
- tokens.push(ident);
713
- }
714
- } else if (/[\d.-]/.test(char)) {
715
- let num = "";
716
- while (i < innerStr.length && /[\d.eE+-]/.test(innerStr[i])) {
717
- num += innerStr[i];
718
- i++;
719
- }
720
- if (i < innerStr.length && innerStr[i] === "%") {
721
- num += "%";
722
- i++;
723
- tokens.push(num);
724
- } else if (i < innerStr.length && /[a-zA-Z]/.test(innerStr[i])) {
725
- let unit = "";
726
- while (i < innerStr.length && /[a-zA-Z]/.test(innerStr[i])) {
727
- unit += innerStr[i];
728
- i++;
729
- }
730
- tokens.push(num + unit);
731
- } else {
732
- tokens.push(num);
733
- }
734
- } else {
735
- throw new Error(`Unexpected character: ${char}`);
736
- }
737
- }
738
- return tokens;
739
- };
740
- const validateRelativeColorSpace = (str, name2) => {
741
- const prefix = "color(from ";
742
- if (str.slice(0, 11) !== prefix || str[str.length - 1] !== ")") {
743
- return false;
744
- }
745
- const innerStr = str.slice(prefix.length, -1).trim();
746
- const { expression, end } = extractBalancedExpression(innerStr, 0);
747
- if (!expression) {
748
- return false;
749
- }
750
- const rest = innerStr.slice(end).trim();
751
- const parts = rest.split(/\s+/);
752
- if (parts.length < 1) {
753
- return false;
754
- }
755
- const colorSpace = parts[0];
756
- return colorSpace === name2;
757
- };
758
- const { components, bridge, fromBridge, toBridge, alphaVariant, supportsLegacy } = converter;
759
- const { PI: pi, E: e, pow, sqrt, sin, cos, tan, asin, acos, atan, atan2, exp, log, log10, log2, abs, min, max, hypot, round, ceil, floor, sign, trunc, random } = Math;
760
- return {
761
- isValid: (str) => {
762
- const { alphaVariant: alphaVariant2 = name } = converter;
763
- if (name in colorSpaces) {
764
- const startsWithColor = str.slice(0, `color(${name} `.length) === `color(${name} `;
765
- const startsWithFrom = str.slice(0, "color(from".length) === "color(from" && validateRelativeColorSpace(str, name);
766
- return (startsWithColor || startsWithFrom) && str[str.length - 1] === ")";
767
- }
768
- return (str.slice(0, `${name}(`.length) === `${name}(` || str.slice(0, `${alphaVariant2}(`.length) === `${alphaVariant2}(`) && str[str.length - 1] === ")";
769
- },
770
- bridge,
771
- toBridge: (coords) => [...toBridge(coords.slice(0, 3)), coords[3] ?? 1],
772
- parse: (str) => {
773
- const tokens = tokenize(str);
774
- const ast = getAST(tokens);
775
- const components2 = parseAST(ast);
776
- return [...components2.slice(0, 3), components2[3] ?? 1];
777
- },
778
- fromBridge: (coords) => [...fromBridge(coords), coords[3] ?? 1],
779
- format: ([c1, c2, c3, a = 1], options = {}) => {
780
- const { legacy = false, fit: fitMethod = config.defaults.fit, precision, units = false } = options;
781
- const clipped = fit([c1, c2, c3], name, { method: fitMethod, precision });
782
- const alpha = Number(min(max(a, 0), 1).toFixed(3)).toString();
783
- const formatted = clipped.map((v, index) => {
784
- if ((units || legacy) && components) {
785
- const def = Object.values(components).find((comp) => comp.index === index);
786
- if (def?.value === "percentage")
787
- return `${v}%`;
788
- if (def?.value === "angle" && units)
789
- return `${v}deg`;
790
- }
791
- return v.toString();
792
- });
793
- if (name in colorSpaces) {
794
- return `color(${name} ${formatted.join(" ")}${a !== 1 ? ` / ${alpha}` : ""})`;
795
- }
796
- if (legacy && supportsLegacy) {
797
- return a === 1 ? `${name}(${formatted.join(", ")})` : `${alphaVariant || name}(${formatted.join(", ")}, ${alpha})`;
798
- }
799
- return `${name}(${formatted.join(" ")}${a !== 1 ? ` / ${alpha}` : ""})`;
800
- }
801
- };
802
- }
803
174
  function spaceConverterToModelConverter(name, converter) {
804
175
  const { fromLinear = (c) => c, toLinear = (c) => c, toBridgeMatrix, fromBridgeMatrix } = converter;
805
176
  return {
806
177
  supportsLegacy: false,
807
178
  targetGamut: converter.targetGamut === null ? null : name,
808
- components: Object.fromEntries(converter.components.map((comp, index) => [comp, { index, value: [0, 1], precision: 5 }])),
179
+ components: {
180
+ ...Object.fromEntries(converter.components.map((comp, index) => [comp, { index, value: [0, 1], precision: 5 }])),
181
+ alpha: { index: 3, value: [0, 1], precision: 3 }
182
+ },
809
183
  bridge: converter.bridge,
810
184
  toBridge: (coords) => {
811
185
  return multiplyMatrices(toBridgeMatrix, coords.map((c) => toLinear(c)));
@@ -813,8 +187,91 @@ var Saturon = (() => {
813
187
  fromBridge: (coords) => multiplyMatrices(fromBridgeMatrix, coords).map((c) => fromLinear(c))
814
188
  };
815
189
  }
190
+ function hueDelta(a, b) {
191
+ const d = ((b - a) % 360 + 360) % 360;
192
+ return d > 180 ? d - 360 : d;
193
+ }
194
+ function hueDeltaLong(a, b) {
195
+ const d = hueDelta(a, b);
196
+ return d >= 0 ? d - 360 : d + 360;
197
+ }
198
+ function interpHue(a, b, t, method) {
199
+ if (Number.isNaN(a) || Number.isNaN(b))
200
+ return NaN;
201
+ switch (method) {
202
+ case "shorter":
203
+ return ((a + t * hueDelta(a, b)) % 360 + 360) % 360;
204
+ case "longer":
205
+ return ((a + t * hueDeltaLong(a, b)) % 360 + 360) % 360;
206
+ case "increasing":
207
+ return ((a * (1 - t) + (b < a ? b + 360 : b) * t) % 360 + 360) % 360;
208
+ case "decreasing":
209
+ return ((a * (1 - t) + (b > a ? b - 360 : b) * t) % 360 + 360) % 360;
210
+ }
211
+ throw new Error(`Invalid hue interpolation: ${method}`);
212
+ }
213
+ function mixTwo(colorA, colorB, t, model = "oklab", hue = "shorter") {
214
+ const A = colorA.in(model).coords.slice();
215
+ const B = colorB.in(model).coords.slice();
216
+ const { components } = colorModels[model];
217
+ if (!components)
218
+ throw new Error(`Model ${model} does not have defined components.`);
219
+ const norm = (v) => Array.isArray(v) ? v : v === "hue" ? [0, 360] : [0, 100];
220
+ const repair = (a, b, v) => {
221
+ const [min2, max2] = norm(v);
222
+ const fixInf = (x) => x === Infinity ? max2 : x === -Infinity ? min2 : x;
223
+ a = fixInf(a);
224
+ b = fixInf(b);
225
+ const aNaN = Number.isNaN(a);
226
+ const bNaN = Number.isNaN(b);
227
+ if (aNaN && bNaN)
228
+ return { a: NaN, b: NaN };
229
+ if (aNaN)
230
+ return { a: b, b };
231
+ if (bNaN)
232
+ return { a, b: a };
233
+ return { a, b, ok: true };
234
+ };
235
+ const hueIndex = components.h?.index ?? -1;
236
+ for (const key in components) {
237
+ const { index, value } = components[key];
238
+ if (index > 3)
239
+ continue;
240
+ const r = repair(A[index], B[index], value);
241
+ if (r.ok) {
242
+ const [min2, max2] = norm(value);
243
+ const fixInf = (x) => x === Infinity ? max2 : x === -Infinity ? min2 : x;
244
+ A[index] = fixInf(A[index]);
245
+ B[index] = fixInf(B[index]);
246
+ } else {
247
+ A[index] = r.a;
248
+ B[index] = r.b;
249
+ }
250
+ }
251
+ t = Math.min(1, Math.max(0, t));
252
+ if (t === 0)
253
+ return new Color(model, [...A]);
254
+ if (t === 1)
255
+ return new Color(model, [...B]);
256
+ const aA = A[3] ?? 1;
257
+ const aB = B[3] ?? 1;
258
+ const resolvedA = A.slice(0, 3);
259
+ const resolvedB = B.slice(0, 3);
260
+ if (aA < 1 || aB < 1) {
261
+ const premixed = resolvedA.map((a2, i) => {
262
+ const b = resolvedB[i];
263
+ return i === hueIndex ? interpHue(a2, b, t, hue) : a2 * aA * (1 - t) + b * aB * t;
264
+ });
265
+ const a = aA * (1 - t) + aB * t;
266
+ const out = a > 0 ? premixed.map((v, i) => i === hueIndex ? v : v / a) : resolvedA.map((_, i) => i === hueIndex ? premixed[i] : 0);
267
+ return new Color(model, [...out, a]);
268
+ }
269
+ const mixed = resolvedA.map((a, i) => i === hueIndex ? interpHue(a, resolvedB[i], t, hue) : a + (resolvedB[i] - a) * t);
270
+ return new Color(model, [...mixed, 1]);
271
+ }
816
272
 
817
273
  // dist/math.js
274
+ var EPSILON = 1e-5;
818
275
  var MATRICES = {
819
276
  D50_to_D65: [
820
277
  [0.955473421488075, -0.02309845494876471, 0.06325924320057072],
@@ -897,15 +354,6 @@ var Saturon = (() => {
897
354
  [1, -0.0894841775298119, -1.2914855480194092]
898
355
  ]
899
356
  };
900
- var EASINGS = {
901
- linear: (t) => t,
902
- "ease-in": (t) => t * t,
903
- "ease-out": (t) => t * (2 - t),
904
- "ease-in-out": (t) => t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t,
905
- "ease-in-cubic": (t) => t * t * t,
906
- "ease-out-cubic": (t) => --t * t * t + 1,
907
- "ease-in-out-cubic": (t) => t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2
908
- };
909
357
  var fitMethods = {
910
358
  "chroma-reduction": (coords, model) => {
911
359
  const color = new Color(model, coords);
@@ -921,7 +369,7 @@ var Saturon = (() => {
921
369
  let C_low = 0;
922
370
  let C_high = 1;
923
371
  const epsilon = 1e-6;
924
- let clipped = [];
372
+ let clipped;
925
373
  while (C_high - C_low > epsilon) {
926
374
  const C_mid = (C_low + C_high) / 2;
927
375
  const candidate_color = new Color("oklch", [L_clipped, C_mid, H]);
@@ -970,14 +418,14 @@ var Saturon = (() => {
970
418
  const E = current.deltaEOK(initialClippedColor);
971
419
  if (E < JND)
972
420
  return clipped;
973
- let min = 0;
974
- let max = C;
421
+ let min2 = 0;
422
+ let max2 = C;
975
423
  let min_inGamut = true;
976
- while (max - min > epsilon) {
977
- const chroma = (min + max) / 2;
424
+ while (max2 - min2 > epsilon) {
425
+ const chroma = (min2 + max2) / 2;
978
426
  const candidate = new Color("oklch", [L, chroma, H]);
979
427
  if (min_inGamut && candidate.inGamut(targetGamut, 1e-5))
980
- min = chroma;
428
+ min2 = chroma;
981
429
  else {
982
430
  const clippedCoords = fit(candidate.in(model).toArray({ fit: "none", precision: null }).slice(0, 3), model, { method: "clip" });
983
431
  clipped = clippedCoords;
@@ -988,10 +436,10 @@ var Saturon = (() => {
988
436
  return clipped;
989
437
  else {
990
438
  min_inGamut = false;
991
- min = chroma;
439
+ min2 = chroma;
992
440
  }
993
441
  } else
994
- max = chroma;
442
+ max2 = chroma;
995
443
  }
996
444
  }
997
445
  return clipped;
@@ -1000,16 +448,16 @@ var Saturon = (() => {
1000
448
  function RGB_to_XYZD65(rgb) {
1001
449
  const lin_sRGB = rgb.map((v) => {
1002
450
  const n = v / 255;
1003
- const sign = n < 0 ? -1 : 1, abs = Math.abs(n);
1004
- return abs <= 0.04045 ? n / 12.92 : sign * ((abs + 0.055) / 1.055) ** 2.4;
451
+ const sign2 = n < 0 ? -1 : 1, abs2 = Math.abs(n);
452
+ return abs2 <= 0.04045 ? n / 12.92 : sign2 * ((abs2 + 0.055) / 1.055) ** 2.4;
1005
453
  });
1006
454
  return multiplyMatrices(MATRICES.SRGB_to_XYZD65, lin_sRGB);
1007
455
  }
1008
456
  function XYZD65_to_RGB(xyz) {
1009
457
  const linRGB = multiplyMatrices(MATRICES.XYZD65_to_SRGB, xyz);
1010
458
  const gammaRGB = linRGB.map((v) => {
1011
- const sign = v < 0 ? -1 : 1, abs = Math.abs(v);
1012
- return abs > 31308e-7 ? sign * (1.055 * abs ** (1 / 2.4) - 0.055) : 12.92 * v;
459
+ const sign2 = v < 0 ? -1 : 1, abs2 = Math.abs(v);
460
+ return abs2 > 31308e-7 ? sign2 * (1.055 * abs2 ** (1 / 2.4) - 0.055) : 12.92 * v;
1013
461
  });
1014
462
  return gammaRGB.map((v) => v * 255);
1015
463
  }
@@ -1025,16 +473,18 @@ var Saturon = (() => {
1025
473
  }
1026
474
  function RGB_to_HSL(rgb) {
1027
475
  const [r, g, b] = rgb.map((v) => v / 255);
1028
- const max = Math.max(r, g, b), min = Math.min(r, g, b);
1029
- const L = (max + min) / 2;
1030
- let H = 0, S = 0;
1031
- const d = max - min;
1032
- if (d !== 0) {
476
+ const max2 = Math.max(r, g, b), min2 = Math.min(r, g, b);
477
+ const L = (max2 + min2) / 2;
478
+ const d = max2 - min2;
479
+ let H = NaN, S = 0;
480
+ if (d > 0) {
1033
481
  S = d / (1 - Math.abs(2 * L - 1));
1034
- H = max === r ? (g - b) / d + (g < b ? 6 : 0) : max === g ? (b - r) / d + 2 : (r - g) / d + 4;
1035
- H *= 60;
482
+ H = max2 === r ? (g - b) / d + (g < b ? 6 : 0) : max2 === g ? (b - r) / d + 2 : (r - g) / d + 4;
483
+ H = H * 60 % 360;
1036
484
  }
1037
- return [H % 360, S * 100, L * 100];
485
+ const S_out = S * 100, L_out = L * 100;
486
+ const invalid = L_out <= EPSILON || 100 - L_out <= EPSILON || S_out <= EPSILON;
487
+ return [invalid ? NaN : H, S_out, L_out];
1038
488
  }
1039
489
  function HWB_to_RGB([h, w, b]) {
1040
490
  w /= 100;
@@ -1048,16 +498,14 @@ var Saturon = (() => {
1048
498
  }
1049
499
  function RGB_to_HWB(rgb) {
1050
500
  const [r, g, b] = rgb.map((v) => v / 255);
1051
- const max = Math.max(r, g, b), min = Math.min(r, g, b);
1052
- let H = 0;
1053
- const d = max - min;
1054
- if (d === 0)
1055
- H = 0;
1056
- else {
1057
- H = max === r ? (g - b) / d + (g < b ? 6 : 0) : max === g ? (b - r) / d + 2 : (r - g) / d + 4;
501
+ const max2 = Math.max(r, g, b), min2 = Math.min(r, g, b);
502
+ const d = max2 - min2;
503
+ let H = NaN;
504
+ if (d > EPSILON) {
505
+ H = max2 === r ? (g - b) / d + (g < b ? 6 : 0) : max2 === g ? (b - r) / d + 2 : (r - g) / d + 4;
1058
506
  H = H * 60 % 360;
1059
507
  }
1060
- return [H, min * 100, (1 - max) * 100];
508
+ return [H, min2 * 100, (1 - max2) * 100];
1061
509
  }
1062
510
  function LAB_to_XYZD50([L, a, b]) {
1063
511
  const D50 = [0.3457 / 0.3585, 1, (1 - 0.3457 - 0.3585) / 0.3585];
@@ -1085,7 +533,7 @@ var Saturon = (() => {
1085
533
  let H = Math.atan2(b, a) * 180 / Math.PI;
1086
534
  if (H < 0)
1087
535
  H += 360;
1088
- return [L, C, H];
536
+ return [L, C, C <= EPSILON ? NaN : H];
1089
537
  }
1090
538
  function OKLAB_to_XYZD65(oklab) {
1091
539
  const { LMS_to_XYZD65, OKLAB_to_LMS } = MATRICES;
@@ -1105,7 +553,7 @@ var Saturon = (() => {
1105
553
  let H = Math.atan2(b, a) * 180 / Math.PI;
1106
554
  if (H < 0)
1107
555
  H += 360;
1108
- return [L, C, H];
556
+ return [L, C, C <= EPSILON ? NaN : H];
1109
557
  }
1110
558
 
1111
559
  // dist/converters.js
@@ -1264,18 +712,18 @@ var Saturon = (() => {
1264
712
  components: ["r", "g", "b"],
1265
713
  bridge: "xyz-d65",
1266
714
  toLinear: (c) => {
1267
- const sign = c < 0 ? -1 : 1;
1268
- const abs = Math.abs(c);
1269
- if (abs <= 0.04045)
1270
- return sign * (abs / 12.92);
1271
- return sign * Math.pow((abs + 0.055) / 1.055, 2.4);
715
+ const sign2 = c < 0 ? -1 : 1;
716
+ const abs2 = Math.abs(c);
717
+ if (abs2 <= 0.04045)
718
+ return sign2 * (abs2 / 12.92);
719
+ return sign2 * Math.pow((abs2 + 0.055) / 1.055, 2.4);
1272
720
  },
1273
721
  fromLinear: (c) => {
1274
- const sign = c < 0 ? -1 : 1;
1275
- const abs = Math.abs(c);
1276
- if (abs > 31308e-7)
1277
- return sign * (1.055 * Math.pow(abs, 1 / 2.4) - 0.055);
1278
- return sign * (12.92 * abs);
722
+ const sign2 = c < 0 ? -1 : 1;
723
+ const abs2 = Math.abs(c);
724
+ if (abs2 > 31308e-7)
725
+ return sign2 * (1.055 * Math.pow(abs2, 1 / 2.4) - 0.055);
726
+ return sign2 * (12.92 * abs2);
1279
727
  },
1280
728
  toBridgeMatrix: MATRICES.SRGB_to_XYZD65,
1281
729
  fromBridgeMatrix: MATRICES.XYZD65_to_SRGB
@@ -1290,18 +738,18 @@ var Saturon = (() => {
1290
738
  components: ["r", "g", "b"],
1291
739
  bridge: "xyz-d65",
1292
740
  toLinear: (c) => {
1293
- const sign = c < 0 ? -1 : 1;
1294
- const abs = Math.abs(c);
1295
- if (abs <= 0.04045)
1296
- return sign * (abs / 12.92);
1297
- return sign * Math.pow((abs + 0.055) / 1.055, 2.4);
741
+ const sign2 = c < 0 ? -1 : 1;
742
+ const abs2 = Math.abs(c);
743
+ if (abs2 <= 0.04045)
744
+ return sign2 * (abs2 / 12.92);
745
+ return sign2 * Math.pow((abs2 + 0.055) / 1.055, 2.4);
1298
746
  },
1299
747
  fromLinear: (c) => {
1300
- const sign = c < 0 ? -1 : 1;
1301
- const abs = Math.abs(c);
1302
- if (abs > 31308e-7)
1303
- return sign * (1.055 * Math.pow(abs, 1 / 2.4) - 0.055);
1304
- return sign * (12.92 * abs);
748
+ const sign2 = c < 0 ? -1 : 1;
749
+ const abs2 = Math.abs(c);
750
+ if (abs2 > 31308e-7)
751
+ return sign2 * (1.055 * Math.pow(abs2, 1 / 2.4) - 0.055);
752
+ return sign2 * (12.92 * abs2);
1305
753
  },
1306
754
  toBridgeMatrix: MATRICES.P3_to_XYZD65,
1307
755
  fromBridgeMatrix: MATRICES.XYZD65_to_P3
@@ -1312,20 +760,20 @@ var Saturon = (() => {
1312
760
  toLinear: (c) => {
1313
761
  const \u03B1 = 1.09929682680944;
1314
762
  const \u03B2 = 0.018053968510807;
1315
- const sign = c < 0 ? -1 : 1;
1316
- const abs = Math.abs(c);
1317
- if (abs < \u03B2 * 4.5)
1318
- return sign * (abs / 4.5);
1319
- return sign * Math.pow((abs + \u03B1 - 1) / \u03B1, 1 / 0.45);
763
+ const sign2 = c < 0 ? -1 : 1;
764
+ const abs2 = Math.abs(c);
765
+ if (abs2 < \u03B2 * 4.5)
766
+ return sign2 * (abs2 / 4.5);
767
+ return sign2 * Math.pow((abs2 + \u03B1 - 1) / \u03B1, 1 / 0.45);
1320
768
  },
1321
769
  fromLinear: (c) => {
1322
770
  const \u03B1 = 1.09929682680944;
1323
771
  const \u03B2 = 0.018053968510807;
1324
- const sign = c < 0 ? -1 : 1;
1325
- const abs = Math.abs(c);
1326
- if (abs > \u03B2)
1327
- return sign * (\u03B1 * Math.pow(abs, 0.45) - (\u03B1 - 1));
1328
- return sign * (4.5 * abs);
772
+ const sign2 = c < 0 ? -1 : 1;
773
+ const abs2 = Math.abs(c);
774
+ if (abs2 > \u03B2)
775
+ return sign2 * (\u03B1 * Math.pow(abs2, 0.45) - (\u03B1 - 1));
776
+ return sign2 * (4.5 * abs2);
1329
777
  },
1330
778
  toBridgeMatrix: MATRICES.REC2020_to_XYZD65,
1331
779
  fromBridgeMatrix: MATRICES.XYZD65_to_REC2020
@@ -1334,14 +782,14 @@ var Saturon = (() => {
1334
782
  components: ["r", "g", "b"],
1335
783
  bridge: "xyz-d65",
1336
784
  toLinear: (c) => {
1337
- const sign = c < 0 ? -1 : 1;
1338
- const abs = Math.abs(c);
1339
- return sign * Math.pow(abs, 563 / 256);
785
+ const sign2 = c < 0 ? -1 : 1;
786
+ const abs2 = Math.abs(c);
787
+ return sign2 * Math.pow(abs2, 563 / 256);
1340
788
  },
1341
789
  fromLinear: (c) => {
1342
- const sign = c < 0 ? -1 : 1;
1343
- const abs = Math.abs(c);
1344
- return sign * Math.pow(abs, 256 / 563);
790
+ const sign2 = c < 0 ? -1 : 1;
791
+ const abs2 = Math.abs(c);
792
+ return sign2 * Math.pow(abs2, 256 / 563);
1345
793
  },
1346
794
  toBridgeMatrix: MATRICES.A98_to_XYZD65,
1347
795
  fromBridgeMatrix: MATRICES.XYZD65_to_A98
@@ -1351,19 +799,19 @@ var Saturon = (() => {
1351
799
  bridge: "xyz-d50",
1352
800
  toLinear: (c) => {
1353
801
  const Et2 = 16 / 512;
1354
- const sign = c < 0 ? -1 : 1;
1355
- const abs = Math.abs(c);
1356
- if (abs <= Et2)
1357
- return sign * (abs / 16);
1358
- return sign * Math.pow(abs, 1.8);
802
+ const sign2 = c < 0 ? -1 : 1;
803
+ const abs2 = Math.abs(c);
804
+ if (abs2 <= Et2)
805
+ return sign2 * (abs2 / 16);
806
+ return sign2 * Math.pow(abs2, 1.8);
1359
807
  },
1360
808
  fromLinear: (c) => {
1361
809
  const Et = 1 / 512;
1362
- const sign = c < 0 ? -1 : 1;
1363
- const abs = Math.abs(c);
1364
- if (abs >= Et)
1365
- return sign * Math.pow(abs, 1 / 1.8);
1366
- return sign * (16 * abs);
810
+ const sign2 = c < 0 ? -1 : 1;
811
+ const abs2 = Math.abs(c);
812
+ if (abs2 >= Et)
813
+ return sign2 * Math.pow(abs2, 1 / 1.8);
814
+ return sign2 * (16 * abs2);
1367
815
  },
1368
816
  toBridgeMatrix: MATRICES.ProPhoto_to_XYZD50,
1369
817
  fromBridgeMatrix: MATRICES.XYZD50_to_ProPhoto
@@ -1413,7 +861,8 @@ var Saturon = (() => {
1413
861
  components: {
1414
862
  r: { index: 0, value: [0, 255], precision: 0 },
1415
863
  g: { index: 1, value: [0, 255], precision: 0 },
1416
- b: { index: 2, value: [0, 255], precision: 0 }
864
+ b: { index: 2, value: [0, 255], precision: 0 },
865
+ alpha: { index: 3, value: [0, 1], precision: 3 }
1417
866
  },
1418
867
  bridge: "xyz-d65",
1419
868
  toBridge: RGB_to_XYZD65,
@@ -1423,9 +872,10 @@ var Saturon = (() => {
1423
872
  supportsLegacy: true,
1424
873
  alphaVariant: "hsla",
1425
874
  components: {
1426
- h: { index: 0, value: "angle", precision: 0 },
875
+ h: { index: 0, value: "hue", precision: 0 },
1427
876
  s: { index: 1, value: "percentage", precision: 0 },
1428
- l: { index: 2, value: "percentage", precision: 0 }
877
+ l: { index: 2, value: "percentage", precision: 0 },
878
+ alpha: { index: 3, value: [0, 1], precision: 3 }
1429
879
  },
1430
880
  bridge: "rgb",
1431
881
  toBridge: HSL_to_RGB,
@@ -1433,9 +883,10 @@ var Saturon = (() => {
1433
883
  },
1434
884
  hwb: {
1435
885
  components: {
1436
- h: { index: 0, value: "angle", precision: 0 },
886
+ h: { index: 0, value: "hue", precision: 0 },
1437
887
  w: { index: 1, value: "percentage", precision: 0 },
1438
- b: { index: 2, value: "percentage", precision: 0 }
888
+ b: { index: 2, value: "percentage", precision: 0 },
889
+ alpha: { index: 3, value: [0, 1], precision: 3 }
1439
890
  },
1440
891
  bridge: "rgb",
1441
892
  toBridge: HWB_to_RGB,
@@ -1446,7 +897,8 @@ var Saturon = (() => {
1446
897
  components: {
1447
898
  l: { index: 0, value: "percentage", precision: 5 },
1448
899
  a: { index: 1, value: [-125, 125], precision: 5 },
1449
- b: { index: 2, value: [-125, 125], precision: 5 }
900
+ b: { index: 2, value: [-125, 125], precision: 5 },
901
+ alpha: { index: 3, value: [0, 1], precision: 3 }
1450
902
  },
1451
903
  bridge: "xyz-d50",
1452
904
  toBridge: LAB_to_XYZD50,
@@ -1457,7 +909,8 @@ var Saturon = (() => {
1457
909
  components: {
1458
910
  l: { index: 0, value: "percentage", precision: 5 },
1459
911
  c: { index: 1, value: [0, 150], precision: 5 },
1460
- h: { index: 2, value: "angle", precision: 5 }
912
+ h: { index: 2, value: "hue", precision: 5 },
913
+ alpha: { index: 3, value: [0, 1], precision: 3 }
1461
914
  },
1462
915
  bridge: "lab",
1463
916
  toBridge: LCH_to_LAB,
@@ -1468,7 +921,8 @@ var Saturon = (() => {
1468
921
  components: {
1469
922
  l: { index: 0, value: [0, 1], precision: 5 },
1470
923
  a: { index: 1, value: [-0.4, 0.4], precision: 5 },
1471
- b: { index: 2, value: [-0.4, 0.4], precision: 5 }
924
+ b: { index: 2, value: [-0.4, 0.4], precision: 5 },
925
+ alpha: { index: 3, value: [0, 1], precision: 3 }
1472
926
  },
1473
927
  bridge: "xyz-d65",
1474
928
  toBridge: OKLAB_to_XYZD65,
@@ -1479,7 +933,8 @@ var Saturon = (() => {
1479
933
  components: {
1480
934
  l: { index: 0, value: [0, 1], precision: 5 },
1481
935
  c: { index: 1, value: [0, 0.4], precision: 5 },
1482
- h: { index: 2, value: "angle", precision: 5 }
936
+ h: { index: 2, value: "hue", precision: 5 },
937
+ alpha: { index: 3, value: [0, 1], precision: 3 }
1483
938
  },
1484
939
  bridge: "oklab",
1485
940
  toBridge: OKLCH_to_OKLAB,
@@ -1487,401 +942,1516 @@ var Saturon = (() => {
1487
942
  },
1488
943
  ...colorSpaces
1489
944
  };
1490
- var colorFunctions = Object.fromEntries(Object.entries(colorModels).map(([name, converter]) => [
1491
- name,
1492
- modelConverterToColorConverter(name, converter)
1493
- ]));
1494
- var colorBases = {
1495
- "hex-color": {
1496
- isValid: (str) => str[0] === "#",
1497
- bridge: "rgb",
1498
- toBridge: (coords) => coords,
1499
- parse: (str) => {
1500
- const HEX = str.slice(1);
1501
- if (![3, 4, 6, 8].includes(HEX.length))
1502
- throw new Error("Invalid hex color length.");
1503
- for (const ch of HEX) {
1504
- const code = ch.charCodeAt(0);
1505
- const isDigit = code >= 48 && code <= 57;
1506
- const isLower = code >= 97 && code <= 102;
1507
- const isUpper = code >= 65 && code <= 70;
1508
- if (!(isDigit || isLower || isUpper))
1509
- throw new Error("Invalid hex color character.");
1510
- }
1511
- const expand = (c) => parseInt(c.length === 1 ? c + c : c, 16);
1512
- const [r, g, b, a = 255] = HEX.length <= 4 ? HEX.split("").map(expand) : [HEX.slice(0, 2), HEX.slice(2, 4), HEX.slice(4, 6), HEX.slice(6, 8)].map((c) => parseInt(c || "ff", 16));
1513
- return [r, g, b, a / 255];
1514
- },
1515
- fromBridge: (coords) => coords,
1516
- format: ([r, g, b, a = 1]) => {
1517
- const toHex = (v) => v.toString(16).padStart(2, "0");
1518
- const hex = [r, g, b].map((v) => toHex(Math.round(Math.max(0, Math.min(255, v))))).join("");
1519
- return `#${hex}${a < 1 ? toHex(Math.round(a * 255)) : ""}`;
1520
- }
1521
- },
1522
- ...colorFunctions,
1523
- "named-color": {
1524
- isValid: (str) => Object.keys(namedColors).some((key) => key === str),
1525
- bridge: "rgb",
1526
- toBridge: (coords) => coords,
1527
- parse: (name) => {
1528
- const rgb = namedColors[name];
1529
- if (!rgb)
1530
- throw new Error(`Invalid named-color: ${name}.`);
1531
- return [...rgb, 1];
1532
- },
1533
- fromBridge: (coords) => coords,
1534
- format: (rgb) => {
1535
- const [r, g, b] = rgb.map((v, i) => i < 3 ? Math.round(Math.min(255, Math.max(0, v))) : v);
1536
- for (const [name, [nr, ng, nb]] of Object.entries(namedColors)) {
1537
- if (r === nr && g === ng && b === nb)
1538
- return name;
945
+
946
+ // dist/engine.js
947
+ var grammar = `
948
+ /* ==========================================================================
949
+ Core Color Types
950
+ ========================================================================== */
951
+ <color> = <color-base> | currentColor | <system-color> | <contrast-color()> | <device-cmyk()> | <light-dark-color>
952
+
953
+ <color-base> = <hex-color> | <color-function> | <named-color> | <color-mix()> | transparent
954
+
955
+ <color-function> = <rgb()> | <rgba()> | <hsl()> | <hsla()> | <hwb()> | <lab()> | <lch()> | <oklab()> | <oklch()> | <alpha()> | <color()> | <custom-color-function>
956
+
957
+ /* ==========================================================================
958
+ RGB & RGBA Syntax
959
+ ========================================================================== */
960
+ <rgb()> = [ <legacy-rgb-syntax> | <modern-rgb-syntax> | <relative-rgb-syntax> ]
961
+ <rgba()> = [ <legacy-rgba-syntax> | <modern-rgba-syntax> | <relative-rgba-syntax> ]
962
+
963
+ <legacy-rgb-syntax> = rgb( <percentage>#{3} [ , <alpha-value> ]? ) | rgb( <number>#{3} [ , <alpha-value> ]? )
964
+ <legacy-rgba-syntax> = rgba( <percentage>#{3} [ , <alpha-value> ]? ) | rgba( <number>#{3} [ , <alpha-value> ]? )
965
+
966
+ <modern-rgb-syntax> = rgb( <rgb-absolute-component>{3} [ / <alpha-absolute-value> ]? )
967
+ <modern-rgba-syntax> = rgba( <rgb-absolute-component>{3} [ / <alpha-absolute-value> ]? )
968
+ <rgb-absolute-component> = <number> | <percentage> | none
969
+ <alpha-absolute-value> = <alpha-value> | none
970
+
971
+ <relative-rgb-syntax> = rgb( from <color> <rgb-relative-component>{3} [ / <rgb-relative-alpha> ]? )
972
+ <relative-rgba-syntax> = rgba( from <color> <rgb-relative-component>{3} [ / <rgb-relative-alpha> ]? )
973
+
974
+ /* ==========================================================================
975
+ HSL & HSLA Syntax
976
+ ========================================================================== */
977
+ <hsl()> = [ <legacy-hsl-syntax> | <modern-hsl-syntax> | <relative-hsl-syntax> ]
978
+ <hsla()> = [ <legacy-hsla-syntax> | <modern-hsla-syntax> | <relative-hsla-syntax> ]
979
+
980
+ <legacy-hsl-syntax> = hsl( <hue> , <percentage> , <percentage> [ , <alpha-value> ]? )
981
+ <legacy-hsla-syntax> = hsla( <hue> , <percentage> , <percentage> [ , <alpha-value> ]? )
982
+
983
+ <modern-hsl-syntax> = hsl( [ <hue> | none ] [ <percentage> | <number> | none ]{2} [ / <alpha-absolute-value> ]? )
984
+ <modern-hsla-syntax> = hsla( [ <hue> | none ] [ <percentage> | <number> | none ]{2} [ / <alpha-absolute-value> ]? )
985
+
986
+ <relative-hsl-syntax> = hsl( from <color> <hsl-relative-hue> <hsl-relative-component>{2} [ / <hsl-relative-alpha> ]? )
987
+ <relative-hsla-syntax> = hsla( from <color> <hsl-relative-hue> <hsl-relative-component>{2} [ / <hsl-relative-alpha> ]? )
988
+ <hsl-relative-hue> = <hue> | none | h | s | l | alpha
989
+ <hsl-relative-component> = <percentage> | <number> | none | h | s | l | alpha
990
+ <hsl-relative-alpha> = <alpha-value> | none | h | s | l | alpha
991
+
992
+ /* ==========================================================================
993
+ HWB Syntax
994
+ ========================================================================== */
995
+ <hwb()> = [ <absolute-hwb-syntax> | <relative-hwb-syntax> ]
996
+ <absolute-hwb-syntax> = hwb( [ <hue> | none ] [ <percentage> | <number> | none ]{2} [ / <alpha-absolute-value> ]? )
997
+ <relative-hwb-syntax> = hwb( from <color> <hwb-relative-hue> <hwb-relative-component>{2} [ / <hwb-relative-alpha> ]? )
998
+ <hwb-relative-hue> = <hue> | none | h | w | b | alpha
999
+ <hwb-relative-component> = <percentage> | <number> | none | h | w | b | alpha
1000
+ <hwb-relative-alpha> = <alpha-value> | none | h | w | b | alpha
1001
+
1002
+ /* ==========================================================================
1003
+ LAB & LCH Syntax
1004
+ ========================================================================== */
1005
+ <lab()> = [ <absolute-lab-syntax> | <relative-lab-syntax> ]
1006
+ <absolute-lab-syntax> = lab( [ <percentage> | <number> | none ]{3} [ / <alpha-absolute-value> ]? )
1007
+ <relative-lab-syntax> = lab( from <color> <lab-relative-component>{3} [ / <lab-relative-alpha> ]? )
1008
+ <lab-relative-component> = <percentage> | <number> | none | l | a | b | alpha
1009
+ <lab-relative-alpha> = <alpha-value> | none | l | a | b | alpha
1010
+
1011
+ <lch()> = [ <absolute-lch-syntax> | <relative-lch-syntax> ]
1012
+ <absolute-lch-syntax> = lch( [ <percentage> | <number> | none ]{2} [ <hue> | none ] [ / <alpha-absolute-value> ]? )
1013
+ <relative-lch-syntax> = lch( from <color> <lch-relative-component>{2} <lch-relative-hue> [ / <lch-relative-alpha> ]? )
1014
+ <lch-relative-component> = <percentage> | <number> | none | l | c | h | alpha
1015
+ <lch-relative-hue> = <hue> | none | l | c | h | alpha
1016
+ <lch-relative-alpha> = <alpha-value> | none | l | c | h | alpha
1017
+
1018
+ /* ==========================================================================
1019
+ OKLAB & OKLCH Syntax
1020
+ ========================================================================== */
1021
+ <oklab()> = [ <absolute-oklab-syntax> | <relative-oklab-syntax> ]
1022
+ <absolute-oklab-syntax> = oklab( [ <percentage> | <number> | none ]{3} [ / <alpha-absolute-value> ]? )
1023
+ <relative-oklab-syntax> = oklab( from <color> <oklab-relative-component>{3} [ / <oklab-relative-alpha> ]? )
1024
+ <oklab-relative-component> = <percentage> | <number> | none | l | a | b | alpha
1025
+ <oklab-relative-alpha> = <alpha-value> | none | l | a | b | alpha
1026
+
1027
+ <oklch()> = [ <absolute-oklch-syntax> | <relative-oklch-syntax> ]
1028
+ <absolute-oklch-syntax> = oklch( [ <percentage> | <number> | none ]{2} [ <hue> | none ] [ / <alpha-absolute-value> ]? )
1029
+ <relative-oklch-syntax> = oklch( from <color> <oklch-relative-component>{2} <oklch-relative-hue> [ / <oklch-relative-alpha> ]? )
1030
+ <oklch-relative-component> = <percentage> | <number> | none | l | c | h | alpha
1031
+ <oklch-relative-hue> = <hue> | none | l | c | h | alpha
1032
+ <oklch-relative-alpha> = <alpha-value> | none | l | c | h | alpha
1033
+
1034
+ /* ==========================================================================
1035
+ ALPHA Syntax
1036
+ ========================================================================== */
1037
+ <alpha()> = alpha( from <color> [ / [ <alpha-value> | none | alpha ] ]? )
1038
+
1039
+ /* ==========================================================================
1040
+ COLOR Space Function Syntax
1041
+ ========================================================================== */
1042
+ <color()> = [ <absolute-color-syntax> | <relative-color-syntax> ]
1043
+ <absolute-color-syntax> = color( [ <predefined-rgb> | <xyz-space> | <custom-color-space> ] [ <number> | <percentage> | none ]{3} [ / [ <alpha-value> | none ] ]? )
1044
+ <relative-color-syntax> = [ <relative-rgb-color-syntax> | <relative-xyz-color-syntax> | <relative-custom-color-syntax> ]
1045
+
1046
+ <relative-rgb-color-syntax> = color( from <color> <predefined-rgb> <rgb-relative-component>{3} [ / <rgb-relative-alpha> ]? )
1047
+
1048
+ <relative-xyz-color-syntax> = color( from <color> <xyz-space> <xyz-relative-component>{3} [ / <xyz-relative-alpha> ]? )
1049
+ <xyz-relative-component> = <number> | <percentage> | none | x | y | z | alpha
1050
+ <xyz-relative-alpha> = <alpha-value> | none | x | y | z | alpha
1051
+
1052
+ <predefined-rgb> = srgb | srgb-linear | display-p3 | a98-rgb | prophoto-rgb | rec2020
1053
+ <xyz-space> = xyz | xyz-d50 | xyz-d65
1054
+
1055
+ <relative-custom-color-syntax> = %unregistered-sentinal-value%
1056
+
1057
+ /* ==========================================================================
1058
+ Functional Utilities (Light/Dark, Contrast, CMYK, Mix)
1059
+ ========================================================================== */
1060
+ <light-dark-color> = light-dark( <color> , <color> )
1061
+
1062
+ <contrast-color()> = contrast-color( <color> )
1063
+
1064
+ <device-cmyk()> = <legacy-device-cmyk-syntax> | <modern-device-cmyk-syntax>
1065
+ <legacy-device-cmyk-syntax> = device-cmyk( <number>#{4} )
1066
+ <modern-device-cmyk-syntax> = device-cmyk( <cmyk-component>{4} [ / [ <alpha-value> | none ] ]? )
1067
+ <cmyk-component> = <number> | <percentage> | none
1068
+
1069
+ <color-mix()> = color-mix( [ <color-interpolation-method> , ]? [ <color> && <percentage [0,100]>? ]# )
1070
+ <color-interpolation-method> = in [ <rectangular-color-space> | <polar-color-space> <hue-interpolation-method>? ]
1071
+ <color-space> = <rectangular-color-space> | <polar-color-space>
1072
+ <rectangular-color-space> = srgb | srgb-linear | display-p3 | display-p3-linear | a98-rgb | prophoto-rgb | rec2020 | lab | oklab | <xyz-space> | <custom-color-space> | <custom-rectangular-space>
1073
+ <polar-color-space> = hsl | hwb | lch | oklch | <custom-polar-space>
1074
+ <hue-interpolation-method> = [ shorter | longer | increasing | decreasing ] hue
1075
+
1076
+ <custom-color-function> = %unregistered-sentinel-value%
1077
+ <custom-rectangular-space> = %unregistered-sentinel-value%
1078
+ <custom-polar-space> = %unregistered-sentinel-value%
1079
+
1080
+ /* ==========================================================================
1081
+ Shared Value Components
1082
+ ========================================================================== */
1083
+ <alpha-value> = <number> | <percentage>
1084
+ <hue> = <number> | <angle>
1085
+ <rgb-relative-component> = <number> | <percentage> | none | r | g | b | alpha
1086
+ <rgb-relative-alpha> = <alpha-value> | none | r | g | b | alpha
1087
+ <custom-color-space> = %unregistered-sentinel-value%
1088
+ `;
1089
+ var RULE_START_RE = /^<[^>]+>\s*=/;
1090
+ var HASH_REPEAT_RE = /^(.*)#\{(\d+)\}$/;
1091
+ var REPEAT_RE = /^(.*)\{(\d+)\}$/;
1092
+ var REF_RE = /^<([^>\s]+)(?:\s+([^>]+))?>$/;
1093
+ var ARG_RE = /\[[^\]]+\]|[^\s]+/g;
1094
+ var grammarMap = parseGrammar(grammar);
1095
+ function concatNodes(a, b) {
1096
+ const lenA = a.length;
1097
+ const lenB = b.length;
1098
+ const result = new Array(lenA + lenB);
1099
+ for (let i = 0; i < lenA; i++)
1100
+ result[i] = a[i];
1101
+ for (let i = 0; i < lenB; i++)
1102
+ result[lenA + i] = b[i];
1103
+ return result;
1104
+ }
1105
+ function parseCmykComponent(n) {
1106
+ if (!n?.value)
1107
+ return 0;
1108
+ const raw = Array.isArray(n.value) ? n.value[0]?.value : n.value;
1109
+ return parseFloat(raw.trim());
1110
+ }
1111
+ function getLiteralValue(n) {
1112
+ if (!n)
1113
+ return "";
1114
+ if (n.type === "literal")
1115
+ return n.value;
1116
+ if (Array.isArray(n.value))
1117
+ return getLiteralValue(n.value[0]);
1118
+ return "";
1119
+ }
1120
+ function validateTokens(tokens, rootRule) {
1121
+ const memo = /* @__PURE__ */ new Map();
1122
+ const rulesToTest = rootRule ? [rootRule] : Object.keys(grammarMap);
1123
+ for (let i = 0; i < rulesToTest.length; i++) {
1124
+ const ruleName = rulesToTest[i];
1125
+ const results = matchRule(ruleName, tokens, 0, grammarMap, memo, null);
1126
+ for (let j = 0; j < results.length; j++) {
1127
+ const result = results[j];
1128
+ if (result.success && result.nextIndex === tokens.length) {
1129
+ return result.nodes[0];
1539
1130
  }
1540
1131
  }
1541
- },
1542
- "color-mix": {
1543
- isValid: (str) => str.slice(0, 10) === "color-mix(" && str[str.length - 1] === ")",
1544
- bridge: "rgb",
1545
- toBridge: (coords) => coords,
1546
- parse: (str) => {
1547
- const extractColorAndWeight = (colorStr) => {
1548
- const s = colorStr.trim();
1549
- let weight;
1550
- let remaining = s;
1551
- const leadingWeightMatch = remaining.match(/^(\d+)%\s+/);
1552
- if (leadingWeightMatch) {
1553
- const raw = parseInt(leadingWeightMatch[1], 10);
1554
- weight = Math.min(1, Math.max(0, raw / 100));
1555
- remaining = remaining.slice(leadingWeightMatch[0].length).trim();
1556
- }
1557
- let colorExpression = "";
1558
- let rest = "";
1559
- if (/^[a-z]/i.test(remaining)) {
1560
- const { expression: expr, end: e } = extractBalancedExpression(remaining, 0);
1561
- if (expr) {
1562
- colorExpression = expr;
1563
- rest = remaining.slice(e).trim();
1564
- } else {
1565
- const m = remaining.match(/^([^\s]+)(.*)$/);
1566
- colorExpression = m ? m[1] : remaining;
1567
- rest = m ? m[2].trim() : "";
1568
- }
1569
- } else {
1570
- const m = remaining.match(/^([^\s]+)(.*)$/);
1571
- colorExpression = m ? m[1] : remaining;
1572
- rest = m ? m[2].trim() : "";
1132
+ }
1133
+ throw new Error("Invalid syntax");
1134
+ }
1135
+ function matchRule(ruleName, tokens, index, grammar2, memo, currentFunction, args) {
1136
+ const key = `${ruleName}:${index}:${currentFunction || ""}:${args ? args.join(",") : ""}`;
1137
+ const cached = memo.get(key);
1138
+ if (cached)
1139
+ return cached;
1140
+ const token = tokens[index];
1141
+ const validator = validators[ruleName];
1142
+ if (validator) {
1143
+ if (!token)
1144
+ return [];
1145
+ if (validator(token, args)) {
1146
+ const result = [
1147
+ {
1148
+ success: true,
1149
+ nextIndex: index + 1,
1150
+ nodes: [{ type: ruleName, value: token }]
1573
1151
  }
1574
- if (weight === void 0) {
1575
- const trailingWeightMatch = rest.match(/^(-?(?:\d+\.?\d*|\.\d+))%/);
1576
- if (trailingWeightMatch) {
1577
- const raw = parseInt(trailingWeightMatch[1], 10);
1578
- weight = raw / 100;
1579
- rest = rest.slice(trailingWeightMatch[0].length).trim();
1580
- if (weight < 0) {
1581
- throw new Error("Percentages less than 0 are not valid.");
1582
- } else if (weight > 1) {
1583
- throw new Error("Percentages greater than 100 are not valid.");
1584
- }
1585
- } else if (rest.slice(0, 5) === "calc(") {
1586
- const { expression: calcExpr, end } = extractBalancedExpression(rest, 0);
1587
- if (!calcExpr) {
1588
- throw new Error("Malformed calc() weight expression.");
1152
+ ];
1153
+ memo.set(key, result);
1154
+ return result;
1155
+ }
1156
+ memo.set(key, []);
1157
+ return [];
1158
+ }
1159
+ const rule = grammar2[ruleName];
1160
+ if (!rule) {
1161
+ memo.set(key, []);
1162
+ return [];
1163
+ }
1164
+ const results = matchNode(rule, tokens, index, grammar2, memo, currentFunction);
1165
+ const wrapped = new Array(results.length);
1166
+ for (let i = 0; i < results.length; i++) {
1167
+ wrapped[i] = {
1168
+ success: true,
1169
+ nextIndex: results[i].nextIndex,
1170
+ nodes: [{ type: ruleName, value: results[i].nodes }]
1171
+ };
1172
+ }
1173
+ memo.set(key, wrapped);
1174
+ return wrapped;
1175
+ }
1176
+ function matchNode(node, tokens, index, grammar2, memo, currentFunction) {
1177
+ switch (node.kind) {
1178
+ case "literal": {
1179
+ const token = tokens[index];
1180
+ if (!token || token !== node.value.toLowerCase())
1181
+ return [];
1182
+ return [
1183
+ {
1184
+ success: true,
1185
+ nextIndex: index + 1,
1186
+ nodes: [
1187
+ {
1188
+ type: token === "(" || token === ")" || token === "," || token === "/" ? "symbol" : "literal",
1189
+ value: token
1589
1190
  }
1590
- rest = rest.slice(end).trim();
1591
- }
1592
- }
1593
- if (rest.length > 0) {
1594
- throw new Error(`Unexpected extra tokens after color: '${rest}'.`);
1595
- }
1596
- const color = Color.from(colorExpression);
1597
- return { color, weight };
1598
- };
1599
- const getWeight2Prime = (weight12, weight22) => {
1600
- if (weight12 === void 0 && typeof weight22 === "number") {
1601
- weight12 = 1 - weight22;
1602
- } else if (typeof weight12 === "number" && weight22 === void 0) {
1603
- weight22 = 1 - weight12;
1604
- } else if (weight12 === void 0 && weight22 === void 0) {
1605
- weight12 = weight22 = 0.5;
1606
- } else {
1607
- weight12 = weight12;
1608
- weight22 = weight22;
1609
- if (weight12 + weight22 <= 0) {
1610
- throw new Error("Sum of percengates cannot be 0%.");
1611
- }
1191
+ ]
1612
1192
  }
1613
- const totalWeight = weight12 + weight22;
1614
- let alphaMultiplier2;
1615
- if (totalWeight > 1) {
1616
- weight12 /= totalWeight;
1617
- weight22 /= totalWeight;
1618
- } else {
1619
- weight12 /= totalWeight;
1620
- weight22 /= totalWeight;
1621
- alphaMultiplier2 = totalWeight;
1193
+ ];
1194
+ }
1195
+ case "ref": {
1196
+ const token = tokens[index];
1197
+ if (!token)
1198
+ return [];
1199
+ if (currentFunction && colorModels[currentFunction]) {
1200
+ const component = colorModels[currentFunction].components[token];
1201
+ if (component && component.value === node.name) {
1202
+ return [
1203
+ {
1204
+ success: true,
1205
+ nextIndex: index + 1,
1206
+ nodes: [{ type: node.name, value: token }]
1207
+ }
1208
+ ];
1622
1209
  }
1623
- return { amount: weight22, alphaMultiplier: alphaMultiplier2 };
1624
- };
1625
- const fnName = "color-mix";
1626
- const { expression } = extractBalancedExpression(str, fnName.length);
1627
- if (!expression)
1628
- throw new Error("Malformed color-mix expression.");
1629
- const inner = expression.slice(1, -1).trim();
1630
- const parts = [];
1631
- let i = 0;
1632
- let current = "";
1633
- while (i < inner.length) {
1634
- const char = inner[i];
1635
- if (char === ",") {
1636
- parts.push(current.trim());
1637
- current = "";
1638
- i++;
1639
- continue;
1210
+ }
1211
+ return matchRule(node.name, tokens, index, grammar2, memo, currentFunction, node.args);
1212
+ }
1213
+ case "choice": {
1214
+ const results = [];
1215
+ for (let i = 0; i < node.nodes.length; i++) {
1216
+ const childMatches = matchNode(node.nodes[i], tokens, index, grammar2, memo, currentFunction);
1217
+ for (let j = 0; j < childMatches.length; j++) {
1218
+ results.push(childMatches[j]);
1640
1219
  }
1641
- if (char === "(" || /[a-zA-Z]/.test(char)) {
1642
- const { expression: expr, end } = extractBalancedExpression(inner, i);
1643
- if (expr) {
1644
- current += expr;
1645
- i = end;
1646
- continue;
1220
+ }
1221
+ return results;
1222
+ }
1223
+ case "sequence": {
1224
+ let states = [{ success: true, nextIndex: index, nodes: [] }];
1225
+ for (let i = 0; i < node.nodes.length; i++) {
1226
+ const child = node.nodes[i];
1227
+ const nextStates = [];
1228
+ for (let j = 0; j < states.length; j++) {
1229
+ const state = states[j];
1230
+ const matches = matchNode(child, tokens, state.nextIndex, grammar2, memo, currentFunction);
1231
+ for (let k = 0; k < matches.length; k++) {
1232
+ nextStates.push({
1233
+ success: true,
1234
+ nextIndex: matches[k].nextIndex,
1235
+ nodes: concatNodes(state.nodes, matches[k].nodes)
1236
+ });
1647
1237
  }
1648
1238
  }
1649
- current += char;
1650
- i++;
1239
+ states = nextStates;
1240
+ if (states.length === 0)
1241
+ return [];
1651
1242
  }
1652
- parts.push(current.trim());
1653
- if (parts.length !== 3) {
1654
- throw new Error("color-mix must have three comma-separated parts.");
1243
+ return states;
1244
+ }
1245
+ case "optional": {
1246
+ const matches = matchNode(node.node, tokens, index, grammar2, memo, currentFunction);
1247
+ const results = new Array(matches.length + 1);
1248
+ results[0] = { success: true, nextIndex: index, nodes: [] };
1249
+ for (let i = 0; i < matches.length; i++) {
1250
+ results[i + 1] = matches[i];
1655
1251
  }
1656
- const inPart = parts[0];
1657
- const inMatch = inPart.match(/^in\s+([a-z0-9-]+)(?:\s+(shorter|longer|increasing|decreasing)\s+hue)?$/);
1658
- if (!inMatch) {
1659
- throw new Error("Invalid model and hue format.");
1252
+ return results;
1253
+ }
1254
+ case "repeat": {
1255
+ let states = [{ success: true, nextIndex: index, nodes: [] }];
1256
+ for (let i = 0; i < node.min; i++) {
1257
+ const nextStates = [];
1258
+ for (let j = 0; j < states.length; j++) {
1259
+ const state = states[j];
1260
+ const matches = matchNode(node.node, tokens, state.nextIndex, grammar2, memo, currentFunction);
1261
+ for (let k = 0; k < matches.length; k++) {
1262
+ nextStates.push({
1263
+ success: true,
1264
+ nextIndex: matches[k].nextIndex,
1265
+ nodes: concatNodes(state.nodes, matches[k].nodes)
1266
+ });
1267
+ }
1268
+ }
1269
+ states = nextStates;
1270
+ if (states.length === 0)
1271
+ return [];
1660
1272
  }
1661
- const model = inMatch[1];
1662
- let hue = inMatch[2];
1663
- if (model === "rgb")
1664
- throw new Error(`RGB model is not allowed in color-mix(). Use "srgb" instead.`);
1665
- if (hue) {
1666
- const comps = colorModels[model].components;
1667
- if (!comps) {
1668
- throw new Error(`Unknown color model: ${model}.`);
1273
+ return states;
1274
+ }
1275
+ case "permutation": {
1276
+ const numNodes = node.nodes.length;
1277
+ const fullMask = (1 << numNodes) - 1;
1278
+ let states = [{ nextIndex: index, nodes: [], remainingMask: fullMask }];
1279
+ const finalResults = [];
1280
+ while (states.length > 0) {
1281
+ const nextStates = [];
1282
+ for (let i = 0; i < states.length; i++) {
1283
+ const state = states[i];
1284
+ let allRemainingOptional = true;
1285
+ for (let bit = 0; bit < numNodes; bit++) {
1286
+ if ((state.remainingMask & 1 << bit) !== 0) {
1287
+ if (node.nodes[bit].kind !== "optional") {
1288
+ allRemainingOptional = false;
1289
+ break;
1290
+ }
1291
+ }
1292
+ }
1293
+ if (allRemainingOptional) {
1294
+ finalResults.push({
1295
+ success: true,
1296
+ nextIndex: state.nextIndex,
1297
+ nodes: state.nodes
1298
+ });
1299
+ }
1300
+ for (let bit = 0; bit < numNodes; bit++) {
1301
+ if ((state.remainingMask & 1 << bit) !== 0) {
1302
+ const child = node.nodes[bit];
1303
+ const nodeToMatch = child.kind === "optional" ? child.node : child;
1304
+ const matches = matchNode(nodeToMatch, tokens, state.nextIndex, grammar2, memo, currentFunction);
1305
+ const nextMask = state.remainingMask & ~(1 << bit);
1306
+ for (let m = 0; m < matches.length; m++) {
1307
+ nextStates.push({
1308
+ nextIndex: matches[m].nextIndex,
1309
+ nodes: concatNodes(state.nodes, matches[m].nodes),
1310
+ remainingMask: nextMask
1311
+ });
1312
+ }
1313
+ }
1314
+ }
1669
1315
  }
1670
- const hasHue = Object.values(comps).some((c) => c.value === "angle");
1671
- if (!hasHue) {
1672
- throw new Error(`Hue interpolation not supported in ${model} space.`);
1316
+ states = nextStates;
1317
+ }
1318
+ return finalResults;
1319
+ }
1320
+ case "hashList": {
1321
+ let currentStates = matchNode(node.node, tokens, index, grammar2, memo, currentFunction);
1322
+ const allSuccessfulStates = [];
1323
+ while (currentStates.length > 0) {
1324
+ const nextStates = [];
1325
+ for (let i = 0; i < currentStates.length; i++) {
1326
+ const state = currentStates[i];
1327
+ allSuccessfulStates.push(state);
1328
+ const commaIdx = state.nextIndex;
1329
+ if (commaIdx < tokens.length && tokens[commaIdx] === ",") {
1330
+ const commaNode = { type: "symbol", value: "," };
1331
+ const matches = matchNode(node.node, tokens, commaIdx + 1, grammar2, memo, currentFunction);
1332
+ for (let m = 0; m < matches.length; m++) {
1333
+ const match = matches[m];
1334
+ if (match.nextIndex > commaIdx) {
1335
+ const newNodes = new Array(state.nodes.length + 1 + match.nodes.length);
1336
+ let idx = 0;
1337
+ for (let n = 0; n < state.nodes.length; n++)
1338
+ newNodes[idx++] = state.nodes[n];
1339
+ newNodes[idx++] = commaNode;
1340
+ for (let n = 0; n < match.nodes.length; n++)
1341
+ newNodes[idx++] = match.nodes[n];
1342
+ nextStates.push({
1343
+ success: true,
1344
+ nextIndex: match.nextIndex,
1345
+ nodes: newNodes
1346
+ });
1347
+ }
1348
+ }
1349
+ }
1673
1350
  }
1674
- } else {
1675
- hue = "shorter";
1351
+ currentStates = nextStates;
1676
1352
  }
1677
- const { color: color1, weight: weight1 } = extractColorAndWeight(parts[1]);
1678
- const { color: color2, weight: weight2 } = extractColorAndWeight(parts[2]);
1679
- const { amount, alphaMultiplier = 1 } = getWeight2Prime(weight1, weight2);
1680
- return color1.with({ alpha: (a) => a * alphaMultiplier }).in(model).mix(color2.with({ alpha: (a) => a * alphaMultiplier }), { amount, hue }).in("rgb").toArray({ fit: "none", precision: null });
1353
+ return allSuccessfulStates;
1681
1354
  }
1682
- },
1683
- transparent: {
1684
- isValid: (str) => str === "transparent",
1685
- bridge: "rgb",
1686
- toBridge: (coords) => coords,
1687
- parse: (str) => [0, 0, 0, 0]
1688
- // eslint-disable-line no-unused-vars
1689
1355
  }
1690
- };
1691
- var colorTypes = {
1692
- ...colorBases,
1693
- currentColor: {
1694
- isValid: (str) => str === "currentcolor",
1695
- bridge: "rgb",
1696
- toBridge: (coords) => coords,
1697
- parse: (str) => [0, 0, 0, 1]
1698
- // eslint-disable-line no-unused-vars
1699
- },
1700
- "system-color": {
1701
- isValid: (str) => Object.keys(config.systemColors).some((key) => key.toLowerCase() === str),
1702
- bridge: "rgb",
1703
- toBridge: (coords) => coords,
1704
- parse: (str) => {
1705
- const { systemColors: systemColors2 } = config;
1706
- const key = Object.keys(systemColors2).find((k) => k.toLowerCase() === str);
1707
- const rgbArr = systemColors2[key][config.theme === "light" ? 0 : 1];
1708
- return [...rgbArr, 1];
1356
+ }
1357
+ function tokenize(input) {
1358
+ const lower = input.toLowerCase();
1359
+ const tokens = [];
1360
+ let i = 0;
1361
+ while (i < lower.length) {
1362
+ if (lower.slice(i, i + 2) === "/*") {
1363
+ const endIndex = lower.indexOf("*/", i + 2);
1364
+ if (endIndex !== -1) {
1365
+ i = endIndex + 2;
1366
+ } else {
1367
+ i = lower.length;
1368
+ }
1369
+ continue;
1709
1370
  }
1710
- },
1711
- "contrast-color": {
1712
- isValid: (str) => str.slice(0, 15) === "contrast-color(" && str[str.length - 1] === ")",
1713
- bridge: "rgb",
1714
- toBridge: (coords) => coords,
1715
- parse: (str) => {
1716
- const inner = str.slice(15, -1);
1717
- const [, luminance] = Color.from(inner).in("xyz-d65").toArray({ fit: "none", precision: null });
1718
- return luminance > 0.5 ? [0, 0, 0, 1] : [255, 255, 255, 1];
1371
+ const ch = lower[i];
1372
+ if (/\s/.test(ch)) {
1373
+ i++;
1374
+ continue;
1719
1375
  }
1720
- },
1721
- "device-cmyk": {
1722
- isValid: (str) => str.slice(0, 12) === "device-cmyk(" && str[str.length - 1] === ")",
1723
- bridge: "rgb",
1724
- toBridge: (coords) => coords,
1725
- parse: (str) => {
1726
- const fnName = "device-cmyk";
1727
- const fnIndex = str.indexOf(fnName);
1728
- if (fnIndex === -1)
1729
- throw new Error("Invalid device-cmyk syntax");
1730
- const { expression } = extractBalancedExpression(str, fnIndex + fnName.length);
1731
- if (!expression)
1732
- throw new Error("Malformed device-cmyk expression");
1733
- const content = expression.slice(1, -1).trim();
1734
- const tokens = [];
1735
- let i = 0;
1736
- while (i < content.length) {
1737
- const char = content[i];
1738
- if (char === " ") {
1376
+ if (lower.slice(i, i + 5) === "calc(") {
1377
+ const start2 = i;
1378
+ let parenCount = 0;
1379
+ while (i < lower.length) {
1380
+ if (lower[i] === "(")
1381
+ parenCount++;
1382
+ else if (lower[i] === ")") {
1383
+ parenCount--;
1384
+ if (parenCount === 0) {
1385
+ i++;
1386
+ break;
1387
+ }
1388
+ }
1389
+ i++;
1390
+ }
1391
+ tokens.push(lower.slice(start2, i));
1392
+ continue;
1393
+ }
1394
+ if ("(),/".includes(ch)) {
1395
+ tokens.push(ch);
1396
+ i++;
1397
+ continue;
1398
+ }
1399
+ const start = i;
1400
+ while (i < lower.length && !/\s/.test(lower[i]) && !"(),/".includes(lower[i])) {
1401
+ i++;
1402
+ }
1403
+ tokens.push(lower.slice(start, i));
1404
+ }
1405
+ return tokens;
1406
+ }
1407
+ function parseGrammar(grammar2) {
1408
+ const rules = {};
1409
+ const lines = grammar2.replace(/\/\*[\s\S]*?\*\//g, "").split("\n");
1410
+ let currentRuleName = "";
1411
+ let currentDef = "";
1412
+ for (let i = 0; i < lines.length; i++) {
1413
+ const line = lines[i].trim();
1414
+ if (!line)
1415
+ continue;
1416
+ if (RULE_START_RE.test(line)) {
1417
+ if (currentRuleName)
1418
+ rules[currentRuleName] = parseExpression(currentDef);
1419
+ const eqIndex = line.indexOf("=");
1420
+ currentRuleName = line.slice(0, eqIndex).trim();
1421
+ currentDef = line.slice(eqIndex + 1).trim();
1422
+ } else if (currentRuleName) {
1423
+ currentDef += " " + line;
1424
+ }
1425
+ }
1426
+ if (currentRuleName)
1427
+ rules[currentRuleName] = parseExpression(currentDef);
1428
+ return rules;
1429
+ }
1430
+ function parseExpression(expr) {
1431
+ const parts = splitTopLevel(expr, "|");
1432
+ if (parts.length > 1)
1433
+ return { kind: "choice", nodes: parts.map(parsePermutation) };
1434
+ return parsePermutation(expr);
1435
+ }
1436
+ function splitTopLevel(str, separator) {
1437
+ const result = [];
1438
+ let depth = 0;
1439
+ let start = 0;
1440
+ const sepLen = separator.length;
1441
+ const len = str.length;
1442
+ for (let i = 0; i < len; i++) {
1443
+ const ch = str.charCodeAt(i);
1444
+ if (ch === 91 || ch === 40) {
1445
+ depth++;
1446
+ } else if (ch === 93 || ch === 41) {
1447
+ depth--;
1448
+ } else if (depth === 0 && str.startsWith(separator, i)) {
1449
+ const part = str.slice(start, i).trim();
1450
+ if (part)
1451
+ result.push(part);
1452
+ i += sepLen - 1;
1453
+ start = i + 1;
1454
+ }
1455
+ }
1456
+ const last = str.slice(start).trim();
1457
+ if (last)
1458
+ result.push(last);
1459
+ return result;
1460
+ }
1461
+ function splitSequence(str) {
1462
+ const result = [];
1463
+ let i = 0;
1464
+ const len = str.length;
1465
+ while (i < len) {
1466
+ const ch = str.charCodeAt(i);
1467
+ if (ch === 32 || ch === 9 || ch === 10 || ch === 13) {
1468
+ i++;
1469
+ continue;
1470
+ }
1471
+ if (ch === 40 || ch === 41 || ch === 44 || ch === 47 || ch === 124) {
1472
+ result.push(str[i]);
1473
+ i++;
1474
+ continue;
1475
+ }
1476
+ const start = i;
1477
+ if (ch === 60) {
1478
+ while (i < len && str.charCodeAt(i) !== 62)
1479
+ i++;
1480
+ i++;
1481
+ while (i < len) {
1482
+ const c = str.charCodeAt(i);
1483
+ if (c === 32 || c === 9 || c === 10 || c === 13 || c === 40 || c === 41 || c === 44 || c === 47 || c === 124)
1484
+ break;
1485
+ i++;
1486
+ }
1487
+ result.push(str.slice(start, i));
1488
+ continue;
1489
+ }
1490
+ if (ch === 91) {
1491
+ let depth = 1;
1492
+ i++;
1493
+ while (i < len && depth > 0) {
1494
+ const c = str.charCodeAt(i);
1495
+ if (c === 91)
1496
+ depth++;
1497
+ else if (c === 93)
1498
+ depth--;
1499
+ i++;
1500
+ }
1501
+ while (i < len) {
1502
+ const c = str.charCodeAt(i);
1503
+ if (c === 32 || c === 9 || c === 10 || c === 13 || c === 40 || c === 41 || c === 44 || c === 47 || c === 124)
1504
+ break;
1505
+ i++;
1506
+ }
1507
+ result.push(str.slice(start, i));
1508
+ continue;
1509
+ }
1510
+ while (i < len) {
1511
+ const c = str.charCodeAt(i);
1512
+ if (c === 32 || c === 9 || c === 10 || c === 13 || c === 40 || c === 41 || c === 44 || c === 47 || c === 124)
1513
+ break;
1514
+ i++;
1515
+ }
1516
+ result.push(str.slice(start, i));
1517
+ }
1518
+ return result;
1519
+ }
1520
+ function parsePermutation(targetExpr) {
1521
+ const parts = splitTopLevel(targetExpr, "&&");
1522
+ if (parts.length > 1)
1523
+ return { kind: "permutation", nodes: parts.map(parseSequence) };
1524
+ return parseSequence(targetExpr);
1525
+ }
1526
+ function parseSequence(targetExpr) {
1527
+ const tokens = splitSequence(targetExpr);
1528
+ return { kind: "sequence", nodes: tokens.map(parseTerm) };
1529
+ }
1530
+ function parseTerm(term) {
1531
+ term = term.trim();
1532
+ const len = term.length;
1533
+ if (len === 1) {
1534
+ const c = term.charCodeAt(0);
1535
+ if (c === 40 || c === 41 || c === 44 || c === 47) {
1536
+ return { kind: "literal", value: term };
1537
+ }
1538
+ }
1539
+ const lastChar = term.charCodeAt(len - 1);
1540
+ if (lastChar === 35 && term.indexOf("{") === -1) {
1541
+ return { kind: "hashList", node: parseTerm(term.slice(0, -1)) };
1542
+ }
1543
+ if (lastChar === 63) {
1544
+ return { kind: "optional", node: parseTerm(term.slice(0, -1)) };
1545
+ }
1546
+ const hashRepeatMatch = HASH_REPEAT_RE.exec(term);
1547
+ if (hashRepeatMatch) {
1548
+ const innerNode = parseTerm(hashRepeatMatch[1]);
1549
+ const count = Number(hashRepeatMatch[2]);
1550
+ const commaNode = { kind: "literal", value: "," };
1551
+ const nodes = [];
1552
+ for (let i = 0; i < count; i++) {
1553
+ nodes.push(innerNode);
1554
+ if (i < count - 1)
1555
+ nodes.push(commaNode);
1556
+ }
1557
+ return { kind: "sequence", nodes };
1558
+ }
1559
+ const repeatMatch = REPEAT_RE.exec(term);
1560
+ if (repeatMatch) {
1561
+ const num = Number(repeatMatch[2]);
1562
+ return {
1563
+ kind: "repeat",
1564
+ node: parseTerm(repeatMatch[1]),
1565
+ min: num,
1566
+ max: num
1567
+ };
1568
+ }
1569
+ const firstChar = term.charCodeAt(0);
1570
+ if (firstChar === 91 && lastChar === 93) {
1571
+ return parseExpression(term.slice(1, -1));
1572
+ }
1573
+ if (firstChar === 60 && lastChar === 62) {
1574
+ const match = REF_RE.exec(term);
1575
+ if (match) {
1576
+ let argsArray = void 0;
1577
+ if (match[2]) {
1578
+ const argMatches = match[2].match(ARG_RE);
1579
+ if (argMatches) {
1580
+ argsArray = argMatches.map((arg) => arg.trim());
1581
+ }
1582
+ }
1583
+ return { kind: "ref", name: `<${match[1]}>`, args: argsArray };
1584
+ }
1585
+ return { kind: "ref", name: term };
1586
+ }
1587
+ return { kind: "literal", value: term };
1588
+ }
1589
+ var { PI: pi, E: e, pow, sqrt, sin, cos, tan, asin, acos, atan, atan2, exp, log, log10, log2, abs, min, max, hypot, round, ceil, floor, sign, trunc, random } = Math;
1590
+ var BASE_MATH_ENV = {
1591
+ pi,
1592
+ e,
1593
+ tau: pi * 2,
1594
+ pow,
1595
+ sqrt,
1596
+ sin,
1597
+ cos,
1598
+ tan,
1599
+ asin,
1600
+ acos,
1601
+ atan,
1602
+ atan2,
1603
+ exp,
1604
+ log,
1605
+ log10,
1606
+ log2,
1607
+ abs,
1608
+ min,
1609
+ max,
1610
+ hypot,
1611
+ round,
1612
+ ceil,
1613
+ floor,
1614
+ sign,
1615
+ trunc,
1616
+ random
1617
+ };
1618
+ function extractToken(n) {
1619
+ if (typeof n === "string")
1620
+ return n;
1621
+ if (n.type === "<percentage>")
1622
+ return String(n.value || "");
1623
+ if (Array.isArray(n.value))
1624
+ return extractToken(n.value[0]);
1625
+ return String(n.value || "");
1626
+ }
1627
+ function parsePercent(str, type, min2, max2) {
1628
+ const percent = parseFloat(str);
1629
+ if (isNaN(percent))
1630
+ throw new Error(`Invalid percentage: '${str}'.`);
1631
+ if (type === "percentage")
1632
+ return percent;
1633
+ if (min2 < 0 && max2 > 0)
1634
+ return percent / 100 * (max2 - min2) / 2;
1635
+ return percent / 100 * (max2 - min2) + min2;
1636
+ }
1637
+ function parseHue(str) {
1638
+ const val = parseFloat(str);
1639
+ if (isNaN(val))
1640
+ throw new Error(`Invalid hue: '${str}'.`);
1641
+ if (str[str.length - 1] === "deg")
1642
+ return val;
1643
+ if (str[str.length - 1] === "rad")
1644
+ return val * (180 / pi);
1645
+ if (str[str.length - 1] === "grad")
1646
+ return val * 0.9;
1647
+ if (str[str.length - 1] === "turn")
1648
+ return val * 360;
1649
+ return val;
1650
+ }
1651
+ function parseCalcExpression(expr, type, base, min2, max2) {
1652
+ if (expr === "infinity")
1653
+ return Infinity;
1654
+ if (expr === "-infinity")
1655
+ return -Infinity;
1656
+ if (expr === "nan")
1657
+ return NaN;
1658
+ const UNIT_NUM = 1;
1659
+ const UNIT_PCT = 2;
1660
+ const UNIT_ANG = 4;
1661
+ const getUnitName = (u) => u === UNIT_ANG ? "angle" : u === UNIT_PCT ? "percent" : "number";
1662
+ const tokenize2 = (s) => {
1663
+ const out = [];
1664
+ for (let i = 0; i < s.length; ) {
1665
+ const c = s[i];
1666
+ if (/\s/.test(c)) {
1667
+ i++;
1668
+ continue;
1669
+ }
1670
+ if (s.slice(i, i + 2) === "**") {
1671
+ out.push({ type: "operator", value: "**" });
1672
+ i += 2;
1673
+ continue;
1674
+ }
1675
+ if (/[0-9]/.test(c) || c === "." && /[0-9]/.test(s[i + 1] || "")) {
1676
+ let numStr = "";
1677
+ while (i < s.length && /[0-9.]/.test(s[i]))
1678
+ numStr += s[i++];
1679
+ if (i < s.length && /[eE]/.test(s[i])) {
1680
+ numStr += s[i++];
1681
+ if (/[+-]/.test(s[i]))
1682
+ numStr += s[i++];
1683
+ while (i < s.length && /[0-9]/.test(s[i]))
1684
+ numStr += s[i++];
1685
+ }
1686
+ let unitType = UNIT_NUM;
1687
+ let rawUnit = "";
1688
+ if (s[i] === "%") {
1689
+ unitType = UNIT_PCT;
1690
+ rawUnit = "%";
1739
1691
  i++;
1740
- continue;
1692
+ } else if (s.slice(i, i + 3) === "deg") {
1693
+ unitType = UNIT_ANG;
1694
+ rawUnit = "deg";
1695
+ i += 3;
1696
+ } else if (s.slice(i, i + 3) === "rad") {
1697
+ unitType = UNIT_ANG;
1698
+ rawUnit = "rad";
1699
+ i += 3;
1700
+ } else if (s.slice(i, i + 4) === "grad") {
1701
+ unitType = UNIT_ANG;
1702
+ rawUnit = "grad";
1703
+ i += 4;
1704
+ } else if (s.slice(i, i + 4) === "turn") {
1705
+ unitType = UNIT_ANG;
1706
+ rawUnit = "turn";
1707
+ i += 4;
1741
1708
  }
1742
- if (char === "(" || /[a-zA-Z-]/.test(char)) {
1743
- const { expression: expr, end } = extractBalancedExpression(content, i);
1744
- if (expr) {
1745
- tokens.push(expr);
1746
- i = end;
1747
- continue;
1748
- }
1709
+ let val = parseFloat(numStr);
1710
+ if (unitType === UNIT_PCT) {
1711
+ val = parsePercent(numStr + rawUnit, type, min2, max2);
1712
+ } else if (unitType === UNIT_ANG) {
1713
+ val = parseHue(numStr + rawUnit);
1749
1714
  }
1750
- if (/[\d.+-]/.test(char)) {
1751
- let num = "";
1752
- while (i < content.length && /[\d.eE+-]/.test(content[i])) {
1753
- num += content[i];
1754
- i++;
1755
- }
1756
- if (i < content.length && content[i] === "%") {
1757
- num += "%";
1758
- i++;
1759
- } else if (i < content.length && /[a-zA-Z]/.test(content[i])) {
1760
- while (i < content.length && /[a-zA-Z]/.test(content[i])) {
1761
- num += content[i];
1762
- i++;
1715
+ out.push({ type: "number", value: val, unit: unitType });
1716
+ continue;
1717
+ }
1718
+ if (/[a-zA-Z_]/.test(c)) {
1719
+ let id = "";
1720
+ while (i < s.length && /[a-zA-Z0-9_]/.test(s[i]))
1721
+ id += s[i++];
1722
+ out.push({ type: "identifier", value: id });
1723
+ continue;
1724
+ }
1725
+ if ("+-*/%(),".includes(c)) {
1726
+ out.push({ type: "operator", value: c });
1727
+ i++;
1728
+ continue;
1729
+ }
1730
+ throw new Error(`Unexpected character: ${c}`);
1731
+ }
1732
+ return out;
1733
+ };
1734
+ const parse = (tokens) => {
1735
+ let pos = 0;
1736
+ const cur = () => pos < tokens.length ? tokens[pos] : null;
1737
+ const nxt = () => {
1738
+ if (pos >= tokens.length)
1739
+ throw new Error("Unexpected end of input");
1740
+ return tokens[pos++];
1741
+ };
1742
+ const expect = (v) => {
1743
+ const t = cur();
1744
+ if (!t || t.value !== v) {
1745
+ throw new Error(`Expected "${v}" but got "${t ? t.value : "end of input"}"`);
1746
+ }
1747
+ nxt();
1748
+ };
1749
+ const parsePrimary = () => {
1750
+ const t = cur();
1751
+ if (!t)
1752
+ throw new Error("Unexpected end of input");
1753
+ if (t.type === "number") {
1754
+ nxt();
1755
+ return { type: "number", value: t.value, unit: t.unit };
1756
+ }
1757
+ if (t.type === "identifier") {
1758
+ nxt();
1759
+ if (cur() && cur().value === "(") {
1760
+ nxt();
1761
+ const args = [];
1762
+ if (cur() && cur().value !== ")") {
1763
+ args.push(parseAdd());
1764
+ while (cur() && cur().value === ",") {
1765
+ nxt();
1766
+ args.push(parseAdd());
1763
1767
  }
1764
1768
  }
1765
- tokens.push(num);
1766
- continue;
1769
+ expect(")");
1770
+ return { type: "call", func: t.value, args };
1767
1771
  }
1768
- if (char === "," || char === "/") {
1769
- tokens.push(char);
1770
- i++;
1771
- continue;
1772
+ return { type: "var", name: t.value };
1773
+ }
1774
+ if (t.value === "(") {
1775
+ nxt();
1776
+ const e2 = parseAdd();
1777
+ expect(")");
1778
+ return e2;
1779
+ }
1780
+ throw new Error(`Unexpected token: ${t.value}`);
1781
+ };
1782
+ const parseUnary = () => {
1783
+ if (cur() && (cur().value === "+" || cur().value === "-")) {
1784
+ const op = nxt().value;
1785
+ return { type: "unary", op, arg: parseUnary() };
1786
+ }
1787
+ return parsePrimary();
1788
+ };
1789
+ const parsePower = () => {
1790
+ let left = parseUnary();
1791
+ while (cur() && cur().value === "**") {
1792
+ const op = nxt().value;
1793
+ left = { type: "binary", op, left, right: parseUnary() };
1794
+ }
1795
+ return left;
1796
+ };
1797
+ const parseMul = () => {
1798
+ let left = parsePower();
1799
+ while (cur() && ["*", "/", "%"].includes(String(cur().value))) {
1800
+ const op = nxt().value;
1801
+ left = { type: "binary", op, left, right: parsePower() };
1802
+ }
1803
+ return left;
1804
+ };
1805
+ const parseAdd = () => {
1806
+ let left = parseMul();
1807
+ while (cur() && (cur().value === "+" || cur().value === "-")) {
1808
+ const op = nxt().value;
1809
+ left = { type: "binary", op, left, right: parseMul() };
1810
+ }
1811
+ return left;
1812
+ };
1813
+ const ast = parseAdd();
1814
+ if (pos < tokens.length) {
1815
+ throw new Error(`Extra tokens after expression: ${tokens.slice(pos).map((t) => t.value).join(" ")}`);
1816
+ }
1817
+ return ast;
1818
+ };
1819
+ const evaluate = (ast, env) => {
1820
+ switch (ast.type) {
1821
+ case "number":
1822
+ return { value: ast.value, unit: ast.unit };
1823
+ case "var": {
1824
+ const v = env[ast.name];
1825
+ if (v === void 0)
1826
+ throw new Error(`Unknown variable: ${ast.name}`);
1827
+ if (typeof v === "function") {
1828
+ throw new Error(`Expected variable but found function: ${ast.name}`);
1772
1829
  }
1773
- throw new Error(`Unexpected character: ${char}`);
1830
+ return { value: v, unit: UNIT_NUM };
1774
1831
  }
1775
- const parseValue = (token) => {
1776
- return token[token.length - 1] === "%" ? parseFloat(token) / 100 : parseFloat(token);
1777
- };
1778
- if (tokens.length >= 2 && tokens[1] === ",") {
1779
- const values = tokens.filter((t) => t !== ",");
1780
- if (values.length === 4 || values.length === 5) {
1781
- const [c2, m2, y2, k2, alpha2 = 1] = values.map(parseValue);
1782
- const red2 = 1 - Math.min(1, c2 * (1 - k2) + k2);
1783
- const green2 = 1 - Math.min(1, m2 * (1 - k2) + k2);
1784
- const blue2 = 1 - Math.min(1, y2 * (1 - k2) + k2);
1785
- return [red2 * 255, green2 * 255, blue2 * 255, alpha2];
1832
+ case "binary": {
1833
+ const L = evaluate(ast.left, env);
1834
+ const R = evaluate(ast.right, env);
1835
+ switch (ast.op) {
1836
+ case "+":
1837
+ case "-": {
1838
+ const val = ast.op === "+" ? L.value + R.value : L.value - R.value;
1839
+ const unitMask = L.unit | R.unit;
1840
+ if (L.unit === R.unit)
1841
+ return { value: val, unit: L.unit };
1842
+ if (unitMask === 5)
1843
+ return { value: val, unit: UNIT_ANG };
1844
+ throw new Error(`Cannot ${ast.op} mismatched units: ${getUnitName(L.unit)} and ${getUnitName(R.unit)}`);
1845
+ }
1846
+ case "*": {
1847
+ const val = L.value * R.value;
1848
+ if (L.unit === UNIT_NUM)
1849
+ return { value: val, unit: R.unit };
1850
+ if (R.unit === UNIT_NUM)
1851
+ return { value: val, unit: L.unit };
1852
+ throw new Error(`Cannot multiply units: ${getUnitName(L.unit)} and ${getUnitName(R.unit)}`);
1853
+ }
1854
+ case "/": {
1855
+ if (R.unit === UNIT_NUM)
1856
+ return { value: L.value / R.value, unit: L.unit };
1857
+ if (L.unit === R.unit)
1858
+ return { value: L.value / R.value, unit: UNIT_NUM };
1859
+ throw new Error(`Cannot divide units: ${getUnitName(L.unit)} by ${getUnitName(R.unit)}`);
1860
+ }
1861
+ case "%": {
1862
+ if (R.unit === UNIT_NUM)
1863
+ return { value: L.value % R.value, unit: L.unit };
1864
+ if (L.unit === R.unit)
1865
+ return { value: L.value % R.value, unit: L.unit };
1866
+ throw new Error(`Cannot modulo units: ${getUnitName(L.unit)} by ${getUnitName(R.unit)}`);
1867
+ }
1868
+ case "**": {
1869
+ if (R.unit !== UNIT_NUM)
1870
+ throw new Error(`Exponent must be a number, got ${getUnitName(R.unit)}`);
1871
+ return { value: L.value ** R.value, unit: L.unit };
1872
+ }
1873
+ default:
1874
+ throw new Error(`Unknown binary operator: ${ast.op}`);
1786
1875
  }
1787
- throw new Error("Invalid number of components for comma-separated device-cmyk");
1788
1876
  }
1789
- let idx = 0;
1790
- const components = [];
1791
- while (idx < tokens.length && components.length < 4 && tokens[idx] !== "/" && tokens[idx] !== ",") {
1792
- components.push(tokens[idx]);
1793
- idx++;
1877
+ case "unary": {
1878
+ const res = evaluate(ast.arg, env);
1879
+ switch (ast.op) {
1880
+ case "+":
1881
+ return { value: +res.value, unit: res.unit };
1882
+ case "-":
1883
+ return { value: -res.value, unit: res.unit };
1884
+ default:
1885
+ throw new Error(`Unknown unary operator: ${ast.op}`);
1886
+ }
1794
1887
  }
1795
- if (components.length !== 4) {
1796
- throw new Error("Invalid number of components for space-separated device-cmyk");
1888
+ case "call": {
1889
+ const fn = env[ast.func];
1890
+ if (typeof fn !== "function")
1891
+ throw new Error(`Unknown function: ${ast.func}`);
1892
+ const evaluatedArgs = ast.args.map((a) => evaluate(a, env));
1893
+ const numArgs = evaluatedArgs.map((a) => a.value);
1894
+ const val = fn(...numArgs);
1895
+ const preserveUnitFuncs = ["min", "max", "round", "ceil", "floor", "abs", "sign", "trunc"];
1896
+ if (preserveUnitFuncs.includes(ast.func)) {
1897
+ let combinedMask = 0;
1898
+ for (let i = 0; i < evaluatedArgs.length; i++) {
1899
+ combinedMask |= evaluatedArgs[i].unit;
1900
+ }
1901
+ if ((combinedMask & 6) === 6)
1902
+ throw new Error(`Function ${ast.func} cannot mix angle and percent`);
1903
+ if ((combinedMask & 3) === 3)
1904
+ throw new Error(`Function ${ast.func} cannot mix number and percent`);
1905
+ const finalUnit = combinedMask & UNIT_ANG ? UNIT_ANG : combinedMask & UNIT_PCT ? UNIT_PCT : UNIT_NUM;
1906
+ return { value: val, unit: finalUnit };
1907
+ }
1908
+ return { value: val, unit: UNIT_NUM };
1797
1909
  }
1798
- const [c, m, y, k] = components.map(parseValue);
1799
- let alpha = 1;
1800
- if (idx < tokens.length && tokens[idx] === "/") {
1801
- idx++;
1802
- if (idx >= tokens.length)
1803
- throw new Error("Missing alpha value");
1804
- alpha = parseValue(tokens[idx]);
1805
- idx++;
1910
+ default: {
1911
+ throw new Error(`Unknown AST node type: ${ast.type}`);
1912
+ }
1913
+ }
1914
+ };
1915
+ const calcEnv = Object.assign({}, BASE_MATH_ENV, base);
1916
+ return evaluate(parse(tokenize2(expr)), calcEnv).value;
1917
+ }
1918
+ function evaluateComponent(node, expectedType, base = {}) {
1919
+ if (!node)
1920
+ return NaN;
1921
+ const token = extractToken(node);
1922
+ return evaluateCSSValue(token, expectedType, base);
1923
+ }
1924
+ function evaluateCSSValue(tokenString, expectedType, base = {}) {
1925
+ const token = tokenString.trim();
1926
+ if (token === "none")
1927
+ return NaN;
1928
+ if (token in base)
1929
+ return base[token];
1930
+ const [tMin, tMax] = Array.isArray(expectedType) ? expectedType : expectedType === "percentage" ? [0, 100] : [0, 1];
1931
+ if (/^-?(?:\d+|\d*\.\d+)$/.test(token)) {
1932
+ return parseFloat(token);
1933
+ }
1934
+ if (token[token.length - 1] === "%" && /^-?(?:\d+|\d*\.\d+)%$/.test(token)) {
1935
+ return parsePercent(token, expectedType, tMin, tMax);
1936
+ }
1937
+ if (/^-?(?:\d+|\d*\.\d+)(deg|rad|grad|turn)$/.test(token)) {
1938
+ return parseHue(token);
1939
+ }
1940
+ if (token.slice(0, 5) === "calc(" && token[token.length - 1] === ")") {
1941
+ return parseCalcExpression(token.slice(5, -1).trim(), expectedType, base, tMin, tMax);
1942
+ }
1943
+ throw new Error(`Unable to parse component token: ${token}`);
1944
+ }
1945
+ function createLegacyColorParser(model, componentKeys) {
1946
+ return (node) => {
1947
+ const t = node.value;
1948
+ const config2 = colorModels[model].components;
1949
+ const c1 = evaluateComponent(t[2], config2[componentKeys[0]].value);
1950
+ const c2 = evaluateComponent(t[4], config2[componentKeys[1]].value);
1951
+ const c3 = evaluateComponent(t[6], config2[componentKeys[2]].value);
1952
+ let alpha = 1;
1953
+ if (t[7]?.type === "symbol" && t[7]?.value === ",")
1954
+ alpha = evaluateComponent(t[8], [0, 1]);
1955
+ return { model, coords: [c1, c2, c3, alpha] };
1956
+ };
1957
+ }
1958
+ function createModernColorParser(model, componentKeys) {
1959
+ return (node) => {
1960
+ const t = node.value;
1961
+ const config2 = colorModels[model].components;
1962
+ const c1 = evaluateComponent(t[2], config2[componentKeys[0]].value);
1963
+ const c2 = evaluateComponent(t[3], config2[componentKeys[1]].value);
1964
+ const c3 = evaluateComponent(t[4], config2[componentKeys[2]].value);
1965
+ let alpha = 1;
1966
+ if (t[5]?.type === "symbol" && t[5]?.value === "/")
1967
+ alpha = evaluateComponent(t[6], [0, 1]);
1968
+ return { model, coords: [c1, c2, c3, alpha] };
1969
+ };
1970
+ }
1971
+ function createRelativeColorParser(model, componentKeys) {
1972
+ return (node) => {
1973
+ const t = node.value;
1974
+ const config2 = colorModels[model].components;
1975
+ const originColorNode = t[3];
1976
+ const originParsed = parseNode(originColorNode);
1977
+ const originInstance = new Color(originParsed.model, originParsed.coords).in(model);
1978
+ const baseEnv = originInstance.toObject({ fit: "none", precision: null });
1979
+ const c1 = evaluateComponent(t[4], config2[componentKeys[0]].value, baseEnv);
1980
+ const c2 = evaluateComponent(t[5], config2[componentKeys[1]].value, baseEnv);
1981
+ const c3 = evaluateComponent(t[6], config2[componentKeys[2]].value, baseEnv);
1982
+ let alpha = 1;
1983
+ if (t[7]?.type === "symbol" && t[7]?.value === "/") {
1984
+ alpha = evaluateComponent(t[8], [0, 1], baseEnv);
1985
+ }
1986
+ return {
1987
+ model,
1988
+ coords: [c1, c2, c3, alpha]
1989
+ };
1990
+ };
1991
+ }
1992
+ function createRelativeColorFunctionParser(node) {
1993
+ const t = node.value;
1994
+ const { model, coords } = parseNode(t[3]);
1995
+ const spaceNode = t[4];
1996
+ const targetModel = Array.isArray(spaceNode.value) ? spaceNode.value[0]?.value : spaceNode.value;
1997
+ const originInstance = new Color(model, coords).in(targetModel);
1998
+ const baseEnv = originInstance.toObject({ fit: "none", precision: null });
1999
+ const c1 = evaluateComponent(t[5], [0, 1], baseEnv);
2000
+ const c2 = evaluateComponent(t[6], [0, 1], baseEnv);
2001
+ const c3 = evaluateComponent(t[7], [0, 1], baseEnv);
2002
+ let alpha = 1;
2003
+ if (t[8]?.type === "symbol" && t[8]?.value === "/") {
2004
+ alpha = evaluateComponent(t[9], [0, 1], baseEnv);
2005
+ }
2006
+ return {
2007
+ model: targetModel,
2008
+ coords: [c1, c2, c3, alpha]
2009
+ };
2010
+ }
2011
+ var validators = (() => {
2012
+ const NUMBER_REGEX = /^-?(?:\d+(?:\.\d+)?|\.\d+)$/;
2013
+ const PERCENT_REGEX = /^-?(?:\d+(?:\.\d+)?|\.\d+)%$/;
2014
+ const ANGLE_REGEX = /^-?(?:\d+(?:\.\d+)?|\.\d+)(?:deg|rad|turn|grad)$/;
2015
+ const RANGE_REGEX = /^\[([^,]+),([^\]]+)\]$/;
2016
+ const systemColorsSet = new Set(Object.keys(systemColors).map((key) => key.toLowerCase()));
2017
+ const rangeCache = /* @__PURE__ */ new Map();
2018
+ const getRange = (args) => {
2019
+ if (!args)
2020
+ return void 0;
2021
+ for (let i = 0; i < args.length; i++) {
2022
+ if (RANGE_REGEX.test(args[i]))
2023
+ return args[i];
2024
+ }
2025
+ return void 0;
2026
+ };
2027
+ const checkRange = (val, rangeStr) => {
2028
+ let bounds = rangeCache.get(rangeStr);
2029
+ if (!bounds) {
2030
+ const match = rangeStr.match(RANGE_REGEX);
2031
+ bounds = {
2032
+ min: match[1].trim() === "-INF" ? -Infinity : parseFloat(match[1]),
2033
+ max: match[2].trim() === "INF" || match[2].trim() === "+INF" ? Infinity : parseFloat(match[2])
2034
+ };
2035
+ rangeCache.set(rangeStr, bounds);
2036
+ }
2037
+ if (bounds.min !== -Infinity && val < bounds.min)
2038
+ return false;
2039
+ if (bounds.max !== Infinity && val > bounds.max)
2040
+ return false;
2041
+ return true;
2042
+ };
2043
+ return {
2044
+ "<named-color>": (str) => Object.hasOwn(namedColors, str),
2045
+ "<system-color>": (str) => systemColorsSet.has(str),
2046
+ "<hex-color>": (str) => {
2047
+ return str[0] === "#" && (str.length === 4 || str.length === 5 || str.length === 7 || str.length === 9) && /^[0-9a-fA-F]+$/.test(str.slice(1));
2048
+ },
2049
+ "<number>": (str, args) => {
2050
+ if (str.startsWith("calc(") && str.endsWith(")"))
2051
+ return true;
2052
+ if (!NUMBER_REGEX.test(str))
2053
+ return false;
2054
+ if (args !== void 0) {
2055
+ const rangeArg = getRange(args);
2056
+ if (rangeArg)
2057
+ return checkRange(parseFloat(str), rangeArg);
1806
2058
  }
1807
- if (idx < tokens.length && tokens[idx] === ",") {
1808
- idx++;
1809
- const fallbackStr = tokens.slice(idx).join(" ");
1810
- return Color.from(fallbackStr).in("rgb").toArray({ fit: "none", precision: null });
2059
+ return true;
2060
+ },
2061
+ "<percentage>": (str, args) => {
2062
+ if (str.startsWith("calc(") && str.endsWith(")"))
2063
+ return true;
2064
+ if (!PERCENT_REGEX.test(str))
2065
+ return false;
2066
+ if (args !== void 0) {
2067
+ const rangeArg = getRange(args);
2068
+ if (rangeArg)
2069
+ return checkRange(parseFloat(str), rangeArg);
1811
2070
  }
1812
- const red = 1 - Math.min(1, c * (1 - k) + k);
1813
- const green = 1 - Math.min(1, m * (1 - k) + k);
1814
- const blue = 1 - Math.min(1, y * (1 - k) + k);
1815
- return [red * 255, green * 255, blue * 255, alpha];
2071
+ return true;
1816
2072
  },
1817
- fromBridge: (coords) => coords,
1818
- format: ([red, green, blue, alpha = 1], options = {}) => {
1819
- const { legacy = false, precision = 3, fit: fitMethod = config.defaults.fit } = options;
1820
- const [fr, fg, fb] = fit([red, green, blue], "rgb", { method: fitMethod });
1821
- const r = fr / 255;
1822
- const g = fg / 255;
1823
- const b = fb / 255;
1824
- const k = 1 - Math.max(r, g, b);
1825
- const formatComponent = (value) => {
1826
- return Number(precision === null ? value : value.toFixed(precision)).toString();
1827
- };
1828
- const c = formatComponent(k === 1 ? 0 : (1 - r - k) / (1 - k));
1829
- const m = formatComponent(k === 1 ? 0 : (1 - g - k) / (1 - k));
1830
- const y = formatComponent(k === 1 ? 0 : (1 - b - k) / (1 - k));
1831
- const kFormatted = formatComponent(k);
1832
- const alphaFormatted = Number(alpha.toFixed(3)).toString();
1833
- if (legacy) {
1834
- return `device-cmyk(${c}, ${m}, ${y}, ${kFormatted}${alpha < 1 ? `, ${alphaFormatted}` : ""})`;
2073
+ "<angle>": (str, args) => {
2074
+ if (str.startsWith("calc(") && str.endsWith(")"))
2075
+ return true;
2076
+ if (!ANGLE_REGEX.test(str))
2077
+ return false;
2078
+ if (args !== void 0) {
2079
+ const rangeArg = getRange(args);
2080
+ if (rangeArg)
2081
+ return checkRange(parseFloat(str), rangeArg);
1835
2082
  }
1836
- const rgbPart = colorFunctions.rgb.format?.([fr, fg, fb, alpha], options);
1837
- return `device-cmyk(${c} ${m} ${y} ${kFormatted}${alpha < 1 ? ` / ${alphaFormatted}` : ""}, ${rgbPart})`;
2083
+ return true;
2084
+ }
2085
+ };
2086
+ })();
2087
+ var parsers = {
2088
+ "<color>": (node) => {
2089
+ const child = node.value[0];
2090
+ const { type, value } = child;
2091
+ if (type === "literal") {
2092
+ if (value === "currentcolor")
2093
+ return { model: "rgb", coords: [0, 0, 0, 1] };
2094
+ else
2095
+ throw new Error(`Unknown <color>: ${value}`);
1838
2096
  }
2097
+ return parseNode(child);
1839
2098
  },
1840
- "light-dark": {
1841
- isValid: (str) => str.slice(0, 11) === "light-dark(" && str[str.length - 1] === ")",
1842
- bridge: "rgb",
1843
- toBridge: (coords) => coords,
1844
- parse: (str) => {
1845
- const fnName = "light-dark";
1846
- const fnIndex = str.indexOf(fnName);
1847
- if (fnIndex === -1)
1848
- throw new Error("Not a <light-dark()> expression");
1849
- const { expression } = extractBalancedExpression(str, fnIndex + fnName.length);
1850
- if (!expression)
1851
- throw new Error("Malformed <light-dark()> expression");
1852
- const inner = expression.slice(1, -1).trim();
1853
- const parts = [];
1854
- let current = "";
1855
- let i = 0;
1856
- while (i < inner.length) {
1857
- const char = inner[i];
1858
- if (char === ",") {
1859
- parts.push(current.trim());
1860
- current = "";
2099
+ "<color-base>": (node) => {
2100
+ const child = node.value[0];
2101
+ const { type, value } = child;
2102
+ if (type === "literal") {
2103
+ if (value === "transparent")
2104
+ return { model: "rgb", coords: [0, 0, 0, 0] };
2105
+ else
2106
+ throw new Error(`Unknown <color-base>: ${value}`);
2107
+ }
2108
+ return parseNode(child);
2109
+ },
2110
+ "<named-color>": (node) => {
2111
+ const { value: name } = node;
2112
+ const rgb = namedColors[name];
2113
+ if (!rgb)
2114
+ throw new Error(`Unknown <named-color>: ${name}`);
2115
+ return { model: "rgb", coords: [...rgb, 1] };
2116
+ },
2117
+ "<system-color>": (node) => {
2118
+ const { value: name } = node;
2119
+ const index = config.theme === "light" ? 0 : 1;
2120
+ const key = Object.keys(systemColors).find((k) => k.toLowerCase() === name);
2121
+ const rgb = systemColors[key][index];
2122
+ if (!rgb)
2123
+ throw new Error(`Unknown <system-color>: ${name}`);
2124
+ return { model: "rgb", coords: [...rgb, 1] };
2125
+ },
2126
+ "<contrast-color()>": (node) => {
2127
+ const colorNode = node.value[2];
2128
+ const { model, coords } = parseNode(colorNode);
2129
+ const [, luminance] = new Color(model, coords).in("xyz-d65").coords;
2130
+ return { model: "rgb", coords: luminance > 0.5 ? [0, 0, 0, 1] : [255, 255, 255, 1] };
2131
+ },
2132
+ "<light-dark-color>": (node) => {
2133
+ const children = node.value;
2134
+ const lightNode = children[2];
2135
+ const darkNode = children[4];
2136
+ const { model, coords } = parseNode(config?.theme === "light" ? lightNode : darkNode);
2137
+ const { coords: rgbCoords } = new Color(model, coords).in("rgb");
2138
+ return { model: "rgb", coords: rgbCoords };
2139
+ },
2140
+ "<hex-color>": (node) => {
2141
+ const hex = node.value;
2142
+ const len = hex.length;
2143
+ let r = 0, g = 0, b = 0, a = 1;
2144
+ if (len === 4 || len === 5) {
2145
+ const rCh = hex.charCodeAt(1);
2146
+ const gCh = hex.charCodeAt(2);
2147
+ const bCh = hex.charCodeAt(3);
2148
+ const rVal = rCh > 64 ? (rCh & 7) + 9 : rCh & 15;
2149
+ const gVal = gCh > 64 ? (gCh & 7) + 9 : gCh & 15;
2150
+ const bVal = bCh > 64 ? (bCh & 7) + 9 : bCh & 15;
2151
+ r = rVal << 4 | rVal;
2152
+ g = gVal << 4 | gVal;
2153
+ b = bVal << 4 | bVal;
2154
+ if (len === 5) {
2155
+ const aCh = hex.charCodeAt(4);
2156
+ const aVal = aCh > 64 ? (aCh & 7) + 9 : aCh & 15;
2157
+ a = (aVal << 4 | aVal) / 255;
2158
+ }
2159
+ } else {
2160
+ const num = parseInt(hex.slice(1), 16);
2161
+ if (len === 7) {
2162
+ r = num >> 16 & 255;
2163
+ g = num >> 8 & 255;
2164
+ b = num & 255;
2165
+ } else if (len === 9) {
2166
+ r = num >> 24 & 255;
2167
+ g = num >> 16 & 255;
2168
+ b = num >> 8 & 255;
2169
+ a = (num & 255) / 255;
2170
+ }
2171
+ }
2172
+ return {
2173
+ model: "rgb",
2174
+ coords: [r, g, b, a]
2175
+ };
2176
+ },
2177
+ "<device-cmyk()>": (node) => parseNode(node.value[0]),
2178
+ "<legacy-device-cmyk-syntax>": (node) => {
2179
+ const t = node.value;
2180
+ const [c, m, y, k] = [t[2], t[4], t[6], t[8]].map(parseCmykComponent);
2181
+ return {
2182
+ model: "rgb",
2183
+ coords: [(1 - (c * (1 - k) + k)) * 255, (1 - (m * (1 - k) + k)) * 255, (1 - (y * (1 - k) + k)) * 255, 1]
2184
+ };
2185
+ },
2186
+ "<modern-device-cmyk-syntax>": (node) => {
2187
+ const t = node.value;
2188
+ const c = evaluateComponent(t[2], [0, 1]);
2189
+ const m = evaluateComponent(t[3], [0, 1]);
2190
+ const y = evaluateComponent(t[4], [0, 1]);
2191
+ const k = evaluateComponent(t[5], [0, 1]);
2192
+ const alphaNode = t.find((c2, idx) => idx === 7 && (c2.type === "<alpha-value>" || c2.type === "literal" && c2.value === "none"));
2193
+ const alpha = alphaNode ? evaluateComponent(alphaNode, [0, 1]) : 1;
2194
+ const cMath = normalize(c, [0, 1]);
2195
+ const mMath = normalize(m, [0, 1]);
2196
+ const yMath = normalize(y, [0, 1]);
2197
+ const kMath = normalize(k, [0, 1]);
2198
+ const alphaMath = isNaN(alpha) ? alpha : Math.max(0, Math.min(1, alpha));
2199
+ return {
2200
+ model: "rgb",
2201
+ coords: [
2202
+ (1 - (cMath * (1 - kMath) + kMath)) * 255,
2203
+ (1 - (mMath * (1 - kMath) + kMath)) * 255,
2204
+ (1 - (yMath * (1 - kMath) + kMath)) * 255,
2205
+ alphaMath
2206
+ ]
2207
+ };
2208
+ },
2209
+ "<color-mix()>": (node) => {
2210
+ let model = "oklab";
2211
+ let hue = "shorter";
2212
+ const children = node.value;
2213
+ let i = 2;
2214
+ if (children[i]?.type === "<color-interpolation-method>") {
2215
+ const methodParts = children[i].value;
2216
+ i++;
2217
+ if (methodParts[1]) {
2218
+ model = getLiteralValue(methodParts[1]);
2219
+ }
2220
+ if (methodParts[2] && methodParts[2].type !== "symbol") {
2221
+ hue = getLiteralValue(methodParts[2]);
2222
+ }
2223
+ }
2224
+ const items = [];
2225
+ while (i < children.length) {
2226
+ const current = children[i];
2227
+ if (current.type === "symbol" && (current.value === "," || current.value === ")")) {
2228
+ i++;
2229
+ continue;
2230
+ }
2231
+ let percentage = void 0;
2232
+ let colorNode;
2233
+ if (current.type === "<percentage>") {
2234
+ const tokenStr = String(current.value || "");
2235
+ percentage = evaluateCSSValue(tokenStr, "percentage");
2236
+ i++;
2237
+ colorNode = children[i];
2238
+ } else {
2239
+ colorNode = current;
2240
+ const nextNode = children[i + 1];
2241
+ if (nextNode?.type === "<percentage>") {
2242
+ const tokenStr = String(nextNode.value || "");
2243
+ percentage = evaluateCSSValue(tokenStr, "percentage");
1861
2244
  i++;
1862
- continue;
1863
2245
  }
1864
- if (char === "(" || /[a-zA-Z]/.test(char)) {
1865
- const { expression: expr, end } = extractBalancedExpression(inner, i);
1866
- if (expr) {
1867
- current += expr;
1868
- i = end;
1869
- continue;
2246
+ }
2247
+ const colorData = colorNode ? parseNode(colorNode) : null;
2248
+ if (colorData && colorData.coords) {
2249
+ i++;
2250
+ items.push({
2251
+ color: new Color(colorData.model, colorData.coords),
2252
+ percentage
2253
+ });
2254
+ continue;
2255
+ }
2256
+ i++;
2257
+ }
2258
+ if (items.length === 0) {
2259
+ throw new Error("color-mix() must contain at least one color component.");
2260
+ }
2261
+ const mixedResult = Color.mix(items, { in: model, hue });
2262
+ return {
2263
+ model: mixedResult.model,
2264
+ coords: mixedResult.coords
2265
+ };
2266
+ },
2267
+ "<color-function>": (node) => parseNode(node.value[0]),
2268
+ "<custom-color-function>": (node) => parseNode(node.value[0]),
2269
+ "<rgb()>": (node) => parseNode(node.value[0]),
2270
+ "<rgba()>": (node) => parseNode(node.value[0]),
2271
+ "<hsl()>": (node) => parseNode(node.value[0]),
2272
+ "<hsla()>": (node) => parseNode(node.value[0]),
2273
+ "<hwb()>": (node) => parseNode(node.value[0]),
2274
+ "<lab()>": (node) => parseNode(node.value[0]),
2275
+ "<lch()>": (node) => parseNode(node.value[0]),
2276
+ "<oklab()>": (node) => parseNode(node.value[0]),
2277
+ "<oklch()>": (node) => parseNode(node.value[0]),
2278
+ "<legacy-rgb-syntax>": createLegacyColorParser("rgb", ["r", "g", "b"]),
2279
+ "<legacy-rgba-syntax>": createLegacyColorParser("rgb", ["r", "g", "b"]),
2280
+ "<legacy-hsl-syntax>": createLegacyColorParser("hsl", ["h", "s", "l"]),
2281
+ "<legacy-hsla-syntax>": createLegacyColorParser("hsl", ["h", "s", "l"]),
2282
+ "<modern-rgb-syntax>": createModernColorParser("rgb", ["r", "g", "b"]),
2283
+ "<modern-rgba-syntax>": createModernColorParser("rgb", ["r", "g", "b"]),
2284
+ "<modern-hsl-syntax>": createModernColorParser("hsl", ["h", "s", "l"]),
2285
+ "<modern-hsla-syntax>": createModernColorParser("hsl", ["h", "s", "l"]),
2286
+ "<absolute-hwb-syntax>": createModernColorParser("hwb", ["h", "w", "b"]),
2287
+ "<absolute-lab-syntax>": createModernColorParser("lab", ["l", "a", "b"]),
2288
+ "<absolute-lch-syntax>": createModernColorParser("lch", ["l", "c", "h"]),
2289
+ "<absolute-oklab-syntax>": createModernColorParser("oklab", ["l", "a", "b"]),
2290
+ "<absolute-oklch-syntax>": createModernColorParser("oklch", ["l", "c", "h"]),
2291
+ "<relative-rgb-syntax>": createRelativeColorParser("rgb", ["r", "g", "b"]),
2292
+ "<relative-rgba-syntax>": createRelativeColorParser("rgb", ["r", "g", "b"]),
2293
+ "<relative-hsl-syntax>": createRelativeColorParser("hsl", ["h", "s", "l"]),
2294
+ "<relative-hsla-syntax>": createRelativeColorParser("hsl", ["h", "s", "l"]),
2295
+ "<relative-hwb-syntax>": createRelativeColorParser("hwb", ["h", "w", "b"]),
2296
+ "<relative-lab-syntax>": createRelativeColorParser("lab", ["l", "a", "b"]),
2297
+ "<relative-lch-syntax>": createRelativeColorParser("lch", ["l", "c", "h"]),
2298
+ "<relative-oklab-syntax>": createRelativeColorParser("oklab", ["l", "a", "b"]),
2299
+ "<relative-oklch-syntax>": createRelativeColorParser("oklch", ["l", "c", "h"]),
2300
+ "<alpha()>": (node) => {
2301
+ const children = node.value;
2302
+ const colorNode = children[3];
2303
+ const originColor = parseNode(colorNode);
2304
+ if (!originColor)
2305
+ throw new Error("Missing or invalid origin color in alpha() function");
2306
+ const originInstance = new Color(originColor.model, originColor.coords);
2307
+ const baseEnv = originInstance.toObject({ fit: "none", precision: null });
2308
+ const alphaNode = children.find((c, idx) => c.type === "<alpha-value>" || idx === 5 && c.type === "literal" && c.value === "none");
2309
+ const targetAlpha = alphaNode ? evaluateComponent(alphaNode, [0, 1], baseEnv) : originColor.coords[3];
2310
+ return {
2311
+ model: originColor.model,
2312
+ coords: [...originColor.coords.slice(0, 3), targetAlpha]
2313
+ };
2314
+ },
2315
+ "<color()>": (node) => parseNode(node.value[0]),
2316
+ "<relative-color-syntax>": (node) => parseNode(node.value[0]),
2317
+ "<absolute-color-syntax>": (node) => {
2318
+ const t = node.value;
2319
+ const spaceNode = t[2];
2320
+ const model = Array.isArray(spaceNode.value) ? spaceNode.value[0]?.value : spaceNode.value;
2321
+ const c1 = evaluateComponent(t[3], [0, 1]);
2322
+ const c2 = evaluateComponent(t[4], [0, 1]);
2323
+ const c3 = evaluateComponent(t[5], [0, 1]);
2324
+ const alphaNode = t.find((c, idx) => idx > 5 && (c.type === "<alpha-value>" || c.type === "literal" && c.value === "none"));
2325
+ const alpha = alphaNode ? evaluateComponent(alphaNode, [0, 1]) : 1;
2326
+ return {
2327
+ model,
2328
+ coords: [c1, c2, c3, alpha]
2329
+ };
2330
+ },
2331
+ "<relative-rgb-color-syntax>": createRelativeColorFunctionParser,
2332
+ "<relative-xyz-color-syntax>": createRelativeColorFunctionParser,
2333
+ "<relative-custom-color-syntax>": (node) => parseNode(node.value[0])
2334
+ };
2335
+ var formatters = (() => {
2336
+ const colorFunctionFormatters = (() => {
2337
+ const out = {};
2338
+ for (const model of Object.keys(colorModels)) {
2339
+ const { bridge, fromBridge, supportsLegacy, components, alphaVariant } = colorModels[model];
2340
+ out[model] = {
2341
+ bridge,
2342
+ fromBridge: (coords) => [...fromBridge(coords), coords[3] ?? 1],
2343
+ format: ([c1, c2, c3, a = 1], options = {}) => {
2344
+ const { legacy = false, fit: fitMethod = config.defaults.fit, precision, units = false } = options;
2345
+ const fitted = fit([c1, c2, c3], model, { method: fitMethod, precision });
2346
+ const formatted = [...fitted, a].map((c, index) => {
2347
+ const norm = normalize(c, Object.values(components)[index].value);
2348
+ if ((units || legacy) && components) {
2349
+ const def = Object.values(components).find((comp) => comp.index === index);
2350
+ if (def?.value === "percentage")
2351
+ return `${norm}%`;
2352
+ if (def?.value === "hue" && units)
2353
+ return `${norm}deg`;
2354
+ }
2355
+ return norm.toString();
2356
+ });
2357
+ const f = formatted.slice(0, 3);
2358
+ const alpha = formatted[3];
2359
+ if (model in colorSpaces)
2360
+ return `color(${model} ${f.join(" ")}${a !== 1 ? ` / ${alpha}` : ""})`;
2361
+ if (legacy && supportsLegacy) {
2362
+ return a === 1 ? `${model}(${f.join(", ")})` : `${alphaVariant || model}(${f.join(", ")}, ${alpha})`;
1870
2363
  }
2364
+ return `${model}(${f.join(" ")}${a !== 1 ? ` / ${alpha}` : ""})`;
1871
2365
  }
1872
- current += char;
1873
- i++;
2366
+ };
2367
+ }
2368
+ return out;
2369
+ })();
2370
+ return {
2371
+ ...colorFunctionFormatters,
2372
+ "hex-color": {
2373
+ bridge: "rgb",
2374
+ fromBridge: (coords) => coords,
2375
+ format: ([r, g, b, a = 1]) => {
2376
+ const toHex = (v) => v.toString(16).padStart(2, "0");
2377
+ const hex = [r, g, b].map((v) => toHex(Math.round(Math.max(0, Math.min(255, v))))).join("");
2378
+ return `#${hex}${a < 1 ? toHex(Math.round(a * 255)) : ""}`;
1874
2379
  }
1875
- parts.push(current.trim());
1876
- if (parts.length !== 2) {
1877
- throw new Error("Invalid light-dark format");
2380
+ },
2381
+ "named-color": {
2382
+ bridge: "rgb",
2383
+ fromBridge: (coords) => coords,
2384
+ format: (rgb) => {
2385
+ const [r, g, b] = rgb.map((v, i) => i < 3 ? Math.round(Math.min(255, Math.max(0, v))) : v);
2386
+ for (const [name, [nr, ng, nb]] of Object.entries(namedColors)) {
2387
+ if (r === nr && g === ng && b === nb)
2388
+ return name;
2389
+ }
1878
2390
  }
1879
- const [color1, color2] = parts;
1880
- const { theme } = config;
1881
- return Color.from(theme === "light" ? color1 : color2).toArray({ fit: "none", precision: null });
2391
+ },
2392
+ "device-cmyk": {
2393
+ bridge: "rgb",
2394
+ fromBridge: (coords) => coords,
2395
+ format: ([red, green, blue, alpha = 1], options = {}) => {
2396
+ const { legacy = false, precision = 3, fit: fitMethod = config.defaults.fit } = options;
2397
+ const [fr, fg, fb] = fit([red, green, blue], "rgb", { method: fitMethod });
2398
+ const r = fr / 255;
2399
+ const g = fg / 255;
2400
+ const b = fb / 255;
2401
+ const k = 1 - Math.max(r, g, b);
2402
+ const formatComponent = (value) => {
2403
+ return Number(precision === null ? value : value.toFixed(precision)).toString();
2404
+ };
2405
+ const c = formatComponent(k === 1 ? 0 : (1 - r - k) / (1 - k));
2406
+ const m = formatComponent(k === 1 ? 0 : (1 - g - k) / (1 - k));
2407
+ const y = formatComponent(k === 1 ? 0 : (1 - b - k) / (1 - k));
2408
+ const kFormatted = formatComponent(k);
2409
+ const alphaFormatted = Number(alpha.toFixed(3)).toString();
2410
+ if (legacy) {
2411
+ return `device-cmyk(${c}, ${m}, ${y}, ${kFormatted}${alpha < 1 ? `, ${alphaFormatted}` : ""})`;
2412
+ }
2413
+ const rgbPart = colorFunctionFormatters.rgb.format?.([fr, fg, fb, alpha], options);
2414
+ return `device-cmyk(${c} ${m} ${y} ${kFormatted}${alpha < 1 ? ` / ${alphaFormatted}` : ""}, ${rgbPart})`;
2415
+ }
2416
+ }
2417
+ };
2418
+ })();
2419
+ function parseNode(node) {
2420
+ const parser = parsers[node.type];
2421
+ if (!parser)
2422
+ throw new Error(`No parser for rule: ${node.type}`);
2423
+ return parser(node);
2424
+ }
2425
+ function isValid(input, rule) {
2426
+ const cleaned = input.replace(/[A-Z]/g, (c) => String.fromCharCode(c.charCodeAt(0) + 32));
2427
+ const tokens = tokenize(cleaned);
2428
+ const memo = /* @__PURE__ */ new Map();
2429
+ if (rule) {
2430
+ if (grammarMap[rule]) {
2431
+ const results = matchRule(rule, tokens, 0, grammarMap, memo, null);
2432
+ return results.some((result) => result.success && result.nextIndex === tokens.length);
2433
+ }
2434
+ if (validators[rule]) {
2435
+ const results = matchRule(rule, tokens, 0, grammarMap, memo, null);
2436
+ return results.some((result) => result.success && result.nextIndex === tokens.length);
2437
+ }
2438
+ try {
2439
+ const inlineNode = parseExpression(rule);
2440
+ const results = matchNode(inlineNode, tokens, 0, grammarMap, memo, null);
2441
+ return results.some((result) => result.success && result.nextIndex === tokens.length);
2442
+ } catch (error) {
2443
+ throw new Error(`Failed to resolve rule or parse inline expression: "${rule}"`, { cause: error });
1882
2444
  }
1883
2445
  }
1884
- };
2446
+ const allRules = /* @__PURE__ */ new Set([...Object.keys(grammarMap), ...Object.keys(validators)]);
2447
+ for (const ruleName of allRules) {
2448
+ const results = matchRule(ruleName, tokens, 0, grammarMap, memo, null);
2449
+ const isMatch = results.some((result) => result.success && result.nextIndex === tokens.length);
2450
+ if (isMatch)
2451
+ return true;
2452
+ }
2453
+ return false;
2454
+ }
1885
2455
 
1886
2456
  // dist/Color.js
1887
2457
  var Color = class _Color {
@@ -1899,59 +2469,26 @@ var Saturon = (() => {
1899
2469
  this.coords = c;
1900
2470
  }
1901
2471
  static from(color) {
1902
- const c = clean(color);
1903
- for (const type in colorTypes) {
1904
- const t = type;
1905
- const { parse, bridge, toBridge, isValid } = colorTypes[t];
1906
- if (!isValid(c))
1907
- continue;
1908
- const parsed = parse(c);
1909
- const coords = t in colorModels ? parsed : toBridge(parsed);
1910
- const model = t in colorModels ? t : bridge;
2472
+ const tokens = tokenize(color);
2473
+ const tree = validateTokens(tokens);
2474
+ try {
2475
+ const { model, coords } = parseNode(tree);
1911
2476
  return new _Color(model, coords);
2477
+ } catch (error) {
2478
+ throw new Error(`Failed to parse color string '${color}'`, { cause: error });
1912
2479
  }
1913
- throw new Error(`Unsupported or invalid color format: '${color}'.`);
1914
2480
  }
1915
2481
  /**
1916
- * Returns the detected color type, or `undefined` if unrecognized.
2482
+ * Validates a color string.
1917
2483
  *
1918
- * @param color - Color string to analyze.
1919
- * @param strict - Whether to validate full round-trip conversion.
2484
+ * @param color - Color string to check.
2485
+ * @param options - Validation options.
2486
+ * @returns `true` if valid, otherwise `false`.
1920
2487
  */
1921
- static type(color, strict = false) {
1922
- const c = clean(color);
1923
- for (const type in colorTypes) {
1924
- const t = type;
1925
- const { isValid, bridge, parse, toBridge } = colorTypes[t];
1926
- if (!isValid(c))
1927
- continue;
1928
- if (!strict)
1929
- return t;
1930
- try {
1931
- const parsed = parse(c);
1932
- const coords = t in colorModels ? parsed : toBridge(parsed);
1933
- const model = type in colorModels ? type : bridge;
1934
- return typeof new _Color(model, coords) === "object" ? t : void 0;
1935
- } catch {
1936
- return void 0;
1937
- }
1938
- }
1939
- return void 0;
1940
- }
1941
- static isValid(color, type) {
2488
+ static isValid(color, options = {}) {
2489
+ const { rule } = options;
1942
2490
  try {
1943
- if (type) {
1944
- const t = type?.trim().toLowerCase();
1945
- const c = clean(color);
1946
- const { isValid, bridge, parse, toBridge } = colorTypes[t];
1947
- if (!isValid(c))
1948
- return false;
1949
- const parsed = parse(c);
1950
- const coords = t in colorModels ? parsed : toBridge(parsed);
1951
- const model = t in colorModels ? t : bridge;
1952
- return !!new _Color(model, coords);
1953
- }
1954
- return !!_Color.from(color);
2491
+ return isValid(color, rule) && !!_Color.from(color);
1955
2492
  } catch {
1956
2493
  return false;
1957
2494
  }
@@ -1968,7 +2505,7 @@ var Saturon = (() => {
1968
2505
  const models = Object.keys(colorModels);
1969
2506
  const model = options.model ?? models[Math.floor(Math.random() * models.length)];
1970
2507
  const { components } = colorModels[model];
1971
- const valid = /* @__PURE__ */ new Set([...Object.keys(components), "alpha"]);
2508
+ const valid = new Set(Object.keys(components));
1972
2509
  for (const section of ["limits", "bias", "base", "deviation"]) {
1973
2510
  const record = options[section];
1974
2511
  if (!record)
@@ -1989,36 +2526,91 @@ var Saturon = (() => {
1989
2526
  const v = Math.random() || 1e-9;
1990
2527
  value = base + Math.sqrt(-2 * Math.log(u)) * Math.cos(2 * Math.PI * v) * dev;
1991
2528
  } else {
1992
- let [min, max] = comp.value === "angle" ? [0, 360] : comp.value === "percentage" ? [0, 100] : comp.value;
2529
+ let [min2, max2] = comp.value === "hue" ? [0, 360] : comp.value === "percentage" ? [0, 100] : comp.value;
1993
2530
  const limits = options.limits?.[name];
1994
2531
  if (limits) {
1995
2532
  const [lMin, lMax] = limits;
1996
- min = Math.max(min, lMin);
1997
- max = Math.min(max, lMax);
2533
+ min2 = Math.max(min2, lMin);
2534
+ max2 = Math.min(max2, lMax);
1998
2535
  }
1999
2536
  let r = Math.random();
2000
2537
  const biasFn = options.bias?.[name];
2001
2538
  if (biasFn)
2002
2539
  r = biasFn(r);
2003
- value = min + r * (max - min);
2540
+ value = min2 + r * (max2 - min2);
2004
2541
  }
2005
- if (comp.value === "angle")
2542
+ if (comp.value === "hue")
2006
2543
  value = (value % 360 + 360) % 360;
2007
2544
  else if (comp.value === "percentage")
2008
2545
  value = Math.min(100, Math.max(0, value));
2009
2546
  else if (Array.isArray(comp.value)) {
2010
- const [min, max] = comp.value;
2011
- value = Math.min(max, Math.max(min, value));
2547
+ const [min2, max2] = comp.value;
2548
+ value = Math.min(max2, Math.max(min2, value));
2012
2549
  } else
2013
2550
  throw new Error(`Invalid component value definition for "${name}".`);
2014
2551
  coords[comp.index] = value;
2015
2552
  }
2016
2553
  return new _Color(model, coords);
2017
2554
  }
2555
+ /**
2556
+ * Statically mixes multiple colors together by specified percentages.
2557
+ * Fully compliant with CSS Color Module Level 5 specification constraints.
2558
+ *
2559
+ * @param colors - Array of MixItems containing colors and optional percentages.
2560
+ * @param options - Options containing the interpolation space and hue policy.
2561
+ * @returns A new `Color` instance representing the mixed color.
2562
+ */
2563
+ static mix(colors, options) {
2564
+ const { in: model = "oklab", hue = "shorter" } = options;
2565
+ if (!colors || colors.length === 0) {
2566
+ throw new Error("At least one color must be provided in the colors array.");
2567
+ }
2568
+ if (colors.length === 1)
2569
+ return colors[0].color.in(model);
2570
+ const weights = colors.map((c) => c.percentage !== void 0 ? c.percentage / 100 : void 0);
2571
+ const definedSum = weights.reduce((sum, w) => sum + (w || 0), 0);
2572
+ const missingCount = weights.filter((w) => w === void 0).length;
2573
+ let alphaMult = 1;
2574
+ if (weights.length > 0 && weights.every((w) => w === 0)) {
2575
+ alphaMult = 0;
2576
+ for (let i = 0; i < weights.length; i++)
2577
+ weights[i] = 1 / weights.length;
2578
+ } else if (missingCount === 0 && definedSum < 1) {
2579
+ alphaMult = definedSum;
2580
+ }
2581
+ if (alphaMult > 0) {
2582
+ if (missingCount > 0) {
2583
+ const remaining = Math.max(0, 1 - definedSum);
2584
+ const share = remaining / missingCount;
2585
+ for (let i = 0; i < weights.length; i++) {
2586
+ if (weights[i] === void 0)
2587
+ weights[i] = share;
2588
+ }
2589
+ } else if (definedSum > 0 && definedSum !== 1) {
2590
+ for (let i = 0; i < weights.length; i++) {
2591
+ weights[i] = weights[i] / definedSum;
2592
+ }
2593
+ }
2594
+ }
2595
+ let accColor = colors[0].color.in(model);
2596
+ let accWeight = weights[0];
2597
+ for (let i = 1; i < colors.length; i++) {
2598
+ const currentWeight = weights[i];
2599
+ accWeight += currentWeight;
2600
+ const t = accWeight === 0 ? 0 : currentWeight / accWeight;
2601
+ accColor = mixTwo(accColor, colors[i].color.in(model), t, model, hue);
2602
+ }
2603
+ if (alphaMult !== 1) {
2604
+ const finalCoords = [...accColor.coords];
2605
+ finalCoords[3] = (finalCoords[3] ?? 1) * alphaMult;
2606
+ return new _Color(model, finalCoords);
2607
+ }
2608
+ return accColor;
2609
+ }
2018
2610
  to(type, options = {}) {
2019
2611
  const t = type.toLowerCase();
2020
2612
  const { legacy = false, fit: fit2 = config.defaults.fit, precision, units = false } = options;
2021
- const conv = colorTypes[t];
2613
+ const conv = formatters[t];
2022
2614
  if (!conv)
2023
2615
  throw new Error(`Unsupported color type: '${t}'.`);
2024
2616
  const { fromBridge, bridge, format } = conv;
@@ -2052,7 +2644,6 @@ var Saturon = (() => {
2052
2644
  cache.set("paths", paths);
2053
2645
  }
2054
2646
  const key = `${from}-${to}`;
2055
- const coords = this.coords.slice(0, 3);
2056
2647
  if (!paths.has(key)) {
2057
2648
  const queue = [from], parent = { [from]: null };
2058
2649
  for (let i = 0; i < queue.length; i++) {
@@ -2075,7 +2666,7 @@ var Saturon = (() => {
2075
2666
  }
2076
2667
  paths.set(key, path2);
2077
2668
  }
2078
- let value = [...coords];
2669
+ let value = [...this.coords.map((v) => isNaN(v) ? 0 : v)];
2079
2670
  const path = paths.get(key);
2080
2671
  for (let i = 0; i < path.length - 1; i++) {
2081
2672
  const a = path[i], b = path[i + 1];
@@ -2097,7 +2688,7 @@ var Saturon = (() => {
2097
2688
  * @returns The formatted color string.
2098
2689
  */
2099
2690
  toString(options = {}) {
2100
- const { format } = colorTypes[this.model];
2691
+ const { format } = formatters[this.model];
2101
2692
  const { legacy = false, fit: fit2 = config.defaults.fit, precision, units = false } = options;
2102
2693
  return format?.(this.coords, { legacy, fit: fit2, precision, units });
2103
2694
  }
@@ -2105,20 +2696,17 @@ var Saturon = (() => {
2105
2696
  * Returns the color as an object of component values.
2106
2697
  *
2107
2698
  * @param options - Optional retrieval options.
2108
- * @returns An object mapping each component (and alpha) to its numeric value.
2699
+ * @returns An object mapping each component to its numeric value.
2109
2700
  * @throws If the model has no defined components.
2110
2701
  */
2702
+ // eslint-disable-next-line no-unused-vars
2111
2703
  toObject(options = {}) {
2112
2704
  const coords = this.toArray(options);
2113
2705
  const { components } = colorModels[this.model];
2114
2706
  if (!components)
2115
2707
  throw new Error(`Model ${this.model} does not have defined components.`);
2116
- const fullComponents = {
2117
- ...components,
2118
- alpha: { index: 3, value: [0, 1], precision: 3 }
2119
- };
2120
2708
  const result = {};
2121
- for (const [name, { index }] of Object.entries(fullComponents))
2709
+ for (const [name, { index }] of Object.entries(components))
2122
2710
  result[name] = coords[index];
2123
2711
  return result;
2124
2712
  }
@@ -2126,7 +2714,7 @@ var Saturon = (() => {
2126
2714
  * Returns the color as an array of component values, optionally normalized and fitted.
2127
2715
  *
2128
2716
  * @param options - Conversion configuration.
2129
- * @returns An array of normalized color components and alpha.
2717
+ * @returns An array of normalized color components.
2130
2718
  * @throws If the model has no defined components.
2131
2719
  */
2132
2720
  toArray(options = {}) {
@@ -2135,30 +2723,15 @@ var Saturon = (() => {
2135
2723
  const { components } = colorModels[model];
2136
2724
  if (!components)
2137
2725
  throw new Error(`Model ${model} does not have defined components.`);
2138
- const defs = {
2139
- ...components,
2140
- alpha: { index: 3, value: [0, 1], precision: 3 }
2141
- };
2142
- const normalize = (c, i) => {
2143
- const v = Object.values(defs)[i]?.value;
2144
- const [min, max] = Array.isArray(v) ? v : v === "angle" ? [0, 360] : [0, 100];
2145
- if (Number.isNaN(c))
2146
- return 0;
2147
- if (c === Infinity)
2148
- return max;
2149
- if (c === -Infinity)
2150
- return min;
2151
- return typeof c === "number" ? c : 0;
2152
- };
2153
- const norm = coords.slice(0, 3).map(normalize);
2726
+ const norm = coords.slice(0, 3).map((c, i) => normalize(c, Object.values(components)[i].value));
2154
2727
  const fitted = fit(norm, model, {
2155
2728
  method,
2156
2729
  precision
2157
2730
  });
2158
- return [...fitted.slice(0, 3), coords[3]];
2731
+ return [...fitted, coords[3]];
2159
2732
  }
2160
2733
  /**
2161
- * Returns a new `Color` instance with updated or replaced component values.
2734
+ * Creates a new Color instance with modified component values.
2162
2735
  *
2163
2736
  * This method supports several flexible update styles:
2164
2737
  *
@@ -2197,143 +2770,58 @@ var Saturon = (() => {
2197
2770
  * color.with(({ r, g, b }) => [r * 0.5, g * 0.5, b * 0.5, 1]);
2198
2771
  * ```
2199
2772
  *
2200
- * @param values - Either:
2201
- * - a partial object of component values,
2202
- * - an updater function returning an object or array,
2203
- * - or an array of new coordinates.
2204
- * @returns A new `Color` instance with updated values.
2205
- * @throws If the color model has no defined components.
2773
+ * @template M - The color model type
2774
+ * @param values - The new component values to apply. Can be:
2775
+ * - A partial object mapping component names to numbers or update functions
2776
+ * - A function that receives current components and returns partial updates or an array of values
2777
+ * - An array of new values corresponding to component indices
2778
+ * @param normalized - Whether to normalize component values to their valid ranges. Defaults to `true`.
2779
+ * When `false`, values are not clamped or validated against their ranges.
2780
+ * @returns A new Color instance with the updated component values
2206
2781
  */
2207
2782
  /* eslint-disable no-unused-vars */
2208
2783
  with(values) {
2209
2784
  const { model } = this;
2210
- const coords = this.toArray({ fit: "none", precision: null });
2785
+ const coords = this.coords.slice();
2211
2786
  const { components } = colorModels[model];
2212
- if (!components)
2213
- throw new Error(`Model ${model} does not have defined components.`);
2214
- const defs = {
2215
- ...components,
2216
- alpha: { index: 3, value: [0, 1], precision: 3 }
2217
- };
2218
- const names = Object.keys(defs);
2219
- let newValues;
2220
- if (typeof values === "function") {
2221
- const result = values(Object.fromEntries(names.map((c) => [c, coords[defs[c].index]])));
2222
- newValues = result;
2223
- } else {
2224
- newValues = values;
2225
- }
2787
+ const names = Object.keys(components);
2788
+ const componentDefs = Object.values(components);
2789
+ const newValues = typeof values === "function" ? values(Object.fromEntries(names.map((k) => {
2790
+ const def = components[k];
2791
+ return [k, normalize(coords[def.index], def.value)];
2792
+ }))) : values;
2226
2793
  if (Array.isArray(newValues)) {
2227
2794
  const adjusted = coords.map((curr, i) => {
2228
2795
  const incoming = newValues[i];
2229
- const { value } = Object.values(defs).find((d) => d.index === i);
2230
2796
  if (typeof incoming !== "number")
2231
2797
  return curr;
2232
- const [min, max] = Array.isArray(value) ? value : value === "angle" ? [0, 360] : [0, 100];
2233
- if (Number.isNaN(incoming))
2234
- return 0;
2235
- if (incoming === Infinity)
2236
- return max;
2237
- if (incoming === -Infinity)
2238
- return min;
2239
- return incoming;
2798
+ const def = componentDefs.find((d) => d.index === i);
2799
+ return normalize(incoming, def.value);
2240
2800
  });
2241
- return new _Color(model, [...adjusted.slice(0, 3), coords[3]]);
2801
+ return new _Color(model, [...adjusted.slice(0, 3), coords[3] ?? 1]);
2242
2802
  }
2243
2803
  const next = [...coords];
2244
2804
  for (const name of names) {
2245
2805
  if (!(name in newValues))
2246
2806
  continue;
2247
- const { index, value } = defs[name];
2248
- const current = coords[index];
2807
+ const { index, value } = components[name];
2249
2808
  const raw = newValues[name];
2250
- let val = typeof raw === "function" ? raw(current) : raw;
2251
- if (typeof val === "number") {
2252
- const [min, max] = Array.isArray(value) ? value : value === "angle" ? [0, 360] : [0, 100];
2253
- if (Number.isNaN(val))
2254
- val = 0;
2255
- else if (val === Infinity)
2256
- val = max;
2257
- else if (val === -Infinity)
2258
- val = min;
2259
- }
2809
+ const prev = normalize(coords[index], value);
2810
+ const val = typeof raw === "function" ? normalize(raw(prev), value) : typeof raw === "number" ? normalize(raw, value) : coords[index];
2260
2811
  next[index] = val;
2261
2812
  }
2262
2813
  return new _Color(model, [...next.slice(0, 3), next[3] ?? coords[3]]);
2263
2814
  }
2264
2815
  /**
2265
- * Mixes this color with another by a specified amount.
2816
+ * Creates a new Color instance with values fitted to the color model's gamut.
2266
2817
  *
2267
- * @param other - The color to mix with. Can be a string or a `Color` instance.
2268
- * @param options - Options for how the colors are mixed.
2269
- * @returns A new `Color` instance representing the mixed color.
2270
- * @throws If the color model does not have defined components.
2818
+ * @param options - Configuration options for fitting
2819
+ * @returns A new Color instance with fitted values
2271
2820
  */
2272
- mix(other, options = {}) {
2273
- const { model } = this;
2274
- const coords = this.toArray({ fit: "none", precision: null });
2275
- const { components } = colorModels[model];
2276
- if (!components)
2277
- throw new Error(`Model ${model} does not have defined components.`);
2278
- const { hue = "shorter", amount = 0.5, easing = "linear", gamma = 1 } = options;
2279
- const defs = {
2280
- ...components,
2281
- alpha: { index: 3, value: [0, 1], precision: 3 }
2282
- };
2283
- const hueDelta = (a, b) => {
2284
- const d = ((b - a) % 360 + 360) % 360;
2285
- return d > 180 ? d - 360 : d;
2286
- };
2287
- const hueDeltaLong = (a, b) => hueDelta(a, b) >= 0 ? hueDelta(a, b) - 360 : hueDelta(a, b) + 360;
2288
- const interpolateHue = (a, b, t2, method) => {
2289
- const wrapped = (v) => (v % 360 + 360) % 360;
2290
- switch (method) {
2291
- case "shorter":
2292
- return wrapped(a + t2 * hueDelta(a, b));
2293
- case "longer":
2294
- return wrapped(a + t2 * hueDeltaLong(a, b));
2295
- case "increasing":
2296
- return wrapped(a * (1 - t2) + (b < a ? b + 360 : b) * t2);
2297
- case "decreasing":
2298
- return wrapped(a * (1 - t2) + (b > a ? b - 360 : b) * t2);
2299
- default:
2300
- throw new Error(`Invalid hue interpolation method: ${method}`);
2301
- }
2302
- };
2303
- const t = Math.max(0, Math.min(1, amount));
2304
- const ease = typeof easing === "function" ? easing : EASINGS[easing];
2305
- const tt = Math.pow(ease(t), 1 / gamma);
2306
- const otherColor = typeof other === "string" ? _Color.from(other) : other;
2307
- const thisCoords = coords.slice(0, 3);
2308
- const otherCoords = otherColor.in(model).toArray({ fit: "none", precision: null }).slice(0, 3);
2309
- const thisAlpha = coords[3];
2310
- const otherAlpha = otherColor.coords[3];
2311
- const hueIndex = Object.entries(defs).find(([k]) => k === "h")?.[1].index;
2312
- if (t === 0)
2313
- return new _Color(model, [...thisCoords, thisAlpha]);
2314
- if (t === 1)
2315
- return new _Color(model, [...otherCoords, otherAlpha]);
2316
- if (thisAlpha < 1 || otherAlpha < 1) {
2317
- const premixed = thisCoords.map((start, i) => {
2318
- const end = otherCoords[i];
2319
- if (i === hueIndex)
2320
- return interpolateHue(start, end, tt, hue);
2321
- const a = start * thisAlpha;
2322
- const b = end * otherAlpha;
2323
- return a * (1 - tt) + b * tt;
2324
- });
2325
- const mixedAlpha = thisAlpha * (1 - tt) + otherAlpha * tt;
2326
- const mixed = mixedAlpha > 0 ? premixed.map((c, i) => i === hueIndex ? c : c / mixedAlpha) : thisCoords.map((_, i) => i === hueIndex ? premixed[i] : 0);
2327
- return new _Color(model, [...mixed, mixedAlpha]);
2328
- }
2329
- const mixedCoords = thisCoords.map((start, i) => {
2330
- const entry = Object.values(defs).find((d) => d.index === i);
2331
- if (!entry)
2332
- return start;
2333
- const end = otherCoords[i];
2334
- return entry.value === "angle" ? interpolateHue(start, end, tt, hue) : start + (end - start) * tt;
2335
- });
2336
- return new _Color(model, [...mixedCoords, 1]);
2821
+ fit(options = {}) {
2822
+ const { fit: method = config.defaults.fit, precision } = options;
2823
+ const fitted = this.toArray({ fit: method, precision });
2824
+ return new _Color(this.model, fitted);
2337
2825
  }
2338
2826
  /**
2339
2827
  * Fits this color within the specified gamut using a given method.
@@ -2353,7 +2841,7 @@ var Saturon = (() => {
2353
2841
  /**
2354
2842
  * Calculates the WCAG 2.1 contrast ratio between this color and another.
2355
2843
  *
2356
- * @param other - The comparison color (instance or string).
2844
+ * @param other - The comparison Color instance.
2357
2845
  * @returns Contrast ratio from 1 to 21.
2358
2846
  *
2359
2847
  * @remarks
@@ -2361,15 +2849,14 @@ var Saturon = (() => {
2361
2849
  * - For perceptual accuracy, consider using APCA instead.
2362
2850
  */
2363
2851
  contrast(other) {
2364
- const o = typeof other === "string" ? _Color.from(other) : other;
2365
2852
  const [, L1] = this.in("xyz-d65").toArray({ fit: "none", precision: null });
2366
- const [, L2] = o.in("xyz-d65").toArray({ fit: "none", precision: null });
2853
+ const [, L2] = other.in("xyz-d65").toArray({ fit: "none", precision: null });
2367
2854
  return (Math.max(L1, L2) + 0.05) / (Math.min(L1, L2) + 0.05);
2368
2855
  }
2369
2856
  /**
2370
2857
  * Calculates the color difference (ΔEOK) between the current color and another color using the OKLAB color space.
2371
2858
  *
2372
- * @param other - The other color to compare against (as a Color instance or string).
2859
+ * @param other - The other Color instance to compare against.
2373
2860
  * @returns A number in range (0-1) (smaller indicates more similar colors).
2374
2861
  *
2375
2862
  * @remarks
@@ -2380,7 +2867,7 @@ var Saturon = (() => {
2380
2867
  deltaEOK(other) {
2381
2868
  const coordsOptions = { fit: "none", precision: null };
2382
2869
  const [L1, a1, b1] = this.in("oklab").toArray(coordsOptions);
2383
- const [L2, a2, b2] = (typeof other === "string" ? _Color.from(other) : other).in("oklab").toArray(coordsOptions);
2870
+ const [L2, a2, b2] = other.in("oklab").toArray(coordsOptions);
2384
2871
  const \u0394L = L1 - L2;
2385
2872
  const \u0394A = a1 - a2;
2386
2873
  const \u0394B = b1 - b2;
@@ -2391,13 +2878,13 @@ var Saturon = (() => {
2391
2878
  * Calculates the color difference (ΔE) between two colors using the CIE76 formula.
2392
2879
  * This is a simple Euclidean distance in LAB color space.
2393
2880
  *
2394
- * @param other - The other color to compare against (as a Color instance or string).
2881
+ * @param other - The other Color instance to compare against.
2395
2882
  * @returns A number in range (0-1) (smaller indicates more similar colors).
2396
2883
  */
2397
2884
  deltaE76(other) {
2398
2885
  const coordsOptions = { fit: "none", precision: null };
2399
2886
  const [L1, a1, b1] = this.in("lab").toArray(coordsOptions);
2400
- const [L2, a2, b2] = (typeof other === "string" ? _Color.from(other) : other).in("lab").toArray(coordsOptions);
2887
+ const [L2, a2, b2] = other.in("lab").toArray(coordsOptions);
2401
2888
  const \u0394L = L1 - L2;
2402
2889
  const \u0394A = a1 - a2;
2403
2890
  const \u0394B = b1 - b2;
@@ -2407,13 +2894,13 @@ var Saturon = (() => {
2407
2894
  * Calculates the color difference (ΔE) between two colors using the CIE94 formula.
2408
2895
  * This method improves perceptual accuracy over CIE76 by applying weighting factors.
2409
2896
  *
2410
- * @param other - The other color to compare against (as a Color instance or string).
2897
+ * @param other - The other Color instance to compare against.
2411
2898
  * @returns A number in range (0-1) (smaller indicates more similar colors).
2412
2899
  */
2413
2900
  deltaE94(other) {
2414
2901
  const coordsOptions = { fit: "none", precision: null };
2415
2902
  const [L1, a1, b1] = this.in("lab").toArray(coordsOptions);
2416
- const [L2, a2, b2] = (typeof other === "string" ? _Color.from(other) : other).in("lab").toArray(coordsOptions);
2903
+ const [L2, a2, b2] = other.in("lab").toArray(coordsOptions);
2417
2904
  const \u0394L = L1 - L2;
2418
2905
  const \u0394A = a1 - a2;
2419
2906
  const \u0394B = b1 - b2;
@@ -2431,13 +2918,13 @@ var Saturon = (() => {
2431
2918
  * Calculates the color difference (ΔE) between two colors using the CIEDE2000 formula.
2432
2919
  * This is the most perceptually accurate method, accounting for interactions between hue, chroma, and lightness.
2433
2920
  *
2434
- * @param other - The other color to compare against (as a Color instance or string).
2921
+ * @param other - The other Color instance to compare against.
2435
2922
  * @returns A number in range (0-1) (smaller indicates more similar colors).
2436
2923
  */
2437
2924
  deltaE2000(other) {
2438
2925
  const coordsOptions = { fit: "none", precision: null };
2439
2926
  const [L1, a1, b1] = this.in("lab").toArray(coordsOptions);
2440
- const [L2, a2, b2] = (typeof other === "string" ? _Color.from(other) : other).in("lab").toArray(coordsOptions);
2927
+ const [L2, a2, b2] = other.in("lab").toArray(coordsOptions);
2441
2928
  const \u03C0 = Math.PI, d2r = \u03C0 / 180, r2d = 180 / \u03C0;
2442
2929
  const C1 = Math.sqrt(a1 ** 2 + b1 ** 2);
2443
2930
  const C2 = Math.sqrt(a2 ** 2 + b2 ** 2);
@@ -2475,7 +2962,7 @@ var Saturon = (() => {
2475
2962
  const Cdash = (Cdash1 + Cdash2) / 2;
2476
2963
  const Cdash7 = Math.pow(Cdash, 7);
2477
2964
  const hsum = h1 + h2;
2478
- let hdash = 0;
2965
+ let hdash;
2479
2966
  if (Cdash1 === 0 && Cdash2 === 0) {
2480
2967
  hdash = hsum;
2481
2968
  } else if (habs <= 180) {
@@ -2506,8 +2993,8 @@ var Saturon = (() => {
2506
2993
  /**
2507
2994
  * Checks numeric equality with another color within a tolerance.
2508
2995
  *
2509
- * @param other - Color or string to compare.
2510
- * @param epsilon - Allowed floating-point difference (default: 1e-5).
2996
+ * @param other - Color instnace to compare.
2997
+ * @param epsilon - Allowed floating-point difference (defaults to the value of `EPSILON` in `"saturon/math"`).
2511
2998
  * @returns `true` if equal within tolerance.
2512
2999
  *
2513
3000
  * @remarks
@@ -2521,22 +3008,23 @@ var Saturon = (() => {
2521
3008
  * - {@link deltaE94} (weighted improvements over LAB)
2522
3009
  * - {@link deltaE2000} (most accurate, accounts for perceptual interactions)
2523
3010
  */
2524
- equals(other, epsilon = 1e-5) {
2525
- const o = typeof other === "string" ? _Color.from(other) : other;
2526
- if (o.model === this.model)
2527
- return this.coords.every((v, i) => Math.abs(v - o.coords[i]) <= epsilon);
3011
+ equals(other, epsilon = EPSILON) {
3012
+ const thisCoords = this.toArray({ fit: "none", precision: null });
3013
+ const otherCoords = other.toArray({ fit: "none", precision: null });
3014
+ if (other.model === this.model)
3015
+ return thisCoords.every((v, i) => Math.abs(v - otherCoords[i]) <= epsilon);
2528
3016
  const a = this.in("xyz-d65").toArray({ fit: "none", precision: null });
2529
- const b = o.in("xyz-d65").toArray({ fit: "none", precision: null });
3017
+ const b = other.in("xyz-d65").toArray({ fit: "none", precision: null });
2530
3018
  return a.every((v, i) => Math.abs(v - b[i]) <= epsilon);
2531
3019
  }
2532
3020
  /**
2533
3021
  * Determines whether this color lies within a given gamut.
2534
3022
  *
2535
3023
  * @param gamut - Target color space.
2536
- * @param epsilon - Floating-point tolerance (default: 1e-5).
3024
+ * @param epsilon - Floating-point tolerance (defaults to the value of `EPSILON` in `"saturon/math"`).
2537
3025
  * @returns `true` if inside gamut, else `false`.
2538
3026
  */
2539
- inGamut(gamut, epsilon = 1e-5) {
3027
+ inGamut(gamut, epsilon = EPSILON) {
2540
3028
  const g = gamut.trim().toLowerCase();
2541
3029
  if (!(g in colorSpaces))
2542
3030
  throw new Error(`Unsupported color gamut: '${g}'.`);
@@ -2546,8 +3034,8 @@ var Saturon = (() => {
2546
3034
  const coords = this.in(g).toArray({ fit: "none", precision: null });
2547
3035
  return Object.values(components).every(({ index, value }) => {
2548
3036
  const v = coords[index];
2549
- const [min, max] = Array.isArray(value) ? value : value === "angle" ? [0, 360] : [0, 100];
2550
- return v >= min - epsilon && v <= max + epsilon;
3037
+ const [min2, max2] = Array.isArray(value) ? value : value === "hue" ? [0, 360] : [0, 100];
3038
+ return v >= min2 - epsilon && v <= max2 + epsilon;
2551
3039
  });
2552
3040
  }
2553
3041
  };