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/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 };