@jesscss/less-parser 2.0.0-alpha.6 → 2.0.0-alpha.7

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 (62) hide show
  1. package/README.md +133 -5
  2. package/lib/builders.d.ts +381 -0
  3. package/lib/builders.d.ts.map +1 -0
  4. package/lib/cst.cjs +14 -0
  5. package/lib/cst.d.ts +6 -0
  6. package/lib/cst.d.ts.map +1 -0
  7. package/lib/cst.js +12 -0
  8. package/lib/functional-parser.cjs +2653 -0
  9. package/lib/functional-parser.d.ts +18 -0
  10. package/lib/functional-parser.d.ts.map +1 -0
  11. package/lib/functional-parser.js +2588 -0
  12. package/lib/grammar.cjs +30621 -0
  13. package/lib/grammar.d.ts +2 -0
  14. package/lib/grammar.d.ts.map +1 -0
  15. package/lib/grammar.js +30620 -0
  16. package/lib/index.cjs +17 -4096
  17. package/lib/index.d.ts +9 -149
  18. package/lib/index.d.ts.map +1 -1
  19. package/lib/index.js +6 -4091
  20. package/lib/jess.cjs +3968 -0
  21. package/lib/jess.d.ts +7 -0
  22. package/lib/jess.d.ts.map +1 -0
  23. package/lib/jess.js +3962 -0
  24. package/lib/lessParser.d.ts +38 -0
  25. package/lib/lessParser.d.ts.map +1 -0
  26. package/lib/lessRecursiveParser.d.ts +99 -0
  27. package/lib/lessRecursiveParser.d.ts.map +1 -0
  28. package/lib/lessTokens.d.ts +23 -0
  29. package/lib/lessTokens.d.ts.map +1 -0
  30. package/lib/productions/guards.d.ts +113 -0
  31. package/lib/productions/guards.d.ts.map +1 -0
  32. package/lib/productions/index.d.ts +5 -0
  33. package/lib/productions/index.d.ts.map +1 -0
  34. package/lib/productions/root.d.ts +38 -0
  35. package/lib/productions/root.d.ts.map +1 -0
  36. package/lib/productions/selectors.d.ts +41 -0
  37. package/lib/productions/selectors.d.ts.map +1 -0
  38. package/lib/productions/values.d.ts +35 -0
  39. package/lib/productions/values.d.ts.map +1 -0
  40. package/lib/utils.d.ts +9 -0
  41. package/lib/utils.d.ts.map +1 -0
  42. package/package.json +41 -10
  43. package/src/__tests__/debug-log.ts +35 -0
  44. package/src/__tests__/wall5-parse.test.ts +67 -0
  45. package/src/builders.ts +3183 -0
  46. package/src/cst.ts +25 -0
  47. package/src/functional-parser.ts +162 -0
  48. package/src/grammar.ts +869 -0
  49. package/src/index.ts +19 -0
  50. package/src/jess.ts +6 -0
  51. package/src/lessParser.ts +120 -0
  52. package/src/lessRecursiveParser.ts +279 -0
  53. package/src/lessTokens.ts +350 -0
  54. package/src/productions/guards.ts +1066 -0
  55. package/src/productions/index.ts +29 -0
  56. package/src/productions/root.ts +1613 -0
  57. package/src/productions/selectors.ts +1309 -0
  58. package/src/productions/values.ts +1449 -0
  59. package/src/utils.ts +178 -0
  60. package/lib/index.d.cts +0 -150
  61. package/lib/index.d.cts.map +0 -1
  62. package/lib/index.js.map +0 -1
@@ -0,0 +1,1449 @@
1
+ // @ts-nocheck — Retired Chevrotain parser. Uses the legacy 6-tuple `.location`
2
+ // shape removed from Node in the provenance-side-table refactor; the functional
3
+ // Parséman grammar (grammar-rules.ts + builders.ts) is the maintained parser.
4
+ // Not type-checked.
5
+ // Values productions for LessRecursiveParser
6
+ // Converted from Chevrotain-based productions.ts (lines 2060-3015)
7
+ import type { RuleContext, TokenMap } from '../lessRecursiveParser.js';
8
+ import type { IToken } from 'chevrotain';
9
+ import { productions as cssProductions } from '@jesscss/css-parser/jess';
10
+ import {
11
+ type LocationInfo,
12
+ type TreeContext,
13
+ type Operator,
14
+ Node,
15
+ Any,
16
+ Block,
17
+ For,
18
+ Rules,
19
+ List,
20
+ Sequence,
21
+ Call,
22
+ Paren,
23
+ Operation,
24
+ Quoted,
25
+ Interpolated,
26
+ Reference,
27
+ Url,
28
+ Dimension,
29
+ Num,
30
+ negative,
31
+ Negative,
32
+ Rest,
33
+ VarDeclaration,
34
+ Expression,
35
+ INTERPOLATION_PLACEHOLDER,
36
+ isNode,
37
+ N
38
+ } from '@jesscss/core';
39
+ import { createInterpolatedReference, getInterpolatedOrString } from '../utils.js';
40
+
41
+ /** Use `any` for `this` to avoid structural incompatibility between LessRecursiveParser and CssRecursiveParser */
42
+ type P = any;
43
+ type Alt = Array<{ ALT: () => any; GATE?: () => boolean }>;
44
+ type AltContext = (ctx?: RuleContext) => Alt;
45
+ type ProductionRule = (...args: any[]) => any;
46
+ const OPERATORS = new Set<string>(['+', '-', '*', '/', '%']);
47
+
48
+ function toOperator(image: string): Operator {
49
+ if (image === './') {
50
+ return '/';
51
+ }
52
+ if (OPERATORS.has(image)) {
53
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
54
+ return image as Operator;
55
+ }
56
+ throw new Error(`Unexpected operator "${image}".`);
57
+ }
58
+
59
+ function toQuote(image: string): '"' | '\'' {
60
+ if (image === '"' || image === '\'') {
61
+ return image;
62
+ }
63
+ throw new Error(`Unexpected quote "${image}".`);
64
+ }
65
+
66
+ function nonEmptyVarDeclarations(vars: VarDeclaration[]): [VarDeclaration, ...VarDeclaration[]] {
67
+ if (vars.length === 0) {
68
+ throw new Error('Expected at least one variable declaration.');
69
+ }
70
+ return [vars[0]!, ...vars.slice(1)];
71
+ }
72
+
73
+ // ── Save references to CSS production factories ────────────────────────
74
+ const cssNthValue = cssProductions.nthValue;
75
+ const cssKnownFunctions = cssProductions.knownFunctions;
76
+ const cssMathValue = cssProductions.mathValue;
77
+
78
+ // ── Helpers ───────────────────────────────────────────────────────────
79
+
80
+ function getParenFrames(ctx: RuleContext | undefined): boolean[] {
81
+ return (ctx?.parenFrames as boolean[] | undefined) ?? [];
82
+ }
83
+
84
+ function withCalcFrame(ctx: RuleContext | undefined, delta: number): RuleContext {
85
+ const calcFrames = ((ctx?.calcFrames as number | undefined) ?? 0) + delta;
86
+ return { ...(ctx ?? {}), calcFrames };
87
+ }
88
+
89
+ function slashDivisionEnabled($: P, ctx: RuleContext | undefined): boolean {
90
+ const inParens = getParenFrames(ctx).at(-1) ?? false;
91
+ const mathMode = $.mathMode ?? 'parens-division';
92
+ return mathMode === 'always' || inParens;
93
+ }
94
+
95
+ function isDivisionLikeNode(node: Node | undefined): boolean {
96
+ if (!node) {
97
+ return false;
98
+ }
99
+ if (
100
+ isNode(node, N.Color)
101
+ || isNode(node, N.Dimension)
102
+ || node instanceof Num
103
+ || isNode(node, N.Reference)
104
+ || isNode(node, N.Call)
105
+ || isNode(node, N.Operation)
106
+ || node.type === 'Negative'
107
+ || isNode(node, N.Expression)
108
+ ) {
109
+ return true;
110
+ }
111
+ if (isNode(node, N.Paren) || isNode(node, N.Expression)) {
112
+ return isDivisionLikeNode(node.value as Node | undefined);
113
+ }
114
+ return false;
115
+ }
116
+
117
+ function isSlashListContinuationToken($: P, T: TokenMap): boolean {
118
+ const next = $.LA(1);
119
+ return !(
120
+ next.tokenType?.name === 'EOF'
121
+ || $.matchToken(next, T.Comma)
122
+ || $.matchToken(next, T.Semi)
123
+ || $.matchToken(next, T.RCurly)
124
+ || $.matchToken(next, T.RParen)
125
+ || $.matchToken(next, T.RSquare)
126
+ || $.matchToken(next, T.Important)
127
+ || $.matchToken(next, T.Plus)
128
+ || $.matchToken(next, T.Minus)
129
+ || $.matchToken(next, T.Star)
130
+ || $.matchToken(next, T.Slash)
131
+ || $.matchToken(next, T.Percent)
132
+ );
133
+ }
134
+
135
+ function shouldParseSlashDivision($: P, T: TokenMap, ctx: RuleContext | undefined, left: Node, right: Node): boolean {
136
+ const enabled = slashDivisionEnabled($, ctx);
137
+ const leftLike = isDivisionLikeNode(left);
138
+ const rightLike = isDivisionLikeNode(right);
139
+ const continuation = isSlashListContinuationToken($, T);
140
+ if (!enabled) {
141
+ return false;
142
+ }
143
+ if (!leftLike || !rightLike) {
144
+ return false;
145
+ }
146
+ if (continuation) {
147
+ return false;
148
+ }
149
+ return true;
150
+ }
151
+
152
+ function startsCustomValueToken($: P, T: TokenMap): boolean {
153
+ return $.isType(T.LParen)
154
+ || $.isType(T.FunctionStart)
155
+ || $.isType(T.FunctionalPseudoClass)
156
+ || $.isType(T.LSquare)
157
+ || $.isType(T.LCurly)
158
+ || $.isType(T.SingleQuoteStart)
159
+ || $.isType(T.DoubleQuoteStart)
160
+ || $.isType(T.Value)
161
+ || $.isType(T.PlainIdent)
162
+ || $.isType(T.AtKeyword)
163
+ || $.isType(T.PropertyReference)
164
+ || $.isType(T.CustomProperty)
165
+ || $.isType(T.Dimension)
166
+ || $.isType(T.Number)
167
+ || $.isType(T.Color)
168
+ || $.isType(T.UnicodeRange)
169
+ || $.isType(T.Colon)
170
+ || $.isType(T.Comma)
171
+ || $.isType(T.Important)
172
+ || $.isType(T.Unknown);
173
+ }
174
+
175
+ function createEachPattern(
176
+ mixin: Node,
177
+ location: LocationInfo,
178
+ context: any
179
+ ): {
180
+ kind: 'single';
181
+ value: VarDeclaration;
182
+ } | {
183
+ kind: 'tuple';
184
+ values: [VarDeclaration, ...VarDeclaration[]];
185
+ } {
186
+ const defaultVars = ['value', 'key', 'index'].map((name) => {
187
+ return new VarDeclaration({
188
+ name,
189
+ value: new Any('', { role: 'any' })
190
+ }, { paramVar: true }, location, context);
191
+ });
192
+
193
+ if (!isNode(mixin, N.Mixin) || !mixin.params) {
194
+ return {
195
+ kind: 'tuple',
196
+ values: nonEmptyVarDeclarations(defaultVars)
197
+ };
198
+ }
199
+
200
+ const params = mixin.params.value
201
+ .map((param: Node) => {
202
+ if (isNode(param, N.VarDeclaration)) {
203
+ return param;
204
+ }
205
+ if (isNode(param, N.Any) && param.role === 'property') {
206
+ return new VarDeclaration({
207
+ name: String(param.value),
208
+ value: new Any('', { role: 'any' })
209
+ }, { paramVar: true }, param.location, context);
210
+ }
211
+ return undefined;
212
+ })
213
+ .filter((param): param is VarDeclaration => Boolean(param));
214
+
215
+ if (params.length === 0) {
216
+ return {
217
+ kind: 'tuple',
218
+ values: nonEmptyVarDeclarations(defaultVars)
219
+ };
220
+ }
221
+
222
+ if (params.length === 1) {
223
+ return {
224
+ kind: 'single',
225
+ value: params[0]!
226
+ };
227
+ }
228
+
229
+ return {
230
+ kind: 'tuple',
231
+ values: nonEmptyVarDeclarations(params)
232
+ };
233
+ }
234
+
235
+ // ── Production rules ──────────────────────────────────────────────────
236
+
237
+ export function expressionSum(this: P, T: TokenMap) {
238
+ const $ = this;
239
+ return (ctx: RuleContext = {}) => {
240
+ $.startRule();
241
+
242
+ let left = $.SUBRULE($.expressionProduct, { ARGS: [ctx] });
243
+
244
+ while (true) {
245
+ let op: string | undefined;
246
+ let right: Node | undefined;
247
+
248
+ if ($.isType(T.Plus)) {
249
+ const opToken = $.CONSUME(T.Plus);
250
+ op = opToken.image;
251
+ right = $.SUBRULE2($.expressionProduct, { ARGS: [ctx] });
252
+ } else if ($.isType(T.Minus)) {
253
+ const opToken = $.CONSUME(T.Minus);
254
+ op = opToken.image;
255
+ right = $.SUBRULE4($.expressionProduct, { ARGS: [ctx] });
256
+ } else if ($.noSep() && $.matchToken($.LA(1), T.Signed)) {
257
+ const tok = $.CONSUME(T.Signed);
258
+ let startValue: Node | undefined;
259
+ const str = tok.image;
260
+ op = str[0];
261
+ if (tok.payload && tok.payload[1]) {
262
+ const dim = { number: parseFloat(tok.payload[0]), unit: tok.payload[1] };
263
+ startValue = new Dimension(dim, undefined, $.getLocationInfo(tok), $.context);
264
+ } else {
265
+ const num = parseFloat(str);
266
+ if (!Number.isNaN(num)) {
267
+ startValue = new Num(num, undefined, $.getLocationInfo(tok), $.context);
268
+ } else {
269
+ startValue = $.processValueToken(tok);
270
+ }
271
+ }
272
+ right = $.SUBRULE3($.expressionProduct, { ARGS: [{ ...ctx, startValue }] });
273
+ } else {
274
+ break;
275
+ }
276
+
277
+ const operation = new Operation(
278
+ [left, toOperator(op!), right!],
279
+ undefined,
280
+ $.getLocationFromNodes([left, right!]),
281
+ $.context
282
+ );
283
+ left = operation;
284
+ }
285
+
286
+ $.endRule();
287
+
288
+ return left;
289
+ };
290
+ }
291
+
292
+ export function expressionProduct(this: P, T: TokenMap) {
293
+ const $ = this;
294
+ return (ctx: RuleContext = {}) => {
295
+ $.startRule();
296
+
297
+ let left = ctx.startValue ?? $.SUBRULE($.expressionValue, { ARGS: [ctx] });
298
+
299
+ while (true) {
300
+ let op: IToken;
301
+
302
+ if ($.isType(T.Star)) {
303
+ op = $.CONSUME(T.Star);
304
+ } else if ($.isType(T.Slash)) {
305
+ op = $.CONSUME(T.Slash);
306
+ } else if ($.isType(T.Percent)) {
307
+ op = $.CONSUME(T.Percent);
308
+ } else {
309
+ break;
310
+ }
311
+ // Check for deprecated ./ operator
312
+ if (op!.image === './') {
313
+ $.warnDeprecation(
314
+ './ operator is deprecated',
315
+ op!,
316
+ 'dot-slash-operator'
317
+ );
318
+ }
319
+ let right: Node = $.SUBRULE2($.expressionValue, { ARGS: [ctx] });
320
+ const location = $.getLocationFromNodes([left, right]);
321
+
322
+ if (op.image === '/' && !shouldParseSlashDivision($, T, ctx, left, right)) {
323
+ if (isNode(left, N.List) && left.options?.sep === '/') {
324
+ left = new List([...left.value, right], { sep: '/' }, location, $.context);
325
+ } else {
326
+ left = new List([left, right], { sep: '/' }, location, $.context);
327
+ }
328
+ continue;
329
+ }
330
+
331
+ const operation = new Operation(
332
+ [left, toOperator(op!.image), right],
333
+ undefined,
334
+ location,
335
+ $.context
336
+ );
337
+ left = operation;
338
+ }
339
+
340
+ $.endRule();
341
+
342
+ return left;
343
+ };
344
+ }
345
+
346
+ export function customValue(this: P, T: TokenMap) {
347
+ const $ = this;
348
+ return (ctx: RuleContext = {}) => {
349
+ if (
350
+ $.isType(T.UrlStart)
351
+ || $.isType(T.Var)
352
+ || $.isType(T.Calc)
353
+ || $.isType(T.IfFunction)
354
+ || $.isType(T.BooleanFunction)
355
+ || $.isType(T.FunctionStart)
356
+ ) {
357
+ return $.SUBRULE($.functionCall, { ARGS: [ctx] });
358
+ }
359
+ if (
360
+ $.isType(T.LParen)
361
+ || $.isType(T.FunctionalPseudoClass)
362
+ || $.isType(T.LSquare)
363
+ || $.isType(T.LCurly)
364
+ ) {
365
+ return $.SUBRULE($.customBlock, { ARGS: [ctx] });
366
+ }
367
+ if ($.isType(T.SingleQuoteStart) || $.isType(T.DoubleQuoteStart)) {
368
+ return $.SUBRULE($.string, { ARGS: [ctx] });
369
+ }
370
+
371
+ let token: IToken;
372
+ if ($.isType(T.Value)) {
373
+ token = $.CONSUME(T.Value);
374
+ } else if ($.isType(T.PlainIdent)) {
375
+ token = $.CONSUME(T.PlainIdent);
376
+ } else if ($.isType(T.AtKeyword)) {
377
+ token = $.CONSUME(T.AtKeyword);
378
+ } else if ($.isType(T.PropertyReference)) {
379
+ token = $.CONSUME(T.PropertyReference);
380
+ } else if ($.isType(T.CustomProperty)) {
381
+ token = $.CONSUME(T.CustomProperty);
382
+ } else if ($.isType(T.Dimension)) {
383
+ token = $.CONSUME(T.Dimension);
384
+ } else if ($.isType(T.Number)) {
385
+ token = $.CONSUME(T.Number);
386
+ } else if ($.isType(T.Color)) {
387
+ token = $.CONSUME(T.Color);
388
+ } else if ($.isType(T.UnicodeRange)) {
389
+ token = $.CONSUME(T.UnicodeRange);
390
+ } else if ($.isType(T.Colon)) {
391
+ token = $.CONSUME(T.Colon);
392
+ } else if ($.isType(T.Comma)) {
393
+ token = $.CONSUME(T.Comma);
394
+ } else if ($.isType(T.Important)) {
395
+ token = $.CONSUME(T.Important);
396
+ } else {
397
+ token = $.CONSUME(T.Unknown);
398
+ }
399
+
400
+ if (!$.RECORDING_PHASE) {
401
+ return $.processValueToken(token, ctx);
402
+ }
403
+ };
404
+ }
405
+
406
+ export function innerCustomValue(this: P, T: TokenMap) {
407
+ const $ = this;
408
+ return (ctx: RuleContext = {}) => {
409
+ if ($.isType(T.Semi)) {
410
+ const semi = $.CONSUME(T.Semi);
411
+ if ($.RECORDING_PHASE) {
412
+ return;
413
+ }
414
+ return new Any(semi.image, { role: 'semi' }, $.getLocationInfo(semi), $.context);
415
+ }
416
+ return $.SUBRULE($.customValue, { ARGS: [ctx] });
417
+ };
418
+ }
419
+
420
+ export function customBlock(this: P, T: TokenMap) {
421
+ const $ = this;
422
+ return (ctx: RuleContext = {}) => {
423
+ const RECORDING_PHASE = $.RECORDING_PHASE;
424
+ $.startRule();
425
+
426
+ let start: IToken | undefined;
427
+ let end: IToken | undefined;
428
+ let nodes: Node[] | undefined;
429
+ if (!RECORDING_PHASE) {
430
+ nodes = [];
431
+ }
432
+
433
+ if ($.isType(T.LParen)) {
434
+ start = $.CONSUME(T.LParen);
435
+ while (!$.isType(T.RParen) && (startsCustomValueToken($, T) || $.isType(T.Semi))) {
436
+ const val = $.SUBRULE($.innerCustomValue, { ARGS: [ctx] });
437
+ if (!RECORDING_PHASE) {
438
+ nodes!.push(val);
439
+ }
440
+ }
441
+ end = $.CONSUME(T.RParen);
442
+ } else if ($.isType(T.FunctionStart) || $.isType(T.FunctionalPseudoClass)) {
443
+ start = $.isType(T.FunctionStart)
444
+ ? $.CONSUME(T.FunctionStart)
445
+ : $.CONSUME(T.FunctionalPseudoClass);
446
+ while (!$.isType(T.RParen) && (startsCustomValueToken($, T) || $.isType(T.Semi))) {
447
+ const val = $.SUBRULE2($.innerCustomValue, { ARGS: [ctx] });
448
+ if (!RECORDING_PHASE) {
449
+ nodes!.push(val);
450
+ }
451
+ }
452
+ end = $.CONSUME2(T.RParen);
453
+ } else if ($.isType(T.LSquare)) {
454
+ start = $.CONSUME(T.LSquare);
455
+ while (!$.isType(T.RSquare) && (startsCustomValueToken($, T) || $.isType(T.Semi))) {
456
+ const val = $.SUBRULE3($.innerCustomValue, { ARGS: [ctx] });
457
+ if (!RECORDING_PHASE) {
458
+ nodes!.push(val);
459
+ }
460
+ }
461
+ end = $.CONSUME(T.RSquare);
462
+ } else {
463
+ start = $.CONSUME(T.LCurly);
464
+ while (!$.isType(T.RCurly) && (startsCustomValueToken($, T) || $.isType(T.Semi))) {
465
+ const val = $.SUBRULE4($.innerCustomValue, { ARGS: [ctx] });
466
+ if (!RECORDING_PHASE) {
467
+ nodes!.push(val);
468
+ }
469
+ }
470
+ end = $.CONSUME(T.RCurly);
471
+ }
472
+
473
+ if (RECORDING_PHASE) {
474
+ return;
475
+ }
476
+ const location = $.endRule();
477
+ let type: 'square' | 'curly' | undefined;
478
+ switch (start!.image) {
479
+ case '[':
480
+ type = 'square';
481
+ break;
482
+ case '{':
483
+ type = 'curly';
484
+ break;
485
+ }
486
+ if (type) {
487
+ const seqLoc = nodes!.length ? $.getLocationFromNodes(nodes!) : undefined;
488
+ const seq = new Sequence(nodes!, undefined, seqLoc, $.context);
489
+ return new Block(seq, { type }, location, $.context);
490
+ }
491
+ const startNode = new Any(start!.image, { role: 'any' }, $.getLocationInfo(start!), $.context);
492
+ const endNode = new Any(end!.image, { role: 'any' }, $.getLocationInfo(end!), $.context);
493
+ return new Sequence([startNode, ...nodes!, endNode], undefined, location, $.context);
494
+ };
495
+ }
496
+
497
+ export function expressionValue(this: P, T: TokenMap) {
498
+ const $ = this;
499
+ return (ctx: RuleContext = {}) => {
500
+ $.startRule();
501
+ /** Can create a negative expression */
502
+ let minus = $.OPTION(() => $.CONSUME(T.Minus));
503
+ let node = $.OR([
504
+ {
505
+ ALT: () => {
506
+ $.startRule();
507
+ let escape: IToken | undefined;
508
+ $.OPTION2(() => {
509
+ escape = $.CONSUME(T.Tilde);
510
+ });
511
+
512
+ $.CONSUME(T.LParen);
513
+ const innerCtx: RuleContext = {
514
+ ...ctx,
515
+ inner: true,
516
+ allowComma: true,
517
+ // Parentheses in Less enable "math in parens" semantics
518
+ parenFrames: [...getParenFrames(ctx), true]
519
+ };
520
+ let node = $.SUBRULE($.valueList, { ARGS: [innerCtx] });
521
+
522
+ // ~() paren escapes also support semicolons as separators: ~(1; 2; 3)
523
+ let isSemiList = false;
524
+ if (escape) {
525
+ let semiNodes: Node[] = [];
526
+ $.OPTION3(() => {
527
+ $.CONSUME(T.Semi);
528
+ isSemiList = true;
529
+ semiNodes.push(node);
530
+ node = $.SUBRULE2($.valueList, { ARGS: [innerCtx] });
531
+ semiNodes.push(node);
532
+ $.MANY({
533
+ GATE: () => $.isType(T.Semi),
534
+ DEF: () => {
535
+ $.CONSUME2(T.Semi);
536
+ node = $.SUBRULE3($.valueList, { ARGS: [innerCtx] });
537
+ semiNodes.push(node);
538
+ }
539
+ });
540
+ });
541
+ if (isSemiList) {
542
+ node = new List(semiNodes, { sep: ';' });
543
+ }
544
+ }
545
+
546
+ $.CONSUME(T.RParen);
547
+
548
+ let location = $.endRule();
549
+ node = node;
550
+ return new Paren(node, { escaped: !!escape }, location, $.context);
551
+ }
552
+ },
553
+ { ALT: () => $.SUBRULE($.value, { ARGS: [ctx] }) }
554
+ ]);
555
+ let location = $.endRule();
556
+ if (minus) {
557
+ return new Negative(node, undefined, location, $.context);
558
+ }
559
+ return node;
560
+ };
561
+ }
562
+
563
+ /**
564
+ * Add interpolation
565
+ */
566
+ export function nthValue(this: P, T: TokenMap) {
567
+ const $ = this;
568
+ return (ctx: RuleContext = {}) => {
569
+ let nthValueAlt = (ctx: RuleContext = {}) => [
570
+ { ALT: () => $.CONSUME(T.InterpolatedIdent) },
571
+ { ALT: () => $.CONSUME(T.NthOdd) },
572
+ { ALT: () => $.CONSUME(T.NthEven) },
573
+ { ALT: () => $.CONSUME(T.Integer) },
574
+ {
575
+ ALT: () => {
576
+ $.OR2([
577
+ { ALT: () => $.CONSUME(T.NthSignedDimension) },
578
+ { ALT: () => $.CONSUME(T.NthUnsignedDimension) },
579
+ { ALT: () => $.CONSUME(T.NthSignedPlus) },
580
+ { ALT: () => $.CONSUME(T.NthIdent) }
581
+ ]);
582
+ $.OPTION(() => {
583
+ $.OR3([
584
+ { ALT: () => $.CONSUME(T.SignedInt) },
585
+ {
586
+ ALT: () => {
587
+ $.CONSUME(T.Minus);
588
+ $.CONSUME(T.UnsignedInt);
589
+ }
590
+ }
591
+ ]);
592
+ });
593
+ $.OPTION2(() => {
594
+ $.CONSUME(T.Of);
595
+ $.SUBRULE($.complexSelector, { ARGS: [ctx] });
596
+ });
597
+ }
598
+ }
599
+ ];
600
+
601
+ return cssNthValue.call($, T, nthValueAlt)(ctx);
602
+ };
603
+ }
604
+
605
+ export function knownFunctions(this: P, T: TokenMap) {
606
+ const $ = this;
607
+ return (ctx: RuleContext = {}) => {
608
+ let functions = (ctx: RuleContext = {}) => [
609
+ { ALT: () => $.SUBRULE($.urlFunction, { ARGS: [ctx] }) },
610
+ { ALT: () => $.SUBRULE2($.varFunction, { ARGS: [ctx] }) },
611
+ { ALT: () => $.SUBRULE3($.calcFunction, { ARGS: [ctx] }) },
612
+ // colorFunction is already in cssKnownFunctions default, so we don't need to add it here
613
+ { ALT: () => $.SUBRULE4($.ifFunction, { ARGS: [ctx] }) },
614
+ { ALT: () => $.SUBRULE5($.booleanFunction, { ARGS: [ctx] }) }
615
+ ];
616
+
617
+ return cssKnownFunctions.call($, T, functions)(ctx);
618
+ };
619
+ }
620
+
621
+ export function urlFunction(this: P, T: TokenMap) {
622
+ const $ = this;
623
+ return (ctx: RuleContext = {}) => {
624
+ $.startRule();
625
+
626
+ $.CONSUME(T.UrlStart);
627
+ let node: Any | IToken | Node = $.OR([
628
+ { ALT: () => $.SUBRULE($.string, { ARGS: [ctx] }) },
629
+ { ALT: () => $.SUBRULE($.varReference, { ARGS: [ctx] }) },
630
+ { ALT: () => $.CONSUME(T.NonQuotedUrl) }
631
+ ]);
632
+ $.CONSUME(T.UrlEnd);
633
+
634
+ if ($.RECORDING_PHASE) {
635
+ return;
636
+ }
637
+
638
+ const location = $.endRule();
639
+ if (!(node instanceof Node)) {
640
+ const rawValue = node.image;
641
+ const tokenLocation = $.getLocationInfo(node);
642
+ if (rawValue.startsWith('@') || rawValue.startsWith('$')) {
643
+ const resolved = getInterpolatedOrString(rawValue, tokenLocation, $.context);
644
+ if (resolved instanceof Interpolated) {
645
+ node = resolved;
646
+ } else {
647
+ node = new Reference(
648
+ resolved,
649
+ { type: rawValue.startsWith('$') ? 'property' : 'variable' },
650
+ tokenLocation,
651
+ $.context
652
+ );
653
+ }
654
+ } else {
655
+ node = new Any(rawValue, { role: 'urlvalue' }, tokenLocation, $.context);
656
+ }
657
+ }
658
+ return new Url(node, undefined, location, $.context);
659
+ };
660
+ }
661
+
662
+ /**
663
+ * Override CSS calc() parsing so we can maintain parse-time `calcFrames`.
664
+ * This is the parse-time analogue of `Call.evalNode`'s calcFrames++/--.
665
+ */
666
+ export function calcFunction(this: P, T: TokenMap) {
667
+ const $ = this;
668
+ return (ctx: RuleContext = {}) => {
669
+ $.startRule();
670
+
671
+ $.CONSUME(T.Calc);
672
+ const innerCtx = withCalcFrame(ctx, 1);
673
+ const args = $.SUBRULE($.mathSum, { ARGS: [innerCtx] });
674
+ $.CONSUME(T.RParen);
675
+
676
+ const location = $.endRule();
677
+ return new Call({
678
+ name: 'calc',
679
+ args: new List([args])
680
+ }, undefined, location, $.context);
681
+ };
682
+ }
683
+
684
+ export function ifFunction(this: P, T: TokenMap) {
685
+ const $ = this;
686
+ return (ctx: RuleContext = {}) => {
687
+ $.startRule();
688
+
689
+ let name = $.CONSUME(T.IfFunction);
690
+ let args = new List<Node>([]);
691
+ let isCssBranch = false;
692
+ const firstNode = $.SUBRULE($.guardInner, { ARGS: [{ ...ctx, inValueList: true }] });
693
+
694
+ if ($.isType(T.Assign)) {
695
+ isCssBranch = true;
696
+ const branches: Node[] = [];
697
+ const pushBranch = (condition: Node, value: Node) => {
698
+ const sep = new Any(':', { role: 'operator' }, undefined, $.context);
699
+ const loc = $.getLocationFromNodes([condition, value]);
700
+ branches.push(new Sequence([condition, sep, value], undefined, loc, $.context));
701
+ };
702
+
703
+ $.CONSUME(T.Assign);
704
+ let branchValue = $.SUBRULE($.valueList, { ARGS: [{ ...ctx, inner: true }] });
705
+ pushBranch(firstNode, branchValue);
706
+
707
+ $.MANY({
708
+ GATE: () => $.isType(T.Semi) && !$.isTypeAt(2, T.RParen),
709
+ DEF: () => {
710
+ $.CONSUME2(T.Semi);
711
+ const condition = $.SUBRULE2($.guardInner, { ARGS: [{ ...ctx, inValueList: true }] });
712
+ $.CONSUME2(T.Assign);
713
+ const value = $.SUBRULE2($.valueList, { ARGS: [{ ...ctx, inner: true }] });
714
+ pushBranch(condition, value);
715
+ }
716
+ });
717
+ $.OPTION(() => $.CONSUME3(T.Semi));
718
+ $.CONSUME2(T.RParen);
719
+
720
+ const cssArgs = branches.length === 1
721
+ ? branches[0]!
722
+ : new List(branches, { sep: ';' }, $.getLocationFromNodes(branches), $.context);
723
+ args = new List([cssArgs]);
724
+ } else {
725
+ isCssBranch = false;
726
+ let node: Node = firstNode;
727
+ const parenValue = node instanceof Paren ? node.value : undefined;
728
+ const condNode = parenValue instanceof Node ? parenValue : node;
729
+ args = new List([condNode]);
730
+
731
+ $.OR([
732
+ {
733
+ ALT: () => {
734
+ $.CONSUME(T.Semi);
735
+ node = $.SUBRULE2($.valueList, { ARGS: [{ ...ctx, allowAnonymousMixins: true }] });
736
+ args = new List([...args.value, node], args.options, $.getLocationFromNodes([...args.value, node]), $.context);
737
+ $.OPTION(() => {
738
+ $.CONSUME4(T.Semi);
739
+ node = $.SUBRULE3($.valueList, { ARGS: [{ ...ctx, allowAnonymousMixins: true }] });
740
+ args = new List([...args.value, node], args.options, $.getLocationFromNodes([...args.value, node]), $.context);
741
+ });
742
+ }
743
+ },
744
+ {
745
+ ALT: () => {
746
+ $.CONSUME(T.Comma);
747
+ node = $.SUBRULE($.callArgument, { ARGS: [{ ...ctx, allowAnonymousMixins: true }] });
748
+ args = new List([...args.value, node], args.options, $.getLocationFromNodes([...args.value, node]), $.context);
749
+ $.OPTION2(() => {
750
+ $.CONSUME2(T.Comma);
751
+ node = $.SUBRULE2($.callArgument, { ARGS: [{ ...ctx, allowAnonymousMixins: true }] });
752
+ args = new List([...args.value, node], args.options, $.getLocationFromNodes([...args.value, node]), $.context);
753
+ });
754
+ }
755
+ }
756
+ ]);
757
+ $.CONSUME3(T.RParen);
758
+ }
759
+
760
+ let location = $.endRule();
761
+ let nameNode = new Reference('if', {
762
+ type: 'function',
763
+ fallbackValue: isCssBranch ? true : undefined
764
+ }, $.getLocationInfo(name), $.context);
765
+ const callNode = new Call({ name: nameNode, args }, undefined, location, $.context);
766
+ return callNode;
767
+ };
768
+ }
769
+
770
+ export function booleanFunction(this: P, T: TokenMap) {
771
+ const $ = this;
772
+ return (ctx: RuleContext = {}) => {
773
+ $.startRule();
774
+ $.CONSUME(T.BooleanFunction);
775
+ let arg: Node = $.SUBRULE($.guardInner, { ARGS: [{ ...ctx, inValueList: true }] });
776
+ $.CONSUME(T.RParen);
777
+
778
+ let location = $.endRule();
779
+ const argValue = arg instanceof Paren ? arg.value : undefined;
780
+ const conditionNode = argValue instanceof Node ? argValue : arg;
781
+ const exprNode = new Expression(conditionNode, { parens: true }, location, $.context);
782
+ return exprNode;
783
+ };
784
+ }
785
+
786
+ export function varReference(this: P, T: TokenMap): ProductionRule {
787
+ const $ = this;
788
+ return (ctx: RuleContext = {}) => {
789
+ let node: Node | undefined = $.OR([
790
+ {
791
+ ALT: () => {
792
+ let token = $.CONSUME(T.PropertyReference);
793
+ if ($.RECORDING_PHASE) {
794
+ return;
795
+ }
796
+ // Warn about $ident in custom property values - it's treated as literal text, not a property reference
797
+ if (ctx.inCustomPropertyValue) {
798
+ const atName = token.image;
799
+ const ident = token.image.slice(1);
800
+ $.warnDeprecation(
801
+ `${atName} in custom property values is treated as literal text, not a property reference. Use \${${ident}} if you want it to be evaluated.`,
802
+ token,
803
+ 'property-in-unknown-value'
804
+ );
805
+ return new Reference(
806
+ { key: new Quoted(token.image.slice(1), { quote: '\'' }, $.getLocationInfo(token), $.context) },
807
+ { type: 'index', role: 'ident' },
808
+ $.getLocationInfo(token),
809
+ $.context
810
+ );
811
+ }
812
+ return new Reference(
813
+ { key: new Quoted(token.image.slice(1), { quote: '\'' }, $.getLocationInfo(token), $.context) },
814
+ { type: 'index' },
815
+ $.getLocationInfo(token),
816
+ $.context
817
+ );
818
+ }
819
+ },
820
+ {
821
+ ALT: () => {
822
+ let token = $.CONSUME(T.NestedReference);
823
+ if ($.RECORDING_PHASE) {
824
+ return;
825
+ }
826
+ const raw = token.image;
827
+ const isPropertyLookup = raw.startsWith('$');
828
+ const type: 'variable' | 'index' = raw.startsWith('@') ? 'variable' : 'index';
829
+ const rawKey = getInterpolatedOrString(raw);
830
+ const key = isPropertyLookup
831
+ ? (typeof rawKey === 'string'
832
+ ? new Quoted(rawKey, { quote: '\'' }, $.getLocationInfo(token), $.context)
833
+ : new Quoted(rawKey, { quote: '\'' }, $.getLocationInfo(token), $.context))
834
+ : rawKey;
835
+ if (ctx.inCustomPropertyValue && typeof key === 'string') {
836
+ return new Reference({ key }, { type: 'variable', role: 'ident' }, $.getLocationInfo(token), $.context);
837
+ }
838
+ if (typeof key === 'string') {
839
+ return new Reference(key, { type }, $.getLocationInfo(token), $.context);
840
+ }
841
+ return new Reference({ key }, { type }, $.getLocationInfo(token), $.context);
842
+ }
843
+ },
844
+ {
845
+ ALT: () => {
846
+ let token = $.SUBRULE($.varName, { ARGS: [ctx] });
847
+ if ($.RECORDING_PHASE) {
848
+ return;
849
+ }
850
+ // Warn about @ident in custom property values - it's treated as literal text, not a variable reference
851
+ if (ctx.inCustomPropertyValue) {
852
+ const atName = token.image;
853
+ const ident = token.image.slice(1);
854
+ $.warnDeprecation(
855
+ `${atName} in custom property values is treated as literal text, not a variable reference. Use @{${ident}} if you want it to be evaluated.`,
856
+ token,
857
+ 'variable-in-unknown-value'
858
+ );
859
+ return new Reference(
860
+ { key: token.image.slice(1) },
861
+ { type: 'variable', role: 'ident' },
862
+ $.getLocationInfo(token),
863
+ $.context
864
+ );
865
+ }
866
+ if (ctx.atRulePreludeBareVariableAs === 'index') {
867
+ const nextToken = $.LA(1).tokenType;
868
+ const hasExplicitAccessorOrCall = $.noSep()
869
+ && (nextToken === T.LSquare || nextToken === T.LParen);
870
+ if (hasExplicitAccessorOrCall) {
871
+ return new Reference(token.image.slice(1), { type: 'variable' }, $.getLocationInfo(token), $.context);
872
+ }
873
+ const atName = token.image;
874
+ const ident = token.image.slice(1);
875
+ $.warnDeprecation(
876
+ `"${atName}" in at-rule preludes is deprecated. Use "@{${ident}}" in Less; outside declaration values this is normalized to indexed lookup syntax.`,
877
+ token,
878
+ 'at-rule-prelude-variable'
879
+ );
880
+ return new Reference(
881
+ { key: ident },
882
+ { type: 'index', role: 'ident' },
883
+ $.getLocationInfo(token),
884
+ $.context
885
+ );
886
+ }
887
+ return new Reference(token.image.slice(1), { type: 'variable' }, $.getLocationInfo(token), $.context);
888
+ }
889
+ }
890
+ ]);
891
+ $.OR2([
892
+ {
893
+ ALT: () => {
894
+ /** This spreads a (list) value within a containing list when evaluated */
895
+ let token = $.CONSUME(T.Ellipsis);
896
+ if (!$.RECORDING_PHASE) {
897
+ node = new Rest(node, undefined, $.getLocationFromNodes([node!, token]), $.context);
898
+ }
899
+ }
900
+ },
901
+ {
902
+ /** Only variables can have accessors */
903
+ GATE: () => {
904
+ if (node?.options?.type !== 'variable') {
905
+ return false;
906
+ }
907
+ let next = $.LA(1).tokenType;
908
+ if (next !== T.LSquare && next !== T.LParen) {
909
+ return false;
910
+ }
911
+ if (!$.noSep()) {
912
+ return false;
913
+ }
914
+ return true;
915
+ },
916
+ ALT: () => {
917
+ $.AT_LEAST_ONE({
918
+ GATE: () => {
919
+ let next = $.LA(1).tokenType;
920
+ if (next !== T.LSquare && next !== T.LParen) {
921
+ return false;
922
+ }
923
+ if (!$.noSep()) {
924
+ return false;
925
+ }
926
+ return true;
927
+ },
928
+ DEF: () => {
929
+ node = $.SUBRULE($.lookupOrCall, { ARGS: [{ ...ctx, node: node! }] });
930
+ }
931
+ });
932
+ $.OPTION(() => {
933
+ $.OPTION2(() => $.CONSUME(T.Gt));
934
+ node = $.SUBRULE($.mixinReference, { ARGS: [{ ...ctx, node: node! }] });
935
+ });
936
+ }
937
+ },
938
+ { ALT: () => undefined }
939
+ ]);
940
+
941
+ return node!;
942
+ };
943
+ }
944
+
945
+ export function valueReference(this: P, T: TokenMap) {
946
+ const $ = this;
947
+ return (ctx: RuleContext = {}) => {
948
+ return $.OR([
949
+ { ALT: () => $.SUBRULE($.varReference, { ARGS: [ctx] }) },
950
+ { ALT: () => $.SUBRULE2($.mixinReference, { ARGS: [ctx] }) }
951
+ ]);
952
+ };
953
+ }
954
+
955
+ export function functionCall(this: P, T: TokenMap) {
956
+ const $ = this;
957
+ return (ctx: RuleContext = {}) => {
958
+ const modernColorFunctions = new Set(['rgb', 'rgba', 'hsl', 'hsla']);
959
+ const isModernColorCall = (name: string, args?: List<Node>) => {
960
+ if (!modernColorFunctions.has(name.toLowerCase())) {
961
+ return false;
962
+ }
963
+ if (!args || args.value.length !== 1) {
964
+ return false;
965
+ }
966
+ const firstArg = args.value[0];
967
+ return Boolean(isNode(firstArg, N.Sequence) && firstArg.value.length >= 2);
968
+ };
969
+
970
+ let funcAlt = (ctx: RuleContext = {}) => [
971
+ {
972
+ // Disambiguate known functions by their dedicated tokens
973
+ GATE: () => {
974
+ let tokenType = $.LA(1).tokenType;
975
+ return tokenType === T.UrlStart
976
+ || tokenType === T.Var
977
+ || tokenType === T.Calc
978
+ || tokenType === T.IfFunction
979
+ || tokenType === T.BooleanFunction;
980
+ },
981
+ ALT: () => $.SUBRULE($.knownFunctions, { ARGS: [ctx] })
982
+ },
983
+ {
984
+ // Generic function via FunctionStart token
985
+ GATE: () => {
986
+ let tokenType = $.LA(1).tokenType;
987
+ return tokenType !== T.UrlStart
988
+ && tokenType !== T.Var
989
+ && tokenType !== T.Calc
990
+ && tokenType !== T.IfFunction
991
+ && tokenType !== T.BooleanFunction;
992
+ },
993
+ ALT: () => {
994
+ $.startRule();
995
+ const fnStart = $.CONSUME(T.FunctionStart);
996
+ const fnNameForCtx = fnStart.image.slice(0, -1);
997
+ let args: List<Node> | undefined;
998
+ $.OPTION(() => args = $.SUBRULE2($.functionCallArgs, { ARGS: [{ ...ctx, currentFunctionName: fnNameForCtx }] }));
999
+ $.CONSUME(T.RParen);
1000
+ const location = $.endRule();
1001
+ const nameValue = fnNameForCtx;
1002
+ if (nameValue === 'unit' && args?.value[1] instanceof Any) {
1003
+ const unitArg = args.value[1];
1004
+ const quotedUnit = new Quoted(unitArg.valueOf(), { quote: '"' }, undefined, $.context);
1005
+ const newArgsData = [...args.value];
1006
+ newArgsData[1] = quotedUnit;
1007
+ args = new List(newArgsData, args.options, $.getLocationFromNodes(newArgsData), $.context);
1008
+ }
1009
+ if (ctx.detachedRulesetUsage === 'default-param' && nameValue === 'default') {
1010
+ return new Call(
1011
+ { name: 'default', args },
1012
+ undefined,
1013
+ location,
1014
+ $.context
1015
+ );
1016
+ }
1017
+ if (
1018
+ nameValue === 'each'
1019
+ && args?.value.length === 2
1020
+ && isNode(args.value[1], N.Mixin)
1021
+ ) {
1022
+ const iterable = args.value[0]!;
1023
+ const callback = args.value[1]!;
1024
+ return new For({
1025
+ pattern: createEachPattern(callback, location, $.context),
1026
+ iterable: { kind: 'node', value: iterable },
1027
+ rules: callback.rules
1028
+ }, undefined, location, $.context);
1029
+ }
1030
+ const nameNode = new Reference(nameValue, { type: 'function', fallbackValue: true }, $.getLocationInfo(fnStart), $.context);
1031
+ /** Less / Sass functions we try to call that throw just get turned into calls. */
1032
+ const modernSyntax = isModernColorCall(nameValue, args);
1033
+ return new Call(
1034
+ { name: nameNode, args },
1035
+ { silentFail: true, ...(modernSyntax ? { modernSyntax: true } : {}) },
1036
+ location,
1037
+ $.context
1038
+ );
1039
+ }
1040
+ }
1041
+ ];
1042
+
1043
+ return $.OR(funcAlt(ctx));
1044
+ };
1045
+ }
1046
+
1047
+ export function functionCallArgs(this: P, T: TokenMap): ProductionRule {
1048
+ const $ = this;
1049
+ return (ctx: RuleContext = {}) => {
1050
+ $.startRule();
1051
+
1052
+ // Inside function arguments, allow inner tokens like ':'
1053
+ const prevInner = ctx.inner;
1054
+ ctx.inner = true;
1055
+ // Calls intentionally push a `false` paren frame (matches `Call.evalNode`)
1056
+ const argCtx: RuleContext = {
1057
+ ...ctx,
1058
+ allowComma: false,
1059
+ parenFrames: [...getParenFrames(ctx), false],
1060
+ detachedRulesetUsage: 'function-arg',
1061
+ inFunctionArgs: true
1062
+ };
1063
+ let commaNodes: Node[];
1064
+ let semiNodes: Node[] = [];
1065
+ let isSemiList = false;
1066
+ try {
1067
+ let node = $.SUBRULE($.callArgument, { ARGS: [argCtx] });
1068
+
1069
+ commaNodes = [node];
1070
+
1071
+ // First, consume any comma-separated arguments
1072
+ $.MANY({
1073
+ GATE: () => $.isType(T.Comma),
1074
+ DEF: () => {
1075
+ $.CONSUME(T.Comma);
1076
+ node = $.SUBRULE2($.callArgument, { ARGS: [argCtx] });
1077
+ commaNodes!.push(node);
1078
+ }
1079
+ });
1080
+
1081
+ // Then, optionally switch to semicolon-separated list and continue with semicolons
1082
+ $.OPTION(() => {
1083
+ $.CONSUME(T.Semi);
1084
+ isSemiList = true;
1085
+
1086
+ // Aggregate the previous set of comma-nodes as the first semi item
1087
+ if (commaNodes.length > 1) {
1088
+ semiNodes.push(new List(commaNodes, undefined, $.getLocationFromNodes(commaNodes), $.context));
1089
+ } else {
1090
+ semiNodes.push(commaNodes[0]!);
1091
+ }
1092
+
1093
+ node = $.SUBRULE3($.callArgument, { ARGS: [{ ...argCtx, allowComma: true }] });
1094
+ semiNodes.push(node);
1095
+
1096
+ $.MANY2({
1097
+ GATE: () => $.isType(T.Semi),
1098
+ DEF: () => {
1099
+ $.CONSUME2(T.Semi);
1100
+ node = $.SUBRULE4($.callArgument, { ARGS: [{ ...argCtx, allowComma: true }] });
1101
+ semiNodes.push(node);
1102
+ }
1103
+ });
1104
+ });
1105
+ } finally {
1106
+ ctx.inner = prevInner;
1107
+ }
1108
+ $.endRule();
1109
+ const nodes = isSemiList ? semiNodes! : commaNodes!;
1110
+ return new List(nodes, isSemiList ? { sep: ';' } : undefined);
1111
+ };
1112
+ }
1113
+
1114
+ export function value(this: P, T: TokenMap): ProductionRule {
1115
+ const $ = this;
1116
+ return (ctx: RuleContext = {}) => {
1117
+ if ($.isType(T.Percent)) {
1118
+ // no-op: preserved from original
1119
+ }
1120
+ // eslint-disable-next-line @typescript-eslint/naming-convention
1121
+ let _isMixinReference = undefined as boolean | undefined;
1122
+ const isMixinReference = () => {
1123
+ if (_isMixinReference === undefined) {
1124
+ let tt1 = $.LA(1).tokenType;
1125
+ let tt2 = $.LA(2).tokenType;
1126
+ /**
1127
+ * We'll allow a few "bare" mixin references without parens
1128
+ * or square brackets, but not if they'll conflict with
1129
+ * other syntax.
1130
+ */
1131
+ _isMixinReference =
1132
+ tt1 === T.DotName
1133
+ || tt1 === T.HashName
1134
+ || tt1 === T.InterpolatedSelector
1135
+ || (
1136
+ (
1137
+ tt1 === T.ColorIdentStart
1138
+ || tt1 === T.InterpolatedSelector
1139
+ ) && (
1140
+ tt2 === T.Gt
1141
+ || tt2 === T.DotName
1142
+ || tt2 === T.HashName
1143
+ || tt2 === T.InterpolatedSelector
1144
+ || (
1145
+ $.noSep(1)
1146
+ && (
1147
+ tt2 === T.LParen
1148
+ || tt2 === T.LSquare
1149
+ || tt2 === T.HashName
1150
+ || tt2 === T.DotName
1151
+ )
1152
+ )
1153
+ )
1154
+ );
1155
+ }
1156
+ return _isMixinReference;
1157
+ };
1158
+ let node: Node = $.OR([
1159
+ {
1160
+ GATE: () => $.check(T.FunctionStart),
1161
+ ALT: () => $.SUBRULE($.functionCall, { ARGS: [ctx] })
1162
+ },
1163
+ {
1164
+ GATE: () => $.isType(T.Star) && $.isTypeAt(2, T.LSquare),
1165
+ ALT: () => $.SUBRULE($.selectorCapture, { ARGS: [ctx] })
1166
+ },
1167
+ {
1168
+ GATE: isMixinReference,
1169
+ ALT: () => $.SUBRULE($.mixinReference, { ARGS: [ctx] })
1170
+ },
1171
+ {
1172
+ GATE: () => !isMixinReference(),
1173
+ ALT: () => $.CONSUME(T.Color)
1174
+ },
1175
+ {
1176
+ GATE: () => !isMixinReference(),
1177
+ ALT: () => $.CONSUME2(T.Ident)
1178
+ },
1179
+ { ALT: () => $.SUBRULE($.varReference, { ARGS: [ctx] }) },
1180
+ { ALT: () => $.CONSUME(T.DefaultGuardFunc) },
1181
+ { ALT: () => $.CONSUME(T.Dimension) },
1182
+ { ALT: () => $.CONSUME(T.Number) },
1183
+ {
1184
+ GATE: () => ctx.currentFunctionName === 'unit',
1185
+ ALT: () => $.CONSUME(T.Percent)
1186
+ },
1187
+ { ALT: () => $.CONSUME(T.UnicodeRange) },
1188
+ { ALT: () => $.SUBRULE($.string, { ARGS: [ctx] }) },
1189
+ { ALT: () => $.CONSUME(T.JavaScript) },
1190
+ /** Explicitly not marked as an ident */
1191
+ { ALT: () => $.CONSUME(T.When) },
1192
+ { ALT: () => $.SUBRULE($.squareValue, { ARGS: [ctx] }) },
1193
+ {
1194
+ GATE: () => $.looseMode && !!ctx.inner,
1195
+ ALT: () => $.CONSUME(T.Colon)
1196
+ },
1197
+ {
1198
+ /** e.g. alpha(opacity=@var) */
1199
+ GATE: () => $.looseMode && !!ctx.inFunctionArgs,
1200
+ ALT: () => $.CONSUME(T.Eq)
1201
+ },
1202
+ {
1203
+ GATE: () => $.looseMode,
1204
+ ALT: () => $.CONSUME(T.Unknown)
1205
+ },
1206
+ {
1207
+ /** e.g. progid:DXImageTransform.Microsoft.Blur(pixelradius=2) */
1208
+ GATE: () => $.legacyMode,
1209
+ ALT: () => $.CONSUME(T.LegacyMSFilter)
1210
+ }
1211
+ ]);
1212
+ if (!$.RECORDING_PHASE) {
1213
+ if (!(node instanceof Node)) {
1214
+ node = $.processValueToken(node);
1215
+ }
1216
+ return node;
1217
+ }
1218
+ };
1219
+ }
1220
+
1221
+ export function string(this: P, T: TokenMap) {
1222
+ const $ = this;
1223
+ return (ctx: RuleContext = {}) => {
1224
+ let stringAlt = [
1225
+ {
1226
+ GATE: () => $.isType(T.SingleQuoteStart),
1227
+ ALT: () => {
1228
+ $.startRule();
1229
+ let quote: IToken = $.CONSUME(T.SingleQuoteStart);
1230
+ let contents: IToken | undefined;
1231
+ $.OPTION2(() => contents = $.CONSUME(T.SingleQuoteStringContents));
1232
+ $.CONSUME(T.SingleQuoteEnd);
1233
+ let quoteImg: string = quote.image;
1234
+ let escaped = false;
1235
+ if (quoteImg.startsWith('~')) {
1236
+ escaped = true;
1237
+ quoteImg = quoteImg.slice(1);
1238
+ }
1239
+ let location = $.endRule();
1240
+ let value = contents?.image;
1241
+ if (escaped && value) {
1242
+ value = value.replace(/\\(?:\r\n?|\n|\f)/g, '\n');
1243
+ }
1244
+ if ($.RECORDING_PHASE) {
1245
+ return;
1246
+ }
1247
+
1248
+ const quoteChar = toQuote(quoteImg);
1249
+ if (value && (value.includes('@{') || value.includes('${'))) {
1250
+ return new Quoted(processStringInterpolation(value, location, $.context), { quote: quoteChar, escaped }, location, $.context);
1251
+ }
1252
+
1253
+ return new Quoted(new Any(value ?? '', { role: 'any' }), { quote: quoteChar, escaped }, location, $.context);
1254
+ }
1255
+ },
1256
+ {
1257
+ GATE: () => $.isType(T.DoubleQuoteStart),
1258
+ ALT: () => {
1259
+ $.startRule();
1260
+ let quote: IToken = $.CONSUME(T.DoubleQuoteStart);
1261
+ let contents: IToken | undefined;
1262
+ $.OPTION3(() => contents = $.CONSUME(T.DoubleQuoteStringContents));
1263
+ $.CONSUME(T.DoubleQuoteEnd);
1264
+ let quoteImg: string = quote.image;
1265
+ let escaped = false;
1266
+ if (quoteImg.startsWith('~')) {
1267
+ escaped = true;
1268
+ quoteImg = quoteImg.slice(1);
1269
+ }
1270
+ let location = $.endRule();
1271
+ let value = contents?.image;
1272
+ if (escaped && value) {
1273
+ value = value.replace(/\\(?:\r\n?|\n|\f)/g, '\n');
1274
+ }
1275
+ if ($.RECORDING_PHASE) {
1276
+ return;
1277
+ }
1278
+
1279
+ const quoteChar = toQuote(quoteImg);
1280
+ if (value && (value.includes('@{') || value.includes('${'))) {
1281
+ return new Quoted(processStringInterpolation(value, location, $.context), { quote: quoteChar, escaped }, location, $.context);
1282
+ }
1283
+
1284
+ return new Quoted(new Any(value ?? '', { role: 'any' }), { quote: quoteChar, escaped }, location, $.context);
1285
+ }
1286
+ }
1287
+ ];
1288
+
1289
+ return $.OR(stringAlt);
1290
+ };
1291
+ }
1292
+
1293
+ // ── String interpolation helpers ──────────────────────────────────────
1294
+
1295
+ /**
1296
+ * Find interpolation patterns like @{...} or ${...}, handling nested braces.
1297
+ * Returns an array of { start, end, prefix, content } for each match.
1298
+ */
1299
+ function findInterpolations(value: string): Array<{ start: number; end: number; prefix: string; content: string }> {
1300
+ const matches: Array<{ start: number; end: number; prefix: string; content: string }> = [];
1301
+ let i = 0;
1302
+
1303
+ while (i < value.length) {
1304
+ // Look for @{ or ${
1305
+ if ((value[i] === '@' || value[i] === '$') && value[i + 1] === '{') {
1306
+ const prefix = value[i]!;
1307
+ const start = i;
1308
+ i += 2; // Skip @{ or ${
1309
+ let braceCount = 1;
1310
+ const contentStart = i;
1311
+
1312
+ // Find matching closing brace, counting nested braces
1313
+ while (i < value.length && braceCount > 0) {
1314
+ if (value[i] === '{') {
1315
+ braceCount++;
1316
+ } else if (value[i] === '}') {
1317
+ braceCount--;
1318
+ }
1319
+ i++;
1320
+ }
1321
+
1322
+ if (braceCount === 0) {
1323
+ const content = value.slice(contentStart, i - 1);
1324
+ matches.push({ start, end: i, prefix, content });
1325
+ }
1326
+ } else {
1327
+ i++;
1328
+ }
1329
+ }
1330
+
1331
+ return matches;
1332
+ }
1333
+
1334
+ // Helper function to process string interpolation (handles nested @{...} patterns)
1335
+ function processStringInterpolation(value: string, location: LocationInfo, context: TreeContext): Any | Interpolated {
1336
+ const matches = findInterpolations(value);
1337
+
1338
+ if (matches.length === 0) {
1339
+ return new Any(value, { role: 'any' }, location, context);
1340
+ }
1341
+
1342
+ const replacements: Node[] = [];
1343
+ let source = value;
1344
+ let offset = 0;
1345
+
1346
+ for (const match of matches) {
1347
+ const adjustedStart = match.start - offset;
1348
+ const adjustedEnd = match.end - offset;
1349
+ const before = source.slice(0, adjustedStart);
1350
+ const after = source.slice(adjustedEnd);
1351
+ source = before + INTERPOLATION_PLACEHOLDER + after;
1352
+ offset += (match.end - match.start) - INTERPOLATION_PLACEHOLDER.length;
1353
+
1354
+ // Recursively process the content in case it has nested interpolation
1355
+ const innerResult = processStringInterpolation(match.content, location, context);
1356
+ if (innerResult instanceof Interpolated) {
1357
+ // Nested interpolation in string contexts still resolves through a reference,
1358
+ // but must remain expression-wrapped in Jess output.
1359
+ const nestedRef = new Reference({ key: innerResult }, { type: 'variable', role: 'ident' }, location, context);
1360
+ replacements.push(new Expression(nestedRef, undefined, location, context));
1361
+ } else {
1362
+ // Simple interpolation reference
1363
+ replacements.push(createInterpolatedReference(match.prefix, match.content, location, context));
1364
+ }
1365
+ }
1366
+
1367
+ return new Interpolated({ source, replacements }, { role: 'ident' }, location, context);
1368
+ }
1369
+
1370
+ export function mathValue(this: P, T: TokenMap): ProductionRule {
1371
+ const $ = this;
1372
+ return (ctx: RuleContext = {}) => {
1373
+ let valueAlt = (ctx: RuleContext = {}) => [
1374
+ { ALT: () => $.CONSUME(T.AtKeyword) },
1375
+ { ALT: () => $.CONSUME(T.Number) },
1376
+ { ALT: () => $.CONSUME(T.Dimension) },
1377
+ // Allow identifiers like channel names in color space calcs (e.g., calc(l - 0.1))
1378
+ { ALT: () => $.CONSUME(T.Ident) },
1379
+ { ALT: () => $.SUBRULE($.functionCall, { ARGS: [ctx] }) },
1380
+ {
1381
+ /** Only allow escaped strings in calc */
1382
+ GATE: () => $.LA(1).image.startsWith('~'),
1383
+ ALT: () => $.SUBRULE2($.string, { ARGS: [ctx] })
1384
+ },
1385
+ {
1386
+ /** For some reason, e() goes here instead of $.function */
1387
+ GATE: () => !$.isTypeAt(2, T.LParen),
1388
+ ALT: () => $.CONSUME(T.MathConstant)
1389
+ },
1390
+ { ALT: () => $.SUBRULE($.mathParen, { ARGS: [ctx] }) }
1391
+ ];
1392
+
1393
+ return cssMathValue.call($, T, valueAlt)(ctx);
1394
+ };
1395
+ }
1396
+
1397
+ export function mathProduct(this: P, T: TokenMap): ProductionRule {
1398
+ const $ = this;
1399
+ return (ctx: RuleContext = {}) => {
1400
+ const RECORDING_PHASE = $.RECORDING_PHASE;
1401
+ $.startRule();
1402
+
1403
+ let left: Node = $.SUBRULE($.mathValue, { ARGS: [ctx] });
1404
+
1405
+ while ($.isType(T.Star) || $.isType(T.Divide)) {
1406
+ const op: IToken = $.isType(T.Star)
1407
+ ? $.CONSUME(T.Star)
1408
+ : $.CONSUME(T.Divide);
1409
+ const right: Node = $.SUBRULE2($.mathValue, { ARGS: [ctx] });
1410
+
1411
+ if (!RECORDING_PHASE) {
1412
+ const opStr = toOperator(op.image);
1413
+ left = new Operation([left, opStr, right], { inCalc: true }, undefined, $.context);
1414
+ }
1415
+ }
1416
+
1417
+ if (RECORDING_PHASE) {
1418
+ return;
1419
+ }
1420
+ left._location = $.endRule();
1421
+ return left;
1422
+ };
1423
+ }
1424
+
1425
+ export function mathSum(this: P, T: TokenMap): ProductionRule {
1426
+ const $ = this;
1427
+ return (ctx: RuleContext = {}) => {
1428
+ const RECORDING_PHASE = $.RECORDING_PHASE;
1429
+ $.startRule();
1430
+
1431
+ let left: Node = $.SUBRULE($.mathProduct, { ARGS: [ctx] });
1432
+
1433
+ $.MANY(() => {
1434
+ const op: IToken = $.CONSUME(T.AdditionOperator);
1435
+ const right: Node = $.SUBRULE2($.mathProduct, { ARGS: [ctx] });
1436
+
1437
+ if (!RECORDING_PHASE) {
1438
+ const opStr = toOperator(op.image);
1439
+ left = new Operation([left, opStr, right], { inCalc: true }, undefined, $.context);
1440
+ }
1441
+ });
1442
+
1443
+ if (RECORDING_PHASE) {
1444
+ return;
1445
+ }
1446
+ left._location = $.endRule();
1447
+ return left;
1448
+ };
1449
+ }