gradiente 1.0.1 → 2.0.0

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