@tenphi/tasty 0.0.0-snapshot.27b6708 → 0.0.0-snapshot.28eb83c
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/README.md +38 -54
- package/dist/{babel-BUQGeOXA.d.ts → babel-D6lOuSwk.d.ts} +2 -2
- package/dist/chunks/async-storage-DKK-wTD4.js.map +1 -0
- package/dist/chunks/build-config-BBThSdlo.js +45 -0
- package/dist/chunks/build-config-BBThSdlo.js.map +1 -0
- package/dist/{collector-DTahQUiV.js → chunks/collector-DcViW2ZQ.js} +28 -14
- package/dist/chunks/collector-DcViW2ZQ.js.map +1 -0
- package/dist/chunks/config-engine-DQVQK4NU.js +403 -0
- package/dist/chunks/config-engine-DQVQK4NU.js.map +1 -0
- package/dist/chunks/css-definitions-BZ9vm0ci.js +1284 -0
- package/dist/chunks/css-definitions-BZ9vm0ci.js.map +1 -0
- package/dist/chunks/css-resources-Cyl_axbI.js +149 -0
- package/dist/chunks/css-resources-Cyl_axbI.js.map +1 -0
- package/dist/chunks/debug-Vyauml7U.js +583 -0
- package/dist/chunks/debug-Vyauml7U.js.map +1 -0
- package/dist/chunks/dsl-CXvoaLnj.js +2097 -0
- package/dist/chunks/dsl-CXvoaLnj.js.map +1 -0
- package/dist/chunks/hydration-CLdVKcH2.js +59 -0
- package/dist/chunks/hydration-CLdVKcH2.js.map +1 -0
- package/dist/chunks/react-runtime-so_X_yMo.js +1526 -0
- package/dist/chunks/react-runtime-so_X_yMo.js.map +1 -0
- package/dist/chunks/runtime-engine-DQ5laRou.js +3056 -0
- package/dist/chunks/runtime-engine-DQ5laRou.js.map +1 -0
- package/dist/{merge-styles-oklji0KB.js → chunks/shared-utils-OIg0N_dp.js} +42 -3
- package/dist/chunks/shared-utils-OIg0N_dp.js.map +1 -0
- package/dist/{config-B5kHzuNz.js → chunks/style-engine-CBuOt1e4.js} +2387 -7442
- package/dist/chunks/style-engine-CBuOt1e4.js.map +1 -0
- package/dist/{css-writer-B-J87ncv.js → chunks/zero-engine-BFh7gKbK.js} +3 -3
- package/dist/chunks/zero-engine-BFh7gKbK.js.map +1 -0
- package/dist/{collector-DUaHCcTS.d.ts → collector-CAmndLXe.d.ts} +23 -3
- package/dist/{config-YsxGv4tq.d.ts → config-BWYrv_Aa.d.ts} +141 -37
- package/dist/core/index.d.ts +5 -5
- package/dist/core/index.js +9 -6
- package/dist/{index-Cd45t5NM.d.ts → index-BzAC3p06.d.ts} +118 -23
- package/dist/{index-PqN-DIpn.d.ts → index-D4kRLj2o.d.ts} +128 -68
- package/dist/index.d.ts +5 -5
- package/dist/index.js +10 -922
- package/dist/{merge-styles-CU7JbEwg.d.ts → merge-styles-BFktNP8J.d.ts} +2 -2
- package/dist/ssr/astro-client.js +1 -1
- package/dist/ssr/astro-middleware-extract-static.d.ts +11 -0
- package/dist/ssr/astro-middleware-extract-static.js +9 -0
- package/dist/ssr/astro-middleware-extract-static.js.map +1 -0
- package/dist/ssr/astro-middleware-extract.d.ts +11 -0
- package/dist/ssr/astro-middleware-extract.js +9 -0
- package/dist/ssr/astro-middleware-extract.js.map +1 -0
- package/dist/ssr/astro-middleware-static.d.ts +3 -1
- package/dist/ssr/astro-middleware.d.ts +3 -1
- package/dist/ssr/astro.d.ts +45 -3
- package/dist/ssr/astro.js +157 -14
- package/dist/ssr/astro.js.map +1 -1
- package/dist/ssr/index.d.ts +7 -7
- package/dist/ssr/index.js +4 -4
- package/dist/ssr/index.js.map +1 -1
- package/dist/ssr/next-config.d.ts +66 -0
- package/dist/ssr/next-config.js +135 -0
- package/dist/ssr/next-config.js.map +1 -0
- package/dist/ssr/next.d.ts +9 -2
- package/dist/ssr/next.js +26 -10
- package/dist/ssr/next.js.map +1 -1
- package/dist/static/index.d.ts +2 -2
- package/dist/static/index.js +1 -1
- package/dist/zero/babel.d.ts +1 -1
- package/dist/zero/babel.js +25 -24
- package/dist/zero/babel.js.map +1 -1
- package/dist/zero/index.d.ts +1 -1
- package/dist/zero/index.js +1 -1
- package/dist/zero/next.d.ts +1 -1
- package/docs/README.md +5 -3
- package/docs/adoption.md +3 -3
- package/docs/ai-agents.md +83 -78
- package/docs/comparison.md +12 -12
- package/docs/configuration.md +47 -47
- package/docs/debug.md +19 -7
- package/docs/dsl.md +3 -3
- package/docs/getting-started.md +10 -10
- package/docs/injector.md +62 -25
- package/docs/methodology.md +3 -3
- package/docs/migration-v3.md +49 -49
- package/docs/plugins.md +37 -33
- package/docs/react-api.md +2 -2
- package/docs/runtime-benchmarks.md +351 -0
- package/docs/ssr.md +219 -61
- package/docs/styles.md +43 -11
- package/docs/tasty-static.md +136 -103
- package/package.json +80 -11
- package/dist/async-storage-DKK-wTD4.js.map +0 -1
- package/dist/collector-DTahQUiV.js.map +0 -1
- package/dist/config-B5kHzuNz.js.map +0 -1
- package/dist/context-CA8YKeMn.js +0 -24
- package/dist/context-CA8YKeMn.js.map +0 -1
- package/dist/core-Dr4u1NVD.js +0 -1566
- package/dist/core-Dr4u1NVD.js.map +0 -1
- package/dist/css-writer-B-J87ncv.js.map +0 -1
- package/dist/format-global-rules-DklyaXv-.js +0 -22
- package/dist/format-global-rules-DklyaXv-.js.map +0 -1
- package/dist/format-rules-DKOA-6qu.js +0 -130
- package/dist/format-rules-DKOA-6qu.js.map +0 -1
- package/dist/hydrate-OeMX99We.js +0 -37
- package/dist/hydrate-OeMX99We.js.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/keyframes-CV8azJf3.js +0 -493
- package/dist/keyframes-CV8azJf3.js.map +0 -1
- package/dist/merge-styles-oklji0KB.js.map +0 -1
- package/dist/resolve-recipes-DTG81rzl.js +0 -144
- package/dist/resolve-recipes-DTG81rzl.js.map +0 -1
- /package/dist/{async-storage-DKK-wTD4.js → chunks/async-storage-DKK-wTD4.js} +0 -0
|
@@ -0,0 +1,2097 @@
|
|
|
1
|
+
import { a as warnOnceDev, o as isDevEnv } from "./shared-utils-OIg0N_dp.js";
|
|
2
|
+
//#region src/parser/const.ts
|
|
3
|
+
const VALUE_KEYWORDS = new Set([
|
|
4
|
+
"auto",
|
|
5
|
+
"max-content",
|
|
6
|
+
"min-content",
|
|
7
|
+
"fit-content",
|
|
8
|
+
"stretch",
|
|
9
|
+
"initial",
|
|
10
|
+
"inherit",
|
|
11
|
+
"revert",
|
|
12
|
+
"unset",
|
|
13
|
+
"revert-layer"
|
|
14
|
+
]);
|
|
15
|
+
const CSS_WIDE_KEYWORDS = new Set([
|
|
16
|
+
"initial",
|
|
17
|
+
"inherit",
|
|
18
|
+
"revert",
|
|
19
|
+
"unset",
|
|
20
|
+
"revert-layer"
|
|
21
|
+
]);
|
|
22
|
+
/**
|
|
23
|
+
* Color functions that *derive* a color from other colors. They take no alpha
|
|
24
|
+
* channel, so opacity has to wrap the whole call in `color-mix()` instead of
|
|
25
|
+
* being appended after a slash.
|
|
26
|
+
*/
|
|
27
|
+
const DERIVED_COLOR_FUNCS_LIST = [
|
|
28
|
+
"color-mix",
|
|
29
|
+
"color-contrast",
|
|
30
|
+
"contrast-color",
|
|
31
|
+
"light-dark"
|
|
32
|
+
];
|
|
33
|
+
const DERIVED_COLOR_FUNCS = new Set(DERIVED_COLOR_FUNCS_LIST);
|
|
34
|
+
/**
|
|
35
|
+
* Every color function the parser recognizes. The ones outside
|
|
36
|
+
* `DERIVED_COLOR_FUNCS` take channels as arguments, optionally followed by
|
|
37
|
+
* `/ <alpha>` — which is where an opacity suffix writes.
|
|
38
|
+
*/
|
|
39
|
+
const COLOR_FUNCS = new Set([
|
|
40
|
+
"rgb",
|
|
41
|
+
"rgba",
|
|
42
|
+
"hsl",
|
|
43
|
+
"hsla",
|
|
44
|
+
"hwb",
|
|
45
|
+
"lab",
|
|
46
|
+
"lch",
|
|
47
|
+
"oklab",
|
|
48
|
+
"oklch",
|
|
49
|
+
"color",
|
|
50
|
+
"device-cmyk",
|
|
51
|
+
"gray",
|
|
52
|
+
...DERIVED_COLOR_FUNCS_LIST
|
|
53
|
+
]);
|
|
54
|
+
/** A value that is a single `name(args)` call: captures the name and the args. */
|
|
55
|
+
const RE_FUNC_CALL = /^([a-z][a-z0-9-]*)\((.+)\)$/i;
|
|
56
|
+
const RE_UNIT_NUM = /^[+-]?(?:\d*\.\d+|\d+)([a-z][a-z0-9]*)$/;
|
|
57
|
+
const RE_NUMBER = /^[+-]?(?:\d*\.\d+|\d+)$/;
|
|
58
|
+
const RE_HEX = /^(?:[0-9a-f]{3,4}|[0-9a-f]{6}(?:[0-9a-f]{2})?)$/;
|
|
59
|
+
const RE_RAW_UNIT = /^([+-]?(?:\d*\.\d+|\d+))([a-z%]+)$/;
|
|
60
|
+
const CANONICAL_FUNC_CASE = new Map([
|
|
61
|
+
["translatex", "translateX"],
|
|
62
|
+
["translatey", "translateY"],
|
|
63
|
+
["translatez", "translateZ"],
|
|
64
|
+
["scalex", "scaleX"],
|
|
65
|
+
["scaley", "scaleY"],
|
|
66
|
+
["scalez", "scaleZ"],
|
|
67
|
+
["rotatex", "rotateX"],
|
|
68
|
+
["rotatey", "rotateY"],
|
|
69
|
+
["rotatez", "rotateZ"],
|
|
70
|
+
["skewx", "skewX"],
|
|
71
|
+
["skewy", "skewY"]
|
|
72
|
+
]);
|
|
73
|
+
function canonicalFuncName(lowered) {
|
|
74
|
+
return CANONICAL_FUNC_CASE.get(lowered) ?? lowered;
|
|
75
|
+
}
|
|
76
|
+
//#endregion
|
|
77
|
+
//#region src/utils/string.ts
|
|
78
|
+
function toSnakeCase(str) {
|
|
79
|
+
return str.replace(/[A-Z]/g, (s) => `-${s.toLowerCase()}`);
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Normalize a DSL identifier to the CSS custom-property name it emits.
|
|
83
|
+
*
|
|
84
|
+
* CSS custom-property names are case-sensitive, so `$myVar` has to keep its
|
|
85
|
+
* inner case to reference `--myVar`. A leading capital is the one exception:
|
|
86
|
+
* `$Foo` is not a supported way to name a property, so the first character is
|
|
87
|
+
* folded rather than kebab-cased (`--foo`, not `---foo`) and every later
|
|
88
|
+
* character is left alone.
|
|
89
|
+
*
|
|
90
|
+
* Every place that turns a `$name` / `#name` identifier into a CSS name goes
|
|
91
|
+
* through this, so authoring a token and referencing it always agree.
|
|
92
|
+
*/
|
|
93
|
+
function normalizeDslName(name) {
|
|
94
|
+
if (!name) return name;
|
|
95
|
+
const first = name[0];
|
|
96
|
+
const lower = first.toLowerCase();
|
|
97
|
+
return lower === first ? name : lower + name.slice(1);
|
|
98
|
+
}
|
|
99
|
+
/** A DSL identifier: `$name`, `$$name`, `#name` or `##name`. */
|
|
100
|
+
const RE_DSL_IDENTIFIER = /(\$\$?|##?)([a-zA-Z_][a-zA-Z0-9_-]*)/g;
|
|
101
|
+
/**
|
|
102
|
+
* Case-fold a DSL string for parsing, preserving the case of custom-property
|
|
103
|
+
* names.
|
|
104
|
+
*
|
|
105
|
+
* Everything the parser matches on — keywords, units, modifiers, function names,
|
|
106
|
+
* color-token names — is compared lowercase, so the whole string used to be
|
|
107
|
+
* lowercased up front. That also folded custom-property names, which are
|
|
108
|
+
* case-sensitive in CSS: `$myVar` referenced `var(--myvar)` while the token
|
|
109
|
+
* definition emitted `--myVar`, so the two could never meet.
|
|
110
|
+
*
|
|
111
|
+
* Identifier bodies now keep their case (their first character still folds, see
|
|
112
|
+
* {@link normalizeDslName}); everything else is lowercased exactly as before.
|
|
113
|
+
* Hex literals are excluded — `#FF0000` is a color, not a name, and must fold to
|
|
114
|
+
* match {@link RE_HEX}.
|
|
115
|
+
*/
|
|
116
|
+
function foldDslCase(src) {
|
|
117
|
+
const folded = src.toLowerCase();
|
|
118
|
+
if (folded === src || !src.includes("$") && !src.includes("#")) return folded;
|
|
119
|
+
RE_DSL_IDENTIFIER.lastIndex = 0;
|
|
120
|
+
let out = "";
|
|
121
|
+
let last = 0;
|
|
122
|
+
let match;
|
|
123
|
+
while (match = RE_DSL_IDENTIFIER.exec(src)) {
|
|
124
|
+
const [whole, prefix, body] = match;
|
|
125
|
+
out += src.slice(last, match.index).toLowerCase();
|
|
126
|
+
out += prefix + (prefix[0] === "#" && RE_HEX.test(body.toLowerCase()) ? body.toLowerCase() : normalizeDslName(body));
|
|
127
|
+
last = match.index + whole.length;
|
|
128
|
+
}
|
|
129
|
+
return out + src.slice(last).toLowerCase();
|
|
130
|
+
}
|
|
131
|
+
//#endregion
|
|
132
|
+
//#region src/utils/color-math.ts
|
|
133
|
+
const OKLab_to_LMS_M = [
|
|
134
|
+
[
|
|
135
|
+
1,
|
|
136
|
+
.3963377773761749,
|
|
137
|
+
.2158037573099136
|
|
138
|
+
],
|
|
139
|
+
[
|
|
140
|
+
1,
|
|
141
|
+
-.1055613458156586,
|
|
142
|
+
-.0638541728258133
|
|
143
|
+
],
|
|
144
|
+
[
|
|
145
|
+
1,
|
|
146
|
+
-.0894841775298119,
|
|
147
|
+
-1.2914855480194092
|
|
148
|
+
]
|
|
149
|
+
];
|
|
150
|
+
const LMS_to_linear_sRGB_M = [
|
|
151
|
+
[
|
|
152
|
+
4.076741636075959,
|
|
153
|
+
-3.307711539258062,
|
|
154
|
+
.2309699031821041
|
|
155
|
+
],
|
|
156
|
+
[
|
|
157
|
+
-1.2684379732850313,
|
|
158
|
+
2.6097573492876878,
|
|
159
|
+
-.3413193760026569
|
|
160
|
+
],
|
|
161
|
+
[
|
|
162
|
+
-.004196076138675526,
|
|
163
|
+
-.703418617935936,
|
|
164
|
+
1.7076146940746113
|
|
165
|
+
]
|
|
166
|
+
];
|
|
167
|
+
const OKLab_to_linear_sRGB_coefficients = [
|
|
168
|
+
[[-1.8817030993265873, -.8093650129914302], [
|
|
169
|
+
1.19086277,
|
|
170
|
+
1.76576728,
|
|
171
|
+
.59662641,
|
|
172
|
+
.75515197,
|
|
173
|
+
.56771245
|
|
174
|
+
]],
|
|
175
|
+
[[1.8144407988010998, -1.194452667805235], [
|
|
176
|
+
.73956515,
|
|
177
|
+
-.45954404,
|
|
178
|
+
.08285427,
|
|
179
|
+
.1254107,
|
|
180
|
+
.14503204
|
|
181
|
+
]],
|
|
182
|
+
[[.13110757611180954, 1.813339709266608], [
|
|
183
|
+
1.35733652,
|
|
184
|
+
-.00915799,
|
|
185
|
+
-1.1513021,
|
|
186
|
+
-.50559606,
|
|
187
|
+
.00692167
|
|
188
|
+
]]
|
|
189
|
+
];
|
|
190
|
+
const dotXY = (a, b) => a[0] * b[0] + a[1] * b[1];
|
|
191
|
+
const INV_GAMMA = 1 / 2.4;
|
|
192
|
+
function srgbLinearToGamma(val) {
|
|
193
|
+
const sign = val < 0 ? -1 : 1;
|
|
194
|
+
const abs = Math.abs(val);
|
|
195
|
+
return abs > .0031308 ? sign * (1.055 * abs ** INV_GAMMA - .055) : 12.92 * val;
|
|
196
|
+
}
|
|
197
|
+
const TAU = 2 * Math.PI;
|
|
198
|
+
const K1 = .206;
|
|
199
|
+
const K2 = .03;
|
|
200
|
+
const K3 = 1.206 / 1.03;
|
|
201
|
+
const clamp$1 = (value, min, max) => Math.max(Math.min(value, max), min);
|
|
202
|
+
const constrainAngle = (angle) => (angle % 360 + 360) % 360;
|
|
203
|
+
const toe = (x) => .5 * (K3 * x - K1 + Math.sqrt((K3 * x - K1) * (K3 * x - K1) + 4 * K2 * K3 * x));
|
|
204
|
+
const toeInv = (x) => (x ** 2 + K1 * x) / (K3 * (x + K2));
|
|
205
|
+
const OKHST_REF_EPS = .05;
|
|
206
|
+
function yToL(y) {
|
|
207
|
+
return toe(Math.cbrt(Math.max(0, y)));
|
|
208
|
+
}
|
|
209
|
+
function yFromTone(t, eps = OKHST_REF_EPS) {
|
|
210
|
+
const den = Math.log(1 + eps) - Math.log(eps);
|
|
211
|
+
return Math.exp(t / 100 * den + Math.log(eps)) - eps;
|
|
212
|
+
}
|
|
213
|
+
function fromTone(t, eps = OKHST_REF_EPS) {
|
|
214
|
+
return yToL(yFromTone(t, eps));
|
|
215
|
+
}
|
|
216
|
+
const oklabToLinearSrgb = (lab) => {
|
|
217
|
+
const L = lab[0];
|
|
218
|
+
const a = lab[1];
|
|
219
|
+
const b = lab[2];
|
|
220
|
+
const l_ = L + .3963377773761749 * a + .2158037573099136 * b;
|
|
221
|
+
const m_ = L - .1055613458156586 * a - .0638541728258133 * b;
|
|
222
|
+
const s_ = L - .0894841775298119 * a - 1.2914855480194092 * b;
|
|
223
|
+
const l = l_ * l_ * l_;
|
|
224
|
+
const m = m_ * m_ * m_;
|
|
225
|
+
const s = s_ * s_ * s_;
|
|
226
|
+
return [
|
|
227
|
+
4.076741636075959 * l - 3.307711539258062 * m + .2309699031821041 * s,
|
|
228
|
+
-1.2684379732850313 * l + 2.6097573492876878 * m - .3413193760026569 * s,
|
|
229
|
+
-.004196076138675526 * l - .703418617935936 * m + 1.7076146940746113 * s
|
|
230
|
+
];
|
|
231
|
+
};
|
|
232
|
+
const computeMaxSaturationOKLC = (a, b) => {
|
|
233
|
+
const okCoeff = OKLab_to_linear_sRGB_coefficients;
|
|
234
|
+
const lmsToRgb = LMS_to_linear_sRGB_M;
|
|
235
|
+
const tmp2 = [a, b];
|
|
236
|
+
const tmp3 = [
|
|
237
|
+
0,
|
|
238
|
+
a,
|
|
239
|
+
b
|
|
240
|
+
];
|
|
241
|
+
let chnlCoeff;
|
|
242
|
+
let chnlLMS;
|
|
243
|
+
if (dotXY(okCoeff[0][0], tmp2) > 1) {
|
|
244
|
+
chnlCoeff = okCoeff[0][1];
|
|
245
|
+
chnlLMS = lmsToRgb[0];
|
|
246
|
+
} else if (dotXY(okCoeff[1][0], tmp2) > 1) {
|
|
247
|
+
chnlCoeff = okCoeff[1][1];
|
|
248
|
+
chnlLMS = lmsToRgb[1];
|
|
249
|
+
} else {
|
|
250
|
+
chnlCoeff = okCoeff[2][1];
|
|
251
|
+
chnlLMS = lmsToRgb[2];
|
|
252
|
+
}
|
|
253
|
+
const [k0, k1, k2, k3, k4] = chnlCoeff;
|
|
254
|
+
const [wl, wm, ws] = chnlLMS;
|
|
255
|
+
let sat = k0 + k1 * a + k2 * b + k3 * (a * a) + k4 * a * b;
|
|
256
|
+
const dotYZ = (mat, vec) => mat[1] * vec[1] + mat[2] * vec[2];
|
|
257
|
+
const kl = dotYZ(OKLab_to_LMS_M[0], tmp3);
|
|
258
|
+
const km = dotYZ(OKLab_to_LMS_M[1], tmp3);
|
|
259
|
+
const ks = dotYZ(OKLab_to_LMS_M[2], tmp3);
|
|
260
|
+
const l_ = 1 + sat * kl;
|
|
261
|
+
const m_ = 1 + sat * km;
|
|
262
|
+
const s_ = 1 + sat * ks;
|
|
263
|
+
const l = l_ ** 3;
|
|
264
|
+
const m = m_ ** 3;
|
|
265
|
+
const s = s_ ** 3;
|
|
266
|
+
const lds = 3 * kl * (l_ * l_);
|
|
267
|
+
const mds = 3 * km * (m_ * m_);
|
|
268
|
+
const sds = 3 * ks * (s_ * s_);
|
|
269
|
+
const lds2 = 6 * (kl * kl) * l_;
|
|
270
|
+
const mds2 = 6 * (km * km) * m_;
|
|
271
|
+
const sds2 = 6 * (ks * ks) * s_;
|
|
272
|
+
const f = wl * l + wm * m + ws * s;
|
|
273
|
+
const f1 = wl * lds + wm * mds + ws * sds;
|
|
274
|
+
const f2 = wl * lds2 + wm * mds2 + ws * sds2;
|
|
275
|
+
sat = sat - f * f1 / (f1 * f1 - .5 * f * f2);
|
|
276
|
+
return sat;
|
|
277
|
+
};
|
|
278
|
+
const findCuspOKLCH = (a, b) => {
|
|
279
|
+
const S_cusp = computeMaxSaturationOKLC(a, b);
|
|
280
|
+
const rgb_at_max = oklabToLinearSrgb([
|
|
281
|
+
1,
|
|
282
|
+
S_cusp * a,
|
|
283
|
+
S_cusp * b
|
|
284
|
+
]);
|
|
285
|
+
const L_cusp = Math.cbrt(1 / Math.max(Math.max(rgb_at_max[0], rgb_at_max[1]), Math.max(rgb_at_max[2], 0)));
|
|
286
|
+
return [L_cusp, L_cusp * S_cusp];
|
|
287
|
+
};
|
|
288
|
+
const findGamutIntersectionOKLCH = (a, b, l1, c1, l0, cusp) => {
|
|
289
|
+
const lmsToRgb = LMS_to_linear_sRGB_M;
|
|
290
|
+
const tmp3 = [
|
|
291
|
+
0,
|
|
292
|
+
a,
|
|
293
|
+
b
|
|
294
|
+
];
|
|
295
|
+
const floatMax = Number.MAX_VALUE;
|
|
296
|
+
let t;
|
|
297
|
+
const dotYZ = (mat, vec) => mat[1] * vec[1] + mat[2] * vec[2];
|
|
298
|
+
const dotXYZ = (vec, x, y, z) => vec[0] * x + vec[1] * y + vec[2] * z;
|
|
299
|
+
if ((l1 - l0) * cusp[1] - (cusp[0] - l0) * c1 <= 0) {
|
|
300
|
+
const denom = c1 * cusp[0] + cusp[1] * (l0 - l1);
|
|
301
|
+
t = denom === 0 ? 0 : cusp[1] * l0 / denom;
|
|
302
|
+
} else {
|
|
303
|
+
const denom = c1 * (cusp[0] - 1) + cusp[1] * (l0 - l1);
|
|
304
|
+
t = denom === 0 ? 0 : cusp[1] * (l0 - 1) / denom;
|
|
305
|
+
const dl = l1 - l0;
|
|
306
|
+
const dc = c1;
|
|
307
|
+
const kl = dotYZ(OKLab_to_LMS_M[0], tmp3);
|
|
308
|
+
const km = dotYZ(OKLab_to_LMS_M[1], tmp3);
|
|
309
|
+
const ks = dotYZ(OKLab_to_LMS_M[2], tmp3);
|
|
310
|
+
const ldt_ = dl + dc * kl;
|
|
311
|
+
const mdt_ = dl + dc * km;
|
|
312
|
+
const sdt_ = dl + dc * ks;
|
|
313
|
+
const L = l0 * (1 - t) + t * l1;
|
|
314
|
+
const C = t * c1;
|
|
315
|
+
const l_ = L + C * kl;
|
|
316
|
+
const m_ = L + C * km;
|
|
317
|
+
const s_ = L + C * ks;
|
|
318
|
+
const l = l_ ** 3;
|
|
319
|
+
const m = m_ ** 3;
|
|
320
|
+
const s = s_ ** 3;
|
|
321
|
+
const ldt = 3 * ldt_ * l_ * l_;
|
|
322
|
+
const mdt = 3 * mdt_ * m_ * m_;
|
|
323
|
+
const sdt = 3 * sdt_ * s_ * s_;
|
|
324
|
+
const ldt2 = 6 * ldt_ * ldt_ * l_;
|
|
325
|
+
const mdt2 = 6 * mdt_ * mdt_ * m_;
|
|
326
|
+
const sdt2 = 6 * sdt_ * sdt_ * s_;
|
|
327
|
+
const r_ = dotXYZ(lmsToRgb[0], l, m, s) - 1;
|
|
328
|
+
const r1 = dotXYZ(lmsToRgb[0], ldt, mdt, sdt);
|
|
329
|
+
const r2 = dotXYZ(lmsToRgb[0], ldt2, mdt2, sdt2);
|
|
330
|
+
const ur = r1 / (r1 * r1 - .5 * r_ * r2);
|
|
331
|
+
let tr = -r_ * ur;
|
|
332
|
+
const g_ = dotXYZ(lmsToRgb[1], l, m, s) - 1;
|
|
333
|
+
const g1 = dotXYZ(lmsToRgb[1], ldt, mdt, sdt);
|
|
334
|
+
const g2 = dotXYZ(lmsToRgb[1], ldt2, mdt2, sdt2);
|
|
335
|
+
const ug = g1 / (g1 * g1 - .5 * g_ * g2);
|
|
336
|
+
let tg = -g_ * ug;
|
|
337
|
+
const b_ = dotXYZ(lmsToRgb[2], l, m, s) - 1;
|
|
338
|
+
const b1 = dotXYZ(lmsToRgb[2], ldt, mdt, sdt);
|
|
339
|
+
const b2 = dotXYZ(lmsToRgb[2], ldt2, mdt2, sdt2);
|
|
340
|
+
const ub = b1 / (b1 * b1 - .5 * b_ * b2);
|
|
341
|
+
let tb = -b_ * ub;
|
|
342
|
+
tr = ur >= 0 ? tr : floatMax;
|
|
343
|
+
tg = ug >= 0 ? tg : floatMax;
|
|
344
|
+
tb = ub >= 0 ? tb : floatMax;
|
|
345
|
+
t += Math.min(tr, Math.min(tg, tb));
|
|
346
|
+
}
|
|
347
|
+
return t;
|
|
348
|
+
};
|
|
349
|
+
const computeSt = (cusp) => [cusp[1] / cusp[0], cusp[1] / (1 - cusp[0])];
|
|
350
|
+
const computeStMid = (a, b) => [.11516993 + 1 / (7.4477897 + 4.1590124 * b + a * (-2.19557347 + 1.75198401 * b + a * (-2.13704948 - 10.02301043 * b + a * (-4.24894561 + 5.38770819 * b + 4.69891013 * a)))), .11239642 + 1 / (1.6132032 - .68124379 * b + a * (.40370612 + .90148123 * b + a * (-.27087943 + .6122399 * b + a * (.00299215 - .45399568 * b - .14661872 * a))))];
|
|
351
|
+
const getCs = (L, a, b, cusp) => {
|
|
352
|
+
const cMax = findGamutIntersectionOKLCH(a, b, L, 1, L, cusp);
|
|
353
|
+
const stMax = computeSt(cusp);
|
|
354
|
+
const k = cMax / Math.min(L * stMax[0], (1 - L) * stMax[1]);
|
|
355
|
+
const stMid = computeStMid(a, b);
|
|
356
|
+
let ca = L * stMid[0];
|
|
357
|
+
let cb = (1 - L) * stMid[1];
|
|
358
|
+
const cMid = .9 * k * Math.sqrt(Math.sqrt(1 / (1 / ca ** 4 + 1 / cb ** 4)));
|
|
359
|
+
ca = L * .4;
|
|
360
|
+
cb = (1 - L) * .8;
|
|
361
|
+
return [
|
|
362
|
+
Math.sqrt(1 / (1 / ca ** 2 + 1 / cb ** 2)),
|
|
363
|
+
cMid,
|
|
364
|
+
cMax
|
|
365
|
+
];
|
|
366
|
+
};
|
|
367
|
+
const okhslToOklab = (hsl) => {
|
|
368
|
+
let h = hsl[0];
|
|
369
|
+
const s = hsl[1];
|
|
370
|
+
const l = hsl[2];
|
|
371
|
+
const L = toeInv(l);
|
|
372
|
+
let a = 0;
|
|
373
|
+
let b = 0;
|
|
374
|
+
h = constrainAngle(h) / 360;
|
|
375
|
+
if (L !== 0 && L !== 1 && s !== 0) {
|
|
376
|
+
const a_ = Math.cos(TAU * h);
|
|
377
|
+
const b_ = Math.sin(TAU * h);
|
|
378
|
+
const [c0, cMid, cMax] = getCs(L, a_, b_, findCuspOKLCH(a_, b_));
|
|
379
|
+
const mid = .8;
|
|
380
|
+
const midInv = 1.25;
|
|
381
|
+
let t, k0, k1, k2;
|
|
382
|
+
if (s < mid) {
|
|
383
|
+
t = midInv * s;
|
|
384
|
+
k0 = 0;
|
|
385
|
+
k1 = mid * c0;
|
|
386
|
+
k2 = 1 - k1 / cMid;
|
|
387
|
+
} else {
|
|
388
|
+
t = 5 * (s - .8);
|
|
389
|
+
k0 = cMid;
|
|
390
|
+
k1 = .2 * cMid ** 2 * 1.25 ** 2 / c0;
|
|
391
|
+
k2 = 1 - k1 / (cMax - cMid);
|
|
392
|
+
}
|
|
393
|
+
const c = k0 + t * k1 / (1 - k2 * t);
|
|
394
|
+
a = c * a_;
|
|
395
|
+
b = c * b_;
|
|
396
|
+
}
|
|
397
|
+
return [
|
|
398
|
+
L,
|
|
399
|
+
a,
|
|
400
|
+
b
|
|
401
|
+
];
|
|
402
|
+
};
|
|
403
|
+
/**
|
|
404
|
+
* HSL to RGB.
|
|
405
|
+
* Algorithm from CSS Color 4 spec.
|
|
406
|
+
*
|
|
407
|
+
* @param h - Hue in degrees (0-360)
|
|
408
|
+
* @param s - Saturation (0-1)
|
|
409
|
+
* @param l - Lightness (0-1)
|
|
410
|
+
* @returns RGB values in 0-255 range (may have fractional values)
|
|
411
|
+
*/
|
|
412
|
+
function hslToRgbValues(h, s, l) {
|
|
413
|
+
const a = s * Math.min(l, 1 - l);
|
|
414
|
+
const f = (n) => {
|
|
415
|
+
const k = (n + h / 30) % 12;
|
|
416
|
+
return l - a * Math.max(-1, Math.min(k - 3, 9 - k, 1));
|
|
417
|
+
};
|
|
418
|
+
return [
|
|
419
|
+
f(0) * 255,
|
|
420
|
+
f(8) * 255,
|
|
421
|
+
f(4) * 255
|
|
422
|
+
];
|
|
423
|
+
}
|
|
424
|
+
/**
|
|
425
|
+
* OKHSL to sRGB (0-1 range).
|
|
426
|
+
* @param h - Hue in degrees (0-360)
|
|
427
|
+
* @param s - Saturation (0-1)
|
|
428
|
+
* @param l - Lightness (0-1)
|
|
429
|
+
* @returns sRGB values in 0-1 range, clamped to gamut
|
|
430
|
+
*/
|
|
431
|
+
function okhslToSrgb(h, s, l) {
|
|
432
|
+
const linearRGB = oklabToLinearSrgb(okhslToOklab([
|
|
433
|
+
h,
|
|
434
|
+
s,
|
|
435
|
+
l
|
|
436
|
+
]));
|
|
437
|
+
return [
|
|
438
|
+
clamp$1(srgbLinearToGamma(linearRGB[0]), 0, 1),
|
|
439
|
+
clamp$1(srgbLinearToGamma(linearRGB[1]), 0, 1),
|
|
440
|
+
clamp$1(srgbLinearToGamma(linearRGB[2]), 0, 1)
|
|
441
|
+
];
|
|
442
|
+
}
|
|
443
|
+
/**
|
|
444
|
+
* OKHST to sRGB (0-1 range).
|
|
445
|
+
* @param h - Hue in degrees (0-360)
|
|
446
|
+
* @param s - Saturation (0-1)
|
|
447
|
+
* @param t - Tone (0-1)
|
|
448
|
+
* @returns sRGB values in 0-1 range, clamped to gamut
|
|
449
|
+
*/
|
|
450
|
+
function okhstToSrgb(h, s, t) {
|
|
451
|
+
return okhslToSrgb(h, clamp$1(s, 0, 1), clamp$1(fromTone(t * 100), 0, 1));
|
|
452
|
+
}
|
|
453
|
+
/**
|
|
454
|
+
* OKLCH to sRGB (0-255 range).
|
|
455
|
+
* @param L - Lightness (0-1)
|
|
456
|
+
* @param C - Chroma (typically 0-0.4)
|
|
457
|
+
* @param H - Hue in degrees (0-360)
|
|
458
|
+
* @returns RGB values in 0-255 range, clamped to gamut
|
|
459
|
+
*/
|
|
460
|
+
function oklchToRgbValues(L, C, H) {
|
|
461
|
+
const hRad = H * Math.PI / 180;
|
|
462
|
+
const linear = oklabToLinearSrgb([
|
|
463
|
+
L,
|
|
464
|
+
C * Math.cos(hRad),
|
|
465
|
+
C * Math.sin(hRad)
|
|
466
|
+
]);
|
|
467
|
+
return [
|
|
468
|
+
clamp$1(srgbLinearToGamma(linear[0]), 0, 1) * 255,
|
|
469
|
+
clamp$1(srgbLinearToGamma(linear[1]), 0, 1) * 255,
|
|
470
|
+
clamp$1(srgbLinearToGamma(linear[2]), 0, 1) * 255
|
|
471
|
+
];
|
|
472
|
+
}
|
|
473
|
+
let _namedColorHex = null;
|
|
474
|
+
function getNamedColorHex() {
|
|
475
|
+
if (_namedColorHex) return _namedColorHex;
|
|
476
|
+
_namedColorHex = new Map([
|
|
477
|
+
["aliceblue", "#f0f8ff"],
|
|
478
|
+
["antiquewhite", "#faebd7"],
|
|
479
|
+
["aqua", "#00ffff"],
|
|
480
|
+
["aquamarine", "#7fffd4"],
|
|
481
|
+
["azure", "#f0ffff"],
|
|
482
|
+
["beige", "#f5f5dc"],
|
|
483
|
+
["bisque", "#ffe4c4"],
|
|
484
|
+
["black", "#000000"],
|
|
485
|
+
["blanchedalmond", "#ffebcd"],
|
|
486
|
+
["blue", "#0000ff"],
|
|
487
|
+
["blueviolet", "#8a2be2"],
|
|
488
|
+
["brown", "#a52a2a"],
|
|
489
|
+
["burlywood", "#deb887"],
|
|
490
|
+
["cadetblue", "#5f9ea0"],
|
|
491
|
+
["chartreuse", "#7fff00"],
|
|
492
|
+
["chocolate", "#d2691e"],
|
|
493
|
+
["coral", "#ff7f50"],
|
|
494
|
+
["cornflowerblue", "#6495ed"],
|
|
495
|
+
["cornsilk", "#fff8dc"],
|
|
496
|
+
["crimson", "#dc143c"],
|
|
497
|
+
["cyan", "#00ffff"],
|
|
498
|
+
["darkblue", "#00008b"],
|
|
499
|
+
["darkcyan", "#008b8b"],
|
|
500
|
+
["darkgoldenrod", "#b8860b"],
|
|
501
|
+
["darkgray", "#a9a9a9"],
|
|
502
|
+
["darkgreen", "#006400"],
|
|
503
|
+
["darkgrey", "#a9a9a9"],
|
|
504
|
+
["darkkhaki", "#bdb76b"],
|
|
505
|
+
["darkmagenta", "#8b008b"],
|
|
506
|
+
["darkolivegreen", "#556b2f"],
|
|
507
|
+
["darkorange", "#ff8c00"],
|
|
508
|
+
["darkorchid", "#9932cc"],
|
|
509
|
+
["darkred", "#8b0000"],
|
|
510
|
+
["darksalmon", "#e9967a"],
|
|
511
|
+
["darkseagreen", "#8fbc8f"],
|
|
512
|
+
["darkslateblue", "#483d8b"],
|
|
513
|
+
["darkslategray", "#2f4f4f"],
|
|
514
|
+
["darkslategrey", "#2f4f4f"],
|
|
515
|
+
["darkturquoise", "#00ced1"],
|
|
516
|
+
["darkviolet", "#9400d3"],
|
|
517
|
+
["deeppink", "#ff1493"],
|
|
518
|
+
["deepskyblue", "#00bfff"],
|
|
519
|
+
["dimgray", "#696969"],
|
|
520
|
+
["dimgrey", "#696969"],
|
|
521
|
+
["dodgerblue", "#1e90ff"],
|
|
522
|
+
["firebrick", "#b22222"],
|
|
523
|
+
["floralwhite", "#fffaf0"],
|
|
524
|
+
["forestgreen", "#228b22"],
|
|
525
|
+
["fuchsia", "#ff00ff"],
|
|
526
|
+
["gainsboro", "#dcdcdc"],
|
|
527
|
+
["ghostwhite", "#f8f8ff"],
|
|
528
|
+
["gold", "#ffd700"],
|
|
529
|
+
["goldenrod", "#daa520"],
|
|
530
|
+
["gray", "#808080"],
|
|
531
|
+
["green", "#008000"],
|
|
532
|
+
["greenyellow", "#adff2f"],
|
|
533
|
+
["grey", "#808080"],
|
|
534
|
+
["honeydew", "#f0fff0"],
|
|
535
|
+
["hotpink", "#ff69b4"],
|
|
536
|
+
["indianred", "#cd5c5c"],
|
|
537
|
+
["indigo", "#4b0082"],
|
|
538
|
+
["ivory", "#fffff0"],
|
|
539
|
+
["khaki", "#f0e68c"],
|
|
540
|
+
["lavender", "#e6e6fa"],
|
|
541
|
+
["lavenderblush", "#fff0f5"],
|
|
542
|
+
["lawngreen", "#7cfc00"],
|
|
543
|
+
["lemonchiffon", "#fffacd"],
|
|
544
|
+
["lightblue", "#add8e6"],
|
|
545
|
+
["lightcoral", "#f08080"],
|
|
546
|
+
["lightcyan", "#e0ffff"],
|
|
547
|
+
["lightgoldenrodyellow", "#fafad2"],
|
|
548
|
+
["lightgray", "#d3d3d3"],
|
|
549
|
+
["lightgreen", "#90ee90"],
|
|
550
|
+
["lightgrey", "#d3d3d3"],
|
|
551
|
+
["lightpink", "#ffb6c1"],
|
|
552
|
+
["lightsalmon", "#ffa07a"],
|
|
553
|
+
["lightseagreen", "#20b2aa"],
|
|
554
|
+
["lightskyblue", "#87cefa"],
|
|
555
|
+
["lightslategray", "#778899"],
|
|
556
|
+
["lightslategrey", "#778899"],
|
|
557
|
+
["lightsteelblue", "#b0c4de"],
|
|
558
|
+
["lightyellow", "#ffffe0"],
|
|
559
|
+
["lime", "#00ff00"],
|
|
560
|
+
["limegreen", "#32cd32"],
|
|
561
|
+
["linen", "#faf0e6"],
|
|
562
|
+
["magenta", "#ff00ff"],
|
|
563
|
+
["maroon", "#800000"],
|
|
564
|
+
["mediumaquamarine", "#66cdaa"],
|
|
565
|
+
["mediumblue", "#0000cd"],
|
|
566
|
+
["mediumorchid", "#ba55d3"],
|
|
567
|
+
["mediumpurple", "#9370db"],
|
|
568
|
+
["mediumseagreen", "#3cb371"],
|
|
569
|
+
["mediumslateblue", "#7b68ee"],
|
|
570
|
+
["mediumspringgreen", "#00fa9a"],
|
|
571
|
+
["mediumturquoise", "#48d1cc"],
|
|
572
|
+
["mediumvioletred", "#c71585"],
|
|
573
|
+
["midnightblue", "#191970"],
|
|
574
|
+
["mintcream", "#f5fffa"],
|
|
575
|
+
["mistyrose", "#ffe4e1"],
|
|
576
|
+
["moccasin", "#ffe4b5"],
|
|
577
|
+
["navajowhite", "#ffdead"],
|
|
578
|
+
["navy", "#000080"],
|
|
579
|
+
["oldlace", "#fdf5e6"],
|
|
580
|
+
["olive", "#808000"],
|
|
581
|
+
["olivedrab", "#6b8e23"],
|
|
582
|
+
["orange", "#ffa500"],
|
|
583
|
+
["orangered", "#ff4500"],
|
|
584
|
+
["orchid", "#da70d6"],
|
|
585
|
+
["palegoldenrod", "#eee8aa"],
|
|
586
|
+
["palegreen", "#98fb98"],
|
|
587
|
+
["paleturquoise", "#afeeee"],
|
|
588
|
+
["palevioletred", "#db7093"],
|
|
589
|
+
["papayawhip", "#ffefd5"],
|
|
590
|
+
["peachpuff", "#ffdab9"],
|
|
591
|
+
["peru", "#cd853f"],
|
|
592
|
+
["pink", "#ffc0cb"],
|
|
593
|
+
["plum", "#dda0dd"],
|
|
594
|
+
["powderblue", "#b0e0e6"],
|
|
595
|
+
["purple", "#800080"],
|
|
596
|
+
["rebeccapurple", "#663399"],
|
|
597
|
+
["red", "#ff0000"],
|
|
598
|
+
["rosybrown", "#bc8f8f"],
|
|
599
|
+
["royalblue", "#4169e1"],
|
|
600
|
+
["saddlebrown", "#8b4513"],
|
|
601
|
+
["salmon", "#fa8072"],
|
|
602
|
+
["sandybrown", "#f4a460"],
|
|
603
|
+
["seagreen", "#2e8b57"],
|
|
604
|
+
["seashell", "#fff5ee"],
|
|
605
|
+
["sienna", "#a0522d"],
|
|
606
|
+
["silver", "#c0c0c0"],
|
|
607
|
+
["skyblue", "#87ceeb"],
|
|
608
|
+
["slateblue", "#6a5acd"],
|
|
609
|
+
["slategray", "#708090"],
|
|
610
|
+
["slategrey", "#708090"],
|
|
611
|
+
["snow", "#fffafa"],
|
|
612
|
+
["springgreen", "#00ff7f"],
|
|
613
|
+
["steelblue", "#4682b4"],
|
|
614
|
+
["tan", "#d2b48c"],
|
|
615
|
+
["teal", "#008080"],
|
|
616
|
+
["thistle", "#d8bfd8"],
|
|
617
|
+
["tomato", "#ff6347"],
|
|
618
|
+
["turquoise", "#40e0d0"],
|
|
619
|
+
["violet", "#ee82ee"],
|
|
620
|
+
["wheat", "#f5deb3"],
|
|
621
|
+
["white", "#ffffff"],
|
|
622
|
+
["whitesmoke", "#f5f5f5"],
|
|
623
|
+
["yellow", "#ffff00"],
|
|
624
|
+
["yellowgreen", "#9acd32"]
|
|
625
|
+
]);
|
|
626
|
+
return _namedColorHex;
|
|
627
|
+
}
|
|
628
|
+
/**
|
|
629
|
+
* Convert hex color string to `rgb()` CSS string.
|
|
630
|
+
* Supports 3, 4, 6, and 8 character hex values (with or without `#`).
|
|
631
|
+
*/
|
|
632
|
+
function hexToRgb(hex) {
|
|
633
|
+
const matched = hex.replace(/^#?([a-f\d])([a-f\d])([a-f\d])$/i, (_m, r, g, b) => "#" + r + r + g + g + b + b).substring(1).match(/.{2}/g);
|
|
634
|
+
if (!matched) return null;
|
|
635
|
+
const rgba = matched.map((x, i) => parseInt(x, 16) * (i === 3 ? 1 / 255 : 1));
|
|
636
|
+
if (rgba.some((v) => Number.isNaN(v))) return null;
|
|
637
|
+
if (rgba.length >= 3) return `rgb(${rgba.slice(0, 3).join(" ")}${rgba.length > 3 ? ` / ${rgba[3]}` : ""})`;
|
|
638
|
+
return null;
|
|
639
|
+
}
|
|
640
|
+
/**
|
|
641
|
+
* Extract RGB values from an `rgb()`/`rgba()` string.
|
|
642
|
+
* Supports comma-separated, space-separated, fractional, percentage,
|
|
643
|
+
* and slash alpha notation.
|
|
644
|
+
*
|
|
645
|
+
* @returns Array of RGB values (0-255 range), converting percentages as needed.
|
|
646
|
+
*/
|
|
647
|
+
function getRgbValuesFromRgbaString(str) {
|
|
648
|
+
const match = str.match(/rgba?\(([^)]+)\)/i);
|
|
649
|
+
if (!match) return [];
|
|
650
|
+
const [colorPart] = match[1].trim().split("/");
|
|
651
|
+
return colorPart.trim().split(/[,\s]+/).filter(Boolean).slice(0, 3).map((part) => {
|
|
652
|
+
part = part.trim();
|
|
653
|
+
if (part.endsWith("%")) return parseFloat(part) / 100 * 255;
|
|
654
|
+
return parseFloat(part);
|
|
655
|
+
});
|
|
656
|
+
}
|
|
657
|
+
/**
|
|
658
|
+
* Convert any recognized color string to an `rgb()` CSS string.
|
|
659
|
+
* Handles hex, `okhsl()`, `okhst()`, `hsl()`/`hsla()`, named CSS colors,
|
|
660
|
+
* and `rgb()`/`rgba()` pass-through.
|
|
661
|
+
*/
|
|
662
|
+
function strToRgb(color, _ignoreAlpha = false) {
|
|
663
|
+
if (!color) return void 0;
|
|
664
|
+
if (color.startsWith("rgb")) return color;
|
|
665
|
+
if (color.startsWith("#")) return hexToRgb(color);
|
|
666
|
+
if (color.startsWith("oklch(")) return oklchStringToRgb(color);
|
|
667
|
+
if (color.startsWith("hsl")) return hslStringToRgb(color);
|
|
668
|
+
const namedHex = getNamedColorHex().get(color.toLowerCase());
|
|
669
|
+
if (namedHex) return hexToRgb(namedHex);
|
|
670
|
+
return null;
|
|
671
|
+
}
|
|
672
|
+
/**
|
|
673
|
+
* Convert an HSL/HSLA color string to an `rgb()`/`rgba()` CSS string.
|
|
674
|
+
* Supports modern space-separated and legacy comma-separated syntax,
|
|
675
|
+
* deg/turn/rad hue units, and slash alpha notation.
|
|
676
|
+
*/
|
|
677
|
+
function hslStringToRgb(hslStr) {
|
|
678
|
+
const match = hslStr.match(/hsla?\(([^)]+)\)/i);
|
|
679
|
+
if (!match) return null;
|
|
680
|
+
const [colorPart, slashAlpha] = match[1].trim().split("/");
|
|
681
|
+
const parts = colorPart.trim().split(/[,\s]+/).filter(Boolean);
|
|
682
|
+
if (parts.length < 3) return null;
|
|
683
|
+
const alphaPart = slashAlpha?.trim() || (parts.length >= 4 ? parts[3] : null);
|
|
684
|
+
let h = parseFloat(parts[0]);
|
|
685
|
+
const hueStr = parts[0].toLowerCase();
|
|
686
|
+
if (hueStr.endsWith("turn")) h = parseFloat(hueStr) * 360;
|
|
687
|
+
else if (hueStr.endsWith("rad")) h = parseFloat(hueStr) * 180 / Math.PI;
|
|
688
|
+
h = (h % 360 + 360) % 360;
|
|
689
|
+
const parsePercent = (val) => {
|
|
690
|
+
const num = parseFloat(val);
|
|
691
|
+
return val.includes("%") ? num / 100 : num;
|
|
692
|
+
};
|
|
693
|
+
const s = Math.max(0, Math.min(1, parsePercent(parts[1])));
|
|
694
|
+
const l = Math.max(0, Math.min(1, parsePercent(parts[2])));
|
|
695
|
+
const [r, g, b] = hslToRgbValues(h, s, l);
|
|
696
|
+
if (alphaPart) {
|
|
697
|
+
const alpha = parseFloat(alphaPart.trim());
|
|
698
|
+
return `rgba(${Math.round(r)}, ${Math.round(g)}, ${Math.round(b)}, ${alpha})`;
|
|
699
|
+
}
|
|
700
|
+
return `rgb(${Math.round(r)} ${Math.round(g)} ${Math.round(b)})`;
|
|
701
|
+
}
|
|
702
|
+
/**
|
|
703
|
+
* Convert an `oklch()` color string to an `rgb()`/`rgba()` CSS string.
|
|
704
|
+
* Supports deg/turn/rad hue units and percentage lightness.
|
|
705
|
+
*/
|
|
706
|
+
function oklchStringToRgb(oklchStr) {
|
|
707
|
+
const match = oklchStr.match(/oklch\(([^)]+)\)/i);
|
|
708
|
+
if (!match) return null;
|
|
709
|
+
const [colorPart, alphaPart] = match[1].trim().split("/");
|
|
710
|
+
const parts = colorPart.trim().split(/[,\s]+/).filter(Boolean);
|
|
711
|
+
if (parts.length < 3) return null;
|
|
712
|
+
const parsePercent = (val) => {
|
|
713
|
+
const num = parseFloat(val);
|
|
714
|
+
return val.includes("%") ? num / 100 : num;
|
|
715
|
+
};
|
|
716
|
+
const L = Math.max(0, Math.min(1, parsePercent(parts[0])));
|
|
717
|
+
const C = Math.max(0, parseFloat(parts[1]));
|
|
718
|
+
let H = parseFloat(parts[2]);
|
|
719
|
+
const hueStr = parts[2].toLowerCase();
|
|
720
|
+
if (hueStr.endsWith("turn")) H = parseFloat(hueStr) * 360;
|
|
721
|
+
else if (hueStr.endsWith("rad")) H = parseFloat(hueStr) * 180 / Math.PI;
|
|
722
|
+
else if (hueStr.endsWith("deg")) H = parseFloat(hueStr);
|
|
723
|
+
const [r, g, b] = oklchToRgbValues(L, C, H);
|
|
724
|
+
if (alphaPart) {
|
|
725
|
+
const alpha = parseFloat(alphaPart.trim());
|
|
726
|
+
return `rgba(${Math.round(r)}, ${Math.round(g)}, ${Math.round(b)}, ${alpha})`;
|
|
727
|
+
}
|
|
728
|
+
return `rgb(${Math.round(r)} ${Math.round(g)} ${Math.round(b)})`;
|
|
729
|
+
}
|
|
730
|
+
//#endregion
|
|
731
|
+
//#region src/utils/color-space.ts
|
|
732
|
+
/**
|
|
733
|
+
* Set the alpha of a whole color, replacing any it already carries.
|
|
734
|
+
*
|
|
735
|
+
* CSS relative color syntax is how opacity is applied to every color Tasty
|
|
736
|
+
* emits. `oklch(from <color> l c h / <alpha>)` copies the channels over and
|
|
737
|
+
* writes the alpha slot, which needs nothing from the color except that it *is*
|
|
738
|
+
* a color: a `color-mix()`, a `light-dark()`, a `currentcolor`, a
|
|
739
|
+
* `--name-color` written by hand-authored CSS that Tasty never defined — all of
|
|
740
|
+
* them work, where writing into a channel slot directly would require channels
|
|
741
|
+
* the engine has no way to compute.
|
|
742
|
+
*
|
|
743
|
+
* Alpha is *replaced*, not composed. A token holding `rgb(255 0 0 / .8)` faded
|
|
744
|
+
* to `.5` is alpha `.5`. `color-mix()` against `transparent` cannot do this —
|
|
745
|
+
* it would multiply the two to `.4`.
|
|
746
|
+
*
|
|
747
|
+
* The alpha slot takes a `<number>` or a `<percentage>`, so an opacity custom
|
|
748
|
+
* property passes straight through in whichever form the author declared it.
|
|
749
|
+
*
|
|
750
|
+
* The space is always `oklch`, regardless of the configured {@link ColorSpace}:
|
|
751
|
+
* it is unbounded, so a wide-gamut color survives the round trip that a
|
|
752
|
+
* gamut-limited space would clamp, and it is the space the computed value used
|
|
753
|
+
* to be reported in. Channels are copied rather than interpolated, so the polar
|
|
754
|
+
* form costs nothing even for an achromatic color, whose hue is carried through
|
|
755
|
+
* untouched.
|
|
756
|
+
*/
|
|
757
|
+
function overrideColorAlpha(color, alpha) {
|
|
758
|
+
return `oklch(from ${color} l c h / ${alpha})`;
|
|
759
|
+
}
|
|
760
|
+
/**
|
|
761
|
+
* Compose an alpha onto a color, multiplying with any it already carries.
|
|
762
|
+
*
|
|
763
|
+
* This is the counterpart to {@link overrideColorAlpha}, and the difference is
|
|
764
|
+
* the point. A design token names a color, so fading it *sets* its alpha. But
|
|
765
|
+
* `currentcolor` is the color an element **inherits**, which an ancestor may
|
|
766
|
+
* already have faded — `#current.4` there means "40% of what reaches me", and a
|
|
767
|
+
* nested `#current.18` under it composes to `.072`. Design systems build ramps
|
|
768
|
+
* out of exactly that, so `#current` composes and a token replaces.
|
|
769
|
+
*
|
|
770
|
+
* `color-mix()` against `transparent` is what composes: mixing premultiplied
|
|
771
|
+
* leaves the channels alone and multiplies the alphas. Its percentage slot takes
|
|
772
|
+
* no `<number>`, so a `$prop` alpha has to be scaled — which is why an opacity
|
|
773
|
+
* property used as `#current.$prop` has to hold a unitless number, where a token
|
|
774
|
+
* accepts either form.
|
|
775
|
+
*/
|
|
776
|
+
function mixColorAlpha(color, percentage) {
|
|
777
|
+
return `color-mix(in oklab, ${color} ${percentage}, transparent)`;
|
|
778
|
+
}
|
|
779
|
+
/**
|
|
780
|
+
* Matches what {@link overrideColorAlpha} builds. The first group is greedy so a
|
|
781
|
+
* nested override splits on its outermost layer.
|
|
782
|
+
*/
|
|
783
|
+
const RE_ALPHA_OVERRIDE = /^oklch\(from (.+) l c h \/ (.+)\)$/;
|
|
784
|
+
/**
|
|
785
|
+
* Split what {@link overrideColorAlpha} builds back into the color it faded and
|
|
786
|
+
* the alpha, or `null` when the value is not one of those.
|
|
787
|
+
*/
|
|
788
|
+
function parseAlphaOverride(value) {
|
|
789
|
+
const match = value.match(RE_ALPHA_OVERRIDE);
|
|
790
|
+
return match ? {
|
|
791
|
+
color: match[1],
|
|
792
|
+
alpha: match[2]
|
|
793
|
+
} : null;
|
|
794
|
+
}
|
|
795
|
+
//#endregion
|
|
796
|
+
//#region src/parser/types.ts
|
|
797
|
+
const makeEmptyPart = () => ({
|
|
798
|
+
mods: [],
|
|
799
|
+
values: [],
|
|
800
|
+
colors: [],
|
|
801
|
+
all: [],
|
|
802
|
+
output: ""
|
|
803
|
+
});
|
|
804
|
+
const makeEmptyDetails = () => ({
|
|
805
|
+
input: "",
|
|
806
|
+
output: "",
|
|
807
|
+
mods: [],
|
|
808
|
+
values: [],
|
|
809
|
+
colors: [],
|
|
810
|
+
all: [],
|
|
811
|
+
parts: []
|
|
812
|
+
});
|
|
813
|
+
const finalizePart = (p) => {
|
|
814
|
+
p.output = p.all.join(" ");
|
|
815
|
+
return p;
|
|
816
|
+
};
|
|
817
|
+
/**
|
|
818
|
+
* Aggregate parts into a StyleDetails group.
|
|
819
|
+
* Combines all parts' arrays into group-level arrays for backward compatibility.
|
|
820
|
+
*/
|
|
821
|
+
const finalizeGroup = (d, parts) => {
|
|
822
|
+
d.parts = parts;
|
|
823
|
+
for (const part of parts) {
|
|
824
|
+
d.mods.push(...part.mods);
|
|
825
|
+
d.values.push(...part.values);
|
|
826
|
+
d.colors.push(...part.colors);
|
|
827
|
+
d.all.push(...part.all);
|
|
828
|
+
}
|
|
829
|
+
d.output = parts.map((p) => p.output).join(" / ");
|
|
830
|
+
return d;
|
|
831
|
+
};
|
|
832
|
+
//#endregion
|
|
833
|
+
//#region src/parser/classify.ts
|
|
834
|
+
/**
|
|
835
|
+
* Convert an opacity suffix to the alpha value it denotes.
|
|
836
|
+
*
|
|
837
|
+
* The authored digits are kept verbatim — `.07` stays `.07` rather than being
|
|
838
|
+
* multiplied into `7.000000000000001%` — and a `$prop` suffix passes the
|
|
839
|
+
* reference straight through, so it works whether the property holds a
|
|
840
|
+
* `<number>` or a `<percentage>`. Both are what the alpha slot accepts.
|
|
841
|
+
*/
|
|
842
|
+
function alphaSuffixToAlpha(rawAlpha) {
|
|
843
|
+
if (rawAlpha.startsWith("$")) return `var(--${rawAlpha.slice(1)})`;
|
|
844
|
+
if (rawAlpha === "0") return "0";
|
|
845
|
+
return `.${rawAlpha}`;
|
|
846
|
+
}
|
|
847
|
+
/** Apply an opacity suffix to a color, replacing any alpha it already carries. */
|
|
848
|
+
function fadeColor(color, rawAlpha) {
|
|
849
|
+
return overrideColorAlpha(color, alphaSuffixToAlpha(rawAlpha));
|
|
850
|
+
}
|
|
851
|
+
/**
|
|
852
|
+
* Convert an opacity suffix to the percentage `color-mix()` needs, by shifting
|
|
853
|
+
* the decimal point rather than multiplying: `.07` is `7%`, not the
|
|
854
|
+
* `7.000000000000001%` that `parseFloat('.07') * 100` produces.
|
|
855
|
+
*/
|
|
856
|
+
function alphaSuffixToPercentage(rawAlpha) {
|
|
857
|
+
if (rawAlpha.startsWith("$")) return `calc(var(--${rawAlpha.slice(1)}) * 100%)`;
|
|
858
|
+
if (rawAlpha === "0") return "0%";
|
|
859
|
+
const digits = rawAlpha.length === 1 ? `${rawAlpha}0` : rawAlpha;
|
|
860
|
+
const whole = String(Number(digits.slice(0, 2)));
|
|
861
|
+
const fraction = digits.slice(2);
|
|
862
|
+
return `${fraction ? `${whole}.${fraction}` : whole}%`;
|
|
863
|
+
}
|
|
864
|
+
/**
|
|
865
|
+
* Apply an opacity suffix to `currentcolor`, composing with any alpha an
|
|
866
|
+
* ancestor already applied. See {@link mixColorAlpha} for why this differs from
|
|
867
|
+
* how a token is faded.
|
|
868
|
+
*/
|
|
869
|
+
function fadeCurrentColor(rawAlpha) {
|
|
870
|
+
return mixColorAlpha("currentcolor", alphaSuffixToPercentage(rawAlpha));
|
|
871
|
+
}
|
|
872
|
+
/**
|
|
873
|
+
* Whether parsed function arguments hold a color. Colors reach either the color
|
|
874
|
+
* bucket (`#token`, a nested color function, `transparent`) or — for the CSS
|
|
875
|
+
* named colors, which the parser has no token syntax for — the modifier bucket.
|
|
876
|
+
*/
|
|
877
|
+
function hasColorArgs(parsed) {
|
|
878
|
+
const namedColors = getNamedColorHex();
|
|
879
|
+
return parsed.groups.some((group) => group.colors.length > 0 || group.mods.some((mod) => namedColors.has(mod)));
|
|
880
|
+
}
|
|
881
|
+
/**
|
|
882
|
+
* Re-parses a value through the parser until it stabilizes (no changes)
|
|
883
|
+
* or max iterations reached. This allows units to reference other units.
|
|
884
|
+
* Example: { x: '8px', y: '2x' } -> '1y' resolves to '16px'
|
|
885
|
+
*/
|
|
886
|
+
function resolveUntilStable(value, opts, recurse, maxIterations = 10) {
|
|
887
|
+
let current = value;
|
|
888
|
+
for (let i = 0; i < maxIterations; i++) {
|
|
889
|
+
const unitMatch = current.match(RE_UNIT_NUM);
|
|
890
|
+
if (!unitMatch) break;
|
|
891
|
+
const unitName = unitMatch[1];
|
|
892
|
+
if (!opts.units || !(unitName in opts.units)) break;
|
|
893
|
+
const result = recurse(current);
|
|
894
|
+
if (result.output === current) break;
|
|
895
|
+
current = result.output;
|
|
896
|
+
}
|
|
897
|
+
return current;
|
|
898
|
+
}
|
|
899
|
+
function classify(raw, opts, recurse) {
|
|
900
|
+
const token = raw.trim();
|
|
901
|
+
if (!token) return {
|
|
902
|
+
bucket: 2,
|
|
903
|
+
processed: ""
|
|
904
|
+
};
|
|
905
|
+
{
|
|
906
|
+
let depth = 0;
|
|
907
|
+
let inQuote = 0;
|
|
908
|
+
for (let i = 0; i < token.length; i++) {
|
|
909
|
+
const ch = token[i];
|
|
910
|
+
if (inQuote) {
|
|
911
|
+
if (ch === inQuote && token[i - 1] !== "\\") inQuote = 0;
|
|
912
|
+
continue;
|
|
913
|
+
}
|
|
914
|
+
if (ch === "\"" || ch === "'") {
|
|
915
|
+
inQuote = ch;
|
|
916
|
+
continue;
|
|
917
|
+
}
|
|
918
|
+
if (ch === "(") depth++;
|
|
919
|
+
else if (ch === ")") depth = Math.max(0, depth - 1);
|
|
920
|
+
}
|
|
921
|
+
if (depth !== 0) {
|
|
922
|
+
console.warn("[Tasty] skipped invalid function token with unmatched parentheses:", token);
|
|
923
|
+
return {
|
|
924
|
+
bucket: 2,
|
|
925
|
+
processed: ""
|
|
926
|
+
};
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
if (token.startsWith("\"") && token.endsWith("\"") || token.startsWith("'") && token.endsWith("'")) return {
|
|
930
|
+
bucket: 1,
|
|
931
|
+
processed: token
|
|
932
|
+
};
|
|
933
|
+
if (token.startsWith("$$")) {
|
|
934
|
+
const rest = token.slice(2);
|
|
935
|
+
const openIdx = rest.indexOf("(");
|
|
936
|
+
if (openIdx > 0 && rest.endsWith(")")) {
|
|
937
|
+
const fname = rest.slice(0, openIdx);
|
|
938
|
+
if (/^[a-z_][a-z0-9-_]*$/i.test(fname)) {
|
|
939
|
+
const inner = rest.slice(openIdx + 1, -1).trim();
|
|
940
|
+
const cssName = `--${fname}`;
|
|
941
|
+
if (opts.functions && cssName in opts.functions) {
|
|
942
|
+
const groups = inner ? new StyleParser(opts).process(inner).groups : [];
|
|
943
|
+
const funcResult = opts.functions[cssName](groups);
|
|
944
|
+
if (funcResult !== "") return classify(funcResult, {
|
|
945
|
+
...opts,
|
|
946
|
+
functions: void 0
|
|
947
|
+
}, recurse);
|
|
948
|
+
}
|
|
949
|
+
return {
|
|
950
|
+
bucket: 1,
|
|
951
|
+
processed: `--${fname}(${inner ? recurse(inner).output : ""})`
|
|
952
|
+
};
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
if (/^[a-z_][a-z0-9-_]*$/i.test(rest)) return {
|
|
956
|
+
bucket: 1,
|
|
957
|
+
processed: `--${rest}`
|
|
958
|
+
};
|
|
959
|
+
}
|
|
960
|
+
if (token.startsWith("##")) {
|
|
961
|
+
const rest = token.slice(2);
|
|
962
|
+
const openIdx = rest.indexOf("(");
|
|
963
|
+
if (openIdx > 0 && rest.endsWith(")")) {
|
|
964
|
+
const fname = rest.slice(0, openIdx);
|
|
965
|
+
if (/^[a-z_][a-z0-9-_]*$/i.test(fname)) {
|
|
966
|
+
const inner = rest.slice(openIdx + 1, -1).trim();
|
|
967
|
+
return {
|
|
968
|
+
bucket: 1,
|
|
969
|
+
processed: `--${fname}-color(${inner ? recurse(inner).output : ""})`
|
|
970
|
+
};
|
|
971
|
+
}
|
|
972
|
+
}
|
|
973
|
+
if (/^[a-z_][a-z0-9-_]*$/i.test(rest)) return {
|
|
974
|
+
bucket: 1,
|
|
975
|
+
processed: `--${rest}-color`
|
|
976
|
+
};
|
|
977
|
+
}
|
|
978
|
+
if (token === "#current") return {
|
|
979
|
+
bucket: 0,
|
|
980
|
+
processed: "currentcolor"
|
|
981
|
+
};
|
|
982
|
+
const currentAlphaMatch = token.match(/^#current\.(\$[a-z_][a-z0-9-_]*|[0-9]+)$/i);
|
|
983
|
+
if (currentAlphaMatch) return {
|
|
984
|
+
bucket: 0,
|
|
985
|
+
processed: fadeCurrentColor(currentAlphaMatch[1])
|
|
986
|
+
};
|
|
987
|
+
if (token[0] === "$" || token[0] === "#") {
|
|
988
|
+
const predefinedTokens = getGlobalPredefinedTokens();
|
|
989
|
+
if (predefinedTokens) {
|
|
990
|
+
const lookupKey = token.toLowerCase();
|
|
991
|
+
if (lookupKey in predefinedTokens) {
|
|
992
|
+
const tokenValue = predefinedTokens[lookupKey];
|
|
993
|
+
return classify(foldDslCase(tokenValue), opts, recurse);
|
|
994
|
+
}
|
|
995
|
+
if (token[0] === "#") {
|
|
996
|
+
const alphaMatch = token.match(/^(#[a-z0-9-]+)\.(\$[a-z_][a-z0-9-_]*|[0-9]+)$/i);
|
|
997
|
+
if (alphaMatch) {
|
|
998
|
+
const [, baseToken, rawAlpha] = alphaMatch;
|
|
999
|
+
const baseKey = baseToken.toLowerCase();
|
|
1000
|
+
if (baseKey in predefinedTokens) {
|
|
1001
|
+
const resolvedValue = predefinedTokens[baseKey];
|
|
1002
|
+
if (resolvedValue.startsWith("#")) return classify(`${foldDslCase(resolvedValue)}.${rawAlpha}`, opts, recurse);
|
|
1003
|
+
const funcMatch = resolvedValue.match(RE_FUNC_CALL);
|
|
1004
|
+
if (funcMatch) {
|
|
1005
|
+
const [, funcName, args] = funcMatch;
|
|
1006
|
+
const lowerFunc = funcName.toLowerCase();
|
|
1007
|
+
if (DERIVED_COLOR_FUNCS.has(lowerFunc)) return {
|
|
1008
|
+
bucket: 0,
|
|
1009
|
+
processed: fadeColor(classify(foldDslCase(resolvedValue), opts, recurse).processed, rawAlpha)
|
|
1010
|
+
};
|
|
1011
|
+
const isCustomFunc = !!(opts.functions && lowerFunc in opts.functions && !COLOR_FUNCS.has(lowerFunc) && !COLOR_FUNCS.has(funcName.replace(/a$/i, "").toLowerCase()));
|
|
1012
|
+
const normalizedFunc = isCustomFunc ? lowerFunc : funcName.replace(/a$/i, "").toLowerCase();
|
|
1013
|
+
if (!(COLOR_FUNCS.has(normalizedFunc) || COLOR_FUNCS.has(lowerFunc) || isCustomFunc)) return classify(`${resolvedValue}.${rawAlpha}`, opts, recurse);
|
|
1014
|
+
let alpha;
|
|
1015
|
+
if (rawAlpha.startsWith("$")) alpha = `var(--${rawAlpha.slice(1)})`;
|
|
1016
|
+
else alpha = rawAlpha === "0" ? "0" : `.${rawAlpha}`;
|
|
1017
|
+
const normalizeArgs = (a) => {
|
|
1018
|
+
let result = "";
|
|
1019
|
+
let depth = 0;
|
|
1020
|
+
for (let i = 0; i < a.length; i++) {
|
|
1021
|
+
const c = a[i];
|
|
1022
|
+
if (c === "(") {
|
|
1023
|
+
depth++;
|
|
1024
|
+
result += c;
|
|
1025
|
+
} else if (c === ")") {
|
|
1026
|
+
depth = Math.max(0, depth - 1);
|
|
1027
|
+
result += c;
|
|
1028
|
+
} else if (c === "," && depth === 0) {
|
|
1029
|
+
while (i + 1 < a.length && /\s/.test(a[i + 1])) i++;
|
|
1030
|
+
result += " ";
|
|
1031
|
+
} else result += c;
|
|
1032
|
+
}
|
|
1033
|
+
return result;
|
|
1034
|
+
};
|
|
1035
|
+
const findLastTopLevel = (str, ch) => {
|
|
1036
|
+
let depth = 0;
|
|
1037
|
+
for (let i = str.length - 1; i >= 0; i--) {
|
|
1038
|
+
const c = str[i];
|
|
1039
|
+
if (c === ")") depth++;
|
|
1040
|
+
else if (c === "(") depth = Math.max(0, depth - 1);
|
|
1041
|
+
else if (c === ch && depth === 0) return i;
|
|
1042
|
+
}
|
|
1043
|
+
return -1;
|
|
1044
|
+
};
|
|
1045
|
+
const slashIdx = findLastTopLevel(args, "/");
|
|
1046
|
+
const hasModernAlpha = slashIdx !== -1;
|
|
1047
|
+
let topLevelCommaCount = 0;
|
|
1048
|
+
let lastTopLevelComma = -1;
|
|
1049
|
+
{
|
|
1050
|
+
let depth = 0;
|
|
1051
|
+
for (let i = 0; i < args.length; i++) {
|
|
1052
|
+
const c = args[i];
|
|
1053
|
+
if (c === "(") depth++;
|
|
1054
|
+
else if (c === ")") depth = Math.max(0, depth - 1);
|
|
1055
|
+
else if (c === "," && depth === 0) {
|
|
1056
|
+
topLevelCommaCount++;
|
|
1057
|
+
lastTopLevelComma = i;
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
1061
|
+
const hasLegacyAlpha = !hasModernAlpha && /a$/i.test(funcName) && topLevelCommaCount === 3;
|
|
1062
|
+
const constructed = `${normalizedFunc}(${hasModernAlpha || hasLegacyAlpha ? normalizeArgs(hasModernAlpha ? args.slice(0, slashIdx).trim() : args.slice(0, lastTopLevelComma).trim()) : normalizeArgs(args)} / ${alpha})`;
|
|
1063
|
+
if (!COLOR_FUNCS.has(normalizedFunc) && opts.functions && normalizedFunc in opts.functions) return classify(constructed, opts, recurse);
|
|
1064
|
+
return {
|
|
1065
|
+
bucket: 0,
|
|
1066
|
+
processed: constructed
|
|
1067
|
+
};
|
|
1068
|
+
}
|
|
1069
|
+
return classify(`${resolvedValue}.${rawAlpha}`, opts, recurse);
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
1075
|
+
if (token.match(/^var\(--([a-zA-Z0-9-]+)-color\)$/)) return {
|
|
1076
|
+
bucket: 0,
|
|
1077
|
+
processed: token
|
|
1078
|
+
};
|
|
1079
|
+
if (token.startsWith("url(")) return {
|
|
1080
|
+
bucket: 1,
|
|
1081
|
+
processed: token
|
|
1082
|
+
};
|
|
1083
|
+
if (token[0] === "$") {
|
|
1084
|
+
const identMatch = token.match(/^\$([a-z_][a-zA-Z0-9-_]*)$/);
|
|
1085
|
+
if (identMatch) {
|
|
1086
|
+
const name = identMatch[1];
|
|
1087
|
+
const processed = `var(--${name})`;
|
|
1088
|
+
return {
|
|
1089
|
+
bucket: name.endsWith("-color") ? 3 : 1,
|
|
1090
|
+
processed
|
|
1091
|
+
};
|
|
1092
|
+
}
|
|
1093
|
+
}
|
|
1094
|
+
if (token[0] === "#" && token.length > 1) {
|
|
1095
|
+
const alphaMatch = token.match(/^#([a-z0-9-]+)\.(\$[a-z_][a-z0-9-_]*|[0-9]+)$/i);
|
|
1096
|
+
if (alphaMatch) {
|
|
1097
|
+
const [, base, rawAlpha] = alphaMatch;
|
|
1098
|
+
return {
|
|
1099
|
+
bucket: 0,
|
|
1100
|
+
processed: fadeColor(`var(--${base}-color)`, rawAlpha)
|
|
1101
|
+
};
|
|
1102
|
+
}
|
|
1103
|
+
const name = token.slice(1);
|
|
1104
|
+
if (RE_HEX.test(name)) return {
|
|
1105
|
+
bucket: 0,
|
|
1106
|
+
processed: `var(--${name}-color, #${name})`
|
|
1107
|
+
};
|
|
1108
|
+
return {
|
|
1109
|
+
bucket: 0,
|
|
1110
|
+
processed: `var(--${name}-color)`
|
|
1111
|
+
};
|
|
1112
|
+
}
|
|
1113
|
+
const openIdx = token.indexOf("(");
|
|
1114
|
+
if (openIdx > 0 && token.endsWith(")")) {
|
|
1115
|
+
const fname = token.slice(0, openIdx);
|
|
1116
|
+
const inner = token.slice(openIdx + 1, -1);
|
|
1117
|
+
if (COLOR_FUNCS.has(fname)) {
|
|
1118
|
+
const parsedInner = recurse(inner);
|
|
1119
|
+
const argProcessed = parsedInner.output.replace(/,\s+/g, ",");
|
|
1120
|
+
const processed = `${canonicalFuncName(fname)}(${argProcessed})`;
|
|
1121
|
+
return {
|
|
1122
|
+
bucket: fname === "light-dark" && !hasColorArgs(parsedInner) ? 1 : 0,
|
|
1123
|
+
processed
|
|
1124
|
+
};
|
|
1125
|
+
}
|
|
1126
|
+
if (opts.functions && fname in opts.functions) {
|
|
1127
|
+
const tmp = new StyleParser(opts).process(inner);
|
|
1128
|
+
return classify(opts.functions[fname](tmp.groups), {
|
|
1129
|
+
...opts,
|
|
1130
|
+
functions: void 0
|
|
1131
|
+
}, recurse);
|
|
1132
|
+
}
|
|
1133
|
+
const argProcessed = recurse(inner).output;
|
|
1134
|
+
return {
|
|
1135
|
+
bucket: 1,
|
|
1136
|
+
processed: `${canonicalFuncName(fname)}(${argProcessed})`
|
|
1137
|
+
};
|
|
1138
|
+
}
|
|
1139
|
+
if (token.startsWith("(") && token.endsWith(")")) {
|
|
1140
|
+
const colorMatch = token.slice(1, -1).match(/^#([a-z0-9-]+)\s*,\s*(.*)$/i);
|
|
1141
|
+
if (colorMatch) {
|
|
1142
|
+
const [, name, fallback] = colorMatch;
|
|
1143
|
+
return {
|
|
1144
|
+
bucket: 0,
|
|
1145
|
+
processed: `var(--${name}-color, ${recurse(fallback).output})`
|
|
1146
|
+
};
|
|
1147
|
+
}
|
|
1148
|
+
}
|
|
1149
|
+
if (token.startsWith("(") && token.endsWith(")")) {
|
|
1150
|
+
const match = token.slice(1, -1).match(/^\$([a-z_][a-zA-Z0-9-_]*)\s*,\s*(.*)$/);
|
|
1151
|
+
if (match) {
|
|
1152
|
+
const [, name, fallback] = match;
|
|
1153
|
+
const processedFallback = recurse(fallback).output;
|
|
1154
|
+
return {
|
|
1155
|
+
bucket: name.endsWith("-color") ? 3 : 1,
|
|
1156
|
+
processed: `var(--${name}, ${processedFallback})`
|
|
1157
|
+
};
|
|
1158
|
+
}
|
|
1159
|
+
}
|
|
1160
|
+
if (token[0] === "(" && token[token.length - 1] === ")") return {
|
|
1161
|
+
bucket: 1,
|
|
1162
|
+
processed: `calc(${recurse(token.slice(1, -1)).output})`
|
|
1163
|
+
};
|
|
1164
|
+
const um = token.match(RE_UNIT_NUM);
|
|
1165
|
+
if (um) {
|
|
1166
|
+
const unit = um[1];
|
|
1167
|
+
const numericPart = parseFloat(token.slice(0, -unit.length));
|
|
1168
|
+
const handler = opts.units && opts.units[unit];
|
|
1169
|
+
if (handler) if (typeof handler === "string") {
|
|
1170
|
+
const rawMatch = handler.match(RE_RAW_UNIT);
|
|
1171
|
+
if (rawMatch) {
|
|
1172
|
+
const [, baseNum, cssUnit] = rawMatch;
|
|
1173
|
+
return {
|
|
1174
|
+
bucket: 1,
|
|
1175
|
+
processed: resolveUntilStable(`${numericPart * parseFloat(baseNum)}${cssUnit}`, opts, recurse)
|
|
1176
|
+
};
|
|
1177
|
+
}
|
|
1178
|
+
const base = handler;
|
|
1179
|
+
if (numericPart === 1) return {
|
|
1180
|
+
bucket: 1,
|
|
1181
|
+
processed: base
|
|
1182
|
+
};
|
|
1183
|
+
return {
|
|
1184
|
+
bucket: 1,
|
|
1185
|
+
processed: `calc(${numericPart} * ${base})`
|
|
1186
|
+
};
|
|
1187
|
+
} else return {
|
|
1188
|
+
bucket: 1,
|
|
1189
|
+
processed: handler(numericPart)
|
|
1190
|
+
};
|
|
1191
|
+
}
|
|
1192
|
+
if (/^[+-]?(?:\d*\.\d+|\d+)[a-z%]+$/.test(token)) return {
|
|
1193
|
+
bucket: 1,
|
|
1194
|
+
processed: token
|
|
1195
|
+
};
|
|
1196
|
+
if (RE_NUMBER.test(token)) return {
|
|
1197
|
+
bucket: 1,
|
|
1198
|
+
processed: token
|
|
1199
|
+
};
|
|
1200
|
+
if (VALUE_KEYWORDS.has(token)) return {
|
|
1201
|
+
bucket: 1,
|
|
1202
|
+
processed: token
|
|
1203
|
+
};
|
|
1204
|
+
if (token === "transparent" || token === "currentcolor") return {
|
|
1205
|
+
bucket: 0,
|
|
1206
|
+
processed: token
|
|
1207
|
+
};
|
|
1208
|
+
return {
|
|
1209
|
+
bucket: 2,
|
|
1210
|
+
processed: token
|
|
1211
|
+
};
|
|
1212
|
+
}
|
|
1213
|
+
//#endregion
|
|
1214
|
+
//#region src/parser/lru.ts
|
|
1215
|
+
/**
|
|
1216
|
+
* Doubly-linked-list LRU.
|
|
1217
|
+
*
|
|
1218
|
+
* The list holds direct node references rather than keys, so rewiring on a
|
|
1219
|
+
* `get()` of a non-MRU entry is pure pointer work. Storing keys instead cost
|
|
1220
|
+
* three extra `Map.get()` lookups per read (prev, next and the current head),
|
|
1221
|
+
* which is measurable on these read-mostly caches: they are hit on every
|
|
1222
|
+
* render once the style pipeline is warm.
|
|
1223
|
+
*/
|
|
1224
|
+
var Lru = class {
|
|
1225
|
+
limit;
|
|
1226
|
+
map = /* @__PURE__ */ new Map();
|
|
1227
|
+
head = null;
|
|
1228
|
+
tail = null;
|
|
1229
|
+
onEvict;
|
|
1230
|
+
constructor(limit = 1e3, onEvict) {
|
|
1231
|
+
this.limit = limit;
|
|
1232
|
+
let normalized = Number.isFinite(this.limit) ? Math.floor(this.limit) : 1e3;
|
|
1233
|
+
if (normalized <= 0) normalized = 1e3;
|
|
1234
|
+
this.limit = normalized;
|
|
1235
|
+
this.onEvict = onEvict;
|
|
1236
|
+
}
|
|
1237
|
+
setOnEvict(fn) {
|
|
1238
|
+
this.onEvict = fn;
|
|
1239
|
+
}
|
|
1240
|
+
get(key) {
|
|
1241
|
+
const node = this.map.get(key);
|
|
1242
|
+
if (node === void 0) return void 0;
|
|
1243
|
+
if (this.head !== node) this.moveToHead(node);
|
|
1244
|
+
return node.value;
|
|
1245
|
+
}
|
|
1246
|
+
set(key, value) {
|
|
1247
|
+
const existing = this.map.get(key);
|
|
1248
|
+
if (existing !== void 0) {
|
|
1249
|
+
existing.value = value;
|
|
1250
|
+
if (this.head !== existing) this.moveToHead(existing);
|
|
1251
|
+
return;
|
|
1252
|
+
}
|
|
1253
|
+
const node = {
|
|
1254
|
+
key,
|
|
1255
|
+
prev: null,
|
|
1256
|
+
next: this.head,
|
|
1257
|
+
value
|
|
1258
|
+
};
|
|
1259
|
+
if (this.head) this.head.prev = node;
|
|
1260
|
+
this.head = node;
|
|
1261
|
+
if (!this.tail) this.tail = node;
|
|
1262
|
+
this.map.set(key, node);
|
|
1263
|
+
if (this.map.size > this.limit) this.evict();
|
|
1264
|
+
}
|
|
1265
|
+
delete(key) {
|
|
1266
|
+
const node = this.map.get(key);
|
|
1267
|
+
if (node === void 0) return;
|
|
1268
|
+
this.detach(node);
|
|
1269
|
+
this.map.delete(key);
|
|
1270
|
+
}
|
|
1271
|
+
keys() {
|
|
1272
|
+
return this.map.keys();
|
|
1273
|
+
}
|
|
1274
|
+
detach(node) {
|
|
1275
|
+
if (node.prev) node.prev.next = node.next;
|
|
1276
|
+
if (node.next) node.next.prev = node.prev;
|
|
1277
|
+
if (this.head === node) this.head = node.next;
|
|
1278
|
+
if (this.tail === node) this.tail = node.prev;
|
|
1279
|
+
node.prev = null;
|
|
1280
|
+
node.next = null;
|
|
1281
|
+
}
|
|
1282
|
+
moveToHead(node) {
|
|
1283
|
+
this.detach(node);
|
|
1284
|
+
node.next = this.head;
|
|
1285
|
+
if (this.head) this.head.prev = node;
|
|
1286
|
+
this.head = node;
|
|
1287
|
+
if (!this.tail) this.tail = node;
|
|
1288
|
+
}
|
|
1289
|
+
evict() {
|
|
1290
|
+
const node = this.tail;
|
|
1291
|
+
if (!node) return;
|
|
1292
|
+
this.detach(node);
|
|
1293
|
+
this.map.delete(node.key);
|
|
1294
|
+
if (this.onEvict) try {
|
|
1295
|
+
this.onEvict(node.key, node.value);
|
|
1296
|
+
} catch {}
|
|
1297
|
+
}
|
|
1298
|
+
clear() {
|
|
1299
|
+
this.map.clear();
|
|
1300
|
+
this.head = this.tail = null;
|
|
1301
|
+
}
|
|
1302
|
+
};
|
|
1303
|
+
//#endregion
|
|
1304
|
+
//#region src/parser/tokenizer.ts
|
|
1305
|
+
const isWhitespace = (ch) => ch === " " || ch === "\n" || ch === " " || ch === "\r" || ch === "\f";
|
|
1306
|
+
function scan(src, cb) {
|
|
1307
|
+
let depth = 0;
|
|
1308
|
+
let inUrl = false;
|
|
1309
|
+
let inQuote = 0;
|
|
1310
|
+
let start = 0;
|
|
1311
|
+
let i = 0;
|
|
1312
|
+
let pendingSlash = false;
|
|
1313
|
+
const flush = (isComma, isSlash) => {
|
|
1314
|
+
const actualSlash = isSlash || pendingSlash;
|
|
1315
|
+
pendingSlash = false;
|
|
1316
|
+
if (start < i) {
|
|
1317
|
+
const prevChar = start > 0 ? src[start - 1] : null;
|
|
1318
|
+
cb(src.slice(start, i), isComma, actualSlash, prevChar);
|
|
1319
|
+
} else if (isComma) cb("", true, false, null);
|
|
1320
|
+
else if (actualSlash) cb("", false, true, null);
|
|
1321
|
+
start = i + 1;
|
|
1322
|
+
};
|
|
1323
|
+
for (; i < src.length; i++) {
|
|
1324
|
+
const ch = src[i];
|
|
1325
|
+
if (inQuote) {
|
|
1326
|
+
if (ch === inQuote && src[i - 1] !== "\\") inQuote = 0;
|
|
1327
|
+
continue;
|
|
1328
|
+
}
|
|
1329
|
+
if (ch === "\"" || ch === "'") {
|
|
1330
|
+
inQuote = ch;
|
|
1331
|
+
continue;
|
|
1332
|
+
}
|
|
1333
|
+
if (ch === "(") {
|
|
1334
|
+
if (!depth) {
|
|
1335
|
+
if (src.slice(Math.max(0, i - 3), i + 1) === "url(") inUrl = true;
|
|
1336
|
+
}
|
|
1337
|
+
depth++;
|
|
1338
|
+
continue;
|
|
1339
|
+
}
|
|
1340
|
+
if (ch === ")") {
|
|
1341
|
+
depth = Math.max(0, depth - 1);
|
|
1342
|
+
if (inUrl && depth === 0) inUrl = false;
|
|
1343
|
+
continue;
|
|
1344
|
+
}
|
|
1345
|
+
if (inUrl) continue;
|
|
1346
|
+
if (!depth) {
|
|
1347
|
+
if (ch === ",") {
|
|
1348
|
+
flush(true, false);
|
|
1349
|
+
continue;
|
|
1350
|
+
}
|
|
1351
|
+
if (ch === "/") {
|
|
1352
|
+
const prevIsWhitespace = isWhitespace(src[i - 1]);
|
|
1353
|
+
const nextIsWhitespace = isWhitespace(src[i + 1]);
|
|
1354
|
+
if (prevIsWhitespace && nextIsWhitespace) {
|
|
1355
|
+
pendingSlash = true;
|
|
1356
|
+
start = i + 1;
|
|
1357
|
+
continue;
|
|
1358
|
+
}
|
|
1359
|
+
continue;
|
|
1360
|
+
}
|
|
1361
|
+
if (isWhitespace(ch)) {
|
|
1362
|
+
flush(false, false);
|
|
1363
|
+
continue;
|
|
1364
|
+
}
|
|
1365
|
+
}
|
|
1366
|
+
}
|
|
1367
|
+
flush(false, false);
|
|
1368
|
+
}
|
|
1369
|
+
//#endregion
|
|
1370
|
+
//#region src/parser/parser.ts
|
|
1371
|
+
var StyleParser = class {
|
|
1372
|
+
opts;
|
|
1373
|
+
cache;
|
|
1374
|
+
constructor(opts = {}) {
|
|
1375
|
+
this.opts = opts;
|
|
1376
|
+
this.cache = new Lru(this.opts.cacheSize ?? 1e3);
|
|
1377
|
+
}
|
|
1378
|
+
process(src) {
|
|
1379
|
+
const key = String(src);
|
|
1380
|
+
const hit = this.cache.get(key);
|
|
1381
|
+
if (hit) return hit;
|
|
1382
|
+
const stripped = foldDslCase(src.replace(/\/\*[^*]*\*+(?:[^/*][^*]*\*+)*\//g, ""));
|
|
1383
|
+
const groups = [];
|
|
1384
|
+
let currentGroup = makeEmptyDetails();
|
|
1385
|
+
let currentPart = makeEmptyPart();
|
|
1386
|
+
let parts = [];
|
|
1387
|
+
const pushToken = (bucket, processed) => {
|
|
1388
|
+
if (!processed) return;
|
|
1389
|
+
const mergeIntoPrevUrl = () => {
|
|
1390
|
+
const lastIdx = currentPart.values.length - 1;
|
|
1391
|
+
currentPart.values[lastIdx] += ` ${processed}`;
|
|
1392
|
+
const lastAllIdx = currentPart.all.length - 1;
|
|
1393
|
+
currentPart.all[lastAllIdx] += ` ${processed}`;
|
|
1394
|
+
};
|
|
1395
|
+
if (currentPart.values.length > 0 && currentPart.values[currentPart.values.length - 1].startsWith("url(")) {
|
|
1396
|
+
mergeIntoPrevUrl();
|
|
1397
|
+
return;
|
|
1398
|
+
}
|
|
1399
|
+
switch (bucket) {
|
|
1400
|
+
case 0:
|
|
1401
|
+
currentPart.colors.push(processed);
|
|
1402
|
+
break;
|
|
1403
|
+
case 1:
|
|
1404
|
+
currentPart.values.push(processed);
|
|
1405
|
+
break;
|
|
1406
|
+
case 2:
|
|
1407
|
+
currentPart.mods.push(processed);
|
|
1408
|
+
break;
|
|
1409
|
+
case 3:
|
|
1410
|
+
currentPart.colors.push(processed);
|
|
1411
|
+
currentPart.values.push(processed);
|
|
1412
|
+
break;
|
|
1413
|
+
}
|
|
1414
|
+
currentPart.all.push(processed);
|
|
1415
|
+
};
|
|
1416
|
+
const endPart = () => {
|
|
1417
|
+
if (currentPart.all.length > 0) {
|
|
1418
|
+
finalizePart(currentPart);
|
|
1419
|
+
parts.push(currentPart);
|
|
1420
|
+
}
|
|
1421
|
+
currentPart = makeEmptyPart();
|
|
1422
|
+
};
|
|
1423
|
+
const endGroup = () => {
|
|
1424
|
+
endPart();
|
|
1425
|
+
if (parts.length === 0) {
|
|
1426
|
+
parts.push(makeEmptyPart());
|
|
1427
|
+
finalizePart(parts[0]);
|
|
1428
|
+
}
|
|
1429
|
+
finalizeGroup(currentGroup, parts);
|
|
1430
|
+
groups.push(currentGroup);
|
|
1431
|
+
currentGroup = makeEmptyDetails();
|
|
1432
|
+
parts = [];
|
|
1433
|
+
currentPart = makeEmptyPart();
|
|
1434
|
+
};
|
|
1435
|
+
scan(stripped, (tok, isComma, isSlash, _prevChar) => {
|
|
1436
|
+
if (tok) {
|
|
1437
|
+
if (currentGroup.input) currentGroup.input += ` ${tok}`;
|
|
1438
|
+
else currentGroup.input = tok;
|
|
1439
|
+
const { bucket, processed } = classify(tok, this.opts, (sub) => this.process(sub));
|
|
1440
|
+
pushToken(bucket, processed);
|
|
1441
|
+
}
|
|
1442
|
+
if (isSlash) endPart();
|
|
1443
|
+
if (isComma) endGroup();
|
|
1444
|
+
});
|
|
1445
|
+
if (currentPart.all.length || parts.length || !groups.length) endGroup();
|
|
1446
|
+
const result = {
|
|
1447
|
+
output: groups.map((g) => g.output).join(", "),
|
|
1448
|
+
groups
|
|
1449
|
+
};
|
|
1450
|
+
Object.freeze(result);
|
|
1451
|
+
this.cache.set(key, result);
|
|
1452
|
+
return result;
|
|
1453
|
+
}
|
|
1454
|
+
setFunctions(functions) {
|
|
1455
|
+
this.opts.functions = functions;
|
|
1456
|
+
this.cache.clear();
|
|
1457
|
+
}
|
|
1458
|
+
setUnits(units) {
|
|
1459
|
+
this.opts.units = units;
|
|
1460
|
+
this.cache.clear();
|
|
1461
|
+
}
|
|
1462
|
+
updateOptions(patch) {
|
|
1463
|
+
Object.assign(this.opts, patch);
|
|
1464
|
+
if (patch.cacheSize) this.cache = new Lru(patch.cacheSize);
|
|
1465
|
+
else this.cache.clear();
|
|
1466
|
+
}
|
|
1467
|
+
/**
|
|
1468
|
+
* Clear the parser cache.
|
|
1469
|
+
* Call this when external state that affects parsing results has changed
|
|
1470
|
+
* (e.g., predefined tokens).
|
|
1471
|
+
*/
|
|
1472
|
+
clearCache() {
|
|
1473
|
+
this.cache.clear();
|
|
1474
|
+
}
|
|
1475
|
+
/**
|
|
1476
|
+
* Get the current units configuration.
|
|
1477
|
+
*/
|
|
1478
|
+
getUnits() {
|
|
1479
|
+
return this.opts.units;
|
|
1480
|
+
}
|
|
1481
|
+
};
|
|
1482
|
+
//#endregion
|
|
1483
|
+
//#region src/plugins/color-func.ts
|
|
1484
|
+
const clamp = (value, min, max) => Math.max(Math.min(value, max), min);
|
|
1485
|
+
/**
|
|
1486
|
+
* Parse an angle value with optional unit.
|
|
1487
|
+
* Supports: deg, turn, rad, or unitless (treated as degrees).
|
|
1488
|
+
*/
|
|
1489
|
+
const parseAngle = (value) => {
|
|
1490
|
+
const match = value.match(/^([+-]?\d*\.?\d+)(deg|turn|rad)?$/);
|
|
1491
|
+
if (!match) return 0;
|
|
1492
|
+
const num = parseFloat(match[1]);
|
|
1493
|
+
switch (match[2]) {
|
|
1494
|
+
case "turn": return num * 360;
|
|
1495
|
+
case "rad": return num * 180 / Math.PI;
|
|
1496
|
+
default: return num;
|
|
1497
|
+
}
|
|
1498
|
+
};
|
|
1499
|
+
/**
|
|
1500
|
+
* Parse a percentage value (e.g., "50%") to a 0-1 range. A unitless number is
|
|
1501
|
+
* read as the factor it already looks like, so `okhsl(280 .8 .52)` and
|
|
1502
|
+
* `okhsl(280 80% 52%)` are the same color.
|
|
1503
|
+
*/
|
|
1504
|
+
const parsePercentage = (value) => {
|
|
1505
|
+
const match = value.match(/^([+-]?\d*\.?\d+)%?$/);
|
|
1506
|
+
if (!match) return 0;
|
|
1507
|
+
const num = parseFloat(match[1]);
|
|
1508
|
+
return value.includes("%") ? num / 100 : num;
|
|
1509
|
+
};
|
|
1510
|
+
/**
|
|
1511
|
+
* Creates a color function handler for the tasty parser.
|
|
1512
|
+
*
|
|
1513
|
+
* A color function is a custom `functions` entry that converts its arguments
|
|
1514
|
+
* to an `rgb(...)` string (an already-supported color), so it needs no special
|
|
1515
|
+
* core integration — registering it as a parse function is enough.
|
|
1516
|
+
*
|
|
1517
|
+
* @param name The name of the color function (e.g., 'okhsl', 'okhst')
|
|
1518
|
+
* @param convert Converts the parsed first three channel values (after angle/
|
|
1519
|
+
* percent normalization) to sRGB `[r, g, b]` in the 0-1 range
|
|
1520
|
+
* @param label Optional diagnostic label shown in dev warnings when the wrong
|
|
1521
|
+
* number of arguments is supplied (e.g., 'H S L'). Has no effect on parsing
|
|
1522
|
+
* or output.
|
|
1523
|
+
*/
|
|
1524
|
+
function createColorFunc(name, convert, label) {
|
|
1525
|
+
const conversionCache = new Lru(500);
|
|
1526
|
+
const expected = label ? ` (${label})` : "";
|
|
1527
|
+
return (groups) => {
|
|
1528
|
+
if (groups.length === 0 || groups[0].all.length < 3) {
|
|
1529
|
+
if (isDevEnv()) console.warn(`[Tasty] ${name}(): expected 3 values${expected}, got:`, groups);
|
|
1530
|
+
return "rgb(0% 0% 0%)";
|
|
1531
|
+
}
|
|
1532
|
+
const group = groups[0];
|
|
1533
|
+
const tokens = group.all;
|
|
1534
|
+
const alpha = group.parts.length > 1 && group.parts[1].all.length > 0 ? group.parts[1].output : void 0;
|
|
1535
|
+
const cacheKey = tokens.slice(0, 3).join(" ") + (alpha ? ` / ${alpha}` : "");
|
|
1536
|
+
const cached = conversionCache.get(cacheKey);
|
|
1537
|
+
if (cached) return cached;
|
|
1538
|
+
const h = parseAngle(tokens[0]);
|
|
1539
|
+
const c2 = parsePercentage(tokens[1]);
|
|
1540
|
+
const c3 = parsePercentage(tokens[2]);
|
|
1541
|
+
const misscaled = [tokens[1], tokens[2]].filter((raw, i) => !raw.includes("%") && (i === 0 ? c2 : c3) > 1);
|
|
1542
|
+
if (misscaled.length) warnOnceDev(`${name}:percent-scale`, `${name}(): channels are factors (0-1) or percentages, so ${misscaled.map((v) => `"${v}"`).join(" and ")} ${misscaled.length > 1 ? "clamp" : "clamps"} to 1 — add the missing '%', or write the value as a 0-1 number.`);
|
|
1543
|
+
const [r, g, b] = convert(h, clamp(c2, 0, 1), clamp(c3, 0, 1));
|
|
1544
|
+
const format = (n) => {
|
|
1545
|
+
const pct = n * 100;
|
|
1546
|
+
return parseFloat(pct.toFixed(1)).toString() + "%";
|
|
1547
|
+
};
|
|
1548
|
+
const result = alpha ? `rgb(${format(r)} ${format(g)} ${format(b)} / ${alpha})` : `rgb(${format(r)} ${format(g)} ${format(b)})`;
|
|
1549
|
+
conversionCache.set(cacheKey, result);
|
|
1550
|
+
return result;
|
|
1551
|
+
};
|
|
1552
|
+
}
|
|
1553
|
+
//#endregion
|
|
1554
|
+
//#region src/plugins/okhsl-plugin.ts
|
|
1555
|
+
/**
|
|
1556
|
+
* OKHSL Plugin for Tasty
|
|
1557
|
+
*
|
|
1558
|
+
* Converts OKHSL color syntax to RGB notation.
|
|
1559
|
+
* Supports angle units: deg, turn, rad, or unitless (degrees).
|
|
1560
|
+
*
|
|
1561
|
+
* Examples:
|
|
1562
|
+
* okhsl(240.5 50% 50%)
|
|
1563
|
+
* okhsl(240.5deg 50% 50%)
|
|
1564
|
+
* okhsl(0.25turn 50% 50%)
|
|
1565
|
+
* okhsl(1.57rad 50% 50%)
|
|
1566
|
+
*/
|
|
1567
|
+
/**
|
|
1568
|
+
* The okhsl function handler for tasty parser.
|
|
1569
|
+
* Receives parsed style groups and returns an RGB color string.
|
|
1570
|
+
*/
|
|
1571
|
+
const okhslFunction = createColorFunc("okhsl", okhslToSrgb, "H S L");
|
|
1572
|
+
/**
|
|
1573
|
+
* OKHSL Plugin for Tasty.
|
|
1574
|
+
*
|
|
1575
|
+
* Adds support for the `okhsl()` color function in tasty styles.
|
|
1576
|
+
*
|
|
1577
|
+
* @example
|
|
1578
|
+
* ```ts
|
|
1579
|
+
* import { configure } from '@tenphi/tasty';
|
|
1580
|
+
* import { okhslPlugin } from '@tenphi/tasty';
|
|
1581
|
+
*
|
|
1582
|
+
* configure({
|
|
1583
|
+
* plugins: [okhslPlugin()],
|
|
1584
|
+
* });
|
|
1585
|
+
*
|
|
1586
|
+
* // Now you can use okhsl in styles:
|
|
1587
|
+
* const Box = tasty({
|
|
1588
|
+
* styles: {
|
|
1589
|
+
* fill: 'okhsl(240 50% 50%)',
|
|
1590
|
+
* },
|
|
1591
|
+
* });
|
|
1592
|
+
* ```
|
|
1593
|
+
*/
|
|
1594
|
+
const okhslPlugin = () => ({
|
|
1595
|
+
name: "okhsl",
|
|
1596
|
+
functions: { okhsl: okhslFunction }
|
|
1597
|
+
});
|
|
1598
|
+
//#endregion
|
|
1599
|
+
//#region src/plugins/okhst-plugin.ts
|
|
1600
|
+
/**
|
|
1601
|
+
* OKHST Plugin for Tasty
|
|
1602
|
+
*
|
|
1603
|
+
* Converts OKHST color syntax to RGB notation.
|
|
1604
|
+
* OKHST is OKHSL with the lightness axis replaced by a contrast-uniform tone axis.
|
|
1605
|
+
* Supports angle units: deg, turn, rad, or unitless (degrees).
|
|
1606
|
+
*
|
|
1607
|
+
* Examples:
|
|
1608
|
+
* okhst(240.5 50% 50%)
|
|
1609
|
+
* okhst(240.5deg 50% 50%)
|
|
1610
|
+
* okhst(0.25turn 50% 50%)
|
|
1611
|
+
* okhst(1.57rad 50% 50%)
|
|
1612
|
+
*/
|
|
1613
|
+
/**
|
|
1614
|
+
* The okhst function handler for tasty parser.
|
|
1615
|
+
* Receives parsed style groups and returns an RGB color string.
|
|
1616
|
+
*/
|
|
1617
|
+
const okhstFunction = createColorFunc("okhst", okhstToSrgb, "H S T");
|
|
1618
|
+
/**
|
|
1619
|
+
* OKHST Plugin for Tasty.
|
|
1620
|
+
*
|
|
1621
|
+
* Adds support for the `okhst()` color function in tasty styles.
|
|
1622
|
+
*
|
|
1623
|
+
* @example
|
|
1624
|
+
* ```ts
|
|
1625
|
+
* import { configure } from '@tenphi/tasty';
|
|
1626
|
+
* import { okhstPlugin } from '@tenphi/tasty';
|
|
1627
|
+
*
|
|
1628
|
+
* configure({
|
|
1629
|
+
* plugins: [okhstPlugin()],
|
|
1630
|
+
* });
|
|
1631
|
+
*
|
|
1632
|
+
* // Now you can use okhst in styles:
|
|
1633
|
+
* const Box = tasty({
|
|
1634
|
+
* styles: {
|
|
1635
|
+
* fill: 'okhst(240 50% 50%)',
|
|
1636
|
+
* },
|
|
1637
|
+
* });
|
|
1638
|
+
* ```
|
|
1639
|
+
*/
|
|
1640
|
+
const okhstPlugin = () => ({
|
|
1641
|
+
name: "okhst",
|
|
1642
|
+
functions: { okhst: okhstFunction }
|
|
1643
|
+
});
|
|
1644
|
+
//#endregion
|
|
1645
|
+
//#region src/plugins/defaults.ts
|
|
1646
|
+
/**
|
|
1647
|
+
* Register the default color functions (okhsl, okhst) with the global parser.
|
|
1648
|
+
*
|
|
1649
|
+
* They are ordinary `functions` entries — the same mechanism any third-party
|
|
1650
|
+
* color plugin uses — so they need no special-casing in core.
|
|
1651
|
+
*
|
|
1652
|
+
* Idempotent and safe to call from the lazy parser initializer so that
|
|
1653
|
+
* zero-config usage keeps working. `resetGlobalParseFunctions()` re-invokes it
|
|
1654
|
+
* after clearing user-registered functions so the defaults survive a config reset.
|
|
1655
|
+
*
|
|
1656
|
+
* @internal
|
|
1657
|
+
*/
|
|
1658
|
+
function registerDefaultFunctions() {
|
|
1659
|
+
customFunction("okhsl", okhslFunction);
|
|
1660
|
+
customFunction("okhst", okhstFunction);
|
|
1661
|
+
}
|
|
1662
|
+
//#endregion
|
|
1663
|
+
//#region src/utils/styles.ts
|
|
1664
|
+
/**
|
|
1665
|
+
* Normalize a color token value.
|
|
1666
|
+
* - Boolean `true` is converted to `'transparent'`
|
|
1667
|
+
* - Boolean `false` returns `null` (signals the token should be skipped)
|
|
1668
|
+
* - Other values are returned as-is
|
|
1669
|
+
*
|
|
1670
|
+
* @param value - The raw token value
|
|
1671
|
+
* @returns Normalized value or null if the token should be skipped
|
|
1672
|
+
*/
|
|
1673
|
+
function normalizeColorTokenValue(value) {
|
|
1674
|
+
if (value === true) return "transparent";
|
|
1675
|
+
if (value === false) return null;
|
|
1676
|
+
return value;
|
|
1677
|
+
}
|
|
1678
|
+
const COLOR_VAR_PATTERN = /^var\(--([a-z0-9-]+)-color[,)]/;
|
|
1679
|
+
const RGB_ALPHA_PATTERN = /\/\s*([0-9.]+)\)/;
|
|
1680
|
+
const RE_HEX_COLOR = /^#[0-9a-fA-F]{3,8}$/;
|
|
1681
|
+
const RE_VAR_COLOR = /^var\(--[a-z0-9-]+-color/;
|
|
1682
|
+
function isSimpleColorFast(val) {
|
|
1683
|
+
switch (val.charCodeAt(0)) {
|
|
1684
|
+
case 35: return RE_HEX_COLOR.test(val);
|
|
1685
|
+
case 114:
|
|
1686
|
+
if (val.charCodeAt(1) === 103 && val.charCodeAt(2) === 98) return true;
|
|
1687
|
+
break;
|
|
1688
|
+
case 104:
|
|
1689
|
+
if (val.charCodeAt(1) === 115 && val.charCodeAt(2) === 108) return true;
|
|
1690
|
+
break;
|
|
1691
|
+
case 108:
|
|
1692
|
+
if (val.charCodeAt(1) === 99 && val.charCodeAt(2) === 104) return true;
|
|
1693
|
+
break;
|
|
1694
|
+
case 111:
|
|
1695
|
+
if (val.startsWith("oklch(")) return true;
|
|
1696
|
+
break;
|
|
1697
|
+
case 118:
|
|
1698
|
+
if (RE_VAR_COLOR.test(val)) return true;
|
|
1699
|
+
break;
|
|
1700
|
+
case 99:
|
|
1701
|
+
if (val === "currentColor" || val === "currentcolor") return true;
|
|
1702
|
+
break;
|
|
1703
|
+
case 116:
|
|
1704
|
+
if (val === "transparent") return true;
|
|
1705
|
+
break;
|
|
1706
|
+
}
|
|
1707
|
+
return getNamedColorHex().has(val.toLowerCase());
|
|
1708
|
+
}
|
|
1709
|
+
let colorWarningCount = 0;
|
|
1710
|
+
const MAX_COLOR_WARNINGS = 10;
|
|
1711
|
+
const CUSTOM_UNITS = {
|
|
1712
|
+
r: "6px",
|
|
1713
|
+
cr: "10px",
|
|
1714
|
+
bw: "1px",
|
|
1715
|
+
ow: "3px",
|
|
1716
|
+
x: "8px",
|
|
1717
|
+
sf: function sf(num) {
|
|
1718
|
+
return `minmax(0, ${num}fr)`;
|
|
1719
|
+
}
|
|
1720
|
+
};
|
|
1721
|
+
const DIRECTIONS = [
|
|
1722
|
+
"top",
|
|
1723
|
+
"right",
|
|
1724
|
+
"bottom",
|
|
1725
|
+
"left"
|
|
1726
|
+
];
|
|
1727
|
+
let __tastyParser = null;
|
|
1728
|
+
function getOrCreateParser() {
|
|
1729
|
+
if (!__tastyParser) {
|
|
1730
|
+
__tastyParser = new StyleParser({ units: CUSTOM_UNITS });
|
|
1731
|
+
__tastyParser.setFunctions(__tastyParseFunctions);
|
|
1732
|
+
registerDefaultFunctions();
|
|
1733
|
+
}
|
|
1734
|
+
return __tastyParser;
|
|
1735
|
+
}
|
|
1736
|
+
const __tastyParseFunctions = {};
|
|
1737
|
+
/**
|
|
1738
|
+
* Register a parse-time function.
|
|
1739
|
+
*
|
|
1740
|
+
* @internal Use `configure({ functions })`. This bypasses the configuration lock
|
|
1741
|
+
* and is kept only for the lazy default-function bootstrap and the `@function`
|
|
1742
|
+
* polyfill, both of which must register after `configure()` has run.
|
|
1743
|
+
*/
|
|
1744
|
+
function customFunction(name, fn) {
|
|
1745
|
+
__tastyParseFunctions[name] = fn;
|
|
1746
|
+
getOrCreateParser().setFunctions(__tastyParseFunctions);
|
|
1747
|
+
}
|
|
1748
|
+
/**
|
|
1749
|
+
* Reset parse functions back to the built-in set (okhsl/okhst).
|
|
1750
|
+
*
|
|
1751
|
+
* @internal Called by `resetConfig()`.
|
|
1752
|
+
*/
|
|
1753
|
+
function resetGlobalParseFunctions() {
|
|
1754
|
+
for (const key of Object.keys(__tastyParseFunctions)) delete __tastyParseFunctions[key];
|
|
1755
|
+
registerDefaultFunctions();
|
|
1756
|
+
getOrCreateParser().setFunctions(__tastyParseFunctions);
|
|
1757
|
+
}
|
|
1758
|
+
/**
|
|
1759
|
+
* Get the global StyleParser instance.
|
|
1760
|
+
* Used by configure() to apply parser configuration.
|
|
1761
|
+
*/
|
|
1762
|
+
function getGlobalParser() {
|
|
1763
|
+
return getOrCreateParser();
|
|
1764
|
+
}
|
|
1765
|
+
/**
|
|
1766
|
+
* Get the current custom functions registry.
|
|
1767
|
+
* Used by configure() to merge with new functions.
|
|
1768
|
+
*/
|
|
1769
|
+
/**
|
|
1770
|
+
* The live parse-function registry.
|
|
1771
|
+
*
|
|
1772
|
+
* @internal Returns the mutable internal map; writing to it bypasses the
|
|
1773
|
+
* parser's cache invalidation, so the write silently does not take effect.
|
|
1774
|
+
* Read-only, internal use.
|
|
1775
|
+
*/
|
|
1776
|
+
function getGlobalParseFunctions() {
|
|
1777
|
+
return __tastyParseFunctions;
|
|
1778
|
+
}
|
|
1779
|
+
/**
|
|
1780
|
+
* Storage for predefined tokens that are replaced during style parsing.
|
|
1781
|
+
* Keys are token names (with $ or # prefix), values are pre-processed CSS values.
|
|
1782
|
+
*/
|
|
1783
|
+
let __globalPredefinedTokens = null;
|
|
1784
|
+
/**
|
|
1785
|
+
* Set global predefined tokens.
|
|
1786
|
+
* Called from configure() after processing token values.
|
|
1787
|
+
* Merges with existing tokens (new tokens override existing ones with same key).
|
|
1788
|
+
* Keys are normalized to lowercase (parser lowercases input before classification).
|
|
1789
|
+
* @internal
|
|
1790
|
+
*/
|
|
1791
|
+
function setGlobalPredefinedTokens(tokens) {
|
|
1792
|
+
const normalizedTokens = {};
|
|
1793
|
+
for (const [key, value] of Object.entries(tokens)) {
|
|
1794
|
+
const lowerKey = key.toLowerCase();
|
|
1795
|
+
const lowerValue = value.toLowerCase();
|
|
1796
|
+
if (lowerKey.startsWith("#") && lowerValue === "#current") {
|
|
1797
|
+
console.warn(`[Tasty] Using #current to define color token "${key}" is not supported. The #current token represents currentcolor which cannot be used as a base for other tokens.`);
|
|
1798
|
+
continue;
|
|
1799
|
+
}
|
|
1800
|
+
normalizedTokens[lowerKey] = value;
|
|
1801
|
+
}
|
|
1802
|
+
__globalPredefinedTokens = __globalPredefinedTokens ? {
|
|
1803
|
+
...__globalPredefinedTokens,
|
|
1804
|
+
...normalizedTokens
|
|
1805
|
+
} : normalizedTokens;
|
|
1806
|
+
getOrCreateParser().clearCache();
|
|
1807
|
+
}
|
|
1808
|
+
/**
|
|
1809
|
+
* Get the current global predefined tokens.
|
|
1810
|
+
* Returns null if no tokens are configured.
|
|
1811
|
+
*/
|
|
1812
|
+
function getGlobalPredefinedTokens() {
|
|
1813
|
+
return __globalPredefinedTokens;
|
|
1814
|
+
}
|
|
1815
|
+
/**
|
|
1816
|
+
* Reset global predefined tokens.
|
|
1817
|
+
* Used for testing.
|
|
1818
|
+
* @internal
|
|
1819
|
+
*/
|
|
1820
|
+
function resetGlobalPredefinedTokens() {
|
|
1821
|
+
__globalPredefinedTokens = null;
|
|
1822
|
+
getOrCreateParser().clearCache();
|
|
1823
|
+
}
|
|
1824
|
+
/**
|
|
1825
|
+
*
|
|
1826
|
+
* @param {String} value
|
|
1827
|
+
* @param {Number} mode
|
|
1828
|
+
* @returns {Object<String,String|Array>}
|
|
1829
|
+
*/
|
|
1830
|
+
function parseStyle(value) {
|
|
1831
|
+
let str;
|
|
1832
|
+
if (typeof value === "string") str = value;
|
|
1833
|
+
else if (typeof value === "number") str = String(value);
|
|
1834
|
+
else str = "";
|
|
1835
|
+
return getOrCreateParser().process(str);
|
|
1836
|
+
}
|
|
1837
|
+
/** A `$`-prefixed custom-property reference — the only DSL syntax that starts with `$`. */
|
|
1838
|
+
const RE_CUSTOM_PROPERTY_REF = /\$[a-zA-Z_$]/;
|
|
1839
|
+
/**
|
|
1840
|
+
* Substitute custom-property references (`$ident` → `var(--ident)`) inside a
|
|
1841
|
+
* value a handler would otherwise emit to CSS verbatim.
|
|
1842
|
+
*
|
|
1843
|
+
* Handlers for keyword-valued properties (`display`, `align`, `text-transform`,
|
|
1844
|
+
* `overflow`, …) pass their input straight through, since there are no units or
|
|
1845
|
+
* color tokens to resolve. That leaked the raw DSL into the stylesheet:
|
|
1846
|
+
* `display: '$my-display'` emitted `display: $my-display`, which the browser
|
|
1847
|
+
* drops as invalid. The same applies to color props whose value the parser could
|
|
1848
|
+
* not classify as a color — `$ident` is a color only when the name ends with
|
|
1849
|
+
* `-color`, so `fill: '$my-fill'` fell back to the unparsed input.
|
|
1850
|
+
*
|
|
1851
|
+
* Values without a `$` skip the parser entirely, so pass-through behaviour (and
|
|
1852
|
+
* the parser's case folding) is unchanged for every value that was already
|
|
1853
|
+
* valid CSS.
|
|
1854
|
+
*/
|
|
1855
|
+
function resolveCustomProperties(value) {
|
|
1856
|
+
if (!RE_CUSTOM_PROPERTY_REF.test(value)) return value;
|
|
1857
|
+
return parseStyle(value).output || value;
|
|
1858
|
+
}
|
|
1859
|
+
/**
|
|
1860
|
+
* Parse color. Find it value, name and opacity.
|
|
1861
|
+
* Optimized to avoid heavy parseStyle calls for simple color patterns.
|
|
1862
|
+
*/
|
|
1863
|
+
function parseColor(val, ignoreError = false) {
|
|
1864
|
+
if (typeof val !== "string") val = String(val ?? "");
|
|
1865
|
+
val = val.trim();
|
|
1866
|
+
if (!val) return {};
|
|
1867
|
+
const isSimpleColor = isSimpleColorFast(val);
|
|
1868
|
+
let firstColor;
|
|
1869
|
+
if (isSimpleColor) firstColor = val;
|
|
1870
|
+
else {
|
|
1871
|
+
const extractedColor = parseStyle(val).groups.find((g) => g.colors.length)?.colors[0];
|
|
1872
|
+
if (!extractedColor) {
|
|
1873
|
+
if (!ignoreError && colorWarningCount < MAX_COLOR_WARNINGS) {
|
|
1874
|
+
console.warn("[Tasty] unable to parse color:", val);
|
|
1875
|
+
colorWarningCount++;
|
|
1876
|
+
if (colorWarningCount === MAX_COLOR_WARNINGS) console.warn("[Tasty] color parsing warnings will be suppressed from now on");
|
|
1877
|
+
}
|
|
1878
|
+
return {};
|
|
1879
|
+
}
|
|
1880
|
+
firstColor = extractedColor;
|
|
1881
|
+
}
|
|
1882
|
+
const faded = parseAlphaOverride(firstColor);
|
|
1883
|
+
const baseColor = faded ? faded.color : firstColor;
|
|
1884
|
+
const nameMatch = baseColor.match(COLOR_VAR_PATTERN);
|
|
1885
|
+
let opacity;
|
|
1886
|
+
if (faded) {
|
|
1887
|
+
const { alpha } = faded;
|
|
1888
|
+
const v = parseFloat(alpha);
|
|
1889
|
+
if (!isNaN(v)) opacity = alpha.endsWith("%") ? v : v * 100;
|
|
1890
|
+
} else if (baseColor.startsWith("rgb") || baseColor.startsWith("hsl") || baseColor.startsWith("lch") || baseColor.startsWith("oklch")) {
|
|
1891
|
+
const alphaMatch = baseColor.match(RGB_ALPHA_PATTERN);
|
|
1892
|
+
if (alphaMatch) {
|
|
1893
|
+
const v = parseFloat(alphaMatch[1]);
|
|
1894
|
+
if (!isNaN(v)) opacity = v * 100;
|
|
1895
|
+
}
|
|
1896
|
+
}
|
|
1897
|
+
return {
|
|
1898
|
+
color: firstColor,
|
|
1899
|
+
name: nameMatch ? nameMatch[1] : void 0,
|
|
1900
|
+
opacity
|
|
1901
|
+
};
|
|
1902
|
+
}
|
|
1903
|
+
function filterMods(mods, allowedMods) {
|
|
1904
|
+
return mods.filter((mod) => allowedMods.includes(mod));
|
|
1905
|
+
}
|
|
1906
|
+
const _innerCache = /* @__PURE__ */ new WeakMap();
|
|
1907
|
+
const _topLevelCache = /* @__PURE__ */ new WeakMap();
|
|
1908
|
+
function stringifyStyles(styles) {
|
|
1909
|
+
if (styles == null || typeof styles !== "object") return "";
|
|
1910
|
+
const cached = _topLevelCache.get(styles);
|
|
1911
|
+
if (cached !== void 0) return cached;
|
|
1912
|
+
const obj = styles;
|
|
1913
|
+
const keys = Object.keys(obj).sort();
|
|
1914
|
+
const parts = [];
|
|
1915
|
+
for (const k of keys) {
|
|
1916
|
+
const v = obj[k];
|
|
1917
|
+
if (v === void 0 || typeof v === "function" || typeof v === "symbol") continue;
|
|
1918
|
+
const c0 = k.charCodeAt(0);
|
|
1919
|
+
const needsInnerSort = (c0 >= 65 && c0 <= 90 || c0 === 38) && v && typeof v === "object" && !Array.isArray(v);
|
|
1920
|
+
let sv;
|
|
1921
|
+
if (needsInnerSort) {
|
|
1922
|
+
sv = _innerCache.get(v);
|
|
1923
|
+
if (sv === void 0) {
|
|
1924
|
+
const innerObj = v;
|
|
1925
|
+
const innerKeys = Object.keys(innerObj).sort();
|
|
1926
|
+
const innerParts = [];
|
|
1927
|
+
for (const ik of innerKeys) {
|
|
1928
|
+
const ivs = JSON.stringify(innerObj[ik]);
|
|
1929
|
+
if (ivs !== void 0) innerParts.push(JSON.stringify(ik) + ":" + ivs);
|
|
1930
|
+
}
|
|
1931
|
+
sv = "{" + innerParts.join(",") + "}";
|
|
1932
|
+
_innerCache.set(v, sv);
|
|
1933
|
+
}
|
|
1934
|
+
} else sv = JSON.stringify(v);
|
|
1935
|
+
parts.push(JSON.stringify(k) + ":" + sv);
|
|
1936
|
+
}
|
|
1937
|
+
const result = "{" + parts.join(",") + "}";
|
|
1938
|
+
_topLevelCache.set(styles, result);
|
|
1939
|
+
return result;
|
|
1940
|
+
}
|
|
1941
|
+
//#endregion
|
|
1942
|
+
//#region src/utils/function-color.ts
|
|
1943
|
+
const RE_FUNC_NAME = /^([a-z][a-z0-9-]*)\s*\(/i;
|
|
1944
|
+
const RE_COLOR_OUT = /^(?:rgb|hsl|hwb|lab|lch|oklab|oklch|color)\(|^#|^var\(--/i;
|
|
1945
|
+
/**
|
|
1946
|
+
* Resolve a `name(...)` value produced by a registered custom parse function
|
|
1947
|
+
* into its concrete color output.
|
|
1948
|
+
*
|
|
1949
|
+
* A color function is just a `functions` entry whose output is an already
|
|
1950
|
+
* supported color (`rgb`, `hsl`, `#…`, `oklch`, …). This helper delegates the
|
|
1951
|
+
* value to the global parser (which already runs the registered parse function)
|
|
1952
|
+
* and returns the result only when it looks like a color. Returns `null` when
|
|
1953
|
+
* `str` is not a registered custom function or its output is not a color.
|
|
1954
|
+
*
|
|
1955
|
+
* This is the generic replacement for the previously hardcoded okhsl/okhst
|
|
1956
|
+
* conversion branches scattered across `strToRgb`, `resolveToRgbaValues`, and
|
|
1957
|
+
* the `#token.alpha` injection path.
|
|
1958
|
+
*/
|
|
1959
|
+
function resolveFunctionColor(str) {
|
|
1960
|
+
const m = RE_FUNC_NAME.exec(str);
|
|
1961
|
+
if (!m) return null;
|
|
1962
|
+
const name = m[1].toLowerCase();
|
|
1963
|
+
getGlobalParser();
|
|
1964
|
+
if (!(name in getGlobalParseFunctions())) return null;
|
|
1965
|
+
const out = getGlobalParser().process(str).output;
|
|
1966
|
+
if (!out || !RE_COLOR_OUT.test(out)) return null;
|
|
1967
|
+
return out;
|
|
1968
|
+
}
|
|
1969
|
+
//#endregion
|
|
1970
|
+
//#region src/utils/colors.ts
|
|
1971
|
+
function color(name, opacity = 1) {
|
|
1972
|
+
if (opacity !== 1) return overrideColorAlpha(`var(--${name}-color)`, String(opacity));
|
|
1973
|
+
return `var(--${name}-color)`;
|
|
1974
|
+
}
|
|
1975
|
+
//#endregion
|
|
1976
|
+
//#region src/utils/dotize.ts
|
|
1977
|
+
const dotize = {
|
|
1978
|
+
valTypes: {
|
|
1979
|
+
none: "NONE",
|
|
1980
|
+
primitive: "PRIM",
|
|
1981
|
+
object: "OBJECT",
|
|
1982
|
+
array: "ARRAY"
|
|
1983
|
+
},
|
|
1984
|
+
getValType: function(val) {
|
|
1985
|
+
if (!val || typeof val != "object" || Array.isArray(val)) return dotize.valTypes.primitive;
|
|
1986
|
+
if (typeof val == "object") return dotize.valTypes.object;
|
|
1987
|
+
},
|
|
1988
|
+
getPathType: function(arrPath) {
|
|
1989
|
+
const arrPathTypes = [];
|
|
1990
|
+
for (const path in arrPath) {
|
|
1991
|
+
const pathVal = arrPath[path];
|
|
1992
|
+
if (!pathVal) arrPathTypes.push(dotize.valTypes.none);
|
|
1993
|
+
else if (dotize.isNumber(pathVal)) arrPathTypes.push(dotize.valTypes.array);
|
|
1994
|
+
else arrPathTypes.push(dotize.valTypes.object);
|
|
1995
|
+
}
|
|
1996
|
+
return arrPathTypes;
|
|
1997
|
+
},
|
|
1998
|
+
isUndefined: function(obj) {
|
|
1999
|
+
return typeof obj == "undefined";
|
|
2000
|
+
},
|
|
2001
|
+
isNumber: function(f) {
|
|
2002
|
+
return !isNaN(parseInt(f));
|
|
2003
|
+
},
|
|
2004
|
+
isEmptyObj: function(obj) {
|
|
2005
|
+
for (const prop in obj) if (Object.hasOwnProperty.call(obj, prop)) return false;
|
|
2006
|
+
return JSON.stringify(obj) === JSON.stringify({});
|
|
2007
|
+
},
|
|
2008
|
+
isPlainObject: function(obj) {
|
|
2009
|
+
if (typeof obj !== "object" || obj === null) return false;
|
|
2010
|
+
return Object.getPrototypeOf(obj) === Object.prototype;
|
|
2011
|
+
},
|
|
2012
|
+
isNotObject: function(obj) {
|
|
2013
|
+
return !obj || !this.isPlainObject(obj);
|
|
2014
|
+
},
|
|
2015
|
+
isEmptyArray: function(arr) {
|
|
2016
|
+
return Array.isArray(arr) && arr.length == 0;
|
|
2017
|
+
},
|
|
2018
|
+
isNotArray: function(arr) {
|
|
2019
|
+
return Array.isArray(arr) == false;
|
|
2020
|
+
},
|
|
2021
|
+
removeEmptyArrayItem: function(arr) {
|
|
2022
|
+
return arr.filter(function(el) {
|
|
2023
|
+
return el != null && el != "";
|
|
2024
|
+
});
|
|
2025
|
+
},
|
|
2026
|
+
getFieldName: function(field, prefix, isRoot, isArrayItem, isArray) {
|
|
2027
|
+
if (isArray) return (prefix ? prefix : "") + (dotize.isNumber(field) ? "[" + field + "]" : (isRoot && !prefix ? "" : ".") + field);
|
|
2028
|
+
else if (isArrayItem) return (prefix ? prefix : "") + "[" + field + "]";
|
|
2029
|
+
else return (prefix ? prefix + "." : "") + field;
|
|
2030
|
+
},
|
|
2031
|
+
startsWith: function(val, valToSearch) {
|
|
2032
|
+
return val.indexOf(valToSearch) == 0;
|
|
2033
|
+
},
|
|
2034
|
+
convert: function(obj, prefix = "") {
|
|
2035
|
+
let newObj = {};
|
|
2036
|
+
if (dotize.isNotObject(obj)) if (prefix) {
|
|
2037
|
+
newObj[prefix] = obj;
|
|
2038
|
+
return newObj;
|
|
2039
|
+
} else return obj;
|
|
2040
|
+
return (function recurse(o, p, isRoot) {
|
|
2041
|
+
const isArrayItem = Array.isArray(o);
|
|
2042
|
+
for (const f in o) {
|
|
2043
|
+
const currentProp = o[f];
|
|
2044
|
+
if (currentProp && typeof currentProp === "object" && !Array.isArray(currentProp) && dotize.isPlainObject(currentProp)) {
|
|
2045
|
+
if (isArrayItem && dotize.isEmptyObj(currentProp) == false) newObj = recurse(currentProp, dotize.getFieldName(f, p, isRoot, true));
|
|
2046
|
+
else if (dotize.isEmptyObj(currentProp) == false) newObj = recurse(currentProp, dotize.getFieldName(f, p, isRoot));
|
|
2047
|
+
else if (dotize.isEmptyObj(currentProp)) newObj[dotize.getFieldName(f, p, isRoot, isArrayItem)] = currentProp;
|
|
2048
|
+
} else if (isArrayItem || dotize.isNumber(f)) newObj[dotize.getFieldName(f, p, isRoot, true)] = currentProp;
|
|
2049
|
+
else newObj[dotize.getFieldName(f, p, isRoot)] = currentProp;
|
|
2050
|
+
}
|
|
2051
|
+
return newObj;
|
|
2052
|
+
})(obj, prefix, true);
|
|
2053
|
+
},
|
|
2054
|
+
backward: function(obj, prefix) {
|
|
2055
|
+
let newObj = {};
|
|
2056
|
+
const arStartRegex = /\[(\d+)\]/g;
|
|
2057
|
+
if (dotize.isNotObject(obj) && dotize.isNotArray(obj)) if (prefix) return obj[prefix];
|
|
2058
|
+
else return obj;
|
|
2059
|
+
for (let tProp in obj) {
|
|
2060
|
+
const tPropVal = obj[tProp];
|
|
2061
|
+
if (prefix) {
|
|
2062
|
+
const prefixRegex = new RegExp("^" + prefix);
|
|
2063
|
+
tProp = tProp.replace(prefixRegex, "");
|
|
2064
|
+
}
|
|
2065
|
+
tProp = tProp.replace(arStartRegex, ".$1");
|
|
2066
|
+
if (dotize.startsWith(tProp, ".")) tProp = tProp.replace(/^\./, "");
|
|
2067
|
+
const arrPath = tProp.split(".");
|
|
2068
|
+
const arrPathTypes = dotize.getPathType(arrPath);
|
|
2069
|
+
if (!dotize.isUndefined(arrPathTypes) && arrPathTypes[0] == dotize.valTypes.array && Array.isArray(newObj) == false) newObj = [];
|
|
2070
|
+
(function recurse(rPropVal, rObj, rPropValPrev, rObjPrev) {
|
|
2071
|
+
let currentPath = arrPath.shift();
|
|
2072
|
+
const currentPathType = arrPathTypes.shift();
|
|
2073
|
+
if (typeof currentPath == "undefined" || currentPath == "") {
|
|
2074
|
+
newObj = rPropVal;
|
|
2075
|
+
return;
|
|
2076
|
+
}
|
|
2077
|
+
const isArray = currentPathType == dotize.valTypes.array;
|
|
2078
|
+
if (dotize.isNumber(currentPath)) currentPath = parseInt(currentPath);
|
|
2079
|
+
if (arrPath.length > 0) {
|
|
2080
|
+
if (typeof rObj[currentPath] == "undefined") if (isArray) rObj[currentPath] = [];
|
|
2081
|
+
else rObj[currentPath] = {};
|
|
2082
|
+
recurse(rPropVal, rObj[currentPath], currentPath, rObj);
|
|
2083
|
+
return;
|
|
2084
|
+
}
|
|
2085
|
+
if (currentPathType == dotize.valTypes.array && rPropValPrev && rObjPrev) {
|
|
2086
|
+
if (Array.isArray(rObjPrev[rPropValPrev]) == false) rObjPrev[rPropValPrev] = [];
|
|
2087
|
+
rObjPrev[rPropValPrev].push(rPropVal);
|
|
2088
|
+
} else rObj[currentPath] = rPropVal;
|
|
2089
|
+
})(tPropVal, newObj);
|
|
2090
|
+
}
|
|
2091
|
+
return newObj;
|
|
2092
|
+
}
|
|
2093
|
+
};
|
|
2094
|
+
//#endregion
|
|
2095
|
+
export { hslToRgbValues as A, createColorFunc as C, getNamedColorHex as D, makeEmptyDetails as E, RE_NUMBER as F, RE_RAW_UNIT as I, normalizeDslName as M, toSnakeCase as N, getRgbValuesFromRgbaString as O, CSS_WIDE_KEYWORDS as P, okhslPlugin as S, Lru as T, setGlobalPredefinedTokens as _, DIRECTIONS as a, okhstPlugin as b, getGlobalParseFunctions as c, normalizeColorTokenValue as d, parseColor as f, resolveCustomProperties as g, resetGlobalPredefinedTokens as h, CUSTOM_UNITS as i, strToRgb as j, hexToRgb as k, getGlobalParser as l, resetGlobalParseFunctions as m, color as n, customFunction as o, parseStyle as p, resolveFunctionColor as r, filterMods as s, dotize as t, getGlobalPredefinedTokens as u, stringifyStyles as v, StyleParser as w, okhslFunction as x, okhstFunction as y };
|
|
2096
|
+
|
|
2097
|
+
//# sourceMappingURL=dsl-CXvoaLnj.js.map
|