@jesscss/less-parser 1.0.8-alpha.6 → 2.0.0-alpha.2
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 +34 -1
- package/lib/__tests__/debug-log.d.ts +1 -0
- package/lib/__tests__/debug-log.js +34 -0
- package/lib/__tests__/debug-log.js.map +1 -0
- package/lib/index.d.ts +30 -8
- package/lib/index.js +66 -29
- package/lib/index.js.map +1 -0
- package/lib/lessActionsParser.d.ts +156 -0
- package/lib/lessActionsParser.js +145 -0
- package/lib/lessActionsParser.js.map +1 -0
- package/lib/lessErrorMessageProvider.d.ts +3 -0
- package/lib/lessErrorMessageProvider.js +4 -0
- package/lib/lessErrorMessageProvider.js.map +1 -0
- package/lib/lessTokens.d.ts +22 -3
- package/lib/lessTokens.js +242 -148
- package/lib/lessTokens.js.map +1 -0
- package/lib/productions.d.ts +181 -0
- package/lib/productions.js +3521 -0
- package/lib/productions.js.map +1 -0
- package/lib/utils.d.ts +2 -0
- package/lib/utils.js +88 -0
- package/lib/utils.js.map +1 -0
- package/package.json +41 -19
- package/lib/lessParser.d.ts +0 -36
- package/lib/lessParser.js +0 -49
- package/lib/productions/atRules.d.ts +0 -2
- package/lib/productions/atRules.js +0 -160
- package/lib/productions/blocks.d.ts +0 -2
- package/lib/productions/blocks.js +0 -60
- package/lib/productions/declarations.d.ts +0 -2
- package/lib/productions/declarations.js +0 -36
- package/lib/productions/interpolation.d.ts +0 -2
- package/lib/productions/interpolation.js +0 -18
- package/lib/productions/mixin.d.ts +0 -2
- package/lib/productions/mixin.js +0 -373
- package/lib/productions/root.d.ts +0 -2
- package/lib/productions/root.js +0 -33
- package/lib/productions/selectors.d.ts +0 -2
- package/lib/productions/selectors.js +0 -91
- package/lib/productions/values.d.ts +0 -2
- package/lib/productions/values.js +0 -305
|
@@ -0,0 +1,3521 @@
|
|
|
1
|
+
import { tokenMatcher, EMPTY_ALT, NoViableAltException } from 'chevrotain';
|
|
2
|
+
import { main as cssMain, declaration as cssDeclaration, mediaInParens as cssMediaInParens, complexSelector as cssComplexSelector, nthValue as cssNthValue, knownFunctions as cssKnownFunctions, mathValue as cssMathValue, innerAtRule as cssInnerAtRule, productions as cssProductions } from '@jesscss/css-parser';
|
|
3
|
+
import { Node, Ampersand, Block, Any, Ruleset, BasicSelector, Combinator, List, Sequence, Call, Paren, Operation, Quoted, AtRule, Interpolated, InterpolatedSelector, Reference, Dimension, Num, Extend, Negative, Mixin, Condition, VarDeclaration, DefaultGuard, Rest, StyleImport, Expression, SelectorCapture, ComplexSelector, CompoundSelector, SelectorList, Rules, Nil, Collection, INTERPOLATION_PLACEHOLDER, isNode, shouldOperateWithMathFrames } from '@jesscss/core';
|
|
4
|
+
import { getInterpolatedOrString } from './utils.js';
|
|
5
|
+
import { all } from 'known-css-properties';
|
|
6
|
+
function getParenFrames(ctx) {
|
|
7
|
+
return ctx?.parenFrames ?? [];
|
|
8
|
+
}
|
|
9
|
+
function getCalcFrames(ctx) {
|
|
10
|
+
return ctx?.calcFrames ?? 0;
|
|
11
|
+
}
|
|
12
|
+
function withCalcFrame(ctx, delta) {
|
|
13
|
+
const calcFrames = getCalcFrames(ctx) + delta;
|
|
14
|
+
return { ...(ctx ?? {}), calcFrames };
|
|
15
|
+
}
|
|
16
|
+
function guardContainsDefaultCall(node) {
|
|
17
|
+
if (!node) {
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
const isNodeLike = (value) => {
|
|
21
|
+
return Boolean(value
|
|
22
|
+
&& typeof value === 'object'
|
|
23
|
+
&& typeof value.type === 'string'
|
|
24
|
+
&& typeof value.valueOf === 'function');
|
|
25
|
+
};
|
|
26
|
+
const queue = [node];
|
|
27
|
+
const seen = new Set();
|
|
28
|
+
while (queue.length > 0) {
|
|
29
|
+
const current = queue.shift();
|
|
30
|
+
if (!current || seen.has(current) || !isNodeLike(current)) {
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
seen.add(current);
|
|
34
|
+
if (current.type === 'DefaultGuard') {
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
37
|
+
if (current.type === 'Call') {
|
|
38
|
+
const callName = current.value.name;
|
|
39
|
+
const callNameStr = String(callName?.valueOf?.() ?? callName ?? '');
|
|
40
|
+
if (callNameStr === 'default' || callNameStr === '??') {
|
|
41
|
+
return true;
|
|
42
|
+
}
|
|
43
|
+
const key = callName?.value?.key;
|
|
44
|
+
const keyStr = String(key?.valueOf?.() ?? key ?? '');
|
|
45
|
+
if (keyStr === 'default' || keyStr === '??') {
|
|
46
|
+
return true;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
const value = current.value;
|
|
50
|
+
if (Array.isArray(value)) {
|
|
51
|
+
queue.push(...value);
|
|
52
|
+
}
|
|
53
|
+
else if (value && typeof value === 'object') {
|
|
54
|
+
queue.push(...Object.values(value));
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
function isDefaultGuardCall(node) {
|
|
60
|
+
if (!node || node.type !== 'Call') {
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
const callName = node.value.name;
|
|
64
|
+
const callNameStr = String(callName?.valueOf?.() ?? callName ?? '');
|
|
65
|
+
if (callNameStr === 'default' || callNameStr === '??') {
|
|
66
|
+
return true;
|
|
67
|
+
}
|
|
68
|
+
const key = callName?.value?.key;
|
|
69
|
+
const keyStr = String(key?.valueOf?.() ?? key ?? '');
|
|
70
|
+
return keyStr === 'default' || keyStr === '??';
|
|
71
|
+
}
|
|
72
|
+
function loc(node) {
|
|
73
|
+
const location = node.location;
|
|
74
|
+
return location.length === 6 ? location : undefined;
|
|
75
|
+
}
|
|
76
|
+
function wrapOuterExpressionIfNeeded(node, ctx) {
|
|
77
|
+
if (!this.wrapOuterExpressions) {
|
|
78
|
+
return node;
|
|
79
|
+
}
|
|
80
|
+
if (!ctx?.wrapInExpression) {
|
|
81
|
+
return node;
|
|
82
|
+
}
|
|
83
|
+
// Expressions should never contain Expressions; avoid nesting.
|
|
84
|
+
if (node instanceof Expression) {
|
|
85
|
+
return node;
|
|
86
|
+
}
|
|
87
|
+
// Math expressions: only wrap if this operation would actually be performed.
|
|
88
|
+
if (isNode(node, 'Operation')) {
|
|
89
|
+
const [left, op, right] = node.value;
|
|
90
|
+
const mathMode = this.mathMode ?? 'parens-division';
|
|
91
|
+
const shouldOperate = shouldOperateWithMathFrames({
|
|
92
|
+
mathMode,
|
|
93
|
+
parenFrames: getParenFrames(ctx),
|
|
94
|
+
calcFrames: getCalcFrames(ctx)
|
|
95
|
+
}, op, left, right);
|
|
96
|
+
if (shouldOperate) {
|
|
97
|
+
return new Expression(node, { parens: true }, loc(node), this.context);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return node;
|
|
101
|
+
}
|
|
102
|
+
const isEscapedString = function (T) {
|
|
103
|
+
const next = this.LA(1);
|
|
104
|
+
return tokenMatcher(next, T.QuoteStart) && next.image.startsWith('~');
|
|
105
|
+
};
|
|
106
|
+
/** Charset moved within `main` (explained in that rule) */
|
|
107
|
+
export function stylesheet(T) {
|
|
108
|
+
const $ = this;
|
|
109
|
+
// stylesheet
|
|
110
|
+
// : CHARSET? main EOF
|
|
111
|
+
// ;
|
|
112
|
+
return (options = {}) => {
|
|
113
|
+
let RECORDING_PHASE = $.RECORDING_PHASE;
|
|
114
|
+
let context;
|
|
115
|
+
if (!RECORDING_PHASE) {
|
|
116
|
+
if (options.context) {
|
|
117
|
+
context = this._context = options.context;
|
|
118
|
+
}
|
|
119
|
+
else {
|
|
120
|
+
context = this.context;
|
|
121
|
+
}
|
|
122
|
+
// Less may set scope in context.opts; TreeContext has no scope
|
|
123
|
+
}
|
|
124
|
+
let charset;
|
|
125
|
+
$.OPTION({
|
|
126
|
+
GATE: () => !$.looseMode,
|
|
127
|
+
DEF: () => {
|
|
128
|
+
charset = $.CONSUME(T.Charset);
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
const ctx = { isRoot: true };
|
|
132
|
+
let root = $.SUBRULE($.main, { ARGS: [ctx] });
|
|
133
|
+
if (!RECORDING_PHASE) {
|
|
134
|
+
let rules = root?.value;
|
|
135
|
+
if (charset) {
|
|
136
|
+
let loc = $.getLocationInfo(charset);
|
|
137
|
+
let rootLoc = root.location;
|
|
138
|
+
rules.unshift(new Any(charset.image, { role: 'charset' }, loc, context));
|
|
139
|
+
rootLoc[0] = loc[0];
|
|
140
|
+
rootLoc[1] = loc[1];
|
|
141
|
+
rootLoc[2] = loc[2];
|
|
142
|
+
}
|
|
143
|
+
return root;
|
|
144
|
+
}
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Starts with a colon, with these conditions
|
|
149
|
+
* 1. It is not preceded by a space or
|
|
150
|
+
* 2. If it is preceded by a space, then it is
|
|
151
|
+
* followed by a space.
|
|
152
|
+
*/
|
|
153
|
+
function isVariableLike(T) {
|
|
154
|
+
const $ = this;
|
|
155
|
+
let token = $.LA(2);
|
|
156
|
+
let isColon = token.tokenType === T.Colon;
|
|
157
|
+
let isParen = token.tokenType === T.LParen;
|
|
158
|
+
let postToken = $.LA(3);
|
|
159
|
+
// During recording phase, preSkippedTokenMap might not exist
|
|
160
|
+
if (!$.preSkippedTokenMap) {
|
|
161
|
+
return false;
|
|
162
|
+
}
|
|
163
|
+
if (!isColon && !isParen) {
|
|
164
|
+
return false;
|
|
165
|
+
}
|
|
166
|
+
let isVariable = !$.preSkippedTokenMap.has(token.startOffset)
|
|
167
|
+
|| (isColon && $.preSkippedTokenMap.has(postToken.startOffset));
|
|
168
|
+
return isVariable;
|
|
169
|
+
}
|
|
170
|
+
;
|
|
171
|
+
export function main(T) {
|
|
172
|
+
const $ = this;
|
|
173
|
+
const ruleAlt = (ctx = {}) => {
|
|
174
|
+
let isVariable = isVariableLike.call(this, T);
|
|
175
|
+
return [
|
|
176
|
+
{ ALT: () => $.SUBRULE($.functionCall, { ARGS: [ctx] }) },
|
|
177
|
+
{ ALT: () => $.SUBRULE($.ampersandExtend, { ARGS: [ctx] }) },
|
|
178
|
+
{
|
|
179
|
+
GATE: () => {
|
|
180
|
+
let next = $.LA(1).tokenType;
|
|
181
|
+
return next === T.DotName || next === T.HashName || next === T.ColorIdentStart;
|
|
182
|
+
},
|
|
183
|
+
ALT: () => $.SUBRULE($.mixinOrQualifiedRule, { ARGS: [ctx] })
|
|
184
|
+
},
|
|
185
|
+
{
|
|
186
|
+
GATE: () => {
|
|
187
|
+
let next = $.LA(1).tokenType;
|
|
188
|
+
return next !== T.DotName
|
|
189
|
+
&& next !== T.HashName
|
|
190
|
+
&& next !== T.ColorIdentStart;
|
|
191
|
+
},
|
|
192
|
+
ALT: () => $.SUBRULE($.qualifiedRule, { ARGS: [ctx] })
|
|
193
|
+
},
|
|
194
|
+
{
|
|
195
|
+
GATE: () => isVariable,
|
|
196
|
+
ALT: () => $.SUBRULE($.varDeclarationOrCall, { ARGS: [ctx] })
|
|
197
|
+
},
|
|
198
|
+
{
|
|
199
|
+
GATE: () => !isVariable,
|
|
200
|
+
ALT: () => $.SUBRULE($.atRule, { ARGS: [ctx] })
|
|
201
|
+
},
|
|
202
|
+
/**
|
|
203
|
+
* Historically, Less allows `@charset` anywhere,
|
|
204
|
+
* to avoid outputting it in the wrong place.
|
|
205
|
+
* Ideally, this would result in an error if, say,
|
|
206
|
+
* the `@charset` was defined at the bottom of the file,
|
|
207
|
+
* but that wasn't the solution made.
|
|
208
|
+
* @see https://github.com/less/less.js/issues/2126
|
|
209
|
+
*/
|
|
210
|
+
{
|
|
211
|
+
GATE: () => $.looseMode,
|
|
212
|
+
ALT: () => $.CONSUME(T.Charset)
|
|
213
|
+
},
|
|
214
|
+
{ ALT: () => $.CONSUME2(T.Semi) }
|
|
215
|
+
// { ALT: () => $.SUBRULE($.mixinCall) }
|
|
216
|
+
];
|
|
217
|
+
};
|
|
218
|
+
return (ctx = {}) => {
|
|
219
|
+
let RECORDING_PHASE = $.RECORDING_PHASE;
|
|
220
|
+
let context;
|
|
221
|
+
if (!RECORDING_PHASE) {
|
|
222
|
+
context = this.context;
|
|
223
|
+
}
|
|
224
|
+
let rules;
|
|
225
|
+
if (!RECORDING_PHASE) {
|
|
226
|
+
rules = [];
|
|
227
|
+
}
|
|
228
|
+
let requiredSemi = false;
|
|
229
|
+
let lastRule;
|
|
230
|
+
/**
|
|
231
|
+
* In this production rule, semi-colons are not required
|
|
232
|
+
* but this is repurposed by declarationList and by Less / Sass,
|
|
233
|
+
* so that's why this gate is here.
|
|
234
|
+
*/
|
|
235
|
+
$.MANY({
|
|
236
|
+
GATE: () => !requiredSemi || (requiredSemi && ($.LA(1).tokenType === T.Semi
|
|
237
|
+
|| $.LA(0).tokenType === T.Semi)),
|
|
238
|
+
DEF: () => {
|
|
239
|
+
const localAlt = typeof ruleAlt === 'function' ? ruleAlt(ctx) : ruleAlt;
|
|
240
|
+
let value = $.OR(localAlt);
|
|
241
|
+
if (!RECORDING_PHASE) {
|
|
242
|
+
/** @todo - When do we not have a value? */
|
|
243
|
+
if (value) {
|
|
244
|
+
if (!(value instanceof Node)) {
|
|
245
|
+
/** This is a semi-colon or charset token */
|
|
246
|
+
if (value.image.includes('@charset')) {
|
|
247
|
+
rules.push(new Any(value.image, { role: 'charset' }, $.getLocationInfo(value), context));
|
|
248
|
+
}
|
|
249
|
+
else {
|
|
250
|
+
if (lastRule) {
|
|
251
|
+
lastRule.options.semi = true;
|
|
252
|
+
}
|
|
253
|
+
else {
|
|
254
|
+
rules.push(new Any(';', { role: 'semi' }, $.getLocationInfo($.LA(1)), context));
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
else {
|
|
259
|
+
requiredSemi = !!value.requiredSemi;
|
|
260
|
+
rules.push(value);
|
|
261
|
+
lastRule = value;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
});
|
|
267
|
+
if (!RECORDING_PHASE) {
|
|
268
|
+
// Process any extendNodes that were set (e.g., by ampersandExtend at root level)
|
|
269
|
+
if (ctx.extendNodes && ctx.extendNodes.length > 0) {
|
|
270
|
+
// Filter out Nil nodes (returned by ampersandExtend to avoid duplication)
|
|
271
|
+
const filteredRules = rules.filter(r => !(r instanceof Nil));
|
|
272
|
+
rules = [...ctx.extendNodes, ...filteredRules];
|
|
273
|
+
ctx.extendNodes = undefined;
|
|
274
|
+
}
|
|
275
|
+
let returnNode = $.getRulesWithComments(rules, $.getLocationInfo($.LA(1)));
|
|
276
|
+
// Attaches remaining whitespace at the end of rules
|
|
277
|
+
const wrapped = $.wrap(returnNode, true);
|
|
278
|
+
return wrapped;
|
|
279
|
+
}
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
export function declarationList(T) {
|
|
283
|
+
const $ = this;
|
|
284
|
+
let ruleAlt = (ctx = {}) => {
|
|
285
|
+
let isVariable = isVariableLike.call(this, T);
|
|
286
|
+
return [
|
|
287
|
+
{
|
|
288
|
+
ALT: () => {
|
|
289
|
+
return $.SUBRULE($.declaration, { ARGS: [ctx] });
|
|
290
|
+
}
|
|
291
|
+
},
|
|
292
|
+
{ ALT: () => $.SUBRULE($.ampersandExtend, { ARGS: [ctx] }) },
|
|
293
|
+
{
|
|
294
|
+
ALT: () => {
|
|
295
|
+
const fnCall = $.SUBRULE($.functionCall, { ARGS: [ctx] });
|
|
296
|
+
if (!$.RECORDING_PHASE && fnCall instanceof Call) {
|
|
297
|
+
// Less allows function calls like `each(...){...}` in declaration lists
|
|
298
|
+
// without a required trailing semicolon.
|
|
299
|
+
fnCall.requiredSemi = false;
|
|
300
|
+
}
|
|
301
|
+
return fnCall;
|
|
302
|
+
}
|
|
303
|
+
},
|
|
304
|
+
{
|
|
305
|
+
GATE: () => isVariable,
|
|
306
|
+
ALT: () => $.SUBRULE($.varDeclarationOrCall, { ARGS: [ctx] })
|
|
307
|
+
},
|
|
308
|
+
{
|
|
309
|
+
GATE: () => !isVariable,
|
|
310
|
+
ALT: () => $.SUBRULE($.innerAtRule, { ARGS: [ctx] })
|
|
311
|
+
},
|
|
312
|
+
{
|
|
313
|
+
GATE: () => {
|
|
314
|
+
let next = $.LA(1).tokenType;
|
|
315
|
+
return next === T.DotName || next === T.HashName || next === T.ColorIdentStart;
|
|
316
|
+
},
|
|
317
|
+
ALT: () => {
|
|
318
|
+
return $.SUBRULE($.mixinOrQualifiedRule, { ARGS: [{ ...ctx, inner: true }] });
|
|
319
|
+
}
|
|
320
|
+
},
|
|
321
|
+
{
|
|
322
|
+
GATE: () => {
|
|
323
|
+
let next = $.LA(1).tokenType;
|
|
324
|
+
return next !== T.DotName
|
|
325
|
+
&& next !== T.HashName
|
|
326
|
+
&& next !== T.ColorIdentStart;
|
|
327
|
+
},
|
|
328
|
+
ALT: () => {
|
|
329
|
+
return $.SUBRULE($.qualifiedRule, { ARGS: [{ ...ctx, inner: true }] });
|
|
330
|
+
}
|
|
331
|
+
},
|
|
332
|
+
{ ALT: () => $.CONSUME2(T.Semi) }
|
|
333
|
+
];
|
|
334
|
+
};
|
|
335
|
+
return (ctx = {}) => cssMain.call(this, T, ruleAlt)(ctx);
|
|
336
|
+
}
|
|
337
|
+
// Wrapper to ensure a mixin call in declaration context ends with a semicolon
|
|
338
|
+
// export function mixinCallStatement(this: P, T: TokenMap) {
|
|
339
|
+
// const $ = this;
|
|
340
|
+
// return () => {
|
|
341
|
+
// const node = $.SUBRULE($.mixinCall);
|
|
342
|
+
// // If mixinCall did not consume a semicolon, require one now
|
|
343
|
+
// if ($.LA(1).tokenType === T.Semi) {
|
|
344
|
+
// $.CONSUME(T.Semi);
|
|
345
|
+
// }
|
|
346
|
+
// return node;
|
|
347
|
+
// };
|
|
348
|
+
// }
|
|
349
|
+
let interpolatedRegex = /([$@]){([^}]+)}/g;
|
|
350
|
+
const createInterpolatedReference = (prefix, value, location, context) => {
|
|
351
|
+
const isProperty = prefix === '$';
|
|
352
|
+
const key = isProperty
|
|
353
|
+
? new Quoted(value, { quote: '\'' }, location, context)
|
|
354
|
+
: value;
|
|
355
|
+
return new Reference({ key }, { type: isProperty ? 'property' : 'variable', role: 'ident' }, location, context);
|
|
356
|
+
};
|
|
357
|
+
const getInterpolated = (name, location, context) => {
|
|
358
|
+
const replacements = [];
|
|
359
|
+
let result;
|
|
360
|
+
let source = name;
|
|
361
|
+
while (result = interpolatedRegex.exec(name)) {
|
|
362
|
+
const [match, propOrVar, value] = result;
|
|
363
|
+
source = source.replace(match, INTERPOLATION_PLACEHOLDER);
|
|
364
|
+
const reference = createInterpolatedReference(propOrVar, value, location, context);
|
|
365
|
+
replacements.push(reference);
|
|
366
|
+
}
|
|
367
|
+
return new Interpolated({ source, replacements }, { role: 'ident' }, location, context);
|
|
368
|
+
};
|
|
369
|
+
export function declaration(T) {
|
|
370
|
+
const $ = this;
|
|
371
|
+
let ruleAlt = (ctx = {}) => [
|
|
372
|
+
{
|
|
373
|
+
ALT: () => {
|
|
374
|
+
let name;
|
|
375
|
+
$.OR2([
|
|
376
|
+
{
|
|
377
|
+
ALT: () => {
|
|
378
|
+
name = $.CONSUME(T.Ident);
|
|
379
|
+
}
|
|
380
|
+
},
|
|
381
|
+
{
|
|
382
|
+
GATE: () => $.legacyMode,
|
|
383
|
+
ALT: () => name = $.CONSUME(T.LegacyPropIdent)
|
|
384
|
+
}
|
|
385
|
+
]);
|
|
386
|
+
let assign = $.CONSUME(T.Assign);
|
|
387
|
+
let value = $.SUBRULE($.valueList, { ARGS: [ctx] });
|
|
388
|
+
let important;
|
|
389
|
+
$.OPTION(() => {
|
|
390
|
+
important = $.CONSUME(T.Important);
|
|
391
|
+
});
|
|
392
|
+
if (!$.RECORDING_PHASE) {
|
|
393
|
+
let nameNode;
|
|
394
|
+
let nameValue = name.image;
|
|
395
|
+
if (nameValue.includes('@') || nameValue.includes('$')) {
|
|
396
|
+
nameNode = getInterpolated(nameValue, $.getLocationInfo(name), this.context);
|
|
397
|
+
}
|
|
398
|
+
else {
|
|
399
|
+
nameNode = $.wrap(new Any(name.image, { role: 'property' }, $.getLocationInfo(name), this.context), true);
|
|
400
|
+
}
|
|
401
|
+
return [nameNode, assign, value, important];
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
},
|
|
405
|
+
{
|
|
406
|
+
ALT: () => {
|
|
407
|
+
let RECORDING_PHASE = $.RECORDING_PHASE;
|
|
408
|
+
let nodes;
|
|
409
|
+
if (!RECORDING_PHASE) {
|
|
410
|
+
nodes = [];
|
|
411
|
+
}
|
|
412
|
+
let name = $.OR3([
|
|
413
|
+
{ ALT: () => $.CONSUME(T.InterpolatedCustomProperty) },
|
|
414
|
+
{ ALT: () => $.CONSUME(T.CustomProperty) }
|
|
415
|
+
]);
|
|
416
|
+
let assign = $.CONSUME2(T.Assign);
|
|
417
|
+
$.startRule();
|
|
418
|
+
$.MANY(() => {
|
|
419
|
+
let val = $.SUBRULE($.customValue, { ARGS: [{ ...ctx, inCustomPropertyValue: true }] });
|
|
420
|
+
if (!RECORDING_PHASE) {
|
|
421
|
+
nodes.push(val);
|
|
422
|
+
}
|
|
423
|
+
});
|
|
424
|
+
if (!RECORDING_PHASE) {
|
|
425
|
+
let location = $.endRule();
|
|
426
|
+
let nameNode;
|
|
427
|
+
let nameValue = name.image;
|
|
428
|
+
if (nameValue.includes('@') || nameValue.includes('$')) {
|
|
429
|
+
nameNode = getInterpolated(nameValue, $.getLocationInfo(name), this.context);
|
|
430
|
+
}
|
|
431
|
+
else {
|
|
432
|
+
nameNode = $.wrap(new Any(name.image, { role: 'property' }, $.getLocationInfo(name), this.context), true);
|
|
433
|
+
}
|
|
434
|
+
let value = new Sequence(nodes, undefined, location, this.context);
|
|
435
|
+
return [nameNode, assign, value];
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
];
|
|
440
|
+
return (ctx = {}) => {
|
|
441
|
+
return cssDeclaration.call(this, T, ruleAlt)(ctx);
|
|
442
|
+
};
|
|
443
|
+
}
|
|
444
|
+
export function mediaInParens(T) {
|
|
445
|
+
const $ = this;
|
|
446
|
+
let isEscaped = isEscapedString.bind(this, T);
|
|
447
|
+
return (ctx = {}) => $.OR([
|
|
448
|
+
/**
|
|
449
|
+
* It's up to the Less author to validate that this will produce
|
|
450
|
+
* valid media queries.
|
|
451
|
+
*/
|
|
452
|
+
{
|
|
453
|
+
/** Allow escaped strings */
|
|
454
|
+
GATE: isEscaped,
|
|
455
|
+
ALT: () => $.SUBRULE($.string, { ARGS: [ctx] })
|
|
456
|
+
},
|
|
457
|
+
/**
|
|
458
|
+
* After Less evaluation, should throw an error
|
|
459
|
+
* if the value of `@myvar` is a ruleset
|
|
460
|
+
*/
|
|
461
|
+
{
|
|
462
|
+
ALT: () => {
|
|
463
|
+
return $.SUBRULE($.valueReference, { ARGS: [{ ...ctx, requireAccessorsAfterMixinCall: true }] });
|
|
464
|
+
}
|
|
465
|
+
},
|
|
466
|
+
{
|
|
467
|
+
ALT: cssMediaInParens.call(this, T)
|
|
468
|
+
}
|
|
469
|
+
]);
|
|
470
|
+
}
|
|
471
|
+
export function containerInParens(_T, _alt) {
|
|
472
|
+
const $ = this;
|
|
473
|
+
// Reuse mediaInParens which already handles variables
|
|
474
|
+
return (ctx = {}) => $.SUBRULE($.mediaInParens, { ARGS: [ctx] });
|
|
475
|
+
}
|
|
476
|
+
export function mfValue(_T) {
|
|
477
|
+
const $ = this;
|
|
478
|
+
return (ctx = {}) =>
|
|
479
|
+
/**
|
|
480
|
+
* Like the original Less Parser, we're
|
|
481
|
+
* going to allow any value expression,
|
|
482
|
+
* and it's up to the Less author to know
|
|
483
|
+
* if it's valid.
|
|
484
|
+
*/
|
|
485
|
+
(() => {
|
|
486
|
+
const exprCtx = { ...ctx, wrapInExpression: true };
|
|
487
|
+
const node = $.SUBRULE($.expressionSum, { ARGS: [exprCtx] });
|
|
488
|
+
if (!$.RECORDING_PHASE) {
|
|
489
|
+
return wrapOuterExpressionIfNeeded.call($, node, exprCtx);
|
|
490
|
+
}
|
|
491
|
+
return node;
|
|
492
|
+
})();
|
|
493
|
+
}
|
|
494
|
+
export function mfNonIdentifierValue(T, _alt) {
|
|
495
|
+
const $ = this;
|
|
496
|
+
return (ctx = {}) => $.OR([
|
|
497
|
+
{
|
|
498
|
+
GATE: () => {
|
|
499
|
+
const next = $.LA(1);
|
|
500
|
+
return next.tokenType === T.AtKeyword || next.tokenType === T.PropertyReference || next.tokenType === T.NestedReference;
|
|
501
|
+
},
|
|
502
|
+
ALT: () => $.SUBRULE($.valueReference, { ARGS: [{ ...ctx, requireAccessorsAfterMixinCall: true }] })
|
|
503
|
+
},
|
|
504
|
+
{
|
|
505
|
+
ALT: () => {
|
|
506
|
+
$.startRule();
|
|
507
|
+
let num1 = $.CONSUME(T.Number);
|
|
508
|
+
let num2;
|
|
509
|
+
$.OPTION(() => {
|
|
510
|
+
$.CONSUME(T.Slash);
|
|
511
|
+
num2 = $.CONSUME2(T.Number);
|
|
512
|
+
});
|
|
513
|
+
if (!$.RECORDING_PHASE) {
|
|
514
|
+
let location = $.endRule();
|
|
515
|
+
let num1Node = $.wrap($.processValueToken(num1), 'both');
|
|
516
|
+
if (!num2) {
|
|
517
|
+
return num1Node;
|
|
518
|
+
}
|
|
519
|
+
let num2Node = $.wrap($.processValueToken(num2), 'both');
|
|
520
|
+
return new List([num1Node, num2Node], { sep: '/' }, location, this.context);
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
},
|
|
524
|
+
{
|
|
525
|
+
ALT: () => {
|
|
526
|
+
let dim = $.CONSUME(T.Dimension);
|
|
527
|
+
if (!$.RECORDING_PHASE) {
|
|
528
|
+
return $.wrap($.processValueToken(dim), 'both');
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
]);
|
|
533
|
+
}
|
|
534
|
+
export function wrappedDeclarationList(T) {
|
|
535
|
+
const $ = this;
|
|
536
|
+
return (ctx = {}) => {
|
|
537
|
+
$.CONSUME(T.LCurly);
|
|
538
|
+
let rules = $.SUBRULE($.declarationList, { ARGS: [ctx] });
|
|
539
|
+
$.CONSUME(T.RCurly);
|
|
540
|
+
return rules;
|
|
541
|
+
};
|
|
542
|
+
}
|
|
543
|
+
export function qualifiedRuleBody(T) {
|
|
544
|
+
const $ = this;
|
|
545
|
+
return (ctx = {}) => {
|
|
546
|
+
let RECORDING_PHASE = $.RECORDING_PHASE;
|
|
547
|
+
let selector;
|
|
548
|
+
let isSelectorList;
|
|
549
|
+
if (!RECORDING_PHASE) {
|
|
550
|
+
selector = ctx.selector;
|
|
551
|
+
isSelectorList = ctx.isSelectorList ?? selector instanceof SelectorList;
|
|
552
|
+
}
|
|
553
|
+
let guard;
|
|
554
|
+
$.OPTION4({
|
|
555
|
+
GATE: () => !isSelectorList,
|
|
556
|
+
DEF: () => {
|
|
557
|
+
guard = $.SUBRULE2($.guard, { ARGS: [ctx] });
|
|
558
|
+
}
|
|
559
|
+
});
|
|
560
|
+
$.CONSUME2(T.LCurly);
|
|
561
|
+
// Save extendNodes before parsing declarationList, so nested rulesets don't inherit them
|
|
562
|
+
// Make a copy of the array (not just a reference) so mutations during nested parsing don't affect it
|
|
563
|
+
let savedExtendNodes = ctx.extendNodes ? [...ctx.extendNodes] : undefined;
|
|
564
|
+
ctx.extendNodes = undefined;
|
|
565
|
+
let rules = $.SUBRULE2($.declarationList, { ARGS: [ctx] });
|
|
566
|
+
let end = $.CONSUME2(T.RCurly);
|
|
567
|
+
// After declarationList, check if new extends were added (e.g., by ampersandExtend)
|
|
568
|
+
// If so, merge them with the saved extends; otherwise restore the saved extends
|
|
569
|
+
const newExtends = ctx.extendNodes;
|
|
570
|
+
if (newExtends && newExtends.length) {
|
|
571
|
+
// New extends were added during declarationList (e.g., &:extend())
|
|
572
|
+
if (savedExtendNodes && savedExtendNodes.length > 0) {
|
|
573
|
+
// Merge with saved extends
|
|
574
|
+
ctx.extendNodes = [...savedExtendNodes, ...newExtends];
|
|
575
|
+
}
|
|
576
|
+
else {
|
|
577
|
+
// Keep the new extends
|
|
578
|
+
ctx.extendNodes = newExtends;
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
else {
|
|
582
|
+
// No new extends, restore saved extends
|
|
583
|
+
ctx.extendNodes = savedExtendNodes;
|
|
584
|
+
}
|
|
585
|
+
if (!RECORDING_PHASE) {
|
|
586
|
+
let extend = ctx.extendNodes;
|
|
587
|
+
if (extend?.length) {
|
|
588
|
+
/** If it's not a selector list, then our only extend does not need to be grouped */
|
|
589
|
+
if (!isSelectorList) {
|
|
590
|
+
/** For extends inside rulesets (not bubbled), selector should be undefined
|
|
591
|
+
* so it defaults to ampersand and resolves to the ruleset's selector */
|
|
592
|
+
for (let e of extend) {
|
|
593
|
+
e.value.selector = undefined;
|
|
594
|
+
}
|
|
595
|
+
rules.value = [...extend, ...rules.value];
|
|
596
|
+
ctx.extendNodes = undefined;
|
|
597
|
+
}
|
|
598
|
+
else {
|
|
599
|
+
const selectorList = selector;
|
|
600
|
+
const selectorCount = selectorList.value.length;
|
|
601
|
+
const extendCount = extend.length;
|
|
602
|
+
// Determine if extends should bubble up:
|
|
603
|
+
// 1. If any selectors in the list have extends (extendCount < selectorCount)
|
|
604
|
+
// 2. If all selectors have extends but their "all" flags don't match
|
|
605
|
+
let shouldBubble = false;
|
|
606
|
+
if (extendCount < selectorCount) {
|
|
607
|
+
// Some selectors have extends, some don't - bubble up
|
|
608
|
+
shouldBubble = true;
|
|
609
|
+
}
|
|
610
|
+
else if (extendCount === selectorCount) {
|
|
611
|
+
// All selectors have extends - check if flags match
|
|
612
|
+
let finalExtends = groupExtendsByTargetAndFlag(extend);
|
|
613
|
+
if (finalExtends.length === 1) {
|
|
614
|
+
// All extends have same target and flag - can be inside ruleset
|
|
615
|
+
let extendNodes = finalExtends[0];
|
|
616
|
+
let finalExtend = isArray(extendNodes) ? extendNodes[0] : extendNodes;
|
|
617
|
+
finalExtend.value.selector = undefined;
|
|
618
|
+
rules.value = [finalExtend, ...rules.value];
|
|
619
|
+
ctx.extendNodes = undefined;
|
|
620
|
+
}
|
|
621
|
+
else {
|
|
622
|
+
// Multiple extend groups (different targets/flags) - bubble up
|
|
623
|
+
shouldBubble = true;
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
else {
|
|
627
|
+
// extendCount > selectorCount - shouldn't happen, but bubble to be safe
|
|
628
|
+
shouldBubble = true;
|
|
629
|
+
}
|
|
630
|
+
if (shouldBubble) {
|
|
631
|
+
// Keep extends in ctx.extendNodes so they bubble up to qualifiedRule
|
|
632
|
+
// Don't clear ctx.extendNodes - let them bubble
|
|
633
|
+
// The extends will be prepended above the ruleset in qualifiedRule
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
let node = new Ruleset({ selector, rules, guard }, undefined, undefined, this.context);
|
|
638
|
+
let [startOffset, startLine, startColumn] = selector.location;
|
|
639
|
+
let { endOffset, endLine, endColumn } = end;
|
|
640
|
+
node._location = [startOffset, startLine, startColumn, endOffset, endLine, endColumn];
|
|
641
|
+
return node;
|
|
642
|
+
}
|
|
643
|
+
};
|
|
644
|
+
}
|
|
645
|
+
export function qualifiedRule(_T, altContext) {
|
|
646
|
+
const $ = this;
|
|
647
|
+
let selectorAlt = altContext ?? ((ctx) => [
|
|
648
|
+
{
|
|
649
|
+
GATE: () => !ctx.inner,
|
|
650
|
+
ALT: () => {
|
|
651
|
+
let initialQualifiedRule = ctx.qualifiedRule;
|
|
652
|
+
ctx.qualifiedRule = true;
|
|
653
|
+
let rule = $.SUBRULE($.selectorList, { ARGS: [ctx] });
|
|
654
|
+
ctx.qualifiedRule = initialQualifiedRule;
|
|
655
|
+
return rule;
|
|
656
|
+
}
|
|
657
|
+
},
|
|
658
|
+
{
|
|
659
|
+
GATE: () => !!ctx.inner,
|
|
660
|
+
ALT: () => {
|
|
661
|
+
let initialQualifiedRule = ctx.qualifiedRule;
|
|
662
|
+
let initialFirstSelector = ctx.firstSelector;
|
|
663
|
+
ctx.firstSelector = true;
|
|
664
|
+
ctx.qualifiedRule = true;
|
|
665
|
+
let rule = $.SUBRULE($.forgivingSelectorList, { ARGS: [ctx] });
|
|
666
|
+
ctx.qualifiedRule = initialQualifiedRule;
|
|
667
|
+
ctx.firstSelector = initialFirstSelector;
|
|
668
|
+
return rule;
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
]);
|
|
672
|
+
// qualifiedRule
|
|
673
|
+
// : selectorList WS* LCURLY declarationList RCURLY
|
|
674
|
+
// ;
|
|
675
|
+
return (ctx = {}) => {
|
|
676
|
+
// Save parent's extendNodes before parsing selector (which may set extendNodes)
|
|
677
|
+
let savedExtendNodes = ctx.extendNodes ? [...ctx.extendNodes] : undefined;
|
|
678
|
+
// Set extendNodes to a fresh empty array upon entry to this qualifiedRule
|
|
679
|
+
// so nested rulesets don't inherit extends from parent rulesets
|
|
680
|
+
ctx.extendNodes = undefined;
|
|
681
|
+
let selector = $.OR(selectorAlt(ctx));
|
|
682
|
+
// Use the same context object so modifications propagate back
|
|
683
|
+
ctx.selector = selector;
|
|
684
|
+
// Now extendNodes may have been set by extend() during selector parsing
|
|
685
|
+
// Save it for this ruleset, then clear it so nested rulesets don't see it
|
|
686
|
+
let thisExtendNodes = ctx.extendNodes ? [...ctx.extendNodes] : undefined;
|
|
687
|
+
ctx.extendNodes = undefined;
|
|
688
|
+
let rule = $.SUBRULE($.qualifiedRuleBody, { ARGS: [ctx] });
|
|
689
|
+
// After qualifiedRuleBody returns, ctx.extendNodes may contain:
|
|
690
|
+
// 1. Extends that should bubble up (from nested rulesets or this ruleset that didn't match)
|
|
691
|
+
// 2. Nothing (if all extends were processed)
|
|
692
|
+
// Restore this ruleset's extendNodes (from selector parsing) to process them
|
|
693
|
+
const bubblingExtends = ctx.extendNodes; // Extends that should bubble up
|
|
694
|
+
ctx.extendNodes = thisExtendNodes;
|
|
695
|
+
// Restore parent's extendNodes after processing this ruleset's extends
|
|
696
|
+
let parentExtendNodes = savedExtendNodes;
|
|
697
|
+
if (ctx.extendNodes) {
|
|
698
|
+
let qRuleset = rule;
|
|
699
|
+
// Set the Extend nodes' selector to the ruleset's selector (not &)
|
|
700
|
+
// This allows the extends to work correctly when evaluated in the wrapper Rules context
|
|
701
|
+
for (const extendNode of ctx.extendNodes) {
|
|
702
|
+
if (extendNode.value.selector === undefined || extendNode.value.selector.type === 'Ampersand') {
|
|
703
|
+
extendNode.value.selector = selector;
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
/** Prepend a rules block */
|
|
707
|
+
rule = new Rules([
|
|
708
|
+
...ctx.extendNodes,
|
|
709
|
+
qRuleset
|
|
710
|
+
]);
|
|
711
|
+
// Set location from the ruleset (which has proper location info)
|
|
712
|
+
if (qRuleset._location) {
|
|
713
|
+
rule._location = qRuleset._location;
|
|
714
|
+
}
|
|
715
|
+
ctx.extendNodes = undefined;
|
|
716
|
+
}
|
|
717
|
+
// Restore parent's extendNodes and merge with any bubbling extends
|
|
718
|
+
const hasBubblingExtends = bubblingExtends && bubblingExtends.length > 0;
|
|
719
|
+
if (hasBubblingExtends) {
|
|
720
|
+
// Bubble them up to parent
|
|
721
|
+
if (parentExtendNodes && parentExtendNodes.length > 0) {
|
|
722
|
+
ctx.extendNodes = [...parentExtendNodes, ...bubblingExtends];
|
|
723
|
+
}
|
|
724
|
+
else {
|
|
725
|
+
ctx.extendNodes = bubblingExtends;
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
else {
|
|
729
|
+
ctx.extendNodes = parentExtendNodes;
|
|
730
|
+
}
|
|
731
|
+
return rule;
|
|
732
|
+
};
|
|
733
|
+
}
|
|
734
|
+
/**
|
|
735
|
+
* In order to not do any backtracking, anything with a class or id selector start
|
|
736
|
+
* will end up here, and everything else will be shunted to the qualified rule.
|
|
737
|
+
*/
|
|
738
|
+
export function mixinOrQualifiedRule(T) {
|
|
739
|
+
const $ = this;
|
|
740
|
+
// Helper function to convert Any nodes to VarDeclaration nodes for mixin definition parameters
|
|
741
|
+
const convertArgsForDefinition = (args) => {
|
|
742
|
+
if (!args || !args.value) {
|
|
743
|
+
return;
|
|
744
|
+
}
|
|
745
|
+
for (let i = 0; i < args.value.length; i++) {
|
|
746
|
+
const node = args.value[i];
|
|
747
|
+
const location = node.location && node.location.length > 0 ? node.location : undefined;
|
|
748
|
+
// If it's an Any node with role: 'name', convert it to VarDeclaration for mixin definition parameters
|
|
749
|
+
if (node instanceof Any && node.options?.role === 'name') {
|
|
750
|
+
// Reuse the existing Any node but change its role to 'property' for the name
|
|
751
|
+
node.options.role = 'property';
|
|
752
|
+
args.value[i] = new VarDeclaration({
|
|
753
|
+
name: node,
|
|
754
|
+
value: new Nil(undefined, undefined, location, this.context)
|
|
755
|
+
}, { paramVar: true }, location, this.context);
|
|
756
|
+
}
|
|
757
|
+
// Rest nodes with string values can stay as-is for mixin definitions
|
|
758
|
+
}
|
|
759
|
+
};
|
|
760
|
+
// Helper function to convert Any nodes to Reference nodes for mixin call arguments
|
|
761
|
+
const convertArgsForCall = (args) => {
|
|
762
|
+
if (!args || !args.value) {
|
|
763
|
+
return;
|
|
764
|
+
}
|
|
765
|
+
for (let i = 0; i < args.value.length; i++) {
|
|
766
|
+
const node = args.value[i];
|
|
767
|
+
const location = node.location && node.location.length > 0 ? node.location : undefined;
|
|
768
|
+
// If it's an Any node with role: 'name', convert it to Reference for mixin call arguments
|
|
769
|
+
if (node instanceof Any && node.options?.role === 'name') {
|
|
770
|
+
args.value[i] = new Reference({ key: node.value }, { type: 'variable' }, location, this.context);
|
|
771
|
+
}
|
|
772
|
+
else if (node instanceof Rest && typeof node.value === 'string') {
|
|
773
|
+
// If it's a Rest node with a string value, convert it to Rest with Reference for mixin call arguments
|
|
774
|
+
args.value[i] = new Rest(new Reference({ key: node.value }, { type: 'variable' }, location, this.context), undefined, location, this.context);
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
};
|
|
778
|
+
// qualifiedRule
|
|
779
|
+
// : selectorList WS* LCURLY declarationList RCURLY
|
|
780
|
+
// ;
|
|
781
|
+
return (ctx = {}) => {
|
|
782
|
+
let RECORDING_PHASE = $.RECORDING_PHASE;
|
|
783
|
+
$.startRule();
|
|
784
|
+
let selector = $.OR([
|
|
785
|
+
{
|
|
786
|
+
GATE: () => !ctx.inner,
|
|
787
|
+
ALT: () => {
|
|
788
|
+
let initialQualifiedRule = ctx.qualifiedRule;
|
|
789
|
+
ctx.qualifiedRule = true;
|
|
790
|
+
let rule = $.SUBRULE($.selectorList, { ARGS: [ctx] });
|
|
791
|
+
ctx.qualifiedRule = initialQualifiedRule;
|
|
792
|
+
return rule;
|
|
793
|
+
}
|
|
794
|
+
},
|
|
795
|
+
{
|
|
796
|
+
GATE: () => !!ctx.inner,
|
|
797
|
+
ALT: () => {
|
|
798
|
+
let initialQualifiedRule = ctx.qualifiedRule;
|
|
799
|
+
let initialFirstSelector = ctx.firstSelector;
|
|
800
|
+
ctx.firstSelector = true;
|
|
801
|
+
ctx.qualifiedRule = true;
|
|
802
|
+
let rule = $.SUBRULE($.forgivingSelectorList, { ARGS: [ctx] });
|
|
803
|
+
ctx.qualifiedRule = initialQualifiedRule;
|
|
804
|
+
ctx.firstSelector = initialFirstSelector;
|
|
805
|
+
return rule;
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
]);
|
|
809
|
+
let isSelectorList = selector instanceof SelectorList;
|
|
810
|
+
let guard;
|
|
811
|
+
let args;
|
|
812
|
+
let important;
|
|
813
|
+
const createMixinCall = (location) => {
|
|
814
|
+
/** Okay, treat the call as a recursive reference */
|
|
815
|
+
if (RECORDING_PHASE) {
|
|
816
|
+
return;
|
|
817
|
+
}
|
|
818
|
+
let leftNode;
|
|
819
|
+
// If selector is a CompoundSelector, ComplexSelector, or single BasicSelector (but not SelectorList),
|
|
820
|
+
// create a single Reference with the selector instance as the key instead of nested references.
|
|
821
|
+
// This handles cases like .foo.bar() or .foo > .bar() as a single call.
|
|
822
|
+
// Note: .foo().bar() still creates nested calls because .foo() is parsed separately.
|
|
823
|
+
if (!isSelectorList && (selector instanceof CompoundSelector
|
|
824
|
+
|| selector instanceof ComplexSelector
|
|
825
|
+
|| selector instanceof BasicSelector)) {
|
|
826
|
+
// Create a single Reference with the selector instance as the key
|
|
827
|
+
leftNode = new Reference({ key: selector }, { type: 'mixin-ruleset', role: 'name' }, undefined, this.context);
|
|
828
|
+
}
|
|
829
|
+
else {
|
|
830
|
+
// For other cases (like SelectorList or when we need nested references),
|
|
831
|
+
// iterate through selector nodes and create nested references
|
|
832
|
+
for (let s of selector.nodes()) {
|
|
833
|
+
if (s instanceof BasicSelector) {
|
|
834
|
+
leftNode = new Reference({ target: leftNode, key: s.valueOf() }, { type: 'mixin-ruleset', role: 'name' }, undefined, this.context);
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
/** Finally, pass this reference into a call */
|
|
839
|
+
leftNode = new Call({ name: leftNode, args }, { markImportant: !!important }, location, this.context);
|
|
840
|
+
return leftNode;
|
|
841
|
+
};
|
|
842
|
+
let isPossibleMixinDefinition = (selector instanceof BasicSelector && (selector.isClass || selector.isId))
|
|
843
|
+
|| (selector instanceof InterpolatedSelector && (selector.isClass || selector.isId));
|
|
844
|
+
let isPossibleMixinCall = true;
|
|
845
|
+
if (!isSelectorList && !isPossibleMixinDefinition && !RECORDING_PHASE) {
|
|
846
|
+
for (let s of selector.nodes()) {
|
|
847
|
+
/** Keep going until we get to basic selectors. */
|
|
848
|
+
if (s instanceof ComplexSelector || s instanceof CompoundSelector) {
|
|
849
|
+
continue;
|
|
850
|
+
}
|
|
851
|
+
if ((s instanceof BasicSelector && (s.isClass || s.isId))
|
|
852
|
+
|| (s instanceof InterpolatedSelector && (s.isClass || s.isId))
|
|
853
|
+
|| (s instanceof Combinator && (s.value === '>' || s.value === ' '))) {
|
|
854
|
+
continue;
|
|
855
|
+
}
|
|
856
|
+
isPossibleMixinCall = false;
|
|
857
|
+
break;
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
return $.OR2([
|
|
861
|
+
{
|
|
862
|
+
GATE: () => isPossibleMixinDefinition || isPossibleMixinCall,
|
|
863
|
+
ALT: () => {
|
|
864
|
+
args = $.SUBRULE($.mixinArgs, { ARGS: [ctx] });
|
|
865
|
+
let next = $.LA(1).tokenType;
|
|
866
|
+
if (next === T.LCurly || next === T.When) {
|
|
867
|
+
isPossibleMixinCall = false;
|
|
868
|
+
}
|
|
869
|
+
return $.OR3([
|
|
870
|
+
{
|
|
871
|
+
GATE: () => isPossibleMixinDefinition,
|
|
872
|
+
/** Mixin definition */
|
|
873
|
+
ALT: () => {
|
|
874
|
+
$.OPTION(() => {
|
|
875
|
+
guard = $.SUBRULE($.guard, { ARGS: [ctx] });
|
|
876
|
+
});
|
|
877
|
+
$.CONSUME(T.LCurly);
|
|
878
|
+
let rules = $.SUBRULE($.declarationList, { ARGS: [ctx] });
|
|
879
|
+
$.CONSUME(T.RCurly);
|
|
880
|
+
if (!RECORDING_PHASE) {
|
|
881
|
+
// Convert Any nodes to VarDeclaration nodes for mixin definition parameters
|
|
882
|
+
convertArgsForDefinition(args);
|
|
883
|
+
const guardText = String(guard?.toString?.() ?? '');
|
|
884
|
+
const hasDefault = Boolean(ctx.hasDefault) || guardContainsDefaultCall(guard) || guardText.includes('??()');
|
|
885
|
+
const node = new Mixin({ name: selector.valueOf(), params: args, rules, guard }, hasDefault ? { hasDefault: true } : undefined, $.endRule(), this.context);
|
|
886
|
+
ctx.hasDefault = false;
|
|
887
|
+
return node;
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
},
|
|
891
|
+
{
|
|
892
|
+
GATE: () => isPossibleMixinCall,
|
|
893
|
+
/** Mixin call */
|
|
894
|
+
ALT: () => {
|
|
895
|
+
$.OPTION2(() => {
|
|
896
|
+
important = $.CONSUME(T.Important);
|
|
897
|
+
});
|
|
898
|
+
let location;
|
|
899
|
+
if (!RECORDING_PHASE) {
|
|
900
|
+
location = $.endRule();
|
|
901
|
+
// Convert Any nodes to Reference nodes for mixin call arguments
|
|
902
|
+
convertArgsForCall(args);
|
|
903
|
+
}
|
|
904
|
+
let result = $.OPTION3({
|
|
905
|
+
/** in Less legacy mode, mixin calls can happen without a space. */
|
|
906
|
+
GATE: () => {
|
|
907
|
+
let noSpace = $.noSep();
|
|
908
|
+
let next = $.LA(1).tokenType;
|
|
909
|
+
return (noSpace && next === T.LSquare) || ((noSpace || $.looseMode) && next === T.LParen);
|
|
910
|
+
},
|
|
911
|
+
DEF: () => $.SUBRULE($.lookupOrCall, { ARGS: [{ ...ctx, node: createMixinCall(location) }] })
|
|
912
|
+
});
|
|
913
|
+
// Note: Mixin calls without parentheses are handled in the semicolon-terminated ALT below
|
|
914
|
+
return result ?? createMixinCall(location);
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
]);
|
|
918
|
+
}
|
|
919
|
+
},
|
|
920
|
+
{
|
|
921
|
+
/** Parse as qualified rule */
|
|
922
|
+
ALT: () => {
|
|
923
|
+
if (!RECORDING_PHASE) {
|
|
924
|
+
$.endRule();
|
|
925
|
+
}
|
|
926
|
+
let initialSelector = ctx.selector;
|
|
927
|
+
let initialIsSelectorList = ctx.isSelectorList;
|
|
928
|
+
ctx.selector = selector;
|
|
929
|
+
ctx.isSelectorList = isSelectorList;
|
|
930
|
+
let rule = $.SUBRULE($.qualifiedRuleBody, { ARGS: [ctx] });
|
|
931
|
+
ctx.selector = initialSelector;
|
|
932
|
+
ctx.isSelectorList = initialIsSelectorList;
|
|
933
|
+
if (ctx.extendNodes) {
|
|
934
|
+
/** Prepend a rules block */
|
|
935
|
+
let qRule = rule;
|
|
936
|
+
// Set the Extend nodes' selector to the ruleset's selector (not &)
|
|
937
|
+
// This allows the extends to work correctly when evaluated in the wrapper Rules context
|
|
938
|
+
for (const extendNode of ctx.extendNodes) {
|
|
939
|
+
if (extendNode.value.selector === undefined || extendNode.value.selector.type === 'Ampersand') {
|
|
940
|
+
extendNode.value.selector = selector;
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
rule = new Rules([
|
|
944
|
+
...ctx.extendNodes,
|
|
945
|
+
qRule
|
|
946
|
+
]);
|
|
947
|
+
rule._location = qRule._location;
|
|
948
|
+
ctx.extendNodes = undefined;
|
|
949
|
+
}
|
|
950
|
+
return rule;
|
|
951
|
+
}
|
|
952
|
+
},
|
|
953
|
+
{
|
|
954
|
+
GATE: () => isPossibleMixinCall,
|
|
955
|
+
ALT: () => {
|
|
956
|
+
// Call terminated by a semi-colon and not parens, deprecated
|
|
957
|
+
const semi = $.CONSUME(T.Semi);
|
|
958
|
+
if (!RECORDING_PHASE) {
|
|
959
|
+
const location = $.endRule();
|
|
960
|
+
// Mixin call without parentheses - deprecated
|
|
961
|
+
$.warnDeprecation('Calling a mixin without parentheses is deprecated', semi, 'mixin-call-no-parens');
|
|
962
|
+
return createMixinCall(location);
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
]);
|
|
967
|
+
};
|
|
968
|
+
}
|
|
969
|
+
/**
|
|
970
|
+
* We need to now handle a returned `Extend` node from the complexSelector rule
|
|
971
|
+
*/
|
|
972
|
+
export function relativeSelector(T) {
|
|
973
|
+
const $ = this;
|
|
974
|
+
return (ctx = {}) => {
|
|
975
|
+
return $.OR([
|
|
976
|
+
{
|
|
977
|
+
ALT: () => {
|
|
978
|
+
let co = $.CONSUME(T.Combinator);
|
|
979
|
+
let node = $.SUBRULE($.complexSelector, { ARGS: [ctx] });
|
|
980
|
+
if (!$.RECORDING_PHASE) {
|
|
981
|
+
let combinator = new Combinator(co.image, undefined, $.getLocationInfo(co), this.context);
|
|
982
|
+
let targetNode = node instanceof Extend
|
|
983
|
+
? node.value.selector
|
|
984
|
+
: node;
|
|
985
|
+
if (targetNode instanceof ComplexSelector) {
|
|
986
|
+
targetNode.value.unshift(combinator);
|
|
987
|
+
targetNode._location = $.getLocationFromNodes(targetNode.value);
|
|
988
|
+
}
|
|
989
|
+
else {
|
|
990
|
+
let nodes = [combinator, targetNode];
|
|
991
|
+
let complex = new ComplexSelector(nodes, undefined, $.getLocationFromNodes(nodes), this.context);
|
|
992
|
+
if (node instanceof Extend) {
|
|
993
|
+
node.value.selector = complex;
|
|
994
|
+
let location = node.location;
|
|
995
|
+
location[0] = co.startOffset;
|
|
996
|
+
location[1] = co.startLine;
|
|
997
|
+
location[2] = co.startColumn;
|
|
998
|
+
}
|
|
999
|
+
else {
|
|
1000
|
+
node = complex;
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
1004
|
+
return node;
|
|
1005
|
+
}
|
|
1006
|
+
},
|
|
1007
|
+
{
|
|
1008
|
+
ALT: () => $.SUBRULE2($.complexSelector, { ARGS: [ctx] })
|
|
1009
|
+
}
|
|
1010
|
+
]);
|
|
1011
|
+
};
|
|
1012
|
+
}
|
|
1013
|
+
export function compoundSelector(T) {
|
|
1014
|
+
const $ = this;
|
|
1015
|
+
/**
|
|
1016
|
+
A sequence of simple selectors that are not separated by
|
|
1017
|
+
a combinator.
|
|
1018
|
+
.e.g. `a#selected`
|
|
1019
|
+
*/
|
|
1020
|
+
// compoundSelector
|
|
1021
|
+
// : simpleSelector+
|
|
1022
|
+
// ;
|
|
1023
|
+
return (ctx = {}) => {
|
|
1024
|
+
let RECORDING_PHASE = $.RECORDING_PHASE;
|
|
1025
|
+
let selectors;
|
|
1026
|
+
if (!RECORDING_PHASE) {
|
|
1027
|
+
selectors = [];
|
|
1028
|
+
}
|
|
1029
|
+
let sel = $.SUBRULE($.simpleSelector, { ARGS: [ctx] });
|
|
1030
|
+
if (!RECORDING_PHASE) {
|
|
1031
|
+
selectors.push(sel);
|
|
1032
|
+
}
|
|
1033
|
+
$.MANY({
|
|
1034
|
+
/** Make sure we don't ignore space combinators */
|
|
1035
|
+
GATE: () => !$.hasWS() && !(ctx.inExtend && $.LA(1).tokenType === T.All),
|
|
1036
|
+
DEF: () => {
|
|
1037
|
+
let sel = $.SUBRULE2($.simpleSelector, { ARGS: [ctx] });
|
|
1038
|
+
if (!RECORDING_PHASE) {
|
|
1039
|
+
/** Make sure we don't add implicit whitespace */
|
|
1040
|
+
sel.pre = 0;
|
|
1041
|
+
selectors.push(sel);
|
|
1042
|
+
}
|
|
1043
|
+
}
|
|
1044
|
+
});
|
|
1045
|
+
if (!RECORDING_PHASE) {
|
|
1046
|
+
if (selectors.length === 1) {
|
|
1047
|
+
return selectors[0];
|
|
1048
|
+
}
|
|
1049
|
+
return new CompoundSelector(selectors, undefined, $.getLocationFromNodes(selectors), this.context);
|
|
1050
|
+
}
|
|
1051
|
+
};
|
|
1052
|
+
}
|
|
1053
|
+
/**
|
|
1054
|
+
* Extended with :extend
|
|
1055
|
+
*/
|
|
1056
|
+
export function complexSelector(T) {
|
|
1057
|
+
const $ = this;
|
|
1058
|
+
let originalComplexRule = cssComplexSelector.call(this, T, (ctx) => () => !ctx.inExtend || $.LA(1).tokenType !== T.All);
|
|
1059
|
+
return (ctx = {}) => {
|
|
1060
|
+
let selector = originalComplexRule(ctx);
|
|
1061
|
+
let isQualifiedRule = !!ctx.qualifiedRule;
|
|
1062
|
+
let flag;
|
|
1063
|
+
$.OR([
|
|
1064
|
+
{
|
|
1065
|
+
/** When we're inside the :extend(...), we can capture the "all" keyword */
|
|
1066
|
+
GATE: () => !!ctx.inExtend,
|
|
1067
|
+
ALT: () => flag = $.CONSUME(T.All)
|
|
1068
|
+
},
|
|
1069
|
+
{
|
|
1070
|
+
GATE: () => isQualifiedRule && !ctx.inExtend,
|
|
1071
|
+
ALT: () => {
|
|
1072
|
+
ctx.selector = selector;
|
|
1073
|
+
$.SUBRULE($.extend, { ARGS: [ctx] });
|
|
1074
|
+
ctx.selector = undefined;
|
|
1075
|
+
}
|
|
1076
|
+
},
|
|
1077
|
+
{
|
|
1078
|
+
ALT: EMPTY_ALT()
|
|
1079
|
+
}
|
|
1080
|
+
]);
|
|
1081
|
+
if (!$.RECORDING_PHASE && ctx.inExtend) {
|
|
1082
|
+
(ctx.extendTargets ??= []).push({ selector: ctx.selector, target: selector, flag });
|
|
1083
|
+
}
|
|
1084
|
+
return selector;
|
|
1085
|
+
};
|
|
1086
|
+
}
|
|
1087
|
+
const { isArray } = Array;
|
|
1088
|
+
/**
|
|
1089
|
+
* Groups extends by target (using valueOf()) and flag.
|
|
1090
|
+
* Returns an array of grouped Extend nodes where extends with the same target and flag
|
|
1091
|
+
* are combined into a single Extend node with a SelectorList of all matching selectors.
|
|
1092
|
+
*
|
|
1093
|
+
* @todo Group complex selectors into selector lists
|
|
1094
|
+
*/
|
|
1095
|
+
function groupExtendsByTargetAndFlag(extendNodes) {
|
|
1096
|
+
// Group extends by target and flag
|
|
1097
|
+
const groups = new Map();
|
|
1098
|
+
for (const ext of extendNodes) {
|
|
1099
|
+
let target = ext.value.target;
|
|
1100
|
+
let flag = ext.value.flag ?? 1; // ExtendFlag.Exact = 1
|
|
1101
|
+
// Create a key from target valueOf() and flag
|
|
1102
|
+
const key = `${target.valueOf()}|${flag}`;
|
|
1103
|
+
let group = groups.get(key);
|
|
1104
|
+
if (!group) {
|
|
1105
|
+
groups.set(key, ext);
|
|
1106
|
+
}
|
|
1107
|
+
else if (isArray(group)) {
|
|
1108
|
+
group.push(ext);
|
|
1109
|
+
}
|
|
1110
|
+
else {
|
|
1111
|
+
groups.set(key, [group, ext]);
|
|
1112
|
+
}
|
|
1113
|
+
}
|
|
1114
|
+
return Array.from(groups.values());
|
|
1115
|
+
}
|
|
1116
|
+
function mergeExtends(selector, extendTargets, location, context, flag) {
|
|
1117
|
+
let extendNodes;
|
|
1118
|
+
let currentTarget = extendTargets[0].target;
|
|
1119
|
+
let currentFlag = (extendTargets[0].flag ?? flag) ? 0 : 1; // ExtendFlag.All = 0, ExtendFlag.Exact = 1
|
|
1120
|
+
let currentNode = new Extend({
|
|
1121
|
+
selector,
|
|
1122
|
+
target: currentTarget,
|
|
1123
|
+
flag: currentFlag
|
|
1124
|
+
}, undefined, location, context);
|
|
1125
|
+
for (let i = 1; i < extendTargets.length; i++) {
|
|
1126
|
+
let ext = extendTargets[i];
|
|
1127
|
+
let thisFlag = (ext.flag ?? flag) ? 0 : 1;
|
|
1128
|
+
/**
|
|
1129
|
+
* Merge extends. We do this instead of merging earlier so that
|
|
1130
|
+
* selector lists with different flags are not merged.
|
|
1131
|
+
*/
|
|
1132
|
+
if (thisFlag === currentFlag) {
|
|
1133
|
+
let target = currentNode.value.target;
|
|
1134
|
+
if (!(target instanceof SelectorList)) {
|
|
1135
|
+
currentNode.value.target = new SelectorList([target, ext.target], undefined, location, context);
|
|
1136
|
+
}
|
|
1137
|
+
else {
|
|
1138
|
+
target.value.push(ext.target);
|
|
1139
|
+
}
|
|
1140
|
+
}
|
|
1141
|
+
else {
|
|
1142
|
+
if (!extendNodes || !extendNodes.includes(currentNode)) {
|
|
1143
|
+
(extendNodes ??= []).push(currentNode);
|
|
1144
|
+
}
|
|
1145
|
+
currentFlag = thisFlag;
|
|
1146
|
+
currentTarget = ext.target;
|
|
1147
|
+
currentNode = new Extend({
|
|
1148
|
+
selector,
|
|
1149
|
+
target: currentTarget,
|
|
1150
|
+
flag: currentFlag
|
|
1151
|
+
}, undefined, location, context);
|
|
1152
|
+
extendNodes.push(currentNode);
|
|
1153
|
+
}
|
|
1154
|
+
}
|
|
1155
|
+
;
|
|
1156
|
+
if (!extendNodes) {
|
|
1157
|
+
return currentNode;
|
|
1158
|
+
}
|
|
1159
|
+
if (extendNodes.length === 1) {
|
|
1160
|
+
return extendNodes[0];
|
|
1161
|
+
}
|
|
1162
|
+
return extendNodes;
|
|
1163
|
+
}
|
|
1164
|
+
/**
|
|
1165
|
+
* &:extend(...) statement ending with a semicolon.
|
|
1166
|
+
* This is the only valid standalone extend statement in Less.
|
|
1167
|
+
*/
|
|
1168
|
+
export function ampersandExtend(T) {
|
|
1169
|
+
const $ = this;
|
|
1170
|
+
return (ctx = {}) => {
|
|
1171
|
+
let RECORDING_PHASE = $.RECORDING_PHASE;
|
|
1172
|
+
$.startRule();
|
|
1173
|
+
$.CONSUME(T.AmpersandExtend);
|
|
1174
|
+
ctx.inExtend = true;
|
|
1175
|
+
$.SUBRULE($.selectorList, { ARGS: [ctx] });
|
|
1176
|
+
ctx.inExtend = false;
|
|
1177
|
+
let extendTargets = ctx.extendTargets;
|
|
1178
|
+
let flag = $.OPTION(() => $.CONSUME(T.AllFlag));
|
|
1179
|
+
$.CONSUME(T.RParen);
|
|
1180
|
+
$.CONSUME(T.Semi);
|
|
1181
|
+
if (!RECORDING_PHASE) {
|
|
1182
|
+
let location = $.endRule();
|
|
1183
|
+
let result = mergeExtends(undefined, extendTargets, location, this.context, flag);
|
|
1184
|
+
/** We've converted these extend targets to nodes, so we can reset extend targets */
|
|
1185
|
+
ctx.extendTargets = undefined;
|
|
1186
|
+
if (ctx.extendNodes) {
|
|
1187
|
+
if (isArray(result)) {
|
|
1188
|
+
ctx.extendNodes = [...ctx.extendNodes, ...result];
|
|
1189
|
+
}
|
|
1190
|
+
else {
|
|
1191
|
+
ctx.extendNodes.push(result);
|
|
1192
|
+
}
|
|
1193
|
+
}
|
|
1194
|
+
else {
|
|
1195
|
+
if (isArray(result)) {
|
|
1196
|
+
ctx.extendNodes = result;
|
|
1197
|
+
}
|
|
1198
|
+
else {
|
|
1199
|
+
ctx.extendNodes = [result];
|
|
1200
|
+
}
|
|
1201
|
+
}
|
|
1202
|
+
// Return Nil instead of the extend node - extends are handled via ctx.extendNodes
|
|
1203
|
+
// pathway to avoid duplicates (cssMain collects returned nodes into rules.value).
|
|
1204
|
+
// Nil is needed because undefined confuses cssMain (treats it as semicolon).
|
|
1205
|
+
return new Nil(undefined, undefined, location, this.context);
|
|
1206
|
+
}
|
|
1207
|
+
};
|
|
1208
|
+
}
|
|
1209
|
+
export function extend(T) {
|
|
1210
|
+
const $ = this;
|
|
1211
|
+
return (ctx = {}) => {
|
|
1212
|
+
$.startRule();
|
|
1213
|
+
$.CONSUME(T.Extend);
|
|
1214
|
+
ctx.inExtend = true;
|
|
1215
|
+
$.SUBRULE($.selectorList, { ARGS: [ctx] });
|
|
1216
|
+
let extendTargets = ctx.extendTargets;
|
|
1217
|
+
ctx.inExtend = false;
|
|
1218
|
+
let selector = ctx.selector;
|
|
1219
|
+
let flag = $.OPTION(() => $.CONSUME(T.AllFlag));
|
|
1220
|
+
$.CONSUME(T.RParen);
|
|
1221
|
+
if (!$.RECORDING_PHASE) {
|
|
1222
|
+
let location = $.endRule();
|
|
1223
|
+
// When .c:extend(...) is parsed, selector is .c
|
|
1224
|
+
// The extend will be processed in qualifiedRuleBody where selector: undefined is set
|
|
1225
|
+
// for extends that stay inside the ruleset (not bubbled)
|
|
1226
|
+
// Bubbled extends keep their selector and get it set correctly in qualifiedRule
|
|
1227
|
+
let merged = mergeExtends(selector, extendTargets, location, this.context, flag);
|
|
1228
|
+
/**
|
|
1229
|
+
* If we don't have as many extends as we have selectors, we need a way to signal
|
|
1230
|
+
* that these should be bumped above the ruleset.
|
|
1231
|
+
*/
|
|
1232
|
+
/** We've converted these extend targets to nodes, so we can reset extend targets */
|
|
1233
|
+
ctx.extendTargets = undefined;
|
|
1234
|
+
if (ctx.extendNodes) {
|
|
1235
|
+
if (isArray(merged)) {
|
|
1236
|
+
ctx.extendNodes = [...ctx.extendNodes, ...merged];
|
|
1237
|
+
}
|
|
1238
|
+
else {
|
|
1239
|
+
ctx.extendNodes.push(merged);
|
|
1240
|
+
}
|
|
1241
|
+
}
|
|
1242
|
+
else {
|
|
1243
|
+
if (isArray(merged)) {
|
|
1244
|
+
ctx.extendNodes = merged;
|
|
1245
|
+
}
|
|
1246
|
+
else {
|
|
1247
|
+
ctx.extendNodes = [merged];
|
|
1248
|
+
}
|
|
1249
|
+
}
|
|
1250
|
+
}
|
|
1251
|
+
};
|
|
1252
|
+
}
|
|
1253
|
+
function getAmpersandTemplateValue(image) {
|
|
1254
|
+
if (image === '&') {
|
|
1255
|
+
return undefined;
|
|
1256
|
+
}
|
|
1257
|
+
if (image.startsWith('&(') && image.endsWith(')')) {
|
|
1258
|
+
return image.slice(2, -1);
|
|
1259
|
+
}
|
|
1260
|
+
if (image.startsWith('&')) {
|
|
1261
|
+
return image.slice(1) || undefined;
|
|
1262
|
+
}
|
|
1263
|
+
if (image.includes('&')) {
|
|
1264
|
+
return image;
|
|
1265
|
+
}
|
|
1266
|
+
return undefined;
|
|
1267
|
+
}
|
|
1268
|
+
export function simpleSelector(T) {
|
|
1269
|
+
const $ = this;
|
|
1270
|
+
let selectorAlt = (ctx) => [
|
|
1271
|
+
{
|
|
1272
|
+
GATE: () => ((!ctx.inExtend || $.LA(1).tokenType !== T.All)
|
|
1273
|
+
&& $.LA(1).tokenType !== T.InterpolatedIdent),
|
|
1274
|
+
/**
|
|
1275
|
+
* In Less/Sass (and now CSS), the first inner selector can be an identifier
|
|
1276
|
+
*/
|
|
1277
|
+
ALT: () => $.CONSUME(T.Ident)
|
|
1278
|
+
},
|
|
1279
|
+
{
|
|
1280
|
+
/**
|
|
1281
|
+
* Unlike CSS Nesting, Less allows outer qualified rules
|
|
1282
|
+
* to have `&`, and it is just silently absorbed if there
|
|
1283
|
+
* is no parent selector.
|
|
1284
|
+
*/
|
|
1285
|
+
ALT: () => {
|
|
1286
|
+
let amp = $.CONSUME(T.Ampersand);
|
|
1287
|
+
if (!$.RECORDING_PHASE) {
|
|
1288
|
+
const value = getAmpersandTemplateValue(amp.image);
|
|
1289
|
+
return new Ampersand(value || undefined, undefined, $.getLocationInfo(amp), this.context);
|
|
1290
|
+
}
|
|
1291
|
+
}
|
|
1292
|
+
},
|
|
1293
|
+
{ ALT: () => $.CONSUME(T.InterpolatedIdent) },
|
|
1294
|
+
{ ALT: () => $.CONSUME(T.InterpolatedSelector) },
|
|
1295
|
+
{ ALT: () => $.SUBRULE($.classSelector) },
|
|
1296
|
+
{ ALT: () => $.SUBRULE($.idSelector) },
|
|
1297
|
+
{ ALT: () => $.CONSUME(T.Star) },
|
|
1298
|
+
{ ALT: () => {
|
|
1299
|
+
let initialIsQualifiedRule = ctx.qualifiedRule;
|
|
1300
|
+
ctx.qualifiedRule = false;
|
|
1301
|
+
/** Make sure we prevent things like :extend() inside pseudo-selectors */
|
|
1302
|
+
let pseudo = $.SUBRULE($.pseudoSelector, { ARGS: [ctx] });
|
|
1303
|
+
ctx.qualifiedRule = initialIsQualifiedRule;
|
|
1304
|
+
return pseudo;
|
|
1305
|
+
} },
|
|
1306
|
+
{ ALT: () => $.SUBRULE($.attributeSelector) },
|
|
1307
|
+
/** Supports keyframes selectors */
|
|
1308
|
+
{ ALT: () => $.CONSUME(T.DimensionInt) },
|
|
1309
|
+
{ ALT: () => $.CONSUME(T.DimensionNum) }
|
|
1310
|
+
];
|
|
1311
|
+
return (ctx = {}) => {
|
|
1312
|
+
let selector = $.OR(selectorAlt(ctx));
|
|
1313
|
+
if (!$.RECORDING_PHASE) {
|
|
1314
|
+
if ($.isToken(selector)) {
|
|
1315
|
+
if (selector.tokenType.name === 'Ampersand') {
|
|
1316
|
+
const value = getAmpersandTemplateValue(selector.image);
|
|
1317
|
+
return new Ampersand(value || undefined, undefined, $.getLocationInfo(selector), this.context);
|
|
1318
|
+
}
|
|
1319
|
+
if (selector.tokenType.name === 'InterpolatedSelector'
|
|
1320
|
+
|| selector.tokenType.name === 'InterpolatedIdent') {
|
|
1321
|
+
// Create an InterpolatedSelector wrapper for interpolated selectors
|
|
1322
|
+
let nameValue = selector.image;
|
|
1323
|
+
let interpolatedNode = getInterpolated(nameValue, $.getLocationInfo(selector), this.context);
|
|
1324
|
+
return new InterpolatedSelector(interpolatedNode, undefined, $.getLocationInfo(selector), this.context);
|
|
1325
|
+
}
|
|
1326
|
+
return new BasicSelector(selector.image, undefined, $.getLocationInfo(selector), this.context);
|
|
1327
|
+
}
|
|
1328
|
+
return selector;
|
|
1329
|
+
}
|
|
1330
|
+
};
|
|
1331
|
+
}
|
|
1332
|
+
export function anonymousMixinDefinition(T) {
|
|
1333
|
+
const $ = this;
|
|
1334
|
+
return (ctx = {}) => {
|
|
1335
|
+
$.startRule();
|
|
1336
|
+
let params;
|
|
1337
|
+
let anonToken;
|
|
1338
|
+
$.OPTION(() => {
|
|
1339
|
+
anonToken = $.CONSUME(T.AnonMixinStart);
|
|
1340
|
+
params = $.SUBRULE($.mixinArgList, { ARGS: [{ ...ctx, isDefinition: true }] });
|
|
1341
|
+
$.CONSUME(T.RParen);
|
|
1342
|
+
});
|
|
1343
|
+
let rules = $.SUBRULE($.wrappedDeclarationList, { ARGS: [ctx] });
|
|
1344
|
+
if (!$.RECORDING_PHASE) {
|
|
1345
|
+
// Set rulesVisibility for detached rulesets based on leakyRules
|
|
1346
|
+
// Less, for whatever reason, has slightly different lookup rules for
|
|
1347
|
+
// "detached rulesets".
|
|
1348
|
+
// Parse as Anonymous mixin
|
|
1349
|
+
if (!rules.options.rulesVisibility) {
|
|
1350
|
+
rules.options.rulesVisibility = {};
|
|
1351
|
+
}
|
|
1352
|
+
if (this.leakyRules) {
|
|
1353
|
+
rules.options.rulesVisibility.Mixin = 'public';
|
|
1354
|
+
rules.options.rulesVisibility.VarDeclaration = 'private';
|
|
1355
|
+
}
|
|
1356
|
+
else {
|
|
1357
|
+
rules.options.rulesVisibility.Mixin = 'private';
|
|
1358
|
+
rules.options.rulesVisibility.VarDeclaration = 'private';
|
|
1359
|
+
}
|
|
1360
|
+
if (!anonToken) {
|
|
1361
|
+
/** To Less, this is a "detached ruleset" */
|
|
1362
|
+
// Check if this should be parsed as Collection or Rules
|
|
1363
|
+
const shouldBeCollection = (() => {
|
|
1364
|
+
let properties = [];
|
|
1365
|
+
for (const node of rules.value) {
|
|
1366
|
+
if (node.type === 'Declaration') {
|
|
1367
|
+
properties.push(node);
|
|
1368
|
+
}
|
|
1369
|
+
else if (node.type === 'Comment' || node.type === 'VarDeclaration') {
|
|
1370
|
+
continue;
|
|
1371
|
+
}
|
|
1372
|
+
else {
|
|
1373
|
+
/** Not a valid collection, parse as anonymous mixin */
|
|
1374
|
+
return false;
|
|
1375
|
+
}
|
|
1376
|
+
}
|
|
1377
|
+
if (properties.length === 0) {
|
|
1378
|
+
/** If just var declarations and/or comments, parse as collection */
|
|
1379
|
+
return true;
|
|
1380
|
+
}
|
|
1381
|
+
const validPropertyCount = properties.filter((decl) => {
|
|
1382
|
+
const name = decl.value.name;
|
|
1383
|
+
const propName = typeof name === 'string' ? name : name.valueOf();
|
|
1384
|
+
// Skip custom properties (--*)
|
|
1385
|
+
if (propName.startsWith('--')) {
|
|
1386
|
+
return true; // Custom properties are always valid
|
|
1387
|
+
}
|
|
1388
|
+
return all.includes(propName);
|
|
1389
|
+
}).length;
|
|
1390
|
+
// Majority means more than 50%
|
|
1391
|
+
const majorityValid = validPropertyCount > properties.length / 2;
|
|
1392
|
+
/** If this looks like mostly CSS properties, parse as mixin instead */
|
|
1393
|
+
return !majorityValid;
|
|
1394
|
+
})();
|
|
1395
|
+
const usage = ctx.detachedRulesetUsage ?? 'none';
|
|
1396
|
+
const forceMixinForDynamicUsage = usage === 'function-arg'
|
|
1397
|
+
|| usage === 'mixin-arg'
|
|
1398
|
+
|| usage === 'default-param';
|
|
1399
|
+
const shouldBeCollectionFinal = shouldBeCollection && !forceMixinForDynamicUsage;
|
|
1400
|
+
if (shouldBeCollectionFinal) {
|
|
1401
|
+
return new Collection(rules.value, rules.options, $.endRule(), this.context);
|
|
1402
|
+
}
|
|
1403
|
+
}
|
|
1404
|
+
// If anonToken exists, it's an anonymous mixin with (optional) parameters, return as Mixin
|
|
1405
|
+
return new Mixin({ params, rules }, undefined, $.endRule(), this.context);
|
|
1406
|
+
}
|
|
1407
|
+
};
|
|
1408
|
+
}
|
|
1409
|
+
/**
|
|
1410
|
+
* Mostly copied from css importAtRule, but it maps
|
|
1411
|
+
* differently to Jess nodes depending on if it's meant
|
|
1412
|
+
* to be a Jess-style import or just an at-rule
|
|
1413
|
+
*/
|
|
1414
|
+
export function importAtRule(T) {
|
|
1415
|
+
const $ = this;
|
|
1416
|
+
const isCssUrl = (url, options) => {
|
|
1417
|
+
if (options.includes('inline')) {
|
|
1418
|
+
return false;
|
|
1419
|
+
}
|
|
1420
|
+
const lower = url.toLowerCase();
|
|
1421
|
+
const forcedLess = options.includes('less');
|
|
1422
|
+
if (forcedLess) {
|
|
1423
|
+
return false;
|
|
1424
|
+
}
|
|
1425
|
+
if (options.includes('css')) {
|
|
1426
|
+
return true;
|
|
1427
|
+
}
|
|
1428
|
+
if (/\.css([?#].*)?$/.test(lower)) {
|
|
1429
|
+
return true;
|
|
1430
|
+
}
|
|
1431
|
+
if (/\.less([?#].*)?$/.test(lower)) {
|
|
1432
|
+
return false;
|
|
1433
|
+
}
|
|
1434
|
+
// Remote imports default to CSS.
|
|
1435
|
+
if (lower.startsWith('http://') || lower.startsWith('https://') || lower.startsWith('//')) {
|
|
1436
|
+
return true;
|
|
1437
|
+
}
|
|
1438
|
+
return false;
|
|
1439
|
+
};
|
|
1440
|
+
return (ctx = {}) => {
|
|
1441
|
+
let RECORDING_PHASE = $.RECORDING_PHASE;
|
|
1442
|
+
$.startRule();
|
|
1443
|
+
let name = $.CONSUME(T.AtImport);
|
|
1444
|
+
let options;
|
|
1445
|
+
if (!RECORDING_PHASE) {
|
|
1446
|
+
options = [];
|
|
1447
|
+
}
|
|
1448
|
+
$.OPTION(() => {
|
|
1449
|
+
$.CONSUME(T.LParen);
|
|
1450
|
+
$.AT_LEAST_ONE_SEP({
|
|
1451
|
+
SEP: T.Comma,
|
|
1452
|
+
DEF: () => {
|
|
1453
|
+
let opt = $.CONSUME(T.PlainIdent);
|
|
1454
|
+
if (!RECORDING_PHASE) {
|
|
1455
|
+
options.push(opt.image);
|
|
1456
|
+
}
|
|
1457
|
+
}
|
|
1458
|
+
});
|
|
1459
|
+
$.CONSUME(T.RParen);
|
|
1460
|
+
});
|
|
1461
|
+
let urlNode = $.OR([
|
|
1462
|
+
{ ALT: () => $.SUBRULE($.urlFunction, { ARGS: [ctx] }) },
|
|
1463
|
+
{ ALT: () => $.SUBRULE($.string, { ARGS: [ctx] }) }
|
|
1464
|
+
]);
|
|
1465
|
+
let isAtRule;
|
|
1466
|
+
let postludeNode;
|
|
1467
|
+
if (!RECORDING_PHASE) {
|
|
1468
|
+
let url = urlNode.valueOf();
|
|
1469
|
+
isAtRule = isCssUrl(url, options);
|
|
1470
|
+
}
|
|
1471
|
+
let preludeNodes;
|
|
1472
|
+
if (!RECORDING_PHASE) {
|
|
1473
|
+
preludeNodes = [$.wrap(urlNode)];
|
|
1474
|
+
}
|
|
1475
|
+
let extraNodes;
|
|
1476
|
+
$.OPTION2(() => {
|
|
1477
|
+
extraNodes = $.SUBRULE($.importPostlude);
|
|
1478
|
+
});
|
|
1479
|
+
if (!RECORDING_PHASE && extraNodes && extraNodes.length) {
|
|
1480
|
+
if (isAtRule) {
|
|
1481
|
+
isAtRule = true;
|
|
1482
|
+
for (const n of extraNodes) {
|
|
1483
|
+
preludeNodes.push(n);
|
|
1484
|
+
}
|
|
1485
|
+
}
|
|
1486
|
+
else {
|
|
1487
|
+
// Less-style imports with media/query/layer postludes should evaluate
|
|
1488
|
+
// the target and then wrap output (for both inline and non-inline forms).
|
|
1489
|
+
// Keep this on import options so StyleImport.evalNode can apply wrappers.
|
|
1490
|
+
const postludeLoc = $.getLocationFromNodes(extraNodes);
|
|
1491
|
+
postludeNode = new Sequence(extraNodes, undefined, postludeLoc, this.context);
|
|
1492
|
+
}
|
|
1493
|
+
}
|
|
1494
|
+
$.CONSUME(T.Semi);
|
|
1495
|
+
if (!RECORDING_PHASE) {
|
|
1496
|
+
let location = $.endRule();
|
|
1497
|
+
if (isAtRule) {
|
|
1498
|
+
const prelude = new Sequence(preludeNodes, undefined, $.getLocationFromNodes(preludeNodes), this.context);
|
|
1499
|
+
const atRule = new AtRule({
|
|
1500
|
+
name: $.wrap(new Any(name.image, { role: 'atkeyword' }, $.getLocationInfo(name), this.context), true),
|
|
1501
|
+
prelude: prelude
|
|
1502
|
+
}, undefined, location, this.context);
|
|
1503
|
+
return atRule;
|
|
1504
|
+
}
|
|
1505
|
+
return new StyleImport({
|
|
1506
|
+
path: urlNode
|
|
1507
|
+
}, {
|
|
1508
|
+
type: 'import',
|
|
1509
|
+
importOptions: {
|
|
1510
|
+
type: options.includes('less') ? 'less' : undefined,
|
|
1511
|
+
reference: options.includes('reference'),
|
|
1512
|
+
once: !options.includes('multiple'),
|
|
1513
|
+
multiple: options.includes('multiple'),
|
|
1514
|
+
optional: options.includes('optional'),
|
|
1515
|
+
inline: options.includes('inline'),
|
|
1516
|
+
postlude: postludeNode
|
|
1517
|
+
}
|
|
1518
|
+
}, location, this.context);
|
|
1519
|
+
}
|
|
1520
|
+
};
|
|
1521
|
+
}
|
|
1522
|
+
/** Less variables */
|
|
1523
|
+
export function varDeclarationOrCall(T) {
|
|
1524
|
+
const $ = this;
|
|
1525
|
+
/**
|
|
1526
|
+
* Less doesn't allow variable variables anymore? It used to. Not sure
|
|
1527
|
+
* when that changed.
|
|
1528
|
+
*/
|
|
1529
|
+
// let nameAlt = [
|
|
1530
|
+
// { ALT: () => $.SUBRULE($.varName) },
|
|
1531
|
+
// { ALT: () => $.CONSUME(T.NestedReference) }
|
|
1532
|
+
// ];
|
|
1533
|
+
return (ctx = {}) => {
|
|
1534
|
+
$.startRule();
|
|
1535
|
+
let name = $.SUBRULE($.varName, { ARGS: [ctx] });
|
|
1536
|
+
let value;
|
|
1537
|
+
let args;
|
|
1538
|
+
let important;
|
|
1539
|
+
$.OR([
|
|
1540
|
+
{
|
|
1541
|
+
/**
|
|
1542
|
+
* This is a variable declaration
|
|
1543
|
+
* Disallows `@atrule :foo;` because it resembles a pseudo-selector
|
|
1544
|
+
*/
|
|
1545
|
+
ALT: () => {
|
|
1546
|
+
$.CONSUME(T.Colon);
|
|
1547
|
+
return $.OR2([
|
|
1548
|
+
/**
|
|
1549
|
+
* This needs to be gated early, even though it is
|
|
1550
|
+
* gated again in the valueList production, because
|
|
1551
|
+
* chevrotain-allstar needs to pick a path first.
|
|
1552
|
+
*/
|
|
1553
|
+
{
|
|
1554
|
+
GATE: () => {
|
|
1555
|
+
let type = $.LA(1).tokenType;
|
|
1556
|
+
return type === T.AnonMixinStart || type === T.LCurly;
|
|
1557
|
+
},
|
|
1558
|
+
ALT: () => {
|
|
1559
|
+
value = $.SUBRULE($.anonymousMixinDefinition, { ARGS: [ctx] });
|
|
1560
|
+
$.OPTION2(() => $.CONSUME2(T.Semi));
|
|
1561
|
+
return value;
|
|
1562
|
+
}
|
|
1563
|
+
},
|
|
1564
|
+
{
|
|
1565
|
+
GATE: () => {
|
|
1566
|
+
let type = $.LA(1).tokenType;
|
|
1567
|
+
return type !== T.AnonMixinStart && type !== T.LCurly;
|
|
1568
|
+
},
|
|
1569
|
+
ALT: () => {
|
|
1570
|
+
value = $.SUBRULE($.valueList, { ARGS: [{ ...ctx, allowMixinCallWithoutAccessor: true }] });
|
|
1571
|
+
$.OPTION(() => {
|
|
1572
|
+
important = $.CONSUME(T.Important);
|
|
1573
|
+
});
|
|
1574
|
+
return value;
|
|
1575
|
+
}
|
|
1576
|
+
}
|
|
1577
|
+
]);
|
|
1578
|
+
}
|
|
1579
|
+
},
|
|
1580
|
+
/** This is a variable call. Allow optional whitespace between name and (. */
|
|
1581
|
+
{
|
|
1582
|
+
GATE: () => $.LA(1).tokenType === T.LParen,
|
|
1583
|
+
/**
|
|
1584
|
+
* This is a change from Less 1.x-4.x
|
|
1585
|
+
* e.g.
|
|
1586
|
+
* ```
|
|
1587
|
+
* @dr: #(@var1, @var2) {
|
|
1588
|
+
* // ...
|
|
1589
|
+
* }
|
|
1590
|
+
* @dr(arg1, arg2);
|
|
1591
|
+
*/
|
|
1592
|
+
ALT: () => {
|
|
1593
|
+
args = $.SUBRULE($.mixinArgs, { ARGS: [ctx] });
|
|
1594
|
+
return args;
|
|
1595
|
+
}
|
|
1596
|
+
}
|
|
1597
|
+
]);
|
|
1598
|
+
if (!$.RECORDING_PHASE) {
|
|
1599
|
+
let location = $.endRule();
|
|
1600
|
+
let nameVal = getInterpolatedOrString(name.image);
|
|
1601
|
+
let nameNode;
|
|
1602
|
+
if (!(nameVal instanceof Interpolated)) {
|
|
1603
|
+
nameNode = new Any(nameVal, { role: 'ident' }, $.getLocationInfo(name), this.context);
|
|
1604
|
+
}
|
|
1605
|
+
else {
|
|
1606
|
+
nameNode = nameVal;
|
|
1607
|
+
}
|
|
1608
|
+
/** An anonymous mixin call */
|
|
1609
|
+
if (!value) {
|
|
1610
|
+
// When @variable() is called, look up the variable first
|
|
1611
|
+
// The variable's value (which may be a Call node) will be executed
|
|
1612
|
+
const nameRef = nameNode instanceof Interpolated
|
|
1613
|
+
? new Reference({ key: nameNode }, { type: 'variable', role: 'name' })
|
|
1614
|
+
: new Reference({ key: nameNode }, { type: 'variable', role: 'name' });
|
|
1615
|
+
// Pass markImportant in options if !important is present
|
|
1616
|
+
const callOptions = important ? { markImportant: true } : undefined;
|
|
1617
|
+
const callNode = new Call({ name: nameRef, args }, callOptions, location, this.context);
|
|
1618
|
+
// Clear important since it's now on the Call
|
|
1619
|
+
if (important) {
|
|
1620
|
+
important = undefined;
|
|
1621
|
+
}
|
|
1622
|
+
// Variable calls are expressions at the outermost level (but not parenthesized).
|
|
1623
|
+
// e.g. `$media()`, NOT `$(media())`
|
|
1624
|
+
return new Expression(callNode, undefined, location, this.context);
|
|
1625
|
+
}
|
|
1626
|
+
// If the value is a Call node and we have !important, set markImportant on the Call
|
|
1627
|
+
// instead of on the VarDeclaration (mixin call semantics)
|
|
1628
|
+
if (important && value instanceof Call) {
|
|
1629
|
+
value.options = value.options || {};
|
|
1630
|
+
value.options.markImportant = true;
|
|
1631
|
+
important = undefined;
|
|
1632
|
+
}
|
|
1633
|
+
if (value && isLegacySelectorLikeValue(value)) {
|
|
1634
|
+
const varName = String(nameNode.valueOf());
|
|
1635
|
+
$._errors.push(new NoViableAltException(`Unquoted selector capture in '${varName}' is no longer supported. Use '*[ ... ]' (e.g. ${varName}: *[.a, .b]).`, $.LA(1), $.LA(0)));
|
|
1636
|
+
}
|
|
1637
|
+
return new VarDeclaration({
|
|
1638
|
+
name: $.wrap(nameNode, true),
|
|
1639
|
+
value: $.wrap(value, true),
|
|
1640
|
+
important: important ? $.wrap(new Any(important.image, { role: 'flag' }, $.getLocationInfo(important), this.context), true) : undefined
|
|
1641
|
+
}, undefined, location, this.context);
|
|
1642
|
+
}
|
|
1643
|
+
};
|
|
1644
|
+
}
|
|
1645
|
+
/** True for a node that could be one item in the old unquoted selector list (e.g. .a or #id). */
|
|
1646
|
+
function isSelectorLikeListItem(node) {
|
|
1647
|
+
if (node.type === 'SelectorCapture' || isNode(node, 'Call')) {
|
|
1648
|
+
return false;
|
|
1649
|
+
}
|
|
1650
|
+
if (isNode(node, 'Reference')) {
|
|
1651
|
+
return node.options.type === 'mixin-ruleset';
|
|
1652
|
+
}
|
|
1653
|
+
if (isNode(node, 'List') || isNode(node, 'Sequence')) {
|
|
1654
|
+
return node.value.length > 0 && node.value.every(isSelectorLikeListItem);
|
|
1655
|
+
}
|
|
1656
|
+
return false;
|
|
1657
|
+
}
|
|
1658
|
+
/** True only for the legacy unquoted selector-list form (e.g. @var: .a, .b, .c), not @var: .a; */
|
|
1659
|
+
function isLegacySelectorLikeValue(node) {
|
|
1660
|
+
if (node.type === 'SelectorCapture' || isNode(node, 'Call')) {
|
|
1661
|
+
return false;
|
|
1662
|
+
}
|
|
1663
|
+
if (isNode(node, 'Reference')) {
|
|
1664
|
+
return false; // Single mixin reference is valid.
|
|
1665
|
+
}
|
|
1666
|
+
if (isNode(node, 'List') || isNode(node, 'Sequence')) {
|
|
1667
|
+
return node.value.length > 1 && node.value.every(isSelectorLikeListItem);
|
|
1668
|
+
}
|
|
1669
|
+
return false;
|
|
1670
|
+
}
|
|
1671
|
+
export function selectorCapture(T) {
|
|
1672
|
+
const $ = this;
|
|
1673
|
+
return (ctx = {}) => {
|
|
1674
|
+
$.startRule();
|
|
1675
|
+
$.CONSUME(T.Star);
|
|
1676
|
+
// Use $.OR with a gate for a positive assertion
|
|
1677
|
+
const selector = $.OR([
|
|
1678
|
+
{
|
|
1679
|
+
GATE: $.noSep,
|
|
1680
|
+
ALT: () => {
|
|
1681
|
+
$.CONSUME(T.LSquare);
|
|
1682
|
+
const selector = $.SUBRULE($.forgivingSelectorList, { ARGS: [{ ...ctx, inner: true }] });
|
|
1683
|
+
$.CONSUME(T.RSquare);
|
|
1684
|
+
return selector;
|
|
1685
|
+
}
|
|
1686
|
+
}
|
|
1687
|
+
]);
|
|
1688
|
+
if (!$.RECORDING_PHASE) {
|
|
1689
|
+
const location = $.endRule();
|
|
1690
|
+
return new SelectorCapture($.wrap(selector, true), undefined, location, this.context);
|
|
1691
|
+
}
|
|
1692
|
+
};
|
|
1693
|
+
}
|
|
1694
|
+
export function valueSequence(_T) {
|
|
1695
|
+
const $ = this;
|
|
1696
|
+
return (ctx = {}) => {
|
|
1697
|
+
let RECORDING_PHASE = $.RECORDING_PHASE;
|
|
1698
|
+
$.startRule();
|
|
1699
|
+
let nodes;
|
|
1700
|
+
if (!RECORDING_PHASE) {
|
|
1701
|
+
nodes = [];
|
|
1702
|
+
}
|
|
1703
|
+
$.OR([
|
|
1704
|
+
{
|
|
1705
|
+
GATE: () => $.looseMode,
|
|
1706
|
+
ALT: () => {
|
|
1707
|
+
$.MANY(() => {
|
|
1708
|
+
const exprCtx = { ...ctx, wrapInExpression: true };
|
|
1709
|
+
let value = $.SUBRULE($.expressionSum, { ARGS: [exprCtx] });
|
|
1710
|
+
if (!RECORDING_PHASE) {
|
|
1711
|
+
value = wrapOuterExpressionIfNeeded.call($, value, exprCtx);
|
|
1712
|
+
nodes.push(value);
|
|
1713
|
+
}
|
|
1714
|
+
});
|
|
1715
|
+
}
|
|
1716
|
+
},
|
|
1717
|
+
{
|
|
1718
|
+
GATE: () => !$.looseMode,
|
|
1719
|
+
/** @todo - create warning if there isn't a value */
|
|
1720
|
+
ALT: () => {
|
|
1721
|
+
$.AT_LEAST_ONE(() => {
|
|
1722
|
+
const exprCtx = { ...ctx, wrapInExpression: true };
|
|
1723
|
+
let value = $.SUBRULE2($.expressionSum, { ARGS: [exprCtx] });
|
|
1724
|
+
if (!RECORDING_PHASE) {
|
|
1725
|
+
value = wrapOuterExpressionIfNeeded.call($, value, exprCtx);
|
|
1726
|
+
nodes.push(value);
|
|
1727
|
+
}
|
|
1728
|
+
});
|
|
1729
|
+
}
|
|
1730
|
+
}
|
|
1731
|
+
]);
|
|
1732
|
+
if (!RECORDING_PHASE) {
|
|
1733
|
+
let location = $.endRule();
|
|
1734
|
+
if (nodes.length === 1) {
|
|
1735
|
+
const single = nodes[0];
|
|
1736
|
+
return single;
|
|
1737
|
+
}
|
|
1738
|
+
const seq = new Sequence(nodes, undefined, location, this.context);
|
|
1739
|
+
return seq;
|
|
1740
|
+
}
|
|
1741
|
+
};
|
|
1742
|
+
}
|
|
1743
|
+
export function squareValue(T) {
|
|
1744
|
+
const $ = this;
|
|
1745
|
+
return (ctx = {}) => {
|
|
1746
|
+
$.startRule();
|
|
1747
|
+
let RECORDING_PHASE = $.RECORDING_PHASE;
|
|
1748
|
+
$.CONSUME(T.LSquare);
|
|
1749
|
+
let node = $.OR([
|
|
1750
|
+
{
|
|
1751
|
+
GATE: () => !$.looseMode,
|
|
1752
|
+
ALT: () => {
|
|
1753
|
+
let ident = $.CONSUME(T.Ident);
|
|
1754
|
+
if (!RECORDING_PHASE) {
|
|
1755
|
+
return new Any(ident.image, { role: 'ident' }, $.getLocationInfo(ident), this.context);
|
|
1756
|
+
}
|
|
1757
|
+
}
|
|
1758
|
+
},
|
|
1759
|
+
{
|
|
1760
|
+
GATE: () => !!$.looseMode,
|
|
1761
|
+
ALT: () => {
|
|
1762
|
+
let nodes;
|
|
1763
|
+
if (!RECORDING_PHASE) {
|
|
1764
|
+
nodes = [];
|
|
1765
|
+
}
|
|
1766
|
+
$.MANY(() => {
|
|
1767
|
+
let node = $.SUBRULE($.anyInnerValue, { ARGS: [ctx] });
|
|
1768
|
+
if (!RECORDING_PHASE) {
|
|
1769
|
+
const wrapped = $.wrap(node);
|
|
1770
|
+
nodes.push(wrapped);
|
|
1771
|
+
}
|
|
1772
|
+
});
|
|
1773
|
+
if (!RECORDING_PHASE) {
|
|
1774
|
+
const seq = new Sequence(nodes, undefined, $.getLocationFromNodes(nodes), this.context);
|
|
1775
|
+
return seq;
|
|
1776
|
+
}
|
|
1777
|
+
}
|
|
1778
|
+
}
|
|
1779
|
+
]);
|
|
1780
|
+
$.CONSUME(T.RSquare);
|
|
1781
|
+
if (!$.RECORDING_PHASE) {
|
|
1782
|
+
let location = $.endRule();
|
|
1783
|
+
const blk = new Block(node, { type: 'square' }, location, this.context);
|
|
1784
|
+
return blk;
|
|
1785
|
+
}
|
|
1786
|
+
};
|
|
1787
|
+
}
|
|
1788
|
+
/**
|
|
1789
|
+
* In CSS, would be a single value.
|
|
1790
|
+
* In Less, these are math expressions which
|
|
1791
|
+
* represent a single value. During AST construction,
|
|
1792
|
+
* these will be grouped by order of operations.
|
|
1793
|
+
*/
|
|
1794
|
+
export function expressionSum(T) {
|
|
1795
|
+
const $ = this;
|
|
1796
|
+
return (ctx = {}) => {
|
|
1797
|
+
let RECORDING_PHASE = $.RECORDING_PHASE;
|
|
1798
|
+
$.startRule();
|
|
1799
|
+
let left = $.SUBRULE($.expressionProduct, { ARGS: [ctx] });
|
|
1800
|
+
$.MANY({
|
|
1801
|
+
/**
|
|
1802
|
+
* What this GATE does. We need to dis-ambiguate
|
|
1803
|
+
* 1 -1 (a value sequence) from 1-1 (a Less expression),
|
|
1804
|
+
* so Less is white-space sensitive here.
|
|
1805
|
+
*/
|
|
1806
|
+
GATE: () => {
|
|
1807
|
+
const next = $.LA(1);
|
|
1808
|
+
const nextType = next.tokenType;
|
|
1809
|
+
return (nextType === T.Plus
|
|
1810
|
+
|| nextType === T.Minus
|
|
1811
|
+
|| ($.noSep() && tokenMatcher(next, T.Signed)));
|
|
1812
|
+
},
|
|
1813
|
+
DEF: () => {
|
|
1814
|
+
let op;
|
|
1815
|
+
let right;
|
|
1816
|
+
$.OR([
|
|
1817
|
+
{
|
|
1818
|
+
ALT: () => {
|
|
1819
|
+
let opToken = $.OR2([
|
|
1820
|
+
{ ALT: () => $.CONSUME(T.Plus) },
|
|
1821
|
+
{ ALT: () => $.CONSUME(T.Minus) }
|
|
1822
|
+
]);
|
|
1823
|
+
if (!RECORDING_PHASE) {
|
|
1824
|
+
op = opToken.image;
|
|
1825
|
+
}
|
|
1826
|
+
right = $.SUBRULE2($.expressionProduct, { ARGS: [ctx] });
|
|
1827
|
+
}
|
|
1828
|
+
},
|
|
1829
|
+
/** This will be interpreted by Less as a complete expression */
|
|
1830
|
+
{
|
|
1831
|
+
ALT: () => {
|
|
1832
|
+
// Consume a signed literal and convert it without rewinding
|
|
1833
|
+
const tok = $.CONSUME(T.Signed);
|
|
1834
|
+
if (!RECORDING_PHASE) {
|
|
1835
|
+
const str = tok.image;
|
|
1836
|
+
op = str[0];
|
|
1837
|
+
// Build a literal node from the signed token directly
|
|
1838
|
+
// Prefer dimension if payload exists, else number, else ident fallback
|
|
1839
|
+
if (tok.payload && tok.payload[1]) {
|
|
1840
|
+
const dim = { number: parseFloat(tok.payload[0]), unit: tok.payload[1] };
|
|
1841
|
+
right = new Dimension(dim, undefined, $.getLocationInfo(tok), this.context);
|
|
1842
|
+
}
|
|
1843
|
+
else {
|
|
1844
|
+
const num = parseFloat(str);
|
|
1845
|
+
if (!Number.isNaN(num)) {
|
|
1846
|
+
right = new Num(num, undefined, $.getLocationInfo(tok), this.context);
|
|
1847
|
+
}
|
|
1848
|
+
else {
|
|
1849
|
+
right = $.processValueToken(tok);
|
|
1850
|
+
}
|
|
1851
|
+
}
|
|
1852
|
+
}
|
|
1853
|
+
}
|
|
1854
|
+
}
|
|
1855
|
+
]);
|
|
1856
|
+
if (!RECORDING_PHASE) {
|
|
1857
|
+
const operation = new Operation([$.wrap(left, true), op, $.wrap(right)], undefined, $.getLocationFromNodes([left, right]), this.context);
|
|
1858
|
+
left = operation;
|
|
1859
|
+
return left;
|
|
1860
|
+
}
|
|
1861
|
+
}
|
|
1862
|
+
});
|
|
1863
|
+
if (!RECORDING_PHASE) {
|
|
1864
|
+
$.endRule();
|
|
1865
|
+
return left;
|
|
1866
|
+
}
|
|
1867
|
+
};
|
|
1868
|
+
}
|
|
1869
|
+
export function expressionProduct(T) {
|
|
1870
|
+
const $ = this;
|
|
1871
|
+
let opAlt = [
|
|
1872
|
+
{ ALT: () => $.CONSUME(T.Star) },
|
|
1873
|
+
{ ALT: () => $.CONSUME(T.Slash) },
|
|
1874
|
+
{ ALT: () => $.CONSUME(T.Percent) }
|
|
1875
|
+
];
|
|
1876
|
+
return (ctx = {}) => {
|
|
1877
|
+
let RECORDING_PHASE = $.RECORDING_PHASE;
|
|
1878
|
+
$.startRule();
|
|
1879
|
+
let left = $.SUBRULE($.expressionValue, { ARGS: [ctx] });
|
|
1880
|
+
$.MANY(() => {
|
|
1881
|
+
let op = $.OR(opAlt);
|
|
1882
|
+
// Check for deprecated ./ operator
|
|
1883
|
+
if (!RECORDING_PHASE && op.image === './') {
|
|
1884
|
+
$.warnDeprecation('./ operator is deprecated', op, 'dot-slash-operator');
|
|
1885
|
+
}
|
|
1886
|
+
let right = $.SUBRULE2($.expressionValue, { ARGS: [ctx] });
|
|
1887
|
+
if (!RECORDING_PHASE) {
|
|
1888
|
+
const operation = new Operation([$.wrap(left, true), op.image, $.wrap(right)], undefined, $.getLocationFromNodes([left, right]), this.context);
|
|
1889
|
+
left = operation;
|
|
1890
|
+
}
|
|
1891
|
+
});
|
|
1892
|
+
if (!RECORDING_PHASE) {
|
|
1893
|
+
$.endRule();
|
|
1894
|
+
return left;
|
|
1895
|
+
}
|
|
1896
|
+
};
|
|
1897
|
+
}
|
|
1898
|
+
export function expressionValue(T) {
|
|
1899
|
+
const $ = this;
|
|
1900
|
+
return (ctx = {}) => {
|
|
1901
|
+
let RECORDING_PHASE = $.RECORDING_PHASE;
|
|
1902
|
+
$.startRule();
|
|
1903
|
+
/** Can create a negative expression */
|
|
1904
|
+
let minus = $.OPTION(() => $.CONSUME(T.Minus));
|
|
1905
|
+
let node = $.OR([
|
|
1906
|
+
{
|
|
1907
|
+
ALT: () => {
|
|
1908
|
+
$.startRule();
|
|
1909
|
+
let escape;
|
|
1910
|
+
$.OPTION2(() => {
|
|
1911
|
+
escape = $.CONSUME(T.Tilde);
|
|
1912
|
+
});
|
|
1913
|
+
$.CONSUME(T.LParen);
|
|
1914
|
+
const innerCtx = {
|
|
1915
|
+
...ctx,
|
|
1916
|
+
inner: true,
|
|
1917
|
+
allowComma: true,
|
|
1918
|
+
// Parentheses in Less enable "math in parens" semantics
|
|
1919
|
+
parenFrames: [...getParenFrames(ctx), true]
|
|
1920
|
+
};
|
|
1921
|
+
let node = $.SUBRULE($.valueList, { ARGS: [innerCtx] });
|
|
1922
|
+
$.CONSUME(T.RParen);
|
|
1923
|
+
if (!RECORDING_PHASE) {
|
|
1924
|
+
let location = $.endRule();
|
|
1925
|
+
node = $.wrap(node, 'both');
|
|
1926
|
+
return new Paren(node, { escaped: !!escape }, location, this.context);
|
|
1927
|
+
}
|
|
1928
|
+
}
|
|
1929
|
+
},
|
|
1930
|
+
{ ALT: () => $.SUBRULE($.value, { ARGS: [ctx] }) }
|
|
1931
|
+
]);
|
|
1932
|
+
if (!RECORDING_PHASE) {
|
|
1933
|
+
let location = $.endRule();
|
|
1934
|
+
if (minus) {
|
|
1935
|
+
return new Negative(node, undefined, location, this.context);
|
|
1936
|
+
}
|
|
1937
|
+
return node;
|
|
1938
|
+
}
|
|
1939
|
+
};
|
|
1940
|
+
}
|
|
1941
|
+
/**
|
|
1942
|
+
* Add interpolation
|
|
1943
|
+
*/
|
|
1944
|
+
export function nthValue(T) {
|
|
1945
|
+
const $ = this;
|
|
1946
|
+
let nthValueAlt = (ctx = {}) => [
|
|
1947
|
+
{ ALT: () => $.CONSUME(T.InterpolatedIdent) },
|
|
1948
|
+
{ ALT: () => $.CONSUME(T.NthOdd) },
|
|
1949
|
+
{ ALT: () => $.CONSUME(T.NthEven) },
|
|
1950
|
+
{ ALT: () => $.CONSUME(T.Integer) },
|
|
1951
|
+
{
|
|
1952
|
+
ALT: () => {
|
|
1953
|
+
$.OR2([
|
|
1954
|
+
{ ALT: () => $.CONSUME(T.NthSignedDimension) },
|
|
1955
|
+
{ ALT: () => $.CONSUME(T.NthUnsignedDimension) },
|
|
1956
|
+
{ ALT: () => $.CONSUME(T.NthSignedPlus) },
|
|
1957
|
+
{ ALT: () => $.CONSUME(T.NthIdent) }
|
|
1958
|
+
]);
|
|
1959
|
+
$.OPTION(() => {
|
|
1960
|
+
$.OR3([
|
|
1961
|
+
{ ALT: () => $.CONSUME(T.SignedInt) },
|
|
1962
|
+
{
|
|
1963
|
+
ALT: () => {
|
|
1964
|
+
$.CONSUME(T.Minus);
|
|
1965
|
+
$.CONSUME(T.UnsignedInt);
|
|
1966
|
+
}
|
|
1967
|
+
}
|
|
1968
|
+
]);
|
|
1969
|
+
});
|
|
1970
|
+
$.OPTION2(() => {
|
|
1971
|
+
$.CONSUME(T.Of);
|
|
1972
|
+
$.SUBRULE($.complexSelector, { ARGS: [ctx] });
|
|
1973
|
+
});
|
|
1974
|
+
}
|
|
1975
|
+
}
|
|
1976
|
+
];
|
|
1977
|
+
return cssNthValue.call(this, T, nthValueAlt);
|
|
1978
|
+
}
|
|
1979
|
+
export function knownFunctions(T) {
|
|
1980
|
+
const $ = this;
|
|
1981
|
+
let functions = (ctx = {}) => [
|
|
1982
|
+
{ ALT: () => $.SUBRULE($.urlFunction, { ARGS: [ctx] }) },
|
|
1983
|
+
{ ALT: () => $.SUBRULE($.varFunction, { ARGS: [ctx] }) },
|
|
1984
|
+
{ ALT: () => $.SUBRULE($.calcFunction, { ARGS: [ctx] }) },
|
|
1985
|
+
// colorFunction is already in cssKnownFunctions default, so we don't need to add it here
|
|
1986
|
+
{ ALT: () => $.SUBRULE($.ifFunction, { ARGS: [ctx] }) },
|
|
1987
|
+
{ ALT: () => $.SUBRULE($.booleanFunction, { ARGS: [ctx] }) }
|
|
1988
|
+
];
|
|
1989
|
+
return cssKnownFunctions.call(this, T, functions);
|
|
1990
|
+
}
|
|
1991
|
+
/**
|
|
1992
|
+
* Override CSS calc() parsing so we can maintain parse-time `calcFrames`.
|
|
1993
|
+
* This is the parse-time analogue of `Call.evalNode`'s calcFrames++/--.
|
|
1994
|
+
*/
|
|
1995
|
+
export function calcFunction(T) {
|
|
1996
|
+
const $ = this;
|
|
1997
|
+
return (ctx = {}) => {
|
|
1998
|
+
$.startRule();
|
|
1999
|
+
$.CONSUME(T.Calc);
|
|
2000
|
+
const innerCtx = withCalcFrame(ctx, 1);
|
|
2001
|
+
const args = $.SUBRULE($.mathSum, { ARGS: [innerCtx] });
|
|
2002
|
+
$.CONSUME2(T.RParen);
|
|
2003
|
+
if (!$.RECORDING_PHASE) {
|
|
2004
|
+
const location = $.endRule();
|
|
2005
|
+
return new Call({
|
|
2006
|
+
name: 'calc',
|
|
2007
|
+
args: new List([args])
|
|
2008
|
+
}, undefined, location, this.context);
|
|
2009
|
+
}
|
|
2010
|
+
};
|
|
2011
|
+
}
|
|
2012
|
+
export function ifFunction(T) {
|
|
2013
|
+
const $ = this;
|
|
2014
|
+
return (ctx = {}) => {
|
|
2015
|
+
let RECORDING_PHASE = $.RECORDING_PHASE;
|
|
2016
|
+
$.startRule();
|
|
2017
|
+
let name = $.CONSUME(T.IfFunction);
|
|
2018
|
+
let args = new List([]);
|
|
2019
|
+
let isCssBranch = false;
|
|
2020
|
+
$.OR([
|
|
2021
|
+
{
|
|
2022
|
+
ALT: () => {
|
|
2023
|
+
isCssBranch = true;
|
|
2024
|
+
const cssArgs = $.SUBRULE($.ifFunctionArgs, { ARGS: [{ ...ctx, inner: true }] });
|
|
2025
|
+
$.CONSUME(T.RParen);
|
|
2026
|
+
if (!RECORDING_PHASE) {
|
|
2027
|
+
args = new List([cssArgs]);
|
|
2028
|
+
}
|
|
2029
|
+
}
|
|
2030
|
+
},
|
|
2031
|
+
{
|
|
2032
|
+
ALT: () => {
|
|
2033
|
+
isCssBranch = false;
|
|
2034
|
+
let node = $.SUBRULE($.guardInner, { ARGS: [{ ...ctx, inValueList: true }] });
|
|
2035
|
+
if (!RECORDING_PHASE) {
|
|
2036
|
+
const condNode = node instanceof Paren && node.value instanceof Node ? node.value : node;
|
|
2037
|
+
args = new List([condNode]);
|
|
2038
|
+
}
|
|
2039
|
+
$.OR2([
|
|
2040
|
+
{
|
|
2041
|
+
ALT: () => {
|
|
2042
|
+
$.CONSUME(T.Semi);
|
|
2043
|
+
node = $.SUBRULE($.valueList, { ARGS: [{ ...ctx, allowAnonymousMixins: true }] });
|
|
2044
|
+
if (!RECORDING_PHASE) {
|
|
2045
|
+
args.value.push(node);
|
|
2046
|
+
}
|
|
2047
|
+
$.OPTION(() => {
|
|
2048
|
+
$.CONSUME2(T.Semi);
|
|
2049
|
+
node = $.SUBRULE2($.valueList, { ARGS: [{ ...ctx, allowAnonymousMixins: true }] });
|
|
2050
|
+
if (!RECORDING_PHASE) {
|
|
2051
|
+
args.value.push(node);
|
|
2052
|
+
}
|
|
2053
|
+
});
|
|
2054
|
+
}
|
|
2055
|
+
},
|
|
2056
|
+
{
|
|
2057
|
+
ALT: () => {
|
|
2058
|
+
$.CONSUME(T.Comma);
|
|
2059
|
+
node = $.SUBRULE($.callArgument, { ARGS: [{ ...ctx, allowAnonymousMixins: true }] });
|
|
2060
|
+
if (!RECORDING_PHASE) {
|
|
2061
|
+
args.value.push(node);
|
|
2062
|
+
}
|
|
2063
|
+
$.OPTION2(() => {
|
|
2064
|
+
$.CONSUME2(T.Comma);
|
|
2065
|
+
node = $.SUBRULE2($.callArgument, { ARGS: [{ ...ctx, allowAnonymousMixins: true }] });
|
|
2066
|
+
if (!RECORDING_PHASE) {
|
|
2067
|
+
args.value.push(node);
|
|
2068
|
+
}
|
|
2069
|
+
});
|
|
2070
|
+
}
|
|
2071
|
+
}
|
|
2072
|
+
]);
|
|
2073
|
+
$.CONSUME2(T.RParen);
|
|
2074
|
+
}
|
|
2075
|
+
}
|
|
2076
|
+
]);
|
|
2077
|
+
if (!RECORDING_PHASE) {
|
|
2078
|
+
let location = $.endRule();
|
|
2079
|
+
let nameNode = new Reference('if', {
|
|
2080
|
+
type: 'function',
|
|
2081
|
+
fallbackValue: isCssBranch ? true : undefined
|
|
2082
|
+
}, $.getLocationInfo(name), this.context);
|
|
2083
|
+
const callNode = new Call({ name: nameNode, args }, undefined, location, this.context);
|
|
2084
|
+
return callNode;
|
|
2085
|
+
}
|
|
2086
|
+
};
|
|
2087
|
+
}
|
|
2088
|
+
export function booleanFunction(T) {
|
|
2089
|
+
const $ = this;
|
|
2090
|
+
return (ctx = {}) => {
|
|
2091
|
+
$.startRule();
|
|
2092
|
+
$.CONSUME(T.BooleanFunction);
|
|
2093
|
+
let arg = $.SUBRULE($.guardInner, { ARGS: [{ ...ctx, inValueList: true }] });
|
|
2094
|
+
$.CONSUME(T.RParen);
|
|
2095
|
+
if (!$.RECORDING_PHASE) {
|
|
2096
|
+
let location = $.endRule();
|
|
2097
|
+
const conditionNode = arg instanceof Paren && arg.value instanceof Node ? arg.value : arg;
|
|
2098
|
+
const exprNode = new Expression(conditionNode, { parens: true }, location, this.context);
|
|
2099
|
+
return exprNode;
|
|
2100
|
+
}
|
|
2101
|
+
};
|
|
2102
|
+
}
|
|
2103
|
+
/** At AST time, join comma-lists together if separated by semis */
|
|
2104
|
+
// $.RULE('functionValueList', (ctx: RuleContext = {}) => {
|
|
2105
|
+
// ctx.allowAnonymousMixins = true
|
|
2106
|
+
// $.SUBRULE($.valueSequence, { ARGS: [ctx] })
|
|
2107
|
+
// $.MANY(() => {
|
|
2108
|
+
// $.OR([
|
|
2109
|
+
// { ALT: () => $.CONSUME(T.Comma) },
|
|
2110
|
+
// { ALT: () => $.CONSUME(T.Semi) }
|
|
2111
|
+
// ])
|
|
2112
|
+
// $.SUBRULE2($.valueSequence, { ARGS: [ctx] })
|
|
2113
|
+
// })
|
|
2114
|
+
// })
|
|
2115
|
+
export function varReference(T) {
|
|
2116
|
+
const $ = this;
|
|
2117
|
+
return (ctx = {}) => {
|
|
2118
|
+
let RECORDING_PHASE = $.RECORDING_PHASE;
|
|
2119
|
+
let node = $.OR([
|
|
2120
|
+
{
|
|
2121
|
+
ALT: () => {
|
|
2122
|
+
let token = $.CONSUME(T.PropertyReference);
|
|
2123
|
+
if (!RECORDING_PHASE) {
|
|
2124
|
+
// Warn about $ident in custom property values - it's treated as literal text, not a property reference
|
|
2125
|
+
if (ctx.inCustomPropertyValue) {
|
|
2126
|
+
const atName = token.image;
|
|
2127
|
+
const ident = token.image.slice(1);
|
|
2128
|
+
$.warnDeprecation(`${atName} in custom property values is treated as literal text, not a property reference. Use \${${ident}} if you want it to be evaluated.`, token, 'property-in-unknown-value');
|
|
2129
|
+
return new Reference({ key: token.image.slice(1) }, { type: 'property', role: 'ident' }, $.getLocationInfo(token), this.context);
|
|
2130
|
+
}
|
|
2131
|
+
return new Reference(token.image.slice(1), { type: 'property' }, $.getLocationInfo(token), this.context);
|
|
2132
|
+
}
|
|
2133
|
+
}
|
|
2134
|
+
},
|
|
2135
|
+
{
|
|
2136
|
+
ALT: () => {
|
|
2137
|
+
let token = $.CONSUME(T.NestedReference);
|
|
2138
|
+
if (!RECORDING_PHASE) {
|
|
2139
|
+
const raw = token.image;
|
|
2140
|
+
const type = raw.startsWith('@') ? 'variable' : 'property';
|
|
2141
|
+
const key = getInterpolatedOrString(raw);
|
|
2142
|
+
if (ctx.inCustomPropertyValue && typeof key === 'string') {
|
|
2143
|
+
return new Reference({ key }, { type: 'variable', role: 'ident' }, $.getLocationInfo(token), this.context);
|
|
2144
|
+
}
|
|
2145
|
+
if (typeof key === 'string') {
|
|
2146
|
+
return new Reference(key, { type }, $.getLocationInfo(token), this.context);
|
|
2147
|
+
}
|
|
2148
|
+
return new Reference({ key }, { type }, $.getLocationInfo(token), this.context);
|
|
2149
|
+
}
|
|
2150
|
+
}
|
|
2151
|
+
},
|
|
2152
|
+
{
|
|
2153
|
+
ALT: () => {
|
|
2154
|
+
let token = $.SUBRULE($.varName, { ARGS: [ctx] });
|
|
2155
|
+
if (!RECORDING_PHASE) {
|
|
2156
|
+
// Warn about @ident in custom property values - it's treated as literal text, not a variable reference
|
|
2157
|
+
if (ctx.inCustomPropertyValue) {
|
|
2158
|
+
const atName = token.image;
|
|
2159
|
+
const ident = token.image.slice(1);
|
|
2160
|
+
$.warnDeprecation(`${atName} in custom property values is treated as literal text, not a variable reference. Use @{${ident}} if you want it to be evaluated.`, token, 'variable-in-unknown-value');
|
|
2161
|
+
return new Reference({ key: token.image.slice(1) }, { type: 'variable', role: 'ident' }, $.getLocationInfo(token), this.context);
|
|
2162
|
+
}
|
|
2163
|
+
return new Reference(token.image.slice(1), { type: 'variable' }, $.getLocationInfo(token), this.context);
|
|
2164
|
+
}
|
|
2165
|
+
}
|
|
2166
|
+
}
|
|
2167
|
+
]);
|
|
2168
|
+
$.OR2([
|
|
2169
|
+
{
|
|
2170
|
+
ALT: () => {
|
|
2171
|
+
/** This spreads a (list) value within a containing list when evaluated */
|
|
2172
|
+
let token = $.CONSUME(T.Ellipsis);
|
|
2173
|
+
if (!RECORDING_PHASE) {
|
|
2174
|
+
node = new Rest(node, undefined, $.getLocationFromNodes([node, token]), this.context);
|
|
2175
|
+
}
|
|
2176
|
+
}
|
|
2177
|
+
},
|
|
2178
|
+
{
|
|
2179
|
+
/** Only variables can have accessors */
|
|
2180
|
+
GATE: () => {
|
|
2181
|
+
if (node?.options?.type !== 'variable') {
|
|
2182
|
+
return false;
|
|
2183
|
+
}
|
|
2184
|
+
let next = $.LA(1).tokenType;
|
|
2185
|
+
if (next !== T.LSquare && next !== T.LParen) {
|
|
2186
|
+
return false;
|
|
2187
|
+
}
|
|
2188
|
+
if (!$.noSep()) {
|
|
2189
|
+
return false;
|
|
2190
|
+
}
|
|
2191
|
+
return true;
|
|
2192
|
+
},
|
|
2193
|
+
ALT: () => {
|
|
2194
|
+
$.AT_LEAST_ONE({
|
|
2195
|
+
GATE: () => {
|
|
2196
|
+
let next = $.LA(1).tokenType;
|
|
2197
|
+
if (next !== T.LSquare && next !== T.LParen) {
|
|
2198
|
+
return false;
|
|
2199
|
+
}
|
|
2200
|
+
if (!$.noSep()) {
|
|
2201
|
+
return false;
|
|
2202
|
+
}
|
|
2203
|
+
return true;
|
|
2204
|
+
},
|
|
2205
|
+
DEF: () => {
|
|
2206
|
+
node = $.SUBRULE($.lookupOrCall, { ARGS: [{ ...ctx, node: node }] });
|
|
2207
|
+
}
|
|
2208
|
+
});
|
|
2209
|
+
$.OPTION(() => {
|
|
2210
|
+
$.OPTION2(() => $.CONSUME(T.Gt));
|
|
2211
|
+
node = $.SUBRULE($.mixinReference, { ARGS: [{ ...ctx, node: node }] });
|
|
2212
|
+
});
|
|
2213
|
+
}
|
|
2214
|
+
},
|
|
2215
|
+
{ ALT: EMPTY_ALT() }
|
|
2216
|
+
]);
|
|
2217
|
+
if (!RECORDING_PHASE) {
|
|
2218
|
+
return $.wrap(node);
|
|
2219
|
+
}
|
|
2220
|
+
};
|
|
2221
|
+
}
|
|
2222
|
+
export function valueReference(_T) {
|
|
2223
|
+
const $ = this;
|
|
2224
|
+
return (ctx = {}) => {
|
|
2225
|
+
return $.OR([
|
|
2226
|
+
{ ALT: () => $.SUBRULE($.varReference, { ARGS: [ctx] }) },
|
|
2227
|
+
{ ALT: () => $.SUBRULE($.mixinReference, { ARGS: [ctx] }) }
|
|
2228
|
+
]);
|
|
2229
|
+
};
|
|
2230
|
+
}
|
|
2231
|
+
export function functionCall(T) {
|
|
2232
|
+
const $ = this;
|
|
2233
|
+
const modernColorFunctions = new Set(['rgb', 'rgba', 'hsl', 'hsla']);
|
|
2234
|
+
const isModernColorCall = (name, args) => {
|
|
2235
|
+
if (!modernColorFunctions.has(name.toLowerCase())) {
|
|
2236
|
+
return false;
|
|
2237
|
+
}
|
|
2238
|
+
if (!args || args.value.length !== 1) {
|
|
2239
|
+
return false;
|
|
2240
|
+
}
|
|
2241
|
+
const firstArg = args.value[0];
|
|
2242
|
+
return Boolean(isNode(firstArg, 'Sequence') && firstArg.value.length >= 2);
|
|
2243
|
+
};
|
|
2244
|
+
let funcAlt = (ctx = {}) => [
|
|
2245
|
+
{
|
|
2246
|
+
// Disambiguate known functions by their dedicated tokens
|
|
2247
|
+
GATE: () => {
|
|
2248
|
+
let tokenType = $.LA(1).tokenType;
|
|
2249
|
+
return tokenType === T.UrlStart
|
|
2250
|
+
|| tokenType === T.Var
|
|
2251
|
+
|| tokenType === T.Calc
|
|
2252
|
+
|| tokenType === T.IfFunction
|
|
2253
|
+
|| tokenType === T.BooleanFunction;
|
|
2254
|
+
},
|
|
2255
|
+
ALT: () => $.SUBRULE($.knownFunctions, { ARGS: [ctx] })
|
|
2256
|
+
},
|
|
2257
|
+
{
|
|
2258
|
+
// Generic function via FunctionStart token
|
|
2259
|
+
GATE: () => {
|
|
2260
|
+
let tokenType = $.LA(1).tokenType;
|
|
2261
|
+
return tokenType !== T.UrlStart
|
|
2262
|
+
&& tokenType !== T.Var
|
|
2263
|
+
&& tokenType !== T.Calc
|
|
2264
|
+
&& tokenType !== T.IfFunction
|
|
2265
|
+
&& tokenType !== T.BooleanFunction;
|
|
2266
|
+
},
|
|
2267
|
+
ALT: () => {
|
|
2268
|
+
$.startRule();
|
|
2269
|
+
const fnStart = $.CONSUME(T.FunctionStart);
|
|
2270
|
+
const fnNameForCtx = fnStart.image.slice(0, -1);
|
|
2271
|
+
let args;
|
|
2272
|
+
$.OPTION(() => args = $.SUBRULE($.functionCallArgs, { ARGS: [{ ...ctx, currentFunctionName: fnNameForCtx }] }));
|
|
2273
|
+
$.CONSUME(T.RParen);
|
|
2274
|
+
if (!$.RECORDING_PHASE) {
|
|
2275
|
+
const location = $.endRule();
|
|
2276
|
+
const nameValue = fnNameForCtx;
|
|
2277
|
+
if (nameValue === 'unit' && args?.value[1] instanceof Any) {
|
|
2278
|
+
const unitArg = args.value[1];
|
|
2279
|
+
const quotedUnit = new Quoted(unitArg.valueOf(), { quote: '"' }, undefined, this.context);
|
|
2280
|
+
quotedUnit.pre = unitArg.pre;
|
|
2281
|
+
quotedUnit.post = unitArg.post;
|
|
2282
|
+
args.value[1] = quotedUnit;
|
|
2283
|
+
}
|
|
2284
|
+
const nameNode = new Reference(nameValue, { type: 'function', fallbackValue: true }, $.getLocationInfo(fnStart), this.context);
|
|
2285
|
+
/** Less / Sass functions we try to call that throw just get turned into calls. */
|
|
2286
|
+
const modernSyntax = isModernColorCall(nameValue, args);
|
|
2287
|
+
return new Call({ name: nameNode, args }, { silentFail: true, ...(modernSyntax ? { modernSyntax: true } : {}) }, location, this.context);
|
|
2288
|
+
}
|
|
2289
|
+
}
|
|
2290
|
+
}
|
|
2291
|
+
];
|
|
2292
|
+
return (ctx = {}) => $.OR(funcAlt(ctx));
|
|
2293
|
+
}
|
|
2294
|
+
export function functionCallArgs(T) {
|
|
2295
|
+
const $ = this;
|
|
2296
|
+
return (ctx = {}) => {
|
|
2297
|
+
let RECORDING_PHASE = $.RECORDING_PHASE;
|
|
2298
|
+
$.startRule();
|
|
2299
|
+
// Inside function arguments, allow inner tokens like ':'
|
|
2300
|
+
const prevInner = ctx.inner;
|
|
2301
|
+
ctx.inner = true;
|
|
2302
|
+
// Calls intentionally push a `false` paren frame (matches `Call.evalNode`)
|
|
2303
|
+
const argCtx = {
|
|
2304
|
+
...ctx,
|
|
2305
|
+
allowComma: false,
|
|
2306
|
+
parenFrames: [...getParenFrames(ctx), false],
|
|
2307
|
+
detachedRulesetUsage: 'function-arg'
|
|
2308
|
+
};
|
|
2309
|
+
let node = $.SUBRULE($.callArgument, { ARGS: [argCtx] });
|
|
2310
|
+
let commaNodes;
|
|
2311
|
+
let semiNodes;
|
|
2312
|
+
if (!RECORDING_PHASE) {
|
|
2313
|
+
commaNodes = [$.wrap(node, true)];
|
|
2314
|
+
semiNodes = [];
|
|
2315
|
+
}
|
|
2316
|
+
let isSemiList = false;
|
|
2317
|
+
// First, consume any comma-separated arguments
|
|
2318
|
+
$.MANY(() => {
|
|
2319
|
+
$.CONSUME(T.Comma);
|
|
2320
|
+
node = $.SUBRULE2($.callArgument, { ARGS: [argCtx] });
|
|
2321
|
+
if (!RECORDING_PHASE) {
|
|
2322
|
+
commaNodes.push($.wrap(node, true));
|
|
2323
|
+
}
|
|
2324
|
+
});
|
|
2325
|
+
// Then, optionally switch to semicolon-separated list and continue with semicolons
|
|
2326
|
+
$.OPTION(() => {
|
|
2327
|
+
isSemiList = true;
|
|
2328
|
+
$.CONSUME(T.Semi);
|
|
2329
|
+
if (!RECORDING_PHASE) {
|
|
2330
|
+
// Aggregate the previous set of comma-nodes as the first semi item
|
|
2331
|
+
if (commaNodes.length > 1) {
|
|
2332
|
+
semiNodes.push(new List(commaNodes, undefined, $.getLocationFromNodes(commaNodes), this.context));
|
|
2333
|
+
}
|
|
2334
|
+
else {
|
|
2335
|
+
semiNodes.push(commaNodes[0]);
|
|
2336
|
+
}
|
|
2337
|
+
}
|
|
2338
|
+
node = $.SUBRULE3($.callArgument, { ARGS: [{ ...argCtx, allowComma: true }] });
|
|
2339
|
+
if (!RECORDING_PHASE) {
|
|
2340
|
+
semiNodes.push($.wrap(node, true));
|
|
2341
|
+
}
|
|
2342
|
+
$.MANY2(() => {
|
|
2343
|
+
$.CONSUME2(T.Semi);
|
|
2344
|
+
node = $.SUBRULE4($.callArgument, { ARGS: [{ ...argCtx, allowComma: true }] });
|
|
2345
|
+
if (!RECORDING_PHASE) {
|
|
2346
|
+
semiNodes.push($.wrap(node, true));
|
|
2347
|
+
}
|
|
2348
|
+
});
|
|
2349
|
+
});
|
|
2350
|
+
if (!RECORDING_PHASE) {
|
|
2351
|
+
ctx.inner = prevInner;
|
|
2352
|
+
$.endRule();
|
|
2353
|
+
const nodes = isSemiList ? semiNodes : commaNodes;
|
|
2354
|
+
return new List(nodes, isSemiList ? { sep: ';' } : undefined);
|
|
2355
|
+
}
|
|
2356
|
+
};
|
|
2357
|
+
}
|
|
2358
|
+
export function value(T) {
|
|
2359
|
+
const $ = this;
|
|
2360
|
+
return (ctx = {}) => {
|
|
2361
|
+
if ($.LA(1).tokenType === T.Percent) {
|
|
2362
|
+
}
|
|
2363
|
+
// eslint-disable-next-line @typescript-eslint/naming-convention
|
|
2364
|
+
let _isMixinReference = undefined;
|
|
2365
|
+
const isMixinReference = () => {
|
|
2366
|
+
if (_isMixinReference === undefined) {
|
|
2367
|
+
let tt1 = $.LA(1).tokenType;
|
|
2368
|
+
let tt2 = $.LA(2).tokenType;
|
|
2369
|
+
/**
|
|
2370
|
+
* We'll allow a few "bare" mixin references without parens
|
|
2371
|
+
* or square brackets, but not if they'll conflict with
|
|
2372
|
+
* other syntax.
|
|
2373
|
+
*/
|
|
2374
|
+
_isMixinReference =
|
|
2375
|
+
tt1 === T.DotName
|
|
2376
|
+
|| tt1 === T.HashName
|
|
2377
|
+
|| tt1 === T.InterpolatedSelector
|
|
2378
|
+
|| ((tt1 === T.ColorIdentStart
|
|
2379
|
+
|| tt1 === T.InterpolatedSelector) && (tt2 === T.Gt
|
|
2380
|
+
|| tt2 === T.DotName
|
|
2381
|
+
|| tt2 === T.HashName
|
|
2382
|
+
|| tt2 === T.InterpolatedSelector
|
|
2383
|
+
|| ($.noSep(1)
|
|
2384
|
+
&& (tt2 === T.LParen
|
|
2385
|
+
|| tt2 === T.LSquare
|
|
2386
|
+
|| tt2 === T.HashName
|
|
2387
|
+
|| tt2 === T.DotName))));
|
|
2388
|
+
}
|
|
2389
|
+
return _isMixinReference;
|
|
2390
|
+
};
|
|
2391
|
+
let node = $.OR([
|
|
2392
|
+
{ ALT: () => $.SUBRULE($.functionCall, { ARGS: [ctx] }) },
|
|
2393
|
+
{
|
|
2394
|
+
GATE: () => $.LA(1).tokenType === T.Star && $.LA(2).tokenType === T.LSquare,
|
|
2395
|
+
ALT: () => $.SUBRULE($.selectorCapture, { ARGS: [ctx] })
|
|
2396
|
+
},
|
|
2397
|
+
{
|
|
2398
|
+
GATE: isMixinReference,
|
|
2399
|
+
ALT: () => $.SUBRULE($.mixinReference, { ARGS: [ctx] })
|
|
2400
|
+
},
|
|
2401
|
+
{
|
|
2402
|
+
GATE: () => !isMixinReference(),
|
|
2403
|
+
ALT: () => $.CONSUME(T.Color)
|
|
2404
|
+
},
|
|
2405
|
+
{
|
|
2406
|
+
GATE: () => !isMixinReference(),
|
|
2407
|
+
ALT: () => $.CONSUME(T.Ident)
|
|
2408
|
+
},
|
|
2409
|
+
{ ALT: () => $.SUBRULE($.varReference, { ARGS: [ctx] }) },
|
|
2410
|
+
{ ALT: () => $.CONSUME(T.DefaultGuardFunc) },
|
|
2411
|
+
{ ALT: () => $.CONSUME(T.Dimension) },
|
|
2412
|
+
{ ALT: () => $.CONSUME(T.Number) },
|
|
2413
|
+
{
|
|
2414
|
+
GATE: () => ctx.currentFunctionName === 'unit',
|
|
2415
|
+
ALT: () => $.CONSUME(T.Percent)
|
|
2416
|
+
},
|
|
2417
|
+
{ ALT: () => $.CONSUME(T.UnicodeRange) },
|
|
2418
|
+
{ ALT: () => $.SUBRULE($.string, { ARGS: [ctx] }) },
|
|
2419
|
+
{ ALT: () => $.CONSUME(T.JavaScript) },
|
|
2420
|
+
/** Explicitly not marked as an ident */
|
|
2421
|
+
{ ALT: () => $.CONSUME(T.When) },
|
|
2422
|
+
{ ALT: () => $.SUBRULE($.squareValue, { ARGS: [ctx] }) },
|
|
2423
|
+
{
|
|
2424
|
+
GATE: () => $.looseMode && !!ctx.inner,
|
|
2425
|
+
ALT: () => $.CONSUME(T.Colon)
|
|
2426
|
+
},
|
|
2427
|
+
{
|
|
2428
|
+
GATE: () => $.looseMode,
|
|
2429
|
+
ALT: () => $.CONSUME(T.Unknown)
|
|
2430
|
+
},
|
|
2431
|
+
{
|
|
2432
|
+
/** e.g. progid:DXImageTransform.Microsoft.Blur(pixelradius=2) */
|
|
2433
|
+
GATE: () => $.legacyMode,
|
|
2434
|
+
ALT: () => $.CONSUME(T.LegacyMSFilter)
|
|
2435
|
+
}
|
|
2436
|
+
]);
|
|
2437
|
+
if (!$.RECORDING_PHASE) {
|
|
2438
|
+
if (!(node instanceof Node)) {
|
|
2439
|
+
node = $.processValueToken(node);
|
|
2440
|
+
}
|
|
2441
|
+
return $.wrap(node);
|
|
2442
|
+
}
|
|
2443
|
+
};
|
|
2444
|
+
}
|
|
2445
|
+
export function string(T) {
|
|
2446
|
+
const $ = this;
|
|
2447
|
+
let stringAlt = [
|
|
2448
|
+
{
|
|
2449
|
+
ALT: () => {
|
|
2450
|
+
$.startRule();
|
|
2451
|
+
let quote = $.CONSUME(T.SingleQuoteStart);
|
|
2452
|
+
let contents;
|
|
2453
|
+
$.OPTION(() => contents = $.CONSUME(T.SingleQuoteStringContents));
|
|
2454
|
+
$.CONSUME(T.SingleQuoteEnd);
|
|
2455
|
+
if (!$.RECORDING_PHASE) {
|
|
2456
|
+
let quoteImg = quote.image;
|
|
2457
|
+
let escaped = false;
|
|
2458
|
+
if (quoteImg.startsWith('~')) {
|
|
2459
|
+
escaped = true;
|
|
2460
|
+
quoteImg = quoteImg.slice(1);
|
|
2461
|
+
}
|
|
2462
|
+
let location = $.endRule();
|
|
2463
|
+
let value = contents?.image;
|
|
2464
|
+
if (escaped && value) {
|
|
2465
|
+
value = value.replace(/\\(?:\r\n?|\n|\f)/g, '\n');
|
|
2466
|
+
}
|
|
2467
|
+
// Handle interpolation in string contents
|
|
2468
|
+
if (value && (value.includes('@{') || value.includes('${'))) {
|
|
2469
|
+
return new Quoted(processStringInterpolation(value, location, this.context), { quote: quoteImg, escaped }, location, this.context);
|
|
2470
|
+
}
|
|
2471
|
+
return new Quoted(new Any(value ?? '', { role: 'any' }), { quote: quoteImg, escaped }, location, this.context);
|
|
2472
|
+
}
|
|
2473
|
+
}
|
|
2474
|
+
},
|
|
2475
|
+
{
|
|
2476
|
+
ALT: () => {
|
|
2477
|
+
$.startRule();
|
|
2478
|
+
let quote = $.CONSUME(T.DoubleQuoteStart);
|
|
2479
|
+
let contents;
|
|
2480
|
+
$.OPTION2(() => contents = $.CONSUME(T.DoubleQuoteStringContents));
|
|
2481
|
+
$.CONSUME(T.DoubleQuoteEnd);
|
|
2482
|
+
if (!$.RECORDING_PHASE) {
|
|
2483
|
+
let quoteImg = quote.image;
|
|
2484
|
+
let escaped = false;
|
|
2485
|
+
if (quoteImg.startsWith('~')) {
|
|
2486
|
+
escaped = true;
|
|
2487
|
+
quoteImg = quoteImg.slice(1);
|
|
2488
|
+
}
|
|
2489
|
+
let location = $.endRule();
|
|
2490
|
+
let value = contents?.image;
|
|
2491
|
+
if (escaped && value) {
|
|
2492
|
+
value = value.replace(/\\(?:\r\n?|\n|\f)/g, '\n');
|
|
2493
|
+
}
|
|
2494
|
+
// Handle interpolation in string contents
|
|
2495
|
+
if (value && (value.includes('@{') || value.includes('${'))) {
|
|
2496
|
+
return new Quoted(processStringInterpolation(value, location, this.context), { quote: quoteImg, escaped }, location, this.context);
|
|
2497
|
+
}
|
|
2498
|
+
return new Quoted(new Any(value ?? '', { role: 'any' }), { quote: quoteImg, escaped }, location, this.context);
|
|
2499
|
+
}
|
|
2500
|
+
}
|
|
2501
|
+
}
|
|
2502
|
+
];
|
|
2503
|
+
return (_ctx = {}) => $.OR(stringAlt);
|
|
2504
|
+
}
|
|
2505
|
+
/**
|
|
2506
|
+
* Find interpolation patterns like @{...} or ${...}, handling nested braces.
|
|
2507
|
+
* Returns an array of { start, end, prefix, content } for each match.
|
|
2508
|
+
*/
|
|
2509
|
+
function findInterpolations(value) {
|
|
2510
|
+
const matches = [];
|
|
2511
|
+
let i = 0;
|
|
2512
|
+
while (i < value.length) {
|
|
2513
|
+
// Look for @{ or ${
|
|
2514
|
+
if ((value[i] === '@' || value[i] === '$') && value[i + 1] === '{') {
|
|
2515
|
+
const prefix = value[i];
|
|
2516
|
+
const start = i;
|
|
2517
|
+
i += 2; // Skip @{ or ${
|
|
2518
|
+
let braceCount = 1;
|
|
2519
|
+
const contentStart = i;
|
|
2520
|
+
// Find matching closing brace, counting nested braces
|
|
2521
|
+
while (i < value.length && braceCount > 0) {
|
|
2522
|
+
if (value[i] === '{') {
|
|
2523
|
+
braceCount++;
|
|
2524
|
+
}
|
|
2525
|
+
else if (value[i] === '}') {
|
|
2526
|
+
braceCount--;
|
|
2527
|
+
}
|
|
2528
|
+
i++;
|
|
2529
|
+
}
|
|
2530
|
+
if (braceCount === 0) {
|
|
2531
|
+
const content = value.slice(contentStart, i - 1);
|
|
2532
|
+
matches.push({ start, end: i, prefix, content });
|
|
2533
|
+
}
|
|
2534
|
+
}
|
|
2535
|
+
else {
|
|
2536
|
+
i++;
|
|
2537
|
+
}
|
|
2538
|
+
}
|
|
2539
|
+
return matches;
|
|
2540
|
+
}
|
|
2541
|
+
// Helper function to process string interpolation (handles nested @{...} patterns)
|
|
2542
|
+
function processStringInterpolation(value, location, context) {
|
|
2543
|
+
const matches = findInterpolations(value);
|
|
2544
|
+
if (matches.length === 0) {
|
|
2545
|
+
return new Any(value, { role: 'any' }, location, context);
|
|
2546
|
+
}
|
|
2547
|
+
const replacements = [];
|
|
2548
|
+
let source = value;
|
|
2549
|
+
let offset = 0;
|
|
2550
|
+
for (const match of matches) {
|
|
2551
|
+
const adjustedStart = match.start - offset;
|
|
2552
|
+
const adjustedEnd = match.end - offset;
|
|
2553
|
+
const before = source.slice(0, adjustedStart);
|
|
2554
|
+
const after = source.slice(adjustedEnd);
|
|
2555
|
+
source = before + INTERPOLATION_PLACEHOLDER + after;
|
|
2556
|
+
offset += (match.end - match.start) - INTERPOLATION_PLACEHOLDER.length;
|
|
2557
|
+
// Recursively process the content in case it has nested interpolation
|
|
2558
|
+
const innerResult = processStringInterpolation(match.content, location, context);
|
|
2559
|
+
if (innerResult instanceof Interpolated) {
|
|
2560
|
+
// Nested interpolation in string contexts still resolves through a reference,
|
|
2561
|
+
// but must remain expression-wrapped in Jess output.
|
|
2562
|
+
const nestedRef = new Reference({ key: innerResult }, { type: 'variable', role: 'ident' }, location, context);
|
|
2563
|
+
replacements.push(new Expression(nestedRef, undefined, location, context));
|
|
2564
|
+
}
|
|
2565
|
+
else {
|
|
2566
|
+
// Simple interpolation reference
|
|
2567
|
+
replacements.push(createInterpolatedReference(match.prefix, match.content, location, context));
|
|
2568
|
+
}
|
|
2569
|
+
}
|
|
2570
|
+
return new Interpolated({ source, replacements }, { role: 'ident' }, location, context);
|
|
2571
|
+
}
|
|
2572
|
+
export function mathValue(T) {
|
|
2573
|
+
const $ = this;
|
|
2574
|
+
let valueAlt = (ctx = {}) => [
|
|
2575
|
+
{ ALT: () => $.CONSUME(T.AtKeyword) },
|
|
2576
|
+
{ ALT: () => $.CONSUME(T.Number) },
|
|
2577
|
+
{ ALT: () => $.CONSUME(T.Dimension) },
|
|
2578
|
+
// Allow identifiers like channel names in color space calcs (e.g., calc(l - 0.1))
|
|
2579
|
+
{ ALT: () => $.CONSUME(T.Ident) },
|
|
2580
|
+
{ ALT: () => $.SUBRULE($.functionCall, { ARGS: [ctx] }) },
|
|
2581
|
+
{
|
|
2582
|
+
/** Only allow escaped strings in calc */
|
|
2583
|
+
GATE: () => $.LA(1).image.startsWith('~'),
|
|
2584
|
+
ALT: () => $.SUBRULE($.string, { ARGS: [ctx] })
|
|
2585
|
+
},
|
|
2586
|
+
{
|
|
2587
|
+
/** For some reason, e() goes here instead of $.function */
|
|
2588
|
+
GATE: () => $.LA(2).tokenType !== T.LParen,
|
|
2589
|
+
ALT: () => $.CONSUME(T.MathConstant)
|
|
2590
|
+
},
|
|
2591
|
+
{ ALT: () => $.SUBRULE($.mathParen, { ARGS: [ctx] }) }
|
|
2592
|
+
];
|
|
2593
|
+
return cssMathValue.call(this, T, valueAlt);
|
|
2594
|
+
}
|
|
2595
|
+
/** @todo - add interpolation */
|
|
2596
|
+
// $.OVERRIDE_RULE('string', () => {
|
|
2597
|
+
// $.OR([
|
|
2598
|
+
// {
|
|
2599
|
+
// ALT: () => {
|
|
2600
|
+
// $.CONSUME(T.SingleQuoteStart)
|
|
2601
|
+
// $.OPTION(() => $.CONSUME(T.SingleQuoteStringContents))
|
|
2602
|
+
// $.CONSUME(T.SingleQuoteEnd)
|
|
2603
|
+
// }
|
|
2604
|
+
// },
|
|
2605
|
+
// {
|
|
2606
|
+
// ALT: () => {
|
|
2607
|
+
// $.CONSUME(T.DoubleQuoteStart)
|
|
2608
|
+
// $.OPTION2(() => $.CONSUME(T.DoubleQuoteStringContents))
|
|
2609
|
+
// $.CONSUME(T.DoubleQuoteEnd)
|
|
2610
|
+
// }
|
|
2611
|
+
// }
|
|
2612
|
+
// ])
|
|
2613
|
+
// })
|
|
2614
|
+
export function guard(T) {
|
|
2615
|
+
const $ = this;
|
|
2616
|
+
return (ctx = {}) => {
|
|
2617
|
+
$.CONSUME(T.When);
|
|
2618
|
+
return $.OR2([
|
|
2619
|
+
{
|
|
2620
|
+
GATE: () => !!ctx.inValueList,
|
|
2621
|
+
ALT: () => $.SUBRULE($.comparison, { ARGS: [ctx] })
|
|
2622
|
+
},
|
|
2623
|
+
{
|
|
2624
|
+
ALT: () => {
|
|
2625
|
+
ctx.allowComma = true;
|
|
2626
|
+
const node = $.SUBRULE($.guardOr, { ARGS: [ctx] });
|
|
2627
|
+
return node;
|
|
2628
|
+
}
|
|
2629
|
+
}
|
|
2630
|
+
]);
|
|
2631
|
+
};
|
|
2632
|
+
}
|
|
2633
|
+
/**
|
|
2634
|
+
* 'or' expression
|
|
2635
|
+
* Allows an (outer) comma like historical media queries
|
|
2636
|
+
*/
|
|
2637
|
+
export function guardOr(T) {
|
|
2638
|
+
const $ = this;
|
|
2639
|
+
return (ctx = {}) => {
|
|
2640
|
+
let RECORDING_PHASE = $.RECORDING_PHASE;
|
|
2641
|
+
$.startRule();
|
|
2642
|
+
let left = $.SUBRULE($.guardAnd, { ARGS: [ctx] });
|
|
2643
|
+
let right;
|
|
2644
|
+
$.MANY({
|
|
2645
|
+
GATE: () => {
|
|
2646
|
+
const next = $.LA(1).tokenType;
|
|
2647
|
+
return (ctx.allowComma && next === T.Comma) || next === T.Or;
|
|
2648
|
+
},
|
|
2649
|
+
DEF: () => {
|
|
2650
|
+
/**
|
|
2651
|
+
* Nest expressions within expressions for correct
|
|
2652
|
+
* order of operations.
|
|
2653
|
+
*/
|
|
2654
|
+
$.OR3([
|
|
2655
|
+
{ ALT: () => $.CONSUME(T.Comma) },
|
|
2656
|
+
{ ALT: () => $.CONSUME(T.Or) }
|
|
2657
|
+
]);
|
|
2658
|
+
right = $.SUBRULE2($.guardAnd, { ARGS: [ctx] });
|
|
2659
|
+
if (!RECORDING_PHASE) {
|
|
2660
|
+
let location = $.endRule();
|
|
2661
|
+
$.startRule();
|
|
2662
|
+
left = new Condition([$.wrap(left, true), 'or', $.wrap(right)], undefined, location, this.context);
|
|
2663
|
+
}
|
|
2664
|
+
}
|
|
2665
|
+
});
|
|
2666
|
+
if (!RECORDING_PHASE) {
|
|
2667
|
+
$.endRule();
|
|
2668
|
+
}
|
|
2669
|
+
return left;
|
|
2670
|
+
};
|
|
2671
|
+
}
|
|
2672
|
+
export function guardDefault(T) {
|
|
2673
|
+
const $ = this;
|
|
2674
|
+
let guardAlt = [
|
|
2675
|
+
{ ALT: () => $.CONSUME(T.DefaultGuardIdent) },
|
|
2676
|
+
{ ALT: () => $.CONSUME(T.DefaultGuardFunc) }
|
|
2677
|
+
];
|
|
2678
|
+
return (ctx = {}) => {
|
|
2679
|
+
let guard = $.OR(guardAlt);
|
|
2680
|
+
ctx.hasDefault = true;
|
|
2681
|
+
if (!$.RECORDING_PHASE) {
|
|
2682
|
+
return new DefaultGuard(guard.image, undefined, $.getLocationInfo(guard), this.context);
|
|
2683
|
+
}
|
|
2684
|
+
};
|
|
2685
|
+
}
|
|
2686
|
+
/**
|
|
2687
|
+
* 'and' and 'or' expressions
|
|
2688
|
+
*
|
|
2689
|
+
* In Media queries level 4, you cannot have
|
|
2690
|
+
* `([expr]) or ([expr]) and ([expr])` because
|
|
2691
|
+
* of evaluation order ambiguity.
|
|
2692
|
+
* However, Less allows it.
|
|
2693
|
+
*/
|
|
2694
|
+
export function guardAnd(T) {
|
|
2695
|
+
const $ = this;
|
|
2696
|
+
return (ctx = {}) => {
|
|
2697
|
+
let left;
|
|
2698
|
+
let RECORDING_PHASE = $.RECORDING_PHASE;
|
|
2699
|
+
$.MANY_SEP({
|
|
2700
|
+
SEP: T.And,
|
|
2701
|
+
DEF: () => {
|
|
2702
|
+
let not;
|
|
2703
|
+
$.OPTION(() => not = $.CONSUME(T.Not));
|
|
2704
|
+
let allowComma = ctx.allowComma;
|
|
2705
|
+
ctx.allowComma = false;
|
|
2706
|
+
let right = $.OR([
|
|
2707
|
+
{ ALT: () => $.SUBRULE($.guardInParens, { ARGS: [ctx] }) },
|
|
2708
|
+
{
|
|
2709
|
+
GATE: () => {
|
|
2710
|
+
const tokenType = $.LA(1).tokenType;
|
|
2711
|
+
return tokenType !== T.Not
|
|
2712
|
+
&& tokenType !== T.DefaultGuardFunc
|
|
2713
|
+
&& tokenType !== T.DefaultGuardIdent;
|
|
2714
|
+
},
|
|
2715
|
+
ALT: () => $.SUBRULE($.value, { ARGS: [ctx] })
|
|
2716
|
+
}
|
|
2717
|
+
]);
|
|
2718
|
+
if (!RECORDING_PHASE && isDefaultGuardCall(right)) {
|
|
2719
|
+
ctx.hasDefault = true;
|
|
2720
|
+
const location = Array.isArray(right.location) && right.location.length === 6
|
|
2721
|
+
? right.location
|
|
2722
|
+
: undefined;
|
|
2723
|
+
right = new DefaultGuard('default()', undefined, location, this.context);
|
|
2724
|
+
}
|
|
2725
|
+
ctx.allowComma = allowComma;
|
|
2726
|
+
if (!RECORDING_PHASE && not) {
|
|
2727
|
+
let [, , , endOffset, endLine, endColumn] = right.location;
|
|
2728
|
+
let [startOffset, startLine, startColumn] = $.getLocationInfo(not);
|
|
2729
|
+
right = new Condition([$.wrap(right, true)], { negate: true }, [startOffset, startLine, startColumn, endOffset, endLine, endColumn], this.context);
|
|
2730
|
+
}
|
|
2731
|
+
if (!left) {
|
|
2732
|
+
left = right;
|
|
2733
|
+
return;
|
|
2734
|
+
}
|
|
2735
|
+
if (!RECORDING_PHASE) {
|
|
2736
|
+
left = new Condition([$.wrap(left, true), 'and', $.wrap(right)], undefined, $.getLocationFromNodes([left, right]), this.context);
|
|
2737
|
+
}
|
|
2738
|
+
}
|
|
2739
|
+
});
|
|
2740
|
+
return left;
|
|
2741
|
+
};
|
|
2742
|
+
}
|
|
2743
|
+
export function guardInParens(T) {
|
|
2744
|
+
const $ = this;
|
|
2745
|
+
return (ctx) => {
|
|
2746
|
+
$.startRule();
|
|
2747
|
+
let node = $.OR([
|
|
2748
|
+
{ ALT: () => $.SUBRULE($.guardDefault, { ARGS: [ctx] }) },
|
|
2749
|
+
{
|
|
2750
|
+
ALT: () => {
|
|
2751
|
+
$.CONSUME(T.LParen);
|
|
2752
|
+
let node = $.SUBRULE($.guardInner, { ARGS: [ctx] });
|
|
2753
|
+
$.CONSUME(T.RParen);
|
|
2754
|
+
return node;
|
|
2755
|
+
}
|
|
2756
|
+
}
|
|
2757
|
+
]);
|
|
2758
|
+
if (!$.RECORDING_PHASE) {
|
|
2759
|
+
if (isDefaultGuardCall(node)) {
|
|
2760
|
+
ctx.hasDefault = true;
|
|
2761
|
+
const location = Array.isArray(node.location) && node.location.length === 6
|
|
2762
|
+
? node.location
|
|
2763
|
+
: undefined;
|
|
2764
|
+
node = new DefaultGuard('default()', undefined, location, this.context);
|
|
2765
|
+
}
|
|
2766
|
+
node = $.wrap(node, 'both');
|
|
2767
|
+
return new Paren(node, undefined, $.endRule(), this.context);
|
|
2768
|
+
}
|
|
2769
|
+
};
|
|
2770
|
+
}
|
|
2771
|
+
// The inner content of a guard inside parentheses
|
|
2772
|
+
export function guardInner(_T) {
|
|
2773
|
+
const $ = this;
|
|
2774
|
+
return (ctx = {}) => $.OR([
|
|
2775
|
+
{ ALT: () => $.SUBRULE($.comparison, { ARGS: [ctx] }) },
|
|
2776
|
+
{
|
|
2777
|
+
ALT: () => $.SUBRULE($.guardOr, { ARGS: [ctx] })
|
|
2778
|
+
}
|
|
2779
|
+
]);
|
|
2780
|
+
}
|
|
2781
|
+
export function guardWithConditionValue(T) {
|
|
2782
|
+
const $ = this;
|
|
2783
|
+
return (ctx = {}) => $.OR([
|
|
2784
|
+
{
|
|
2785
|
+
ALT: () => {
|
|
2786
|
+
$.OR2([
|
|
2787
|
+
{ ALT: () => $.CONSUME(T.DefaultGuardIdent) },
|
|
2788
|
+
{ ALT: () => $.CONSUME(T.DefaultGuardFunc) }
|
|
2789
|
+
]);
|
|
2790
|
+
}
|
|
2791
|
+
},
|
|
2792
|
+
{ ALT: () => $.SUBRULE($.guardInParens, { ARGS: [ctx] }) }
|
|
2793
|
+
]);
|
|
2794
|
+
}
|
|
2795
|
+
export function guardWithCondition(T) {
|
|
2796
|
+
const $ = this;
|
|
2797
|
+
return (ctx = {}) => {
|
|
2798
|
+
$.SUBRULE($.guardWithConditionValue, { ARGS: [ctx] });
|
|
2799
|
+
$.AT_LEAST_ONE(() => {
|
|
2800
|
+
$.OR([
|
|
2801
|
+
{ ALT: () => $.CONSUME(T.Or) },
|
|
2802
|
+
{ ALT: () => $.CONSUME(T.And) },
|
|
2803
|
+
{ ALT: () => $.CONSUME(T.Comma) }
|
|
2804
|
+
]);
|
|
2805
|
+
$.SUBRULE2($.guardWithConditionValue, { ARGS: [ctx] });
|
|
2806
|
+
});
|
|
2807
|
+
};
|
|
2808
|
+
}
|
|
2809
|
+
/**
|
|
2810
|
+
* Currently, Less only allows a single comparison expression,
|
|
2811
|
+
* unlike Media Queries Level 4, which allows a left and right
|
|
2812
|
+
* comparison.
|
|
2813
|
+
*/
|
|
2814
|
+
export function comparison(T) {
|
|
2815
|
+
const $ = this;
|
|
2816
|
+
let opAlt = [
|
|
2817
|
+
{ ALT: () => $.CONSUME(T.Eq) },
|
|
2818
|
+
{ ALT: () => $.CONSUME(T.Gt) },
|
|
2819
|
+
{ ALT: () => $.CONSUME(T.GtEq) },
|
|
2820
|
+
{ ALT: () => $.CONSUME(T.GtEqAlias) },
|
|
2821
|
+
{ ALT: () => $.CONSUME(T.Lt) },
|
|
2822
|
+
{ ALT: () => $.CONSUME(T.LtEq) },
|
|
2823
|
+
{ ALT: () => $.CONSUME(T.LtEqAlias) }
|
|
2824
|
+
];
|
|
2825
|
+
return (ctx = {}) => {
|
|
2826
|
+
let left = $.SUBRULE($.valueList, { ARGS: [ctx] });
|
|
2827
|
+
// $.OPTION(() => {
|
|
2828
|
+
let op = $.OR(opAlt);
|
|
2829
|
+
let right = $.SUBRULE2($.valueList, { ARGS: [ctx] });
|
|
2830
|
+
if (!$.RECORDING_PHASE) {
|
|
2831
|
+
let opStr = op.image;
|
|
2832
|
+
if (opStr === '=>') {
|
|
2833
|
+
opStr = '>=';
|
|
2834
|
+
}
|
|
2835
|
+
else if (opStr === '=<') {
|
|
2836
|
+
opStr = '<=';
|
|
2837
|
+
}
|
|
2838
|
+
left = new Condition([$.wrap(left, true), opStr, $.wrap(right)], undefined, $.getLocationFromNodes([left, right]), this.context);
|
|
2839
|
+
}
|
|
2840
|
+
// })
|
|
2841
|
+
return left;
|
|
2842
|
+
};
|
|
2843
|
+
}
|
|
2844
|
+
/**
|
|
2845
|
+
* Less (perhaps unwisely) allows bubling of normally document-root
|
|
2846
|
+
* at-rules, so we need to override CSS here.
|
|
2847
|
+
*/
|
|
2848
|
+
export function innerAtRule(T) {
|
|
2849
|
+
const $ = this;
|
|
2850
|
+
let ruleAlt = (ctx = {}) => [
|
|
2851
|
+
{ ALT: () => $.SUBRULE($.mediaAtRule, { ARGS: [{ ...ctx, inner: true }] }) },
|
|
2852
|
+
{ ALT: () => $.SUBRULE($.supportsAtRule, { ARGS: [{ ...ctx, inner: true }] }) },
|
|
2853
|
+
{ ALT: () => $.SUBRULE($.layerAtRule, { ARGS: [{ ...ctx, inner: true }] }) },
|
|
2854
|
+
{ ALT: () => $.SUBRULE($.containerAtRule, { ARGS: [{ ...ctx, inner: true }] }) },
|
|
2855
|
+
{ ALT: () => $.SUBRULE($.keyframesAtRule, { ARGS: [{ ...ctx, inner: true }] }) },
|
|
2856
|
+
{ ALT: () => $.SUBRULE($.documentAtRule, { ARGS: [{ ...ctx, inner: true }] }) },
|
|
2857
|
+
{ ALT: () => $.SUBRULE($.importAtRule, { ARGS: [ctx] }) },
|
|
2858
|
+
{ ALT: () => $.SUBRULE($.pageAtRule, { ARGS: [ctx] }) },
|
|
2859
|
+
{ ALT: () => $.SUBRULE($.fontFaceAtRule, { ARGS: [ctx] }) },
|
|
2860
|
+
{ ALT: () => $.SUBRULE($.nestedAtRule, { ARGS: [ctx] }) },
|
|
2861
|
+
{ ALT: () => $.SUBRULE($.nonNestedAtRule, { ARGS: [ctx] }) },
|
|
2862
|
+
{ ALT: () => $.SUBRULE($.unknownAtRule, { ARGS: [{ ...ctx, inner: true }] }) }
|
|
2863
|
+
];
|
|
2864
|
+
return cssInnerAtRule.call(this, T, ruleAlt);
|
|
2865
|
+
}
|
|
2866
|
+
/**
|
|
2867
|
+
* Less override: allow variable reference as the first segment of a layer-name
|
|
2868
|
+
* CSS: <ident> ('.' <ident>)*
|
|
2869
|
+
* Less: (<var-ref> | <ident>) ('.' <ident>)*
|
|
2870
|
+
*/
|
|
2871
|
+
export function layerName(T) {
|
|
2872
|
+
const $ = this;
|
|
2873
|
+
return (ctx = {}) => {
|
|
2874
|
+
const RECORDING_PHASE = $.RECORDING_PHASE;
|
|
2875
|
+
$.startRule();
|
|
2876
|
+
const nodes = RECORDING_PHASE ? [] : [];
|
|
2877
|
+
// First segment: variable reference or plain ident
|
|
2878
|
+
const first = $.OR([
|
|
2879
|
+
{ ALT: () => $.SUBRULE($.valueReference, { ARGS: [ctx] }) },
|
|
2880
|
+
{ ALT: () => $.CONSUME(T.Ident) }
|
|
2881
|
+
]);
|
|
2882
|
+
if (!RECORDING_PHASE) {
|
|
2883
|
+
if (first instanceof Node) {
|
|
2884
|
+
nodes.push($.wrap(first));
|
|
2885
|
+
}
|
|
2886
|
+
else {
|
|
2887
|
+
nodes.push($.wrap($.processValueToken(first)));
|
|
2888
|
+
}
|
|
2889
|
+
}
|
|
2890
|
+
// Remaining segments: dot + ident (same as CSS)
|
|
2891
|
+
$.MANY({
|
|
2892
|
+
GATE: $.noSep,
|
|
2893
|
+
DEF: () => {
|
|
2894
|
+
const seg = $.CONSUME(T.DotName);
|
|
2895
|
+
if (!RECORDING_PHASE) {
|
|
2896
|
+
nodes.push($.wrap($.processValueToken(seg)));
|
|
2897
|
+
}
|
|
2898
|
+
}
|
|
2899
|
+
});
|
|
2900
|
+
if (!RECORDING_PHASE) {
|
|
2901
|
+
const loc = $.endRule();
|
|
2902
|
+
return new Sequence(nodes, undefined, loc, this.context);
|
|
2903
|
+
}
|
|
2904
|
+
};
|
|
2905
|
+
}
|
|
2906
|
+
/**
|
|
2907
|
+
* Less override: allow variable reference for @keyframes name
|
|
2908
|
+
* CSS: Ident | String
|
|
2909
|
+
* Less: valueReference | Ident | String
|
|
2910
|
+
*/
|
|
2911
|
+
// Less override: allow variable reference in keyframes name by overriding keyframesName only
|
|
2912
|
+
export function keyframesName(T) {
|
|
2913
|
+
const $ = this;
|
|
2914
|
+
return (ctx = {}) => {
|
|
2915
|
+
const RECORDING_PHASE = $.RECORDING_PHASE;
|
|
2916
|
+
let node;
|
|
2917
|
+
$.OR({
|
|
2918
|
+
DEF: [
|
|
2919
|
+
{ ALT: () => node = $.SUBRULE($.valueReference, { ARGS: [ctx] }) },
|
|
2920
|
+
{ ALT: () => {
|
|
2921
|
+
const tok = $.CONSUME(T.Ident);
|
|
2922
|
+
if (!RECORDING_PHASE) {
|
|
2923
|
+
node = $.wrap($.processValueToken(tok));
|
|
2924
|
+
}
|
|
2925
|
+
} },
|
|
2926
|
+
{ ALT: () => node = $.SUBRULE($.string) }
|
|
2927
|
+
]
|
|
2928
|
+
});
|
|
2929
|
+
return node;
|
|
2930
|
+
};
|
|
2931
|
+
}
|
|
2932
|
+
/**
|
|
2933
|
+
* One of the rare rules that returns a token, because
|
|
2934
|
+
* other rules will transform it differently.
|
|
2935
|
+
*/
|
|
2936
|
+
export function mixinName(T) {
|
|
2937
|
+
const $ = this;
|
|
2938
|
+
let nameAlt = [
|
|
2939
|
+
{ ALT: () => $.CONSUME(T.HashName) },
|
|
2940
|
+
{ ALT: () => $.CONSUME(T.ColorIdentStart) },
|
|
2941
|
+
{ ALT: () => $.CONSUME(T.DotName) },
|
|
2942
|
+
{ ALT: () => $.CONSUME(T.InterpolatedIdent) },
|
|
2943
|
+
{ ALT: () => $.CONSUME(T.InterpolatedSelector) }
|
|
2944
|
+
];
|
|
2945
|
+
/** e.g. .mixin, #mixin */
|
|
2946
|
+
return (ctx = {}) => {
|
|
2947
|
+
let name = $.OR(nameAlt);
|
|
2948
|
+
if (!$.RECORDING_PHASE) {
|
|
2949
|
+
const asReference = ctx.asReference;
|
|
2950
|
+
let nameNode;
|
|
2951
|
+
let nameValue = name.image;
|
|
2952
|
+
let location = $.getLocationInfo(name);
|
|
2953
|
+
if (nameValue.includes('@') || nameValue.includes('$')) {
|
|
2954
|
+
nameNode = getInterpolated(nameValue, location, this.context);
|
|
2955
|
+
if (asReference) {
|
|
2956
|
+
// For interpolated keys, we can't merge into array easily, so keep nested structure
|
|
2957
|
+
// But we still check type to ensure consistency
|
|
2958
|
+
if (isNode(ctx.node, 'Reference') && ctx.node.options.type === 'mixin-ruleset') {
|
|
2959
|
+
// Keep nested structure for interpolated keys
|
|
2960
|
+
nameNode = new Reference({ target: ctx.node, key: nameNode }, { type: 'mixin-ruleset', role: 'name' }, location, this.context);
|
|
2961
|
+
}
|
|
2962
|
+
else {
|
|
2963
|
+
nameNode = new Reference({ target: ctx.node, key: nameNode }, { type: 'mixin-ruleset', role: 'name' }, location, this.context);
|
|
2964
|
+
}
|
|
2965
|
+
}
|
|
2966
|
+
}
|
|
2967
|
+
else {
|
|
2968
|
+
if (asReference) {
|
|
2969
|
+
// If target is a Reference with matching type, merge keys instead of nesting
|
|
2970
|
+
if (isNode(ctx.node, 'Reference') && ctx.node.options.type === 'mixin-ruleset') {
|
|
2971
|
+
const existingKey = ctx.node.value.key;
|
|
2972
|
+
let mergedKeys;
|
|
2973
|
+
if (Array.isArray(existingKey)) {
|
|
2974
|
+
mergedKeys = [...existingKey];
|
|
2975
|
+
}
|
|
2976
|
+
else {
|
|
2977
|
+
mergedKeys = [existingKey];
|
|
2978
|
+
}
|
|
2979
|
+
mergedKeys.push(nameValue);
|
|
2980
|
+
// Create a single Reference with merged keys (no target)
|
|
2981
|
+
nameNode = new Reference({ key: mergedKeys.length === 1 ? mergedKeys[0] : mergedKeys }, { type: 'mixin-ruleset', role: 'name' }, location, this.context);
|
|
2982
|
+
}
|
|
2983
|
+
else {
|
|
2984
|
+
// Target is Call, Reference with different type, or undefined - create Reference with target
|
|
2985
|
+
nameNode = new Reference({ target: ctx.node, key: nameValue }, { type: 'mixin-ruleset', role: 'name' }, location, this.context);
|
|
2986
|
+
}
|
|
2987
|
+
}
|
|
2988
|
+
else {
|
|
2989
|
+
nameNode = $.wrap(new Any(nameValue, { role: 'name' }, $.getLocationInfo(name), this.context), true);
|
|
2990
|
+
}
|
|
2991
|
+
}
|
|
2992
|
+
return nameNode;
|
|
2993
|
+
}
|
|
2994
|
+
};
|
|
2995
|
+
}
|
|
2996
|
+
/**
|
|
2997
|
+
* Used within a value. These can be
|
|
2998
|
+
* chained more recursively, unlike
|
|
2999
|
+
* Less 1.x-4.x
|
|
3000
|
+
* e.g. .mixin1() > .mixin2[@val1].ns() > .sub-mixin[@val2]
|
|
3001
|
+
*
|
|
3002
|
+
* This production intelligently decides whether to produce a Call or Reference
|
|
3003
|
+
* based on whether there are parentheses at the end:
|
|
3004
|
+
* - foo: #id; // Reference
|
|
3005
|
+
* - foo: .class; // Reference
|
|
3006
|
+
* - foo: #id > .scoped; // Reference
|
|
3007
|
+
* - foo: #id > .scoped(); // Call
|
|
3008
|
+
* - foo: #id[]; // Reference with accessor
|
|
3009
|
+
* - foo: #id > .scoped[foo]; // Reference with accessor
|
|
3010
|
+
* - foo: #id > .scoped[@ref](); // Call with accessor
|
|
3011
|
+
*/
|
|
3012
|
+
export function mixinReference(T) {
|
|
3013
|
+
const $ = this;
|
|
3014
|
+
return (ctx = {}) => {
|
|
3015
|
+
let leftNode = $.SUBRULE($.mixinName, { ARGS: [{ ...ctx, asReference: true }] });
|
|
3016
|
+
$.MANY({
|
|
3017
|
+
GATE: () => {
|
|
3018
|
+
let next = $.LA(1).tokenType;
|
|
3019
|
+
return $.noSep() && (next === T.LParen || next === T.LSquare);
|
|
3020
|
+
},
|
|
3021
|
+
DEF: () => {
|
|
3022
|
+
leftNode = $.SUBRULE($.lookupOrCall, { ARGS: [{ ...ctx, node: leftNode }] });
|
|
3023
|
+
}
|
|
3024
|
+
});
|
|
3025
|
+
$.OPTION(() => {
|
|
3026
|
+
$.OPTION2(() => $.CONSUME(T.Gt));
|
|
3027
|
+
leftNode = $.SUBRULE2($.mixinReference, { ARGS: [{ ...ctx, node: leftNode }] });
|
|
3028
|
+
});
|
|
3029
|
+
return leftNode;
|
|
3030
|
+
};
|
|
3031
|
+
}
|
|
3032
|
+
export function mixinArgs(T) {
|
|
3033
|
+
const $ = this;
|
|
3034
|
+
return (ctx = {}) => {
|
|
3035
|
+
let args;
|
|
3036
|
+
// Check for whitespace before the opening paren (before consuming)
|
|
3037
|
+
const hasWhitespace = !$.RECORDING_PHASE && !$.noSep();
|
|
3038
|
+
const openingParenToken = hasWhitespace ? $.LA(1) : undefined;
|
|
3039
|
+
$.CONSUME(T.LParen);
|
|
3040
|
+
// Clear ctx.node when parsing arguments - arguments should start fresh, not inherit the parent node
|
|
3041
|
+
// Calls intentionally push a `false` paren frame (matches `Call.evalNode`)
|
|
3042
|
+
const argCtx = {
|
|
3043
|
+
...ctx,
|
|
3044
|
+
node: undefined,
|
|
3045
|
+
allowComma: false,
|
|
3046
|
+
parenFrames: [...getParenFrames(ctx), false],
|
|
3047
|
+
detachedRulesetUsage: ctx.isDefinition ? 'default-param' : 'mixin-arg'
|
|
3048
|
+
};
|
|
3049
|
+
$.OPTION(() => {
|
|
3050
|
+
args = $.SUBRULE($.mixinArgList, { ARGS: [argCtx] });
|
|
3051
|
+
});
|
|
3052
|
+
$.CONSUME(T.RParen);
|
|
3053
|
+
// Check for whitespace warning AFTER consuming closing paren
|
|
3054
|
+
// Now we can check what comes next to determine if it's actually a definition
|
|
3055
|
+
if (!$.RECORDING_PHASE && hasWhitespace && openingParenToken) {
|
|
3056
|
+
const nextAfterParens = $.LA(1).tokenType;
|
|
3057
|
+
const isActuallyDefinition = nextAfterParens === T.LCurly || nextAfterParens === T.When;
|
|
3058
|
+
// Only warn if it's NOT a definition (i.e., it's a mixin call)
|
|
3059
|
+
if (!isActuallyDefinition) {
|
|
3060
|
+
$.warnDeprecation('Whitespace between a mixin name and parentheses for a mixin call is deprecated', openingParenToken, 'mixin-call-whitespace');
|
|
3061
|
+
}
|
|
3062
|
+
}
|
|
3063
|
+
return args;
|
|
3064
|
+
};
|
|
3065
|
+
}
|
|
3066
|
+
export function lookupOrCall(T) {
|
|
3067
|
+
const $ = this;
|
|
3068
|
+
let keyAlt = [
|
|
3069
|
+
{ ALT: () => $.CONSUME(T.NestedReference) },
|
|
3070
|
+
{ ALT: () => $.CONSUME(T.AtKeyword) },
|
|
3071
|
+
{ ALT: () => $.CONSUME(T.PropertyReference) },
|
|
3072
|
+
{ ALT: () => $.CONSUME(T.InterpolatedIdent) },
|
|
3073
|
+
{ ALT: () => $.CONSUME(T.Ident) }
|
|
3074
|
+
];
|
|
3075
|
+
return (ctx = {}) => {
|
|
3076
|
+
$.startRule();
|
|
3077
|
+
let RECORDING_PHASE = $.RECORDING_PHASE;
|
|
3078
|
+
return $.OR([
|
|
3079
|
+
{
|
|
3080
|
+
ALT: () => {
|
|
3081
|
+
let keyToken;
|
|
3082
|
+
$.CONSUME(T.LSquare);
|
|
3083
|
+
$.OPTION(() => keyToken = $.OR2(keyAlt));
|
|
3084
|
+
$.CONSUME(T.RSquare);
|
|
3085
|
+
if (!RECORDING_PHASE) {
|
|
3086
|
+
let ref;
|
|
3087
|
+
let target = ctx.node;
|
|
3088
|
+
if (keyToken) {
|
|
3089
|
+
let tokenStr = keyToken.image;
|
|
3090
|
+
let type = tokenStr.startsWith('@') ? 'variable' : 'property';
|
|
3091
|
+
// Handle all token types consistently
|
|
3092
|
+
if (keyToken.tokenType === T.NestedReference) {
|
|
3093
|
+
// For NestedReference, add $ prefix if not present
|
|
3094
|
+
let tokenStr = keyToken.image;
|
|
3095
|
+
if (!tokenStr.startsWith('$') && !tokenStr.startsWith('@')) {
|
|
3096
|
+
tokenStr = '$' + tokenStr;
|
|
3097
|
+
}
|
|
3098
|
+
}
|
|
3099
|
+
let result = getInterpolatedOrString(tokenStr, $.getLocationInfo(keyToken), this.context);
|
|
3100
|
+
// Only merge keys for mixin, mixin-ruleset, or ruleset types
|
|
3101
|
+
// For variable and property types, keep them nested (target.key structure)
|
|
3102
|
+
const targetType = isNode(target, 'Reference') ? target.options.type : undefined;
|
|
3103
|
+
const shouldMergeKeys = targetType === 'mixin' || targetType === 'mixin-ruleset' || targetType === 'ruleset';
|
|
3104
|
+
if (isNode(target, 'Reference') && target.options.type === type && typeof result === 'string' && shouldMergeKeys) {
|
|
3105
|
+
const existingKey = target.value.key;
|
|
3106
|
+
let mergedKeys;
|
|
3107
|
+
if (Array.isArray(existingKey)) {
|
|
3108
|
+
mergedKeys = [...existingKey];
|
|
3109
|
+
}
|
|
3110
|
+
else {
|
|
3111
|
+
mergedKeys = [existingKey];
|
|
3112
|
+
}
|
|
3113
|
+
mergedKeys.push(result);
|
|
3114
|
+
ref = new Reference({ key: mergedKeys.length === 1 ? mergedKeys[0] : mergedKeys }, { type }, $.endRule(), this.context);
|
|
3115
|
+
}
|
|
3116
|
+
else {
|
|
3117
|
+
ref = new Reference({ target, key: result }, { type }, $.endRule(), this.context);
|
|
3118
|
+
}
|
|
3119
|
+
}
|
|
3120
|
+
else {
|
|
3121
|
+
ref = new Reference({ target, key: -1 }, { type: 'index' }, $.endRule(), this.context);
|
|
3122
|
+
}
|
|
3123
|
+
/** Reference targets will technically precede the reference, so we need to update the location to the target start location */
|
|
3124
|
+
if (target) {
|
|
3125
|
+
let [targetStartOffset, targetStartLine, targetStartColumn] = target.location;
|
|
3126
|
+
ref.location[0] = targetStartOffset;
|
|
3127
|
+
ref.location[1] = targetStartLine;
|
|
3128
|
+
ref.location[2] = targetStartColumn;
|
|
3129
|
+
}
|
|
3130
|
+
return ref;
|
|
3131
|
+
}
|
|
3132
|
+
}
|
|
3133
|
+
},
|
|
3134
|
+
{
|
|
3135
|
+
ALT: () => {
|
|
3136
|
+
let args = $.SUBRULE($.mixinArgs, { ARGS: [ctx] });
|
|
3137
|
+
if (!RECORDING_PHASE) {
|
|
3138
|
+
return new Call({ name: ctx.node, args }, undefined, $.endRule(), this.context);
|
|
3139
|
+
}
|
|
3140
|
+
}
|
|
3141
|
+
}
|
|
3142
|
+
]);
|
|
3143
|
+
};
|
|
3144
|
+
}
|
|
3145
|
+
// export function accessors(this: P, T: TokenMap) {
|
|
3146
|
+
// const $ = this;
|
|
3147
|
+
// let keyAlt = [
|
|
3148
|
+
// { ALT: () => $.CONSUME(T.NestedReference) },
|
|
3149
|
+
// { ALT: () => $.CONSUME(T.AtKeyword) },
|
|
3150
|
+
// { ALT: () => $.CONSUME(T.PropertyReference) },
|
|
3151
|
+
// { ALT: () => $.CONSUME(T.InterpolatedIdent) },
|
|
3152
|
+
// { ALT: () => $.CONSUME(T.Ident) }
|
|
3153
|
+
// ];
|
|
3154
|
+
// /** The node passed in is what we're looking up on */
|
|
3155
|
+
// return (ctx: RuleContext = {}) => {
|
|
3156
|
+
// let nodeContext = ctx.node! as Reference;
|
|
3157
|
+
// let RECORDING_PHASE = $.RECORDING_PHASE;
|
|
3158
|
+
// $.startRule();
|
|
3159
|
+
// let keyToken: IToken | undefined;
|
|
3160
|
+
// let key: string | number | Reference | Interpolated;
|
|
3161
|
+
// let returnNode: Node;
|
|
3162
|
+
// $.CONSUME(T.LSquare);
|
|
3163
|
+
// $.OPTION(() => keyToken = $.OR(keyAlt));
|
|
3164
|
+
// $.CONSUME(T.RSquare);
|
|
3165
|
+
// if (!RECORDING_PHASE) {
|
|
3166
|
+
// const location = $.endRule();
|
|
3167
|
+
// if (keyToken) {
|
|
3168
|
+
// let tokenStr = keyToken.image;
|
|
3169
|
+
// let type: 'variable' | 'property' = tokenStr.startsWith('@') ? 'variable' : 'property';
|
|
3170
|
+
// // Handle all token types consistently
|
|
3171
|
+
// if (keyToken.tokenType === T.NestedReference) {
|
|
3172
|
+
// // For NestedReference, add $ prefix if not present
|
|
3173
|
+
// let tokenStr = keyToken.image;
|
|
3174
|
+
// if (!tokenStr.startsWith('$') && !tokenStr.startsWith('@')) {
|
|
3175
|
+
// tokenStr = '$' + tokenStr;
|
|
3176
|
+
// }
|
|
3177
|
+
// }
|
|
3178
|
+
// let result = getInterpolatedOrString(tokenStr, $.getLocationInfo(keyToken), this.context);
|
|
3179
|
+
// returnNode = new Reference({ target: nodeContext, key: result }, { type }, location, this.context);
|
|
3180
|
+
// } else {
|
|
3181
|
+
// key = -1;
|
|
3182
|
+
// returnNode = new Reference({ target: nodeContext, key }, { type: 'index' }, location, this.context);
|
|
3183
|
+
// }
|
|
3184
|
+
// }
|
|
3185
|
+
// /**
|
|
3186
|
+
// * Allows chaining of lookups / calls
|
|
3187
|
+
// * @note - In Less, an additional call or accessor implies
|
|
3188
|
+
// * that the previous accessor is a mixin call, therefore
|
|
3189
|
+
// * it should be returned as a Call node.
|
|
3190
|
+
// */
|
|
3191
|
+
// $.OPTION2(() => {
|
|
3192
|
+
// $.OR2([
|
|
3193
|
+
// {
|
|
3194
|
+
// ALT: () => {
|
|
3195
|
+
// let args = $.SUBRULE($.mixinArgs, { ARGS: [ctx] });
|
|
3196
|
+
// if (!RECORDING_PHASE) {
|
|
3197
|
+
// let [startOffset, startLine, startColumn] = returnNode.location;
|
|
3198
|
+
// let { endOffset, endLine, endColumn } = $.LA(0);
|
|
3199
|
+
// returnNode = new Call({ name: returnNode, args }, undefined, [startOffset!, startLine!, startColumn!, endOffset!, endLine!, endColumn!], this.context);
|
|
3200
|
+
// }
|
|
3201
|
+
// }
|
|
3202
|
+
// },
|
|
3203
|
+
// {
|
|
3204
|
+
// ALT: () => {
|
|
3205
|
+
// returnNode = $.SUBRULE($.inlineMixinCall, { ARGS: [{ ...ctx, node: returnNode }] });
|
|
3206
|
+
// return returnNode;
|
|
3207
|
+
// }
|
|
3208
|
+
// },
|
|
3209
|
+
// {
|
|
3210
|
+
// ALT: () => {
|
|
3211
|
+
// returnNode = $.SUBRULE($.accessors, { ARGS: [{ ...ctx, node: returnNode }] });
|
|
3212
|
+
// return returnNode;
|
|
3213
|
+
// }
|
|
3214
|
+
// }
|
|
3215
|
+
// ]);
|
|
3216
|
+
// });
|
|
3217
|
+
// return returnNode!;
|
|
3218
|
+
// };
|
|
3219
|
+
// }
|
|
3220
|
+
/**
|
|
3221
|
+
* @see https://lesscss.org/features/#mixins-feature-mixins-parametric-feature
|
|
3222
|
+
*
|
|
3223
|
+
* This rule is recursive to allow chevrotain-allstar (hopefully) to lookahead
|
|
3224
|
+
* and find semi-colon separators vs. commas.
|
|
3225
|
+
*/
|
|
3226
|
+
export function mixinArgList(T) {
|
|
3227
|
+
const $ = this;
|
|
3228
|
+
return (ctx = {}) => {
|
|
3229
|
+
let RECORDING_PHASE = $.RECORDING_PHASE;
|
|
3230
|
+
$.startRule();
|
|
3231
|
+
let node = $.SUBRULE($.mixinArg, { ARGS: [ctx] });
|
|
3232
|
+
let commaNodes;
|
|
3233
|
+
let semiNodes;
|
|
3234
|
+
if (!RECORDING_PHASE) {
|
|
3235
|
+
commaNodes = [$.wrap(node, true)];
|
|
3236
|
+
semiNodes = [];
|
|
3237
|
+
}
|
|
3238
|
+
let isSemiList = false;
|
|
3239
|
+
let moreArgs = true;
|
|
3240
|
+
$.MANY({
|
|
3241
|
+
GATE: () => moreArgs,
|
|
3242
|
+
DEF: () => {
|
|
3243
|
+
$.OR([
|
|
3244
|
+
{
|
|
3245
|
+
GATE: () => !isSemiList,
|
|
3246
|
+
ALT: () => {
|
|
3247
|
+
$.CONSUME(T.Comma);
|
|
3248
|
+
let node = $.SUBRULE2($.mixinArg, { ARGS: [ctx] });
|
|
3249
|
+
if (!RECORDING_PHASE) {
|
|
3250
|
+
commaNodes.push($.wrap(node, true));
|
|
3251
|
+
}
|
|
3252
|
+
}
|
|
3253
|
+
},
|
|
3254
|
+
{
|
|
3255
|
+
ALT: () => {
|
|
3256
|
+
isSemiList = true;
|
|
3257
|
+
let semi = $.CONSUME(T.Semi);
|
|
3258
|
+
if (!RECORDING_PHASE) {
|
|
3259
|
+
/**
|
|
3260
|
+
* Aggregate the previous set of comma-nodes
|
|
3261
|
+
*/
|
|
3262
|
+
if (commaNodes) {
|
|
3263
|
+
if (commaNodes.length > 1) {
|
|
3264
|
+
let [first, ...rest] = commaNodes;
|
|
3265
|
+
let hasDeclarations = false;
|
|
3266
|
+
if (first instanceof VarDeclaration) {
|
|
3267
|
+
const nodes = [first.value.value, ...rest];
|
|
3268
|
+
/**
|
|
3269
|
+
* If we still have declarations, we need to push an error.
|
|
3270
|
+
*/
|
|
3271
|
+
hasDeclarations = rest.some(n => n instanceof VarDeclaration);
|
|
3272
|
+
first.value.value = new List(nodes, undefined, $.getLocationFromNodes(nodes), this.context);
|
|
3273
|
+
semiNodes.push(first);
|
|
3274
|
+
}
|
|
3275
|
+
else {
|
|
3276
|
+
hasDeclarations = commaNodes.some(n => n instanceof VarDeclaration);
|
|
3277
|
+
let commaList = new List(commaNodes, undefined, $.getLocationFromNodes(commaNodes), this.context);
|
|
3278
|
+
semiNodes.push(commaList);
|
|
3279
|
+
}
|
|
3280
|
+
if (hasDeclarations) {
|
|
3281
|
+
let indexOfSemi = $.originalInput.indexOf(semi);
|
|
3282
|
+
let previousToken = $.originalInput[indexOfSemi - 1];
|
|
3283
|
+
$._errors.push(new NoViableAltException('Cannot mix ; and , as delimiter types', semi, previousToken));
|
|
3284
|
+
}
|
|
3285
|
+
}
|
|
3286
|
+
else {
|
|
3287
|
+
semiNodes.push(commaNodes[0]);
|
|
3288
|
+
}
|
|
3289
|
+
commaNodes = undefined;
|
|
3290
|
+
}
|
|
3291
|
+
}
|
|
3292
|
+
$.OR2([
|
|
3293
|
+
{
|
|
3294
|
+
GATE: () => $.LA(1).tokenType !== T.RParen,
|
|
3295
|
+
ALT: () => {
|
|
3296
|
+
const prevAllow = ctx.allowComma;
|
|
3297
|
+
ctx.allowComma = true;
|
|
3298
|
+
node = $.SUBRULE3($.mixinArg, { ARGS: [ctx] });
|
|
3299
|
+
ctx.allowComma = prevAllow;
|
|
3300
|
+
if (!RECORDING_PHASE) {
|
|
3301
|
+
semiNodes.push($.wrap(node, true));
|
|
3302
|
+
}
|
|
3303
|
+
}
|
|
3304
|
+
},
|
|
3305
|
+
{
|
|
3306
|
+
ALT: () => {
|
|
3307
|
+
moreArgs = false;
|
|
3308
|
+
EMPTY_ALT();
|
|
3309
|
+
}
|
|
3310
|
+
}
|
|
3311
|
+
]);
|
|
3312
|
+
}
|
|
3313
|
+
}
|
|
3314
|
+
]);
|
|
3315
|
+
}
|
|
3316
|
+
});
|
|
3317
|
+
if (!RECORDING_PHASE) {
|
|
3318
|
+
let location = $.endRule();
|
|
3319
|
+
let nodes = isSemiList ? semiNodes : commaNodes;
|
|
3320
|
+
let sep = isSemiList ? ';' : ',';
|
|
3321
|
+
return $.wrap(new List(nodes, { sep }, location, this.context), 'both');
|
|
3322
|
+
}
|
|
3323
|
+
};
|
|
3324
|
+
}
|
|
3325
|
+
/**
|
|
3326
|
+
* Less is more lenient about at-keywords. See lessTokens.ts for more details.
|
|
3327
|
+
*/
|
|
3328
|
+
export function varName(T) {
|
|
3329
|
+
const $ = this;
|
|
3330
|
+
// AtKeywordLessExtension is categorized as AtName in lessTokens.ts, so consuming
|
|
3331
|
+
// AtName alone preserves behavior while avoiding OR ambiguity warnings.
|
|
3332
|
+
return () => $.CONSUME(T.AtName);
|
|
3333
|
+
}
|
|
3334
|
+
/**
|
|
3335
|
+
* Originally, we were creating alternatives for mixin calls and mixin definitions
|
|
3336
|
+
* that could mostly overlap, which led to longer parsing. Instead, we parse
|
|
3337
|
+
* as if it could be either, and then we disambiguate at the end.
|
|
3338
|
+
*/
|
|
3339
|
+
export function mixinArg(T) {
|
|
3340
|
+
const $ = this;
|
|
3341
|
+
return (ctx = {}) => {
|
|
3342
|
+
let RECORDING_PHASE = $.RECORDING_PHASE;
|
|
3343
|
+
let firstToken = $.LA(1);
|
|
3344
|
+
let atStart = (firstToken.tokenType === T.AtKeyword
|
|
3345
|
+
|| firstToken.tokenType === T.AtKeywordLessExtension);
|
|
3346
|
+
let isDeclaration = atStart && $.LA(2).tokenType === T.Colon;
|
|
3347
|
+
return $.OR([
|
|
3348
|
+
{
|
|
3349
|
+
GATE: () => !isDeclaration && atStart && $.LA(2).tokenType === T.Ellipsis,
|
|
3350
|
+
ALT: () => {
|
|
3351
|
+
$.startRule();
|
|
3352
|
+
let name = $.SUBRULE2($.varName, { ARGS: [ctx] });
|
|
3353
|
+
let ellipsis;
|
|
3354
|
+
/**
|
|
3355
|
+
* Mixin definitions can have a spread parameter, which
|
|
3356
|
+
* means it will match a variable number of elements
|
|
3357
|
+
* at the end.
|
|
3358
|
+
*
|
|
3359
|
+
* However, mixin calls can have a spread argument,
|
|
3360
|
+
* which means it will expand a variable representing
|
|
3361
|
+
* a list, which, to my knowledge, is an undocumented
|
|
3362
|
+
* feature of Less (and only exists in mixin calls?)
|
|
3363
|
+
*
|
|
3364
|
+
* @todo - Intuitively, shouldn't this be available
|
|
3365
|
+
* elsewhere in the language? Or would there be no
|
|
3366
|
+
* reason?
|
|
3367
|
+
*/
|
|
3368
|
+
$.OPTION(() => ellipsis = $.CONSUME(T.Ellipsis));
|
|
3369
|
+
if (!RECORDING_PHASE) {
|
|
3370
|
+
let varName = name.image.slice(1);
|
|
3371
|
+
if (ellipsis) {
|
|
3372
|
+
// For rest parameters, use string which can be converted to Reference later if needed
|
|
3373
|
+
return new Rest(varName, undefined, $.endRule(), this.context);
|
|
3374
|
+
}
|
|
3375
|
+
else {
|
|
3376
|
+
return new Any(varName, { role: 'name' }, $.endRule(), this.context);
|
|
3377
|
+
}
|
|
3378
|
+
}
|
|
3379
|
+
}
|
|
3380
|
+
},
|
|
3381
|
+
{
|
|
3382
|
+
GATE: () => !isDeclaration && !atStart,
|
|
3383
|
+
ALT: () => {
|
|
3384
|
+
return $.SUBRULE($.callArgument, { ARGS: [ctx] });
|
|
3385
|
+
}
|
|
3386
|
+
},
|
|
3387
|
+
{
|
|
3388
|
+
GATE: () => !isDeclaration && atStart && $.LA(2).tokenType !== T.Ellipsis && $.LA(2).tokenType !== T.RParen && $.LA(2).tokenType !== T.Comma && $.LA(2).tokenType !== T.Semi,
|
|
3389
|
+
ALT: () => {
|
|
3390
|
+
return $.SUBRULE3($.callArgument, { ARGS: [ctx] });
|
|
3391
|
+
}
|
|
3392
|
+
},
|
|
3393
|
+
{
|
|
3394
|
+
GATE: () => !isDeclaration && atStart && $.LA(2).tokenType !== T.Ellipsis && ($.LA(2).tokenType === T.RParen || $.LA(2).tokenType === T.Comma || $.LA(2).tokenType === T.Semi),
|
|
3395
|
+
ALT: () => {
|
|
3396
|
+
$.startRule();
|
|
3397
|
+
let name = $.SUBRULE3($.varName, { ARGS: [ctx] });
|
|
3398
|
+
if (!RECORDING_PHASE) {
|
|
3399
|
+
let varName = name.image.slice(1);
|
|
3400
|
+
return new Any(varName, { role: 'name' }, $.endRule(), this.context);
|
|
3401
|
+
}
|
|
3402
|
+
}
|
|
3403
|
+
},
|
|
3404
|
+
{
|
|
3405
|
+
GATE: () => isDeclaration,
|
|
3406
|
+
ALT: () => {
|
|
3407
|
+
$.startRule();
|
|
3408
|
+
let name = $.SUBRULE4($.varName, { ARGS: [ctx] });
|
|
3409
|
+
$.CONSUME(T.Colon);
|
|
3410
|
+
if (!RECORDING_PHASE && typeof name.image === 'string' && name.image.startsWith('@')) {
|
|
3411
|
+
}
|
|
3412
|
+
/** Default value */
|
|
3413
|
+
let value = $.SUBRULE2($.callArgument, { ARGS: [{ ...ctx, allowComma: false, detachedRulesetUsage: 'default-param' }] });
|
|
3414
|
+
if (!RECORDING_PHASE) {
|
|
3415
|
+
let location = $.endRule();
|
|
3416
|
+
return new VarDeclaration({
|
|
3417
|
+
name: new Any(name.image.slice(1), { role: 'property' }, $.getLocationInfo(name), this.context),
|
|
3418
|
+
value
|
|
3419
|
+
}, { paramVar: true }, location, this.context);
|
|
3420
|
+
}
|
|
3421
|
+
}
|
|
3422
|
+
},
|
|
3423
|
+
{
|
|
3424
|
+
ALT: () => {
|
|
3425
|
+
let ellipsis = $.CONSUME2(T.Ellipsis);
|
|
3426
|
+
return new Rest(undefined, undefined, $.getLocationInfo(ellipsis), this.context);
|
|
3427
|
+
}
|
|
3428
|
+
}
|
|
3429
|
+
]);
|
|
3430
|
+
};
|
|
3431
|
+
}
|
|
3432
|
+
export function callArgument(T) {
|
|
3433
|
+
const $ = this;
|
|
3434
|
+
return (ctx = {}) => {
|
|
3435
|
+
return $.OR([
|
|
3436
|
+
{
|
|
3437
|
+
GATE: () => $.LA(1).tokenType === T.AnonMixinStart || $.LA(1).tokenType === T.LCurly,
|
|
3438
|
+
ALT: () => $.SUBRULE($.anonymousMixinDefinition, { ARGS: [ctx] })
|
|
3439
|
+
},
|
|
3440
|
+
{
|
|
3441
|
+
GATE: () => !ctx.allowComma,
|
|
3442
|
+
ALT: () => $.SUBRULE($.valueSequence, { ARGS: [ctx] })
|
|
3443
|
+
},
|
|
3444
|
+
{
|
|
3445
|
+
GATE: () => !!ctx.allowComma,
|
|
3446
|
+
ALT: () => $.SUBRULE($.valueList, { ARGS: [ctx] })
|
|
3447
|
+
}
|
|
3448
|
+
]);
|
|
3449
|
+
};
|
|
3450
|
+
}
|
|
3451
|
+
/**
|
|
3452
|
+
* Override unknownAtRule to handle @-export for stylesheet forwarding.
|
|
3453
|
+
* @-export is like @-compose but with forward semantics and no `with` support.
|
|
3454
|
+
*/
|
|
3455
|
+
export function unknownAtRule(T) {
|
|
3456
|
+
const $ = this;
|
|
3457
|
+
const baseUnknown = cssProductions.unknownAtRule.call(this, T);
|
|
3458
|
+
return (ctx = {}) => {
|
|
3459
|
+
const img = $.LA(1).image;
|
|
3460
|
+
if (img === '@-export') {
|
|
3461
|
+
return $.SUBRULE($.exportAtRule, { ARGS: [ctx] });
|
|
3462
|
+
}
|
|
3463
|
+
return baseUnknown(ctx);
|
|
3464
|
+
};
|
|
3465
|
+
}
|
|
3466
|
+
/**
|
|
3467
|
+
* Parse @-export './foo.jess' [as <namespace>]
|
|
3468
|
+
*
|
|
3469
|
+
* Creates a StyleImport with forward semantics (members not visible locally but transitive).
|
|
3470
|
+
* Does NOT support `with` (unlike @-compose).
|
|
3471
|
+
* Participates in evaldTrees caching like @-compose.
|
|
3472
|
+
*/
|
|
3473
|
+
export function exportAtRule(T) {
|
|
3474
|
+
const $ = this;
|
|
3475
|
+
return (ctx = {}) => {
|
|
3476
|
+
const RECORDING_PHASE = $.RECORDING_PHASE;
|
|
3477
|
+
$.startRule();
|
|
3478
|
+
$.CONSUME(T.AtKeyword); // '@-export'
|
|
3479
|
+
// Parse the path (string or url)
|
|
3480
|
+
const pathNode = $.OR([
|
|
3481
|
+
{ ALT: () => $.SUBRULE($.urlFunction, { ARGS: [ctx] }) },
|
|
3482
|
+
{ ALT: () => $.SUBRULE($.string, { ARGS: [ctx] }) }
|
|
3483
|
+
]);
|
|
3484
|
+
// Optional "as <namespace>"
|
|
3485
|
+
let namespace;
|
|
3486
|
+
$.OPTION({
|
|
3487
|
+
GATE: () => {
|
|
3488
|
+
const la = $.LA(1);
|
|
3489
|
+
return (la.tokenType === T.PlainIdent || la.tokenType === T.Ident) && la.image === 'as';
|
|
3490
|
+
},
|
|
3491
|
+
DEF: () => {
|
|
3492
|
+
// Consume "as"
|
|
3493
|
+
if ($.LA(1).tokenType === T.Ident) {
|
|
3494
|
+
$.CONSUME(T.Ident);
|
|
3495
|
+
}
|
|
3496
|
+
else {
|
|
3497
|
+
$.CONSUME(T.PlainIdent);
|
|
3498
|
+
}
|
|
3499
|
+
// Consume namespace identifier
|
|
3500
|
+
const nsTok = ($.LA(1).tokenType === T.Ident)
|
|
3501
|
+
? $.CONSUME2(T.Ident)
|
|
3502
|
+
: $.CONSUME2(T.PlainIdent);
|
|
3503
|
+
if (!RECORDING_PHASE) {
|
|
3504
|
+
namespace = nsTok.image;
|
|
3505
|
+
}
|
|
3506
|
+
}
|
|
3507
|
+
});
|
|
3508
|
+
$.CONSUME(T.Semi);
|
|
3509
|
+
if (!RECORDING_PHASE) {
|
|
3510
|
+
const loc = $.endRule();
|
|
3511
|
+
return new StyleImport({ path: pathNode }, {
|
|
3512
|
+
type: 'compose',
|
|
3513
|
+
namespace,
|
|
3514
|
+
importOptions: {
|
|
3515
|
+
forward: true
|
|
3516
|
+
}
|
|
3517
|
+
}, loc, $.context);
|
|
3518
|
+
}
|
|
3519
|
+
};
|
|
3520
|
+
}
|
|
3521
|
+
//# sourceMappingURL=productions.js.map
|