gradiente 1.0.2 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +58 -122
- package/dist/index.d.mts +363 -291
- package/dist/index.mjs +1534 -1284
- package/package.json +8 -12
package/dist/index.mjs
CHANGED
|
@@ -1,476 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
const TokenKind = {
|
|
3
|
-
PAREN_OPEN: "paren-open",
|
|
4
|
-
PAREN_CLOSE: "paren-close",
|
|
5
|
-
COMMA: "comma",
|
|
6
|
-
SLASH: "slash",
|
|
7
|
-
FUNCTION_LINEAR_GRADIENT: "function-linear-gradient",
|
|
8
|
-
FUNCTION_REPEATING_LINEAR_GRADIENT: "function-repeating-linear-gradient",
|
|
9
|
-
FUNCTION_RADIAL_GRADIENT: "function-radial-gradient",
|
|
10
|
-
FUNCTION_REPEATING_RADIAL_GRADIENT: "function-repeating-radial-gradient",
|
|
11
|
-
FUNCTION_CONIC_GRADIENT: "function-conic-gradient",
|
|
12
|
-
FUNCTION_REPEATING_CONIC_GRADIENT: "function-repeating-conic-gradient",
|
|
13
|
-
FUNCTION_DIAMOND_GRADIENT: "function-diamond-gradient",
|
|
14
|
-
FUNCTION_REPEATING_DIAMOND_GRADIENT: "function-repeating-diamond-gradient",
|
|
15
|
-
FUNCTION_MESH_GRADIENT: "function-mesh-gradient",
|
|
16
|
-
TO: "to",
|
|
17
|
-
TOP: "top",
|
|
18
|
-
BOTTOM: "bottom",
|
|
19
|
-
LEFT: "left",
|
|
20
|
-
RIGHT: "right",
|
|
21
|
-
AT: "at",
|
|
22
|
-
FROM: "from",
|
|
23
|
-
CENTER: "center",
|
|
24
|
-
CIRCLE: "circle",
|
|
25
|
-
ELLIPSE: "ellipse",
|
|
26
|
-
CLOSEST_SIDE: "closest-side",
|
|
27
|
-
CLOSEST_CORNER: "closest-corner",
|
|
28
|
-
FARTHEST_SIDE: "farthest-side",
|
|
29
|
-
FARTHEST_CORNER: "farthest-corner",
|
|
30
|
-
IN: "in",
|
|
31
|
-
SHORTER: "shorter",
|
|
32
|
-
LONGER: "longer",
|
|
33
|
-
INCREASING: "increasing",
|
|
34
|
-
DECREASING: "decreasing",
|
|
35
|
-
HUE: "hue",
|
|
36
|
-
IDENT: "ident",
|
|
37
|
-
NUMBER: "number",
|
|
38
|
-
PERCENTAGE: "percentage",
|
|
39
|
-
DIMENSION: "dimension",
|
|
40
|
-
FUNCTION: "function",
|
|
41
|
-
HASH: "hash",
|
|
42
|
-
STRING: "string",
|
|
43
|
-
WHITESPACE: "whitespace",
|
|
44
|
-
EOF: "eof",
|
|
45
|
-
UNKNOWN: "unknown"
|
|
46
|
-
};
|
|
47
|
-
const GradientFunctionNameToToken = {
|
|
48
|
-
"linear-gradient": TokenKind.FUNCTION_LINEAR_GRADIENT,
|
|
49
|
-
"repeating-linear-gradient": TokenKind.FUNCTION_REPEATING_LINEAR_GRADIENT,
|
|
50
|
-
"radial-gradient": TokenKind.FUNCTION_RADIAL_GRADIENT,
|
|
51
|
-
"repeating-radial-gradient": TokenKind.FUNCTION_REPEATING_RADIAL_GRADIENT,
|
|
52
|
-
"conic-gradient": TokenKind.FUNCTION_CONIC_GRADIENT,
|
|
53
|
-
"repeating-conic-gradient": TokenKind.FUNCTION_REPEATING_CONIC_GRADIENT,
|
|
54
|
-
"diamond-gradient": TokenKind.FUNCTION_DIAMOND_GRADIENT,
|
|
55
|
-
"repeating-diamond-gradient": TokenKind.FUNCTION_REPEATING_DIAMOND_GRADIENT,
|
|
56
|
-
"mesh-gradient": TokenKind.FUNCTION_MESH_GRADIENT
|
|
57
|
-
};
|
|
58
|
-
const KeywordNameToToken = {
|
|
59
|
-
to: TokenKind.TO,
|
|
60
|
-
top: TokenKind.TOP,
|
|
61
|
-
bottom: TokenKind.BOTTOM,
|
|
62
|
-
left: TokenKind.LEFT,
|
|
63
|
-
right: TokenKind.RIGHT,
|
|
64
|
-
at: TokenKind.AT,
|
|
65
|
-
from: TokenKind.FROM,
|
|
66
|
-
center: TokenKind.CENTER,
|
|
67
|
-
circle: TokenKind.CIRCLE,
|
|
68
|
-
ellipse: TokenKind.ELLIPSE,
|
|
69
|
-
"closest-side": TokenKind.CLOSEST_SIDE,
|
|
70
|
-
"closest-corner": TokenKind.CLOSEST_CORNER,
|
|
71
|
-
"farthest-side": TokenKind.FARTHEST_SIDE,
|
|
72
|
-
"farthest-corner": TokenKind.FARTHEST_CORNER,
|
|
73
|
-
in: TokenKind.IN,
|
|
74
|
-
shorter: TokenKind.SHORTER,
|
|
75
|
-
longer: TokenKind.LONGER,
|
|
76
|
-
increasing: TokenKind.INCREASING,
|
|
77
|
-
decreasing: TokenKind.DECREASING,
|
|
78
|
-
hue: TokenKind.HUE
|
|
79
|
-
};
|
|
80
|
-
//#endregion
|
|
81
|
-
//#region src/lexer/base.ts
|
|
82
|
-
function createLexerState(source) {
|
|
83
|
-
return {
|
|
84
|
-
source,
|
|
85
|
-
length: source.length,
|
|
86
|
-
position: 0
|
|
87
|
-
};
|
|
88
|
-
}
|
|
89
|
-
function isEnd(state) {
|
|
90
|
-
return state.position >= state.length;
|
|
91
|
-
}
|
|
92
|
-
function currentChar(state) {
|
|
93
|
-
if (isEnd(state)) return null;
|
|
94
|
-
return state.source[state.position] ?? null;
|
|
95
|
-
}
|
|
96
|
-
function peekChar(state, offset = 1) {
|
|
97
|
-
const index = state.position + offset;
|
|
98
|
-
if (index < 0 || index >= state.length) return null;
|
|
99
|
-
return state.source[index] ?? null;
|
|
100
|
-
}
|
|
101
|
-
function advance(state, step = 1) {
|
|
102
|
-
state.position = Math.max(0, Math.min(state.position + step, state.length));
|
|
103
|
-
}
|
|
104
|
-
function isWhitespaceChar(char) {
|
|
105
|
-
return char === " " || char === "\n" || char === "\r" || char === " " || char === "\f";
|
|
106
|
-
}
|
|
107
|
-
function isDigitChar(char) {
|
|
108
|
-
return char !== null && char >= "0" && char <= "9";
|
|
109
|
-
}
|
|
110
|
-
function isSignChar(char) {
|
|
111
|
-
return char === "+" || char === "-";
|
|
112
|
-
}
|
|
113
|
-
function isIdentifierStartChar(char) {
|
|
114
|
-
if (char === null) return false;
|
|
115
|
-
return char >= "a" && char <= "z" || char >= "A" && char <= "Z" || char === "_" || char === "-";
|
|
116
|
-
}
|
|
117
|
-
function isIdentifierChar(char) {
|
|
118
|
-
if (char === null) return false;
|
|
119
|
-
return isIdentifierStartChar(char) || isDigitChar(char);
|
|
120
|
-
}
|
|
121
|
-
function isAlphaChar(char) {
|
|
122
|
-
if (char === null) return false;
|
|
123
|
-
return char >= "a" && char <= "z" || char >= "A" && char <= "Z";
|
|
124
|
-
}
|
|
125
|
-
function readWhile(state, predicate) {
|
|
126
|
-
const start = state.position;
|
|
127
|
-
while (!isEnd(state) && predicate(currentChar(state))) advance(state);
|
|
128
|
-
return state.source.slice(start, state.position);
|
|
129
|
-
}
|
|
130
|
-
function createSpan(source, start, end) {
|
|
131
|
-
return {
|
|
132
|
-
start,
|
|
133
|
-
end,
|
|
134
|
-
raw: source.slice(start, end)
|
|
135
|
-
};
|
|
136
|
-
}
|
|
137
|
-
//#endregion
|
|
138
|
-
//#region src/lexer/lexer.ts
|
|
139
|
-
function readWhitespaceToken(state) {
|
|
140
|
-
const start = state.position;
|
|
141
|
-
readWhile(state, isWhitespaceChar);
|
|
142
|
-
const span = createSpan(state.source, start, state.position);
|
|
143
|
-
return {
|
|
144
|
-
kind: TokenKind.WHITESPACE,
|
|
145
|
-
start: span.start,
|
|
146
|
-
end: span.end,
|
|
147
|
-
raw: span.raw
|
|
148
|
-
};
|
|
149
|
-
}
|
|
150
|
-
function readPunctuationToken(state) {
|
|
151
|
-
const start = state.position;
|
|
152
|
-
const char = currentChar(state);
|
|
153
|
-
if (char === null) return null;
|
|
154
|
-
let kind = null;
|
|
155
|
-
switch (char) {
|
|
156
|
-
case "(":
|
|
157
|
-
kind = TokenKind.PAREN_OPEN;
|
|
158
|
-
break;
|
|
159
|
-
case ")":
|
|
160
|
-
kind = TokenKind.PAREN_CLOSE;
|
|
161
|
-
break;
|
|
162
|
-
case ",":
|
|
163
|
-
kind = TokenKind.COMMA;
|
|
164
|
-
break;
|
|
165
|
-
case "/":
|
|
166
|
-
kind = TokenKind.SLASH;
|
|
167
|
-
break;
|
|
168
|
-
default:
|
|
169
|
-
kind = null;
|
|
170
|
-
break;
|
|
171
|
-
}
|
|
172
|
-
if (kind === null) return null;
|
|
173
|
-
advance(state);
|
|
174
|
-
const span = createSpan(state.source, start, state.position);
|
|
175
|
-
return {
|
|
176
|
-
kind,
|
|
177
|
-
start: span.start,
|
|
178
|
-
end: span.end,
|
|
179
|
-
raw: span.raw
|
|
180
|
-
};
|
|
181
|
-
}
|
|
182
|
-
function readHashToken(state) {
|
|
183
|
-
const start = state.position;
|
|
184
|
-
if (currentChar(state) !== "#") return null;
|
|
185
|
-
advance(state);
|
|
186
|
-
const value = readWhile(state, (nextChar) => {
|
|
187
|
-
if (nextChar === null) return false;
|
|
188
|
-
return isIdentifierChar(nextChar);
|
|
189
|
-
});
|
|
190
|
-
const span = createSpan(state.source, start, state.position);
|
|
191
|
-
return {
|
|
192
|
-
kind: TokenKind.HASH,
|
|
193
|
-
start: span.start,
|
|
194
|
-
end: span.end,
|
|
195
|
-
raw: span.raw,
|
|
196
|
-
value
|
|
197
|
-
};
|
|
198
|
-
}
|
|
199
|
-
function readIdentifierLikeToken(state) {
|
|
200
|
-
const start = state.position;
|
|
201
|
-
if (!isIdentifierStartChar(currentChar(state))) return null;
|
|
202
|
-
const value = readWhile(state, isIdentifierChar);
|
|
203
|
-
if (currentChar(state) === "(") {
|
|
204
|
-
const gradientKind = GradientFunctionNameToToken[value];
|
|
205
|
-
const span = createSpan(state.source, start, state.position);
|
|
206
|
-
if (gradientKind !== void 0) return {
|
|
207
|
-
kind: gradientKind,
|
|
208
|
-
start: span.start,
|
|
209
|
-
end: span.end,
|
|
210
|
-
raw: span.raw,
|
|
211
|
-
name: value
|
|
212
|
-
};
|
|
213
|
-
return {
|
|
214
|
-
kind: TokenKind.FUNCTION,
|
|
215
|
-
start: span.start,
|
|
216
|
-
end: span.end,
|
|
217
|
-
raw: span.raw,
|
|
218
|
-
name: value
|
|
219
|
-
};
|
|
220
|
-
}
|
|
221
|
-
const keywordKind = KeywordNameToToken[value];
|
|
222
|
-
const span = createSpan(state.source, start, state.position);
|
|
223
|
-
if (keywordKind !== void 0) return {
|
|
224
|
-
kind: keywordKind,
|
|
225
|
-
start: span.start,
|
|
226
|
-
end: span.end,
|
|
227
|
-
raw: span.raw
|
|
228
|
-
};
|
|
229
|
-
return {
|
|
230
|
-
kind: TokenKind.IDENT,
|
|
231
|
-
start: span.start,
|
|
232
|
-
end: span.end,
|
|
233
|
-
raw: span.raw,
|
|
234
|
-
value
|
|
235
|
-
};
|
|
236
|
-
}
|
|
237
|
-
function isNumberStart(state) {
|
|
238
|
-
const char = currentChar(state);
|
|
239
|
-
const next = peekChar(state);
|
|
240
|
-
if (isDigitChar(char)) return true;
|
|
241
|
-
if (char === "." && isDigitChar(next)) return true;
|
|
242
|
-
if (isSignChar(char)) {
|
|
243
|
-
if (isDigitChar(next)) return true;
|
|
244
|
-
if (next === "." && isDigitChar(peekChar(state, 2))) return true;
|
|
245
|
-
}
|
|
246
|
-
return false;
|
|
247
|
-
}
|
|
248
|
-
function readNumberRaw(state) {
|
|
249
|
-
let sign = 1;
|
|
250
|
-
if (currentChar(state) === "+") advance(state);
|
|
251
|
-
else if (currentChar(state) === "-") {
|
|
252
|
-
sign = -1;
|
|
253
|
-
advance(state);
|
|
254
|
-
}
|
|
255
|
-
const integerPart = readWhile(state, isDigitChar);
|
|
256
|
-
let fractionPart = "";
|
|
257
|
-
if (currentChar(state) === "." && isDigitChar(peekChar(state))) {
|
|
258
|
-
advance(state);
|
|
259
|
-
fractionPart = readWhile(state, isDigitChar);
|
|
260
|
-
}
|
|
261
|
-
const rawNumber = fractionPart.length > 0 ? `${integerPart}.${fractionPart}` : integerPart;
|
|
262
|
-
const numeric = Number(rawNumber);
|
|
263
|
-
const value = sign === -1 ? -numeric : numeric;
|
|
264
|
-
return {
|
|
265
|
-
rawNumber,
|
|
266
|
-
sign,
|
|
267
|
-
value
|
|
268
|
-
};
|
|
269
|
-
}
|
|
270
|
-
function readNumberLikeToken(state) {
|
|
271
|
-
if (!isNumberStart(state)) return null;
|
|
272
|
-
const start = state.position;
|
|
273
|
-
const { sign, value } = readNumberRaw(state);
|
|
274
|
-
if (currentChar(state) === "%") {
|
|
275
|
-
advance(state);
|
|
276
|
-
const span = createSpan(state.source, start, state.position);
|
|
277
|
-
return {
|
|
278
|
-
kind: TokenKind.PERCENTAGE,
|
|
279
|
-
start: span.start,
|
|
280
|
-
end: span.end,
|
|
281
|
-
raw: span.raw,
|
|
282
|
-
value,
|
|
283
|
-
sign
|
|
284
|
-
};
|
|
285
|
-
}
|
|
286
|
-
if (isIdentifierStartChar(currentChar(state))) {
|
|
287
|
-
const unit = readWhile(state, isIdentifierChar);
|
|
288
|
-
const span = createSpan(state.source, start, state.position);
|
|
289
|
-
return {
|
|
290
|
-
kind: TokenKind.DIMENSION,
|
|
291
|
-
start: span.start,
|
|
292
|
-
end: span.end,
|
|
293
|
-
raw: span.raw,
|
|
294
|
-
value,
|
|
295
|
-
sign,
|
|
296
|
-
unit
|
|
297
|
-
};
|
|
298
|
-
}
|
|
299
|
-
const span = createSpan(state.source, start, state.position);
|
|
300
|
-
return {
|
|
301
|
-
kind: TokenKind.NUMBER,
|
|
302
|
-
start: span.start,
|
|
303
|
-
end: span.end,
|
|
304
|
-
raw: span.raw,
|
|
305
|
-
value,
|
|
306
|
-
sign
|
|
307
|
-
};
|
|
308
|
-
}
|
|
309
|
-
function readUnknownToken(state) {
|
|
310
|
-
const start = state.position;
|
|
311
|
-
advance(state);
|
|
312
|
-
const span = createSpan(state.source, start, state.position);
|
|
313
|
-
return {
|
|
314
|
-
kind: TokenKind.UNKNOWN,
|
|
315
|
-
start: span.start,
|
|
316
|
-
end: span.end,
|
|
317
|
-
raw: span.raw
|
|
318
|
-
};
|
|
319
|
-
}
|
|
320
|
-
function readEofToken(state) {
|
|
321
|
-
return {
|
|
322
|
-
kind: TokenKind.EOF,
|
|
323
|
-
start: state.position,
|
|
324
|
-
end: state.position,
|
|
325
|
-
raw: ""
|
|
326
|
-
};
|
|
327
|
-
}
|
|
328
|
-
function nextToken(state) {
|
|
329
|
-
if (isEnd(state)) return readEofToken(state);
|
|
330
|
-
if (isWhitespaceChar(currentChar(state))) return readWhitespaceToken(state);
|
|
331
|
-
const punctuationToken = readPunctuationToken(state);
|
|
332
|
-
if (punctuationToken !== null) return punctuationToken;
|
|
333
|
-
const hashToken = readHashToken(state);
|
|
334
|
-
if (hashToken !== null) return hashToken;
|
|
335
|
-
const numberLikeToken = readNumberLikeToken(state);
|
|
336
|
-
if (numberLikeToken !== null) return numberLikeToken;
|
|
337
|
-
const identifierLikeToken = readIdentifierLikeToken(state);
|
|
338
|
-
if (identifierLikeToken !== null) return identifierLikeToken;
|
|
339
|
-
return readUnknownToken(state);
|
|
340
|
-
}
|
|
341
|
-
function tokenize(source) {
|
|
342
|
-
const state = createLexerState(source);
|
|
343
|
-
const tokens = [];
|
|
344
|
-
while (true) {
|
|
345
|
-
const token = nextToken(state);
|
|
346
|
-
tokens.push(token);
|
|
347
|
-
if (token.kind === TokenKind.EOF) break;
|
|
348
|
-
}
|
|
349
|
-
return tokens;
|
|
350
|
-
}
|
|
351
|
-
//#endregion
|
|
352
|
-
//#region src/guard.ts
|
|
353
|
-
function isGradientFunctionToken(token) {
|
|
354
|
-
return token.kind === TokenKind.FUNCTION_LINEAR_GRADIENT || token.kind === TokenKind.FUNCTION_REPEATING_LINEAR_GRADIENT || token.kind === TokenKind.FUNCTION_RADIAL_GRADIENT || token.kind === TokenKind.FUNCTION_REPEATING_RADIAL_GRADIENT || token.kind === TokenKind.FUNCTION_CONIC_GRADIENT || token.kind === TokenKind.FUNCTION_REPEATING_CONIC_GRADIENT || token.kind === TokenKind.FUNCTION_DIAMOND_GRADIENT || token.kind === TokenKind.FUNCTION_REPEATING_DIAMOND_GRADIENT || token.kind === TokenKind.FUNCTION_MESH_GRADIENT;
|
|
355
|
-
}
|
|
356
|
-
function isKeywordToken(token) {
|
|
357
|
-
switch (token.kind) {
|
|
358
|
-
case TokenKind.TO:
|
|
359
|
-
case TokenKind.TOP:
|
|
360
|
-
case TokenKind.BOTTOM:
|
|
361
|
-
case TokenKind.LEFT:
|
|
362
|
-
case TokenKind.RIGHT:
|
|
363
|
-
case TokenKind.AT:
|
|
364
|
-
case TokenKind.FROM:
|
|
365
|
-
case TokenKind.CENTER:
|
|
366
|
-
case TokenKind.CIRCLE:
|
|
367
|
-
case TokenKind.ELLIPSE:
|
|
368
|
-
case TokenKind.CLOSEST_SIDE:
|
|
369
|
-
case TokenKind.CLOSEST_CORNER:
|
|
370
|
-
case TokenKind.FARTHEST_SIDE:
|
|
371
|
-
case TokenKind.FARTHEST_CORNER:
|
|
372
|
-
case TokenKind.IN:
|
|
373
|
-
case TokenKind.SHORTER:
|
|
374
|
-
case TokenKind.LONGER:
|
|
375
|
-
case TokenKind.INCREASING:
|
|
376
|
-
case TokenKind.DECREASING:
|
|
377
|
-
case TokenKind.HUE: return true;
|
|
378
|
-
default: return false;
|
|
379
|
-
}
|
|
380
|
-
}
|
|
381
|
-
function isNumericToken(token) {
|
|
382
|
-
return token.kind === TokenKind.NUMBER || token.kind === TokenKind.PERCENTAGE || token.kind === TokenKind.DIMENSION;
|
|
383
|
-
}
|
|
384
|
-
//#endregion
|
|
385
|
-
//#region src/source.ts
|
|
386
|
-
function getSourceSlice(source, start, end) {
|
|
387
|
-
return source.slice(start, end);
|
|
388
|
-
}
|
|
389
|
-
function getTokenSourceSlice(source, startToken, endToken) {
|
|
390
|
-
return source.slice(startToken.start, endToken.end);
|
|
391
|
-
}
|
|
392
|
-
function getTokenRangeSourceSlice(source, tokens, startIndex, endIndex) {
|
|
393
|
-
const startToken = tokens[startIndex];
|
|
394
|
-
const endToken = tokens[endIndex];
|
|
395
|
-
if (startToken === void 0 || endToken === void 0) throw new Error(`Invalid token range: ${startIndex}..${endIndex}`);
|
|
396
|
-
return source.slice(startToken.start, endToken.end);
|
|
397
|
-
}
|
|
398
|
-
function findBalancedFunctionEndIndex(tokens, startIndex) {
|
|
399
|
-
const startToken = tokens[startIndex];
|
|
400
|
-
if (startToken === void 0 || !(startToken.kind === TokenKind.FUNCTION || startToken.kind === TokenKind.FUNCTION_LINEAR_GRADIENT || startToken.kind === TokenKind.FUNCTION_REPEATING_LINEAR_GRADIENT || startToken.kind === TokenKind.FUNCTION_RADIAL_GRADIENT || startToken.kind === TokenKind.FUNCTION_REPEATING_RADIAL_GRADIENT || startToken.kind === TokenKind.FUNCTION_CONIC_GRADIENT || startToken.kind === TokenKind.FUNCTION_REPEATING_CONIC_GRADIENT || startToken.kind === TokenKind.FUNCTION_DIAMOND_GRADIENT || startToken.kind === TokenKind.FUNCTION_REPEATING_DIAMOND_GRADIENT || startToken.kind === TokenKind.FUNCTION_MESH_GRADIENT)) throw new Error(`Token at index ${startIndex} is not a function token`);
|
|
401
|
-
if (tokens[startIndex + 1]?.kind !== TokenKind.PAREN_OPEN) throw new Error(`Expected "(" after function token at index ${startIndex}`);
|
|
402
|
-
let depth = 0;
|
|
403
|
-
for (let index = startIndex + 1; index < tokens.length; index++) {
|
|
404
|
-
const token = tokens[index];
|
|
405
|
-
if (token === void 0) break;
|
|
406
|
-
if (token.kind === TokenKind.PAREN_OPEN) {
|
|
407
|
-
depth += 1;
|
|
408
|
-
continue;
|
|
409
|
-
}
|
|
410
|
-
if (token.kind === TokenKind.PAREN_CLOSE) {
|
|
411
|
-
depth -= 1;
|
|
412
|
-
if (depth === 0) return index;
|
|
413
|
-
continue;
|
|
414
|
-
}
|
|
415
|
-
}
|
|
416
|
-
throw new Error(`Unclosed function starting at token index ${startIndex}`);
|
|
417
|
-
}
|
|
418
|
-
//#endregion
|
|
419
|
-
//#region src/utils/parser-utils.ts
|
|
420
|
-
function isTriviaToken(token) {
|
|
421
|
-
return token.kind === TokenKind.WHITESPACE;
|
|
422
|
-
}
|
|
423
|
-
function getTokenAt(tokens, index) {
|
|
424
|
-
return tokens[index] ?? null;
|
|
425
|
-
}
|
|
426
|
-
function findNextNonWhitespaceIndex(tokens, startIndex) {
|
|
427
|
-
let index = startIndex;
|
|
428
|
-
while (index < tokens.length) {
|
|
429
|
-
const token = tokens[index];
|
|
430
|
-
if (token === void 0) break;
|
|
431
|
-
if (!isTriviaToken(token)) return index;
|
|
432
|
-
index += 1;
|
|
433
|
-
}
|
|
434
|
-
return -1;
|
|
435
|
-
}
|
|
436
|
-
function getNonWhitespaceTokenAt(tokens, startIndex) {
|
|
437
|
-
const index = findNextNonWhitespaceIndex(tokens, startIndex);
|
|
438
|
-
if (index === -1) return null;
|
|
439
|
-
return tokens[index] ?? null;
|
|
440
|
-
}
|
|
441
|
-
function skipWhitespace(tokens, startIndex) {
|
|
442
|
-
return findNextNonWhitespaceIndex(tokens, startIndex);
|
|
443
|
-
}
|
|
444
|
-
function consumeIf(tokens, index, kind) {
|
|
445
|
-
const nextIndex = findNextNonWhitespaceIndex(tokens, index);
|
|
446
|
-
if (nextIndex === -1) return {
|
|
447
|
-
matched: false,
|
|
448
|
-
nextIndex: index,
|
|
449
|
-
token: null
|
|
450
|
-
};
|
|
451
|
-
const token = tokens[nextIndex] ?? null;
|
|
452
|
-
if (token === null || token.kind !== kind) return {
|
|
453
|
-
matched: false,
|
|
454
|
-
nextIndex: index,
|
|
455
|
-
token: null
|
|
456
|
-
};
|
|
457
|
-
return {
|
|
458
|
-
matched: true,
|
|
459
|
-
nextIndex: nextIndex + 1,
|
|
460
|
-
token
|
|
461
|
-
};
|
|
462
|
-
}
|
|
463
|
-
function expectToken(tokens, index, kind, message) {
|
|
464
|
-
const nextIndex = findNextNonWhitespaceIndex(tokens, index);
|
|
465
|
-
if (nextIndex === -1) throw new Error(message ?? `Expected token "${kind}", but reached end of token stream`);
|
|
466
|
-
const token = tokens[nextIndex];
|
|
467
|
-
if (token === void 0 || token.kind !== kind) throw new Error(message ?? `Expected token "${kind}", but received "${token?.kind ?? "undefined"}"`);
|
|
468
|
-
return {
|
|
469
|
-
token,
|
|
470
|
-
nextIndex: nextIndex + 1
|
|
471
|
-
};
|
|
472
|
-
}
|
|
473
|
-
//#endregion
|
|
1
|
+
import { converter, formatRgb, parse as parse$1 } from "culori";
|
|
474
2
|
//#region src/utils/math/base.ts
|
|
475
3
|
function roundTo(value, digits) {
|
|
476
4
|
const factor = 10 ** digits;
|
|
@@ -533,898 +61,1620 @@ function toAngleRad(value, unit) {
|
|
|
533
61
|
function normalizeAngle(value, unit, digits = 6) {
|
|
534
62
|
return roundTo(normalizeAngleRad(toAngleRad(value, unit)), digits);
|
|
535
63
|
}
|
|
536
|
-
function parseAngleFromToken(token) {
|
|
537
|
-
if (token.kind !== TokenKind.DIMENSION) return null;
|
|
538
|
-
if (!isAngleUnit(token.unit)) return null;
|
|
539
|
-
return normalizeAngle(token.value, token.unit);
|
|
540
|
-
}
|
|
541
64
|
//#endregion
|
|
542
|
-
//#region src/
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
65
|
+
//#region src/dsl/types.ts
|
|
66
|
+
let PatternTokenKind = /* @__PURE__ */ function(PatternTokenKind) {
|
|
67
|
+
PatternTokenKind["START"] = "^";
|
|
68
|
+
PatternTokenKind["END"] = ".";
|
|
69
|
+
PatternTokenKind["GROUP_OPEN"] = "(";
|
|
70
|
+
PatternTokenKind["GROUP_CLOSE"] = ")";
|
|
71
|
+
PatternTokenKind["COMMA"] = ",";
|
|
72
|
+
PatternTokenKind["SEQUENCE_OPEN"] = "[";
|
|
73
|
+
PatternTokenKind["SEQUENCE_CLOSE"] = "]";
|
|
74
|
+
PatternTokenKind["OR"] = "|";
|
|
75
|
+
PatternTokenKind["AND"] = "&";
|
|
76
|
+
PatternTokenKind["NOT"] = "!";
|
|
77
|
+
PatternTokenKind["REPEAT"] = "~";
|
|
78
|
+
PatternTokenKind["CONFIG"] = "config";
|
|
79
|
+
PatternTokenKind["COLOR_STOP"] = "color-stop";
|
|
80
|
+
PatternTokenKind["COLOR_HINT"] = "color-hint";
|
|
81
|
+
return PatternTokenKind;
|
|
82
|
+
}({});
|
|
83
|
+
//#endregion
|
|
84
|
+
//#region src/dsl/match.ts
|
|
85
|
+
function matchExpression(classified, patternTokens, inputIndex, patternIndex) {
|
|
86
|
+
let currentInputIndex = inputIndex;
|
|
87
|
+
let currentPatternIndex = patternIndex;
|
|
88
|
+
while (currentPatternIndex < patternTokens.length) {
|
|
89
|
+
const token = patternTokens[currentPatternIndex];
|
|
90
|
+
if (token === "." || token === ")" || token === "]" || token === "|") break;
|
|
91
|
+
if (token === ",") {
|
|
92
|
+
currentPatternIndex += 1;
|
|
556
93
|
continue;
|
|
557
94
|
}
|
|
558
|
-
|
|
95
|
+
const result = matchPrimary(classified, patternTokens, currentInputIndex, currentPatternIndex);
|
|
96
|
+
if (!result.matched) return {
|
|
97
|
+
matched: false,
|
|
98
|
+
nextInputIndex: inputIndex,
|
|
99
|
+
nextPatternIndex: patternIndex
|
|
100
|
+
};
|
|
101
|
+
currentInputIndex = result.nextInputIndex;
|
|
102
|
+
currentPatternIndex = result.nextPatternIndex;
|
|
559
103
|
}
|
|
560
104
|
return {
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
position: positions[0]
|
|
565
|
-
},
|
|
566
|
-
nextIndex: index
|
|
105
|
+
matched: true,
|
|
106
|
+
nextInputIndex: currentInputIndex,
|
|
107
|
+
nextPatternIndex: currentPatternIndex
|
|
567
108
|
};
|
|
568
109
|
}
|
|
569
|
-
function
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
110
|
+
function matchEntity(classified, patternTokens, inputIndex, patternIndex) {
|
|
111
|
+
const expected = patternTokens[patternIndex];
|
|
112
|
+
const current = classified[inputIndex];
|
|
113
|
+
if (expected !== "config" && expected !== "color-stop" && expected !== "color-hint") throw new Error(`Expected entity token at pattern index ${patternIndex}`);
|
|
114
|
+
if (!current) return {
|
|
115
|
+
matched: false,
|
|
116
|
+
nextInputIndex: inputIndex,
|
|
117
|
+
nextPatternIndex: patternIndex
|
|
118
|
+
};
|
|
119
|
+
if (current.type !== expected) return {
|
|
120
|
+
matched: false,
|
|
121
|
+
nextInputIndex: inputIndex,
|
|
122
|
+
nextPatternIndex: patternIndex
|
|
573
123
|
};
|
|
574
124
|
return {
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
125
|
+
matched: true,
|
|
126
|
+
nextInputIndex: inputIndex + 1,
|
|
127
|
+
nextPatternIndex: patternIndex + 1
|
|
578
128
|
};
|
|
579
129
|
}
|
|
580
|
-
function
|
|
581
|
-
const token =
|
|
582
|
-
if (token ===
|
|
583
|
-
if (token
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
};
|
|
587
|
-
if (token.kind === TokenKind.FUNCTION) {
|
|
588
|
-
const endIndex = findBalancedFunctionEndIndex$1(tokens, startIndex);
|
|
589
|
-
return {
|
|
590
|
-
node: getTokenRangeSourceSlice(source, tokens, startIndex, endIndex),
|
|
591
|
-
nextIndex: endIndex + 1
|
|
592
|
-
};
|
|
593
|
-
}
|
|
594
|
-
throw new Error(`Expected color source, received "${token.kind}"`);
|
|
130
|
+
function matchPrimary(classified, patternTokens, inputIndex, patternIndex) {
|
|
131
|
+
const token = patternTokens[patternIndex];
|
|
132
|
+
if (token === "config" || token === "color-stop" || token === "color-hint") return matchEntity(classified, patternTokens, inputIndex, patternIndex);
|
|
133
|
+
if (token === "[") return matchSequence(classified, patternTokens, inputIndex, patternIndex);
|
|
134
|
+
if (token === "(") return matchGroup(classified, patternTokens, inputIndex, patternIndex);
|
|
135
|
+
if (token === "~") return matchRepeat(classified, patternTokens, inputIndex, patternIndex);
|
|
136
|
+
throw new Error(`Unsupported primary token "${token}" at pattern index ${patternIndex}`);
|
|
595
137
|
}
|
|
596
|
-
function
|
|
597
|
-
|
|
598
|
-
const openParenToken = tokens[startIndex + 1];
|
|
599
|
-
if (functionToken?.kind !== TokenKind.FUNCTION) throw new Error("Expected generic function token");
|
|
600
|
-
if (openParenToken?.kind !== TokenKind.PAREN_OPEN) throw new Error("Expected \"(\" after function token");
|
|
138
|
+
function findMatchingToken(tokens, startIndex, openToken, closeToken) {
|
|
139
|
+
if (tokens[startIndex] !== openToken) throw new Error(`Expected "${openToken}" at pattern index ${startIndex}, got "${tokens[startIndex]}"`);
|
|
601
140
|
let depth = 0;
|
|
602
|
-
for (let
|
|
603
|
-
const token = tokens[
|
|
604
|
-
if (token ===
|
|
605
|
-
if (token.kind === TokenKind.PAREN_OPEN) {
|
|
141
|
+
for (let i = startIndex; i < tokens.length; i += 1) {
|
|
142
|
+
const token = tokens[i];
|
|
143
|
+
if (token === openToken) {
|
|
606
144
|
depth += 1;
|
|
607
145
|
continue;
|
|
608
146
|
}
|
|
609
|
-
if (token
|
|
147
|
+
if (token === closeToken) {
|
|
610
148
|
depth -= 1;
|
|
611
|
-
if (depth === 0) return
|
|
149
|
+
if (depth === 0) return i;
|
|
612
150
|
}
|
|
613
151
|
}
|
|
614
|
-
throw new Error(
|
|
615
|
-
}
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
let
|
|
621
|
-
while (
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
index = itemResult.nextIndex;
|
|
625
|
-
const nextIndex = findNextNonWhitespaceIndex(tokens, index);
|
|
626
|
-
if (nextIndex === -1) break;
|
|
627
|
-
const token = tokens[nextIndex];
|
|
628
|
-
if (token === void 0) break;
|
|
629
|
-
if (token.kind === TokenKind.COMMA) {
|
|
630
|
-
index = nextIndex + 1;
|
|
152
|
+
throw new Error(`Unclosed token pair "${openToken}${closeToken}"`);
|
|
153
|
+
}
|
|
154
|
+
function matchSequence(classified, patternTokens, inputIndex, patternIndex) {
|
|
155
|
+
if (patternTokens[patternIndex] !== "[") throw new Error(`Expected "[" at pattern index ${patternIndex}`);
|
|
156
|
+
const closeIndex = findMatchingToken(patternTokens, patternIndex, "[", "]");
|
|
157
|
+
let currentInputIndex = inputIndex;
|
|
158
|
+
let currentPatternIndex = patternIndex + 1;
|
|
159
|
+
while (currentPatternIndex < closeIndex) {
|
|
160
|
+
if (patternTokens[currentPatternIndex] === ",") {
|
|
161
|
+
currentPatternIndex += 1;
|
|
631
162
|
continue;
|
|
632
163
|
}
|
|
633
|
-
|
|
634
|
-
|
|
164
|
+
const result = matchPrimary(classified, patternTokens, currentInputIndex, currentPatternIndex);
|
|
165
|
+
if (!result.matched) return {
|
|
166
|
+
matched: false,
|
|
167
|
+
nextInputIndex: inputIndex,
|
|
168
|
+
nextPatternIndex: patternIndex
|
|
169
|
+
};
|
|
170
|
+
currentInputIndex = result.nextInputIndex;
|
|
171
|
+
currentPatternIndex = result.nextPatternIndex;
|
|
635
172
|
}
|
|
636
|
-
if (items.length < 2) throw new Error("Linear gradient requires at least two stop items");
|
|
637
173
|
return {
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
}
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
const item = normalized[itemIndex];
|
|
654
|
-
if (item.kind !== "color-stop") throw new Error("Expected color-stop");
|
|
655
|
-
return item;
|
|
656
|
-
};
|
|
657
|
-
const setColorStopPosition = (itemIndex, value) => {
|
|
658
|
-
normalized[itemIndex] = {
|
|
659
|
-
...getColorStop(itemIndex),
|
|
660
|
-
position: {
|
|
661
|
-
kind: "percentage",
|
|
662
|
-
value
|
|
663
|
-
}
|
|
664
|
-
};
|
|
665
|
-
};
|
|
666
|
-
const getColorStopPosition = (itemIndex) => {
|
|
667
|
-
const position = getColorStop(itemIndex).position;
|
|
668
|
-
if (!position) return null;
|
|
669
|
-
if (position.kind !== "percentage") return null;
|
|
670
|
-
return position.value;
|
|
671
|
-
};
|
|
672
|
-
const firstItemIndex = colorStopIndexes[0];
|
|
673
|
-
const lastItemIndex = colorStopIndexes[colorStopIndexes.length - 1];
|
|
674
|
-
if (firstItemIndex !== void 0 && getColorStopPosition(firstItemIndex) === null) setColorStopPosition(firstItemIndex, 0);
|
|
675
|
-
if (lastItemIndex !== void 0 && getColorStopPosition(lastItemIndex) === null) setColorStopPosition(lastItemIndex, 1);
|
|
676
|
-
let anchorStart = 0;
|
|
677
|
-
while (anchorStart < colorStopIndexes.length) {
|
|
678
|
-
const startItemIndex = colorStopIndexes[anchorStart];
|
|
679
|
-
if (startItemIndex === void 0) break;
|
|
680
|
-
const startValue = getColorStopPosition(startItemIndex);
|
|
681
|
-
if (startValue === null) {
|
|
682
|
-
anchorStart += 1;
|
|
174
|
+
matched: true,
|
|
175
|
+
nextInputIndex: currentInputIndex,
|
|
176
|
+
nextPatternIndex: closeIndex + 1
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
function splitTopLevelOr(tokens) {
|
|
180
|
+
const result = [];
|
|
181
|
+
let current = [];
|
|
182
|
+
let groupDepth = 0;
|
|
183
|
+
let sequenceDepth = 0;
|
|
184
|
+
for (let i = 0; i < tokens.length; i += 1) {
|
|
185
|
+
const token = tokens[i];
|
|
186
|
+
if (token === "(") {
|
|
187
|
+
groupDepth += 1;
|
|
188
|
+
current.push(token);
|
|
683
189
|
continue;
|
|
684
190
|
}
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
for (let i = 1; i <= gapCount; i += 1) {
|
|
695
|
-
const gapItemIndex = colorStopIndexes[anchorStart + i];
|
|
696
|
-
if (gapItemIndex === void 0) continue;
|
|
697
|
-
setColorStopPosition(gapItemIndex, startValue + step * i);
|
|
698
|
-
}
|
|
699
|
-
}
|
|
700
|
-
break;
|
|
701
|
-
}
|
|
702
|
-
anchorEnd += 1;
|
|
191
|
+
if (token === ")") {
|
|
192
|
+
groupDepth -= 1;
|
|
193
|
+
current.push(token);
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
if (token === "[") {
|
|
197
|
+
sequenceDepth += 1;
|
|
198
|
+
current.push(token);
|
|
199
|
+
continue;
|
|
703
200
|
}
|
|
704
|
-
|
|
201
|
+
if (token === "]") {
|
|
202
|
+
sequenceDepth -= 1;
|
|
203
|
+
current.push(token);
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
if (token === "|" && groupDepth === 0 && sequenceDepth === 0) {
|
|
207
|
+
result.push(current);
|
|
208
|
+
current = [];
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
211
|
+
current.push(token);
|
|
705
212
|
}
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
const
|
|
712
|
-
|
|
713
|
-
const
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
if (directionResult.node !== null) index = expectToken(tokens, index, TokenKind.COMMA).nextIndex;
|
|
720
|
-
const stopsResult = parseGradientStopList(source, tokens, index);
|
|
721
|
-
const stops = repeat === "repeating" ? expandColorStops$2(stopsResult.node) : stopsResult.node;
|
|
722
|
-
index = stopsResult.nextIndex;
|
|
723
|
-
index = expectToken(tokens, index, TokenKind.PAREN_CLOSE).nextIndex;
|
|
724
|
-
return {
|
|
725
|
-
node: {
|
|
726
|
-
kind: "linear",
|
|
727
|
-
repeat,
|
|
728
|
-
direction,
|
|
729
|
-
stops
|
|
730
|
-
},
|
|
731
|
-
nextIndex: index
|
|
732
|
-
};
|
|
733
|
-
}
|
|
734
|
-
function parseLinearDirection(tokens, startIndex) {
|
|
735
|
-
const index = findNextNonWhitespaceIndex(tokens, startIndex);
|
|
736
|
-
if (index === -1) return {
|
|
737
|
-
node: null,
|
|
738
|
-
nextIndex: startIndex
|
|
739
|
-
};
|
|
740
|
-
const token = tokens[index];
|
|
741
|
-
if (token === void 0) return {
|
|
742
|
-
node: null,
|
|
743
|
-
nextIndex: startIndex
|
|
744
|
-
};
|
|
745
|
-
if (token.kind === TokenKind.TO) return parseLinearDirectionFromKeywords(tokens, index);
|
|
746
|
-
const angle = parseAngleFromToken(token);
|
|
747
|
-
if (angle !== null) {
|
|
748
|
-
TokenKind.DIMENSION;
|
|
749
|
-
const dimensionToken = token;
|
|
750
|
-
return {
|
|
751
|
-
node: createLinearDirectionFromAngle(dimensionToken.value, dimensionToken.unit, angle),
|
|
752
|
-
nextIndex: index + 1
|
|
213
|
+
if (current.length > 0) result.push(current);
|
|
214
|
+
return result;
|
|
215
|
+
}
|
|
216
|
+
function matchGroup(classified, patternTokens, inputIndex, patternIndex) {
|
|
217
|
+
if (patternTokens[patternIndex] !== "(") throw new Error(`Expected "(" at pattern index ${patternIndex}`);
|
|
218
|
+
const closeIndex = findMatchingToken(patternTokens, patternIndex, "(", ")");
|
|
219
|
+
const branches = splitTopLevelOr(patternTokens.slice(patternIndex + 1, closeIndex));
|
|
220
|
+
for (const branch of branches) {
|
|
221
|
+
const result = matchExpression(classified, branch, inputIndex, 0);
|
|
222
|
+
if (result.matched) return {
|
|
223
|
+
matched: true,
|
|
224
|
+
nextInputIndex: result.nextInputIndex,
|
|
225
|
+
nextPatternIndex: closeIndex + 1
|
|
753
226
|
};
|
|
754
227
|
}
|
|
755
228
|
return {
|
|
756
|
-
|
|
757
|
-
|
|
229
|
+
matched: false,
|
|
230
|
+
nextInputIndex: inputIndex,
|
|
231
|
+
nextPatternIndex: patternIndex
|
|
758
232
|
};
|
|
759
233
|
}
|
|
760
|
-
function
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
234
|
+
function matchRepeat(classified, patternTokens, inputIndex, patternIndex) {
|
|
235
|
+
if (patternTokens[patternIndex] !== "~") throw new Error(`Expected "~" at pattern index ${patternIndex}`);
|
|
236
|
+
let currentInputIndex = inputIndex;
|
|
237
|
+
let currentPatternIndex = patternIndex + 1;
|
|
764
238
|
while (true) {
|
|
765
|
-
const
|
|
766
|
-
if (
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
if (token.kind === TokenKind.TOP || token.kind === TokenKind.BOTTOM || token.kind === TokenKind.LEFT || token.kind === TokenKind.RIGHT) {
|
|
770
|
-
keywords.push(token.kind);
|
|
771
|
-
index = nextIndex + 1;
|
|
772
|
-
continue;
|
|
773
|
-
}
|
|
774
|
-
break;
|
|
239
|
+
const result = matchPrimary(classified, patternTokens, currentInputIndex, currentPatternIndex);
|
|
240
|
+
if (!result.matched) break;
|
|
241
|
+
if (result.nextInputIndex === currentInputIndex) throw new Error("Repeat expression did not consume input");
|
|
242
|
+
currentInputIndex = result.nextInputIndex;
|
|
775
243
|
}
|
|
776
|
-
|
|
244
|
+
const nextPatternIndex = getPrimaryEndIndex(patternTokens, currentPatternIndex);
|
|
777
245
|
return {
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
}
|
|
782
|
-
function createLinearDirectionFromKeywords(keywords) {
|
|
783
|
-
const deg = parseKeywordsToDeg(keywords);
|
|
784
|
-
if (deg === null) throw new Error("Invalid direction keywords");
|
|
785
|
-
const rad = roundTo(degToRad(deg), 6);
|
|
786
|
-
return {
|
|
787
|
-
kind: "angle",
|
|
788
|
-
value: {
|
|
789
|
-
kind: "dimension",
|
|
790
|
-
value: deg,
|
|
791
|
-
unit: "deg"
|
|
792
|
-
},
|
|
793
|
-
valueRaw: {
|
|
794
|
-
kind: "dimension",
|
|
795
|
-
value: rad,
|
|
796
|
-
unit: "rad"
|
|
797
|
-
},
|
|
798
|
-
keywords: [...keywords]
|
|
799
|
-
};
|
|
800
|
-
}
|
|
801
|
-
function createLinearDirectionFromAngle(value, unit, normalizedRad) {
|
|
802
|
-
const fixedRad = roundTo(normalizedRad, 6);
|
|
803
|
-
return {
|
|
804
|
-
kind: "angle",
|
|
805
|
-
value: {
|
|
806
|
-
kind: "dimension",
|
|
807
|
-
value,
|
|
808
|
-
unit
|
|
809
|
-
},
|
|
810
|
-
valueRaw: {
|
|
811
|
-
kind: "dimension",
|
|
812
|
-
value: fixedRad,
|
|
813
|
-
unit: "rad"
|
|
814
|
-
},
|
|
815
|
-
keywords: parseRadToKeywords(fixedRad)
|
|
246
|
+
matched: true,
|
|
247
|
+
nextInputIndex: currentInputIndex,
|
|
248
|
+
nextPatternIndex
|
|
816
249
|
};
|
|
817
250
|
}
|
|
818
|
-
function
|
|
819
|
-
|
|
251
|
+
function getPrimaryEndIndex(patternTokens, patternIndex) {
|
|
252
|
+
const token = patternTokens[patternIndex];
|
|
253
|
+
if (token === "config" || token === "color-stop" || token === "color-hint") return patternIndex + 1;
|
|
254
|
+
if (token === "[") return findMatchingToken(patternTokens, patternIndex, "[", "]") + 1;
|
|
255
|
+
if (token === "(") return findMatchingToken(patternTokens, patternIndex, "(", ")") + 1;
|
|
256
|
+
if (token === "~") return getPrimaryEndIndex(patternTokens, patternIndex + 1);
|
|
257
|
+
throw new Error(`Unsupported token "${token}" in getPrimaryEndIndex`);
|
|
820
258
|
}
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
case "left-top": return 315;
|
|
836
|
-
default: return null;
|
|
259
|
+
//#endregion
|
|
260
|
+
//#region src/dsl/pattern-validator.ts
|
|
261
|
+
function validatePattern(input) {
|
|
262
|
+
validatePatternSyntax(input);
|
|
263
|
+
validatePatternSemantic(input);
|
|
264
|
+
validatePatternStructure(input);
|
|
265
|
+
return true;
|
|
266
|
+
}
|
|
267
|
+
function isPatternValid(input) {
|
|
268
|
+
try {
|
|
269
|
+
validatePattern(input);
|
|
270
|
+
return true;
|
|
271
|
+
} catch {
|
|
272
|
+
return false;
|
|
837
273
|
}
|
|
838
274
|
}
|
|
839
|
-
function
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
"
|
|
864
|
-
|
|
865
|
-
|
|
275
|
+
function validatePatternSyntax(input) {
|
|
276
|
+
const tokens = tokenizePattern(input);
|
|
277
|
+
if (tokens.length === 0) throw new Error("Pattern cannot be empty");
|
|
278
|
+
if (tokens[0] !== "^") throw new Error("Pattern must start with ^");
|
|
279
|
+
if (tokens[tokens.length - 1] !== ".") throw new Error("Pattern must end with \".\"");
|
|
280
|
+
let groupDepth = 0;
|
|
281
|
+
let sequenceDepth = 0;
|
|
282
|
+
for (let i = 0; i < tokens.length; i += 1) {
|
|
283
|
+
const token = tokens[i];
|
|
284
|
+
if (token === "(") {
|
|
285
|
+
groupDepth += 1;
|
|
286
|
+
continue;
|
|
287
|
+
}
|
|
288
|
+
if (token === ")") {
|
|
289
|
+
groupDepth -= 1;
|
|
290
|
+
if (groupDepth < 0) throw new Error(`Unexpected ")" at token index ${i}`);
|
|
291
|
+
continue;
|
|
292
|
+
}
|
|
293
|
+
if (token === "[") {
|
|
294
|
+
sequenceDepth += 1;
|
|
295
|
+
continue;
|
|
296
|
+
}
|
|
297
|
+
if (token === "]") {
|
|
298
|
+
sequenceDepth -= 1;
|
|
299
|
+
if (sequenceDepth < 0) throw new Error(`Unexpected "]" at token index ${i}`);
|
|
300
|
+
continue;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
if (groupDepth !== 0) throw new Error("Unclosed group \"()\" in pattern");
|
|
304
|
+
if (sequenceDepth !== 0) throw new Error("Unclosed sequence \"[]\" in pattern");
|
|
305
|
+
return true;
|
|
306
|
+
}
|
|
307
|
+
function isPatternSyntaxValid(input) {
|
|
308
|
+
try {
|
|
309
|
+
validatePatternSyntax(input);
|
|
310
|
+
return true;
|
|
311
|
+
} catch {
|
|
312
|
+
return false;
|
|
866
313
|
}
|
|
867
314
|
}
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
315
|
+
const NEXT_TOKEN_MAP = {
|
|
316
|
+
"^": [
|
|
317
|
+
"(",
|
|
318
|
+
"[",
|
|
319
|
+
"!",
|
|
320
|
+
"~",
|
|
321
|
+
"config",
|
|
322
|
+
"color-stop",
|
|
323
|
+
"color-hint"
|
|
324
|
+
],
|
|
325
|
+
"(": [
|
|
326
|
+
"(",
|
|
327
|
+
"[",
|
|
328
|
+
"!",
|
|
329
|
+
"~",
|
|
330
|
+
"config",
|
|
331
|
+
"color-stop",
|
|
332
|
+
"color-hint"
|
|
333
|
+
],
|
|
334
|
+
"[": [
|
|
335
|
+
"(",
|
|
336
|
+
"[",
|
|
337
|
+
"!",
|
|
338
|
+
"~",
|
|
339
|
+
"config",
|
|
340
|
+
"color-stop",
|
|
341
|
+
"color-hint"
|
|
342
|
+
],
|
|
343
|
+
"|": [
|
|
344
|
+
"(",
|
|
345
|
+
"[",
|
|
346
|
+
"!",
|
|
347
|
+
"~",
|
|
348
|
+
"config",
|
|
349
|
+
"color-stop",
|
|
350
|
+
"color-hint"
|
|
351
|
+
],
|
|
352
|
+
"&": [
|
|
353
|
+
"(",
|
|
354
|
+
"[",
|
|
355
|
+
"!",
|
|
356
|
+
"~",
|
|
357
|
+
"config",
|
|
358
|
+
"color-stop",
|
|
359
|
+
"color-hint"
|
|
360
|
+
],
|
|
361
|
+
"!": [
|
|
362
|
+
"(",
|
|
363
|
+
"[",
|
|
364
|
+
"!",
|
|
365
|
+
"~",
|
|
366
|
+
"config",
|
|
367
|
+
"color-stop",
|
|
368
|
+
"color-hint"
|
|
369
|
+
],
|
|
370
|
+
"~": [
|
|
371
|
+
"(",
|
|
372
|
+
"[",
|
|
373
|
+
"!",
|
|
374
|
+
"~",
|
|
375
|
+
"config",
|
|
376
|
+
"color-stop",
|
|
377
|
+
"color-hint"
|
|
378
|
+
],
|
|
379
|
+
",": [
|
|
380
|
+
"(",
|
|
381
|
+
"[",
|
|
382
|
+
"!",
|
|
383
|
+
"~",
|
|
384
|
+
"config",
|
|
385
|
+
"color-stop",
|
|
386
|
+
"color-hint"
|
|
387
|
+
],
|
|
388
|
+
"config": [
|
|
389
|
+
",",
|
|
390
|
+
"|",
|
|
391
|
+
"&",
|
|
392
|
+
")",
|
|
393
|
+
"]",
|
|
394
|
+
"."
|
|
395
|
+
],
|
|
396
|
+
"color-stop": [
|
|
397
|
+
",",
|
|
398
|
+
"|",
|
|
399
|
+
"&",
|
|
400
|
+
")",
|
|
401
|
+
"]",
|
|
402
|
+
"."
|
|
403
|
+
],
|
|
404
|
+
"color-hint": [
|
|
405
|
+
",",
|
|
406
|
+
"|",
|
|
407
|
+
"&",
|
|
408
|
+
")",
|
|
409
|
+
"]",
|
|
410
|
+
"."
|
|
411
|
+
],
|
|
412
|
+
")": [
|
|
413
|
+
",",
|
|
414
|
+
"|",
|
|
415
|
+
"&",
|
|
416
|
+
")",
|
|
417
|
+
"]",
|
|
418
|
+
"."
|
|
419
|
+
],
|
|
420
|
+
"]": [
|
|
421
|
+
",",
|
|
422
|
+
"|",
|
|
423
|
+
"&",
|
|
424
|
+
")",
|
|
425
|
+
"]",
|
|
426
|
+
"."
|
|
427
|
+
],
|
|
428
|
+
".": []
|
|
429
|
+
};
|
|
430
|
+
function validatePatternSemantic(input) {
|
|
431
|
+
const tokens = tokenizePattern(input);
|
|
432
|
+
if (tokens.length === 0) throw new Error("Pattern cannot be empty");
|
|
433
|
+
for (let i = 0; i < tokens.length - 1; i += 1) {
|
|
434
|
+
const current = tokens[i];
|
|
435
|
+
const next = tokens[i + 1];
|
|
436
|
+
const allowedNext = NEXT_TOKEN_MAP[current];
|
|
437
|
+
if (!allowedNext) throw new Error(`No semantic transition rule defined for token "${current}"`);
|
|
438
|
+
if (!allowedNext.includes(next)) throw new Error(`Token "${next}" is not allowed after "${current}" at index ${i + 1}`);
|
|
439
|
+
}
|
|
440
|
+
return true;
|
|
441
|
+
}
|
|
442
|
+
function validatePatternStructure(input) {
|
|
443
|
+
const tokens = tokenizePattern(input);
|
|
444
|
+
for (let i = 0; i < tokens.length; i += 1) {
|
|
445
|
+
const current = tokens[i];
|
|
446
|
+
const next = tokens[i + 1];
|
|
447
|
+
const previous = tokens[i - 1];
|
|
448
|
+
if (current === "(" && next === ")") throw new Error(`Empty group "()" is not allowed at token index ${i}`);
|
|
449
|
+
if (current === "[" && next === "]") throw new Error(`Empty sequence "[]" is not allowed at token index ${i}`);
|
|
450
|
+
if (current === ",") {
|
|
451
|
+
if (previous === void 0) throw new Error(`Unexpected "," at token index ${i}`);
|
|
452
|
+
if (next === void 0) throw new Error(`Unexpected "," at token index ${i}`);
|
|
453
|
+
if (previous === "[") throw new Error(`Sequence cannot start with "," at token index ${i}`);
|
|
454
|
+
if (next === "]") throw new Error(`Sequence cannot end with "," at token index ${i}`);
|
|
455
|
+
if (previous === ",") throw new Error(`Unexpected consecutive "," at token index ${i}`);
|
|
456
|
+
if (next === ",") throw new Error(`Unexpected consecutive "," at token index ${i}`);
|
|
899
457
|
}
|
|
900
|
-
const lastGeneratedPosition = newStops[newStops.length - 1]?.position;
|
|
901
|
-
if (!lastGeneratedPosition || lastGeneratedPosition.kind !== "percentage" || lastGeneratedPosition.value > 1) break;
|
|
902
|
-
if (!shouldContinue) break;
|
|
903
|
-
offset = roundTo(offset + percentageSum, 3);
|
|
904
458
|
}
|
|
905
|
-
return
|
|
906
|
-
}
|
|
907
|
-
//#endregion
|
|
908
|
-
//#region src/parser/radial-gradient/parse-radial-gradient.ts
|
|
909
|
-
function parseRadialGradient(source, tokens, startIndex) {
|
|
910
|
-
const functionToken = getTokenAt(tokens, startIndex);
|
|
911
|
-
if (functionToken === null || functionToken.kind !== TokenKind.FUNCTION_RADIAL_GRADIENT && functionToken.kind !== TokenKind.FUNCTION_REPEATING_RADIAL_GRADIENT) throw new Error("Expected radial gradient function token");
|
|
912
|
-
const repeat = functionToken.kind === TokenKind.FUNCTION_REPEATING_RADIAL_GRADIENT ? "repeating" : "normal";
|
|
913
|
-
let index = startIndex + 1;
|
|
914
|
-
index = expectToken(tokens, index, TokenKind.PAREN_OPEN).nextIndex;
|
|
915
|
-
const configResult = parseRadialConfig(tokens, index);
|
|
916
|
-
const config = configResult.node ?? createDefaultRadialConfig();
|
|
917
|
-
index = configResult.nextIndex;
|
|
918
|
-
if (configResult.node !== null) index = expectToken(tokens, index, TokenKind.COMMA).nextIndex;
|
|
919
|
-
const stopsResult = parseGradientStopList(source, tokens, index);
|
|
920
|
-
const stops = repeat === "repeating" ? expandColorStops$1(stopsResult.node) : stopsResult.node;
|
|
921
|
-
index = stopsResult.nextIndex;
|
|
922
|
-
index = expectToken(tokens, index, TokenKind.PAREN_CLOSE).nextIndex;
|
|
923
|
-
return {
|
|
924
|
-
node: {
|
|
925
|
-
kind: "radial",
|
|
926
|
-
repeat,
|
|
927
|
-
shape: config.shape,
|
|
928
|
-
size: config.size,
|
|
929
|
-
position: config.position,
|
|
930
|
-
stops
|
|
931
|
-
},
|
|
932
|
-
nextIndex: index
|
|
933
|
-
};
|
|
934
|
-
}
|
|
935
|
-
function parseRadialConfig(tokens, startIndex) {
|
|
936
|
-
let index = findNextNonWhitespaceIndex(tokens, startIndex);
|
|
937
|
-
if (index === -1) return {
|
|
938
|
-
node: null,
|
|
939
|
-
nextIndex: startIndex
|
|
940
|
-
};
|
|
941
|
-
const shapeResult = parseRadialShape(tokens, index);
|
|
942
|
-
const shape = shapeResult.node ?? "ellipse";
|
|
943
|
-
index = shapeResult.nextIndex;
|
|
944
|
-
const sizeResult = parseRadialSize(tokens, index, shape);
|
|
945
|
-
const size = sizeResult.node ?? createDefaultRadialSize(shape);
|
|
946
|
-
index = sizeResult.nextIndex;
|
|
947
|
-
const positionResult = parseRadialPosition(tokens, index);
|
|
948
|
-
const position = positionResult.node ?? createDefaultRadialPosition();
|
|
949
|
-
index = positionResult.nextIndex;
|
|
950
|
-
if (!(shapeResult.node !== null || sizeResult.node !== null || positionResult.node !== null)) return {
|
|
951
|
-
node: null,
|
|
952
|
-
nextIndex: startIndex
|
|
953
|
-
};
|
|
954
|
-
return {
|
|
955
|
-
node: {
|
|
956
|
-
shape,
|
|
957
|
-
size,
|
|
958
|
-
position
|
|
959
|
-
},
|
|
960
|
-
nextIndex: index
|
|
961
|
-
};
|
|
962
|
-
}
|
|
963
|
-
function parseRadialShape(tokens, startIndex) {
|
|
964
|
-
const index = findNextNonWhitespaceIndex(tokens, startIndex);
|
|
965
|
-
if (index === -1) return {
|
|
966
|
-
node: null,
|
|
967
|
-
nextIndex: startIndex
|
|
968
|
-
};
|
|
969
|
-
const token = tokens[index];
|
|
970
|
-
if (token === void 0) return {
|
|
971
|
-
node: null,
|
|
972
|
-
nextIndex: startIndex
|
|
973
|
-
};
|
|
974
|
-
if (token.kind === TokenKind.CIRCLE) return {
|
|
975
|
-
node: "circle",
|
|
976
|
-
nextIndex: index + 1
|
|
977
|
-
};
|
|
978
|
-
if (token.kind === TokenKind.ELLIPSE) return {
|
|
979
|
-
node: "ellipse",
|
|
980
|
-
nextIndex: index + 1
|
|
981
|
-
};
|
|
982
|
-
return {
|
|
983
|
-
node: null,
|
|
984
|
-
nextIndex: startIndex
|
|
985
|
-
};
|
|
459
|
+
return true;
|
|
986
460
|
}
|
|
987
|
-
function
|
|
988
|
-
const
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
nextIndex: index + 1
|
|
1002
|
-
};
|
|
1003
|
-
const firstSize = parseLengthPercentageToken$1(token);
|
|
1004
|
-
if (firstSize === null) return {
|
|
1005
|
-
node: null,
|
|
1006
|
-
nextIndex: startIndex
|
|
1007
|
-
};
|
|
1008
|
-
let nextIndex = findNextNonWhitespaceIndex(tokens, index + 1);
|
|
1009
|
-
let secondSize = null;
|
|
1010
|
-
if (nextIndex !== -1) {
|
|
1011
|
-
const nextToken = tokens[nextIndex];
|
|
1012
|
-
if (nextToken !== void 0) secondSize = parseLengthPercentageToken$1(nextToken);
|
|
1013
|
-
}
|
|
1014
|
-
if (shape === "circle") return {
|
|
1015
|
-
node: createRadialSizeFromRadii(shape, firstSize, firstSize),
|
|
1016
|
-
nextIndex: index + 1
|
|
1017
|
-
};
|
|
1018
|
-
if (secondSize !== null && nextIndex !== -1) return {
|
|
1019
|
-
node: createRadialSizeFromRadii(shape, firstSize, secondSize),
|
|
1020
|
-
nextIndex
|
|
1021
|
-
};
|
|
1022
|
-
return {
|
|
1023
|
-
node: createRadialSizeFromRadii(shape, firstSize, firstSize),
|
|
1024
|
-
nextIndex: index + 1
|
|
1025
|
-
};
|
|
1026
|
-
}
|
|
1027
|
-
function parseRadialPosition(tokens, startIndex) {
|
|
1028
|
-
let index = findNextNonWhitespaceIndex(tokens, startIndex);
|
|
1029
|
-
if (index === -1) return {
|
|
1030
|
-
node: null,
|
|
1031
|
-
nextIndex: startIndex
|
|
1032
|
-
};
|
|
1033
|
-
const atToken = tokens[index];
|
|
1034
|
-
if (atToken === void 0 || atToken.kind !== TokenKind.AT) return {
|
|
1035
|
-
node: null,
|
|
1036
|
-
nextIndex: startIndex
|
|
1037
|
-
};
|
|
1038
|
-
index += 1;
|
|
1039
|
-
const keywords = [];
|
|
1040
|
-
let x = null;
|
|
1041
|
-
let y = null;
|
|
1042
|
-
while (true) {
|
|
1043
|
-
const nextIndex = findNextNonWhitespaceIndex(tokens, index);
|
|
1044
|
-
if (nextIndex === -1) break;
|
|
1045
|
-
const token = tokens[nextIndex];
|
|
1046
|
-
if (token === void 0) break;
|
|
1047
|
-
const lengthPercentage = parseLengthPercentageToken$1(token);
|
|
1048
|
-
if (lengthPercentage !== null) {
|
|
1049
|
-
if (x === null) x = lengthPercentage;
|
|
1050
|
-
else if (y === null) y = lengthPercentage;
|
|
1051
|
-
else break;
|
|
1052
|
-
index = nextIndex + 1;
|
|
461
|
+
function tokenizePattern(input) {
|
|
462
|
+
const source = input.trim();
|
|
463
|
+
const tokens = [];
|
|
464
|
+
let index = 0;
|
|
465
|
+
while (index < source.length) {
|
|
466
|
+
const rest = source.slice(index);
|
|
467
|
+
const char = source[index];
|
|
468
|
+
if (/\s/.test(char)) {
|
|
469
|
+
index += 1;
|
|
470
|
+
continue;
|
|
471
|
+
}
|
|
472
|
+
if (rest.startsWith("color-stop")) {
|
|
473
|
+
tokens.push("color-stop");
|
|
474
|
+
index += 10;
|
|
1053
475
|
continue;
|
|
1054
476
|
}
|
|
1055
|
-
if (
|
|
1056
|
-
|
|
1057
|
-
index
|
|
477
|
+
if (rest.startsWith("color-hint")) {
|
|
478
|
+
tokens.push("color-hint");
|
|
479
|
+
index += 10;
|
|
1058
480
|
continue;
|
|
1059
481
|
}
|
|
1060
|
-
|
|
482
|
+
if (rest.startsWith("config")) {
|
|
483
|
+
tokens.push("config");
|
|
484
|
+
index += 6;
|
|
485
|
+
continue;
|
|
486
|
+
}
|
|
487
|
+
if (char === "^" || char === "." || char === "(" || char === ")" || char === "[" || char === "]" || char === "," || char === "|" || char === "&" || char === "!" || char === "~") {
|
|
488
|
+
tokens.push(char);
|
|
489
|
+
index += 1;
|
|
490
|
+
continue;
|
|
491
|
+
}
|
|
492
|
+
throw new Error(`Unexpected token near "${rest}" at index ${index}`);
|
|
1061
493
|
}
|
|
1062
|
-
return
|
|
1063
|
-
node: createRadialPositionNode(x, y, keywords),
|
|
1064
|
-
nextIndex: index
|
|
1065
|
-
};
|
|
494
|
+
return tokens;
|
|
1066
495
|
}
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
496
|
+
//#endregion
|
|
497
|
+
//#region src/dsl/index.ts
|
|
498
|
+
function isValid(input, pattern) {
|
|
499
|
+
try {
|
|
500
|
+
validate(input, pattern);
|
|
501
|
+
return true;
|
|
502
|
+
} catch {
|
|
503
|
+
return false;
|
|
504
|
+
}
|
|
1073
505
|
}
|
|
1074
|
-
function
|
|
1075
|
-
|
|
506
|
+
function validate(classified, pattern) {
|
|
507
|
+
validatePattern(pattern);
|
|
508
|
+
const bodyTokens = tokenizePattern(pattern).slice(1, -1);
|
|
509
|
+
const result = matchExpression(classified, bodyTokens, 0, 0);
|
|
510
|
+
if (!result.matched) throw new Error("Input does not match pattern");
|
|
511
|
+
if (result.nextInputIndex !== classified.length) throw new Error("Pattern did not consume all inputs");
|
|
512
|
+
if (result.nextPatternIndex !== bodyTokens.length) throw new Error("Input ended before pattern was fully matched");
|
|
513
|
+
return true;
|
|
1076
514
|
}
|
|
1077
|
-
|
|
515
|
+
//#endregion
|
|
516
|
+
//#region src/abi.ts
|
|
517
|
+
const REPEATING_PREFIX = "repeating-";
|
|
518
|
+
const PARAMS_VALIDATION_PATTERN = "^[([config,color-stop,([color-hint,color-stop]|color-stop)]|color-stop),~([color-hint,color-stop]|color-stop)].";
|
|
519
|
+
function parseStringToAbi(value, pattern = PARAMS_VALIDATION_PATTERN) {
|
|
520
|
+
const source = value.trim();
|
|
521
|
+
if (source.length === 0) throw new Error("Expected function call, received empty string");
|
|
522
|
+
const { functionName, isRepeating, inputs } = extractOuterFunctionCall(source);
|
|
523
|
+
const classified = classifyInputs(inputs);
|
|
524
|
+
validate(classified, pattern);
|
|
1078
525
|
return {
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
value: .5
|
|
1083
|
-
},
|
|
1084
|
-
y: {
|
|
1085
|
-
kind: "percentage",
|
|
1086
|
-
value: .5
|
|
1087
|
-
},
|
|
1088
|
-
keywords: ["center"]
|
|
526
|
+
functionName,
|
|
527
|
+
isRepeating,
|
|
528
|
+
inputs: classified
|
|
1089
529
|
};
|
|
1090
530
|
}
|
|
1091
|
-
function
|
|
1092
|
-
|
|
1093
|
-
kind: "position",
|
|
1094
|
-
x: {
|
|
1095
|
-
kind: "percentage",
|
|
1096
|
-
value: .5
|
|
1097
|
-
},
|
|
1098
|
-
y: {
|
|
1099
|
-
kind: "percentage",
|
|
1100
|
-
value: .5
|
|
1101
|
-
},
|
|
1102
|
-
keywords
|
|
1103
|
-
};
|
|
1104
|
-
return {
|
|
1105
|
-
kind: "position",
|
|
1106
|
-
x: x ?? {
|
|
1107
|
-
kind: "percentage",
|
|
1108
|
-
value: .5
|
|
1109
|
-
},
|
|
1110
|
-
y: y ?? {
|
|
1111
|
-
kind: "percentage",
|
|
1112
|
-
value: .5
|
|
1113
|
-
},
|
|
1114
|
-
keywords
|
|
1115
|
-
};
|
|
531
|
+
function isColorHint(value) {
|
|
532
|
+
return /^-?\d*\.?\d+(%|deg|rad|turn|grad|px|em|rem|vh|vw|vmin|vmax|cm|mm|in|pt|pc|q)?$/i.test(value.trim());
|
|
1116
533
|
}
|
|
1117
|
-
function
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
kind: "percentage",
|
|
1128
|
-
value: shape === "circle" ? 1 : 1
|
|
1129
|
-
}
|
|
1130
|
-
};
|
|
534
|
+
function isColorStop(value) {
|
|
535
|
+
try {
|
|
536
|
+
const chunk = splitTopLevelByWhitespace(value)[0];
|
|
537
|
+
return parse$1(chunk) !== void 0;
|
|
538
|
+
} catch {
|
|
539
|
+
return false;
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
function isConfig(value) {
|
|
543
|
+
return !isColorHint(value) && !isColorStop(value);
|
|
1131
544
|
}
|
|
1132
|
-
function
|
|
545
|
+
function classifyInputs(inputs) {
|
|
546
|
+
const normalizedTypes = inputs.map((value) => value.trim()).filter((value) => value.length > 0);
|
|
547
|
+
if (normalizedTypes.length === 0) return [];
|
|
548
|
+
return normalizedTypes.map((value, index) => {
|
|
549
|
+
if (index === 0 && !isColorStop(value)) return {
|
|
550
|
+
type: "config",
|
|
551
|
+
value
|
|
552
|
+
};
|
|
553
|
+
if (isColorStop(value)) return {
|
|
554
|
+
type: "color-stop",
|
|
555
|
+
value
|
|
556
|
+
};
|
|
557
|
+
if (isColorHint(value)) return {
|
|
558
|
+
type: "color-hint",
|
|
559
|
+
value
|
|
560
|
+
};
|
|
561
|
+
return {
|
|
562
|
+
type: "config",
|
|
563
|
+
value
|
|
564
|
+
};
|
|
565
|
+
});
|
|
566
|
+
}
|
|
567
|
+
function extractOuterFunctionCall(value) {
|
|
568
|
+
const openIndex = value.indexOf("(");
|
|
569
|
+
if (openIndex <= 0) throw new Error("Expected function opening parenthesis");
|
|
570
|
+
let functionName = value.slice(0, openIndex).trim();
|
|
571
|
+
if (functionName.length === 0) throw new Error("Expected function name before \"(\"");
|
|
572
|
+
const isRepeating = functionName.startsWith(REPEATING_PREFIX);
|
|
573
|
+
if (isRepeating) functionName = functionName.slice(10);
|
|
574
|
+
const closeIndex = findOuterClosingParenIndex(value, openIndex);
|
|
575
|
+
if (closeIndex === -1) throw new Error("Unclosed function parenthesis");
|
|
576
|
+
const inputs = splitTopLevelInputs(value.slice(openIndex + 1, closeIndex));
|
|
1133
577
|
return {
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
radiusX,
|
|
1138
|
-
radiusY
|
|
578
|
+
functionName,
|
|
579
|
+
isRepeating,
|
|
580
|
+
inputs
|
|
1139
581
|
};
|
|
1140
582
|
}
|
|
1141
|
-
function
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
583
|
+
function findOuterClosingParenIndex(value, openIndex) {
|
|
584
|
+
let depth = 0;
|
|
585
|
+
for (let i = openIndex; i < value.length; i += 1) {
|
|
586
|
+
const char = value[i];
|
|
587
|
+
if (char === "(") {
|
|
588
|
+
depth += 1;
|
|
589
|
+
continue;
|
|
590
|
+
}
|
|
591
|
+
if (char === ")") {
|
|
592
|
+
depth -= 1;
|
|
593
|
+
if (depth === 0) return i;
|
|
594
|
+
if (depth < 0) return -1;
|
|
595
|
+
}
|
|
1148
596
|
}
|
|
597
|
+
return -1;
|
|
1149
598
|
}
|
|
1150
|
-
function
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
599
|
+
function splitTopLevelInputs(value) {
|
|
600
|
+
const result = [];
|
|
601
|
+
let current = "";
|
|
602
|
+
let parenDepth = 0;
|
|
603
|
+
let braceDepth = 0;
|
|
604
|
+
let bracketDepth = 0;
|
|
605
|
+
for (let i = 0; i < value.length; i += 1) {
|
|
606
|
+
const char = value[i];
|
|
607
|
+
if (char === "(") {
|
|
608
|
+
parenDepth += 1;
|
|
609
|
+
current += char;
|
|
610
|
+
continue;
|
|
611
|
+
}
|
|
612
|
+
if (char === ")") {
|
|
613
|
+
parenDepth -= 1;
|
|
614
|
+
current += char;
|
|
615
|
+
continue;
|
|
616
|
+
}
|
|
617
|
+
if (char === "{") {
|
|
618
|
+
braceDepth += 1;
|
|
619
|
+
current += char;
|
|
620
|
+
continue;
|
|
621
|
+
}
|
|
622
|
+
if (char === "}") {
|
|
623
|
+
braceDepth -= 1;
|
|
624
|
+
current += char;
|
|
625
|
+
continue;
|
|
626
|
+
}
|
|
627
|
+
if (char === "[") {
|
|
628
|
+
bracketDepth += 1;
|
|
629
|
+
current += char;
|
|
630
|
+
continue;
|
|
631
|
+
}
|
|
632
|
+
if (char === "]") {
|
|
633
|
+
bracketDepth -= 1;
|
|
634
|
+
current += char;
|
|
635
|
+
continue;
|
|
636
|
+
}
|
|
637
|
+
if (char === "," && parenDepth === 0 && braceDepth === 0 && bracketDepth === 0) {
|
|
638
|
+
pushTrimmed(result, current);
|
|
639
|
+
current = "";
|
|
640
|
+
continue;
|
|
641
|
+
}
|
|
642
|
+
current += char;
|
|
643
|
+
}
|
|
644
|
+
pushTrimmed(result, current);
|
|
645
|
+
return result;
|
|
646
|
+
}
|
|
647
|
+
function pushTrimmed(target, value) {
|
|
648
|
+
const trimmed = value.trim();
|
|
649
|
+
if (trimmed.length > 0) target.push(trimmed);
|
|
650
|
+
}
|
|
651
|
+
function splitTopLevelByWhitespace(value) {
|
|
652
|
+
const source = value.trim();
|
|
653
|
+
const result = [];
|
|
654
|
+
let current = "";
|
|
655
|
+
let parenDepth = 0;
|
|
656
|
+
let braceDepth = 0;
|
|
657
|
+
let bracketDepth = 0;
|
|
658
|
+
for (let i = 0; i < source.length; i += 1) {
|
|
659
|
+
const char = source[i];
|
|
660
|
+
if (char === "(") {
|
|
661
|
+
parenDepth += 1;
|
|
662
|
+
current += char;
|
|
663
|
+
continue;
|
|
664
|
+
}
|
|
665
|
+
if (char === ")") {
|
|
666
|
+
parenDepth -= 1;
|
|
667
|
+
current += char;
|
|
668
|
+
continue;
|
|
669
|
+
}
|
|
670
|
+
if (char === "{") {
|
|
671
|
+
braceDepth += 1;
|
|
672
|
+
current += char;
|
|
673
|
+
continue;
|
|
674
|
+
}
|
|
675
|
+
if (char === "}") {
|
|
676
|
+
braceDepth -= 1;
|
|
677
|
+
current += char;
|
|
678
|
+
continue;
|
|
679
|
+
}
|
|
680
|
+
if (char === "[") {
|
|
681
|
+
bracketDepth += 1;
|
|
682
|
+
current += char;
|
|
683
|
+
continue;
|
|
684
|
+
}
|
|
685
|
+
if (char === "]") {
|
|
686
|
+
bracketDepth -= 1;
|
|
687
|
+
current += char;
|
|
688
|
+
continue;
|
|
689
|
+
}
|
|
690
|
+
if (/\s/.test(char) && parenDepth === 0 && braceDepth === 0 && bracketDepth === 0) {
|
|
691
|
+
if (current.trim().length > 0) {
|
|
692
|
+
result.push(current.trim());
|
|
693
|
+
current = "";
|
|
694
|
+
}
|
|
695
|
+
continue;
|
|
696
|
+
}
|
|
697
|
+
current += char;
|
|
698
|
+
}
|
|
699
|
+
if (current.trim().length > 0) result.push(current.trim());
|
|
700
|
+
return result;
|
|
1161
701
|
}
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
702
|
+
//#endregion
|
|
703
|
+
//#region src/gradients/GradientBase.ts
|
|
704
|
+
var GradientBase = class {
|
|
705
|
+
_isRepeating;
|
|
706
|
+
_config;
|
|
707
|
+
_stops;
|
|
708
|
+
constructor(options) {
|
|
709
|
+
this._isRepeating = options.isRepeating;
|
|
710
|
+
this._config = this._cloneConfig(options.config);
|
|
711
|
+
this._stops = this._getSortedStops(this._cloneStops(options.stops));
|
|
712
|
+
this._validateConfig(this._config);
|
|
713
|
+
this._validateStops(this._stops);
|
|
714
|
+
}
|
|
715
|
+
get isRepeating() {
|
|
716
|
+
return this._isRepeating;
|
|
717
|
+
}
|
|
718
|
+
get config() {
|
|
719
|
+
return this._cloneConfig(this._config);
|
|
720
|
+
}
|
|
721
|
+
get stops() {
|
|
722
|
+
return this._cloneStops(this._stops);
|
|
723
|
+
}
|
|
724
|
+
toJSON() {
|
|
725
|
+
return {
|
|
726
|
+
type: this.type,
|
|
727
|
+
isRepeating: this.isRepeating,
|
|
728
|
+
config: this.config,
|
|
729
|
+
stops: this.stops
|
|
730
|
+
};
|
|
731
|
+
}
|
|
732
|
+
addStop(stop) {
|
|
733
|
+
const nextStops = [...this._cloneStops(this._stops), ...this._cloneStops([stop])];
|
|
734
|
+
const sortedStops = this._getSortedStops(nextStops);
|
|
735
|
+
this._validateStops(sortedStops);
|
|
736
|
+
this._stops = sortedStops;
|
|
737
|
+
}
|
|
738
|
+
static fromString(_) {
|
|
739
|
+
throw new Error("Not implimented");
|
|
740
|
+
}
|
|
741
|
+
static fromAbi(_) {
|
|
742
|
+
throw new Error("Not implimented");
|
|
743
|
+
}
|
|
744
|
+
removeStop(index) {
|
|
745
|
+
if (!Number.isInteger(index)) throw new TypeError("Gradient stop index must be an integer");
|
|
746
|
+
if (index < 0 || index >= this._stops.length) throw new RangeError("Gradient stop index is out of bounds");
|
|
747
|
+
if (this._stops.filter((stop) => stop.type === "color-stop").length <= this._minColorStopsCount()) throw new Error(`Color stop count should be greather than ${this._minColorStopsCount()}`);
|
|
748
|
+
const nextIndex = index + 1 > this._stops.length - 1 ? this._stops.length - 1 : index + 1;
|
|
749
|
+
const prevIndex = index - 1 >= 0 ? index - 1 : 0;
|
|
750
|
+
if (index !== nextIndex && this._stops[nextIndex].type === "color-hint") this._stops.splice(nextIndex, 1);
|
|
751
|
+
this._stops.splice(index, 1);
|
|
752
|
+
if (index !== prevIndex && this._stops[prevIndex].type === "color-hint") this._stops.splice(prevIndex, 1);
|
|
753
|
+
}
|
|
754
|
+
equals(other) {
|
|
755
|
+
if (this.type !== other.type) return false;
|
|
756
|
+
if (this.isRepeating !== other.isRepeating) return false;
|
|
757
|
+
if (JSON.stringify(this.config) !== JSON.stringify(other.config)) return false;
|
|
758
|
+
if (this.stops.length !== other.stops.length) return false;
|
|
759
|
+
for (let index = 0; index < this.stops.length; index++) {
|
|
760
|
+
const left = this.stops[index];
|
|
761
|
+
const right = other.stops[index];
|
|
762
|
+
if (left.type !== right.type || left.value !== right.value || left.position !== right.position) return false;
|
|
763
|
+
}
|
|
764
|
+
return true;
|
|
765
|
+
}
|
|
766
|
+
_minColorStopsCount() {
|
|
767
|
+
return 0;
|
|
768
|
+
}
|
|
769
|
+
_getSortedStops(stops) {
|
|
770
|
+
return stops.map((stop, index) => ({
|
|
771
|
+
stop,
|
|
772
|
+
index
|
|
773
|
+
})).sort((a, b) => {
|
|
774
|
+
if (a.stop.position !== b.stop.position) return a.stop.position - b.stop.position;
|
|
775
|
+
return a.index - b.index;
|
|
776
|
+
}).map((item) => item.stop);
|
|
777
|
+
}
|
|
778
|
+
_validateStops(value) {
|
|
779
|
+
this._validateStopsShape(value);
|
|
780
|
+
this._validateStopsSequence(value);
|
|
781
|
+
}
|
|
782
|
+
_validateStopsShape(value) {
|
|
783
|
+
if (!Array.isArray(value)) throw new TypeError("Gradient stops must be an array");
|
|
784
|
+
for (const stop of value) {
|
|
785
|
+
if (typeof stop !== "object" || stop === null) throw new TypeError("Gradient stop must be an object");
|
|
786
|
+
if (stop.type !== "color-stop" && stop.type !== "color-hint") throw new TypeError(`Invalid gradient stop type: ${String(stop.type)}`);
|
|
787
|
+
if (typeof stop.value !== "string") throw new TypeError("Gradient stop value must be a string");
|
|
788
|
+
if (typeof stop.position !== "number" || Number.isNaN(stop.position)) throw new TypeError("Gradient stop position must be a valid number");
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
_validateStopsSequence(value) {
|
|
792
|
+
if (value.length < this._minColorStopsCount()) throw new TypeError(`Gradient must contain at least ${this._minColorStopsCount()} stop`);
|
|
793
|
+
if (value[0].type !== "color-stop") throw new TypeError("Gradient stop sequence must start with a color-stop");
|
|
794
|
+
if (value[value.length - 1].type === "color-hint") throw new TypeError("Gradient stop sequence cannot end with a color-hint");
|
|
795
|
+
for (let index = 1; index < value.length; index++) {
|
|
796
|
+
const prev = value[index - 1];
|
|
797
|
+
const current = value[index];
|
|
798
|
+
if (prev.type === "color-hint" && current.type !== "color-stop") throw new TypeError("A color-hint must be followed by a color-stop");
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
_cloneStops(stops) {
|
|
802
|
+
return stops.map((stop) => ({ ...stop }));
|
|
803
|
+
}
|
|
804
|
+
_cloneConfig(value) {
|
|
805
|
+
if (typeof value !== "object" || value === null) return value;
|
|
806
|
+
if (Array.isArray(value)) return [...value];
|
|
807
|
+
return { ...value };
|
|
808
|
+
}
|
|
809
|
+
_buildSerializedStopTokens() {
|
|
810
|
+
const result = [];
|
|
811
|
+
for (let index = 0; index < this.stops.length; index++) {
|
|
812
|
+
const current = this.stops[index];
|
|
813
|
+
if (current.type === "color-hint") {
|
|
814
|
+
result.push({
|
|
815
|
+
type: "color-hint",
|
|
816
|
+
position: current.position
|
|
817
|
+
});
|
|
818
|
+
continue;
|
|
819
|
+
}
|
|
820
|
+
const next = this.stops[index + 1];
|
|
821
|
+
if (next && next.type === "color-stop" && next.value === current.value) {
|
|
822
|
+
result.push({
|
|
823
|
+
type: "color-stop",
|
|
824
|
+
value: current.value,
|
|
825
|
+
positions: [current.position, next.position]
|
|
826
|
+
});
|
|
827
|
+
index += 1;
|
|
828
|
+
continue;
|
|
829
|
+
}
|
|
830
|
+
result.push({
|
|
831
|
+
type: "color-stop",
|
|
832
|
+
value: current.value,
|
|
833
|
+
positions: [current.position]
|
|
1185
834
|
});
|
|
1186
835
|
}
|
|
1187
|
-
|
|
836
|
+
return result;
|
|
1188
837
|
}
|
|
1189
|
-
|
|
1190
|
-
|
|
838
|
+
_canOmitAllStopPositions(tokens) {
|
|
839
|
+
const stopTokens = tokens.filter((token) => token.type === "color-stop");
|
|
840
|
+
if (tokens.some((token) => token.type === "color-hint")) return false;
|
|
841
|
+
if (stopTokens.some((token) => token.positions.length !== 1)) return false;
|
|
842
|
+
if (stopTokens.length <= 1) return false;
|
|
843
|
+
const epsilon = 1e-6;
|
|
844
|
+
for (let index = 0; index < stopTokens.length; index++) {
|
|
845
|
+
const expected = index / (stopTokens.length - 1);
|
|
846
|
+
const actual = stopTokens[index].positions[0];
|
|
847
|
+
if (Math.abs(actual - expected) > epsilon) return false;
|
|
848
|
+
}
|
|
849
|
+
return true;
|
|
850
|
+
}
|
|
851
|
+
_serializeStopsCompact() {
|
|
852
|
+
const tokens = this._buildSerializedStopTokens();
|
|
853
|
+
if (this._canOmitAllStopPositions(tokens)) return tokens.map((token) => {
|
|
854
|
+
if (token.type !== "color-stop") throw new Error("Unexpected color-hint token in compact stop serialization");
|
|
855
|
+
return token.value;
|
|
856
|
+
});
|
|
857
|
+
return tokens.map((token) => {
|
|
858
|
+
if (token.type === "color-hint") return `${this._formatPercent(token.position)}%`;
|
|
859
|
+
if (token.positions.length === 2) return `${token.value} ${this._formatPercent(token.positions[0])}% ${this._formatPercent(token.positions[1])}%`;
|
|
860
|
+
return `${token.value} ${this._formatPercent(token.positions[0])}%`;
|
|
861
|
+
});
|
|
862
|
+
}
|
|
863
|
+
_formatPercent(value) {
|
|
864
|
+
return roundTo(value * 100, 3);
|
|
865
|
+
}
|
|
866
|
+
static _normalizeAbiInputsToStops(inputs) {
|
|
867
|
+
const pending = [];
|
|
868
|
+
for (const input of inputs) {
|
|
869
|
+
if (input.type === "color-hint") {
|
|
870
|
+
pending.push({
|
|
871
|
+
type: "color-hint",
|
|
872
|
+
value: input.value,
|
|
873
|
+
position: this._parsePosition(input.value)
|
|
874
|
+
});
|
|
875
|
+
continue;
|
|
876
|
+
}
|
|
877
|
+
if (input.type === "color-stop") {
|
|
878
|
+
const parsed = this._parseColorStopInput(input.value);
|
|
879
|
+
pending.push(...parsed);
|
|
880
|
+
continue;
|
|
881
|
+
}
|
|
882
|
+
throw new SyntaxError(`Unsupported linear gradient ABI input type: "${input.type}"`);
|
|
883
|
+
}
|
|
884
|
+
return this._resolvePendingStops(pending);
|
|
885
|
+
}
|
|
886
|
+
static _parsePosition(input) {
|
|
887
|
+
const match = input.trim().toLowerCase().match(/^([+-]?(?:\d+\.?\d*|\.\d+))%$/);
|
|
888
|
+
if (match === null) throw new SyntaxError(`Invalid gradient stop position: "${input}"`);
|
|
889
|
+
const numeric = Number(match[1]);
|
|
890
|
+
if (!Number.isFinite(numeric)) throw new SyntaxError(`Invalid gradient stop position: "${input}"`);
|
|
891
|
+
return numeric / 100;
|
|
892
|
+
}
|
|
893
|
+
static _parseColorStopInput(input) {
|
|
894
|
+
const parts = splitTopLevelByWhitespace(input);
|
|
895
|
+
if (parts.length === 0) throw new SyntaxError("Color-stop input cannot be empty");
|
|
896
|
+
const positions = [];
|
|
897
|
+
while (parts.length > 0) {
|
|
898
|
+
const last = parts[parts.length - 1];
|
|
899
|
+
if (!/%$/.test(last)) break;
|
|
900
|
+
positions.unshift(this._parsePosition(last));
|
|
901
|
+
parts.pop();
|
|
902
|
+
if (positions.length === 2) break;
|
|
903
|
+
}
|
|
904
|
+
const color = parts.join(" ").trim();
|
|
905
|
+
if (color.length === 0) throw new SyntaxError(`Color-stop is missing color value: "${input}"`);
|
|
906
|
+
if (positions.length === 0) return [{
|
|
907
|
+
type: "color-stop",
|
|
908
|
+
value: color
|
|
909
|
+
}];
|
|
910
|
+
if (positions.length === 1) return [{
|
|
911
|
+
type: "color-stop",
|
|
912
|
+
value: color,
|
|
913
|
+
position: positions[0]
|
|
914
|
+
}];
|
|
915
|
+
return [{
|
|
916
|
+
type: "color-stop",
|
|
917
|
+
value: color,
|
|
918
|
+
position: positions[0]
|
|
919
|
+
}, {
|
|
920
|
+
type: "color-stop",
|
|
921
|
+
value: color,
|
|
922
|
+
position: positions[1]
|
|
923
|
+
}];
|
|
924
|
+
}
|
|
925
|
+
static _resolvePendingStops(input) {
|
|
926
|
+
if (input.length === 0) throw new SyntaxError("Linear gradient must contain at least one stop");
|
|
927
|
+
const stops = input.map((item) => ({ ...item }));
|
|
928
|
+
const firstColorStopIndex = stops.findIndex((item) => item.type === "color-stop");
|
|
929
|
+
const lastColorStopIndex = [...stops].reverse().findIndex((item) => item.type === "color-stop");
|
|
930
|
+
if (firstColorStopIndex === -1) throw new SyntaxError("Linear gradient must contain at least one color-stop");
|
|
931
|
+
const realLastColorStopIndex = stops.length - 1 - lastColorStopIndex;
|
|
932
|
+
if (stops[firstColorStopIndex].position === void 0) stops[firstColorStopIndex].position = 0;
|
|
933
|
+
if (stops[realLastColorStopIndex].position === void 0) stops[realLastColorStopIndex].position = 1;
|
|
934
|
+
let segmentStart = -1;
|
|
935
|
+
for (let index = 0; index < stops.length; index++) {
|
|
936
|
+
const current = stops[index];
|
|
937
|
+
if (current.position !== void 0) {
|
|
938
|
+
if (segmentStart !== -1) {
|
|
939
|
+
const start = stops[segmentStart];
|
|
940
|
+
const end = current;
|
|
941
|
+
const gap = index - segmentStart;
|
|
942
|
+
for (let inner = 1; inner < gap; inner++) {
|
|
943
|
+
const item = stops[segmentStart + inner];
|
|
944
|
+
if (item.position === void 0) item.position = start.position + (end.position - start.position) * inner / gap;
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
segmentStart = index;
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
return stops.map((item) => {
|
|
951
|
+
if (item.position === void 0) throw new SyntaxError("Failed to resolve gradient stop position");
|
|
952
|
+
return {
|
|
953
|
+
type: item.type,
|
|
954
|
+
value: item.value,
|
|
955
|
+
position: item.position
|
|
956
|
+
};
|
|
957
|
+
});
|
|
958
|
+
}
|
|
959
|
+
};
|
|
1191
960
|
//#endregion
|
|
1192
|
-
//#region src/
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
961
|
+
//#region src/gradients/LinearGradient.ts
|
|
962
|
+
var LinearGradient = class LinearGradient extends GradientBase {
|
|
963
|
+
type = "linear-gradient";
|
|
964
|
+
constructor(config) {
|
|
965
|
+
super(config);
|
|
966
|
+
}
|
|
967
|
+
static normalizeConfig(value) {
|
|
968
|
+
if (typeof value === "string") {
|
|
969
|
+
const tokens = value.trim().toLowerCase().split(/\s+/).filter(Boolean);
|
|
970
|
+
if (tokens.length === 0) throw new SyntaxError("Linear gradient angle keyword cannot be empty");
|
|
971
|
+
if (tokens[0] !== "to") throw new SyntaxError("Linear gradient keyword direction must start with \"to\"");
|
|
972
|
+
const directions = tokens.slice(1);
|
|
973
|
+
if (directions.length === 0 || directions.length > 2) throw new SyntaxError("Linear gradient keyword direction must contain one or two direction tokens");
|
|
974
|
+
const allowed = new Set([
|
|
975
|
+
"top",
|
|
976
|
+
"right",
|
|
977
|
+
"bottom",
|
|
978
|
+
"left"
|
|
979
|
+
]);
|
|
980
|
+
for (const direction of directions) if (!allowed.has(direction)) throw new SyntaxError(`Invalid linear gradient direction token: "${direction}"`);
|
|
981
|
+
if (new Set(directions).size !== directions.length) throw new SyntaxError("Linear gradient keyword direction cannot contain duplicate tokens");
|
|
982
|
+
const hasTop = directions.includes("top");
|
|
983
|
+
const hasRight = directions.includes("right");
|
|
984
|
+
const hasBottom = directions.includes("bottom");
|
|
985
|
+
const hasLeft = directions.includes("left");
|
|
986
|
+
if (hasTop && hasBottom || hasLeft && hasRight) throw new SyntaxError("Linear gradient keyword direction contains conflicting tokens");
|
|
987
|
+
if (hasTop && hasLeft) return { angle: degToRad(315) };
|
|
988
|
+
else if (hasTop && hasRight) return { angle: degToRad(45) };
|
|
989
|
+
else if (hasBottom && hasLeft) return { angle: degToRad(225) };
|
|
990
|
+
else if (hasBottom && hasRight) return { angle: degToRad(135) };
|
|
991
|
+
else if (hasTop) return { angle: degToRad(0) };
|
|
992
|
+
else if (hasRight) return { angle: degToRad(90) };
|
|
993
|
+
else if (hasBottom) return { angle: degToRad(180) };
|
|
994
|
+
else if (hasLeft) return { angle: degToRad(270) };
|
|
995
|
+
throw new SyntaxError(`Unsupported linear gradient keyword direction: "${value}"`);
|
|
996
|
+
}
|
|
997
|
+
switch (value.unit) {
|
|
998
|
+
case "deg": return { angle: normalizeAngleRad(degToRad(value.value)) };
|
|
999
|
+
case "rad": return { angle: normalizeAngleRad(value.value) };
|
|
1000
|
+
case "turn": return { angle: normalizeAngleRad(turnToRad(value.value)) };
|
|
1001
|
+
case "grad": return { angle: normalizeAngleRad(gradToRad(value.value)) };
|
|
1002
|
+
default: throw new SyntaxError(`Unsupported angle unit: "${value.unit}"`);
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
static fromString(input) {
|
|
1006
|
+
return LinearGradient.fromAbi(parseStringToAbi(input));
|
|
1007
|
+
}
|
|
1008
|
+
static fromAbi(abi) {
|
|
1009
|
+
let config = { angle: 0 };
|
|
1010
|
+
if (abi.inputs[0].type === "config") {
|
|
1011
|
+
const inputValue = abi.inputs[0].value.trim().toLowerCase();
|
|
1012
|
+
if (inputValue.length === 0) throw new SyntaxError("Linear gradient config cannot be empty");
|
|
1013
|
+
if (inputValue.startsWith("to ")) config = LinearGradient.normalizeConfig(inputValue);
|
|
1014
|
+
else {
|
|
1015
|
+
const match = inputValue.match(/^([+-]?(?:\d+\.?\d*|\.\d+))(deg|rad|turn|grad)$/i);
|
|
1016
|
+
if (match === null) throw new SyntaxError(`Invalid linear gradient angle: "${inputValue}"`);
|
|
1017
|
+
const rawValue = Number(match[1]);
|
|
1018
|
+
const unit = match[2].toLowerCase();
|
|
1019
|
+
if (!Number.isFinite(rawValue)) throw new SyntaxError(`Invalid linear gradient angle value: "${inputValue}"`);
|
|
1020
|
+
config = LinearGradient.normalizeConfig({
|
|
1021
|
+
value: rawValue,
|
|
1022
|
+
unit
|
|
1023
|
+
});
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
const inputsWithoutConfig = abi.inputs[0]?.type === "config" ? abi.inputs.slice(1) : abi.inputs;
|
|
1027
|
+
const stops = LinearGradient._normalizeAbiInputsToStops(inputsWithoutConfig);
|
|
1028
|
+
return new LinearGradient({
|
|
1029
|
+
isRepeating: abi.isRepeating,
|
|
1030
|
+
config,
|
|
1213
1031
|
stops
|
|
1214
|
-
}
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1032
|
+
});
|
|
1033
|
+
}
|
|
1034
|
+
clone() {
|
|
1035
|
+
return new LinearGradient(this.toJSON());
|
|
1036
|
+
}
|
|
1037
|
+
toString() {
|
|
1038
|
+
return `${this.isRepeating ? `repeating-${this.type}` : this.type}(${[this.config.angle === 0 ? "" : `${roundTo(radToDeg(this.config.angle), 3)}deg`, ...this._serializeStopsCompact()].filter(Boolean).join(", ")})`;
|
|
1039
|
+
}
|
|
1040
|
+
_validateConfig(_) {}
|
|
1041
|
+
};
|
|
1042
|
+
//#endregion
|
|
1043
|
+
//#region src/gradients/RadialGradient.ts
|
|
1044
|
+
var RadialGradient = class RadialGradient extends GradientBase {
|
|
1045
|
+
type = "radial-gradient";
|
|
1046
|
+
constructor(config) {
|
|
1047
|
+
super(config);
|
|
1048
|
+
}
|
|
1049
|
+
static fromString(input) {
|
|
1050
|
+
return RadialGradient.fromAbi(parseStringToAbi(input));
|
|
1051
|
+
}
|
|
1052
|
+
static fromAbi(abi) {
|
|
1053
|
+
if (abi.functionName !== "radial-gradient") throw new Error("Invalid function name for RadialGradient");
|
|
1054
|
+
const config = this._parseConfig(abi.inputs);
|
|
1055
|
+
const inputsWithoutConfig = abi.inputs[0]?.type === "config" ? abi.inputs.slice(1) : abi.inputs;
|
|
1056
|
+
const stops = this._normalizeAbiInputsToStops(inputsWithoutConfig);
|
|
1057
|
+
return new RadialGradient({
|
|
1058
|
+
isRepeating: abi.isRepeating,
|
|
1059
|
+
config,
|
|
1060
|
+
stops
|
|
1061
|
+
});
|
|
1062
|
+
}
|
|
1063
|
+
clone() {
|
|
1064
|
+
return new RadialGradient(this.toJSON());
|
|
1065
|
+
}
|
|
1066
|
+
toString() {
|
|
1067
|
+
return `${this.isRepeating ? `repeating-${this.type}` : this.type}(${[this._serializeRadialConfig(this.config), ...this._serializeStopsCompact()].filter(Boolean).join(", ")})`;
|
|
1068
|
+
}
|
|
1069
|
+
_validateConfig(config) {
|
|
1070
|
+
if (config.shape !== "circle" && config.shape !== "ellipse") throw new Error("Invalid shape");
|
|
1071
|
+
if (!config.position) throw new Error("Position is required");
|
|
1072
|
+
if (!config.size) throw new Error("Size is required");
|
|
1073
|
+
}
|
|
1074
|
+
_serializeRadialConfig(config) {
|
|
1075
|
+
const parts = [];
|
|
1076
|
+
parts.push(config.shape);
|
|
1077
|
+
if (config.size.kind === "extent") parts.push(config.size.value);
|
|
1078
|
+
else {
|
|
1079
|
+
const x = this._formatLengthPercentage(config.size.x);
|
|
1080
|
+
const y = config.size.y ? ` ${this._formatLengthPercentage(config.size.y)}` : "";
|
|
1081
|
+
parts.push(`${x}${y}`);
|
|
1082
|
+
}
|
|
1083
|
+
parts.push(`at ${this._serializePosition(config.position)}`);
|
|
1084
|
+
if (config.interpolation) if (config.interpolation.kind === "rectangular") parts.push(`in ${config.interpolation.space}`);
|
|
1085
|
+
else {
|
|
1086
|
+
let str = `in ${config.interpolation.space}`;
|
|
1087
|
+
if (config.interpolation.hueMethod) str += ` ${config.interpolation.hueMethod} hue`;
|
|
1088
|
+
parts.push(str);
|
|
1089
|
+
}
|
|
1090
|
+
return parts.join(" ");
|
|
1091
|
+
}
|
|
1092
|
+
_serializePosition(position) {
|
|
1093
|
+
if (position.kind === "keywords") return `${position.x} ${position.y}`;
|
|
1094
|
+
const x = this._formatLengthPercentage(position.x);
|
|
1095
|
+
const y = position.y ? this._formatLengthPercentage(position.y) : "";
|
|
1096
|
+
return y === "" ? x : `${x} ${y}`;
|
|
1097
|
+
}
|
|
1098
|
+
_formatLengthPercentage(value) {
|
|
1099
|
+
if (value.kind === "percent") return `${value.value}%`;
|
|
1100
|
+
return `${value.value}${value.unit}`;
|
|
1101
|
+
}
|
|
1102
|
+
static _parseConfig(inputs) {
|
|
1103
|
+
let shape = "ellipse";
|
|
1104
|
+
let size = {
|
|
1105
|
+
kind: "extent",
|
|
1106
|
+
value: "farthest-corner"
|
|
1107
|
+
};
|
|
1108
|
+
let position = {
|
|
1109
|
+
kind: "keywords",
|
|
1110
|
+
x: "center",
|
|
1111
|
+
y: "center"
|
|
1112
|
+
};
|
|
1113
|
+
for (const input of inputs) {
|
|
1114
|
+
if (input.type !== "config") continue;
|
|
1115
|
+
const tokens = splitTopLevelByWhitespace(input.value);
|
|
1116
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
1117
|
+
const t = tokens[i];
|
|
1118
|
+
if (t === "circle" || t === "ellipse") {
|
|
1119
|
+
shape = t;
|
|
1120
|
+
continue;
|
|
1121
|
+
}
|
|
1122
|
+
if (t === "closest-side" || t === "closest-corner" || t === "farthest-side" || t === "farthest-corner") {
|
|
1123
|
+
size = {
|
|
1124
|
+
kind: "extent",
|
|
1125
|
+
value: t
|
|
1126
|
+
};
|
|
1127
|
+
continue;
|
|
1128
|
+
}
|
|
1129
|
+
if (t === "at") {
|
|
1130
|
+
const xToken = tokens[i + 1];
|
|
1131
|
+
const yToken = tokens[i + 2];
|
|
1132
|
+
if ((xToken === "left" || xToken === "center" || xToken === "right") && (yToken === "top" || yToken === "center" || yToken === "bottom")) position = {
|
|
1133
|
+
kind: "keywords",
|
|
1134
|
+
x: xToken,
|
|
1135
|
+
y: yToken
|
|
1136
|
+
};
|
|
1137
|
+
else position = {
|
|
1138
|
+
kind: "values",
|
|
1139
|
+
x: this._parseLengthPercentage(xToken),
|
|
1140
|
+
y: this._parseLengthPercentage(yToken)
|
|
1141
|
+
};
|
|
1142
|
+
i += 2;
|
|
1143
|
+
continue;
|
|
1144
|
+
}
|
|
1145
|
+
}
|
|
1146
|
+
}
|
|
1147
|
+
return {
|
|
1148
|
+
shape,
|
|
1149
|
+
size,
|
|
1233
1150
|
position
|
|
1234
|
-
}
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
}
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
}
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
const
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1151
|
+
};
|
|
1152
|
+
}
|
|
1153
|
+
static _parseLengthPercentage(input) {
|
|
1154
|
+
if (input.endsWith("%")) return {
|
|
1155
|
+
kind: "percent",
|
|
1156
|
+
value: parseFloat(input)
|
|
1157
|
+
};
|
|
1158
|
+
const match = input.match(/^(-?\d*\.?\d+)([a-zA-Z]+)$/);
|
|
1159
|
+
if (!match) throw new Error(`Invalid length-percentage: ${input}`);
|
|
1160
|
+
return {
|
|
1161
|
+
kind: "length",
|
|
1162
|
+
value: parseFloat(match[1]),
|
|
1163
|
+
unit: match[2]
|
|
1164
|
+
};
|
|
1165
|
+
}
|
|
1166
|
+
};
|
|
1167
|
+
//#endregion
|
|
1168
|
+
//#region src/gradients/ConicGradient.ts
|
|
1169
|
+
var ConicGradient = class ConicGradient extends GradientBase {
|
|
1170
|
+
type = "conic-gradient";
|
|
1171
|
+
constructor(config) {
|
|
1172
|
+
super(config);
|
|
1173
|
+
}
|
|
1174
|
+
clone() {
|
|
1175
|
+
return new ConicGradient(this.toJSON());
|
|
1176
|
+
}
|
|
1177
|
+
toString() {
|
|
1178
|
+
return `${this.isRepeating ? `repeating-${this.type}` : this.type}(${[this._serializeConfig(), ...this._serializeStopsCompact()].filter(Boolean).join(", ")})`;
|
|
1179
|
+
}
|
|
1180
|
+
static fromString(input) {
|
|
1181
|
+
return this.fromAbi(parseStringToAbi(input));
|
|
1182
|
+
}
|
|
1183
|
+
static fromAbi(abi) {
|
|
1184
|
+
if (abi.functionName !== "conic-gradient") throw new Error("Invalid function name for ConicGradient");
|
|
1185
|
+
const configInput = abi.inputs.find((input) => input.type === "config");
|
|
1186
|
+
const inputsWithoutConfig = abi.inputs.filter((input) => input.type !== "config");
|
|
1187
|
+
const config = this._parseConfig(configInput?.value);
|
|
1188
|
+
const stops = this._normalizeAbiInputsToStops(inputsWithoutConfig);
|
|
1189
|
+
return new ConicGradient({
|
|
1190
|
+
isRepeating: abi.isRepeating,
|
|
1191
|
+
config,
|
|
1192
|
+
stops
|
|
1193
|
+
});
|
|
1194
|
+
}
|
|
1195
|
+
_validateConfig(config) {}
|
|
1196
|
+
_serializeConfig() {
|
|
1197
|
+
const parts = [];
|
|
1198
|
+
const angle = this.config.from;
|
|
1199
|
+
parts.push(`from ${angle.value}${angle.unit}`);
|
|
1200
|
+
parts.push(`at ${this._serializePosition(this.config.position)}`);
|
|
1201
|
+
if (this.config.interpolation) {
|
|
1202
|
+
const i = this.config.interpolation;
|
|
1203
|
+
if (i.kind === "rectangular") parts.push(`in ${i.space}`);
|
|
1204
|
+
else {
|
|
1205
|
+
let str = `in ${i.space}`;
|
|
1206
|
+
if (i.hueMethod) str += ` ${i.hueMethod} hue`;
|
|
1207
|
+
parts.push(str);
|
|
1208
|
+
}
|
|
1289
1209
|
}
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1210
|
+
return parts.join(" ");
|
|
1211
|
+
}
|
|
1212
|
+
_serializePosition(position) {
|
|
1213
|
+
if (position.kind === "keywords") return `${position.x} ${position.y}`;
|
|
1214
|
+
const x = this._formatLengthPercentage(position.x);
|
|
1215
|
+
const y = position.y ? this._formatLengthPercentage(position.y) : "";
|
|
1216
|
+
return y === "" ? x : `${x} ${y}`;
|
|
1217
|
+
}
|
|
1218
|
+
_formatLengthPercentage(value) {
|
|
1219
|
+
if (value.kind === "percent") return `${value.value}%`;
|
|
1220
|
+
return `${value.value}${value.unit}`;
|
|
1221
|
+
}
|
|
1222
|
+
static _parseConfig(input) {
|
|
1223
|
+
const config = {
|
|
1224
|
+
from: {
|
|
1225
|
+
kind: "angle",
|
|
1226
|
+
value: 0,
|
|
1227
|
+
unit: "deg"
|
|
1228
|
+
},
|
|
1229
|
+
position: {
|
|
1230
|
+
kind: "keywords",
|
|
1231
|
+
x: "center",
|
|
1232
|
+
y: "center"
|
|
1233
|
+
}
|
|
1234
|
+
};
|
|
1235
|
+
if (!input) return config;
|
|
1236
|
+
const tokens = splitTopLevelByWhitespace(input);
|
|
1237
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
1238
|
+
const token = tokens[i];
|
|
1239
|
+
if (token === "from") {
|
|
1240
|
+
config.from = this._parseAngle(tokens[i + 1]);
|
|
1241
|
+
i += 1;
|
|
1242
|
+
continue;
|
|
1243
|
+
}
|
|
1244
|
+
if (token === "at") {
|
|
1245
|
+
const xToken = tokens[i + 1];
|
|
1246
|
+
const yToken = tokens[i + 2];
|
|
1247
|
+
if ((xToken === "left" || xToken === "center" || xToken === "right") && (yToken === "top" || yToken === "center" || yToken === "bottom")) config.position = {
|
|
1248
|
+
kind: "keywords",
|
|
1249
|
+
x: xToken,
|
|
1250
|
+
y: yToken
|
|
1251
|
+
};
|
|
1252
|
+
else config.position = {
|
|
1253
|
+
kind: "values",
|
|
1254
|
+
x: this._parseLengthPercentage(xToken),
|
|
1255
|
+
y: this._parseLengthPercentage(yToken)
|
|
1256
|
+
};
|
|
1257
|
+
i += 2;
|
|
1258
|
+
continue;
|
|
1259
|
+
}
|
|
1260
|
+
if (token === "in") {
|
|
1261
|
+
const space = tokens[i + 1];
|
|
1262
|
+
const hueMethod = tokens[i + 2];
|
|
1263
|
+
if (tokens[i + 3] === "hue" && (hueMethod === "shorter" || hueMethod === "longer" || hueMethod === "increasing" || hueMethod === "decreasing")) {
|
|
1264
|
+
config.interpolation = {
|
|
1265
|
+
kind: "polar",
|
|
1266
|
+
space,
|
|
1267
|
+
hueMethod
|
|
1268
|
+
};
|
|
1269
|
+
i += 3;
|
|
1270
|
+
continue;
|
|
1271
|
+
}
|
|
1272
|
+
config.interpolation = {
|
|
1273
|
+
kind: "rectangular",
|
|
1274
|
+
space
|
|
1275
|
+
};
|
|
1276
|
+
i += 1;
|
|
1277
|
+
}
|
|
1294
1278
|
}
|
|
1295
|
-
|
|
1279
|
+
return config;
|
|
1296
1280
|
}
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
}
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1281
|
+
static _parseLengthPercentage(input) {
|
|
1282
|
+
if (input.endsWith("%")) return {
|
|
1283
|
+
kind: "percent",
|
|
1284
|
+
value: parseFloat(input)
|
|
1285
|
+
};
|
|
1286
|
+
const match = input.match(/^(-?\d*\.?\d+)([a-zA-Z]+)$/);
|
|
1287
|
+
if (!match) throw new Error(`Invalid length-percentage: ${input}`);
|
|
1288
|
+
return {
|
|
1289
|
+
kind: "length",
|
|
1290
|
+
value: parseFloat(match[1]),
|
|
1291
|
+
unit: match[2]
|
|
1292
|
+
};
|
|
1293
|
+
}
|
|
1294
|
+
static _parseAngle(input) {
|
|
1295
|
+
const match = input.match(/^(-?\d*\.?\d+)(deg|rad|turn|grad)$/);
|
|
1296
|
+
if (!match) throw new Error(`Invalid angle: ${input}`);
|
|
1297
|
+
return {
|
|
1298
|
+
kind: "angle",
|
|
1299
|
+
value: Number(match[1]),
|
|
1300
|
+
unit: match[2]
|
|
1301
|
+
};
|
|
1302
|
+
}
|
|
1303
|
+
};
|
|
1304
|
+
//#endregion
|
|
1305
|
+
//#region src/gradient-transformer/modules/css/ModuleTransformerLinearGradientToCss.ts
|
|
1306
|
+
var ModuleTransformerLinearGradientToCss = class {
|
|
1307
|
+
target = "css";
|
|
1308
|
+
gradientType = "linear-gradient";
|
|
1309
|
+
to(input) {
|
|
1310
|
+
if (!(input instanceof LinearGradient)) throw new Error("Expected LinearGradient");
|
|
1311
|
+
return input.toString();
|
|
1312
|
+
}
|
|
1313
|
+
};
|
|
1314
|
+
//#endregion
|
|
1315
|
+
//#region src/gradient-transformer/modules/css/ModuleTransformerRadialGradientToCss.ts
|
|
1316
|
+
var ModuleTransformerRadialGradientToCss = class {
|
|
1317
|
+
target = "css";
|
|
1318
|
+
gradientType = "radial-gradient";
|
|
1319
|
+
to(input) {
|
|
1320
|
+
if (!(input instanceof RadialGradient)) throw new Error("Expected RadialGradient");
|
|
1321
|
+
return input.toString();
|
|
1322
|
+
}
|
|
1323
|
+
};
|
|
1324
|
+
//#endregion
|
|
1325
|
+
//#region src/gradient-transformer/modules/css/ModuleTransformerConicGradientToCss.ts
|
|
1326
|
+
var ModuleTransformerConicGradientToCss = class {
|
|
1327
|
+
target = "css";
|
|
1328
|
+
gradientType = "conic-gradient";
|
|
1329
|
+
to(input) {
|
|
1330
|
+
if (!(input instanceof ConicGradient)) throw new Error("Expected ConicGradient");
|
|
1331
|
+
return input.toString();
|
|
1332
|
+
}
|
|
1333
|
+
};
|
|
1334
|
+
//#endregion
|
|
1335
|
+
//#region src/gradient-transformer/modules/canvas/ModuleTransformerLinearGradientToCanvas.ts
|
|
1336
|
+
const toRgb$2 = converter("rgb");
|
|
1337
|
+
function toCanvasColor$1(input) {
|
|
1338
|
+
const color = toRgb$2(input);
|
|
1339
|
+
if (!color) throw new Error(`Failed to convert color: ${input}`);
|
|
1340
|
+
return formatRgb(color);
|
|
1341
|
+
}
|
|
1342
|
+
function getStopRange$1(stops) {
|
|
1343
|
+
const colorStops = stops.filter((stop) => stop.type === "color-stop" && stop.position != null);
|
|
1344
|
+
if (!colorStops.length) return {
|
|
1345
|
+
min: 0,
|
|
1346
|
+
max: 1,
|
|
1347
|
+
stops: []
|
|
1306
1348
|
};
|
|
1307
|
-
}
|
|
1308
|
-
function createDefaultConicFrom() {
|
|
1309
|
-
return createConicFromNode(0, "deg", 0);
|
|
1310
|
-
}
|
|
1311
|
-
function createDefaultConicPosition() {
|
|
1312
1349
|
return {
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1350
|
+
min: Math.min(...colorStops.map((stop) => stop.position)),
|
|
1351
|
+
max: Math.max(...colorStops.map((stop) => stop.position)),
|
|
1352
|
+
stops: colorStops
|
|
1353
|
+
};
|
|
1354
|
+
}
|
|
1355
|
+
function normalizeStops$1(stops, min, max) {
|
|
1356
|
+
const range = max - min || 1;
|
|
1357
|
+
return stops.filter((stop) => stop.type === "color-stop" && stop.position != null).map((stop) => ({
|
|
1358
|
+
...stop,
|
|
1359
|
+
position: (stop.position - min) / range
|
|
1360
|
+
}));
|
|
1361
|
+
}
|
|
1362
|
+
var ModuleTransformerLinearGradientToCanvas = class {
|
|
1363
|
+
target = "canvas";
|
|
1364
|
+
gradientType = "linear-gradient";
|
|
1365
|
+
to(input) {
|
|
1366
|
+
const gradient = input;
|
|
1367
|
+
return { draw: (ctx, width, height) => {
|
|
1368
|
+
const angle = gradient.config.angle;
|
|
1369
|
+
const cx = width / 2;
|
|
1370
|
+
const cy = height / 2;
|
|
1371
|
+
const half = Math.max(width, height) / 2;
|
|
1372
|
+
const dx = Math.sin(angle) * half;
|
|
1373
|
+
const dy = Math.cos(angle) * half;
|
|
1374
|
+
const x0 = cx - dx;
|
|
1375
|
+
const y0 = cy - dy;
|
|
1376
|
+
const x1 = cx + dx;
|
|
1377
|
+
const y1 = cy + dy;
|
|
1378
|
+
const { min, max, stops } = getStopRange$1(gradient.stops);
|
|
1379
|
+
let startX = x0;
|
|
1380
|
+
let startY = y0;
|
|
1381
|
+
let endX = x1;
|
|
1382
|
+
let endY = y1;
|
|
1383
|
+
let normalizedStops = stops;
|
|
1384
|
+
if (min < 0 || max > 1) {
|
|
1385
|
+
const vx = x1 - x0;
|
|
1386
|
+
const vy = y1 - y0;
|
|
1387
|
+
startX = x0 + vx * min;
|
|
1388
|
+
startY = y0 + vy * min;
|
|
1389
|
+
endX = x0 + vx * max;
|
|
1390
|
+
endY = y0 + vy * max;
|
|
1391
|
+
normalizedStops = normalizeStops$1(stops, min, max);
|
|
1392
|
+
}
|
|
1393
|
+
const canvasGradient = ctx.createLinearGradient(startX, startY, endX, endY);
|
|
1394
|
+
for (const stop of normalizedStops) canvasGradient.addColorStop(stop.position, toCanvasColor$1(stop.value));
|
|
1395
|
+
ctx.fillStyle = canvasGradient;
|
|
1396
|
+
ctx.fillRect(0, 0, width, height);
|
|
1397
|
+
} };
|
|
1398
|
+
}
|
|
1399
|
+
};
|
|
1400
|
+
//#endregion
|
|
1401
|
+
//#region src/gradient-transformer/modules/canvas/ModuleTransformerRadialGradientToCanvas.ts
|
|
1402
|
+
const toRgb$1 = converter("rgb");
|
|
1403
|
+
function toCanvasColor(input) {
|
|
1404
|
+
const color = toRgb$1(input);
|
|
1405
|
+
if (!color) throw new Error(`Failed to convert color: ${input}`);
|
|
1406
|
+
return formatRgb(color);
|
|
1407
|
+
}
|
|
1408
|
+
function getStopRange(stops) {
|
|
1409
|
+
const colorStops = stops.filter((stop) => stop.type === "color-stop" && stop.position != null);
|
|
1410
|
+
if (!colorStops.length) return {
|
|
1411
|
+
min: 0,
|
|
1412
|
+
max: 1,
|
|
1413
|
+
stops: []
|
|
1323
1414
|
};
|
|
1324
|
-
}
|
|
1325
|
-
function createConicFromNode(value, unit, normalizedRad) {
|
|
1326
1415
|
return {
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1416
|
+
min: Math.min(...colorStops.map((stop) => stop.position)),
|
|
1417
|
+
max: Math.max(...colorStops.map((stop) => stop.position)),
|
|
1418
|
+
stops: colorStops
|
|
1419
|
+
};
|
|
1420
|
+
}
|
|
1421
|
+
function normalizeStops(stops, min, max) {
|
|
1422
|
+
const range = max - min || 1;
|
|
1423
|
+
return stops.map((stop) => ({
|
|
1424
|
+
...stop,
|
|
1425
|
+
position: (stop.position - min) / range
|
|
1426
|
+
}));
|
|
1427
|
+
}
|
|
1428
|
+
var ModuleTransformerRadialGradientToCanvas = class {
|
|
1429
|
+
target = "canvas";
|
|
1430
|
+
gradientType = "radial-gradient";
|
|
1431
|
+
to(input) {
|
|
1432
|
+
const gradient = input;
|
|
1433
|
+
return { draw: (ctx, width, height) => {
|
|
1434
|
+
const pos = gradient.config.position;
|
|
1435
|
+
let x = width / 2;
|
|
1436
|
+
let y = height / 2;
|
|
1437
|
+
if (pos.kind === "values") {
|
|
1438
|
+
x = this._resolve(pos.x, width);
|
|
1439
|
+
y = this._resolve(pos.y, height);
|
|
1440
|
+
}
|
|
1441
|
+
const dx = Math.max(x, width - x);
|
|
1442
|
+
const dy = Math.max(y, height - y);
|
|
1443
|
+
const radius = Math.sqrt(dx * dx + dy * dy);
|
|
1444
|
+
const { min, max, stops } = getStopRange(gradient.stops);
|
|
1445
|
+
let innerRadius = 0;
|
|
1446
|
+
let outerRadius = radius;
|
|
1447
|
+
let normalizedStops = stops;
|
|
1448
|
+
if (min >= 0 && (min < 0 || max > 1)) {
|
|
1449
|
+
innerRadius = radius * min;
|
|
1450
|
+
outerRadius = radius * max;
|
|
1451
|
+
normalizedStops = normalizeStops(stops, min, max);
|
|
1452
|
+
} else if (max > 1) {
|
|
1453
|
+
outerRadius = radius * max;
|
|
1454
|
+
normalizedStops = normalizeStops(stops, min, max);
|
|
1455
|
+
}
|
|
1456
|
+
const g = ctx.createRadialGradient(x, y, innerRadius, x, y, outerRadius);
|
|
1457
|
+
for (const stop of normalizedStops) g.addColorStop(stop.position, toCanvasColor(stop.value));
|
|
1458
|
+
ctx.fillStyle = g;
|
|
1459
|
+
ctx.fillRect(0, 0, width, height);
|
|
1460
|
+
} };
|
|
1461
|
+
}
|
|
1462
|
+
_resolve(value, size) {
|
|
1463
|
+
if (value.kind === "percent") return value.value / 100 * size;
|
|
1464
|
+
if (value.unit === "px") return value.value;
|
|
1465
|
+
return value.value;
|
|
1466
|
+
}
|
|
1467
|
+
};
|
|
1468
|
+
//#endregion
|
|
1469
|
+
//#region src/gradient-transformer/modules/canvas/ModuleTransformerConicGradientToCanvas.ts
|
|
1470
|
+
const toRgb = converter("rgb");
|
|
1471
|
+
var ModuleTransformerConicGradientToCanvas = class {
|
|
1472
|
+
target = "canvas";
|
|
1473
|
+
gradientType = "conic-gradient";
|
|
1474
|
+
to(input) {
|
|
1475
|
+
const gradient = input;
|
|
1476
|
+
return { draw: (ctx, width, height) => {
|
|
1477
|
+
const imageData = ctx.createImageData(width, height);
|
|
1478
|
+
const data = imageData.data;
|
|
1479
|
+
const { x: cx, y: cy } = this._resolvePosition(gradient.config.position, width, height);
|
|
1480
|
+
const from = this._toRad(gradient.config.from);
|
|
1481
|
+
const stops = this._normalizeStops(gradient.stops);
|
|
1482
|
+
if (stops.length === 0) {
|
|
1483
|
+
ctx.putImageData(imageData, 0, 0);
|
|
1484
|
+
return;
|
|
1485
|
+
}
|
|
1486
|
+
for (let y = 0; y < height; y++) for (let x = 0; x < width; x++) {
|
|
1487
|
+
const dx = x - cx;
|
|
1488
|
+
const dy = y - cy;
|
|
1489
|
+
let angle = Math.atan2(dy, dx) + Math.PI / 2 - from;
|
|
1490
|
+
while (angle < 0) angle += Math.PI * 2;
|
|
1491
|
+
while (angle >= Math.PI * 2) angle -= Math.PI * 2;
|
|
1492
|
+
const t = angle / (Math.PI * 2);
|
|
1493
|
+
const color = this._sampleColor(stops, t);
|
|
1494
|
+
const index = (y * width + x) * 4;
|
|
1495
|
+
data[index] = color.r;
|
|
1496
|
+
data[index + 1] = color.g;
|
|
1497
|
+
data[index + 2] = color.b;
|
|
1498
|
+
data[index + 3] = color.a;
|
|
1499
|
+
}
|
|
1500
|
+
ctx.putImageData(imageData, 0, 0);
|
|
1501
|
+
} };
|
|
1502
|
+
}
|
|
1503
|
+
_resolvePosition(position, width, height) {
|
|
1504
|
+
if (position.kind === "keywords") return {
|
|
1505
|
+
x: this._resolveKeywordX(position.x, width),
|
|
1506
|
+
y: this._resolveKeywordY(position.y, height)
|
|
1507
|
+
};
|
|
1508
|
+
return {
|
|
1509
|
+
x: this._resolve(position.x, width),
|
|
1510
|
+
y: this._resolve(position.y, height)
|
|
1511
|
+
};
|
|
1512
|
+
}
|
|
1513
|
+
_resolve(value, size) {
|
|
1514
|
+
if (value.kind === "percent") return value.value / 100 * size;
|
|
1515
|
+
if (value.unit === "px") return value.value;
|
|
1516
|
+
return value.value;
|
|
1517
|
+
}
|
|
1518
|
+
_resolveKeywordX(value, width) {
|
|
1519
|
+
if (value === "left") return 0;
|
|
1520
|
+
if (value === "right") return width;
|
|
1521
|
+
return width / 2;
|
|
1522
|
+
}
|
|
1523
|
+
_resolveKeywordY(value, height) {
|
|
1524
|
+
if (value === "top") return 0;
|
|
1525
|
+
if (value === "bottom") return height;
|
|
1526
|
+
return height / 2;
|
|
1527
|
+
}
|
|
1528
|
+
_toRad(angle) {
|
|
1529
|
+
if (angle.unit === "deg") return degToRad(angle.value);
|
|
1530
|
+
if (angle.unit === "turn") return turnToRad(angle.value);
|
|
1531
|
+
if (angle.unit === "grad") return gradToRad(angle.value);
|
|
1532
|
+
return angle.value;
|
|
1533
|
+
}
|
|
1534
|
+
_normalizeStops(stops) {
|
|
1535
|
+
return stops.filter((stop) => stop.type === "color-stop" && stop.position != null).map((stop) => ({
|
|
1536
|
+
position: this._clamp01(stop.position),
|
|
1537
|
+
color: this._parseColor(stop.value)
|
|
1538
|
+
})).sort((a, b) => a.position - b.position);
|
|
1539
|
+
}
|
|
1540
|
+
_sampleColor(stops, t) {
|
|
1541
|
+
if (stops.length === 1) return stops[0].color;
|
|
1542
|
+
const first = stops[0];
|
|
1543
|
+
const extended = [...stops, {
|
|
1544
|
+
...first,
|
|
1545
|
+
position: first.position + 1
|
|
1546
|
+
}];
|
|
1547
|
+
let sampleT = t;
|
|
1548
|
+
if (sampleT < first.position) sampleT += 1;
|
|
1549
|
+
for (let i = 0; i < extended.length - 1; i++) {
|
|
1550
|
+
const left = extended[i];
|
|
1551
|
+
const right = extended[i + 1];
|
|
1552
|
+
if (sampleT >= left.position && sampleT <= right.position) {
|
|
1553
|
+
const span = right.position - left.position || 1;
|
|
1554
|
+
const localT = (sampleT - left.position) / span;
|
|
1555
|
+
return this._mixColor(left.color, right.color, localT);
|
|
1556
|
+
}
|
|
1337
1557
|
}
|
|
1338
|
-
|
|
1558
|
+
return stops[stops.length - 1].color;
|
|
1559
|
+
}
|
|
1560
|
+
_mixColor(from, to, t) {
|
|
1561
|
+
return {
|
|
1562
|
+
r: Math.round(from.r + (to.r - from.r) * t),
|
|
1563
|
+
g: Math.round(from.g + (to.g - from.g) * t),
|
|
1564
|
+
b: Math.round(from.b + (to.b - from.b) * t),
|
|
1565
|
+
a: Math.round(from.a + (to.a - from.a) * t)
|
|
1566
|
+
};
|
|
1567
|
+
}
|
|
1568
|
+
_parseColor(input) {
|
|
1569
|
+
const color = toRgb(input);
|
|
1570
|
+
if (!color) throw new Error(`Failed to convert color: ${input}`);
|
|
1571
|
+
return {
|
|
1572
|
+
r: Math.round((color.r ?? 0) * 255),
|
|
1573
|
+
g: Math.round((color.g ?? 0) * 255),
|
|
1574
|
+
b: Math.round((color.b ?? 0) * 255),
|
|
1575
|
+
a: Math.round((color.alpha ?? 1) * 255)
|
|
1576
|
+
};
|
|
1577
|
+
}
|
|
1578
|
+
_clamp01(value) {
|
|
1579
|
+
return Math.max(0, Math.min(1, value));
|
|
1580
|
+
}
|
|
1581
|
+
};
|
|
1582
|
+
//#endregion
|
|
1583
|
+
//#region src/gradient-transformer/GradientTransformer.ts
|
|
1584
|
+
var GradientTransformer = class {
|
|
1585
|
+
static _modules = /* @__PURE__ */ new Map();
|
|
1586
|
+
static _initialized = false;
|
|
1587
|
+
static add(module) {
|
|
1588
|
+
this._ensureInitialized();
|
|
1589
|
+
this._modules.set(this._getKey(module.target, module.gradientType), module);
|
|
1590
|
+
}
|
|
1591
|
+
static get(target, gradientType) {
|
|
1592
|
+
this._ensureInitialized();
|
|
1593
|
+
return this._modules.get(this._getKey(target, gradientType)) ?? null;
|
|
1594
|
+
}
|
|
1595
|
+
static remove(target, gradientType) {
|
|
1596
|
+
this._ensureInitialized();
|
|
1597
|
+
return this._modules.delete(this._getKey(target, gradientType));
|
|
1598
|
+
}
|
|
1599
|
+
static to(target, input) {
|
|
1600
|
+
const gradient = typeof input === "string" ? GradientFactory.create(input) : input;
|
|
1601
|
+
const module = this.get(target, gradient.type);
|
|
1602
|
+
if (!module) throw new Error(`No transformer registered for target "${target}" and gradient "${gradient.type}"`);
|
|
1603
|
+
return module.to(gradient);
|
|
1604
|
+
}
|
|
1605
|
+
static from(target, gradientType, input) {
|
|
1606
|
+
const module = this.get(target, gradientType);
|
|
1607
|
+
if (!module || !module.from) throw new Error(`No reverse transformer registered for target "${target}" and gradient "${gradientType}"`);
|
|
1608
|
+
return module.from(input);
|
|
1609
|
+
}
|
|
1610
|
+
static _ensureInitialized() {
|
|
1611
|
+
if (this._initialized) return;
|
|
1612
|
+
this._initialized = true;
|
|
1613
|
+
this.add(new ModuleTransformerLinearGradientToCss());
|
|
1614
|
+
this.add(new ModuleTransformerRadialGradientToCss());
|
|
1615
|
+
this.add(new ModuleTransformerConicGradientToCss());
|
|
1616
|
+
this.add(new ModuleTransformerLinearGradientToCanvas());
|
|
1617
|
+
this.add(new ModuleTransformerRadialGradientToCanvas());
|
|
1618
|
+
this.add(new ModuleTransformerConicGradientToCanvas());
|
|
1619
|
+
}
|
|
1620
|
+
static _getKey(target, gradientType) {
|
|
1621
|
+
return `${target}:${gradientType}`;
|
|
1622
|
+
}
|
|
1623
|
+
};
|
|
1624
|
+
//#endregion
|
|
1625
|
+
//#region src/gradients/GradientFactory.ts
|
|
1626
|
+
var GradientFactory = class {
|
|
1627
|
+
static _registry = /* @__PURE__ */ new Map();
|
|
1628
|
+
static _initialized = false;
|
|
1629
|
+
static add(type, value) {
|
|
1630
|
+
this._ensureInitialized();
|
|
1631
|
+
this._registry.set(type, value);
|
|
1632
|
+
}
|
|
1633
|
+
static get(functionName) {
|
|
1634
|
+
this._ensureInitialized();
|
|
1635
|
+
return this._registry.get(functionName) ?? null;
|
|
1636
|
+
}
|
|
1637
|
+
static remove(functionName) {
|
|
1638
|
+
return this._registry.delete(functionName);
|
|
1639
|
+
}
|
|
1640
|
+
static create(input) {
|
|
1641
|
+
const abi = typeof input === "string" ? parseStringToAbi(input) : input;
|
|
1642
|
+
const adapter = this.get(abi.functionName);
|
|
1643
|
+
if (!adapter) throw new Error(`No gradient registered for: ${abi.functionName}`);
|
|
1644
|
+
return adapter.fromAbi(abi);
|
|
1645
|
+
}
|
|
1646
|
+
static isValid(input) {
|
|
1647
|
+
try {
|
|
1648
|
+
this.create(input);
|
|
1649
|
+
return true;
|
|
1650
|
+
} catch {
|
|
1651
|
+
return false;
|
|
1652
|
+
}
|
|
1653
|
+
}
|
|
1654
|
+
static _ensureInitialized() {
|
|
1655
|
+
if (this._initialized) return;
|
|
1656
|
+
this._initialized = true;
|
|
1657
|
+
this.add("linear-gradient", LinearGradient);
|
|
1658
|
+
this.add("radial-gradient", RadialGradient);
|
|
1659
|
+
this.add("conic-gradient", ConicGradient);
|
|
1660
|
+
}
|
|
1661
|
+
};
|
|
1662
|
+
function parse(input) {
|
|
1663
|
+
return GradientFactory.create(input);
|
|
1339
1664
|
}
|
|
1340
|
-
function
|
|
1341
|
-
|
|
1342
|
-
kind: "position",
|
|
1343
|
-
x: {
|
|
1344
|
-
kind: "percentage",
|
|
1345
|
-
value: .5
|
|
1346
|
-
},
|
|
1347
|
-
y: {
|
|
1348
|
-
kind: "percentage",
|
|
1349
|
-
value: .5
|
|
1350
|
-
},
|
|
1351
|
-
keywords: [...keywords]
|
|
1352
|
-
};
|
|
1353
|
-
return {
|
|
1354
|
-
kind: "position",
|
|
1355
|
-
x: x ?? {
|
|
1356
|
-
kind: "percentage",
|
|
1357
|
-
value: .5
|
|
1358
|
-
},
|
|
1359
|
-
y: y ?? {
|
|
1360
|
-
kind: "percentage",
|
|
1361
|
-
value: .5
|
|
1362
|
-
},
|
|
1363
|
-
keywords: [...keywords]
|
|
1364
|
-
};
|
|
1665
|
+
function isGradient(input) {
|
|
1666
|
+
return GradientFactory.isValid(input);
|
|
1365
1667
|
}
|
|
1366
|
-
function
|
|
1367
|
-
if (
|
|
1368
|
-
|
|
1369
|
-
value: token.value
|
|
1370
|
-
};
|
|
1371
|
-
if (token.kind === TokenKind.DIMENSION) return {
|
|
1372
|
-
kind: "dimension",
|
|
1373
|
-
value: token.value,
|
|
1374
|
-
unit: token.unit
|
|
1375
|
-
};
|
|
1376
|
-
return null;
|
|
1668
|
+
function format(input) {
|
|
1669
|
+
if (typeof input === "string") return parse(input).toString();
|
|
1670
|
+
return input.toString();
|
|
1377
1671
|
}
|
|
1378
|
-
function
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
if (!lastPosition || lastPosition.kind !== "percentage") return stops;
|
|
1382
|
-
if (lastPosition.value >= 1) return stops;
|
|
1383
|
-
const percentageSum = roundTo(stops.reduce((sum, stop) => {
|
|
1384
|
-
const position = stop.position;
|
|
1385
|
-
if (!position || position.kind !== "percentage") return sum;
|
|
1386
|
-
return sum + position.value;
|
|
1387
|
-
}, 0), 3);
|
|
1388
|
-
if (percentageSum <= 0) return stops;
|
|
1389
|
-
const newStops = [...stops];
|
|
1390
|
-
let offset = percentageSum;
|
|
1391
|
-
while (true) {
|
|
1392
|
-
let shouldContinue = false;
|
|
1393
|
-
for (const stop of stops) {
|
|
1394
|
-
const position = stop.position;
|
|
1395
|
-
if (!position || position.kind !== "percentage") continue;
|
|
1396
|
-
const nextValue = roundTo(position.value + offset, 3);
|
|
1397
|
-
newStops.push({
|
|
1398
|
-
...stop,
|
|
1399
|
-
position: {
|
|
1400
|
-
kind: "percentage",
|
|
1401
|
-
value: nextValue
|
|
1402
|
-
}
|
|
1403
|
-
});
|
|
1404
|
-
if (nextValue <= 1) shouldContinue = true;
|
|
1405
|
-
}
|
|
1406
|
-
const lastGeneratedPosition = newStops[newStops.length - 1]?.position;
|
|
1407
|
-
if (!lastGeneratedPosition || lastGeneratedPosition.kind !== "percentage" || lastGeneratedPosition.value > 1) break;
|
|
1408
|
-
if (!shouldContinue) break;
|
|
1409
|
-
offset = roundTo(offset + percentageSum, 3);
|
|
1410
|
-
}
|
|
1411
|
-
return newStops;
|
|
1672
|
+
function transformTo(target, input) {
|
|
1673
|
+
const gradient = typeof input === "string" ? parse(input) : input;
|
|
1674
|
+
return GradientTransformer.to(target, gradient);
|
|
1412
1675
|
}
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
function parse(value) {
|
|
1416
|
-
const tokens = tokenize(value);
|
|
1417
|
-
const firstToken = getTokenAt(tokens, 0);
|
|
1418
|
-
if (firstToken === null) throw new Error("Empty input");
|
|
1419
|
-
switch (firstToken.kind) {
|
|
1420
|
-
case TokenKind.FUNCTION_LINEAR_GRADIENT:
|
|
1421
|
-
case TokenKind.FUNCTION_REPEATING_LINEAR_GRADIENT: return parseLinearGradient(value, tokens, 0).node;
|
|
1422
|
-
case TokenKind.FUNCTION_RADIAL_GRADIENT:
|
|
1423
|
-
case TokenKind.FUNCTION_REPEATING_RADIAL_GRADIENT: return parseRadialGradient(value, tokens, 0).node;
|
|
1424
|
-
case TokenKind.FUNCTION_CONIC_GRADIENT:
|
|
1425
|
-
case TokenKind.FUNCTION_REPEATING_CONIC_GRADIENT: return parseConicGradient(value, tokens, 0).node;
|
|
1426
|
-
default: throw new Error(`Unsupported gradient type: ${firstToken.kind}`);
|
|
1427
|
-
}
|
|
1676
|
+
function transformFrom(target, gradientType, input) {
|
|
1677
|
+
return GradientTransformer.from(target, gradientType, input);
|
|
1428
1678
|
}
|
|
1429
1679
|
//#endregion
|
|
1430
|
-
export {
|
|
1680
|
+
export { ConicGradient, GradientBase, GradientFactory, GradientTransformer, LinearGradient, ModuleTransformerConicGradientToCanvas, ModuleTransformerConicGradientToCss, ModuleTransformerLinearGradientToCanvas, ModuleTransformerLinearGradientToCss, ModuleTransformerRadialGradientToCanvas, ModuleTransformerRadialGradientToCss, PatternTokenKind, RadialGradient, ceilTo, clamp, degToRad, floorTo, format, fromPercent, gradToRad, isAngleUnit, isColorHint, isColorStop, isConfig, isGradient, isPatternSyntaxValid, isPatternValid, isValid, matchExpression, normalizeAngle, normalizeAngleDeg, normalizeAngleRad, parse, parseStringToAbi, radToDeg, roundTo, splitTopLevelByWhitespace, toAngleRad, toPercent, tokenizePattern, transformFrom, transformTo, truncTo, turnToRad, validate, validatePattern, validatePatternSemantic, validatePatternStructure, validatePatternSyntax };
|