gradiente 1.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/LICENSE +21 -0
- package/README.md +307 -0
- package/dist/index.d.mts +335 -0
- package/dist/index.mjs +1430 -0
- package/package.json +67 -0
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,1430 @@
|
|
|
1
|
+
//#region src/token.ts
|
|
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
|
|
474
|
+
//#region src/utils/math/base.ts
|
|
475
|
+
function roundTo(value, digits) {
|
|
476
|
+
const factor = 10 ** digits;
|
|
477
|
+
return Math.round(value * factor) / factor;
|
|
478
|
+
}
|
|
479
|
+
function floorTo(value, digits) {
|
|
480
|
+
const factor = 10 ** digits;
|
|
481
|
+
return Math.floor(value * factor) / factor;
|
|
482
|
+
}
|
|
483
|
+
function ceilTo(value, digits) {
|
|
484
|
+
const factor = 10 ** digits;
|
|
485
|
+
return Math.ceil(value * factor) / factor;
|
|
486
|
+
}
|
|
487
|
+
function truncTo(value, digits) {
|
|
488
|
+
const factor = 10 ** digits;
|
|
489
|
+
return Math.trunc(value * factor) / factor;
|
|
490
|
+
}
|
|
491
|
+
function clamp(value, min, max) {
|
|
492
|
+
return Math.min(Math.max(value, min), max);
|
|
493
|
+
}
|
|
494
|
+
function toPercent(value) {
|
|
495
|
+
return value / 100;
|
|
496
|
+
}
|
|
497
|
+
function fromPercent(value) {
|
|
498
|
+
return value * 100;
|
|
499
|
+
}
|
|
500
|
+
//#endregion
|
|
501
|
+
//#region src/utils/math/angle.ts
|
|
502
|
+
function isAngleUnit(unit) {
|
|
503
|
+
return unit === "deg" || unit === "rad" || unit === "turn" || unit === "grad";
|
|
504
|
+
}
|
|
505
|
+
function degToRad(value) {
|
|
506
|
+
return value * Math.PI / 180;
|
|
507
|
+
}
|
|
508
|
+
function radToDeg(value) {
|
|
509
|
+
return value * 180 / Math.PI;
|
|
510
|
+
}
|
|
511
|
+
function turnToRad(value) {
|
|
512
|
+
return value * Math.PI * 2;
|
|
513
|
+
}
|
|
514
|
+
function gradToRad(value) {
|
|
515
|
+
return value * Math.PI / 200;
|
|
516
|
+
}
|
|
517
|
+
function normalizeAngleDeg(value, digits = 3) {
|
|
518
|
+
return roundTo((value % 360 + 360) % 360, digits);
|
|
519
|
+
}
|
|
520
|
+
function normalizeAngleRad(value) {
|
|
521
|
+
const tau = Math.PI * 2;
|
|
522
|
+
return (value % tau + tau) % tau;
|
|
523
|
+
}
|
|
524
|
+
function toAngleRad(value, unit) {
|
|
525
|
+
switch (unit) {
|
|
526
|
+
case "deg": return degToRad(value);
|
|
527
|
+
case "rad": return value;
|
|
528
|
+
case "turn": return turnToRad(value);
|
|
529
|
+
case "grad": return gradToRad(value);
|
|
530
|
+
default: throw new Error(`Unsupported angle unit: ${String(unit)}`);
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
function normalizeAngle(value, unit, digits = 6) {
|
|
534
|
+
return roundTo(normalizeAngleRad(toAngleRad(value, unit)), digits);
|
|
535
|
+
}
|
|
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
|
+
//#endregion
|
|
542
|
+
//#region src/parser/helpers/color-helpers.ts
|
|
543
|
+
function parseColorStop(source, tokens, startIndex) {
|
|
544
|
+
const colorResult = parseColorSource(source, tokens, startIndex);
|
|
545
|
+
let index = colorResult.nextIndex;
|
|
546
|
+
const positions = [];
|
|
547
|
+
while (true) {
|
|
548
|
+
const nextIndex = findNextNonWhitespaceIndex(tokens, index);
|
|
549
|
+
if (nextIndex === -1) break;
|
|
550
|
+
const token = tokens[nextIndex];
|
|
551
|
+
if (token === void 0) break;
|
|
552
|
+
if (token.kind === TokenKind.PERCENTAGE || token.kind === TokenKind.DIMENSION) {
|
|
553
|
+
positions.push(toLengthPercentageNode(token));
|
|
554
|
+
index = nextIndex + 1;
|
|
555
|
+
if (positions.length === 2) break;
|
|
556
|
+
continue;
|
|
557
|
+
}
|
|
558
|
+
break;
|
|
559
|
+
}
|
|
560
|
+
return {
|
|
561
|
+
node: {
|
|
562
|
+
kind: "color-stop",
|
|
563
|
+
color: colorResult.node,
|
|
564
|
+
position: positions[0]
|
|
565
|
+
},
|
|
566
|
+
nextIndex: index
|
|
567
|
+
};
|
|
568
|
+
}
|
|
569
|
+
function toLengthPercentageNode(token) {
|
|
570
|
+
if (token.kind === TokenKind.PERCENTAGE) return {
|
|
571
|
+
kind: "percentage",
|
|
572
|
+
value: roundTo(toPercent(token.value), 5)
|
|
573
|
+
};
|
|
574
|
+
return {
|
|
575
|
+
kind: "dimension",
|
|
576
|
+
value: token.value,
|
|
577
|
+
unit: token.unit
|
|
578
|
+
};
|
|
579
|
+
}
|
|
580
|
+
function parseColorSource(source, tokens, startIndex) {
|
|
581
|
+
const token = tokens[startIndex];
|
|
582
|
+
if (token === void 0) throw new Error("Expected color token");
|
|
583
|
+
if (token.kind === TokenKind.IDENT || token.kind === TokenKind.HASH) return {
|
|
584
|
+
node: token.raw,
|
|
585
|
+
nextIndex: startIndex + 1
|
|
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}"`);
|
|
595
|
+
}
|
|
596
|
+
function findBalancedFunctionEndIndex$1(tokens, startIndex) {
|
|
597
|
+
const functionToken = tokens[startIndex];
|
|
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");
|
|
601
|
+
let depth = 0;
|
|
602
|
+
for (let index = startIndex + 1; index < tokens.length; index += 1) {
|
|
603
|
+
const token = tokens[index];
|
|
604
|
+
if (token === void 0) break;
|
|
605
|
+
if (token.kind === TokenKind.PAREN_OPEN) {
|
|
606
|
+
depth += 1;
|
|
607
|
+
continue;
|
|
608
|
+
}
|
|
609
|
+
if (token.kind === TokenKind.PAREN_CLOSE) {
|
|
610
|
+
depth -= 1;
|
|
611
|
+
if (depth === 0) return index;
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
throw new Error("Unclosed function color source");
|
|
615
|
+
}
|
|
616
|
+
//#endregion
|
|
617
|
+
//#region src/parser/helpers/stop-helpers.ts
|
|
618
|
+
function parseGradientStopList(source, tokens, startIndex) {
|
|
619
|
+
const items = [];
|
|
620
|
+
let index = startIndex;
|
|
621
|
+
while (true) {
|
|
622
|
+
const itemResult = parseGradientStopItem(source, tokens, index);
|
|
623
|
+
items.push(itemResult.node);
|
|
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;
|
|
631
|
+
continue;
|
|
632
|
+
}
|
|
633
|
+
if (token.kind === TokenKind.PAREN_CLOSE) break;
|
|
634
|
+
throw new Error(`Expected comma or ")" in stop list, received "${token.kind}"`);
|
|
635
|
+
}
|
|
636
|
+
if (items.length < 2) throw new Error("Linear gradient requires at least two stop items");
|
|
637
|
+
return {
|
|
638
|
+
node: normalizeGradientStopList(items),
|
|
639
|
+
nextIndex: index
|
|
640
|
+
};
|
|
641
|
+
}
|
|
642
|
+
function parseGradientStopItem(source, tokens, startIndex) {
|
|
643
|
+
const index = findNextNonWhitespaceIndex(tokens, startIndex);
|
|
644
|
+
if (index === -1) throw new Error("Expected stop item, but reached end of token stream");
|
|
645
|
+
if (tokens[index] === void 0) throw new Error("Expected stop item, but token was undefined");
|
|
646
|
+
return parseColorStop(source, tokens, index);
|
|
647
|
+
}
|
|
648
|
+
function normalizeGradientStopList(items) {
|
|
649
|
+
const colorStopIndexes = items.flatMap((item, index) => item.kind === "color-stop" ? [index] : []);
|
|
650
|
+
if (colorStopIndexes.length < 2) return items;
|
|
651
|
+
const normalized = [...items];
|
|
652
|
+
const getColorStop = (itemIndex) => {
|
|
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;
|
|
683
|
+
continue;
|
|
684
|
+
}
|
|
685
|
+
let anchorEnd = anchorStart + 1;
|
|
686
|
+
while (anchorEnd < colorStopIndexes.length) {
|
|
687
|
+
const endItemIndex = colorStopIndexes[anchorEnd];
|
|
688
|
+
if (endItemIndex === void 0) break;
|
|
689
|
+
const endValue = getColorStopPosition(endItemIndex);
|
|
690
|
+
if (endValue !== null) {
|
|
691
|
+
const gapCount = anchorEnd - anchorStart - 1;
|
|
692
|
+
if (gapCount > 0) {
|
|
693
|
+
const step = (endValue - startValue) / (gapCount + 1);
|
|
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;
|
|
703
|
+
}
|
|
704
|
+
anchorStart = anchorEnd;
|
|
705
|
+
}
|
|
706
|
+
return normalized;
|
|
707
|
+
}
|
|
708
|
+
//#endregion
|
|
709
|
+
//#region src/parser/linear-gradient/parse-linear-gradient.ts
|
|
710
|
+
function parseLinearGradient(source, tokens, startIndex) {
|
|
711
|
+
const functionToken = getTokenAt(tokens, startIndex);
|
|
712
|
+
if (functionToken === null || functionToken.kind !== TokenKind.FUNCTION_LINEAR_GRADIENT && functionToken.kind !== TokenKind.FUNCTION_REPEATING_LINEAR_GRADIENT) throw new Error("Expected linear gradient function token");
|
|
713
|
+
const repeat = functionToken.kind === TokenKind.FUNCTION_REPEATING_LINEAR_GRADIENT ? "repeating" : "normal";
|
|
714
|
+
let index = startIndex + 1;
|
|
715
|
+
index = expectToken(tokens, index, TokenKind.PAREN_OPEN).nextIndex;
|
|
716
|
+
const directionResult = parseLinearDirection(tokens, index);
|
|
717
|
+
const direction = directionResult.node ?? createDefaultLinearDirection();
|
|
718
|
+
index = directionResult.nextIndex;
|
|
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
|
|
753
|
+
};
|
|
754
|
+
}
|
|
755
|
+
return {
|
|
756
|
+
node: null,
|
|
757
|
+
nextIndex: startIndex
|
|
758
|
+
};
|
|
759
|
+
}
|
|
760
|
+
function parseLinearDirectionFromKeywords(tokens, startIndex) {
|
|
761
|
+
let index = startIndex;
|
|
762
|
+
index = expectToken(tokens, index, TokenKind.TO).nextIndex;
|
|
763
|
+
const keywords = [];
|
|
764
|
+
while (true) {
|
|
765
|
+
const nextIndex = findNextNonWhitespaceIndex(tokens, index);
|
|
766
|
+
if (nextIndex === -1) break;
|
|
767
|
+
const token = tokens[nextIndex];
|
|
768
|
+
if (token === void 0) break;
|
|
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;
|
|
775
|
+
}
|
|
776
|
+
if (keywords.length === 0) throw new Error("Expected at least one direction keyword after \"to\"");
|
|
777
|
+
return {
|
|
778
|
+
node: createLinearDirectionFromKeywords(["to", ...keywords]),
|
|
779
|
+
nextIndex: index
|
|
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)
|
|
816
|
+
};
|
|
817
|
+
}
|
|
818
|
+
function createDefaultLinearDirection() {
|
|
819
|
+
return createLinearDirectionFromKeywords(["to", "top"]);
|
|
820
|
+
}
|
|
821
|
+
function parseKeywordsToDeg(keywords) {
|
|
822
|
+
if (keywords.length === 0 || keywords[0] !== "to") return null;
|
|
823
|
+
switch ([...keywords.slice(1)].sort().join("-")) {
|
|
824
|
+
case "top": return 0;
|
|
825
|
+
case "right": return 90;
|
|
826
|
+
case "left": return 270;
|
|
827
|
+
case "bottom": return 180;
|
|
828
|
+
case "bottom-right":
|
|
829
|
+
case "right-bottom": return 135;
|
|
830
|
+
case "bottom-left":
|
|
831
|
+
case "left-bottom": return 225;
|
|
832
|
+
case "top-right":
|
|
833
|
+
case "right-top": return 45;
|
|
834
|
+
case "top-left":
|
|
835
|
+
case "left-top": return 315;
|
|
836
|
+
default: return null;
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
function parseDegToKeywords(angle) {
|
|
840
|
+
switch (normalizeAngleDeg(angle)) {
|
|
841
|
+
case 0: return ["to", "top"];
|
|
842
|
+
case 45: return [
|
|
843
|
+
"to",
|
|
844
|
+
"top",
|
|
845
|
+
"right"
|
|
846
|
+
];
|
|
847
|
+
case 90: return ["to", "right"];
|
|
848
|
+
case 135: return [
|
|
849
|
+
"to",
|
|
850
|
+
"bottom",
|
|
851
|
+
"right"
|
|
852
|
+
];
|
|
853
|
+
case 180: return ["to", "bottom"];
|
|
854
|
+
case 225: return [
|
|
855
|
+
"to",
|
|
856
|
+
"bottom",
|
|
857
|
+
"left"
|
|
858
|
+
];
|
|
859
|
+
case 270: return ["to", "left"];
|
|
860
|
+
case 315: return [
|
|
861
|
+
"to",
|
|
862
|
+
"top",
|
|
863
|
+
"left"
|
|
864
|
+
];
|
|
865
|
+
default: return [];
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
function parseRadToKeywords(angle) {
|
|
869
|
+
return parseDegToKeywords(radToDeg(angle));
|
|
870
|
+
}
|
|
871
|
+
function expandColorStops$2(stops) {
|
|
872
|
+
if (stops.length < 2) return stops;
|
|
873
|
+
const lastPosition = stops[stops.length - 1]?.position;
|
|
874
|
+
if (!lastPosition || lastPosition.kind !== "percentage") return stops;
|
|
875
|
+
if (lastPosition.value >= 1) return stops;
|
|
876
|
+
const percentageSum = roundTo(stops.reduce((sum, stop) => {
|
|
877
|
+
const position = stop.position;
|
|
878
|
+
if (!position || position.kind !== "percentage") return sum;
|
|
879
|
+
return sum + position.value;
|
|
880
|
+
}, 0), 3);
|
|
881
|
+
if (percentageSum <= 0) return stops;
|
|
882
|
+
const newStops = [...stops];
|
|
883
|
+
let offset = percentageSum;
|
|
884
|
+
while (true) {
|
|
885
|
+
let shouldContinue = false;
|
|
886
|
+
for (let i = 0; i < stops.length; i++) {
|
|
887
|
+
const stop = stops[i];
|
|
888
|
+
const position = stop.position;
|
|
889
|
+
if (!position || position.kind !== "percentage") continue;
|
|
890
|
+
const nextValue = roundTo(position.value + offset, 3);
|
|
891
|
+
newStops.push({
|
|
892
|
+
...stop,
|
|
893
|
+
position: {
|
|
894
|
+
kind: "percentage",
|
|
895
|
+
value: nextValue
|
|
896
|
+
}
|
|
897
|
+
});
|
|
898
|
+
if (nextValue <= 1) shouldContinue = true;
|
|
899
|
+
}
|
|
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
|
+
}
|
|
905
|
+
return newStops;
|
|
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
|
+
};
|
|
986
|
+
}
|
|
987
|
+
function parseRadialSize(tokens, startIndex, shape) {
|
|
988
|
+
const index = findNextNonWhitespaceIndex(tokens, startIndex);
|
|
989
|
+
if (index === -1) return {
|
|
990
|
+
node: null,
|
|
991
|
+
nextIndex: startIndex
|
|
992
|
+
};
|
|
993
|
+
const token = tokens[index];
|
|
994
|
+
if (token === void 0) return {
|
|
995
|
+
node: null,
|
|
996
|
+
nextIndex: startIndex
|
|
997
|
+
};
|
|
998
|
+
const keyword = parseRadialSizeKeyword(token.kind);
|
|
999
|
+
if (keyword !== null) return {
|
|
1000
|
+
node: createRadialSizeFromKeyword(shape, keyword),
|
|
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;
|
|
1053
|
+
continue;
|
|
1054
|
+
}
|
|
1055
|
+
if (token.kind === TokenKind.LEFT || token.kind === TokenKind.RIGHT || token.kind === TokenKind.TOP || token.kind === TokenKind.BOTTOM || token.kind === TokenKind.CENTER) {
|
|
1056
|
+
keywords.push(token.kind);
|
|
1057
|
+
index = nextIndex + 1;
|
|
1058
|
+
continue;
|
|
1059
|
+
}
|
|
1060
|
+
break;
|
|
1061
|
+
}
|
|
1062
|
+
return {
|
|
1063
|
+
node: createRadialPositionNode(x, y, keywords),
|
|
1064
|
+
nextIndex: index
|
|
1065
|
+
};
|
|
1066
|
+
}
|
|
1067
|
+
function createDefaultRadialConfig() {
|
|
1068
|
+
return {
|
|
1069
|
+
shape: "ellipse",
|
|
1070
|
+
size: createDefaultRadialSize("ellipse"),
|
|
1071
|
+
position: createDefaultRadialPosition()
|
|
1072
|
+
};
|
|
1073
|
+
}
|
|
1074
|
+
function createDefaultRadialSize(shape) {
|
|
1075
|
+
return createRadialSizeFromKeyword(shape, "farthest-corner");
|
|
1076
|
+
}
|
|
1077
|
+
function createDefaultRadialPosition() {
|
|
1078
|
+
return {
|
|
1079
|
+
kind: "position",
|
|
1080
|
+
x: {
|
|
1081
|
+
kind: "percentage",
|
|
1082
|
+
value: .5
|
|
1083
|
+
},
|
|
1084
|
+
y: {
|
|
1085
|
+
kind: "percentage",
|
|
1086
|
+
value: .5
|
|
1087
|
+
},
|
|
1088
|
+
keywords: ["center"]
|
|
1089
|
+
};
|
|
1090
|
+
}
|
|
1091
|
+
function createRadialPositionNode(x, y, keywords) {
|
|
1092
|
+
if (keywords.length > 0 && x === null && y === null) return {
|
|
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
|
+
};
|
|
1116
|
+
}
|
|
1117
|
+
function createRadialSizeFromKeyword(shape, keyword) {
|
|
1118
|
+
return {
|
|
1119
|
+
kind: "size",
|
|
1120
|
+
shape,
|
|
1121
|
+
keyword,
|
|
1122
|
+
radiusX: {
|
|
1123
|
+
kind: "percentage",
|
|
1124
|
+
value: 1
|
|
1125
|
+
},
|
|
1126
|
+
radiusY: {
|
|
1127
|
+
kind: "percentage",
|
|
1128
|
+
value: shape === "circle" ? 1 : 1
|
|
1129
|
+
}
|
|
1130
|
+
};
|
|
1131
|
+
}
|
|
1132
|
+
function createRadialSizeFromRadii(shape, radiusX, radiusY) {
|
|
1133
|
+
return {
|
|
1134
|
+
kind: "size",
|
|
1135
|
+
shape,
|
|
1136
|
+
keyword: "farthest-corner",
|
|
1137
|
+
radiusX,
|
|
1138
|
+
radiusY
|
|
1139
|
+
};
|
|
1140
|
+
}
|
|
1141
|
+
function parseRadialSizeKeyword(kind) {
|
|
1142
|
+
switch (kind) {
|
|
1143
|
+
case TokenKind.CLOSEST_SIDE: return "closest-side";
|
|
1144
|
+
case TokenKind.CLOSEST_CORNER: return "closest-corner";
|
|
1145
|
+
case TokenKind.FARTHEST_SIDE: return "farthest-side";
|
|
1146
|
+
case TokenKind.FARTHEST_CORNER: return "farthest-corner";
|
|
1147
|
+
default: return null;
|
|
1148
|
+
}
|
|
1149
|
+
}
|
|
1150
|
+
function parseLengthPercentageToken$1(token) {
|
|
1151
|
+
if (token.kind === TokenKind.PERCENTAGE) return {
|
|
1152
|
+
kind: "percentage",
|
|
1153
|
+
value: token.value
|
|
1154
|
+
};
|
|
1155
|
+
if (token.kind === TokenKind.DIMENSION) return {
|
|
1156
|
+
kind: "dimension",
|
|
1157
|
+
value: token.value,
|
|
1158
|
+
unit: token.unit
|
|
1159
|
+
};
|
|
1160
|
+
return null;
|
|
1161
|
+
}
|
|
1162
|
+
function expandColorStops$1(stops) {
|
|
1163
|
+
if (stops.length < 2) return stops;
|
|
1164
|
+
const lastPosition = stops[stops.length - 1]?.position;
|
|
1165
|
+
if (!lastPosition || lastPosition.kind !== "percentage") return stops;
|
|
1166
|
+
if (lastPosition.value >= 1) return stops;
|
|
1167
|
+
const percentageSum = stops.reduce((sum, stop) => {
|
|
1168
|
+
const position = stop.position;
|
|
1169
|
+
if (!position || position.kind !== "percentage") return sum;
|
|
1170
|
+
return sum + position.value;
|
|
1171
|
+
}, 0);
|
|
1172
|
+
if (percentageSum <= 0) return stops;
|
|
1173
|
+
const newStops = [...stops];
|
|
1174
|
+
let offset = percentageSum;
|
|
1175
|
+
while ((newStops[newStops.length - 1]?.position.value ?? 0) <= 1) {
|
|
1176
|
+
for (const stop of stops) {
|
|
1177
|
+
const position = stop.position;
|
|
1178
|
+
if (!position || position.kind !== "percentage") continue;
|
|
1179
|
+
newStops.push({
|
|
1180
|
+
...stop,
|
|
1181
|
+
position: {
|
|
1182
|
+
kind: "percentage",
|
|
1183
|
+
value: position.value + offset
|
|
1184
|
+
}
|
|
1185
|
+
});
|
|
1186
|
+
}
|
|
1187
|
+
offset += percentageSum;
|
|
1188
|
+
}
|
|
1189
|
+
return newStops;
|
|
1190
|
+
}
|
|
1191
|
+
//#endregion
|
|
1192
|
+
//#region src/parser/conic-gradient/parse-conic-gradient.ts
|
|
1193
|
+
function parseConicGradient(source, tokens, startIndex) {
|
|
1194
|
+
const functionToken = getTokenAt(tokens, startIndex);
|
|
1195
|
+
if (functionToken === null || functionToken.kind !== TokenKind.FUNCTION_CONIC_GRADIENT && functionToken.kind !== TokenKind.FUNCTION_REPEATING_CONIC_GRADIENT) throw new Error("Expected conic gradient function token");
|
|
1196
|
+
const repeat = functionToken.kind === TokenKind.FUNCTION_REPEATING_CONIC_GRADIENT ? "repeating" : "normal";
|
|
1197
|
+
let index = startIndex + 1;
|
|
1198
|
+
index = expectToken(tokens, index, TokenKind.PAREN_OPEN).nextIndex;
|
|
1199
|
+
const preludeResult = parseConicPrelude(tokens, index);
|
|
1200
|
+
const prelude = preludeResult.node ?? createDefaultConicPrelude();
|
|
1201
|
+
index = preludeResult.nextIndex;
|
|
1202
|
+
if (preludeResult.node !== null) index = expectToken(tokens, index, TokenKind.COMMA).nextIndex;
|
|
1203
|
+
const stopsResult = parseGradientStopList(source, tokens, index);
|
|
1204
|
+
const stops = repeat === "repeating" ? expandColorStops(stopsResult.node) : stopsResult.node;
|
|
1205
|
+
index = stopsResult.nextIndex;
|
|
1206
|
+
index = expectToken(tokens, index, TokenKind.PAREN_CLOSE).nextIndex;
|
|
1207
|
+
return {
|
|
1208
|
+
node: {
|
|
1209
|
+
kind: "conic",
|
|
1210
|
+
repeat,
|
|
1211
|
+
from: prelude.from,
|
|
1212
|
+
position: prelude.position,
|
|
1213
|
+
stops
|
|
1214
|
+
},
|
|
1215
|
+
nextIndex: index
|
|
1216
|
+
};
|
|
1217
|
+
}
|
|
1218
|
+
function parseConicPrelude(tokens, startIndex) {
|
|
1219
|
+
let index = startIndex;
|
|
1220
|
+
const fromResult = parseConicFrom(tokens, index);
|
|
1221
|
+
const from = fromResult.node ?? createDefaultConicFrom();
|
|
1222
|
+
index = fromResult.nextIndex;
|
|
1223
|
+
const positionResult = parseConicPosition(tokens, index);
|
|
1224
|
+
const position = positionResult.node ?? createDefaultConicPosition();
|
|
1225
|
+
index = positionResult.nextIndex;
|
|
1226
|
+
if (!(fromResult.node !== null || positionResult.node !== null)) return {
|
|
1227
|
+
node: null,
|
|
1228
|
+
nextIndex: startIndex
|
|
1229
|
+
};
|
|
1230
|
+
return {
|
|
1231
|
+
node: {
|
|
1232
|
+
from,
|
|
1233
|
+
position
|
|
1234
|
+
},
|
|
1235
|
+
nextIndex: index
|
|
1236
|
+
};
|
|
1237
|
+
}
|
|
1238
|
+
function parseConicFrom(tokens, startIndex) {
|
|
1239
|
+
let index = findNextNonWhitespaceIndex(tokens, startIndex);
|
|
1240
|
+
if (index === -1) return {
|
|
1241
|
+
node: null,
|
|
1242
|
+
nextIndex: startIndex
|
|
1243
|
+
};
|
|
1244
|
+
const fromToken = tokens[index];
|
|
1245
|
+
if (fromToken === void 0 || fromToken.kind !== TokenKind.FROM) return {
|
|
1246
|
+
node: null,
|
|
1247
|
+
nextIndex: startIndex
|
|
1248
|
+
};
|
|
1249
|
+
index += 1;
|
|
1250
|
+
const angleIndex = findNextNonWhitespaceIndex(tokens, index);
|
|
1251
|
+
if (angleIndex === -1) throw new Error("Expected angle after \"from\"");
|
|
1252
|
+
const angleToken = tokens[angleIndex];
|
|
1253
|
+
if (angleToken === void 0) throw new Error("Expected angle token after \"from\"");
|
|
1254
|
+
const angleRad = parseAngleFromToken(angleToken);
|
|
1255
|
+
if (angleRad === null) throw new Error("Expected valid angle after \"from\"");
|
|
1256
|
+
if (angleToken.kind !== TokenKind.DIMENSION) throw new Error("Expected dimension token for conic angle");
|
|
1257
|
+
return {
|
|
1258
|
+
node: createConicFromNode(angleToken.value, angleToken.unit, angleRad),
|
|
1259
|
+
nextIndex: angleIndex + 1
|
|
1260
|
+
};
|
|
1261
|
+
}
|
|
1262
|
+
function parseConicPosition(tokens, startIndex) {
|
|
1263
|
+
let index = findNextNonWhitespaceIndex(tokens, startIndex);
|
|
1264
|
+
if (index === -1) return {
|
|
1265
|
+
node: null,
|
|
1266
|
+
nextIndex: startIndex
|
|
1267
|
+
};
|
|
1268
|
+
const atToken = tokens[index];
|
|
1269
|
+
if (atToken === void 0 || atToken.kind !== TokenKind.AT) return {
|
|
1270
|
+
node: null,
|
|
1271
|
+
nextIndex: startIndex
|
|
1272
|
+
};
|
|
1273
|
+
index += 1;
|
|
1274
|
+
let x = null;
|
|
1275
|
+
let y = null;
|
|
1276
|
+
const keywords = [];
|
|
1277
|
+
while (true) {
|
|
1278
|
+
const nextIndex = findNextNonWhitespaceIndex(tokens, index);
|
|
1279
|
+
if (nextIndex === -1) break;
|
|
1280
|
+
const token = tokens[nextIndex];
|
|
1281
|
+
if (token === void 0) break;
|
|
1282
|
+
const lengthPercentage = parseLengthPercentageToken(token);
|
|
1283
|
+
if (lengthPercentage !== null) {
|
|
1284
|
+
if (x === null) x = lengthPercentage;
|
|
1285
|
+
else if (y === null) y = lengthPercentage;
|
|
1286
|
+
else break;
|
|
1287
|
+
index = nextIndex + 1;
|
|
1288
|
+
continue;
|
|
1289
|
+
}
|
|
1290
|
+
if (token.kind === TokenKind.LEFT || token.kind === TokenKind.RIGHT || token.kind === TokenKind.TOP || token.kind === TokenKind.BOTTOM || token.kind === TokenKind.CENTER) {
|
|
1291
|
+
keywords.push(token.kind);
|
|
1292
|
+
index = nextIndex + 1;
|
|
1293
|
+
continue;
|
|
1294
|
+
}
|
|
1295
|
+
break;
|
|
1296
|
+
}
|
|
1297
|
+
return {
|
|
1298
|
+
node: createConicPositionNode(x, y, keywords),
|
|
1299
|
+
nextIndex: index
|
|
1300
|
+
};
|
|
1301
|
+
}
|
|
1302
|
+
function createDefaultConicPrelude() {
|
|
1303
|
+
return {
|
|
1304
|
+
from: createDefaultConicFrom(),
|
|
1305
|
+
position: createDefaultConicPosition()
|
|
1306
|
+
};
|
|
1307
|
+
}
|
|
1308
|
+
function createDefaultConicFrom() {
|
|
1309
|
+
return createConicFromNode(0, "deg", 0);
|
|
1310
|
+
}
|
|
1311
|
+
function createDefaultConicPosition() {
|
|
1312
|
+
return {
|
|
1313
|
+
kind: "position",
|
|
1314
|
+
x: {
|
|
1315
|
+
kind: "percentage",
|
|
1316
|
+
value: .5
|
|
1317
|
+
},
|
|
1318
|
+
y: {
|
|
1319
|
+
kind: "percentage",
|
|
1320
|
+
value: .5
|
|
1321
|
+
},
|
|
1322
|
+
keywords: ["center"]
|
|
1323
|
+
};
|
|
1324
|
+
}
|
|
1325
|
+
function createConicFromNode(value, unit, normalizedRad) {
|
|
1326
|
+
return {
|
|
1327
|
+
kind: "angle",
|
|
1328
|
+
value: {
|
|
1329
|
+
kind: "dimension",
|
|
1330
|
+
value,
|
|
1331
|
+
unit
|
|
1332
|
+
},
|
|
1333
|
+
valueRaw: {
|
|
1334
|
+
kind: "dimension",
|
|
1335
|
+
value: roundTo(normalizedRad, 6),
|
|
1336
|
+
unit: "rad"
|
|
1337
|
+
}
|
|
1338
|
+
};
|
|
1339
|
+
}
|
|
1340
|
+
function createConicPositionNode(x, y, keywords) {
|
|
1341
|
+
if (keywords.length > 0 && x === null && y === null) return {
|
|
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
|
+
};
|
|
1365
|
+
}
|
|
1366
|
+
function parseLengthPercentageToken(token) {
|
|
1367
|
+
if (token.kind === TokenKind.PERCENTAGE) return {
|
|
1368
|
+
kind: "percentage",
|
|
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;
|
|
1377
|
+
}
|
|
1378
|
+
function expandColorStops(stops) {
|
|
1379
|
+
if (stops.length < 2) return stops;
|
|
1380
|
+
const lastPosition = stops[stops.length - 1]?.position;
|
|
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;
|
|
1412
|
+
}
|
|
1413
|
+
//#endregion
|
|
1414
|
+
//#region src/parser/parse.ts
|
|
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
|
+
}
|
|
1428
|
+
}
|
|
1429
|
+
//#endregion
|
|
1430
|
+
export { GradientFunctionNameToToken, KeywordNameToToken, TokenKind, advance, ceilTo, clamp, consumeIf, createLexerState, createSpan, currentChar, degToRad, expectToken, findBalancedFunctionEndIndex, findNextNonWhitespaceIndex, floorTo, fromPercent, getNonWhitespaceTokenAt, getSourceSlice, getTokenAt, getTokenRangeSourceSlice, getTokenSourceSlice, gradToRad, isAlphaChar, isAngleUnit, isDigitChar, isEnd, isGradientFunctionToken, isIdentifierChar, isIdentifierStartChar, isKeywordToken, isNumericToken, isSignChar, isTriviaToken, isWhitespaceChar, nextToken, normalizeAngle, normalizeAngleDeg, normalizeAngleRad, parse, parseAngleFromToken, parseColorStop, parseConicGradient, parseGradientStopItem, parseGradientStopList, parseLinearGradient, parseRadialGradient, peekChar, radToDeg, readWhile, roundTo, skipWhitespace, toAngleRad, toPercent, tokenize, truncTo, turnToRad };
|