@jesscss/less-parser 2.0.0-alpha.6 → 2.0.0-alpha.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +149 -36
- package/lib/builders.d.ts +381 -0
- package/lib/builders.d.ts.map +1 -0
- package/lib/cst.cjs +14 -0
- package/lib/cst.d.ts +6 -0
- package/lib/cst.d.ts.map +1 -0
- package/lib/cst.js +12 -0
- package/lib/functional-parser.cjs +2654 -0
- package/lib/functional-parser.d.ts +18 -0
- package/lib/functional-parser.d.ts.map +1 -0
- package/lib/functional-parser.js +2589 -0
- package/lib/grammar.cjs +35057 -0
- package/lib/grammar.d.ts +2 -0
- package/lib/grammar.d.ts.map +1 -0
- package/lib/grammar.js +35056 -0
- package/lib/index.cjs +17 -4096
- package/lib/index.d.ts +9 -149
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +6 -4091
- package/lib/jess.cjs +3968 -0
- package/lib/jess.d.ts +7 -0
- package/lib/jess.d.ts.map +1 -0
- package/lib/jess.js +3962 -0
- package/lib/lessParser.d.ts +38 -0
- package/lib/lessParser.d.ts.map +1 -0
- package/lib/lessRecursiveParser.d.ts +99 -0
- package/lib/lessRecursiveParser.d.ts.map +1 -0
- package/lib/lessTokens.d.ts +23 -0
- package/lib/lessTokens.d.ts.map +1 -0
- package/lib/productions/guards.d.ts +113 -0
- package/lib/productions/guards.d.ts.map +1 -0
- package/lib/productions/index.d.ts +5 -0
- package/lib/productions/index.d.ts.map +1 -0
- package/lib/productions/root.d.ts +38 -0
- package/lib/productions/root.d.ts.map +1 -0
- package/lib/productions/selectors.d.ts +41 -0
- package/lib/productions/selectors.d.ts.map +1 -0
- package/lib/productions/values.d.ts +35 -0
- package/lib/productions/values.d.ts.map +1 -0
- package/lib/utils.d.ts +9 -0
- package/lib/utils.d.ts.map +1 -0
- package/package.json +40 -9
- package/src/__tests__/debug-log.ts +35 -0
- package/src/__tests__/wall5-parse.test.ts +67 -0
- package/src/builders.ts +3190 -0
- package/src/cst.ts +25 -0
- package/src/functional-parser.ts +162 -0
- package/src/grammar.ts +870 -0
- package/src/index.ts +19 -0
- package/src/jess.ts +6 -0
- package/src/lessParser.ts +120 -0
- package/src/lessRecursiveParser.ts +279 -0
- package/src/lessTokens.ts +350 -0
- package/src/productions/guards.ts +1066 -0
- package/src/productions/index.ts +29 -0
- package/src/productions/root.ts +1613 -0
- package/src/productions/selectors.ts +1309 -0
- package/src/productions/values.ts +1449 -0
- package/src/utils.ts +178 -0
- package/lib/index.d.cts +0 -150
- package/lib/index.d.cts.map +0 -1
- 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
|
+
}
|