saturon 0.2.2 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +1 -1
- package/README.md +5 -4
- package/dist/Color.d.ts +58 -46
- package/dist/Color.js +144 -221
- package/dist/converters.d.ts +39 -145
- package/dist/converters.js +12 -432
- package/dist/engine.d.ts +148 -0
- package/dist/engine.js +1900 -0
- package/dist/index.umd.js +1800 -1312
- package/dist/index.umd.min.js +143 -1
- package/dist/math.d.ts +3 -12
- package/dist/math.js +12 -21
- package/dist/tests/Color.test.d.ts +17 -0
- package/dist/tests/Color.test.js +1062 -0
- package/dist/tests/wpt.test.d.ts +1 -0
- package/dist/tests/wpt.test.js +2129 -0
- package/dist/types.d.ts +66 -76
- package/dist/utils.d.ts +10 -141
- package/dist/utils.js +111 -1059
- package/package.json +10 -11
package/dist/engine.js
ADDED
|
@@ -0,0 +1,1900 @@
|
|
|
1
|
+
import { Color } from "./Color.js";
|
|
2
|
+
import { config, systemColors } from "./config.js";
|
|
3
|
+
import { colorModels, colorSpaces, namedColors } from "./converters.js";
|
|
4
|
+
import { fitMethods } from "./math.js";
|
|
5
|
+
import { cache, fit, normalize, plugins, spaceConverterToModelConverter } from "./utils.js";
|
|
6
|
+
export const grammar = `
|
|
7
|
+
/* ==========================================================================
|
|
8
|
+
Core Color Types
|
|
9
|
+
========================================================================== */
|
|
10
|
+
<color> = <color-base> | currentColor | <system-color> | <contrast-color()> | <device-cmyk()> | <light-dark-color>
|
|
11
|
+
|
|
12
|
+
<color-base> = <hex-color> | <color-function> | <named-color> | <color-mix()> | transparent
|
|
13
|
+
|
|
14
|
+
<color-function> = <rgb()> | <rgba()> | <hsl()> | <hsla()> | <hwb()> | <lab()> | <lch()> | <oklab()> | <oklch()> | <alpha()> | <color()> | <custom-color-function>
|
|
15
|
+
|
|
16
|
+
/* ==========================================================================
|
|
17
|
+
RGB & RGBA Syntax
|
|
18
|
+
========================================================================== */
|
|
19
|
+
<rgb()> = [ <legacy-rgb-syntax> | <modern-rgb-syntax> | <relative-rgb-syntax> ]
|
|
20
|
+
<rgba()> = [ <legacy-rgba-syntax> | <modern-rgba-syntax> | <relative-rgba-syntax> ]
|
|
21
|
+
|
|
22
|
+
<legacy-rgb-syntax> = rgb( <percentage>#{3} [ , <alpha-value> ]? ) | rgb( <number>#{3} [ , <alpha-value> ]? )
|
|
23
|
+
<legacy-rgba-syntax> = rgba( <percentage>#{3} [ , <alpha-value> ]? ) | rgba( <number>#{3} [ , <alpha-value> ]? )
|
|
24
|
+
|
|
25
|
+
<modern-rgb-syntax> = rgb( <rgb-absolute-component>{3} [ / <alpha-absolute-value> ]? )
|
|
26
|
+
<modern-rgba-syntax> = rgba( <rgb-absolute-component>{3} [ / <alpha-absolute-value> ]? )
|
|
27
|
+
<rgb-absolute-component> = <number> | <percentage> | none
|
|
28
|
+
<alpha-absolute-value> = <alpha-value> | none
|
|
29
|
+
|
|
30
|
+
<relative-rgb-syntax> = rgb( from <color> <rgb-relative-component>{3} [ / <rgb-relative-alpha> ]? )
|
|
31
|
+
<relative-rgba-syntax> = rgba( from <color> <rgb-relative-component>{3} [ / <rgb-relative-alpha> ]? )
|
|
32
|
+
|
|
33
|
+
/* ==========================================================================
|
|
34
|
+
HSL & HSLA Syntax
|
|
35
|
+
========================================================================== */
|
|
36
|
+
<hsl()> = [ <legacy-hsl-syntax> | <modern-hsl-syntax> | <relative-hsl-syntax> ]
|
|
37
|
+
<hsla()> = [ <legacy-hsla-syntax> | <modern-hsla-syntax> | <relative-hsla-syntax> ]
|
|
38
|
+
|
|
39
|
+
<legacy-hsl-syntax> = hsl( <hue> , <percentage> , <percentage> [ , <alpha-value> ]? )
|
|
40
|
+
<legacy-hsla-syntax> = hsla( <hue> , <percentage> , <percentage> [ , <alpha-value> ]? )
|
|
41
|
+
|
|
42
|
+
<modern-hsl-syntax> = hsl( [ <hue> | none ] [ <percentage> | <number> | none ]{2} [ / <alpha-absolute-value> ]? )
|
|
43
|
+
<modern-hsla-syntax> = hsla( [ <hue> | none ] [ <percentage> | <number> | none ]{2} [ / <alpha-absolute-value> ]? )
|
|
44
|
+
|
|
45
|
+
<relative-hsl-syntax> = hsl( from <color> <hsl-relative-hue> <hsl-relative-component>{2} [ / <hsl-relative-alpha> ]? )
|
|
46
|
+
<relative-hsla-syntax> = hsla( from <color> <hsl-relative-hue> <hsl-relative-component>{2} [ / <hsl-relative-alpha> ]? )
|
|
47
|
+
<hsl-relative-hue> = <hue> | none | h | s | l | alpha
|
|
48
|
+
<hsl-relative-component> = <percentage> | <number> | none | h | s | l | alpha
|
|
49
|
+
<hsl-relative-alpha> = <alpha-value> | none | h | s | l | alpha
|
|
50
|
+
|
|
51
|
+
/* ==========================================================================
|
|
52
|
+
HWB Syntax
|
|
53
|
+
========================================================================== */
|
|
54
|
+
<hwb()> = [ <absolute-hwb-syntax> | <relative-hwb-syntax> ]
|
|
55
|
+
<absolute-hwb-syntax> = hwb( [ <hue> | none ] [ <percentage> | <number> | none ]{2} [ / <alpha-absolute-value> ]? )
|
|
56
|
+
<relative-hwb-syntax> = hwb( from <color> <hwb-relative-hue> <hwb-relative-component>{2} [ / <hwb-relative-alpha> ]? )
|
|
57
|
+
<hwb-relative-hue> = <hue> | none | h | w | b | alpha
|
|
58
|
+
<hwb-relative-component> = <percentage> | <number> | none | h | w | b | alpha
|
|
59
|
+
<hwb-relative-alpha> = <alpha-value> | none | h | w | b | alpha
|
|
60
|
+
|
|
61
|
+
/* ==========================================================================
|
|
62
|
+
LAB & LCH Syntax
|
|
63
|
+
========================================================================== */
|
|
64
|
+
<lab()> = [ <absolute-lab-syntax> | <relative-lab-syntax> ]
|
|
65
|
+
<absolute-lab-syntax> = lab( [ <percentage> | <number> | none ]{3} [ / <alpha-absolute-value> ]? )
|
|
66
|
+
<relative-lab-syntax> = lab( from <color> <lab-relative-component>{3} [ / <lab-relative-alpha> ]? )
|
|
67
|
+
<lab-relative-component> = <percentage> | <number> | none | l | a | b | alpha
|
|
68
|
+
<lab-relative-alpha> = <alpha-value> | none | l | a | b | alpha
|
|
69
|
+
|
|
70
|
+
<lch()> = [ <absolute-lch-syntax> | <relative-lch-syntax> ]
|
|
71
|
+
<absolute-lch-syntax> = lch( [ <percentage> | <number> | none ]{2} [ <hue> | none ] [ / <alpha-absolute-value> ]? )
|
|
72
|
+
<relative-lch-syntax> = lch( from <color> <lch-relative-component>{2} <lch-relative-hue> [ / <lch-relative-alpha> ]? )
|
|
73
|
+
<lch-relative-component> = <percentage> | <number> | none | l | c | h | alpha
|
|
74
|
+
<lch-relative-hue> = <hue> | none | l | c | h | alpha
|
|
75
|
+
<lch-relative-alpha> = <alpha-value> | none | l | c | h | alpha
|
|
76
|
+
|
|
77
|
+
/* ==========================================================================
|
|
78
|
+
OKLAB & OKLCH Syntax
|
|
79
|
+
========================================================================== */
|
|
80
|
+
<oklab()> = [ <absolute-oklab-syntax> | <relative-oklab-syntax> ]
|
|
81
|
+
<absolute-oklab-syntax> = oklab( [ <percentage> | <number> | none ]{3} [ / <alpha-absolute-value> ]? )
|
|
82
|
+
<relative-oklab-syntax> = oklab( from <color> <oklab-relative-component>{3} [ / <oklab-relative-alpha> ]? )
|
|
83
|
+
<oklab-relative-component> = <percentage> | <number> | none | l | a | b | alpha
|
|
84
|
+
<oklab-relative-alpha> = <alpha-value> | none | l | a | b | alpha
|
|
85
|
+
|
|
86
|
+
<oklch()> = [ <absolute-oklch-syntax> | <relative-oklch-syntax> ]
|
|
87
|
+
<absolute-oklch-syntax> = oklch( [ <percentage> | <number> | none ]{2} [ <hue> | none ] [ / <alpha-absolute-value> ]? )
|
|
88
|
+
<relative-oklch-syntax> = oklch( from <color> <oklch-relative-component>{2} <oklch-relative-hue> [ / <oklch-relative-alpha> ]? )
|
|
89
|
+
<oklch-relative-component> = <percentage> | <number> | none | l | c | h | alpha
|
|
90
|
+
<oklch-relative-hue> = <hue> | none | l | c | h | alpha
|
|
91
|
+
<oklch-relative-alpha> = <alpha-value> | none | l | c | h | alpha
|
|
92
|
+
|
|
93
|
+
/* ==========================================================================
|
|
94
|
+
ALPHA Syntax
|
|
95
|
+
========================================================================== */
|
|
96
|
+
<alpha()> = alpha( from <color> [ / [ <alpha-value> | none | alpha ] ]? )
|
|
97
|
+
|
|
98
|
+
/* ==========================================================================
|
|
99
|
+
COLOR Space Function Syntax
|
|
100
|
+
========================================================================== */
|
|
101
|
+
<color()> = [ <absolute-color-syntax> | <relative-color-syntax> ]
|
|
102
|
+
<absolute-color-syntax> = color( [ <predefined-rgb> | <xyz-space> | <custom-color-space> ] [ <number> | <percentage> | none ]{3} [ / [ <alpha-value> | none ] ]? )
|
|
103
|
+
<relative-color-syntax> = [ <relative-rgb-color-syntax> | <relative-xyz-color-syntax> | <relative-custom-color-syntax> ]
|
|
104
|
+
|
|
105
|
+
<relative-rgb-color-syntax> = color( from <color> <predefined-rgb> <rgb-relative-component>{3} [ / <rgb-relative-alpha> ]? )
|
|
106
|
+
|
|
107
|
+
<relative-xyz-color-syntax> = color( from <color> <xyz-space> <xyz-relative-component>{3} [ / <xyz-relative-alpha> ]? )
|
|
108
|
+
<xyz-relative-component> = <number> | <percentage> | none | x | y | z | alpha
|
|
109
|
+
<xyz-relative-alpha> = <alpha-value> | none | x | y | z | alpha
|
|
110
|
+
|
|
111
|
+
<predefined-rgb> = srgb | srgb-linear | display-p3 | a98-rgb | prophoto-rgb | rec2020
|
|
112
|
+
<xyz-space> = xyz | xyz-d50 | xyz-d65
|
|
113
|
+
|
|
114
|
+
<relative-custom-color-syntax> = %unregistered-sentinal-value%
|
|
115
|
+
|
|
116
|
+
/* ==========================================================================
|
|
117
|
+
Functional Utilities (Light/Dark, Contrast, CMYK, Mix)
|
|
118
|
+
========================================================================== */
|
|
119
|
+
<light-dark-color> = light-dark( <color> , <color> )
|
|
120
|
+
|
|
121
|
+
<contrast-color()> = contrast-color( <color> )
|
|
122
|
+
|
|
123
|
+
<device-cmyk()> = <legacy-device-cmyk-syntax> | <modern-device-cmyk-syntax>
|
|
124
|
+
<legacy-device-cmyk-syntax> = device-cmyk( <number>#{4} )
|
|
125
|
+
<modern-device-cmyk-syntax> = device-cmyk( <cmyk-component>{4} [ / [ <alpha-value> | none ] ]? )
|
|
126
|
+
<cmyk-component> = <number> | <percentage> | none
|
|
127
|
+
|
|
128
|
+
<color-mix()> = color-mix( [ <color-interpolation-method> , ]? [ <color> && <percentage [0,100]>? ]# )
|
|
129
|
+
<color-interpolation-method> = in [ <rectangular-color-space> | <polar-color-space> <hue-interpolation-method>? ]
|
|
130
|
+
<color-space> = <rectangular-color-space> | <polar-color-space>
|
|
131
|
+
<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>
|
|
132
|
+
<polar-color-space> = hsl | hwb | lch | oklch | <custom-polar-space>
|
|
133
|
+
<hue-interpolation-method> = [ shorter | longer | increasing | decreasing ] hue
|
|
134
|
+
|
|
135
|
+
<custom-color-function> = %unregistered-sentinel-value%
|
|
136
|
+
<custom-rectangular-space> = %unregistered-sentinel-value%
|
|
137
|
+
<custom-polar-space> = %unregistered-sentinel-value%
|
|
138
|
+
|
|
139
|
+
/* ==========================================================================
|
|
140
|
+
Shared Value Components
|
|
141
|
+
========================================================================== */
|
|
142
|
+
<alpha-value> = <number> | <percentage>
|
|
143
|
+
<hue> = <number> | <angle>
|
|
144
|
+
<rgb-relative-component> = <number> | <percentage> | none | r | g | b | alpha
|
|
145
|
+
<rgb-relative-alpha> = <alpha-value> | none | r | g | b | alpha
|
|
146
|
+
<custom-color-space> = %unregistered-sentinel-value%
|
|
147
|
+
`;
|
|
148
|
+
const RULE_START_RE = /^<[^>]+>\s*=/;
|
|
149
|
+
const HASH_REPEAT_RE = /^(.*)#\{(\d+)\}$/;
|
|
150
|
+
const REPEAT_RE = /^(.*)\{(\d+)\}$/;
|
|
151
|
+
const REF_RE = /^<([^>\s]+)(?:\s+([^>]+))?>$/;
|
|
152
|
+
const ARG_RE = /\[[^\]]+\]|[^\s]+/g;
|
|
153
|
+
export const grammarMap = parseGrammar(grammar);
|
|
154
|
+
function concatNodes(a, b) {
|
|
155
|
+
const lenA = a.length;
|
|
156
|
+
const lenB = b.length;
|
|
157
|
+
const result = new Array(lenA + lenB);
|
|
158
|
+
for (let i = 0; i < lenA; i++)
|
|
159
|
+
result[i] = a[i];
|
|
160
|
+
for (let i = 0; i < lenB; i++)
|
|
161
|
+
result[lenA + i] = b[i];
|
|
162
|
+
return result;
|
|
163
|
+
}
|
|
164
|
+
function parseCmykComponent(n) {
|
|
165
|
+
if (!n?.value)
|
|
166
|
+
return 0;
|
|
167
|
+
const raw = Array.isArray(n.value) ? n.value[0]?.value : n.value;
|
|
168
|
+
return parseFloat(raw.trim());
|
|
169
|
+
}
|
|
170
|
+
function getLiteralValue(n) {
|
|
171
|
+
if (!n)
|
|
172
|
+
return "";
|
|
173
|
+
if (n.type === "literal")
|
|
174
|
+
return n.value;
|
|
175
|
+
if (Array.isArray(n.value))
|
|
176
|
+
return getLiteralValue(n.value[0]);
|
|
177
|
+
return "";
|
|
178
|
+
}
|
|
179
|
+
export function validateTokens(tokens, rootRule) {
|
|
180
|
+
const memo = new Map();
|
|
181
|
+
const rulesToTest = rootRule ? [rootRule] : Object.keys(grammarMap);
|
|
182
|
+
for (let i = 0; i < rulesToTest.length; i++) {
|
|
183
|
+
const ruleName = rulesToTest[i];
|
|
184
|
+
const results = matchRule(ruleName, tokens, 0, grammarMap, memo, null);
|
|
185
|
+
for (let j = 0; j < results.length; j++) {
|
|
186
|
+
const result = results[j];
|
|
187
|
+
if (result.success && result.nextIndex === tokens.length) {
|
|
188
|
+
return result.nodes[0];
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
throw new Error("Invalid syntax");
|
|
193
|
+
}
|
|
194
|
+
export function matchRule(ruleName, tokens, index, grammar, memo, currentFunction, args) {
|
|
195
|
+
const key = `${ruleName}:${index}:${currentFunction || ""}:${args ? args.join(",") : ""}`;
|
|
196
|
+
const cached = memo.get(key);
|
|
197
|
+
if (cached)
|
|
198
|
+
return cached;
|
|
199
|
+
const token = tokens[index];
|
|
200
|
+
const validator = validators[ruleName];
|
|
201
|
+
if (validator) {
|
|
202
|
+
if (!token)
|
|
203
|
+
return [];
|
|
204
|
+
if (validator(token, args)) {
|
|
205
|
+
const result = [
|
|
206
|
+
{
|
|
207
|
+
success: true,
|
|
208
|
+
nextIndex: index + 1,
|
|
209
|
+
nodes: [{ type: ruleName, value: token }],
|
|
210
|
+
},
|
|
211
|
+
];
|
|
212
|
+
memo.set(key, result);
|
|
213
|
+
return result;
|
|
214
|
+
}
|
|
215
|
+
memo.set(key, []);
|
|
216
|
+
return [];
|
|
217
|
+
}
|
|
218
|
+
const rule = grammar[ruleName];
|
|
219
|
+
if (!rule) {
|
|
220
|
+
memo.set(key, []);
|
|
221
|
+
return [];
|
|
222
|
+
}
|
|
223
|
+
const results = matchNode(rule, tokens, index, grammar, memo, currentFunction);
|
|
224
|
+
const wrapped = new Array(results.length);
|
|
225
|
+
for (let i = 0; i < results.length; i++) {
|
|
226
|
+
wrapped[i] = {
|
|
227
|
+
success: true,
|
|
228
|
+
nextIndex: results[i].nextIndex,
|
|
229
|
+
nodes: [{ type: ruleName, value: results[i].nodes }],
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
memo.set(key, wrapped);
|
|
233
|
+
return wrapped;
|
|
234
|
+
}
|
|
235
|
+
export function matchNode(node, tokens, index, grammar, memo, currentFunction) {
|
|
236
|
+
switch (node.kind) {
|
|
237
|
+
case "literal": {
|
|
238
|
+
const token = tokens[index];
|
|
239
|
+
if (!token || token !== node.value.toLowerCase())
|
|
240
|
+
return [];
|
|
241
|
+
return [
|
|
242
|
+
{
|
|
243
|
+
success: true,
|
|
244
|
+
nextIndex: index + 1,
|
|
245
|
+
nodes: [
|
|
246
|
+
{
|
|
247
|
+
type: token === "(" || token === ")" || token === "," || token === "/" ? "symbol" : "literal",
|
|
248
|
+
value: token,
|
|
249
|
+
},
|
|
250
|
+
],
|
|
251
|
+
},
|
|
252
|
+
];
|
|
253
|
+
}
|
|
254
|
+
case "ref": {
|
|
255
|
+
const token = tokens[index];
|
|
256
|
+
if (!token)
|
|
257
|
+
return [];
|
|
258
|
+
if (currentFunction && colorModels[currentFunction]) {
|
|
259
|
+
const component = colorModels[currentFunction].components[token];
|
|
260
|
+
if (component && component.value === node.name) {
|
|
261
|
+
return [
|
|
262
|
+
{
|
|
263
|
+
success: true,
|
|
264
|
+
nextIndex: index + 1,
|
|
265
|
+
nodes: [{ type: node.name, value: token }],
|
|
266
|
+
},
|
|
267
|
+
];
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
return matchRule(node.name, tokens, index, grammar, memo, currentFunction, node.args);
|
|
271
|
+
}
|
|
272
|
+
case "choice": {
|
|
273
|
+
const results = [];
|
|
274
|
+
for (let i = 0; i < node.nodes.length; i++) {
|
|
275
|
+
const childMatches = matchNode(node.nodes[i], tokens, index, grammar, memo, currentFunction);
|
|
276
|
+
for (let j = 0; j < childMatches.length; j++) {
|
|
277
|
+
results.push(childMatches[j]);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
return results;
|
|
281
|
+
}
|
|
282
|
+
case "sequence": {
|
|
283
|
+
let states = [{ success: true, nextIndex: index, nodes: [] }];
|
|
284
|
+
for (let i = 0; i < node.nodes.length; i++) {
|
|
285
|
+
const child = node.nodes[i];
|
|
286
|
+
const nextStates = [];
|
|
287
|
+
for (let j = 0; j < states.length; j++) {
|
|
288
|
+
const state = states[j];
|
|
289
|
+
const matches = matchNode(child, tokens, state.nextIndex, grammar, memo, currentFunction);
|
|
290
|
+
for (let k = 0; k < matches.length; k++) {
|
|
291
|
+
nextStates.push({
|
|
292
|
+
success: true,
|
|
293
|
+
nextIndex: matches[k].nextIndex,
|
|
294
|
+
nodes: concatNodes(state.nodes, matches[k].nodes),
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
states = nextStates;
|
|
299
|
+
if (states.length === 0)
|
|
300
|
+
return [];
|
|
301
|
+
}
|
|
302
|
+
return states;
|
|
303
|
+
}
|
|
304
|
+
case "optional": {
|
|
305
|
+
const matches = matchNode(node.node, tokens, index, grammar, memo, currentFunction);
|
|
306
|
+
const results = new Array(matches.length + 1);
|
|
307
|
+
results[0] = { success: true, nextIndex: index, nodes: [] };
|
|
308
|
+
for (let i = 0; i < matches.length; i++) {
|
|
309
|
+
results[i + 1] = matches[i];
|
|
310
|
+
}
|
|
311
|
+
return results;
|
|
312
|
+
}
|
|
313
|
+
case "repeat": {
|
|
314
|
+
let states = [{ success: true, nextIndex: index, nodes: [] }];
|
|
315
|
+
for (let i = 0; i < node.min; i++) {
|
|
316
|
+
const nextStates = [];
|
|
317
|
+
for (let j = 0; j < states.length; j++) {
|
|
318
|
+
const state = states[j];
|
|
319
|
+
const matches = matchNode(node.node, tokens, state.nextIndex, grammar, memo, currentFunction);
|
|
320
|
+
for (let k = 0; k < matches.length; k++) {
|
|
321
|
+
nextStates.push({
|
|
322
|
+
success: true,
|
|
323
|
+
nextIndex: matches[k].nextIndex,
|
|
324
|
+
nodes: concatNodes(state.nodes, matches[k].nodes),
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
states = nextStates;
|
|
329
|
+
if (states.length === 0)
|
|
330
|
+
return [];
|
|
331
|
+
}
|
|
332
|
+
return states;
|
|
333
|
+
}
|
|
334
|
+
case "permutation": {
|
|
335
|
+
const numNodes = node.nodes.length;
|
|
336
|
+
const fullMask = (1 << numNodes) - 1;
|
|
337
|
+
let states = [{ nextIndex: index, nodes: [], remainingMask: fullMask }];
|
|
338
|
+
const finalResults = [];
|
|
339
|
+
while (states.length > 0) {
|
|
340
|
+
const nextStates = [];
|
|
341
|
+
for (let i = 0; i < states.length; i++) {
|
|
342
|
+
const state = states[i];
|
|
343
|
+
let allRemainingOptional = true;
|
|
344
|
+
for (let bit = 0; bit < numNodes; bit++) {
|
|
345
|
+
if ((state.remainingMask & (1 << bit)) !== 0) {
|
|
346
|
+
if (node.nodes[bit].kind !== "optional") {
|
|
347
|
+
allRemainingOptional = false;
|
|
348
|
+
break;
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
if (allRemainingOptional) {
|
|
353
|
+
finalResults.push({
|
|
354
|
+
success: true,
|
|
355
|
+
nextIndex: state.nextIndex,
|
|
356
|
+
nodes: state.nodes,
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
for (let bit = 0; bit < numNodes; bit++) {
|
|
360
|
+
if ((state.remainingMask & (1 << bit)) !== 0) {
|
|
361
|
+
const child = node.nodes[bit];
|
|
362
|
+
const nodeToMatch = child.kind === "optional" ? child.node : child;
|
|
363
|
+
const matches = matchNode(nodeToMatch, tokens, state.nextIndex, grammar, memo, currentFunction);
|
|
364
|
+
const nextMask = state.remainingMask & ~(1 << bit);
|
|
365
|
+
for (let m = 0; m < matches.length; m++) {
|
|
366
|
+
nextStates.push({
|
|
367
|
+
nextIndex: matches[m].nextIndex,
|
|
368
|
+
nodes: concatNodes(state.nodes, matches[m].nodes),
|
|
369
|
+
remainingMask: nextMask,
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
states = nextStates;
|
|
376
|
+
}
|
|
377
|
+
return finalResults;
|
|
378
|
+
}
|
|
379
|
+
case "hashList": {
|
|
380
|
+
let currentStates = matchNode(node.node, tokens, index, grammar, memo, currentFunction);
|
|
381
|
+
const allSuccessfulStates = [];
|
|
382
|
+
while (currentStates.length > 0) {
|
|
383
|
+
const nextStates = [];
|
|
384
|
+
for (let i = 0; i < currentStates.length; i++) {
|
|
385
|
+
const state = currentStates[i];
|
|
386
|
+
allSuccessfulStates.push(state);
|
|
387
|
+
const commaIdx = state.nextIndex;
|
|
388
|
+
if (commaIdx < tokens.length && tokens[commaIdx] === ",") {
|
|
389
|
+
const commaNode = { type: "symbol", value: "," };
|
|
390
|
+
const matches = matchNode(node.node, tokens, commaIdx + 1, grammar, memo, currentFunction);
|
|
391
|
+
for (let m = 0; m < matches.length; m++) {
|
|
392
|
+
const match = matches[m];
|
|
393
|
+
if (match.nextIndex > commaIdx) {
|
|
394
|
+
const newNodes = new Array(state.nodes.length + 1 + match.nodes.length);
|
|
395
|
+
let idx = 0;
|
|
396
|
+
for (let n = 0; n < state.nodes.length; n++)
|
|
397
|
+
newNodes[idx++] = state.nodes[n];
|
|
398
|
+
newNodes[idx++] = commaNode;
|
|
399
|
+
for (let n = 0; n < match.nodes.length; n++)
|
|
400
|
+
newNodes[idx++] = match.nodes[n];
|
|
401
|
+
nextStates.push({
|
|
402
|
+
success: true,
|
|
403
|
+
nextIndex: match.nextIndex,
|
|
404
|
+
nodes: newNodes,
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
currentStates = nextStates;
|
|
411
|
+
}
|
|
412
|
+
return allSuccessfulStates;
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
export function tokenize(input) {
|
|
417
|
+
const lower = input.toLowerCase();
|
|
418
|
+
const tokens = [];
|
|
419
|
+
let i = 0;
|
|
420
|
+
while (i < lower.length) {
|
|
421
|
+
if (lower.slice(i, i + 2) === "/*") {
|
|
422
|
+
const endIndex = lower.indexOf("*/", i + 2);
|
|
423
|
+
if (endIndex !== -1) {
|
|
424
|
+
i = endIndex + 2;
|
|
425
|
+
}
|
|
426
|
+
else {
|
|
427
|
+
i = lower.length;
|
|
428
|
+
}
|
|
429
|
+
continue;
|
|
430
|
+
}
|
|
431
|
+
const ch = lower[i];
|
|
432
|
+
if (/\s/.test(ch)) {
|
|
433
|
+
i++;
|
|
434
|
+
continue;
|
|
435
|
+
}
|
|
436
|
+
if (lower.slice(i, i + 5) === "calc(") {
|
|
437
|
+
const start = i;
|
|
438
|
+
let parenCount = 0;
|
|
439
|
+
while (i < lower.length) {
|
|
440
|
+
if (lower[i] === "(")
|
|
441
|
+
parenCount++;
|
|
442
|
+
else if (lower[i] === ")") {
|
|
443
|
+
parenCount--;
|
|
444
|
+
if (parenCount === 0) {
|
|
445
|
+
i++;
|
|
446
|
+
break;
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
i++;
|
|
450
|
+
}
|
|
451
|
+
tokens.push(lower.slice(start, i));
|
|
452
|
+
continue;
|
|
453
|
+
}
|
|
454
|
+
if ("(),/".includes(ch)) {
|
|
455
|
+
tokens.push(ch);
|
|
456
|
+
i++;
|
|
457
|
+
continue;
|
|
458
|
+
}
|
|
459
|
+
const start = i;
|
|
460
|
+
while (i < lower.length && !/\s/.test(lower[i]) && !"(),/".includes(lower[i])) {
|
|
461
|
+
i++;
|
|
462
|
+
}
|
|
463
|
+
tokens.push(lower.slice(start, i));
|
|
464
|
+
}
|
|
465
|
+
return tokens;
|
|
466
|
+
}
|
|
467
|
+
export function parseGrammar(grammar) {
|
|
468
|
+
const rules = {};
|
|
469
|
+
const lines = grammar.replace(/\/\*[\s\S]*?\*\//g, "").split("\n");
|
|
470
|
+
let currentRuleName = "";
|
|
471
|
+
let currentDef = "";
|
|
472
|
+
for (let i = 0; i < lines.length; i++) {
|
|
473
|
+
const line = lines[i].trim();
|
|
474
|
+
if (!line)
|
|
475
|
+
continue;
|
|
476
|
+
if (RULE_START_RE.test(line)) {
|
|
477
|
+
if (currentRuleName)
|
|
478
|
+
rules[currentRuleName] = parseExpression(currentDef);
|
|
479
|
+
const eqIndex = line.indexOf("=");
|
|
480
|
+
currentRuleName = line.slice(0, eqIndex).trim();
|
|
481
|
+
currentDef = line.slice(eqIndex + 1).trim();
|
|
482
|
+
}
|
|
483
|
+
else if (currentRuleName) {
|
|
484
|
+
currentDef += " " + line;
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
if (currentRuleName)
|
|
488
|
+
rules[currentRuleName] = parseExpression(currentDef);
|
|
489
|
+
return rules;
|
|
490
|
+
}
|
|
491
|
+
export function parseExpression(expr) {
|
|
492
|
+
const parts = splitTopLevel(expr, "|");
|
|
493
|
+
if (parts.length > 1)
|
|
494
|
+
return { kind: "choice", nodes: parts.map(parsePermutation) };
|
|
495
|
+
return parsePermutation(expr);
|
|
496
|
+
}
|
|
497
|
+
function splitTopLevel(str, separator) {
|
|
498
|
+
const result = [];
|
|
499
|
+
let depth = 0;
|
|
500
|
+
let start = 0;
|
|
501
|
+
const sepLen = separator.length;
|
|
502
|
+
const len = str.length;
|
|
503
|
+
for (let i = 0; i < len; i++) {
|
|
504
|
+
const ch = str.charCodeAt(i);
|
|
505
|
+
if (ch === 91 || ch === 40) {
|
|
506
|
+
// '[' or '('
|
|
507
|
+
depth++;
|
|
508
|
+
}
|
|
509
|
+
else if (ch === 93 || ch === 41) {
|
|
510
|
+
// ']' or ')'
|
|
511
|
+
depth--;
|
|
512
|
+
}
|
|
513
|
+
else if (depth === 0 && str.startsWith(separator, i)) {
|
|
514
|
+
const part = str.slice(start, i).trim();
|
|
515
|
+
if (part)
|
|
516
|
+
result.push(part);
|
|
517
|
+
i += sepLen - 1;
|
|
518
|
+
start = i + 1;
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
const last = str.slice(start).trim();
|
|
522
|
+
if (last)
|
|
523
|
+
result.push(last);
|
|
524
|
+
return result;
|
|
525
|
+
}
|
|
526
|
+
function splitSequence(str) {
|
|
527
|
+
const result = [];
|
|
528
|
+
let i = 0;
|
|
529
|
+
const len = str.length;
|
|
530
|
+
while (i < len) {
|
|
531
|
+
const ch = str.charCodeAt(i);
|
|
532
|
+
// space (32), tab (9), newline (10), carriage return (13)
|
|
533
|
+
if (ch === 32 || ch === 9 || ch === 10 || ch === 13) {
|
|
534
|
+
i++;
|
|
535
|
+
continue;
|
|
536
|
+
}
|
|
537
|
+
// '(', ')', ',', '/', '|' -> 40, 41, 44, 47, 124
|
|
538
|
+
if (ch === 40 || ch === 41 || ch === 44 || ch === 47 || ch === 124) {
|
|
539
|
+
result.push(str[i]);
|
|
540
|
+
i++;
|
|
541
|
+
continue;
|
|
542
|
+
}
|
|
543
|
+
const start = i;
|
|
544
|
+
if (ch === 60) {
|
|
545
|
+
// '<'
|
|
546
|
+
while (i < len && str.charCodeAt(i) !== 62)
|
|
547
|
+
i++; // '>'
|
|
548
|
+
i++;
|
|
549
|
+
while (i < len) {
|
|
550
|
+
const c = str.charCodeAt(i);
|
|
551
|
+
if (c === 32 ||
|
|
552
|
+
c === 9 ||
|
|
553
|
+
c === 10 ||
|
|
554
|
+
c === 13 ||
|
|
555
|
+
c === 40 ||
|
|
556
|
+
c === 41 ||
|
|
557
|
+
c === 44 ||
|
|
558
|
+
c === 47 ||
|
|
559
|
+
c === 124)
|
|
560
|
+
break;
|
|
561
|
+
i++;
|
|
562
|
+
}
|
|
563
|
+
result.push(str.slice(start, i));
|
|
564
|
+
continue;
|
|
565
|
+
}
|
|
566
|
+
if (ch === 91) {
|
|
567
|
+
// '['
|
|
568
|
+
let depth = 1;
|
|
569
|
+
i++;
|
|
570
|
+
while (i < len && depth > 0) {
|
|
571
|
+
const c = str.charCodeAt(i);
|
|
572
|
+
if (c === 91)
|
|
573
|
+
depth++; // '['
|
|
574
|
+
else if (c === 93)
|
|
575
|
+
depth--; // ']'
|
|
576
|
+
i++;
|
|
577
|
+
}
|
|
578
|
+
while (i < len) {
|
|
579
|
+
const c = str.charCodeAt(i);
|
|
580
|
+
if (c === 32 ||
|
|
581
|
+
c === 9 ||
|
|
582
|
+
c === 10 ||
|
|
583
|
+
c === 13 ||
|
|
584
|
+
c === 40 ||
|
|
585
|
+
c === 41 ||
|
|
586
|
+
c === 44 ||
|
|
587
|
+
c === 47 ||
|
|
588
|
+
c === 124)
|
|
589
|
+
break;
|
|
590
|
+
i++;
|
|
591
|
+
}
|
|
592
|
+
result.push(str.slice(start, i));
|
|
593
|
+
continue;
|
|
594
|
+
}
|
|
595
|
+
while (i < len) {
|
|
596
|
+
const c = str.charCodeAt(i);
|
|
597
|
+
if (c === 32 ||
|
|
598
|
+
c === 9 ||
|
|
599
|
+
c === 10 ||
|
|
600
|
+
c === 13 ||
|
|
601
|
+
c === 40 ||
|
|
602
|
+
c === 41 ||
|
|
603
|
+
c === 44 ||
|
|
604
|
+
c === 47 ||
|
|
605
|
+
c === 124)
|
|
606
|
+
break;
|
|
607
|
+
i++;
|
|
608
|
+
}
|
|
609
|
+
result.push(str.slice(start, i));
|
|
610
|
+
}
|
|
611
|
+
return result;
|
|
612
|
+
}
|
|
613
|
+
function parsePermutation(targetExpr) {
|
|
614
|
+
const parts = splitTopLevel(targetExpr, "&&");
|
|
615
|
+
if (parts.length > 1)
|
|
616
|
+
return { kind: "permutation", nodes: parts.map(parseSequence) };
|
|
617
|
+
return parseSequence(targetExpr);
|
|
618
|
+
}
|
|
619
|
+
function parseSequence(targetExpr) {
|
|
620
|
+
const tokens = splitSequence(targetExpr);
|
|
621
|
+
return { kind: "sequence", nodes: tokens.map(parseTerm) };
|
|
622
|
+
}
|
|
623
|
+
function parseTerm(term) {
|
|
624
|
+
term = term.trim();
|
|
625
|
+
const len = term.length;
|
|
626
|
+
if (len === 1) {
|
|
627
|
+
const c = term.charCodeAt(0);
|
|
628
|
+
if (c === 40 || c === 41 || c === 44 || c === 47) {
|
|
629
|
+
return { kind: "literal", value: term };
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
const lastChar = term.charCodeAt(len - 1);
|
|
633
|
+
if (lastChar === 35 && term.indexOf("{") === -1) {
|
|
634
|
+
// '#'
|
|
635
|
+
return { kind: "hashList", node: parseTerm(term.slice(0, -1)) };
|
|
636
|
+
}
|
|
637
|
+
if (lastChar === 63) {
|
|
638
|
+
// '?'
|
|
639
|
+
return { kind: "optional", node: parseTerm(term.slice(0, -1)) };
|
|
640
|
+
}
|
|
641
|
+
const hashRepeatMatch = HASH_REPEAT_RE.exec(term);
|
|
642
|
+
if (hashRepeatMatch) {
|
|
643
|
+
const innerNode = parseTerm(hashRepeatMatch[1]);
|
|
644
|
+
const count = Number(hashRepeatMatch[2]);
|
|
645
|
+
const commaNode = { kind: "literal", value: "," };
|
|
646
|
+
const nodes = [];
|
|
647
|
+
for (let i = 0; i < count; i++) {
|
|
648
|
+
nodes.push(innerNode);
|
|
649
|
+
if (i < count - 1)
|
|
650
|
+
nodes.push(commaNode);
|
|
651
|
+
}
|
|
652
|
+
return { kind: "sequence", nodes };
|
|
653
|
+
}
|
|
654
|
+
const repeatMatch = REPEAT_RE.exec(term);
|
|
655
|
+
if (repeatMatch) {
|
|
656
|
+
const num = Number(repeatMatch[2]);
|
|
657
|
+
return {
|
|
658
|
+
kind: "repeat",
|
|
659
|
+
node: parseTerm(repeatMatch[1]),
|
|
660
|
+
min: num,
|
|
661
|
+
max: num,
|
|
662
|
+
};
|
|
663
|
+
}
|
|
664
|
+
const firstChar = term.charCodeAt(0);
|
|
665
|
+
if (firstChar === 91 && lastChar === 93) {
|
|
666
|
+
// '[' and ']'
|
|
667
|
+
return parseExpression(term.slice(1, -1));
|
|
668
|
+
}
|
|
669
|
+
if (firstChar === 60 && lastChar === 62) {
|
|
670
|
+
// '<' and '>'
|
|
671
|
+
const match = REF_RE.exec(term);
|
|
672
|
+
if (match) {
|
|
673
|
+
let argsArray = undefined;
|
|
674
|
+
if (match[2]) {
|
|
675
|
+
const argMatches = match[2].match(ARG_RE);
|
|
676
|
+
if (argMatches) {
|
|
677
|
+
argsArray = argMatches.map((arg) => arg.trim());
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
return { kind: "ref", name: `<${match[1]}>`, args: argsArray };
|
|
681
|
+
}
|
|
682
|
+
return { kind: "ref", name: term };
|
|
683
|
+
}
|
|
684
|
+
return { kind: "literal", value: term };
|
|
685
|
+
}
|
|
686
|
+
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;
|
|
687
|
+
const BASE_MATH_ENV = {
|
|
688
|
+
pi,
|
|
689
|
+
e,
|
|
690
|
+
tau: pi * 2,
|
|
691
|
+
pow,
|
|
692
|
+
sqrt,
|
|
693
|
+
sin,
|
|
694
|
+
cos,
|
|
695
|
+
tan,
|
|
696
|
+
asin,
|
|
697
|
+
acos,
|
|
698
|
+
atan,
|
|
699
|
+
atan2,
|
|
700
|
+
exp,
|
|
701
|
+
log,
|
|
702
|
+
log10,
|
|
703
|
+
log2,
|
|
704
|
+
abs,
|
|
705
|
+
min,
|
|
706
|
+
max,
|
|
707
|
+
hypot,
|
|
708
|
+
round,
|
|
709
|
+
ceil,
|
|
710
|
+
floor,
|
|
711
|
+
sign,
|
|
712
|
+
trunc,
|
|
713
|
+
random,
|
|
714
|
+
};
|
|
715
|
+
function extractToken(n) {
|
|
716
|
+
if (typeof n === "string")
|
|
717
|
+
return n;
|
|
718
|
+
if (n.type === "<percentage>")
|
|
719
|
+
return String(n.value || "");
|
|
720
|
+
if (Array.isArray(n.value))
|
|
721
|
+
return extractToken(n.value[0]);
|
|
722
|
+
return String(n.value || "");
|
|
723
|
+
}
|
|
724
|
+
function parsePercent(str, type, min, max) {
|
|
725
|
+
const percent = parseFloat(str);
|
|
726
|
+
if (isNaN(percent))
|
|
727
|
+
throw new Error(`Invalid percentage: '${str}'.`);
|
|
728
|
+
if (type === "percentage")
|
|
729
|
+
return percent;
|
|
730
|
+
if (min < 0 && max > 0)
|
|
731
|
+
return ((percent / 100) * (max - min)) / 2;
|
|
732
|
+
return (percent / 100) * (max - min) + min;
|
|
733
|
+
}
|
|
734
|
+
function parseHue(str) {
|
|
735
|
+
const val = parseFloat(str);
|
|
736
|
+
if (isNaN(val))
|
|
737
|
+
throw new Error(`Invalid hue: '${str}'.`);
|
|
738
|
+
if (str[str.length - 1] === "deg")
|
|
739
|
+
return val;
|
|
740
|
+
if (str[str.length - 1] === "rad")
|
|
741
|
+
return val * (180 / pi);
|
|
742
|
+
if (str[str.length - 1] === "grad")
|
|
743
|
+
return val * 0.9;
|
|
744
|
+
if (str[str.length - 1] === "turn")
|
|
745
|
+
return val * 360;
|
|
746
|
+
return val;
|
|
747
|
+
}
|
|
748
|
+
function parseCalcExpression(expr, type, base, min, max) {
|
|
749
|
+
if (expr === "infinity")
|
|
750
|
+
return Infinity;
|
|
751
|
+
if (expr === "-infinity")
|
|
752
|
+
return -Infinity;
|
|
753
|
+
if (expr === "nan")
|
|
754
|
+
return NaN;
|
|
755
|
+
const UNIT_NUM = 1; // 001
|
|
756
|
+
const UNIT_PCT = 2; // 010
|
|
757
|
+
const UNIT_ANG = 4; // 100
|
|
758
|
+
const getUnitName = (u) => (u === UNIT_ANG ? "angle" : u === UNIT_PCT ? "percent" : "number");
|
|
759
|
+
const tokenize = (s) => {
|
|
760
|
+
const out = [];
|
|
761
|
+
for (let i = 0; i < s.length;) {
|
|
762
|
+
const c = s[i];
|
|
763
|
+
if (/\s/.test(c)) {
|
|
764
|
+
i++;
|
|
765
|
+
continue;
|
|
766
|
+
}
|
|
767
|
+
if (s.slice(i, i + 2) === "**") {
|
|
768
|
+
out.push({ type: "operator", value: "**" });
|
|
769
|
+
i += 2;
|
|
770
|
+
continue;
|
|
771
|
+
}
|
|
772
|
+
if (/[0-9]/.test(c) || (c === "." && /[0-9]/.test(s[i + 1] || ""))) {
|
|
773
|
+
let numStr = "";
|
|
774
|
+
while (i < s.length && /[0-9.]/.test(s[i]))
|
|
775
|
+
numStr += s[i++];
|
|
776
|
+
if (i < s.length && /[eE]/.test(s[i])) {
|
|
777
|
+
numStr += s[i++];
|
|
778
|
+
if (/[+-]/.test(s[i]))
|
|
779
|
+
numStr += s[i++];
|
|
780
|
+
while (i < s.length && /[0-9]/.test(s[i]))
|
|
781
|
+
numStr += s[i++];
|
|
782
|
+
}
|
|
783
|
+
let unitType = UNIT_NUM;
|
|
784
|
+
let rawUnit = "";
|
|
785
|
+
if (s[i] === "%") {
|
|
786
|
+
unitType = UNIT_PCT;
|
|
787
|
+
rawUnit = "%";
|
|
788
|
+
i++;
|
|
789
|
+
}
|
|
790
|
+
else if (s.slice(i, i + 3) === "deg") {
|
|
791
|
+
unitType = UNIT_ANG;
|
|
792
|
+
rawUnit = "deg";
|
|
793
|
+
i += 3;
|
|
794
|
+
}
|
|
795
|
+
else if (s.slice(i, i + 3) === "rad") {
|
|
796
|
+
unitType = UNIT_ANG;
|
|
797
|
+
rawUnit = "rad";
|
|
798
|
+
i += 3;
|
|
799
|
+
}
|
|
800
|
+
else if (s.slice(i, i + 4) === "grad") {
|
|
801
|
+
unitType = UNIT_ANG;
|
|
802
|
+
rawUnit = "grad";
|
|
803
|
+
i += 4;
|
|
804
|
+
}
|
|
805
|
+
else if (s.slice(i, i + 4) === "turn") {
|
|
806
|
+
unitType = UNIT_ANG;
|
|
807
|
+
rawUnit = "turn";
|
|
808
|
+
i += 4;
|
|
809
|
+
}
|
|
810
|
+
let val = parseFloat(numStr);
|
|
811
|
+
if (unitType === UNIT_PCT) {
|
|
812
|
+
val = parsePercent(numStr + rawUnit, type, min, max);
|
|
813
|
+
}
|
|
814
|
+
else if (unitType === UNIT_ANG) {
|
|
815
|
+
val = parseHue(numStr + rawUnit);
|
|
816
|
+
}
|
|
817
|
+
out.push({ type: "number", value: val, unit: unitType });
|
|
818
|
+
continue;
|
|
819
|
+
}
|
|
820
|
+
if (/[a-zA-Z_]/.test(c)) {
|
|
821
|
+
let id = "";
|
|
822
|
+
while (i < s.length && /[a-zA-Z0-9_]/.test(s[i]))
|
|
823
|
+
id += s[i++];
|
|
824
|
+
out.push({ type: "identifier", value: id });
|
|
825
|
+
continue;
|
|
826
|
+
}
|
|
827
|
+
if ("+-*/%(),".includes(c)) {
|
|
828
|
+
out.push({ type: "operator", value: c });
|
|
829
|
+
i++;
|
|
830
|
+
continue;
|
|
831
|
+
}
|
|
832
|
+
throw new Error(`Unexpected character: ${c}`);
|
|
833
|
+
}
|
|
834
|
+
return out;
|
|
835
|
+
};
|
|
836
|
+
const parse = (tokens) => {
|
|
837
|
+
let pos = 0;
|
|
838
|
+
const cur = () => (pos < tokens.length ? tokens[pos] : null);
|
|
839
|
+
const nxt = () => {
|
|
840
|
+
if (pos >= tokens.length)
|
|
841
|
+
throw new Error("Unexpected end of input");
|
|
842
|
+
return tokens[pos++];
|
|
843
|
+
};
|
|
844
|
+
const expect = (v) => {
|
|
845
|
+
const t = cur();
|
|
846
|
+
if (!t || t.value !== v) {
|
|
847
|
+
throw new Error(`Expected "${v}" but got "${t ? t.value : "end of input"}"`);
|
|
848
|
+
}
|
|
849
|
+
nxt();
|
|
850
|
+
};
|
|
851
|
+
const parsePrimary = () => {
|
|
852
|
+
const t = cur();
|
|
853
|
+
if (!t)
|
|
854
|
+
throw new Error("Unexpected end of input");
|
|
855
|
+
if (t.type === "number") {
|
|
856
|
+
nxt();
|
|
857
|
+
return { type: "number", value: t.value, unit: t.unit };
|
|
858
|
+
}
|
|
859
|
+
if (t.type === "identifier") {
|
|
860
|
+
nxt();
|
|
861
|
+
if (cur() && cur().value === "(") {
|
|
862
|
+
nxt();
|
|
863
|
+
const args = [];
|
|
864
|
+
if (cur() && cur().value !== ")") {
|
|
865
|
+
args.push(parseAdd());
|
|
866
|
+
while (cur() && cur().value === ",") {
|
|
867
|
+
nxt();
|
|
868
|
+
args.push(parseAdd());
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
expect(")");
|
|
872
|
+
return { type: "call", func: t.value, args };
|
|
873
|
+
}
|
|
874
|
+
return { type: "var", name: t.value };
|
|
875
|
+
}
|
|
876
|
+
if (t.value === "(") {
|
|
877
|
+
nxt();
|
|
878
|
+
const e = parseAdd();
|
|
879
|
+
expect(")");
|
|
880
|
+
return e;
|
|
881
|
+
}
|
|
882
|
+
throw new Error(`Unexpected token: ${t.value}`);
|
|
883
|
+
};
|
|
884
|
+
const parseUnary = () => {
|
|
885
|
+
if (cur() && (cur().value === "+" || cur().value === "-")) {
|
|
886
|
+
const op = nxt().value;
|
|
887
|
+
return { type: "unary", op, arg: parseUnary() };
|
|
888
|
+
}
|
|
889
|
+
return parsePrimary();
|
|
890
|
+
};
|
|
891
|
+
const parsePower = () => {
|
|
892
|
+
let left = parseUnary();
|
|
893
|
+
while (cur() && cur().value === "**") {
|
|
894
|
+
const op = nxt().value;
|
|
895
|
+
left = { type: "binary", op, left, right: parseUnary() };
|
|
896
|
+
}
|
|
897
|
+
return left;
|
|
898
|
+
};
|
|
899
|
+
const parseMul = () => {
|
|
900
|
+
let left = parsePower();
|
|
901
|
+
while (cur() && ["*", "/", "%"].includes(String(cur().value))) {
|
|
902
|
+
const op = nxt().value;
|
|
903
|
+
left = { type: "binary", op, left, right: parsePower() };
|
|
904
|
+
}
|
|
905
|
+
return left;
|
|
906
|
+
};
|
|
907
|
+
const parseAdd = () => {
|
|
908
|
+
let left = parseMul();
|
|
909
|
+
while (cur() && (cur().value === "+" || cur().value === "-")) {
|
|
910
|
+
const op = nxt().value;
|
|
911
|
+
left = { type: "binary", op, left, right: parseMul() };
|
|
912
|
+
}
|
|
913
|
+
return left;
|
|
914
|
+
};
|
|
915
|
+
const ast = parseAdd();
|
|
916
|
+
if (pos < tokens.length) {
|
|
917
|
+
throw new Error(`Extra tokens after expression: ${tokens
|
|
918
|
+
.slice(pos)
|
|
919
|
+
.map((t) => t.value)
|
|
920
|
+
.join(" ")}`);
|
|
921
|
+
}
|
|
922
|
+
return ast;
|
|
923
|
+
};
|
|
924
|
+
// eslint-disable-next-line no-unused-vars
|
|
925
|
+
const evaluate = (ast, env) => {
|
|
926
|
+
switch (ast.type) {
|
|
927
|
+
case "number":
|
|
928
|
+
return { value: ast.value, unit: ast.unit };
|
|
929
|
+
case "var": {
|
|
930
|
+
const v = env[ast.name];
|
|
931
|
+
if (v === undefined)
|
|
932
|
+
throw new Error(`Unknown variable: ${ast.name}`);
|
|
933
|
+
if (typeof v === "function") {
|
|
934
|
+
throw new Error(`Expected variable but found function: ${ast.name}`);
|
|
935
|
+
}
|
|
936
|
+
return { value: v, unit: UNIT_NUM };
|
|
937
|
+
}
|
|
938
|
+
case "binary": {
|
|
939
|
+
const L = evaluate(ast.left, env);
|
|
940
|
+
const R = evaluate(ast.right, env);
|
|
941
|
+
switch (ast.op) {
|
|
942
|
+
case "+":
|
|
943
|
+
case "-": {
|
|
944
|
+
const val = ast.op === "+" ? L.value + R.value : L.value - R.value;
|
|
945
|
+
const unitMask = L.unit | R.unit;
|
|
946
|
+
if (L.unit === R.unit)
|
|
947
|
+
return { value: val, unit: L.unit };
|
|
948
|
+
if (unitMask === 5)
|
|
949
|
+
return { value: val, unit: UNIT_ANG };
|
|
950
|
+
throw new Error(`Cannot ${ast.op} mismatched units: ${getUnitName(L.unit)} and ${getUnitName(R.unit)}`);
|
|
951
|
+
}
|
|
952
|
+
case "*": {
|
|
953
|
+
const val = L.value * R.value;
|
|
954
|
+
if (L.unit === UNIT_NUM)
|
|
955
|
+
return { value: val, unit: R.unit };
|
|
956
|
+
if (R.unit === UNIT_NUM)
|
|
957
|
+
return { value: val, unit: L.unit };
|
|
958
|
+
throw new Error(`Cannot multiply units: ${getUnitName(L.unit)} and ${getUnitName(R.unit)}`);
|
|
959
|
+
}
|
|
960
|
+
case "/": {
|
|
961
|
+
if (R.unit === UNIT_NUM)
|
|
962
|
+
return { value: L.value / R.value, unit: L.unit };
|
|
963
|
+
if (L.unit === R.unit)
|
|
964
|
+
return { value: L.value / R.value, unit: UNIT_NUM };
|
|
965
|
+
throw new Error(`Cannot divide units: ${getUnitName(L.unit)} by ${getUnitName(R.unit)}`);
|
|
966
|
+
}
|
|
967
|
+
case "%": {
|
|
968
|
+
if (R.unit === UNIT_NUM)
|
|
969
|
+
return { value: L.value % R.value, unit: L.unit };
|
|
970
|
+
if (L.unit === R.unit)
|
|
971
|
+
return { value: L.value % R.value, unit: L.unit };
|
|
972
|
+
throw new Error(`Cannot modulo units: ${getUnitName(L.unit)} by ${getUnitName(R.unit)}`);
|
|
973
|
+
}
|
|
974
|
+
case "**": {
|
|
975
|
+
if (R.unit !== UNIT_NUM)
|
|
976
|
+
throw new Error(`Exponent must be a number, got ${getUnitName(R.unit)}`);
|
|
977
|
+
return { value: L.value ** R.value, unit: L.unit };
|
|
978
|
+
}
|
|
979
|
+
default:
|
|
980
|
+
throw new Error(`Unknown binary operator: ${ast.op}`);
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
case "unary": {
|
|
984
|
+
const res = evaluate(ast.arg, env);
|
|
985
|
+
switch (ast.op) {
|
|
986
|
+
case "+":
|
|
987
|
+
return { value: +res.value, unit: res.unit };
|
|
988
|
+
case "-":
|
|
989
|
+
return { value: -res.value, unit: res.unit };
|
|
990
|
+
default:
|
|
991
|
+
throw new Error(`Unknown unary operator: ${ast.op}`);
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
case "call": {
|
|
995
|
+
const fn = env[ast.func];
|
|
996
|
+
if (typeof fn !== "function")
|
|
997
|
+
throw new Error(`Unknown function: ${ast.func}`);
|
|
998
|
+
const evaluatedArgs = ast.args.map((a) => evaluate(a, env));
|
|
999
|
+
const numArgs = evaluatedArgs.map((a) => a.value);
|
|
1000
|
+
const val = fn(...numArgs); // eslint-disable-line no-unused-vars
|
|
1001
|
+
const preserveUnitFuncs = ["min", "max", "round", "ceil", "floor", "abs", "sign", "trunc"];
|
|
1002
|
+
if (preserveUnitFuncs.includes(ast.func)) {
|
|
1003
|
+
let combinedMask = 0;
|
|
1004
|
+
for (let i = 0; i < evaluatedArgs.length; i++) {
|
|
1005
|
+
combinedMask |= evaluatedArgs[i].unit;
|
|
1006
|
+
}
|
|
1007
|
+
if ((combinedMask & 6) === 6)
|
|
1008
|
+
throw new Error(`Function ${ast.func} cannot mix angle and percent`);
|
|
1009
|
+
if ((combinedMask & 3) === 3)
|
|
1010
|
+
throw new Error(`Function ${ast.func} cannot mix number and percent`);
|
|
1011
|
+
const finalUnit = combinedMask & UNIT_ANG ? UNIT_ANG : combinedMask & UNIT_PCT ? UNIT_PCT : UNIT_NUM;
|
|
1012
|
+
return { value: val, unit: finalUnit };
|
|
1013
|
+
}
|
|
1014
|
+
return { value: val, unit: UNIT_NUM };
|
|
1015
|
+
}
|
|
1016
|
+
default: {
|
|
1017
|
+
throw new Error(`Unknown AST node type: ${ast.type}`); // eslint-disable-line @typescript-eslint/no-explicit-any
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
};
|
|
1021
|
+
const calcEnv = Object.assign({}, BASE_MATH_ENV, base);
|
|
1022
|
+
return evaluate(parse(tokenize(expr)), calcEnv).value;
|
|
1023
|
+
}
|
|
1024
|
+
export function evaluateComponent(node, expectedType, base = {}) {
|
|
1025
|
+
if (!node)
|
|
1026
|
+
return NaN;
|
|
1027
|
+
const token = extractToken(node);
|
|
1028
|
+
return evaluateCSSValue(token, expectedType, base);
|
|
1029
|
+
}
|
|
1030
|
+
export function evaluateCSSValue(tokenString, expectedType, base = {}) {
|
|
1031
|
+
const token = tokenString.trim();
|
|
1032
|
+
if (token === "none")
|
|
1033
|
+
return NaN;
|
|
1034
|
+
if (token in base)
|
|
1035
|
+
return base[token];
|
|
1036
|
+
const [tMin, tMax] = Array.isArray(expectedType) ? expectedType : expectedType === "percentage" ? [0, 100] : [0, 1];
|
|
1037
|
+
if (/^-?(?:\d+|\d*\.\d+)$/.test(token)) {
|
|
1038
|
+
return parseFloat(token);
|
|
1039
|
+
}
|
|
1040
|
+
if (token[token.length - 1] === "%" && /^-?(?:\d+|\d*\.\d+)%$/.test(token)) {
|
|
1041
|
+
return parsePercent(token, expectedType, tMin, tMax);
|
|
1042
|
+
}
|
|
1043
|
+
if (/^-?(?:\d+|\d*\.\d+)(deg|rad|grad|turn)$/.test(token)) {
|
|
1044
|
+
return parseHue(token);
|
|
1045
|
+
}
|
|
1046
|
+
if (token.slice(0, 5) === "calc(" && token[token.length - 1] === ")") {
|
|
1047
|
+
return parseCalcExpression(token.slice(5, -1).trim(), expectedType, base, tMin, tMax);
|
|
1048
|
+
}
|
|
1049
|
+
throw new Error(`Unable to parse component token: ${token}`);
|
|
1050
|
+
}
|
|
1051
|
+
function createLegacyColorParser(model, componentKeys) {
|
|
1052
|
+
return (node) => {
|
|
1053
|
+
const t = node.value;
|
|
1054
|
+
const config = colorModels[model].components;
|
|
1055
|
+
const c1 = evaluateComponent(t[2], config[componentKeys[0]].value);
|
|
1056
|
+
const c2 = evaluateComponent(t[4], config[componentKeys[1]].value);
|
|
1057
|
+
const c3 = evaluateComponent(t[6], config[componentKeys[2]].value);
|
|
1058
|
+
let alpha = 1;
|
|
1059
|
+
if (t[7]?.type === "symbol" && t[7]?.value === ",")
|
|
1060
|
+
alpha = evaluateComponent(t[8], [0, 1]);
|
|
1061
|
+
return { model, coords: [c1, c2, c3, alpha] };
|
|
1062
|
+
};
|
|
1063
|
+
}
|
|
1064
|
+
function createModernColorParser(model, componentKeys) {
|
|
1065
|
+
return (node) => {
|
|
1066
|
+
const t = node.value;
|
|
1067
|
+
const config = colorModels[model].components;
|
|
1068
|
+
const c1 = evaluateComponent(t[2], config[componentKeys[0]].value);
|
|
1069
|
+
const c2 = evaluateComponent(t[3], config[componentKeys[1]].value);
|
|
1070
|
+
const c3 = evaluateComponent(t[4], config[componentKeys[2]].value);
|
|
1071
|
+
let alpha = 1;
|
|
1072
|
+
if (t[5]?.type === "symbol" && t[5]?.value === "/")
|
|
1073
|
+
alpha = evaluateComponent(t[6], [0, 1]);
|
|
1074
|
+
return { model, coords: [c1, c2, c3, alpha] };
|
|
1075
|
+
};
|
|
1076
|
+
}
|
|
1077
|
+
function createRelativeColorParser(model, componentKeys) {
|
|
1078
|
+
return (node) => {
|
|
1079
|
+
const t = node.value;
|
|
1080
|
+
const config = colorModels[model].components;
|
|
1081
|
+
const originColorNode = t[3];
|
|
1082
|
+
const originParsed = parseNode(originColorNode);
|
|
1083
|
+
const originInstance = new Color(originParsed.model, originParsed.coords).in(model);
|
|
1084
|
+
const baseEnv = originInstance.toObject({ fit: "none", precision: null });
|
|
1085
|
+
const c1 = evaluateComponent(t[4], config[componentKeys[0]].value, baseEnv);
|
|
1086
|
+
const c2 = evaluateComponent(t[5], config[componentKeys[1]].value, baseEnv);
|
|
1087
|
+
const c3 = evaluateComponent(t[6], config[componentKeys[2]].value, baseEnv);
|
|
1088
|
+
let alpha = 1;
|
|
1089
|
+
if (t[7]?.type === "symbol" && t[7]?.value === "/") {
|
|
1090
|
+
alpha = evaluateComponent(t[8], [0, 1], baseEnv);
|
|
1091
|
+
}
|
|
1092
|
+
return {
|
|
1093
|
+
model,
|
|
1094
|
+
coords: [c1, c2, c3, alpha],
|
|
1095
|
+
};
|
|
1096
|
+
};
|
|
1097
|
+
}
|
|
1098
|
+
function createRelativeColorFunctionParser(node) {
|
|
1099
|
+
const t = node.value;
|
|
1100
|
+
const { model, coords } = parseNode(t[3]);
|
|
1101
|
+
const spaceNode = t[4];
|
|
1102
|
+
const targetModel = (Array.isArray(spaceNode.value) ? spaceNode.value[0]?.value : spaceNode.value);
|
|
1103
|
+
const originInstance = new Color(model, coords).in(targetModel);
|
|
1104
|
+
const baseEnv = originInstance.toObject({ fit: "none", precision: null });
|
|
1105
|
+
const c1 = evaluateComponent(t[5], [0, 1], baseEnv);
|
|
1106
|
+
const c2 = evaluateComponent(t[6], [0, 1], baseEnv);
|
|
1107
|
+
const c3 = evaluateComponent(t[7], [0, 1], baseEnv);
|
|
1108
|
+
let alpha = 1;
|
|
1109
|
+
if (t[8]?.type === "symbol" && t[8]?.value === "/") {
|
|
1110
|
+
alpha = evaluateComponent(t[9], [0, 1], baseEnv);
|
|
1111
|
+
}
|
|
1112
|
+
return {
|
|
1113
|
+
model: targetModel,
|
|
1114
|
+
coords: [c1, c2, c3, alpha],
|
|
1115
|
+
};
|
|
1116
|
+
}
|
|
1117
|
+
export const validators = (() => {
|
|
1118
|
+
const NUMBER_REGEX = /^-?(?:\d+(?:\.\d+)?|\.\d+)$/;
|
|
1119
|
+
const PERCENT_REGEX = /^-?(?:\d+(?:\.\d+)?|\.\d+)%$/;
|
|
1120
|
+
const ANGLE_REGEX = /^-?(?:\d+(?:\.\d+)?|\.\d+)(?:deg|rad|turn|grad)$/;
|
|
1121
|
+
const RANGE_REGEX = /^\[([^,]+),([^\]]+)\]$/;
|
|
1122
|
+
const systemColorsSet = new Set(Object.keys(systemColors).map((key) => key.toLowerCase()));
|
|
1123
|
+
const rangeCache = new Map();
|
|
1124
|
+
const getRange = (args) => {
|
|
1125
|
+
if (!args)
|
|
1126
|
+
return undefined;
|
|
1127
|
+
for (let i = 0; i < args.length; i++) {
|
|
1128
|
+
if (RANGE_REGEX.test(args[i]))
|
|
1129
|
+
return args[i];
|
|
1130
|
+
}
|
|
1131
|
+
return undefined;
|
|
1132
|
+
};
|
|
1133
|
+
const checkRange = (val, rangeStr) => {
|
|
1134
|
+
let bounds = rangeCache.get(rangeStr);
|
|
1135
|
+
if (!bounds) {
|
|
1136
|
+
const match = rangeStr.match(RANGE_REGEX);
|
|
1137
|
+
bounds = {
|
|
1138
|
+
min: match[1].trim() === "-INF" ? -Infinity : parseFloat(match[1]),
|
|
1139
|
+
max: match[2].trim() === "INF" || match[2].trim() === "+INF" ? Infinity : parseFloat(match[2]),
|
|
1140
|
+
};
|
|
1141
|
+
rangeCache.set(rangeStr, bounds);
|
|
1142
|
+
}
|
|
1143
|
+
if (bounds.min !== -Infinity && val < bounds.min)
|
|
1144
|
+
return false;
|
|
1145
|
+
if (bounds.max !== Infinity && val > bounds.max)
|
|
1146
|
+
return false;
|
|
1147
|
+
return true;
|
|
1148
|
+
};
|
|
1149
|
+
return {
|
|
1150
|
+
"<named-color>": (str) => Object.hasOwn(namedColors, str),
|
|
1151
|
+
"<system-color>": (str) => systemColorsSet.has(str),
|
|
1152
|
+
"<hex-color>": (str) => {
|
|
1153
|
+
return (str[0] === "#" &&
|
|
1154
|
+
(str.length === 4 || str.length === 5 || str.length === 7 || str.length === 9) &&
|
|
1155
|
+
/^[0-9a-fA-F]+$/.test(str.slice(1)));
|
|
1156
|
+
},
|
|
1157
|
+
"<number>": (str, args) => {
|
|
1158
|
+
if (str.startsWith("calc(") && str.endsWith(")"))
|
|
1159
|
+
return true;
|
|
1160
|
+
if (!NUMBER_REGEX.test(str))
|
|
1161
|
+
return false;
|
|
1162
|
+
if (args !== undefined) {
|
|
1163
|
+
const rangeArg = getRange(args);
|
|
1164
|
+
if (rangeArg)
|
|
1165
|
+
return checkRange(parseFloat(str), rangeArg);
|
|
1166
|
+
}
|
|
1167
|
+
return true;
|
|
1168
|
+
},
|
|
1169
|
+
"<percentage>": (str, args) => {
|
|
1170
|
+
if (str.startsWith("calc(") && str.endsWith(")"))
|
|
1171
|
+
return true;
|
|
1172
|
+
if (!PERCENT_REGEX.test(str))
|
|
1173
|
+
return false;
|
|
1174
|
+
if (args !== undefined) {
|
|
1175
|
+
const rangeArg = getRange(args);
|
|
1176
|
+
if (rangeArg)
|
|
1177
|
+
return checkRange(parseFloat(str), rangeArg);
|
|
1178
|
+
}
|
|
1179
|
+
return true;
|
|
1180
|
+
},
|
|
1181
|
+
"<angle>": (str, args) => {
|
|
1182
|
+
if (str.startsWith("calc(") && str.endsWith(")"))
|
|
1183
|
+
return true;
|
|
1184
|
+
if (!ANGLE_REGEX.test(str))
|
|
1185
|
+
return false;
|
|
1186
|
+
if (args !== undefined) {
|
|
1187
|
+
const rangeArg = getRange(args);
|
|
1188
|
+
if (rangeArg)
|
|
1189
|
+
return checkRange(parseFloat(str), rangeArg);
|
|
1190
|
+
}
|
|
1191
|
+
return true;
|
|
1192
|
+
},
|
|
1193
|
+
}; // eslint-disable-line no-unused-vars
|
|
1194
|
+
})();
|
|
1195
|
+
export const parsers = {
|
|
1196
|
+
"<color>": (node) => {
|
|
1197
|
+
const child = node.value[0];
|
|
1198
|
+
const { type, value } = child;
|
|
1199
|
+
if (type === "literal") {
|
|
1200
|
+
if (value === "currentcolor")
|
|
1201
|
+
return { model: "rgb", coords: [0, 0, 0, 1] };
|
|
1202
|
+
else
|
|
1203
|
+
throw new Error(`Unknown <color>: ${value}`);
|
|
1204
|
+
}
|
|
1205
|
+
return parseNode(child);
|
|
1206
|
+
},
|
|
1207
|
+
"<color-base>": (node) => {
|
|
1208
|
+
const child = node.value[0];
|
|
1209
|
+
const { type, value } = child;
|
|
1210
|
+
if (type === "literal") {
|
|
1211
|
+
if (value === "transparent")
|
|
1212
|
+
return { model: "rgb", coords: [0, 0, 0, 0] };
|
|
1213
|
+
else
|
|
1214
|
+
throw new Error(`Unknown <color-base>: ${value}`);
|
|
1215
|
+
}
|
|
1216
|
+
return parseNode(child);
|
|
1217
|
+
},
|
|
1218
|
+
"<named-color>": (node) => {
|
|
1219
|
+
const { value: name } = node;
|
|
1220
|
+
const rgb = namedColors[name];
|
|
1221
|
+
if (!rgb)
|
|
1222
|
+
throw new Error(`Unknown <named-color>: ${name}`);
|
|
1223
|
+
return { model: "rgb", coords: [...rgb, 1] };
|
|
1224
|
+
},
|
|
1225
|
+
"<system-color>": (node) => {
|
|
1226
|
+
const { value: name } = node;
|
|
1227
|
+
const index = config.theme === "light" ? 0 : 1;
|
|
1228
|
+
const key = Object.keys(systemColors).find((k) => k.toLowerCase() === name);
|
|
1229
|
+
const rgb = systemColors[key][index];
|
|
1230
|
+
if (!rgb)
|
|
1231
|
+
throw new Error(`Unknown <system-color>: ${name}`);
|
|
1232
|
+
return { model: "rgb", coords: [...rgb, 1] };
|
|
1233
|
+
},
|
|
1234
|
+
"<contrast-color()>": (node) => {
|
|
1235
|
+
// contrast-color ( <color> )
|
|
1236
|
+
const colorNode = node.value[2];
|
|
1237
|
+
const { model, coords } = parseNode(colorNode);
|
|
1238
|
+
const [, luminance] = new Color(model, coords).in("xyz-d65").coords;
|
|
1239
|
+
return { model: "rgb", coords: luminance > 0.5 ? [0, 0, 0, 1] : [255, 255, 255, 1] };
|
|
1240
|
+
},
|
|
1241
|
+
"<light-dark-color>": (node) => {
|
|
1242
|
+
const children = node.value;
|
|
1243
|
+
// light-dark ( <color> , <color> )
|
|
1244
|
+
const lightNode = children[2];
|
|
1245
|
+
const darkNode = children[4];
|
|
1246
|
+
const { model, coords } = parseNode(config?.theme === "light" ? lightNode : darkNode);
|
|
1247
|
+
const { coords: rgbCoords } = new Color(model, coords).in("rgb");
|
|
1248
|
+
return { model: "rgb", coords: rgbCoords };
|
|
1249
|
+
},
|
|
1250
|
+
"<hex-color>": (node) => {
|
|
1251
|
+
const hex = node.value;
|
|
1252
|
+
const len = hex.length;
|
|
1253
|
+
let r = 0, g = 0, b = 0, a = 1;
|
|
1254
|
+
if (len === 4 || len === 5) {
|
|
1255
|
+
const rCh = hex.charCodeAt(1);
|
|
1256
|
+
const gCh = hex.charCodeAt(2);
|
|
1257
|
+
const bCh = hex.charCodeAt(3);
|
|
1258
|
+
const rVal = rCh > 64 ? (rCh & 7) + 9 : rCh & 15;
|
|
1259
|
+
const gVal = gCh > 64 ? (gCh & 7) + 9 : gCh & 15;
|
|
1260
|
+
const bVal = bCh > 64 ? (bCh & 7) + 9 : bCh & 15;
|
|
1261
|
+
r = (rVal << 4) | rVal;
|
|
1262
|
+
g = (gVal << 4) | gVal;
|
|
1263
|
+
b = (bVal << 4) | bVal;
|
|
1264
|
+
if (len === 5) {
|
|
1265
|
+
const aCh = hex.charCodeAt(4);
|
|
1266
|
+
const aVal = aCh > 64 ? (aCh & 7) + 9 : aCh & 15;
|
|
1267
|
+
a = ((aVal << 4) | aVal) / 255;
|
|
1268
|
+
}
|
|
1269
|
+
}
|
|
1270
|
+
else {
|
|
1271
|
+
const num = parseInt(hex.slice(1), 16);
|
|
1272
|
+
if (len === 7) {
|
|
1273
|
+
r = (num >> 16) & 255;
|
|
1274
|
+
g = (num >> 8) & 255;
|
|
1275
|
+
b = num & 255;
|
|
1276
|
+
}
|
|
1277
|
+
else if (len === 9) {
|
|
1278
|
+
r = (num >> 24) & 255;
|
|
1279
|
+
g = (num >> 16) & 255;
|
|
1280
|
+
b = (num >> 8) & 255;
|
|
1281
|
+
a = (num & 255) / 255;
|
|
1282
|
+
}
|
|
1283
|
+
}
|
|
1284
|
+
return {
|
|
1285
|
+
model: "rgb",
|
|
1286
|
+
coords: [r, g, b, a],
|
|
1287
|
+
};
|
|
1288
|
+
},
|
|
1289
|
+
"<device-cmyk()>": (node) => parseNode(node.value[0]),
|
|
1290
|
+
"<legacy-device-cmyk-syntax>": (node) => {
|
|
1291
|
+
// device-cmyk( <number>#{4} )
|
|
1292
|
+
const t = node.value;
|
|
1293
|
+
const [c, m, y, k] = [t[2], t[4], t[6], t[8]].map(parseCmykComponent);
|
|
1294
|
+
return {
|
|
1295
|
+
model: "rgb",
|
|
1296
|
+
coords: [(1 - (c * (1 - k) + k)) * 255, (1 - (m * (1 - k) + k)) * 255, (1 - (y * (1 - k) + k)) * 255, 1],
|
|
1297
|
+
};
|
|
1298
|
+
},
|
|
1299
|
+
"<modern-device-cmyk-syntax>": (node) => {
|
|
1300
|
+
// device-cmyk( <cmyk-component>{4} [ / [ <alpha-value> | none ] ]? )
|
|
1301
|
+
const t = node.value;
|
|
1302
|
+
const c = evaluateComponent(t[2], [0, 1]);
|
|
1303
|
+
const m = evaluateComponent(t[3], [0, 1]);
|
|
1304
|
+
const y = evaluateComponent(t[4], [0, 1]);
|
|
1305
|
+
const k = evaluateComponent(t[5], [0, 1]);
|
|
1306
|
+
const alphaNode = t.find((c, idx) => idx === 7 && (c.type === "<alpha-value>" || (c.type === "literal" && c.value === "none")));
|
|
1307
|
+
const alpha = alphaNode ? evaluateComponent(alphaNode, [0, 1]) : 1;
|
|
1308
|
+
const cMath = normalize(c, [0, 1]);
|
|
1309
|
+
const mMath = normalize(m, [0, 1]);
|
|
1310
|
+
const yMath = normalize(y, [0, 1]);
|
|
1311
|
+
const kMath = normalize(k, [0, 1]);
|
|
1312
|
+
const alphaMath = isNaN(alpha) ? alpha : Math.max(0, Math.min(1, alpha));
|
|
1313
|
+
return {
|
|
1314
|
+
model: "rgb",
|
|
1315
|
+
coords: [
|
|
1316
|
+
(1 - (cMath * (1 - kMath) + kMath)) * 255,
|
|
1317
|
+
(1 - (mMath * (1 - kMath) + kMath)) * 255,
|
|
1318
|
+
(1 - (yMath * (1 - kMath) + kMath)) * 255,
|
|
1319
|
+
alphaMath,
|
|
1320
|
+
],
|
|
1321
|
+
};
|
|
1322
|
+
},
|
|
1323
|
+
"<color-mix()>": (node) => {
|
|
1324
|
+
let model = "oklab";
|
|
1325
|
+
let hue = "shorter";
|
|
1326
|
+
const children = node.value;
|
|
1327
|
+
let i = 2;
|
|
1328
|
+
if (children[i]?.type === "<color-interpolation-method>") {
|
|
1329
|
+
const methodParts = children[i].value;
|
|
1330
|
+
i++;
|
|
1331
|
+
if (methodParts[1]) {
|
|
1332
|
+
model = getLiteralValue(methodParts[1]);
|
|
1333
|
+
}
|
|
1334
|
+
if (methodParts[2] && methodParts[2].type !== "symbol") {
|
|
1335
|
+
hue = getLiteralValue(methodParts[2]);
|
|
1336
|
+
}
|
|
1337
|
+
}
|
|
1338
|
+
const items = [];
|
|
1339
|
+
while (i < children.length) {
|
|
1340
|
+
const current = children[i];
|
|
1341
|
+
if (current.type === "symbol" && (current.value === "," || current.value === ")")) {
|
|
1342
|
+
i++;
|
|
1343
|
+
continue;
|
|
1344
|
+
}
|
|
1345
|
+
let percentage = undefined;
|
|
1346
|
+
let colorNode;
|
|
1347
|
+
if (current.type === "<percentage>") {
|
|
1348
|
+
const tokenStr = String(current.value || "");
|
|
1349
|
+
percentage = evaluateCSSValue(tokenStr, "percentage");
|
|
1350
|
+
i++;
|
|
1351
|
+
colorNode = children[i];
|
|
1352
|
+
}
|
|
1353
|
+
else {
|
|
1354
|
+
colorNode = current;
|
|
1355
|
+
const nextNode = children[i + 1];
|
|
1356
|
+
if (nextNode?.type === "<percentage>") {
|
|
1357
|
+
const tokenStr = String(nextNode.value || "");
|
|
1358
|
+
percentage = evaluateCSSValue(tokenStr, "percentage");
|
|
1359
|
+
i++;
|
|
1360
|
+
}
|
|
1361
|
+
}
|
|
1362
|
+
const colorData = colorNode ? parseNode(colorNode) : null;
|
|
1363
|
+
if (colorData && colorData.coords) {
|
|
1364
|
+
i++;
|
|
1365
|
+
items.push({
|
|
1366
|
+
color: new Color(colorData.model, colorData.coords),
|
|
1367
|
+
percentage: percentage,
|
|
1368
|
+
});
|
|
1369
|
+
continue;
|
|
1370
|
+
}
|
|
1371
|
+
i++;
|
|
1372
|
+
}
|
|
1373
|
+
if (items.length === 0) {
|
|
1374
|
+
throw new Error("color-mix() must contain at least one color component.");
|
|
1375
|
+
}
|
|
1376
|
+
const mixedResult = Color.mix(items, { in: model, hue });
|
|
1377
|
+
return {
|
|
1378
|
+
model: mixedResult.model,
|
|
1379
|
+
coords: mixedResult.coords,
|
|
1380
|
+
};
|
|
1381
|
+
},
|
|
1382
|
+
"<color-function>": (node) => parseNode(node.value[0]),
|
|
1383
|
+
"<custom-color-function>": (node) => parseNode(node.value[0]),
|
|
1384
|
+
"<rgb()>": (node) => parseNode(node.value[0]),
|
|
1385
|
+
"<rgba()>": (node) => parseNode(node.value[0]),
|
|
1386
|
+
"<hsl()>": (node) => parseNode(node.value[0]),
|
|
1387
|
+
"<hsla()>": (node) => parseNode(node.value[0]),
|
|
1388
|
+
"<hwb()>": (node) => parseNode(node.value[0]),
|
|
1389
|
+
"<lab()>": (node) => parseNode(node.value[0]),
|
|
1390
|
+
"<lch()>": (node) => parseNode(node.value[0]),
|
|
1391
|
+
"<oklab()>": (node) => parseNode(node.value[0]),
|
|
1392
|
+
"<oklch()>": (node) => parseNode(node.value[0]),
|
|
1393
|
+
"<legacy-rgb-syntax>": createLegacyColorParser("rgb", ["r", "g", "b"]),
|
|
1394
|
+
"<legacy-rgba-syntax>": createLegacyColorParser("rgb", ["r", "g", "b"]),
|
|
1395
|
+
"<legacy-hsl-syntax>": createLegacyColorParser("hsl", ["h", "s", "l"]),
|
|
1396
|
+
"<legacy-hsla-syntax>": createLegacyColorParser("hsl", ["h", "s", "l"]),
|
|
1397
|
+
"<modern-rgb-syntax>": createModernColorParser("rgb", ["r", "g", "b"]),
|
|
1398
|
+
"<modern-rgba-syntax>": createModernColorParser("rgb", ["r", "g", "b"]),
|
|
1399
|
+
"<modern-hsl-syntax>": createModernColorParser("hsl", ["h", "s", "l"]),
|
|
1400
|
+
"<modern-hsla-syntax>": createModernColorParser("hsl", ["h", "s", "l"]),
|
|
1401
|
+
"<absolute-hwb-syntax>": createModernColorParser("hwb", ["h", "w", "b"]),
|
|
1402
|
+
"<absolute-lab-syntax>": createModernColorParser("lab", ["l", "a", "b"]),
|
|
1403
|
+
"<absolute-lch-syntax>": createModernColorParser("lch", ["l", "c", "h"]),
|
|
1404
|
+
"<absolute-oklab-syntax>": createModernColorParser("oklab", ["l", "a", "b"]),
|
|
1405
|
+
"<absolute-oklch-syntax>": createModernColorParser("oklch", ["l", "c", "h"]),
|
|
1406
|
+
"<relative-rgb-syntax>": createRelativeColorParser("rgb", ["r", "g", "b"]),
|
|
1407
|
+
"<relative-rgba-syntax>": createRelativeColorParser("rgb", ["r", "g", "b"]),
|
|
1408
|
+
"<relative-hsl-syntax>": createRelativeColorParser("hsl", ["h", "s", "l"]),
|
|
1409
|
+
"<relative-hsla-syntax>": createRelativeColorParser("hsl", ["h", "s", "l"]),
|
|
1410
|
+
"<relative-hwb-syntax>": createRelativeColorParser("hwb", ["h", "w", "b"]),
|
|
1411
|
+
"<relative-lab-syntax>": createRelativeColorParser("lab", ["l", "a", "b"]),
|
|
1412
|
+
"<relative-lch-syntax>": createRelativeColorParser("lch", ["l", "c", "h"]),
|
|
1413
|
+
"<relative-oklab-syntax>": createRelativeColorParser("oklab", ["l", "a", "b"]),
|
|
1414
|
+
"<relative-oklch-syntax>": createRelativeColorParser("oklch", ["l", "c", "h"]),
|
|
1415
|
+
"<alpha()>": (node) => {
|
|
1416
|
+
const children = node.value;
|
|
1417
|
+
// alpha(from <color> / [ <alpha-value> | none ])
|
|
1418
|
+
const colorNode = children[3];
|
|
1419
|
+
const originColor = parseNode(colorNode);
|
|
1420
|
+
if (!originColor)
|
|
1421
|
+
throw new Error("Missing or invalid origin color in alpha() function");
|
|
1422
|
+
const originInstance = new Color(originColor.model, originColor.coords);
|
|
1423
|
+
const baseEnv = originInstance.toObject({ fit: "none", precision: null });
|
|
1424
|
+
const alphaNode = children.find((c, idx) => c.type === "<alpha-value>" || (idx === 5 && c.type === "literal" && c.value === "none"));
|
|
1425
|
+
const targetAlpha = alphaNode ? evaluateComponent(alphaNode, [0, 1], baseEnv) : originColor.coords[3];
|
|
1426
|
+
return {
|
|
1427
|
+
model: originColor.model,
|
|
1428
|
+
coords: [...originColor.coords.slice(0, 3), targetAlpha],
|
|
1429
|
+
};
|
|
1430
|
+
},
|
|
1431
|
+
"<color()>": (node) => parseNode(node.value[0]),
|
|
1432
|
+
"<relative-color-syntax>": (node) => parseNode(node.value[0]),
|
|
1433
|
+
"<absolute-color-syntax>": (node) => {
|
|
1434
|
+
const t = node.value;
|
|
1435
|
+
const spaceNode = t[2];
|
|
1436
|
+
const model = (Array.isArray(spaceNode.value) ? spaceNode.value[0]?.value : spaceNode.value);
|
|
1437
|
+
const c1 = evaluateComponent(t[3], [0, 1]);
|
|
1438
|
+
const c2 = evaluateComponent(t[4], [0, 1]);
|
|
1439
|
+
const c3 = evaluateComponent(t[5], [0, 1]);
|
|
1440
|
+
const alphaNode = t.find((c, idx) => idx > 5 && (c.type === "<alpha-value>" || (c.type === "literal" && c.value === "none")));
|
|
1441
|
+
const alpha = alphaNode ? evaluateComponent(alphaNode, [0, 1]) : 1;
|
|
1442
|
+
return {
|
|
1443
|
+
model,
|
|
1444
|
+
coords: [c1, c2, c3, alpha],
|
|
1445
|
+
};
|
|
1446
|
+
},
|
|
1447
|
+
"<relative-rgb-color-syntax>": createRelativeColorFunctionParser,
|
|
1448
|
+
"<relative-xyz-color-syntax>": createRelativeColorFunctionParser,
|
|
1449
|
+
"<relative-custom-color-syntax>": (node) => parseNode(node.value[0]),
|
|
1450
|
+
}; // eslint-disable-line no-unused-vars
|
|
1451
|
+
export const formatters = (() => {
|
|
1452
|
+
const colorFunctionFormatters = (() => {
|
|
1453
|
+
const out = {};
|
|
1454
|
+
for (const model of Object.keys(colorModels)) {
|
|
1455
|
+
const { bridge, fromBridge, supportsLegacy, components, alphaVariant } = colorModels[model];
|
|
1456
|
+
out[model] = {
|
|
1457
|
+
bridge,
|
|
1458
|
+
fromBridge: (coords) => [...fromBridge(coords), coords[3] ?? 1],
|
|
1459
|
+
format: ([c1, c2, c3, a = 1], options = {}) => {
|
|
1460
|
+
const { legacy = false, fit: fitMethod = config.defaults.fit, precision, units = false } = options;
|
|
1461
|
+
const fitted = fit([c1, c2, c3], model, { method: fitMethod, precision });
|
|
1462
|
+
const formatted = [...fitted, a].map((c, index) => {
|
|
1463
|
+
const norm = normalize(c, Object.values(components)[index].value);
|
|
1464
|
+
if ((units || legacy) && components) {
|
|
1465
|
+
const def = Object.values(components).find((comp) => comp.index === index);
|
|
1466
|
+
if (def?.value === "percentage")
|
|
1467
|
+
return `${norm}%`;
|
|
1468
|
+
if (def?.value === "hue" && units)
|
|
1469
|
+
return `${norm}deg`;
|
|
1470
|
+
}
|
|
1471
|
+
return norm.toString();
|
|
1472
|
+
});
|
|
1473
|
+
const f = formatted.slice(0, 3);
|
|
1474
|
+
const alpha = formatted[3];
|
|
1475
|
+
if (model in colorSpaces)
|
|
1476
|
+
return `color(${model} ${f.join(" ")}${a !== 1 ? ` / ${alpha}` : ""})`;
|
|
1477
|
+
if (legacy && supportsLegacy) {
|
|
1478
|
+
return a === 1
|
|
1479
|
+
? `${model}(${f.join(", ")})`
|
|
1480
|
+
: `${alphaVariant || model}(${f.join(", ")}, ${alpha})`;
|
|
1481
|
+
}
|
|
1482
|
+
return `${model}(${f.join(" ")}${a !== 1 ? ` / ${alpha}` : ""})`;
|
|
1483
|
+
},
|
|
1484
|
+
};
|
|
1485
|
+
}
|
|
1486
|
+
return out;
|
|
1487
|
+
})();
|
|
1488
|
+
return {
|
|
1489
|
+
...colorFunctionFormatters,
|
|
1490
|
+
"hex-color": {
|
|
1491
|
+
bridge: "rgb",
|
|
1492
|
+
fromBridge: (coords) => coords,
|
|
1493
|
+
format: ([r, g, b, a = 1]) => {
|
|
1494
|
+
const toHex = (v) => v.toString(16).padStart(2, "0");
|
|
1495
|
+
const hex = [r, g, b].map((v) => toHex(Math.round(Math.max(0, Math.min(255, v))))).join("");
|
|
1496
|
+
return `#${hex}${a < 1 ? toHex(Math.round(a * 255)) : ""}`;
|
|
1497
|
+
},
|
|
1498
|
+
},
|
|
1499
|
+
"named-color": {
|
|
1500
|
+
bridge: "rgb",
|
|
1501
|
+
fromBridge: (coords) => coords,
|
|
1502
|
+
format: (rgb) => {
|
|
1503
|
+
const [r, g, b] = rgb.map((v, i) => (i < 3 ? Math.round(Math.min(255, Math.max(0, v))) : v));
|
|
1504
|
+
for (const [name, [nr, ng, nb]] of Object.entries(namedColors)) {
|
|
1505
|
+
if (r === nr && g === ng && b === nb)
|
|
1506
|
+
return name;
|
|
1507
|
+
}
|
|
1508
|
+
},
|
|
1509
|
+
},
|
|
1510
|
+
"device-cmyk": {
|
|
1511
|
+
bridge: "rgb",
|
|
1512
|
+
fromBridge: (coords) => coords,
|
|
1513
|
+
format: ([red, green, blue, alpha = 1], options = {}) => {
|
|
1514
|
+
const { legacy = false, precision = 3, fit: fitMethod = config.defaults.fit } = options;
|
|
1515
|
+
const [fr, fg, fb] = fit([red, green, blue], "rgb", { method: fitMethod });
|
|
1516
|
+
const r = fr / 255;
|
|
1517
|
+
const g = fg / 255;
|
|
1518
|
+
const b = fb / 255;
|
|
1519
|
+
const k = 1 - Math.max(r, g, b);
|
|
1520
|
+
const formatComponent = (value) => {
|
|
1521
|
+
return Number(precision === null ? value : value.toFixed(precision)).toString();
|
|
1522
|
+
};
|
|
1523
|
+
const c = formatComponent(k === 1 ? 0 : (1 - r - k) / (1 - k));
|
|
1524
|
+
const m = formatComponent(k === 1 ? 0 : (1 - g - k) / (1 - k));
|
|
1525
|
+
const y = formatComponent(k === 1 ? 0 : (1 - b - k) / (1 - k));
|
|
1526
|
+
const kFormatted = formatComponent(k);
|
|
1527
|
+
const alphaFormatted = Number(alpha.toFixed(3)).toString();
|
|
1528
|
+
if (legacy) {
|
|
1529
|
+
return `device-cmyk(${c}, ${m}, ${y}, ${kFormatted}${alpha < 1 ? `, ${alphaFormatted}` : ""})`;
|
|
1530
|
+
}
|
|
1531
|
+
const rgbPart = colorFunctionFormatters.rgb.format?.([fr, fg, fb, alpha], options);
|
|
1532
|
+
return `device-cmyk(${c} ${m} ${y} ${kFormatted}${alpha < 1 ? ` / ${alphaFormatted}` : ""}, ${rgbPart})`;
|
|
1533
|
+
},
|
|
1534
|
+
},
|
|
1535
|
+
};
|
|
1536
|
+
})();
|
|
1537
|
+
export function parseNode(node) {
|
|
1538
|
+
const parser = parsers[node.type];
|
|
1539
|
+
if (!parser)
|
|
1540
|
+
throw new Error(`No parser for rule: ${node.type}`);
|
|
1541
|
+
return parser(node);
|
|
1542
|
+
}
|
|
1543
|
+
export function isValid(input, rule) {
|
|
1544
|
+
const cleaned = input.replace(/[A-Z]/g, (c) => String.fromCharCode(c.charCodeAt(0) + 32));
|
|
1545
|
+
const tokens = tokenize(cleaned);
|
|
1546
|
+
const memo = new Map();
|
|
1547
|
+
if (rule) {
|
|
1548
|
+
if (grammarMap[rule]) {
|
|
1549
|
+
const results = matchRule(rule, tokens, 0, grammarMap, memo, null);
|
|
1550
|
+
return results.some((result) => result.success && result.nextIndex === tokens.length);
|
|
1551
|
+
}
|
|
1552
|
+
if (validators[rule]) {
|
|
1553
|
+
const results = matchRule(rule, tokens, 0, grammarMap, memo, null);
|
|
1554
|
+
return results.some((result) => result.success && result.nextIndex === tokens.length);
|
|
1555
|
+
}
|
|
1556
|
+
try {
|
|
1557
|
+
const inlineNode = parseExpression(rule);
|
|
1558
|
+
const results = matchNode(inlineNode, tokens, 0, grammarMap, memo, null);
|
|
1559
|
+
return results.some((result) => result.success && result.nextIndex === tokens.length);
|
|
1560
|
+
}
|
|
1561
|
+
catch (error) {
|
|
1562
|
+
throw new Error(`Failed to resolve rule or parse inline expression: "${rule}"`, { cause: error });
|
|
1563
|
+
}
|
|
1564
|
+
}
|
|
1565
|
+
const allRules = new Set([...Object.keys(grammarMap), ...Object.keys(validators)]);
|
|
1566
|
+
for (const ruleName of allRules) {
|
|
1567
|
+
const results = matchRule(ruleName, tokens, 0, grammarMap, memo, null);
|
|
1568
|
+
const isMatch = results.some((result) => result.success && result.nextIndex === tokens.length);
|
|
1569
|
+
if (isMatch)
|
|
1570
|
+
return true;
|
|
1571
|
+
}
|
|
1572
|
+
return false;
|
|
1573
|
+
}
|
|
1574
|
+
const getters = {
|
|
1575
|
+
"color-models": () => Object.keys(colorModels),
|
|
1576
|
+
"color-spaces": () => Object.keys(colorSpaces),
|
|
1577
|
+
"named-colors": () => Object.keys(namedColors),
|
|
1578
|
+
"system-colors": () => Object.keys(systemColors),
|
|
1579
|
+
"output-types": () => {
|
|
1580
|
+
return Object.keys(formatters).filter((key) => {
|
|
1581
|
+
const type = formatters[key];
|
|
1582
|
+
return typeof type["fromBridge"] === "function" && typeof type["format"] === "function";
|
|
1583
|
+
});
|
|
1584
|
+
},
|
|
1585
|
+
plugins: () => Object.keys(plugins),
|
|
1586
|
+
"fit-methods": () => ["none", "clip", ...Object.keys(fitMethods)],
|
|
1587
|
+
};
|
|
1588
|
+
/**
|
|
1589
|
+
* Retrieve a list of registered items of a specified type.
|
|
1590
|
+
*
|
|
1591
|
+
* @param type - The getter type, e.g. "color-types".
|
|
1592
|
+
* @returns The array returned by the getter.
|
|
1593
|
+
*/
|
|
1594
|
+
export function get(type) {
|
|
1595
|
+
const fn = getters[type];
|
|
1596
|
+
return fn();
|
|
1597
|
+
}
|
|
1598
|
+
const registry = {
|
|
1599
|
+
customSpaces: new Set(),
|
|
1600
|
+
rectangularFunctions: new Set(),
|
|
1601
|
+
polarFunctions: new Set(),
|
|
1602
|
+
functionRules: new Set(),
|
|
1603
|
+
grammarExtensions: new Map(),
|
|
1604
|
+
};
|
|
1605
|
+
const registerers = {
|
|
1606
|
+
"named-color": (name, rgb) => {
|
|
1607
|
+
if (!Array.isArray(rgb) || rgb.length !== 3) {
|
|
1608
|
+
throw new Error(`RGB value must be an array of three numbers, received length ${rgb.length}.`);
|
|
1609
|
+
}
|
|
1610
|
+
const n = name.replace(/[^a-zA-Z]/g, "").toLowerCase();
|
|
1611
|
+
const names = namedColors;
|
|
1612
|
+
if (names[n])
|
|
1613
|
+
throw new Error(`<named-color> '${n}' is already registered.`);
|
|
1614
|
+
const duplicate = Object.entries(names).find(([, value]) => value.every((channel, i) => channel === rgb[i]));
|
|
1615
|
+
if (duplicate)
|
|
1616
|
+
throw new Error(`RGB value [${rgb.join(", ")}] is already registered as '${duplicate[0]}'.`);
|
|
1617
|
+
names[n] = rgb;
|
|
1618
|
+
},
|
|
1619
|
+
"color-function": (name, converter) => {
|
|
1620
|
+
const n = name.replace(/(?:\s+)/g, "").toLowerCase();
|
|
1621
|
+
const models = colorModels;
|
|
1622
|
+
if (typeof converter.components === "object" &&
|
|
1623
|
+
converter.components !== null &&
|
|
1624
|
+
!Array.isArray(converter.components)) {
|
|
1625
|
+
const normalized = {};
|
|
1626
|
+
for (const key of Object.keys(converter.components)) {
|
|
1627
|
+
normalized[key.toLowerCase()] = converter.components[key];
|
|
1628
|
+
}
|
|
1629
|
+
converter.components = normalized;
|
|
1630
|
+
}
|
|
1631
|
+
else {
|
|
1632
|
+
throw new TypeError("Converter.components must be a non-null object.");
|
|
1633
|
+
}
|
|
1634
|
+
if (typeof converter !== "object" || converter === null) {
|
|
1635
|
+
throw new TypeError("Converter must be a non-null object.");
|
|
1636
|
+
}
|
|
1637
|
+
const requiredFns = [
|
|
1638
|
+
["bridge", "string"],
|
|
1639
|
+
["toBridge", "function"],
|
|
1640
|
+
["fromBridge", "function"],
|
|
1641
|
+
];
|
|
1642
|
+
for (const [key, type] of requiredFns) {
|
|
1643
|
+
if (typeof converter[key] !== type) {
|
|
1644
|
+
throw new TypeError(`Converter.${String(key)} must be a ${type}.`);
|
|
1645
|
+
}
|
|
1646
|
+
}
|
|
1647
|
+
if (converter.bridge in colorModels === false) {
|
|
1648
|
+
throw new Error(`Converter.bridge '${converter.bridge}' does not correspond to any registered color model.`);
|
|
1649
|
+
}
|
|
1650
|
+
if ("targetGamut" in converter && converter.targetGamut !== null && typeof converter.targetGamut !== "string") {
|
|
1651
|
+
throw new TypeError(`Converter.targetGamut must be a string or null.`);
|
|
1652
|
+
}
|
|
1653
|
+
if ("supportsLegacy" in converter && typeof converter.supportsLegacy !== "boolean") {
|
|
1654
|
+
throw new TypeError(`Converter.supportsLegacy must be a boolean.`);
|
|
1655
|
+
}
|
|
1656
|
+
if ("alphaVariant" in converter && typeof converter.alphaVariant !== "string") {
|
|
1657
|
+
throw new TypeError(`Converter.alphaVariant must be a string.`);
|
|
1658
|
+
}
|
|
1659
|
+
const componentNames = Object.keys(converter.components);
|
|
1660
|
+
if (new Set(componentNames).size !== componentNames.length) {
|
|
1661
|
+
throw new Error("Converter.components must have unique component names.");
|
|
1662
|
+
}
|
|
1663
|
+
if (componentNames.includes("none")) {
|
|
1664
|
+
throw new Error('Converter.components cannot have a component named "none".');
|
|
1665
|
+
}
|
|
1666
|
+
models[n] = converter;
|
|
1667
|
+
if (converter.alphaVariant) {
|
|
1668
|
+
const av = converter.alphaVariant.replace(/(?:\s+)/g, "").toLowerCase();
|
|
1669
|
+
models[av] = converter;
|
|
1670
|
+
}
|
|
1671
|
+
cache.clear();
|
|
1672
|
+
const compKeys = Object.keys(converter.components);
|
|
1673
|
+
const isPolar = compKeys.some((k) => k === "hue" || k === "h");
|
|
1674
|
+
if (isPolar) {
|
|
1675
|
+
registry.polarFunctions.add(n);
|
|
1676
|
+
}
|
|
1677
|
+
else {
|
|
1678
|
+
registry.rectangularFunctions.add(n);
|
|
1679
|
+
}
|
|
1680
|
+
const compList = compKeys.join(" | ");
|
|
1681
|
+
const modernComponentsExpr = compKeys
|
|
1682
|
+
.map((key) => (key === "hue" || key === "h" ? "[ <hue> | none ]" : "[ <number> | <percentage> | none ]"))
|
|
1683
|
+
.join(" ");
|
|
1684
|
+
const legacyComponentsExpr = compKeys
|
|
1685
|
+
.map((key) => (key === "hue" || key === "h" ? "<hue>" : "[ <number> | <percentage> ]"))
|
|
1686
|
+
.join(" , ");
|
|
1687
|
+
const registerSyntaxForName = (fName) => {
|
|
1688
|
+
const baseRuleName = `<${fName}()>`;
|
|
1689
|
+
const modernRuleName = `<modern-${fName}-syntax>`;
|
|
1690
|
+
const legacyRuleName = `<legacy-${fName}-syntax>`;
|
|
1691
|
+
const relativeRuleName = `<relative-${fName}-syntax>`;
|
|
1692
|
+
const compRuleName = `<${fName}-relative-component>`;
|
|
1693
|
+
const alphaRuleName = `<${fName}-relative-alpha>`;
|
|
1694
|
+
const modernExpr = `${fName}( ${modernComponentsExpr} [ / [ <alpha-value> | none ] ]? )`;
|
|
1695
|
+
const legacyExpr = `${fName}( ${legacyComponentsExpr} [ , <alpha-value> ]? )`;
|
|
1696
|
+
const compExpr = `<number> | <percentage> | none | ${compList} | alpha`;
|
|
1697
|
+
const alphaExpr = `<alpha-value> | none | ${compList} | alpha`;
|
|
1698
|
+
const relativeExpr = `${fName}( from <color> ${compRuleName}{3} [ / ${alphaRuleName} ]? )`;
|
|
1699
|
+
const functionSubOptions = [modernRuleName, relativeRuleName];
|
|
1700
|
+
if (converter.supportsLegacy) {
|
|
1701
|
+
functionSubOptions.unshift(legacyRuleName);
|
|
1702
|
+
}
|
|
1703
|
+
const baseExpr = `[ ${functionSubOptions.join(" | ")} ]`;
|
|
1704
|
+
grammarMap[compRuleName] = parseExpression(compExpr);
|
|
1705
|
+
grammarMap[alphaRuleName] = parseExpression(alphaExpr);
|
|
1706
|
+
grammarMap[modernRuleName] = parseExpression(modernExpr);
|
|
1707
|
+
grammarMap[relativeRuleName] = parseExpression(relativeExpr);
|
|
1708
|
+
if (converter.supportsLegacy) {
|
|
1709
|
+
grammarMap[legacyRuleName] = parseExpression(legacyExpr);
|
|
1710
|
+
parsers[legacyRuleName] = createLegacyColorParser(n, compKeys);
|
|
1711
|
+
}
|
|
1712
|
+
grammarMap[baseRuleName] = parseExpression(baseExpr);
|
|
1713
|
+
registry.functionRules.add(baseRuleName);
|
|
1714
|
+
parsers[baseRuleName] = (node) => parseNode(node.value[0]);
|
|
1715
|
+
parsers[modernRuleName] = createModernColorParser(n, compKeys);
|
|
1716
|
+
parsers[relativeRuleName] = createRelativeColorParser(n, compKeys);
|
|
1717
|
+
};
|
|
1718
|
+
try {
|
|
1719
|
+
registerSyntaxForName(n);
|
|
1720
|
+
if (converter.alphaVariant) {
|
|
1721
|
+
const av = converter.alphaVariant.replace(/(?:\s+)/g, "").toLowerCase();
|
|
1722
|
+
registerSyntaxForName(av);
|
|
1723
|
+
}
|
|
1724
|
+
const masterFunctionChoiceExpr = Array.from(registry.functionRules).join(" | ");
|
|
1725
|
+
grammarMap["<custom-color-function>"] = parseExpression(masterFunctionChoiceExpr);
|
|
1726
|
+
if (registry.rectangularFunctions.size > 0) {
|
|
1727
|
+
const rectChoiceExpr = Array.from(registry.rectangularFunctions).join(" | ");
|
|
1728
|
+
grammarMap["<custom-rectangular-space>"] = parseExpression(rectChoiceExpr);
|
|
1729
|
+
}
|
|
1730
|
+
if (registry.polarFunctions.size > 0) {
|
|
1731
|
+
const polarChoiceExpr = Array.from(registry.polarFunctions).join(" | ");
|
|
1732
|
+
grammarMap["<custom-polar-space>"] = parseExpression(polarChoiceExpr);
|
|
1733
|
+
}
|
|
1734
|
+
}
|
|
1735
|
+
catch (error) {
|
|
1736
|
+
throw new Error(`Failed to dynamically bind custom color definition execution parameters for "${n}".`, {
|
|
1737
|
+
cause: error,
|
|
1738
|
+
});
|
|
1739
|
+
}
|
|
1740
|
+
},
|
|
1741
|
+
"color-space": (name, converter) => {
|
|
1742
|
+
const n = name.replace(/(?:\s+)/g, "-").toLowerCase();
|
|
1743
|
+
const spaces = colorSpaces;
|
|
1744
|
+
const models = colorModels;
|
|
1745
|
+
if (typeof converter !== "object" || converter === null) {
|
|
1746
|
+
throw new TypeError("Converter must be a non-null object.");
|
|
1747
|
+
}
|
|
1748
|
+
if (!Array.isArray(converter.components) || converter.components.some((c) => typeof c !== "string")) {
|
|
1749
|
+
throw new TypeError("Converter.components must be an array of strings.");
|
|
1750
|
+
}
|
|
1751
|
+
if (typeof converter.bridge !== "string") {
|
|
1752
|
+
throw new TypeError("Converter.bridge must be a string.");
|
|
1753
|
+
}
|
|
1754
|
+
const matrixChecks = [
|
|
1755
|
+
["toBridgeMatrix", "toBridgeMatrix"],
|
|
1756
|
+
["fromBridgeMatrix", "fromBridgeMatrix"],
|
|
1757
|
+
];
|
|
1758
|
+
for (const [key, label] of matrixChecks) {
|
|
1759
|
+
const matrix = converter[key];
|
|
1760
|
+
if (!Array.isArray(matrix) ||
|
|
1761
|
+
matrix.some((row) => !Array.isArray(row) || row.some((val) => typeof val !== "number"))) {
|
|
1762
|
+
throw new TypeError(`Converter.${label} must be a 2D array of numbers.`);
|
|
1763
|
+
}
|
|
1764
|
+
}
|
|
1765
|
+
if (converter.bridge in colorModels === false) {
|
|
1766
|
+
throw new Error(`Converter.bridge '${converter.bridge}' does not correspond to any registered color model.`);
|
|
1767
|
+
}
|
|
1768
|
+
if ("targetGamut" in converter && converter.targetGamut !== null) {
|
|
1769
|
+
throw new TypeError("Converter.targetGamut must be null if provided.");
|
|
1770
|
+
}
|
|
1771
|
+
if ("toLinear" in converter && typeof converter.toLinear !== "function") {
|
|
1772
|
+
throw new TypeError("Converter.toLinear must be a function if provided.");
|
|
1773
|
+
}
|
|
1774
|
+
if ("fromLinear" in converter && typeof converter.fromLinear !== "function") {
|
|
1775
|
+
throw new TypeError("Converter.fromLinear must be a function if provided.");
|
|
1776
|
+
}
|
|
1777
|
+
const modelConv = spaceConverterToModelConverter(n, converter);
|
|
1778
|
+
spaces[n] = modelConv;
|
|
1779
|
+
models[n] = modelConv;
|
|
1780
|
+
cache.clear();
|
|
1781
|
+
registry.customSpaces.add(n);
|
|
1782
|
+
const dynamicChoiceExpression = Array.from(registry.customSpaces).join(" | ");
|
|
1783
|
+
const compList = converter.components.join(" | ");
|
|
1784
|
+
const compRuleName = `<${n}-relative-component>`;
|
|
1785
|
+
const alphaRuleName = `<${n}-relative-alpha>`;
|
|
1786
|
+
const syntaxRuleName = `<relative-${n}-color-syntax>`;
|
|
1787
|
+
const compExpr = `<number> | <percentage> | none | ${compList} | alpha`;
|
|
1788
|
+
const alphaExpr = `<alpha-value> | none | ${compList} | alpha`;
|
|
1789
|
+
const syntaxExpr = `color( from <color> ${n} ${compRuleName}{3} [ / ${alphaRuleName} ]? )`;
|
|
1790
|
+
const relativeChoiceExpr = Array.from(registry.customSpaces)
|
|
1791
|
+
.map((space) => `<relative-${space}-color-syntax>`)
|
|
1792
|
+
.join(" | ");
|
|
1793
|
+
try {
|
|
1794
|
+
grammarMap["<custom-color-space>"] = parseExpression(dynamicChoiceExpression);
|
|
1795
|
+
grammarMap[compRuleName] = parseExpression(compExpr);
|
|
1796
|
+
grammarMap[alphaRuleName] = parseExpression(alphaExpr);
|
|
1797
|
+
grammarMap[syntaxRuleName] = parseExpression(syntaxExpr);
|
|
1798
|
+
grammarMap["<relative-custom-color-syntax>"] = parseExpression(relativeChoiceExpr);
|
|
1799
|
+
parsers[syntaxRuleName] = createRelativeColorFunctionParser;
|
|
1800
|
+
}
|
|
1801
|
+
catch (error) {
|
|
1802
|
+
throw new Error(`Failed to dynamically bind color space "${n}" to grammarMap.`, { cause: error });
|
|
1803
|
+
}
|
|
1804
|
+
},
|
|
1805
|
+
parser: (name, spec) => {
|
|
1806
|
+
const n = name.trim().toLowerCase();
|
|
1807
|
+
if (!/^<.+>$/.test(n)) {
|
|
1808
|
+
throw new Error(`Grammar rule name must start with '<' and end with '>', received '${n}'.`);
|
|
1809
|
+
}
|
|
1810
|
+
if (typeof spec !== "object" || spec === null) {
|
|
1811
|
+
throw new TypeError("Grammar rule specification must be a non-null object.");
|
|
1812
|
+
}
|
|
1813
|
+
if (typeof spec.rule !== "string") {
|
|
1814
|
+
throw new TypeError("Grammar rule specification must include a 'rule' string.");
|
|
1815
|
+
}
|
|
1816
|
+
if (spec.parse !== undefined && typeof spec.parse !== "function") {
|
|
1817
|
+
throw new TypeError("Grammar rule specification 'parse' must be a function if provided.");
|
|
1818
|
+
}
|
|
1819
|
+
if (spec.appendTo !== undefined && typeof spec.appendTo !== "string" && !Array.isArray(spec.appendTo)) {
|
|
1820
|
+
throw new TypeError("Grammar rule 'appendTo' must be a string or an array of strings.");
|
|
1821
|
+
}
|
|
1822
|
+
try {
|
|
1823
|
+
grammarMap[n] = parseExpression(spec.rule);
|
|
1824
|
+
if (spec.parse) {
|
|
1825
|
+
parsers[n] = spec.parse;
|
|
1826
|
+
}
|
|
1827
|
+
if (spec.appendTo) {
|
|
1828
|
+
const targets = Array.isArray(spec.appendTo) ? spec.appendTo : [spec.appendTo];
|
|
1829
|
+
for (let target of targets) {
|
|
1830
|
+
target = target.trim().toLowerCase();
|
|
1831
|
+
if (!/^<.+>$/.test(target)) {
|
|
1832
|
+
throw new Error(`Target appendTo rule must be formatted as '<rule-name>', received '${target}'.`);
|
|
1833
|
+
}
|
|
1834
|
+
if (!registry.grammarExtensions.has(target)) {
|
|
1835
|
+
registry.grammarExtensions.set(target, new Set());
|
|
1836
|
+
}
|
|
1837
|
+
registry.grammarExtensions.get(target).add(n);
|
|
1838
|
+
const baseTargetName = `<${target.slice(1, -1)}-core-base>`;
|
|
1839
|
+
if (!grammarMap[baseTargetName]) {
|
|
1840
|
+
if (!grammarMap[target]) {
|
|
1841
|
+
throw new Error(`Target rule '${target}' does not exist in the grammar map to append to.`);
|
|
1842
|
+
}
|
|
1843
|
+
grammarMap[baseTargetName] = grammarMap[target];
|
|
1844
|
+
}
|
|
1845
|
+
const extensions = Array.from(registry.grammarExtensions.get(target));
|
|
1846
|
+
const appendedChoiceExpr = `[ ${baseTargetName} | ${extensions.join(" | ")} ]`;
|
|
1847
|
+
grammarMap[target] = parseExpression(appendedChoiceExpr);
|
|
1848
|
+
parsers[target] = (node) => parseNode(node.value[0]);
|
|
1849
|
+
parsers[baseTargetName] = (node) => parseNode(node.value[0]);
|
|
1850
|
+
}
|
|
1851
|
+
}
|
|
1852
|
+
}
|
|
1853
|
+
catch (error) {
|
|
1854
|
+
throw new Error(`Failed to register custom grammar rule for "${n}".`, { cause: error });
|
|
1855
|
+
}
|
|
1856
|
+
},
|
|
1857
|
+
formatter: (name, converter) => {
|
|
1858
|
+
const n = name.trim().toLowerCase();
|
|
1859
|
+
if (typeof converter !== "object" || converter === null) {
|
|
1860
|
+
throw new TypeError("Formatter must be a non-null object.");
|
|
1861
|
+
}
|
|
1862
|
+
if (typeof converter.bridge !== "string") {
|
|
1863
|
+
throw new TypeError("Formatter.bridge must be a string.");
|
|
1864
|
+
}
|
|
1865
|
+
if (typeof converter.fromBridge !== "function") {
|
|
1866
|
+
throw new TypeError("Formatter.fromBridge must be a function.");
|
|
1867
|
+
}
|
|
1868
|
+
if (typeof converter.format !== "function") {
|
|
1869
|
+
throw new TypeError("Formatter.format must be a function.");
|
|
1870
|
+
}
|
|
1871
|
+
if (converter.bridge in colorModels === false) {
|
|
1872
|
+
throw new Error(`Formatter.bridge '${converter.bridge}' does not correspond to any registered color model.`);
|
|
1873
|
+
}
|
|
1874
|
+
const registeredFormatters = formatters;
|
|
1875
|
+
if (n in registeredFormatters) {
|
|
1876
|
+
throw new Error(`Formatter '${n}' is already registered.`);
|
|
1877
|
+
}
|
|
1878
|
+
registeredFormatters[n] = converter;
|
|
1879
|
+
},
|
|
1880
|
+
"fit-method": (name, method) => {
|
|
1881
|
+
const n = name.trim().replace(/\s+/g, "-").toLowerCase();
|
|
1882
|
+
const methods = fitMethods;
|
|
1883
|
+
if (n in methods)
|
|
1884
|
+
throw new Error(`Fit method '${n}' already exists.`);
|
|
1885
|
+
if (typeof method !== "function")
|
|
1886
|
+
throw new TypeError("Fit method must be a function.");
|
|
1887
|
+
methods[n] = method;
|
|
1888
|
+
},
|
|
1889
|
+
};
|
|
1890
|
+
/**
|
|
1891
|
+
* Bulk register multiple converters of a specified type.
|
|
1892
|
+
*
|
|
1893
|
+
* @param type - The converter type, e.g. "color-function".
|
|
1894
|
+
* @param entries - Array of { name, value } objects.
|
|
1895
|
+
*/
|
|
1896
|
+
export function register(type, entries) {
|
|
1897
|
+
const fn = registerers[type];
|
|
1898
|
+
for (const entry of entries)
|
|
1899
|
+
fn(entry.name, entry.value); // eslint-disable-line @typescript-eslint/no-explicit-any
|
|
1900
|
+
}
|