saturon 0.2.3 → 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") {
146
+ else if (method === "clip") {
168
147
  clipped = coords.map((v, i) => {
169
- const prop = componentProps[i];
148
+ const prop = defs[i];
170
149
  if (!prop)
171
150
  throw new Error(`Missing component properties for index ${i}.`);
172
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];
@@ -186,618 +165,21 @@ var Saturon = (() => {
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 parseHue = (token2) => {
208
- const value2 = parseFloat(token2);
209
- if (isNaN(value2))
210
- throw new Error(`Invalid hue 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 Infinity;
404
- if (inner === "-infinity")
405
- return -Infinity;
406
- if (inner === "NaN")
407
- return NaN;
408
- inner = inner.replace(/(\d+(\.\d+)?)%/g, (m) => {
409
- if (relative === true)
410
- throw new Error("<hue> 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("<hue> and <percentage> values are converted to <number> in relative syntax.");
417
- return String(parseHue(`${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 evaluateHue = () => {
458
- if (/^-?(?:\d+|\d*\.\d+)(?:deg|rad|grad|turn)$/.test(token)) {
459
- return parseHue(token);
460
- }
461
- if (/^-?(?:\d+|\d*\.\d+)$/.test(token)) {
462
- return parseFloat(token);
463
- }
464
- const [min2, max2] = [0, 360];
465
- if (token.slice(0, 5) === "calc(") {
466
- return parseCalc(token, min2, max2);
467
- }
468
- throw new Error(`Invalid hue value: '${token}'. Must be a number, or a number with a unit (deg, rad, grad, turn).`);
469
- };
470
- const evaluatePercent = () => {
471
- if (/^-?(?:\d+|\d*\.\d+)$/.test(token)) {
472
- if (commaSeparated && supportsLegacy === true) {
473
- throw new Error("The legacy color syntax does not allow numbers for <percentage> components.");
474
- }
475
- return parseFloat(token);
476
- }
477
- const [min2, max2] = [0, 100];
478
- if (token[token.length - 1] === "%") {
479
- return parsePercent(token, min2, max2);
480
- }
481
- if (token.slice(0, 5) === "calc(") {
482
- return parseCalc(token, min2, max2);
483
- }
484
- throw new Error(`Invalid percentage value: '${token}'. Must be a percentage or a number.`);
485
- };
486
- const evaluateNumber = () => {
487
- if (/^-?(?:\d+|\d*\.\d+)$/.test(token)) {
488
- return parseFloat(token);
489
- }
490
- const [min2, max2] = value;
491
- if (token[token.length - 1] === "%") {
492
- return parsePercent(token, min2, max2);
493
- }
494
- if (token.slice(0, 5) === "calc(") {
495
- return parseCalc(token, min2, max2);
496
- }
497
- throw new Error(`Invalid number value: '${token}'. Must be a number${relative === false ? " or a percentage" : ""}.`);
498
- };
499
- if (token === "none")
500
- return NaN;
501
- if (token in base)
502
- return base[token];
503
- if (value === "hue")
504
- return evaluateHue();
505
- if (value === "percentage")
506
- return evaluatePercent();
507
- if (Array.isArray(value))
508
- return evaluateNumber();
509
- throw new Error(`Unable to parse component token: ${token}`);
510
- };
511
- const parseAST = (ast) => {
512
- const { fn, space, fromOrigin, c1, c2, c3, alpha, commaSeparated } = ast;
513
- const { components: components2, supportsLegacy: supportsLegacy2 } = converter;
514
- components2.alpha = { index: 3, value: [0, 1], precision: 3 };
515
- if (commaSeparated && supportsLegacy2 !== true) {
516
- throw new Error(`<${fn}()> does not support comma-separated syntax.`);
517
- }
518
- const sorted = Object.entries(components2).sort((a, b) => a[1].index - b[1].index);
519
- if (fromOrigin) {
520
- let colorSpace;
521
- if (fn === "color") {
522
- colorSpace = space;
523
- } else if (fn in colorModels) {
524
- colorSpace = fn;
525
- } else {
526
- for (const model in colorModels) {
527
- if (colorModels[model].alphaVariant === fn) {
528
- colorSpace = model;
529
- break;
530
- }
531
- }
532
- }
533
- const originComponents = Color.from(fromOrigin).in(colorSpace).toObject({ fit: "none", precision: null });
534
- const evaluatedComponents = [c1, c2, c3, alpha].map((token, i) => {
535
- const [, meta] = sorted[i];
536
- return evaluateComponent(token, meta.value, originComponents, commaSeparated, true);
537
- });
538
- return evaluatedComponents.slice(0, 4);
539
- } else {
540
- const result = [];
541
- const percentFlags = [];
542
- const tokens = [c1, c2, c3, alpha];
543
- for (let i = 0; i < sorted.length; i++) {
544
- const [, meta] = sorted[i];
545
- const token = tokens[i];
546
- if (commaSeparated && token === "none") {
547
- throw new Error(`${fn}() cannot use "none" in comma-separated syntax.`);
548
- }
549
- if (meta.index !== 3 && meta.value !== "hue" && meta.value !== "percentage" && token.slice(0, 5) !== "calc(") {
550
- percentFlags.push(token.trim()[token.length - 1] === "%");
551
- }
552
- if (token) {
553
- const value = evaluateComponent(token, meta.value, {}, commaSeparated);
554
- result[meta.index] = value;
555
- }
556
- }
557
- if (commaSeparated && percentFlags.length > 1) {
558
- const allPercent = percentFlags.every(Boolean);
559
- const nonePercent = percentFlags.every((f) => !f);
560
- if (!allPercent && !nonePercent) {
561
- throw new Error(`${fn}()'s <number> components must all be numbers or all percentages.`);
562
- }
563
- }
564
- return result.slice(0, 4);
565
- }
566
- };
567
- const getAST = (tokens) => {
568
- const getAlpha = (index, separator = "/") => {
569
- if (tokens[index] !== void 0) {
570
- if (tokens[index] === separator) {
571
- return { value: tokens[index + 1], hasAlpha: true };
572
- }
573
- throw new Error("Invalid alpha separator");
574
- }
575
- return { value: "1", hasAlpha: false };
576
- };
577
- let fn, space, fromOrigin, c1, c2, c3, alpha, commaSeparated = false, expectedLength;
578
- if (tokens[0] === "color") {
579
- fn = "color";
580
- if (tokens[1] === "from") {
581
- space = tokens[3];
582
- fromOrigin = tokens[2];
583
- c1 = tokens[4];
584
- c2 = tokens[5];
585
- c3 = tokens[6];
586
- const { value, hasAlpha } = getAlpha(7);
587
- expectedLength = hasAlpha ? 9 : 7;
588
- alpha = value;
589
- } else {
590
- space = tokens[1];
591
- fromOrigin = null;
592
- c1 = tokens[2];
593
- c2 = tokens[3];
594
- c3 = tokens[4];
595
- const { value, hasAlpha } = getAlpha(5);
596
- expectedLength = hasAlpha ? 7 : 5;
597
- alpha = value;
598
- }
599
- } else {
600
- fn = tokens[0];
601
- space = null;
602
- if (tokens[1] === "from") {
603
- fromOrigin = tokens[2];
604
- c1 = tokens[3];
605
- c2 = tokens[4];
606
- c3 = tokens[5];
607
- const { value, hasAlpha } = getAlpha(6);
608
- expectedLength = hasAlpha ? 8 : 6;
609
- alpha = value;
610
- } else {
611
- fromOrigin = null;
612
- c1 = tokens[1];
613
- if (tokens[2] === "," && tokens[4] === ",") {
614
- commaSeparated = true;
615
- c2 = tokens[3];
616
- c3 = tokens[5];
617
- const { value, hasAlpha } = getAlpha(6, ",");
618
- expectedLength = hasAlpha ? 8 : 6;
619
- if (hasAlpha && tokens[6] !== ",") {
620
- throw new Error("Comma optional syntax requires no commas at all.");
621
- }
622
- alpha = value;
623
- } else {
624
- c2 = tokens[2];
625
- c3 = tokens[3];
626
- const { value, hasAlpha } = getAlpha(4);
627
- expectedLength = hasAlpha ? 6 : 4;
628
- alpha = value;
629
- }
630
- }
631
- }
632
- if (tokens.length !== expectedLength) {
633
- throw new Error(`Invalid number of tokens for ${fn}(): expected ${expectedLength} but got ${tokens.length}.`);
634
- }
635
- return { fn, space, fromOrigin, c1, c2, c3, alpha, commaSeparated };
636
- };
637
- const tokenize = (str) => {
638
- const tokens = [];
639
- let i = 0;
640
- let funcName = "";
641
- while (i < str.length && str[i] !== "(") {
642
- funcName += str[i];
643
- i++;
644
- }
645
- funcName = funcName.trim();
646
- tokens.push(funcName);
647
- const innerStart = str.indexOf("(") + 1;
648
- const innerStr = str.slice(innerStart, -1).trim();
649
- i = 0;
650
- if (innerStr.slice(0, 5) === "from ") {
651
- tokens.push("from");
652
- i += 5;
653
- while (i < innerStr.length && innerStr[i] === " ")
654
- i++;
655
- const colorStart = i;
656
- while (i < innerStr.length && innerStr[i] !== " ")
657
- i++;
658
- const colorStr = innerStr.slice(colorStart, i);
659
- if (colorStr.includes("(")) {
660
- const { expression, end } = extractBalancedExpression(innerStr, colorStart);
661
- tokens.push(expression);
662
- i = end;
663
- } else {
664
- tokens.push(colorStr);
665
- }
666
- while (i < innerStr.length && innerStr[i] === " ")
667
- i++;
668
- }
669
- if (tokens[0] === "color" && i < innerStr.length) {
670
- const spaceStart = i;
671
- while (i < innerStr.length && innerStr[i] !== " ")
672
- i++;
673
- tokens.push(innerStr.slice(spaceStart, i));
674
- while (i < innerStr.length && innerStr[i] === " ")
675
- i++;
676
- }
677
- while (i < innerStr.length) {
678
- const char = innerStr[i];
679
- if (char === ",") {
680
- tokens.push(",");
681
- i++;
682
- if (innerStr[i] === " ")
683
- i++;
684
- } else if (char === "/") {
685
- tokens.push("/");
686
- i++;
687
- if (innerStr[i] === " ")
688
- i++;
689
- } else if (char === " ") {
690
- i++;
691
- } else if (/[a-zA-Z#]/.test(char)) {
692
- const identStart = i;
693
- let ident = "";
694
- while (i < innerStr.length && /[a-zA-Z0-9-%#]/.test(innerStr[i])) {
695
- ident += innerStr[i];
696
- i++;
697
- }
698
- if (i < innerStr.length && innerStr[i] === "(") {
699
- const { expression, end } = extractBalancedExpression(innerStr, identStart);
700
- tokens.push(expression);
701
- i = end;
702
- } else {
703
- tokens.push(ident);
704
- }
705
- } else if (/[\d.-]/.test(char)) {
706
- let num = "";
707
- while (i < innerStr.length && /[\d.eE+-]/.test(innerStr[i])) {
708
- num += innerStr[i];
709
- i++;
710
- }
711
- if (i < innerStr.length && innerStr[i] === "%") {
712
- num += "%";
713
- i++;
714
- tokens.push(num);
715
- } else if (i < innerStr.length && /[a-zA-Z]/.test(innerStr[i])) {
716
- let unit = "";
717
- while (i < innerStr.length && /[a-zA-Z]/.test(innerStr[i])) {
718
- unit += innerStr[i];
719
- i++;
720
- }
721
- tokens.push(num + unit);
722
- } else {
723
- tokens.push(num);
724
- }
725
- } else {
726
- throw new Error(`Unexpected character: ${char}`);
727
- }
728
- }
729
- return tokens;
730
- };
731
- const validateRelativeColorSpace = (str, name2) => {
732
- const prefix = "color(from ";
733
- if (str.slice(0, 11) !== prefix || str[str.length - 1] !== ")") {
734
- return false;
735
- }
736
- const innerStr = str.slice(prefix.length, -1).trim();
737
- const { expression, end } = extractBalancedExpression(innerStr, 0);
738
- if (!expression) {
739
- return false;
740
- }
741
- const rest = innerStr.slice(end).trim();
742
- const parts = rest.split(/\s+/);
743
- if (parts.length < 1) {
744
- return false;
745
- }
746
- const colorSpace = parts[0];
747
- return colorSpace === name2;
748
- };
749
- const { components, bridge, fromBridge, toBridge, alphaVariant, supportsLegacy } = converter;
750
- 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;
751
- return {
752
- isValid: (str) => {
753
- const { alphaVariant: alphaVariant2 = name } = converter;
754
- if (name in colorSpaces) {
755
- const startsWithColor = str.slice(0, `color(${name} `.length) === `color(${name} `;
756
- const startsWithFrom = str.slice(0, "color(from".length) === "color(from" && validateRelativeColorSpace(str, name);
757
- return (startsWithColor || startsWithFrom) && str[str.length - 1] === ")";
758
- }
759
- return (str.slice(0, `${name}(`.length) === `${name}(` || str.slice(0, `${alphaVariant2}(`.length) === `${alphaVariant2}(`) && str[str.length - 1] === ")";
760
- },
761
- bridge,
762
- toBridge: (coords) => [...toBridge(coords.slice(0, 3)), coords[3] ?? 1],
763
- parse: (str) => {
764
- const tokens = tokenize(str);
765
- const ast = getAST(tokens);
766
- const coords = parseAST(ast);
767
- return [...coords.slice(0, 3), coords[3] ?? 1];
768
- },
769
- fromBridge: (coords) => [...fromBridge(coords), coords[3] ?? 1],
770
- format: ([c1, c2, c3, a = 1], options = {}) => {
771
- const { legacy = false, fit: fitMethod = config.defaults.fit, precision, units = false } = options;
772
- const clipped = fit([c1, c2, c3], name, { method: fitMethod, precision });
773
- const alpha = Number(min(max(a, 0), 1).toFixed(3)).toString();
774
- const formatted = clipped.map((v, index) => {
775
- v = isNaN(v) ? 0 : v;
776
- if ((units || legacy) && components) {
777
- const def = Object.values(components).find((comp) => comp.index === index);
778
- if (def?.value === "percentage")
779
- return `${v}%`;
780
- if (def?.value === "hue" && units)
781
- return `${v}deg`;
782
- }
783
- return v.toString();
784
- });
785
- if (name in colorSpaces) {
786
- return `color(${name} ${formatted.join(" ")}${a !== 1 ? ` / ${alpha}` : ""})`;
787
- }
788
- if (legacy && supportsLegacy) {
789
- return a === 1 ? `${name}(${formatted.join(", ")})` : `${alphaVariant || name}(${formatted.join(", ")}, ${alpha})`;
790
- }
791
- return `${name}(${formatted.join(" ")}${a !== 1 ? ` / ${alpha}` : ""})`;
792
- }
793
- };
794
- }
795
174
  function spaceConverterToModelConverter(name, converter) {
796
175
  const { fromLinear = (c) => c, toLinear = (c) => c, toBridgeMatrix, fromBridgeMatrix } = converter;
797
176
  return {
798
177
  supportsLegacy: false,
799
178
  targetGamut: converter.targetGamut === null ? null : name,
800
- 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
+ },
801
183
  bridge: converter.bridge,
802
184
  toBridge: (coords) => {
803
185
  return multiplyMatrices(toBridgeMatrix, coords.map((c) => toLinear(c)));
@@ -805,6 +187,88 @@ var Saturon = (() => {
805
187
  fromBridge: (coords) => multiplyMatrices(fromBridgeMatrix, coords).map((c) => fromLinear(c))
806
188
  };
807
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
+ }
808
272
 
809
273
  // dist/math.js
810
274
  var EPSILON = 1e-5;
@@ -890,15 +354,6 @@ var Saturon = (() => {
890
354
  [1, -0.0894841775298119, -1.2914855480194092]
891
355
  ]
892
356
  };
893
- var EASINGS = {
894
- linear: (t) => t,
895
- "ease-in": (t) => t * t,
896
- "ease-out": (t) => t * (2 - t),
897
- "ease-in-out": (t) => t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t,
898
- "ease-in-cubic": (t) => t * t * t,
899
- "ease-out-cubic": (t) => --t * t * t + 1,
900
- "ease-in-out-cubic": (t) => t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2
901
- };
902
357
  var fitMethods = {
903
358
  "chroma-reduction": (coords, model) => {
904
359
  const color = new Color(model, coords);
@@ -914,7 +369,7 @@ var Saturon = (() => {
914
369
  let C_low = 0;
915
370
  let C_high = 1;
916
371
  const epsilon = 1e-6;
917
- let clipped = [];
372
+ let clipped;
918
373
  while (C_high - C_low > epsilon) {
919
374
  const C_mid = (C_low + C_high) / 2;
920
375
  const candidate_color = new Color("oklch", [L_clipped, C_mid, H]);
@@ -963,14 +418,14 @@ var Saturon = (() => {
963
418
  const E = current.deltaEOK(initialClippedColor);
964
419
  if (E < JND)
965
420
  return clipped;
966
- let min = 0;
967
- let max = C;
421
+ let min2 = 0;
422
+ let max2 = C;
968
423
  let min_inGamut = true;
969
- while (max - min > epsilon) {
970
- const chroma = (min + max) / 2;
424
+ while (max2 - min2 > epsilon) {
425
+ const chroma = (min2 + max2) / 2;
971
426
  const candidate = new Color("oklch", [L, chroma, H]);
972
427
  if (min_inGamut && candidate.inGamut(targetGamut, 1e-5))
973
- min = chroma;
428
+ min2 = chroma;
974
429
  else {
975
430
  const clippedCoords = fit(candidate.in(model).toArray({ fit: "none", precision: null }).slice(0, 3), model, { method: "clip" });
976
431
  clipped = clippedCoords;
@@ -981,10 +436,10 @@ var Saturon = (() => {
981
436
  return clipped;
982
437
  else {
983
438
  min_inGamut = false;
984
- min = chroma;
439
+ min2 = chroma;
985
440
  }
986
441
  } else
987
- max = chroma;
442
+ max2 = chroma;
988
443
  }
989
444
  }
990
445
  return clipped;
@@ -993,16 +448,16 @@ var Saturon = (() => {
993
448
  function RGB_to_XYZD65(rgb) {
994
449
  const lin_sRGB = rgb.map((v) => {
995
450
  const n = v / 255;
996
- const sign = n < 0 ? -1 : 1, abs = Math.abs(n);
997
- 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;
998
453
  });
999
454
  return multiplyMatrices(MATRICES.SRGB_to_XYZD65, lin_sRGB);
1000
455
  }
1001
456
  function XYZD65_to_RGB(xyz) {
1002
457
  const linRGB = multiplyMatrices(MATRICES.XYZD65_to_SRGB, xyz);
1003
458
  const gammaRGB = linRGB.map((v) => {
1004
- const sign = v < 0 ? -1 : 1, abs = Math.abs(v);
1005
- 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;
1006
461
  });
1007
462
  return gammaRGB.map((v) => v * 255);
1008
463
  }
@@ -1018,13 +473,13 @@ var Saturon = (() => {
1018
473
  }
1019
474
  function RGB_to_HSL(rgb) {
1020
475
  const [r, g, b] = rgb.map((v) => v / 255);
1021
- const max = Math.max(r, g, b), min = Math.min(r, g, b);
1022
- const L = (max + min) / 2;
1023
- const d = max - min;
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;
1024
479
  let H = NaN, S = 0;
1025
480
  if (d > 0) {
1026
481
  S = d / (1 - Math.abs(2 * L - 1));
1027
- H = max === r ? (g - b) / d + (g < b ? 6 : 0) : max === g ? (b - r) / d + 2 : (r - g) / d + 4;
482
+ H = max2 === r ? (g - b) / d + (g < b ? 6 : 0) : max2 === g ? (b - r) / d + 2 : (r - g) / d + 4;
1028
483
  H = H * 60 % 360;
1029
484
  }
1030
485
  const S_out = S * 100, L_out = L * 100;
@@ -1043,14 +498,14 @@ var Saturon = (() => {
1043
498
  }
1044
499
  function RGB_to_HWB(rgb) {
1045
500
  const [r, g, b] = rgb.map((v) => v / 255);
1046
- const max = Math.max(r, g, b), min = Math.min(r, g, b);
1047
- const d = max - min;
501
+ const max2 = Math.max(r, g, b), min2 = Math.min(r, g, b);
502
+ const d = max2 - min2;
1048
503
  let H = NaN;
1049
504
  if (d > EPSILON) {
1050
- H = max === r ? (g - b) / d + (g < b ? 6 : 0) : max === g ? (b - r) / d + 2 : (r - g) / d + 4;
505
+ H = max2 === r ? (g - b) / d + (g < b ? 6 : 0) : max2 === g ? (b - r) / d + 2 : (r - g) / d + 4;
1051
506
  H = H * 60 % 360;
1052
507
  }
1053
- return [H, min * 100, (1 - max) * 100];
508
+ return [H, min2 * 100, (1 - max2) * 100];
1054
509
  }
1055
510
  function LAB_to_XYZD50([L, a, b]) {
1056
511
  const D50 = [0.3457 / 0.3585, 1, (1 - 0.3457 - 0.3585) / 0.3585];
@@ -1257,18 +712,18 @@ var Saturon = (() => {
1257
712
  components: ["r", "g", "b"],
1258
713
  bridge: "xyz-d65",
1259
714
  toLinear: (c) => {
1260
- const sign = c < 0 ? -1 : 1;
1261
- const abs = Math.abs(c);
1262
- if (abs <= 0.04045)
1263
- return sign * (abs / 12.92);
1264
- 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);
1265
720
  },
1266
721
  fromLinear: (c) => {
1267
- const sign = c < 0 ? -1 : 1;
1268
- const abs = Math.abs(c);
1269
- if (abs > 31308e-7)
1270
- return sign * (1.055 * Math.pow(abs, 1 / 2.4) - 0.055);
1271
- 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);
1272
727
  },
1273
728
  toBridgeMatrix: MATRICES.SRGB_to_XYZD65,
1274
729
  fromBridgeMatrix: MATRICES.XYZD65_to_SRGB
@@ -1283,18 +738,18 @@ var Saturon = (() => {
1283
738
  components: ["r", "g", "b"],
1284
739
  bridge: "xyz-d65",
1285
740
  toLinear: (c) => {
1286
- const sign = c < 0 ? -1 : 1;
1287
- const abs = Math.abs(c);
1288
- if (abs <= 0.04045)
1289
- return sign * (abs / 12.92);
1290
- 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);
1291
746
  },
1292
747
  fromLinear: (c) => {
1293
- const sign = c < 0 ? -1 : 1;
1294
- const abs = Math.abs(c);
1295
- if (abs > 31308e-7)
1296
- return sign * (1.055 * Math.pow(abs, 1 / 2.4) - 0.055);
1297
- 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);
1298
753
  },
1299
754
  toBridgeMatrix: MATRICES.P3_to_XYZD65,
1300
755
  fromBridgeMatrix: MATRICES.XYZD65_to_P3
@@ -1305,20 +760,20 @@ var Saturon = (() => {
1305
760
  toLinear: (c) => {
1306
761
  const \u03B1 = 1.09929682680944;
1307
762
  const \u03B2 = 0.018053968510807;
1308
- const sign = c < 0 ? -1 : 1;
1309
- const abs = Math.abs(c);
1310
- if (abs < \u03B2 * 4.5)
1311
- return sign * (abs / 4.5);
1312
- 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);
1313
768
  },
1314
769
  fromLinear: (c) => {
1315
770
  const \u03B1 = 1.09929682680944;
1316
771
  const \u03B2 = 0.018053968510807;
1317
- const sign = c < 0 ? -1 : 1;
1318
- const abs = Math.abs(c);
1319
- if (abs > \u03B2)
1320
- return sign * (\u03B1 * Math.pow(abs, 0.45) - (\u03B1 - 1));
1321
- 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);
1322
777
  },
1323
778
  toBridgeMatrix: MATRICES.REC2020_to_XYZD65,
1324
779
  fromBridgeMatrix: MATRICES.XYZD65_to_REC2020
@@ -1327,14 +782,14 @@ var Saturon = (() => {
1327
782
  components: ["r", "g", "b"],
1328
783
  bridge: "xyz-d65",
1329
784
  toLinear: (c) => {
1330
- const sign = c < 0 ? -1 : 1;
1331
- const abs = Math.abs(c);
1332
- 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);
1333
788
  },
1334
789
  fromLinear: (c) => {
1335
- const sign = c < 0 ? -1 : 1;
1336
- const abs = Math.abs(c);
1337
- 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);
1338
793
  },
1339
794
  toBridgeMatrix: MATRICES.A98_to_XYZD65,
1340
795
  fromBridgeMatrix: MATRICES.XYZD65_to_A98
@@ -1344,19 +799,19 @@ var Saturon = (() => {
1344
799
  bridge: "xyz-d50",
1345
800
  toLinear: (c) => {
1346
801
  const Et2 = 16 / 512;
1347
- const sign = c < 0 ? -1 : 1;
1348
- const abs = Math.abs(c);
1349
- if (abs <= Et2)
1350
- return sign * (abs / 16);
1351
- 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);
1352
807
  },
1353
808
  fromLinear: (c) => {
1354
809
  const Et = 1 / 512;
1355
- const sign = c < 0 ? -1 : 1;
1356
- const abs = Math.abs(c);
1357
- if (abs >= Et)
1358
- return sign * Math.pow(abs, 1 / 1.8);
1359
- 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);
1360
815
  },
1361
816
  toBridgeMatrix: MATRICES.ProPhoto_to_XYZD50,
1362
817
  fromBridgeMatrix: MATRICES.XYZD50_to_ProPhoto
@@ -1406,7 +861,8 @@ var Saturon = (() => {
1406
861
  components: {
1407
862
  r: { index: 0, value: [0, 255], precision: 0 },
1408
863
  g: { index: 1, value: [0, 255], precision: 0 },
1409
- 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 }
1410
866
  },
1411
867
  bridge: "xyz-d65",
1412
868
  toBridge: RGB_to_XYZD65,
@@ -1418,7 +874,8 @@ var Saturon = (() => {
1418
874
  components: {
1419
875
  h: { index: 0, value: "hue", precision: 0 },
1420
876
  s: { index: 1, value: "percentage", precision: 0 },
1421
- l: { index: 2, value: "percentage", precision: 0 }
877
+ l: { index: 2, value: "percentage", precision: 0 },
878
+ alpha: { index: 3, value: [0, 1], precision: 3 }
1422
879
  },
1423
880
  bridge: "rgb",
1424
881
  toBridge: HSL_to_RGB,
@@ -1428,7 +885,8 @@ var Saturon = (() => {
1428
885
  components: {
1429
886
  h: { index: 0, value: "hue", precision: 0 },
1430
887
  w: { index: 1, value: "percentage", precision: 0 },
1431
- b: { index: 2, value: "percentage", precision: 0 }
888
+ b: { index: 2, value: "percentage", precision: 0 },
889
+ alpha: { index: 3, value: [0, 1], precision: 3 }
1432
890
  },
1433
891
  bridge: "rgb",
1434
892
  toBridge: HWB_to_RGB,
@@ -1439,7 +897,8 @@ var Saturon = (() => {
1439
897
  components: {
1440
898
  l: { index: 0, value: "percentage", precision: 5 },
1441
899
  a: { index: 1, value: [-125, 125], precision: 5 },
1442
- 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 }
1443
902
  },
1444
903
  bridge: "xyz-d50",
1445
904
  toBridge: LAB_to_XYZD50,
@@ -1450,7 +909,8 @@ var Saturon = (() => {
1450
909
  components: {
1451
910
  l: { index: 0, value: "percentage", precision: 5 },
1452
911
  c: { index: 1, value: [0, 150], precision: 5 },
1453
- h: { index: 2, value: "hue", precision: 5 }
912
+ h: { index: 2, value: "hue", precision: 5 },
913
+ alpha: { index: 3, value: [0, 1], precision: 3 }
1454
914
  },
1455
915
  bridge: "lab",
1456
916
  toBridge: LCH_to_LAB,
@@ -1461,7 +921,8 @@ var Saturon = (() => {
1461
921
  components: {
1462
922
  l: { index: 0, value: [0, 1], precision: 5 },
1463
923
  a: { index: 1, value: [-0.4, 0.4], precision: 5 },
1464
- 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 }
1465
926
  },
1466
927
  bridge: "xyz-d65",
1467
928
  toBridge: OKLAB_to_XYZD65,
@@ -1472,7 +933,8 @@ var Saturon = (() => {
1472
933
  components: {
1473
934
  l: { index: 0, value: [0, 1], precision: 5 },
1474
935
  c: { index: 1, value: [0, 0.4], precision: 5 },
1475
- h: { index: 2, value: "hue", precision: 5 }
936
+ h: { index: 2, value: "hue", precision: 5 },
937
+ alpha: { index: 3, value: [0, 1], precision: 3 }
1476
938
  },
1477
939
  bridge: "oklab",
1478
940
  toBridge: OKLCH_to_OKLAB,
@@ -1480,396 +942,1516 @@ var Saturon = (() => {
1480
942
  },
1481
943
  ...colorSpaces
1482
944
  };
1483
- var colorFunctions = Object.fromEntries(Object.entries(colorModels).map(([name, converter]) => [
1484
- name,
1485
- modelConverterToColorConverter(name, converter)
1486
- ]));
1487
- var colorBases = {
1488
- "hex-color": {
1489
- isValid: (str) => str[0] === "#",
1490
- bridge: "rgb",
1491
- toBridge: (coords) => coords,
1492
- parse: (str) => {
1493
- const HEX = str.slice(1);
1494
- if (![3, 4, 6, 8].includes(HEX.length))
1495
- throw new Error("Invalid hex color length.");
1496
- for (const ch of HEX)
1497
- if (!/^[0-9a-fA-F]$/.test(ch))
1498
- throw new Error("Invalid hex color character.");
1499
- const expand = (c) => parseInt(c.length === 1 ? c + c : c, 16);
1500
- 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));
1501
- return [r, g, b, a / 255];
1502
- },
1503
- fromBridge: (coords) => coords,
1504
- format: ([r, g, b, a = 1]) => {
1505
- const toHex = (v) => v.toString(16).padStart(2, "0");
1506
- const hex = [r, g, b].map((v) => toHex(Math.round(Math.max(0, Math.min(255, v))))).join("");
1507
- return `#${hex}${a < 1 ? toHex(Math.round(a * 255)) : ""}`;
1508
- }
1509
- },
1510
- ...colorFunctions,
1511
- "named-color": {
1512
- isValid: (str) => Object.keys(namedColors).some((key) => key === str),
1513
- bridge: "rgb",
1514
- toBridge: (coords) => coords,
1515
- parse: (name) => {
1516
- const rgb = namedColors[name];
1517
- if (!rgb)
1518
- throw new Error(`Invalid named-color: ${name}.`);
1519
- return [...rgb, 1];
1520
- },
1521
- fromBridge: (coords) => coords,
1522
- format: (rgb) => {
1523
- const [r, g, b] = rgb.map((v, i) => i < 3 ? Math.round(Math.min(255, Math.max(0, v))) : v);
1524
- for (const [name, [nr, ng, nb]] of Object.entries(namedColors)) {
1525
- if (r === nr && g === ng && b === nb)
1526
- 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];
1527
1130
  }
1528
1131
  }
1529
- },
1530
- "color-mix": {
1531
- isValid: (str) => str.slice(0, 10) === "color-mix(" && str[str.length - 1] === ")",
1532
- bridge: "rgb",
1533
- toBridge: (coords) => coords,
1534
- parse: (str) => {
1535
- const extractColorAndWeight = (colorStr) => {
1536
- const s = colorStr.trim();
1537
- let weight;
1538
- let remaining = s;
1539
- const leadingWeightMatch = remaining.match(/^(\d+)%\s+/);
1540
- if (leadingWeightMatch) {
1541
- const raw = parseInt(leadingWeightMatch[1], 10);
1542
- weight = Math.min(1, Math.max(0, raw / 100));
1543
- remaining = remaining.slice(leadingWeightMatch[0].length).trim();
1544
- }
1545
- let colorExpression = "";
1546
- let rest = "";
1547
- if (/^[a-z]/i.test(remaining)) {
1548
- const { expression: expr, end: e } = extractBalancedExpression(remaining, 0);
1549
- if (expr) {
1550
- colorExpression = expr;
1551
- rest = remaining.slice(e).trim();
1552
- } else {
1553
- const m = remaining.match(/^([^\s]+)(.*)$/);
1554
- colorExpression = m ? m[1] : remaining;
1555
- rest = m ? m[2].trim() : "";
1556
- }
1557
- } else {
1558
- const m = remaining.match(/^([^\s]+)(.*)$/);
1559
- colorExpression = m ? m[1] : remaining;
1560
- 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 }]
1561
1151
  }
1562
- if (weight === void 0) {
1563
- const trailingWeightMatch = rest.match(/^(-?(?:\d+\.?\d*|\.\d+))%/);
1564
- if (trailingWeightMatch) {
1565
- const raw = parseInt(trailingWeightMatch[1], 10);
1566
- weight = raw / 100;
1567
- rest = rest.slice(trailingWeightMatch[0].length).trim();
1568
- if (weight < 0) {
1569
- throw new Error("Percentages less than 0 are not valid.");
1570
- } else if (weight > 1) {
1571
- throw new Error("Percentages greater than 100 are not valid.");
1572
- }
1573
- } else if (rest.slice(0, 5) === "calc(") {
1574
- const { expression: calcExpr, end } = extractBalancedExpression(rest, 0);
1575
- if (!calcExpr) {
1576
- 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
1577
1190
  }
1578
- rest = rest.slice(end).trim();
1579
- }
1191
+ ]
1580
1192
  }
1581
- if (rest.length > 0) {
1582
- throw new Error(`Unexpected extra tokens after color: '${rest}'.`);
1583
- }
1584
- const color = Color.from(colorExpression);
1585
- return { color, weight };
1586
- };
1587
- const getWeight2Prime = (weight12, weight22) => {
1588
- if (weight12 === void 0 && typeof weight22 === "number") {
1589
- weight12 = 1 - weight22;
1590
- } else if (typeof weight12 === "number" && weight22 === void 0) {
1591
- weight22 = 1 - weight12;
1592
- } else if (weight12 === void 0 && weight22 === void 0) {
1593
- weight12 = weight22 = 0.5;
1594
- } else {
1595
- weight12 = weight12;
1596
- weight22 = weight22;
1597
- if (weight12 + weight22 <= 0) {
1598
- throw new Error("Sum of percengates cannot be 0%.");
1599
- }
1600
- }
1601
- const totalWeight = weight12 + weight22;
1602
- let alphaMultiplier2;
1603
- if (totalWeight > 1) {
1604
- weight12 /= totalWeight;
1605
- weight22 /= totalWeight;
1606
- } else {
1607
- weight12 /= totalWeight;
1608
- weight22 /= totalWeight;
1609
- 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
+ ];
1610
1209
  }
1611
- return { amount: weight22, alphaMultiplier: alphaMultiplier2 };
1612
- };
1613
- const fnName = "color-mix";
1614
- const { expression } = extractBalancedExpression(str, fnName.length);
1615
- if (!expression)
1616
- throw new Error("Malformed color-mix expression.");
1617
- const inner = expression.slice(1, -1).trim();
1618
- const parts = [];
1619
- let i = 0;
1620
- let current = "";
1621
- while (i < inner.length) {
1622
- const char = inner[i];
1623
- if (char === ",") {
1624
- parts.push(current.trim());
1625
- current = "";
1626
- i++;
1627
- 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]);
1628
1219
  }
1629
- if (char === "(" || /[a-zA-Z]/.test(char)) {
1630
- const { expression: expr, end } = extractBalancedExpression(inner, i);
1631
- if (expr) {
1632
- current += expr;
1633
- i = end;
1634
- 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
+ });
1635
1237
  }
1636
1238
  }
1637
- current += char;
1638
- i++;
1639
- }
1640
- parts.push(current.trim());
1641
- if (parts.length !== 3) {
1642
- throw new Error("color-mix must have three comma-separated parts.");
1239
+ states = nextStates;
1240
+ if (states.length === 0)
1241
+ return [];
1643
1242
  }
1644
- const inPart = parts[0];
1645
- const inMatch = inPart.match(/^in\s+([a-z0-9-]+)(?:\s+(shorter|longer|increasing|decreasing)\s+hue)?$/);
1646
- if (!inMatch) {
1647
- throw new Error("Invalid model and hue format.");
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];
1648
1251
  }
1649
- const model = inMatch[1];
1650
- let hue = inMatch[2];
1651
- if (model === "rgb")
1652
- throw new Error(`RGB model is not allowed in color-mix(). Use "srgb" instead.`);
1653
- if (hue) {
1654
- const comps = colorModels[model].components;
1655
- if (!comps) {
1656
- throw new Error(`Unknown color model: ${model}.`);
1657
- }
1658
- const hasHue = Object.values(comps).some((c) => c.value === "hue");
1659
- if (!hasHue) {
1660
- throw new Error(`Hue interpolation not supported in ${model} space.`);
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
+ }
1661
1268
  }
1662
- } else {
1663
- hue = "shorter";
1269
+ states = nextStates;
1270
+ if (states.length === 0)
1271
+ return [];
1664
1272
  }
1665
- const { color: color1, weight: weight1 } = extractColorAndWeight(parts[1]);
1666
- const { color: color2, weight: weight2 } = extractColorAndWeight(parts[2]);
1667
- const { amount, alphaMultiplier = 1 } = getWeight2Prime(weight1, weight2);
1668
- return color1.with({ alpha: (a) => isNaN(a) ? a : a * alphaMultiplier }, false).in(model).mix(color2.with({ alpha: (a) => isNaN(a) ? a : a * alphaMultiplier }, false), { amount, hue }).in("rgb").coords;
1669
- }
1670
- },
1671
- transparent: {
1672
- isValid: (str) => str === "transparent",
1673
- bridge: "rgb",
1674
- toBridge: (coords) => coords,
1675
- parse: (str) => [NaN, NaN, NaN, 0]
1676
- // eslint-disable-line no-unused-vars
1677
- }
1678
- };
1679
- var colorTypes = {
1680
- ...colorBases,
1681
- currentColor: {
1682
- isValid: (str) => str === "currentcolor",
1683
- bridge: "rgb",
1684
- toBridge: (coords) => coords,
1685
- parse: (str) => [0, 0, 0, 1]
1686
- // eslint-disable-line no-unused-vars
1687
- },
1688
- "system-color": {
1689
- isValid: (str) => Object.keys(config.systemColors).some((key) => key.toLowerCase() === str),
1690
- bridge: "rgb",
1691
- toBridge: (coords) => coords,
1692
- parse: (str) => {
1693
- const { systemColors: systemColors2 } = config;
1694
- const key = Object.keys(systemColors2).find((k) => k.toLowerCase() === str);
1695
- const rgbArr = systemColors2[key][config.theme === "light" ? 0 : 1];
1696
- return [...rgbArr, 1];
1697
- }
1698
- },
1699
- "contrast-color": {
1700
- isValid: (str) => str.slice(0, 15) === "contrast-color(" && str[str.length - 1] === ")",
1701
- bridge: "rgb",
1702
- toBridge: (coords) => coords,
1703
- parse: (str) => {
1704
- const inner = str.slice(15, -1);
1705
- const [, luminance] = Color.from(inner).in("xyz-d65").coords;
1706
- return luminance > 0.5 ? [0, 0, 0, 1] : [255, 255, 255, 1];
1273
+ return states;
1707
1274
  }
1708
- },
1709
- "device-cmyk": {
1710
- isValid: (str) => str.slice(0, 12) === "device-cmyk(" && str[str.length - 1] === ")",
1711
- bridge: "rgb",
1712
- toBridge: (coords) => coords,
1713
- parse: (str) => {
1714
- const fnName = "device-cmyk";
1715
- const fnIndex = str.indexOf(fnName);
1716
- if (fnIndex === -1)
1717
- throw new Error("Invalid device-cmyk syntax");
1718
- const { expression } = extractBalancedExpression(str, fnIndex + fnName.length);
1719
- if (!expression)
1720
- throw new Error("Malformed device-cmyk expression");
1721
- const content = expression.slice(1, -1).trim();
1722
- const tokens = [];
1723
- let i = 0;
1724
- while (i < content.length) {
1725
- const char = content[i];
1726
- if (char === " ") {
1727
- i++;
1728
- continue;
1729
- }
1730
- if (char === "(" || /[a-zA-Z-]/.test(char)) {
1731
- const { expression: expr, end } = extractBalancedExpression(content, i);
1732
- if (expr) {
1733
- tokens.push(expr);
1734
- i = end;
1735
- continue;
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
+ }
1736
1292
  }
1737
- }
1738
- if (/[\d.+-]/.test(char)) {
1739
- let num = "";
1740
- while (i < content.length && /[\d.eE+-]/.test(content[i])) {
1741
- num += content[i];
1742
- i++;
1293
+ if (allRemainingOptional) {
1294
+ finalResults.push({
1295
+ success: true,
1296
+ nextIndex: state.nextIndex,
1297
+ nodes: state.nodes
1298
+ });
1743
1299
  }
1744
- if (i < content.length && content[i] === "%") {
1745
- num += "%";
1746
- i++;
1747
- } else if (i < content.length && /[a-zA-Z]/.test(content[i])) {
1748
- while (i < content.length && /[a-zA-Z]/.test(content[i])) {
1749
- num += content[i];
1750
- i++;
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
+ }
1751
1313
  }
1752
1314
  }
1753
- tokens.push(num);
1754
- continue;
1755
1315
  }
1756
- if (char === "," || char === "/") {
1757
- tokens.push(char);
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
+ }
1350
+ }
1351
+ currentStates = nextStates;
1352
+ }
1353
+ return allSuccessfulStates;
1354
+ }
1355
+ }
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;
1370
+ }
1371
+ const ch = lower[i];
1372
+ if (/\s/.test(ch)) {
1373
+ i++;
1374
+ continue;
1375
+ }
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 = "%";
1758
1691
  i++;
1759
- 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;
1760
1708
  }
1761
- throw new Error(`Unexpected character: ${char}`);
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);
1714
+ }
1715
+ out.push({ type: "number", value: val, unit: unitType });
1716
+ continue;
1762
1717
  }
1763
- const parseValue = (token) => {
1764
- return token[token.length - 1] === "%" ? parseFloat(token) / 100 : parseFloat(token);
1765
- };
1766
- if (tokens.length >= 2 && tokens[1] === ",") {
1767
- const values = tokens.filter((t) => t !== ",");
1768
- if (values.length === 4 || values.length === 5) {
1769
- const [c2, m2, y2, k2, alpha2 = 1] = values.map(parseValue);
1770
- const red2 = 1 - Math.min(1, c2 * (1 - k2) + k2);
1771
- const green2 = 1 - Math.min(1, m2 * (1 - k2) + k2);
1772
- const blue2 = 1 - Math.min(1, y2 * (1 - k2) + k2);
1773
- return [red2 * 255, green2 * 255, blue2 * 255, alpha2];
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());
1767
+ }
1768
+ }
1769
+ expect(")");
1770
+ return { type: "call", func: t.value, args };
1774
1771
  }
1775
- throw new Error("Invalid number of components for comma-separated device-cmyk");
1772
+ return { type: "var", name: t.value };
1776
1773
  }
1777
- let idx = 0;
1778
- const components = [];
1779
- while (idx < tokens.length && components.length < 4 && tokens[idx] !== "/" && tokens[idx] !== ",") {
1780
- components.push(tokens[idx]);
1781
- idx++;
1774
+ if (t.value === "(") {
1775
+ nxt();
1776
+ const e2 = parseAdd();
1777
+ expect(")");
1778
+ return e2;
1782
1779
  }
1783
- if (components.length !== 4) {
1784
- throw new Error("Invalid number of components for space-separated device-cmyk");
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() };
1785
1786
  }
1786
- const [c, m, y, k] = components.map(parseValue);
1787
- let alpha = 1;
1788
- if (idx < tokens.length && tokens[idx] === "/") {
1789
- idx++;
1790
- if (idx >= tokens.length)
1791
- throw new Error("Missing alpha value");
1792
- alpha = parseValue(tokens[idx]);
1793
- idx++;
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
1794
  }
1795
- if (idx < tokens.length && tokens[idx] === ",") {
1796
- idx++;
1797
- const fallbackStr = tokens.slice(idx).join(" ");
1798
- return Color.from(fallbackStr).in("rgb").coords;
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() };
1799
1802
  }
1800
- const red = 1 - Math.min(1, c * (1 - k) + k);
1801
- const green = 1 - Math.min(1, m * (1 - k) + k);
1802
- const blue = 1 - Math.min(1, y * (1 - k) + k);
1803
- return [red * 255, green * 255, blue * 255, alpha];
1804
- },
1805
- fromBridge: (coords) => coords,
1806
- format: ([red, green, blue, alpha = 1], options = {}) => {
1807
- const { legacy = false, precision = 3, fit: fitMethod = config.defaults.fit } = options;
1808
- const [fr, fg, fb] = fit([red, green, blue], "rgb", { method: fitMethod });
1809
- const r = fr / 255;
1810
- const g = fg / 255;
1811
- const b = fb / 255;
1812
- const k = 1 - Math.max(r, g, b);
1813
- const formatComponent = (value) => {
1814
- return Number(precision === null ? value : value.toFixed(precision)).toString();
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}`);
1829
+ }
1830
+ return { value: v, unit: UNIT_NUM };
1831
+ }
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}`);
1875
+ }
1876
+ }
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
+ }
1887
+ }
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 };
1909
+ }
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])
1815
2034
  };
1816
- const c = formatComponent(k === 1 ? 0 : (1 - r - k) / (1 - k));
1817
- const m = formatComponent(k === 1 ? 0 : (1 - g - k) / (1 - k));
1818
- const y = formatComponent(k === 1 ? 0 : (1 - b - k) / (1 - k));
1819
- const kFormatted = formatComponent(k);
1820
- const alphaFormatted = Number(alpha.toFixed(3)).toString();
1821
- if (legacy) {
1822
- return `device-cmyk(${c}, ${m}, ${y}, ${kFormatted}${alpha < 1 ? `, ${alphaFormatted}` : ""})`;
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);
1823
2058
  }
1824
- const rgbPart = colorFunctions.rgb.format?.([fr, fg, fb, alpha], options);
1825
- return `device-cmyk(${c} ${m} ${y} ${kFormatted}${alpha < 1 ? ` / ${alphaFormatted}` : ""}, ${rgbPart})`;
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);
2070
+ }
2071
+ return true;
2072
+ },
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);
2082
+ }
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}`);
1826
2096
  }
2097
+ return parseNode(child);
1827
2098
  },
1828
- "light-dark": {
1829
- isValid: (str) => str.slice(0, 11) === "light-dark(" && str[str.length - 1] === ")",
1830
- bridge: "rgb",
1831
- toBridge: (coords) => coords,
1832
- parse: (str) => {
1833
- const fnName = "light-dark";
1834
- const fnIndex = str.indexOf(fnName);
1835
- if (fnIndex === -1)
1836
- throw new Error("Not a <light-dark()> expression");
1837
- const { expression } = extractBalancedExpression(str, fnIndex + fnName.length);
1838
- if (!expression)
1839
- throw new Error("Malformed <light-dark()> expression");
1840
- const inner = expression.slice(1, -1).trim();
1841
- const parts = [];
1842
- let current = "";
1843
- let i = 0;
1844
- while (i < inner.length) {
1845
- const char = inner[i];
1846
- if (char === ",") {
1847
- parts.push(current.trim());
1848
- 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");
1849
2244
  i++;
1850
- continue;
1851
2245
  }
1852
- if (char === "(" || /[a-zA-Z]/.test(char)) {
1853
- const { expression: expr, end } = extractBalancedExpression(inner, i);
1854
- if (expr) {
1855
- current += expr;
1856
- i = end;
1857
- 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})`;
1858
2363
  }
2364
+ return `${model}(${f.join(" ")}${a !== 1 ? ` / ${alpha}` : ""})`;
1859
2365
  }
1860
- current += char;
1861
- 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)) : ""}`;
1862
2379
  }
1863
- parts.push(current.trim());
1864
- if (parts.length !== 2) {
1865
- 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
+ }
1866
2390
  }
1867
- const [color1, color2] = parts;
1868
- const { theme } = config;
1869
- return Color.from(theme === "light" ? color1 : color2).coords;
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 });
1870
2444
  }
1871
2445
  }
1872
- };
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
+ }
1873
2455
 
1874
2456
  // dist/Color.js
1875
2457
  var Color = class _Color {
@@ -1887,59 +2469,26 @@ var Saturon = (() => {
1887
2469
  this.coords = c;
1888
2470
  }
1889
2471
  static from(color) {
1890
- const c = clean(color);
1891
- for (const type in colorTypes) {
1892
- const t = type;
1893
- const { parse, bridge, toBridge, isValid } = colorTypes[t];
1894
- if (!isValid(c))
1895
- continue;
1896
- const parsed = parse(c);
1897
- const coords = t in colorModels ? parsed : toBridge(parsed);
1898
- 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);
1899
2476
  return new _Color(model, coords);
2477
+ } catch (error) {
2478
+ throw new Error(`Failed to parse color string '${color}'`, { cause: error });
1900
2479
  }
1901
- throw new Error(`Unsupported or invalid color format: '${color}'.`);
1902
2480
  }
1903
2481
  /**
1904
- * Returns the detected color type, or `undefined` if unrecognized.
2482
+ * Validates a color string.
1905
2483
  *
1906
- * @param color - Color string to analyze.
1907
- * @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`.
1908
2487
  */
1909
- static type(color, strict = false) {
1910
- const c = clean(color);
1911
- for (const type in colorTypes) {
1912
- const t = type;
1913
- const { isValid, bridge, parse, toBridge } = colorTypes[t];
1914
- if (!isValid(c))
1915
- continue;
1916
- if (!strict)
1917
- return t;
1918
- try {
1919
- const parsed = parse(c);
1920
- const coords = t in colorModels ? parsed : toBridge(parsed);
1921
- const model = type in colorModels ? type : bridge;
1922
- return typeof new _Color(model, coords) === "object" ? t : void 0;
1923
- } catch {
1924
- return void 0;
1925
- }
1926
- }
1927
- return void 0;
1928
- }
1929
- static isValid(color, type) {
2488
+ static isValid(color, options = {}) {
2489
+ const { rule } = options;
1930
2490
  try {
1931
- if (type) {
1932
- const t = type?.trim().toLowerCase();
1933
- const c = clean(color);
1934
- const { isValid, bridge, parse, toBridge } = colorTypes[t];
1935
- if (!isValid(c))
1936
- return false;
1937
- const parsed = parse(c);
1938
- const coords = t in colorModels ? parsed : toBridge(parsed);
1939
- const model = t in colorModels ? t : bridge;
1940
- return !!new _Color(model, coords);
1941
- }
1942
- return !!_Color.from(color);
2491
+ return isValid(color, rule) && !!_Color.from(color);
1943
2492
  } catch {
1944
2493
  return false;
1945
2494
  }
@@ -1956,7 +2505,7 @@ var Saturon = (() => {
1956
2505
  const models = Object.keys(colorModels);
1957
2506
  const model = options.model ?? models[Math.floor(Math.random() * models.length)];
1958
2507
  const { components } = colorModels[model];
1959
- const valid = /* @__PURE__ */ new Set([...Object.keys(components), "alpha"]);
2508
+ const valid = new Set(Object.keys(components));
1960
2509
  for (const section of ["limits", "bias", "base", "deviation"]) {
1961
2510
  const record = options[section];
1962
2511
  if (!record)
@@ -1977,36 +2526,91 @@ var Saturon = (() => {
1977
2526
  const v = Math.random() || 1e-9;
1978
2527
  value = base + Math.sqrt(-2 * Math.log(u)) * Math.cos(2 * Math.PI * v) * dev;
1979
2528
  } else {
1980
- let [min, max] = comp.value === "hue" ? [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;
1981
2530
  const limits = options.limits?.[name];
1982
2531
  if (limits) {
1983
2532
  const [lMin, lMax] = limits;
1984
- min = Math.max(min, lMin);
1985
- max = Math.min(max, lMax);
2533
+ min2 = Math.max(min2, lMin);
2534
+ max2 = Math.min(max2, lMax);
1986
2535
  }
1987
2536
  let r = Math.random();
1988
2537
  const biasFn = options.bias?.[name];
1989
2538
  if (biasFn)
1990
2539
  r = biasFn(r);
1991
- value = min + r * (max - min);
2540
+ value = min2 + r * (max2 - min2);
1992
2541
  }
1993
2542
  if (comp.value === "hue")
1994
2543
  value = (value % 360 + 360) % 360;
1995
2544
  else if (comp.value === "percentage")
1996
2545
  value = Math.min(100, Math.max(0, value));
1997
2546
  else if (Array.isArray(comp.value)) {
1998
- const [min, max] = comp.value;
1999
- value = Math.min(max, Math.max(min, value));
2547
+ const [min2, max2] = comp.value;
2548
+ value = Math.min(max2, Math.max(min2, value));
2000
2549
  } else
2001
2550
  throw new Error(`Invalid component value definition for "${name}".`);
2002
2551
  coords[comp.index] = value;
2003
2552
  }
2004
2553
  return new _Color(model, coords);
2005
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
+ }
2006
2610
  to(type, options = {}) {
2007
2611
  const t = type.toLowerCase();
2008
2612
  const { legacy = false, fit: fit2 = config.defaults.fit, precision, units = false } = options;
2009
- const conv = colorTypes[t];
2613
+ const conv = formatters[t];
2010
2614
  if (!conv)
2011
2615
  throw new Error(`Unsupported color type: '${t}'.`);
2012
2616
  const { fromBridge, bridge, format } = conv;
@@ -2084,7 +2688,7 @@ var Saturon = (() => {
2084
2688
  * @returns The formatted color string.
2085
2689
  */
2086
2690
  toString(options = {}) {
2087
- const { format } = colorTypes[this.model];
2691
+ const { format } = formatters[this.model];
2088
2692
  const { legacy = false, fit: fit2 = config.defaults.fit, precision, units = false } = options;
2089
2693
  return format?.(this.coords, { legacy, fit: fit2, precision, units });
2090
2694
  }
@@ -2092,20 +2696,17 @@ var Saturon = (() => {
2092
2696
  * Returns the color as an object of component values.
2093
2697
  *
2094
2698
  * @param options - Optional retrieval options.
2095
- * @returns An object mapping each component (and alpha) to its numeric value.
2699
+ * @returns An object mapping each component to its numeric value.
2096
2700
  * @throws If the model has no defined components.
2097
2701
  */
2702
+ // eslint-disable-next-line no-unused-vars
2098
2703
  toObject(options = {}) {
2099
2704
  const coords = this.toArray(options);
2100
2705
  const { components } = colorModels[this.model];
2101
2706
  if (!components)
2102
2707
  throw new Error(`Model ${this.model} does not have defined components.`);
2103
- const fullComponents = {
2104
- ...components,
2105
- alpha: { index: 3, value: [0, 1], precision: 3 }
2106
- };
2107
2708
  const result = {};
2108
- for (const [name, { index }] of Object.entries(fullComponents))
2709
+ for (const [name, { index }] of Object.entries(components))
2109
2710
  result[name] = coords[index];
2110
2711
  return result;
2111
2712
  }
@@ -2113,7 +2714,7 @@ var Saturon = (() => {
2113
2714
  * Returns the color as an array of component values, optionally normalized and fitted.
2114
2715
  *
2115
2716
  * @param options - Conversion configuration.
2116
- * @returns An array of normalized color components and alpha.
2717
+ * @returns An array of normalized color components.
2117
2718
  * @throws If the model has no defined components.
2118
2719
  */
2119
2720
  toArray(options = {}) {
@@ -2122,22 +2723,7 @@ var Saturon = (() => {
2122
2723
  const { components } = colorModels[model];
2123
2724
  if (!components)
2124
2725
  throw new Error(`Model ${model} does not have defined components.`);
2125
- const defs = {
2126
- ...components,
2127
- alpha: { index: 3, value: [0, 1], precision: 3 }
2128
- };
2129
- const normalize = (c, i) => {
2130
- const v = Object.values(defs)[i]?.value;
2131
- const [min, max] = Array.isArray(v) ? v : v === "hue" ? [0, 360] : [0, 100];
2132
- if (Number.isNaN(c))
2133
- return 0;
2134
- if (c === Infinity)
2135
- return max;
2136
- if (c === -Infinity)
2137
- return min;
2138
- return typeof c === "number" ? c : 0;
2139
- };
2140
- 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));
2141
2727
  const fitted = fit(norm, model, {
2142
2728
  method,
2143
2729
  precision
@@ -2189,44 +2775,28 @@ var Saturon = (() => {
2189
2775
  * - A partial object mapping component names to numbers or update functions
2190
2776
  * - A function that receives current components and returns partial updates or an array of values
2191
2777
  * - An array of new values corresponding to component indices
2192
- * @param normalize - Whether to normalize component values to their valid ranges. Defaults to `true`.
2778
+ * @param normalized - Whether to normalize component values to their valid ranges. Defaults to `true`.
2193
2779
  * When `false`, values are not clamped or validated against their ranges.
2194
2780
  * @returns A new Color instance with the updated component values
2195
2781
  */
2196
2782
  /* eslint-disable no-unused-vars */
2197
- with(values, normalize = true) {
2198
- const normRange = (defValue) => Array.isArray(defValue) ? defValue : defValue === "hue" ? [0, 360] : [0, 100];
2199
- const normalizeComponent = (c, defValue) => {
2200
- if (normalize === false)
2201
- return c;
2202
- const [min, max] = normRange(defValue);
2203
- if (Number.isNaN(c))
2204
- return 0;
2205
- if (c === Infinity)
2206
- return max;
2207
- if (c === -Infinity)
2208
- return min;
2209
- return c;
2210
- };
2783
+ with(values) {
2211
2784
  const { model } = this;
2212
2785
  const coords = this.coords.slice();
2213
- const defs = {
2214
- ...colorModels[model].components,
2215
- // eslint-disable-line @typescript-eslint/no-explicit-any
2216
- alpha: { index: 3, value: [0, 1], precision: 3 }
2217
- };
2218
- const names = Object.keys(defs);
2786
+ const { components } = colorModels[model];
2787
+ const names = Object.keys(components);
2788
+ const componentDefs = Object.values(components);
2219
2789
  const newValues = typeof values === "function" ? values(Object.fromEntries(names.map((k) => {
2220
- const def = defs[k];
2221
- return [k, normalizeComponent(coords[def.index], def.value)];
2790
+ const def = components[k];
2791
+ return [k, normalize(coords[def.index], def.value)];
2222
2792
  }))) : values;
2223
2793
  if (Array.isArray(newValues)) {
2224
2794
  const adjusted = coords.map((curr, i) => {
2225
2795
  const incoming = newValues[i];
2226
2796
  if (typeof incoming !== "number")
2227
2797
  return curr;
2228
- const def = Object.values(defs).find((d) => d.index === i);
2229
- return normalizeComponent(incoming, def.value);
2798
+ const def = componentDefs.find((d) => d.index === i);
2799
+ return normalize(incoming, def.value);
2230
2800
  });
2231
2801
  return new _Color(model, [...adjusted.slice(0, 3), coords[3] ?? 1]);
2232
2802
  }
@@ -2234,107 +2804,24 @@ var Saturon = (() => {
2234
2804
  for (const name of names) {
2235
2805
  if (!(name in newValues))
2236
2806
  continue;
2237
- const { index, value } = defs[name];
2807
+ const { index, value } = components[name];
2238
2808
  const raw = newValues[name];
2239
- const prev = normalizeComponent(coords[index], value);
2240
- const val = typeof raw === "function" ? normalizeComponent(raw(prev), value) : typeof raw === "number" ? normalizeComponent(raw, value) : coords[index];
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];
2241
2811
  next[index] = val;
2242
2812
  }
2243
2813
  return new _Color(model, [...next.slice(0, 3), next[3] ?? coords[3]]);
2244
2814
  }
2245
2815
  /**
2246
- * Mixes this color with another by a specified amount.
2816
+ * Creates a new Color instance with values fitted to the color model's gamut.
2247
2817
  *
2248
- * @param other - The color to mix with. Can be a string or a `Color` instance.
2249
- * @param options - Options for how the colors are mixed.
2250
- * @returns A new `Color` instance representing the mixed color.
2251
- * @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
2252
2820
  */
2253
- mix(other, options = {}) {
2254
- const { model } = this;
2255
- const A = this.coords.slice();
2256
- const B = (typeof other === "string" ? _Color.from(other) : other).in(model).coords.slice();
2257
- const { components } = colorModels[model];
2258
- if (!components)
2259
- throw new Error(`Model ${model} does not have defined components.`);
2260
- const defs = {
2261
- ...components,
2262
- alpha: { index: 3, value: [0, 1], precision: 3 }
2263
- };
2264
- const normRange = (v) => Array.isArray(v) ? v : v === "hue" ? [0, 360] : [0, 100];
2265
- const repairPair = (a, b, v) => {
2266
- const [min, max] = normRange(v);
2267
- const fixInf = (x) => x === Infinity ? max : x === -Infinity ? min : x;
2268
- a = fixInf(a);
2269
- b = fixInf(b);
2270
- const aNaN = Number.isNaN(a);
2271
- const bNaN = Number.isNaN(b);
2272
- if (aNaN && bNaN)
2273
- return { a: 0, b: 0 };
2274
- if (aNaN)
2275
- return { a: b, b };
2276
- if (bNaN)
2277
- return { a, b: a };
2278
- return { a, b, ok: true };
2279
- };
2280
- const hueIndex = defs.h?.index ?? -1;
2281
- for (const key in defs) {
2282
- const { index, value } = defs[key];
2283
- if (index > 3)
2284
- continue;
2285
- const r = repairPair(A[index], B[index], value);
2286
- if (r.ok) {
2287
- const [min, max] = normRange(value);
2288
- const fixInf = (x) => x === Infinity ? max : x === -Infinity ? min : x;
2289
- A[index] = fixInf(A[index]);
2290
- B[index] = fixInf(B[index]);
2291
- } else {
2292
- A[index] = r.a;
2293
- B[index] = r.b;
2294
- }
2295
- }
2296
- const { hue = "shorter", amount = 0.5, easing = "linear", gamma = 1 } = options;
2297
- const ease = typeof easing === "function" ? easing : EASINGS[easing];
2298
- const t = ease(Math.min(1, Math.max(0, amount)));
2299
- const tt = Math.pow(t, 1 / gamma);
2300
- const wrap = (v) => (v % 360 + 360) % 360;
2301
- const hueDelta = (a, b) => {
2302
- const d = wrap(b - a);
2303
- return d > 180 ? d - 360 : d;
2304
- };
2305
- const hueDeltaLong = (a, b) => hueDelta(a, b) >= 0 ? hueDelta(a, b) - 360 : hueDelta(a, b) + 360;
2306
- const interpHue = (a, b, t2, method) => {
2307
- switch (method) {
2308
- case "shorter":
2309
- return wrap(a + t2 * hueDelta(a, b));
2310
- case "longer":
2311
- return wrap(a + t2 * hueDeltaLong(a, b));
2312
- case "increasing":
2313
- return wrap(a * (1 - t2) + (b < a ? b + 360 : b) * t2);
2314
- case "decreasing":
2315
- return wrap(a * (1 - t2) + (b > a ? b - 360 : b) * t2);
2316
- }
2317
- throw new Error(`Invalid hue interpolation: ${method}`);
2318
- };
2319
- if (tt === 0)
2320
- return new _Color(model, [...A]);
2321
- if (tt === 1)
2322
- return new _Color(model, [...B]);
2323
- const aA = A[3];
2324
- const aB = B[3];
2325
- const resolvedA = A.slice(0, 3);
2326
- const resolvedB = B.slice(0, 3);
2327
- if (aA < 1 || aB < 1) {
2328
- const premixed = resolvedA.map((a2, i) => {
2329
- const b = resolvedB[i];
2330
- return i === hueIndex ? interpHue(a2, b, tt, hue) : a2 * aA * (1 - tt) + b * aB * tt;
2331
- });
2332
- const a = aA * (1 - tt) + aB * tt;
2333
- const out = a > 0 ? premixed.map((v, i) => i === hueIndex ? v : v / a) : resolvedA.map((_, i) => i === hueIndex ? premixed[i] : 0);
2334
- return new _Color(model, [...out, a]);
2335
- }
2336
- const mixed = resolvedA.map((a, i) => i === hueIndex ? interpHue(a, resolvedB[i], tt, hue) : a + (resolvedB[i] - a) * tt);
2337
- return new _Color(model, [...mixed, 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);
2338
2825
  }
2339
2826
  /**
2340
2827
  * Fits this color within the specified gamut using a given method.
@@ -2351,21 +2838,10 @@ var Saturon = (() => {
2351
2838
  const fitted = this.in(g).toArray({ fit: method, precision: null });
2352
2839
  return new _Color(g, fitted).in(this.model);
2353
2840
  }
2354
- /**
2355
- * Creates a new Color instance with values fitted to the color model's gamut.
2356
- *
2357
- * @param options - Configuration options for fitting
2358
- * @returns A new Color instance with fitted values
2359
- */
2360
- fit(options = {}) {
2361
- const { fit: method = config.defaults.fit, precision } = options;
2362
- const fitted = this.toArray({ fit: method, precision });
2363
- return new _Color(this.model, fitted);
2364
- }
2365
2841
  /**
2366
2842
  * Calculates the WCAG 2.1 contrast ratio between this color and another.
2367
2843
  *
2368
- * @param other - The comparison color (instance or string).
2844
+ * @param other - The comparison Color instance.
2369
2845
  * @returns Contrast ratio from 1 to 21.
2370
2846
  *
2371
2847
  * @remarks
@@ -2373,15 +2849,14 @@ var Saturon = (() => {
2373
2849
  * - For perceptual accuracy, consider using APCA instead.
2374
2850
  */
2375
2851
  contrast(other) {
2376
- const o = typeof other === "string" ? _Color.from(other) : other;
2377
2852
  const [, L1] = this.in("xyz-d65").toArray({ fit: "none", precision: null });
2378
- const [, L2] = o.in("xyz-d65").toArray({ fit: "none", precision: null });
2853
+ const [, L2] = other.in("xyz-d65").toArray({ fit: "none", precision: null });
2379
2854
  return (Math.max(L1, L2) + 0.05) / (Math.min(L1, L2) + 0.05);
2380
2855
  }
2381
2856
  /**
2382
2857
  * Calculates the color difference (ΔEOK) between the current color and another color using the OKLAB color space.
2383
2858
  *
2384
- * @param other - The other color to compare against (as a Color instance or string).
2859
+ * @param other - The other Color instance to compare against.
2385
2860
  * @returns A number in range (0-1) (smaller indicates more similar colors).
2386
2861
  *
2387
2862
  * @remarks
@@ -2392,7 +2867,7 @@ var Saturon = (() => {
2392
2867
  deltaEOK(other) {
2393
2868
  const coordsOptions = { fit: "none", precision: null };
2394
2869
  const [L1, a1, b1] = this.in("oklab").toArray(coordsOptions);
2395
- 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);
2396
2871
  const \u0394L = L1 - L2;
2397
2872
  const \u0394A = a1 - a2;
2398
2873
  const \u0394B = b1 - b2;
@@ -2403,13 +2878,13 @@ var Saturon = (() => {
2403
2878
  * Calculates the color difference (ΔE) between two colors using the CIE76 formula.
2404
2879
  * This is a simple Euclidean distance in LAB color space.
2405
2880
  *
2406
- * @param other - The other color to compare against (as a Color instance or string).
2881
+ * @param other - The other Color instance to compare against.
2407
2882
  * @returns A number in range (0-1) (smaller indicates more similar colors).
2408
2883
  */
2409
2884
  deltaE76(other) {
2410
2885
  const coordsOptions = { fit: "none", precision: null };
2411
2886
  const [L1, a1, b1] = this.in("lab").toArray(coordsOptions);
2412
- 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);
2413
2888
  const \u0394L = L1 - L2;
2414
2889
  const \u0394A = a1 - a2;
2415
2890
  const \u0394B = b1 - b2;
@@ -2419,13 +2894,13 @@ var Saturon = (() => {
2419
2894
  * Calculates the color difference (ΔE) between two colors using the CIE94 formula.
2420
2895
  * This method improves perceptual accuracy over CIE76 by applying weighting factors.
2421
2896
  *
2422
- * @param other - The other color to compare against (as a Color instance or string).
2897
+ * @param other - The other Color instance to compare against.
2423
2898
  * @returns A number in range (0-1) (smaller indicates more similar colors).
2424
2899
  */
2425
2900
  deltaE94(other) {
2426
2901
  const coordsOptions = { fit: "none", precision: null };
2427
2902
  const [L1, a1, b1] = this.in("lab").toArray(coordsOptions);
2428
- 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);
2429
2904
  const \u0394L = L1 - L2;
2430
2905
  const \u0394A = a1 - a2;
2431
2906
  const \u0394B = b1 - b2;
@@ -2443,13 +2918,13 @@ var Saturon = (() => {
2443
2918
  * Calculates the color difference (ΔE) between two colors using the CIEDE2000 formula.
2444
2919
  * This is the most perceptually accurate method, accounting for interactions between hue, chroma, and lightness.
2445
2920
  *
2446
- * @param other - The other color to compare against (as a Color instance or string).
2921
+ * @param other - The other Color instance to compare against.
2447
2922
  * @returns A number in range (0-1) (smaller indicates more similar colors).
2448
2923
  */
2449
2924
  deltaE2000(other) {
2450
2925
  const coordsOptions = { fit: "none", precision: null };
2451
2926
  const [L1, a1, b1] = this.in("lab").toArray(coordsOptions);
2452
- 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);
2453
2928
  const \u03C0 = Math.PI, d2r = \u03C0 / 180, r2d = 180 / \u03C0;
2454
2929
  const C1 = Math.sqrt(a1 ** 2 + b1 ** 2);
2455
2930
  const C2 = Math.sqrt(a2 ** 2 + b2 ** 2);
@@ -2487,7 +2962,7 @@ var Saturon = (() => {
2487
2962
  const Cdash = (Cdash1 + Cdash2) / 2;
2488
2963
  const Cdash7 = Math.pow(Cdash, 7);
2489
2964
  const hsum = h1 + h2;
2490
- let hdash = 0;
2965
+ let hdash;
2491
2966
  if (Cdash1 === 0 && Cdash2 === 0) {
2492
2967
  hdash = hsum;
2493
2968
  } else if (habs <= 180) {
@@ -2518,8 +2993,8 @@ var Saturon = (() => {
2518
2993
  /**
2519
2994
  * Checks numeric equality with another color within a tolerance.
2520
2995
  *
2521
- * @param other - Color or string to compare.
2522
- * @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"`).
2523
2998
  * @returns `true` if equal within tolerance.
2524
2999
  *
2525
3000
  * @remarks
@@ -2533,24 +3008,23 @@ var Saturon = (() => {
2533
3008
  * - {@link deltaE94} (weighted improvements over LAB)
2534
3009
  * - {@link deltaE2000} (most accurate, accounts for perceptual interactions)
2535
3010
  */
2536
- equals(other, epsilon = 1e-5) {
2537
- const o = typeof other === "string" ? _Color.from(other) : other;
3011
+ equals(other, epsilon = EPSILON) {
2538
3012
  const thisCoords = this.toArray({ fit: "none", precision: null });
2539
- const otherCoords = o.toArray({ fit: "none", precision: null });
2540
- if (o.model === this.model)
3013
+ const otherCoords = other.toArray({ fit: "none", precision: null });
3014
+ if (other.model === this.model)
2541
3015
  return thisCoords.every((v, i) => Math.abs(v - otherCoords[i]) <= epsilon);
2542
3016
  const a = this.in("xyz-d65").toArray({ fit: "none", precision: null });
2543
- const b = o.in("xyz-d65").toArray({ fit: "none", precision: null });
3017
+ const b = other.in("xyz-d65").toArray({ fit: "none", precision: null });
2544
3018
  return a.every((v, i) => Math.abs(v - b[i]) <= epsilon);
2545
3019
  }
2546
3020
  /**
2547
3021
  * Determines whether this color lies within a given gamut.
2548
3022
  *
2549
3023
  * @param gamut - Target color space.
2550
- * @param epsilon - Floating-point tolerance (default: 1e-5).
3024
+ * @param epsilon - Floating-point tolerance (defaults to the value of `EPSILON` in `"saturon/math"`).
2551
3025
  * @returns `true` if inside gamut, else `false`.
2552
3026
  */
2553
- inGamut(gamut, epsilon = 1e-5) {
3027
+ inGamut(gamut, epsilon = EPSILON) {
2554
3028
  const g = gamut.trim().toLowerCase();
2555
3029
  if (!(g in colorSpaces))
2556
3030
  throw new Error(`Unsupported color gamut: '${g}'.`);
@@ -2560,8 +3034,8 @@ var Saturon = (() => {
2560
3034
  const coords = this.in(g).toArray({ fit: "none", precision: null });
2561
3035
  return Object.values(components).every(({ index, value }) => {
2562
3036
  const v = coords[index];
2563
- const [min, max] = Array.isArray(value) ? value : value === "hue" ? [0, 360] : [0, 100];
2564
- 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;
2565
3039
  });
2566
3040
  }
2567
3041
  };