@jesscss/less-parser 2.0.0-alpha.6 → 2.0.0-alpha.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +133 -5
- package/lib/builders.d.ts +381 -0
- package/lib/builders.d.ts.map +1 -0
- package/lib/cst.cjs +14 -0
- package/lib/cst.d.ts +6 -0
- package/lib/cst.d.ts.map +1 -0
- package/lib/cst.js +12 -0
- package/lib/functional-parser.cjs +2653 -0
- package/lib/functional-parser.d.ts +18 -0
- package/lib/functional-parser.d.ts.map +1 -0
- package/lib/functional-parser.js +2588 -0
- package/lib/grammar.cjs +30621 -0
- package/lib/grammar.d.ts +2 -0
- package/lib/grammar.d.ts.map +1 -0
- package/lib/grammar.js +30620 -0
- package/lib/index.cjs +17 -4096
- package/lib/index.d.ts +9 -149
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +6 -4091
- package/lib/jess.cjs +3968 -0
- package/lib/jess.d.ts +7 -0
- package/lib/jess.d.ts.map +1 -0
- package/lib/jess.js +3962 -0
- package/lib/lessParser.d.ts +38 -0
- package/lib/lessParser.d.ts.map +1 -0
- package/lib/lessRecursiveParser.d.ts +99 -0
- package/lib/lessRecursiveParser.d.ts.map +1 -0
- package/lib/lessTokens.d.ts +23 -0
- package/lib/lessTokens.d.ts.map +1 -0
- package/lib/productions/guards.d.ts +113 -0
- package/lib/productions/guards.d.ts.map +1 -0
- package/lib/productions/index.d.ts +5 -0
- package/lib/productions/index.d.ts.map +1 -0
- package/lib/productions/root.d.ts +38 -0
- package/lib/productions/root.d.ts.map +1 -0
- package/lib/productions/selectors.d.ts +41 -0
- package/lib/productions/selectors.d.ts.map +1 -0
- package/lib/productions/values.d.ts +35 -0
- package/lib/productions/values.d.ts.map +1 -0
- package/lib/utils.d.ts +9 -0
- package/lib/utils.d.ts.map +1 -0
- package/package.json +41 -10
- package/src/__tests__/debug-log.ts +35 -0
- package/src/__tests__/wall5-parse.test.ts +67 -0
- package/src/builders.ts +3183 -0
- package/src/cst.ts +25 -0
- package/src/functional-parser.ts +162 -0
- package/src/grammar.ts +869 -0
- package/src/index.ts +19 -0
- package/src/jess.ts +6 -0
- package/src/lessParser.ts +120 -0
- package/src/lessRecursiveParser.ts +279 -0
- package/src/lessTokens.ts +350 -0
- package/src/productions/guards.ts +1066 -0
- package/src/productions/index.ts +29 -0
- package/src/productions/root.ts +1613 -0
- package/src/productions/selectors.ts +1309 -0
- package/src/productions/values.ts +1449 -0
- package/src/utils.ts +178 -0
- package/lib/index.d.cts +0 -150
- package/lib/index.d.cts.map +0 -1
- package/lib/index.js.map +0 -1
|
@@ -0,0 +1,1613 @@
|
|
|
1
|
+
/* eslint-disable -- Retired Chevrotain parser; not linted (see @ts-nocheck below). */
|
|
2
|
+
// @ts-nocheck — Retired Chevrotain parser. Uses the legacy 6-tuple `.location`
|
|
3
|
+
// shape removed from Node in the provenance-side-table refactor; the functional
|
|
4
|
+
// Parséman grammar (grammar-rules.ts + builders.ts) is the maintained parser.
|
|
5
|
+
// Not type-checked.
|
|
6
|
+
// Root production rules for LessRecursiveParser
|
|
7
|
+
// Converted from lines 1-1145 of productions.ts (Chevrotain → hand-written recursive-descent)
|
|
8
|
+
import type { RuleContext } from '../lessRecursiveParser.js';
|
|
9
|
+
import type { IToken } from 'chevrotain';
|
|
10
|
+
import { type IOrAlt } from 'chevrotain';
|
|
11
|
+
import { productions as cssProductions } from '@jesscss/css-parser/jess';
|
|
12
|
+
|
|
13
|
+
import {
|
|
14
|
+
Node,
|
|
15
|
+
Ampersand,
|
|
16
|
+
Block,
|
|
17
|
+
Any,
|
|
18
|
+
type LocationInfo,
|
|
19
|
+
type TreeContext,
|
|
20
|
+
type Operator,
|
|
21
|
+
type ConditionOperator,
|
|
22
|
+
Ruleset,
|
|
23
|
+
BasicSelector,
|
|
24
|
+
Combinator,
|
|
25
|
+
type Combinators,
|
|
26
|
+
List,
|
|
27
|
+
Sequence,
|
|
28
|
+
QueryCondition,
|
|
29
|
+
Call,
|
|
30
|
+
Paren,
|
|
31
|
+
Operation,
|
|
32
|
+
Quoted,
|
|
33
|
+
AtRule,
|
|
34
|
+
Interpolated,
|
|
35
|
+
InterpolatedSelector,
|
|
36
|
+
Reference,
|
|
37
|
+
Dimension,
|
|
38
|
+
Num,
|
|
39
|
+
Extend,
|
|
40
|
+
type Extend as ExtendType,
|
|
41
|
+
Negative,
|
|
42
|
+
Mixin,
|
|
43
|
+
Condition,
|
|
44
|
+
VarDeclaration,
|
|
45
|
+
Declaration,
|
|
46
|
+
CustomDeclaration,
|
|
47
|
+
DefaultGuard,
|
|
48
|
+
Rest,
|
|
49
|
+
StyleImport,
|
|
50
|
+
Expression,
|
|
51
|
+
Keyword,
|
|
52
|
+
SelectorCapture,
|
|
53
|
+
ComplexSelector,
|
|
54
|
+
CompoundSelector,
|
|
55
|
+
SelectorList,
|
|
56
|
+
Rules,
|
|
57
|
+
Url,
|
|
58
|
+
Nil,
|
|
59
|
+
Collection,
|
|
60
|
+
type ComplexSelectorComponent,
|
|
61
|
+
type Selector,
|
|
62
|
+
type SimpleSelector,
|
|
63
|
+
type AnyRole,
|
|
64
|
+
isNode,
|
|
65
|
+
N,
|
|
66
|
+
shouldOperateWithMathFrames
|
|
67
|
+
} from '@jesscss/core';
|
|
68
|
+
import { createInterpolatedReference, getInterpolatedNode, getInterpolatedOrString, normalizeMixinReferenceKey } from '../utils.js';
|
|
69
|
+
import type { ExtendTarget, TokenMap } from '../lessRecursiveParser.js';
|
|
70
|
+
import { all } from 'known-css-properties';
|
|
71
|
+
|
|
72
|
+
/** Use `any` for `this` to avoid structural incompatibility between LessRecursiveParser and CssRecursiveParser */
|
|
73
|
+
type P = any;
|
|
74
|
+
type Alt = IOrAlt<any>[];
|
|
75
|
+
type AltContext = (ctx?: RuleContext) => Alt;
|
|
76
|
+
type ProductionRule = (...args: any[]) => any;
|
|
77
|
+
|
|
78
|
+
// ── Save references to CSS production factories ────────────────────────
|
|
79
|
+
const cssMain = cssProductions.main;
|
|
80
|
+
const cssDeclaration = cssProductions.declaration;
|
|
81
|
+
const cssMediaTypeQuery = cssProductions.mediaTypeQuery;
|
|
82
|
+
|
|
83
|
+
// ── Helper functions ──────────────────────────────────────────────────
|
|
84
|
+
|
|
85
|
+
function extendWithSelector(node: ExtendType, selector: Selector | undefined, context: TreeContext): ExtendType {
|
|
86
|
+
return new Extend({
|
|
87
|
+
selector,
|
|
88
|
+
target: node.target,
|
|
89
|
+
namespace: node.namespace,
|
|
90
|
+
flag: node.flag
|
|
91
|
+
}, undefined, node.location, context);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function prependRules(rules: Rules, nodes: Node[], context: TreeContext): Rules {
|
|
95
|
+
const out = new Rules([...nodes, ...rules.rules], rules.options, rules.location.length ? rules.location : undefined, context);
|
|
96
|
+
out._location = rules._location;
|
|
97
|
+
return out;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function getParenFrames(ctx: RuleContext | undefined): boolean[] {
|
|
101
|
+
return ctx?.parenFrames ?? [];
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function getCalcFrames(ctx: RuleContext | undefined): number {
|
|
105
|
+
return ctx?.calcFrames ?? 0;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function withCalcFrame(ctx: RuleContext | undefined, delta: number): RuleContext {
|
|
109
|
+
const calcFrames = getCalcFrames(ctx) + delta;
|
|
110
|
+
return { ...(ctx ?? {}), calcFrames };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function guardContainsDefaultCall(node: Node | undefined): boolean {
|
|
114
|
+
if (!node) {
|
|
115
|
+
return false;
|
|
116
|
+
}
|
|
117
|
+
const isNodeLike = (value: unknown): value is Node => {
|
|
118
|
+
return Boolean(
|
|
119
|
+
value
|
|
120
|
+
&& typeof value === 'object'
|
|
121
|
+
&& 'type' in value && typeof value.type === 'string'
|
|
122
|
+
&& 'valueOf' in value && typeof value.valueOf === 'function'
|
|
123
|
+
);
|
|
124
|
+
};
|
|
125
|
+
const queue: unknown[] = [node];
|
|
126
|
+
const seen = new Set<unknown>();
|
|
127
|
+
while (queue.length > 0) {
|
|
128
|
+
const current = queue.shift();
|
|
129
|
+
if (!current || seen.has(current) || !isNodeLike(current)) {
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
seen.add(current);
|
|
133
|
+
if (current.type === 'DefaultGuard') {
|
|
134
|
+
return true;
|
|
135
|
+
}
|
|
136
|
+
if (isNode(current, N.Call)) {
|
|
137
|
+
const callName = current.name;
|
|
138
|
+
const callNameStr = String(
|
|
139
|
+
(typeof callName === 'object' && callName !== null && 'valueOf' in callName)
|
|
140
|
+
? callName.valueOf()
|
|
141
|
+
: callName ?? ''
|
|
142
|
+
);
|
|
143
|
+
if (callNameStr === 'default' || callNameStr === '??') {
|
|
144
|
+
return true;
|
|
145
|
+
}
|
|
146
|
+
if (callName instanceof Reference) {
|
|
147
|
+
const key = callName.key;
|
|
148
|
+
const keyStr = String(
|
|
149
|
+
(typeof key === 'object' && key !== null && 'valueOf' in key)
|
|
150
|
+
? key.valueOf()
|
|
151
|
+
: key ?? ''
|
|
152
|
+
);
|
|
153
|
+
if (keyStr === 'default' || keyStr === '??') {
|
|
154
|
+
return true;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
if ('data' in current) {
|
|
159
|
+
const value = current.data;
|
|
160
|
+
if (Array.isArray(value)) {
|
|
161
|
+
queue.push(...value);
|
|
162
|
+
} else if (value && typeof value === 'object') {
|
|
163
|
+
queue.push(...Object.values(value));
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
return false;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function isDefaultGuardCall(node: Node | undefined): node is Call {
|
|
171
|
+
if (!node || !isNode(node, N.Call)) {
|
|
172
|
+
return false;
|
|
173
|
+
}
|
|
174
|
+
const callName = node.name;
|
|
175
|
+
const callNameStr = String(
|
|
176
|
+
(typeof callName === 'object' && callName !== null && 'valueOf' in callName)
|
|
177
|
+
? callName.valueOf()
|
|
178
|
+
: callName ?? ''
|
|
179
|
+
);
|
|
180
|
+
if (callNameStr === 'default' || callNameStr === '??') {
|
|
181
|
+
return true;
|
|
182
|
+
}
|
|
183
|
+
if (callName instanceof Reference) {
|
|
184
|
+
const key = callName.key;
|
|
185
|
+
const keyStr = String(
|
|
186
|
+
(typeof key === 'object' && key !== null && 'valueOf' in key)
|
|
187
|
+
? key.valueOf()
|
|
188
|
+
: key ?? ''
|
|
189
|
+
);
|
|
190
|
+
return keyStr === 'default' || keyStr === '??';
|
|
191
|
+
}
|
|
192
|
+
return false;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function loc(node: Node): LocationInfo | undefined {
|
|
196
|
+
const location = node.location;
|
|
197
|
+
return location.length === 6 ? (location as LocationInfo) : undefined;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function wrapAtRulePreludeExpression(this: P, node: Node, ctx: RuleContext | undefined): Node {
|
|
201
|
+
if (!this.wrapOuterExpressions || ctx?.atRulePreludeBareVariableAs !== 'index') {
|
|
202
|
+
return node;
|
|
203
|
+
}
|
|
204
|
+
if (node instanceof Expression) {
|
|
205
|
+
return node;
|
|
206
|
+
}
|
|
207
|
+
if (isNode(node, N.Reference) && !node.target && typeof node.key === 'string') {
|
|
208
|
+
return node;
|
|
209
|
+
}
|
|
210
|
+
return new Expression(node, undefined, loc(node), this.context);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
export function wrapOuterExpressionIfNeeded(this: P, node: Node, ctx: RuleContext | undefined): Node {
|
|
214
|
+
if (!this.wrapOuterExpressions) {
|
|
215
|
+
return node;
|
|
216
|
+
}
|
|
217
|
+
if (!ctx?.wrapInExpression) {
|
|
218
|
+
return node;
|
|
219
|
+
}
|
|
220
|
+
// Expressions should never contain Expressions; avoid nesting.
|
|
221
|
+
if (node instanceof Expression) {
|
|
222
|
+
return node;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Math expressions: only wrap if this operation would actually be performed.
|
|
226
|
+
if (isNode(node, N.Operation)) {
|
|
227
|
+
const mathMode = this.mathMode ?? 'parens-division';
|
|
228
|
+
const shouldOperate = shouldOperateWithMathFrames(
|
|
229
|
+
{
|
|
230
|
+
mathMode,
|
|
231
|
+
parenFrames: getParenFrames(ctx),
|
|
232
|
+
calcFrames: getCalcFrames(ctx)
|
|
233
|
+
},
|
|
234
|
+
node.operator,
|
|
235
|
+
node.left,
|
|
236
|
+
node.right
|
|
237
|
+
);
|
|
238
|
+
if (shouldOperate) {
|
|
239
|
+
return new Expression(node, { parens: true }, loc(node), this.context);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
return node;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function isEscapedString($: P, T: TokenMap): boolean {
|
|
247
|
+
const next = $.LA(1);
|
|
248
|
+
return (
|
|
249
|
+
next.image.startsWith('~')
|
|
250
|
+
&& (
|
|
251
|
+
$.matchToken(next, T.QuoteStart)
|
|
252
|
+
|| $.matchToken(next, T.DoubleQuoteStart)
|
|
253
|
+
|| $.matchToken(next, T.SingleQuoteStart)
|
|
254
|
+
)
|
|
255
|
+
);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function startsLessMediaQueryReference($: P, T: TokenMap): boolean {
|
|
259
|
+
if (
|
|
260
|
+
$.isType(T.AtName)
|
|
261
|
+
|| $.isType(T.PropertyReference)
|
|
262
|
+
|| $.isType(T.NestedReference)
|
|
263
|
+
|| $.isType(T.DotName)
|
|
264
|
+
|| $.isType(T.HashName)
|
|
265
|
+
|| $.isType(T.InterpolatedIdent)
|
|
266
|
+
|| $.isType(T.InterpolatedSelector)
|
|
267
|
+
) {
|
|
268
|
+
return true;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
if (!$.isType(T.ColorIdentStart)) {
|
|
272
|
+
return false;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
const tt2 = $.LA(2).tokenType;
|
|
276
|
+
return (
|
|
277
|
+
tt2 === T.Gt
|
|
278
|
+
|| tt2 === T.DotName
|
|
279
|
+
|| tt2 === T.HashName
|
|
280
|
+
|| tt2 === T.InterpolatedSelector
|
|
281
|
+
|| (
|
|
282
|
+
$.noSep(1) && (
|
|
283
|
+
tt2 === T.LParen
|
|
284
|
+
|| tt2 === T.LSquare
|
|
285
|
+
|| tt2 === T.HashName
|
|
286
|
+
|| tt2 === T.DotName
|
|
287
|
+
)
|
|
288
|
+
)
|
|
289
|
+
);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function startsCustomValue($: P, T: TokenMap): boolean {
|
|
293
|
+
return $.isType(T.LParen)
|
|
294
|
+
|| $.isType(T.FunctionStart)
|
|
295
|
+
|| $.isType(T.FunctionalPseudoClass)
|
|
296
|
+
|| $.isType(T.LSquare)
|
|
297
|
+
|| $.isType(T.LCurly)
|
|
298
|
+
|| $.isType(T.SingleQuoteStart)
|
|
299
|
+
|| $.isType(T.DoubleQuoteStart)
|
|
300
|
+
|| $.isType(T.Value)
|
|
301
|
+
|| $.isType(T.PlainIdent)
|
|
302
|
+
|| $.isType(T.AtKeyword)
|
|
303
|
+
|| $.isType(T.PropertyReference)
|
|
304
|
+
|| $.isType(T.CustomProperty)
|
|
305
|
+
|| $.isType(T.Dimension)
|
|
306
|
+
|| $.isType(T.Number)
|
|
307
|
+
|| $.isType(T.Color)
|
|
308
|
+
|| $.isType(T.UnicodeRange)
|
|
309
|
+
|| $.isType(T.Colon)
|
|
310
|
+
|| $.isType(T.Comma)
|
|
311
|
+
|| $.isType(T.Important)
|
|
312
|
+
|| $.isType(T.Unknown);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function isVariableLike($: P, T: TokenMap): boolean {
|
|
316
|
+
let token = $.LA(2);
|
|
317
|
+
let isColon = token.tokenType === T.Colon;
|
|
318
|
+
let isParen = token.tokenType === T.LParen;
|
|
319
|
+
let postToken = $.LA(3);
|
|
320
|
+
|
|
321
|
+
if (!$.trivia) {
|
|
322
|
+
return false;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
if (!isColon && !isParen) {
|
|
326
|
+
return false;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// Known at-rule tokens (@media, @supports, etc.) followed by ( without space:
|
|
330
|
+
// Only treat as a variable call if the parens are empty — @media();
|
|
331
|
+
// Otherwise it's an at-rule — @media(min-width: 0) { }
|
|
332
|
+
if (isParen && $.matchToken($.LA(1), T.AtName) && $.LA(1).tokenType !== T.AtKeyword) {
|
|
333
|
+
if (postToken.tokenType === T.RParen) {
|
|
334
|
+
// @media() — empty parens, allow as deprecated variable call
|
|
335
|
+
$.warnDeprecation(
|
|
336
|
+
'Using known at-rule names as variables is deprecated',
|
|
337
|
+
$.LA(1),
|
|
338
|
+
'at-rule-variable'
|
|
339
|
+
);
|
|
340
|
+
return true;
|
|
341
|
+
}
|
|
342
|
+
// @media(min-width: ...) — at-rule, not a variable
|
|
343
|
+
return false;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
let isVariable = !$.trivia.has(token.startOffset, 'before')
|
|
347
|
+
|| (isColon && $.trivia.has(postToken.startOffset, 'before'));
|
|
348
|
+
return isVariable;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
const { isArray } = Array;
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* Groups extends by target (using valueOf()) and flag.
|
|
355
|
+
* Returns an array of grouped Extend nodes where extends with the same target and flag
|
|
356
|
+
* are combined into a single Extend node with a SelectorList of all matching value.
|
|
357
|
+
*
|
|
358
|
+
* @todo Group complex value into selector lists
|
|
359
|
+
*/
|
|
360
|
+
function groupExtendsByTargetAndFlag(
|
|
361
|
+
extendNodes: Extend[]
|
|
362
|
+
): Array<Extend | Extend[]> {
|
|
363
|
+
// Group extends by target and flag
|
|
364
|
+
const groups = new Map<string, Extend | Extend[]>();
|
|
365
|
+
|
|
366
|
+
for (const ext of extendNodes) {
|
|
367
|
+
const { target, flag = 1 } = ext; // ExtendFlag.Exact = 1
|
|
368
|
+
// Create a key from target valueOf() and flag
|
|
369
|
+
const key = `${target.valueOf()}|${flag}`;
|
|
370
|
+
|
|
371
|
+
let group = groups.get(key);
|
|
372
|
+
if (!group) {
|
|
373
|
+
groups.set(key, ext);
|
|
374
|
+
} else if (isArray(group)) {
|
|
375
|
+
group.push(ext);
|
|
376
|
+
} else {
|
|
377
|
+
groups.set(key, [group, ext]);
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
return Array.from(groups.values());
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// ── Exported production rules ─────────────────────────────────────────
|
|
385
|
+
|
|
386
|
+
/** Charset moved within `main` (explained in that rule) */
|
|
387
|
+
export function stylesheet(this: P, T: TokenMap): ProductionRule {
|
|
388
|
+
const $ = this;
|
|
389
|
+
return (options: Record<string, any> = {}) => {
|
|
390
|
+
let context: TreeContext;
|
|
391
|
+
if (options.context) {
|
|
392
|
+
context = $.context = options.context;
|
|
393
|
+
} else {
|
|
394
|
+
context = $.context;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
let charset: IToken | undefined;
|
|
398
|
+
|
|
399
|
+
if (!$.looseMode) {
|
|
400
|
+
$.OPTION(() => {
|
|
401
|
+
charset = $.CONSUME(T.Charset);
|
|
402
|
+
});
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
const ctx: RuleContext = { isRoot: true };
|
|
406
|
+
|
|
407
|
+
let root: Node = $.SUBRULE($.main, { ARGS: [ctx] });
|
|
408
|
+
|
|
409
|
+
if (charset && isNode(root, N.Rules)) {
|
|
410
|
+
let charsetLoc = $.getLocationInfo(charset);
|
|
411
|
+
root = new Rules(
|
|
412
|
+
[new Any(charset.image, { role: 'charset' }, charsetLoc, context!), ...root.rules],
|
|
413
|
+
root.options,
|
|
414
|
+
root.location.length ? root.location : undefined,
|
|
415
|
+
context!
|
|
416
|
+
);
|
|
417
|
+
let rootLoc = root.location;
|
|
418
|
+
rootLoc[0] = charsetLoc[0];
|
|
419
|
+
rootLoc[1] = charsetLoc[1];
|
|
420
|
+
rootLoc[2] = charsetLoc[2];
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
return root;
|
|
424
|
+
};
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
/**
|
|
428
|
+
* Starts with a colon, with these conditions
|
|
429
|
+
* 1. It is not preceded by a space or
|
|
430
|
+
* 2. If it is preceded by a space, then it is
|
|
431
|
+
* followed by a space.
|
|
432
|
+
*/
|
|
433
|
+
|
|
434
|
+
export function main(this: P, T: TokenMap) {
|
|
435
|
+
const $ = this;
|
|
436
|
+
return (ctx: RuleContext = {}) => {
|
|
437
|
+
const isAmpersandExtendStart = () =>
|
|
438
|
+
$.LA(1).tokenType === T.Ampersand && $.LA(2).tokenType === T.Extend;
|
|
439
|
+
const isMixinOrQualifiedStart = () => {
|
|
440
|
+
const next = $.LA(1).tokenType;
|
|
441
|
+
return next === T.DotName || next === T.HashName || next === T.ColorIdentStart;
|
|
442
|
+
};
|
|
443
|
+
const isCustomPropertyStart = () =>
|
|
444
|
+
$.isType(T.InterpolatedCustomProperty) || $.isType(T.CustomProperty);
|
|
445
|
+
const isAtRuleStart = () => $.matchToken($.LA(1), T.AtName);
|
|
446
|
+
const shouldTryQualifiedRule = () =>
|
|
447
|
+
!isCustomPropertyStart()
|
|
448
|
+
&& !isMixinOrQualifiedStart()
|
|
449
|
+
&& !isAtRuleStart()
|
|
450
|
+
&& $.shouldTryQualifiedRuleInDeclarationList();
|
|
451
|
+
|
|
452
|
+
const ruleAlt = (ctx: RuleContext = {}): Alt => {
|
|
453
|
+
let isVariable = isVariableLike($, T);
|
|
454
|
+
return [
|
|
455
|
+
{ ALT: () => $.SUBRULE($.functionCall, { ARGS: [ctx] }) },
|
|
456
|
+
{
|
|
457
|
+
GATE: isAmpersandExtendStart,
|
|
458
|
+
ALT: () => $.SUBRULE2($.ampersandExtend, { ARGS: [ctx] })
|
|
459
|
+
},
|
|
460
|
+
{
|
|
461
|
+
GATE: isMixinOrQualifiedStart,
|
|
462
|
+
ALT: () => $.SUBRULE3($.mixinOrQualifiedRule, { ARGS: [ctx] })
|
|
463
|
+
},
|
|
464
|
+
{
|
|
465
|
+
GATE: () => isVariable,
|
|
466
|
+
ALT: () => $.SUBRULE4($.varDeclarationOrCall, { ARGS: [ctx] })
|
|
467
|
+
},
|
|
468
|
+
{
|
|
469
|
+
GATE: () => !isVariable && isAtRuleStart(),
|
|
470
|
+
ALT: () => $.SUBRULE5($.atRule, { ARGS: [ctx] })
|
|
471
|
+
},
|
|
472
|
+
{
|
|
473
|
+
GATE: isCustomPropertyStart,
|
|
474
|
+
ALT: () => $.SUBRULE7($.declaration, { ARGS: [ctx] })
|
|
475
|
+
},
|
|
476
|
+
{
|
|
477
|
+
GATE: shouldTryQualifiedRule,
|
|
478
|
+
ALT: () => $.SUBRULE6($.qualifiedRule, { ARGS: [ctx] })
|
|
479
|
+
},
|
|
480
|
+
|
|
481
|
+
/**
|
|
482
|
+
* Historically, Less allows `@charset` anywhere,
|
|
483
|
+
* to avoid outputting it in the wrong place.
|
|
484
|
+
* Ideally, this would result in an error if, say,
|
|
485
|
+
* the `@charset` was defined at the bottom of the file,
|
|
486
|
+
* but that wasn't the solution made.
|
|
487
|
+
* @see https://github.com/less/less.js/issues/2126
|
|
488
|
+
*/
|
|
489
|
+
{
|
|
490
|
+
GATE: () => $.looseMode,
|
|
491
|
+
ALT: () => $.CONSUME(T.Charset)
|
|
492
|
+
},
|
|
493
|
+
{ ALT: () => $.CONSUME(T.Semi) }
|
|
494
|
+
];
|
|
495
|
+
};
|
|
496
|
+
|
|
497
|
+
let RECORDING_PHASE = $.RECORDING_PHASE;
|
|
498
|
+
let context: TreeContext;
|
|
499
|
+
let rules: Node[];
|
|
500
|
+
if (!RECORDING_PHASE) {
|
|
501
|
+
context = $.context;
|
|
502
|
+
rules = [];
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
let requiredSemi = false;
|
|
506
|
+
|
|
507
|
+
let lastRule: Node | undefined;
|
|
508
|
+
/**
|
|
509
|
+
* In this production rule, semi-colons are not required
|
|
510
|
+
* but this is repurposed by declarationList and by Less / Sass,
|
|
511
|
+
* so that's why this gate is here.
|
|
512
|
+
*/
|
|
513
|
+
$.MANY({
|
|
514
|
+
GATE: () => {
|
|
515
|
+
const next = $.LA(1);
|
|
516
|
+
// Stop at RCurly (belongs to parent block) or end of input
|
|
517
|
+
if ($.isType(T.RCurly) || next.tokenType.name === 'EOF') {
|
|
518
|
+
return false;
|
|
519
|
+
}
|
|
520
|
+
return !requiredSemi || (requiredSemi && (
|
|
521
|
+
$.isType(T.Semi)
|
|
522
|
+
|| $.isTypeAt(0, T.Semi)
|
|
523
|
+
));
|
|
524
|
+
},
|
|
525
|
+
DEF: () => {
|
|
526
|
+
const localAlt = ruleAlt(ctx);
|
|
527
|
+
let value: Node | IToken = $.OR(localAlt);
|
|
528
|
+
if (!RECORDING_PHASE) {
|
|
529
|
+
/** @todo - When do we not have a value? */
|
|
530
|
+
if (value) {
|
|
531
|
+
if (!(value instanceof Node)) {
|
|
532
|
+
/** This is a semi-colon or charset token */
|
|
533
|
+
let tok = value as IToken;
|
|
534
|
+
if (tok.image.includes('@charset')) {
|
|
535
|
+
rules!.push(new Any(tok.image, { role: 'charset' }, $.getLocationInfo(tok), context!));
|
|
536
|
+
} else {
|
|
537
|
+
if (lastRule) {
|
|
538
|
+
lastRule.options.semi = true;
|
|
539
|
+
} else {
|
|
540
|
+
rules!.push(new Any(';', { role: 'semi' }, $.getLocationInfo($.LA(1)), context!));
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
} else {
|
|
544
|
+
requiredSemi = !!value.requiredSemi;
|
|
545
|
+
rules!.push(value);
|
|
546
|
+
lastRule = value;
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
});
|
|
552
|
+
|
|
553
|
+
if (RECORDING_PHASE) {
|
|
554
|
+
return;
|
|
555
|
+
}
|
|
556
|
+
// Process any extendNodes that were set (e.g., by ampersandExtend at root level)
|
|
557
|
+
if (ctx.extendNodes && ctx.extendNodes!.length > 0) {
|
|
558
|
+
// Filter out Nil nodes (returned by ampersandExtend to avoid duplication)
|
|
559
|
+
const filteredRules = rules!.filter(r => !(r instanceof Nil));
|
|
560
|
+
rules = [...ctx.extendNodes!, ...filteredRules];
|
|
561
|
+
ctx.extendNodes = undefined;
|
|
562
|
+
}
|
|
563
|
+
let returnNode = $.getRulesWithComments(rules!, $.getLocationInfo($.LA(1)));
|
|
564
|
+
// Attaches remaining whitespace at the end of rules
|
|
565
|
+
return returnNode!;
|
|
566
|
+
};
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
export function declarationList(this: P, T: TokenMap): ProductionRule {
|
|
570
|
+
const $ = this;
|
|
571
|
+
return (ctx: RuleContext = {}) => {
|
|
572
|
+
const isMixinOrQualifiedStart = () => {
|
|
573
|
+
const next = $.LA(1).tokenType;
|
|
574
|
+
return next === T.DotName || next === T.HashName || next === T.ColorIdentStart;
|
|
575
|
+
};
|
|
576
|
+
const isCustomPropertyStart = () =>
|
|
577
|
+
$.isType(T.InterpolatedCustomProperty) || $.isType(T.CustomProperty);
|
|
578
|
+
const isAtRuleStart = () => $.matchToken($.LA(1), T.AtName);
|
|
579
|
+
const isAmpersandExtendStart = () =>
|
|
580
|
+
$.LA(1).tokenType === T.Ampersand && $.LA(2).tokenType === T.Extend;
|
|
581
|
+
const shouldTryQualifiedRule = () =>
|
|
582
|
+
!isCustomPropertyStart()
|
|
583
|
+
&& !isMixinOrQualifiedStart()
|
|
584
|
+
&& !isAtRuleStart()
|
|
585
|
+
&& $.shouldTryQualifiedRuleInDeclarationList();
|
|
586
|
+
|
|
587
|
+
const ruleAlt = (ctx: RuleContext = {}): Alt => {
|
|
588
|
+
const isVariable = isVariableLike($, T);
|
|
589
|
+
return [
|
|
590
|
+
{
|
|
591
|
+
GATE: isMixinOrQualifiedStart,
|
|
592
|
+
ALT: () => {
|
|
593
|
+
return $.SUBRULE($.mixinOrQualifiedRule, { ARGS: [{ ...ctx, inner: true }] });
|
|
594
|
+
}
|
|
595
|
+
},
|
|
596
|
+
{
|
|
597
|
+
GATE: () => isVariable,
|
|
598
|
+
ALT: () => $.SUBRULE2($.varDeclarationOrCall, { ARGS: [ctx] })
|
|
599
|
+
},
|
|
600
|
+
{
|
|
601
|
+
GATE: () => !isVariable && isAtRuleStart(),
|
|
602
|
+
ALT: () => $.SUBRULE3($.innerAtRule, { ARGS: [ctx] })
|
|
603
|
+
},
|
|
604
|
+
{
|
|
605
|
+
GATE: isAmpersandExtendStart,
|
|
606
|
+
ALT: () => $.SUBRULE4($.ampersandExtend, { ARGS: [ctx] })
|
|
607
|
+
},
|
|
608
|
+
{
|
|
609
|
+
GATE: () => $.check(T.FunctionStart),
|
|
610
|
+
ALT: () => {
|
|
611
|
+
const fnCall = $.SUBRULE5($.functionCall, { ARGS: [ctx] });
|
|
612
|
+
if (fnCall instanceof Call) {
|
|
613
|
+
fnCall.requiredSemi = false;
|
|
614
|
+
}
|
|
615
|
+
return fnCall;
|
|
616
|
+
}
|
|
617
|
+
},
|
|
618
|
+
{
|
|
619
|
+
GATE: isCustomPropertyStart,
|
|
620
|
+
ALT: () => $.SUBRULE8($.declaration, { ARGS: [ctx] })
|
|
621
|
+
},
|
|
622
|
+
{
|
|
623
|
+
GATE: shouldTryQualifiedRule,
|
|
624
|
+
ALT: () => {
|
|
625
|
+
return $.SUBRULE6($.qualifiedRule, { ARGS: [{ ...ctx, inner: true }] });
|
|
626
|
+
}
|
|
627
|
+
},
|
|
628
|
+
{
|
|
629
|
+
ALT: () => {
|
|
630
|
+
return $.SUBRULE7($.declaration, { ARGS: [ctx] });
|
|
631
|
+
}
|
|
632
|
+
},
|
|
633
|
+
{ ALT: () => $.CONSUME(T.Semi) }
|
|
634
|
+
];
|
|
635
|
+
};
|
|
636
|
+
|
|
637
|
+
return cssMain.call($, T, ruleAlt)(ctx);
|
|
638
|
+
};
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
export function declaration(this: P, T: TokenMap) {
|
|
642
|
+
const $ = this;
|
|
643
|
+
return (ctx: RuleContext = {}) => {
|
|
644
|
+
const normalizeLessAssignToken = (assign: IToken): IToken => {
|
|
645
|
+
if (assign.tokenType === T.PlusAssign) {
|
|
646
|
+
return { ...assign, image: '+,:' };
|
|
647
|
+
}
|
|
648
|
+
if (assign.tokenType === T.UnderscoreAssign) {
|
|
649
|
+
return { ...assign, image: '+_:' };
|
|
650
|
+
}
|
|
651
|
+
return assign;
|
|
652
|
+
};
|
|
653
|
+
|
|
654
|
+
const customPropertyAlt = (consumeName: () => IToken, occurrence: 2 | 3) => ({
|
|
655
|
+
ALT: () => {
|
|
656
|
+
let nodes: Node[] | undefined;
|
|
657
|
+
if (!$.RECORDING_PHASE) {
|
|
658
|
+
nodes = [];
|
|
659
|
+
}
|
|
660
|
+
const name = consumeName();
|
|
661
|
+
const rawAssign = occurrence === 2
|
|
662
|
+
? $.CONSUME2(T.Assign)
|
|
663
|
+
: $.CONSUME3(T.Assign);
|
|
664
|
+
const assign = normalizeLessAssignToken(rawAssign);
|
|
665
|
+
$.startRule();
|
|
666
|
+
while (startsCustomValue($, T)) {
|
|
667
|
+
const val = occurrence === 2
|
|
668
|
+
? $.SUBRULE2($.customValue, { ARGS: [{ ...ctx, inCustomPropertyValue: true }] })
|
|
669
|
+
: $.SUBRULE3($.customValue, { ARGS: [{ ...ctx, inCustomPropertyValue: true }] });
|
|
670
|
+
if (!$.RECORDING_PHASE) {
|
|
671
|
+
nodes!.push(val);
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
if (!$.RECORDING_PHASE) {
|
|
675
|
+
const location = $.endRule();
|
|
676
|
+
let nameNode: Node;
|
|
677
|
+
const nameValue = name.image;
|
|
678
|
+
if (nameValue.includes('@') || nameValue.includes('$')) {
|
|
679
|
+
nameNode = getInterpolatedNode(nameValue, $.getLocationInfo(name), $.context);
|
|
680
|
+
} else {
|
|
681
|
+
nameNode = new Any(name.image, { role: 'property' }, $.getLocationInfo(name), $.context);
|
|
682
|
+
}
|
|
683
|
+
const value = new Sequence(nodes!, undefined, location, $.context);
|
|
684
|
+
return [nameNode, assign, value];
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
});
|
|
688
|
+
|
|
689
|
+
const ruleAlt = (ctx: RuleContext = {}): Alt => [
|
|
690
|
+
{
|
|
691
|
+
ALT: () => {
|
|
692
|
+
let name: IToken;
|
|
693
|
+
$.OR2([
|
|
694
|
+
{
|
|
695
|
+
ALT: () => {
|
|
696
|
+
name = $.CONSUME(T.Ident);
|
|
697
|
+
}
|
|
698
|
+
},
|
|
699
|
+
{
|
|
700
|
+
GATE: () => $.legacyMode,
|
|
701
|
+
ALT: () => name = $.CONSUME(T.LegacyPropIdent)
|
|
702
|
+
}
|
|
703
|
+
]);
|
|
704
|
+
const assign = normalizeLessAssignToken($.CONSUME(T.Assign));
|
|
705
|
+
let value: Node | undefined;
|
|
706
|
+
if ($.looseMode) {
|
|
707
|
+
$.OPTION2({
|
|
708
|
+
GATE: () => !($.isType(T.Semi) || $.isType(T.RCurly)),
|
|
709
|
+
DEF: () => {
|
|
710
|
+
value = $.SUBRULE($.valueList, { ARGS: [ctx] });
|
|
711
|
+
}
|
|
712
|
+
});
|
|
713
|
+
if (!$.RECORDING_PHASE && !value) {
|
|
714
|
+
value = new Sequence([], undefined, undefined, $.context);
|
|
715
|
+
}
|
|
716
|
+
} else {
|
|
717
|
+
value = $.SUBRULE($.valueList, { ARGS: [ctx] });
|
|
718
|
+
}
|
|
719
|
+
let important: IToken | undefined;
|
|
720
|
+
|
|
721
|
+
$.OPTION(() => {
|
|
722
|
+
important = $.CONSUME(T.Important);
|
|
723
|
+
});
|
|
724
|
+
if (!$.RECORDING_PHASE) {
|
|
725
|
+
let nameNode: Node;
|
|
726
|
+
const nameValue = name!.image;
|
|
727
|
+
if (nameValue.includes('@') || nameValue.includes('$')) {
|
|
728
|
+
nameNode = getInterpolatedNode(nameValue, $.getLocationInfo(name!), $.context);
|
|
729
|
+
} else {
|
|
730
|
+
nameNode = new Any(name!.image, { role: 'property' }, $.getLocationInfo(name!), $.context);
|
|
731
|
+
}
|
|
732
|
+
return [nameNode, assign, value, important];
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
},
|
|
736
|
+
customPropertyAlt(() => $.CONSUME(T.InterpolatedCustomProperty), 2),
|
|
737
|
+
customPropertyAlt(() => $.CONSUME(T.CustomProperty), 3)
|
|
738
|
+
];
|
|
739
|
+
|
|
740
|
+
return cssDeclaration.call($, T, ruleAlt)(ctx);
|
|
741
|
+
};
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
export function mediaInParens(this: P, T: TokenMap) {
|
|
745
|
+
const $ = this;
|
|
746
|
+
return (ctx: RuleContext = {}) => {
|
|
747
|
+
const RECORDING_PHASE = $.RECORDING_PHASE;
|
|
748
|
+
$.startRule();
|
|
749
|
+
$.CONSUME(T.LParen);
|
|
750
|
+
|
|
751
|
+
const node = $.OR([
|
|
752
|
+
{
|
|
753
|
+
GATE: () => $.startsMediaCondition(T),
|
|
754
|
+
ALT: () => $.SUBRULE($.mediaCondition, { ARGS: [ctx] })
|
|
755
|
+
},
|
|
756
|
+
{
|
|
757
|
+
GATE: () => isEscapedString($, T),
|
|
758
|
+
ALT: () => $.SUBRULE($.string, { ARGS: [ctx] })
|
|
759
|
+
},
|
|
760
|
+
{
|
|
761
|
+
/**
|
|
762
|
+
* Less allows media/container conditions to be supplied by variables or
|
|
763
|
+
* namespaced references, but only when the inner token stream actually
|
|
764
|
+
* starts like a Less reference, not a plain CSS media feature.
|
|
765
|
+
*/
|
|
766
|
+
GATE: () => (
|
|
767
|
+
$.isType(T.PropertyReference)
|
|
768
|
+
|| $.isType(T.NestedReference)
|
|
769
|
+
|| $.isType(T.AtName)
|
|
770
|
+
|| $.isType(T.HashName)
|
|
771
|
+
|| $.isType(T.DotName)
|
|
772
|
+
|| $.isType(T.ColorIdentStart)
|
|
773
|
+
|| $.isType(T.InterpolatedSelector)
|
|
774
|
+
),
|
|
775
|
+
ALT: () => {
|
|
776
|
+
const node = $.SUBRULE2($.valueReference, { ARGS: [{ ...ctx, requireAccessorsAfterMixinCall: true }] });
|
|
777
|
+
return wrapAtRulePreludeExpression.call($, node, ctx);
|
|
778
|
+
}
|
|
779
|
+
},
|
|
780
|
+
{
|
|
781
|
+
ALT: () => $.SUBRULE($.mediaFeature, { ARGS: [ctx] })
|
|
782
|
+
}
|
|
783
|
+
]);
|
|
784
|
+
|
|
785
|
+
$.CONSUME(T.RParen);
|
|
786
|
+
|
|
787
|
+
if (RECORDING_PHASE) {
|
|
788
|
+
return;
|
|
789
|
+
}
|
|
790
|
+
const location = $.endRule();
|
|
791
|
+
return new Paren(node, undefined, location, $.context);
|
|
792
|
+
};
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
export function mediaQuery(this: P, T: TokenMap) {
|
|
796
|
+
const $ = this;
|
|
797
|
+
return (ctx: RuleContext = {}) => {
|
|
798
|
+
const preludeCtx: RuleContext = { ...ctx, atRulePreludeBareVariableAs: 'index' };
|
|
799
|
+
return $.OR2([
|
|
800
|
+
{
|
|
801
|
+
GATE: () => $.startsMediaCondition(T),
|
|
802
|
+
ALT: () => $.SUBRULE($.mediaConditionWithoutOr, { ARGS: [preludeCtx] })
|
|
803
|
+
},
|
|
804
|
+
{
|
|
805
|
+
GATE: () => isEscapedString($, T),
|
|
806
|
+
ALT: () => $.SUBRULE($.lessMediaQueryFromString, { ARGS: [preludeCtx] })
|
|
807
|
+
},
|
|
808
|
+
{
|
|
809
|
+
GATE: () => startsLessMediaQueryReference($, T),
|
|
810
|
+
ALT: () => $.SUBRULE2($.lessMediaQueryFromReference, { ARGS: [preludeCtx] })
|
|
811
|
+
},
|
|
812
|
+
{
|
|
813
|
+
ALT: () => $.SUBRULE7($.mediaTypeQuery, { ARGS: [preludeCtx] })
|
|
814
|
+
}
|
|
815
|
+
]);
|
|
816
|
+
};
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
export function mediaCondition(this: P, T: TokenMap) {
|
|
820
|
+
const $ = this;
|
|
821
|
+
return (ctx: RuleContext = {}) => $.SUBRULE($.mediaConditionWithoutOr, { ARGS: [ctx] });
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
export function lessMediaQueryFromString(this: P, T: TokenMap) {
|
|
825
|
+
const $ = this;
|
|
826
|
+
return (ctx: RuleContext = {}) => {
|
|
827
|
+
const first = $.SUBRULE($.string, { ARGS: [ctx] });
|
|
828
|
+
return $.SUBRULE($.lessMediaQueryTail, { ARGS: [{ ...ctx, startValue: first }] });
|
|
829
|
+
};
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
export function lessMediaQueryFromReference(this: P, T: TokenMap) {
|
|
833
|
+
const $ = this;
|
|
834
|
+
return (ctx: RuleContext = {}) => {
|
|
835
|
+
const first = $.SUBRULE($.valueReference, { ARGS: [{ ...ctx, requireAccessorsAfterMixinCall: true }] });
|
|
836
|
+
return $.SUBRULE2($.lessMediaQueryTail, {
|
|
837
|
+
ARGS: [{
|
|
838
|
+
...ctx,
|
|
839
|
+
startValue: wrapAtRulePreludeExpression.call($, first, ctx)
|
|
840
|
+
}]
|
|
841
|
+
});
|
|
842
|
+
};
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
export function lessMediaQueryTail(this: P, T: TokenMap): ProductionRule {
|
|
846
|
+
const $ = this;
|
|
847
|
+
return (ctx: RuleContext = {}) => {
|
|
848
|
+
const RECORDING_PHASE = $.RECORDING_PHASE;
|
|
849
|
+
$.startRule();
|
|
850
|
+
let nodes: Node[] | undefined;
|
|
851
|
+
if (!RECORDING_PHASE) {
|
|
852
|
+
nodes = [ctx.startValue!];
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
$.MANY({
|
|
856
|
+
GATE: () => $.isType(T.And),
|
|
857
|
+
DEF: () => {
|
|
858
|
+
const andToken = $.CONSUME(T.And);
|
|
859
|
+
const next = $.OR([
|
|
860
|
+
{
|
|
861
|
+
GATE: () => isEscapedString($, T),
|
|
862
|
+
ALT: () => $.SUBRULE2($.string, { ARGS: [ctx] })
|
|
863
|
+
},
|
|
864
|
+
{
|
|
865
|
+
GATE: () => startsLessMediaQueryReference($, T),
|
|
866
|
+
ALT: () => {
|
|
867
|
+
const next = $.SUBRULE2($.valueReference, { ARGS: [{ ...ctx, requireAccessorsAfterMixinCall: true }] });
|
|
868
|
+
return wrapAtRulePreludeExpression.call($, next, ctx);
|
|
869
|
+
}
|
|
870
|
+
},
|
|
871
|
+
{
|
|
872
|
+
GATE: () => $.startsMediaCondition(T),
|
|
873
|
+
ALT: () => $.SUBRULE2($.mediaConditionWithoutOr, { ARGS: [ctx] })
|
|
874
|
+
},
|
|
875
|
+
{
|
|
876
|
+
ALT: () => $.SUBRULE2($.mediaType, { ARGS: [ctx] })
|
|
877
|
+
}
|
|
878
|
+
]);
|
|
879
|
+
|
|
880
|
+
if (!RECORDING_PHASE) {
|
|
881
|
+
nodes!.push(new Keyword(andToken.image, undefined, $.getLocationInfo(andToken), $.context));
|
|
882
|
+
nodes!.push(next);
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
});
|
|
886
|
+
|
|
887
|
+
if (RECORDING_PHASE) {
|
|
888
|
+
return;
|
|
889
|
+
}
|
|
890
|
+
const location = $.endRule();
|
|
891
|
+
if (nodes!.length === 1) {
|
|
892
|
+
return nodes![0]!;
|
|
893
|
+
}
|
|
894
|
+
return new QueryCondition(nodes!, undefined, location, $.context);
|
|
895
|
+
};
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
export function mediaConditionWithoutOr(this: P, T: TokenMap) {
|
|
899
|
+
const $ = this;
|
|
900
|
+
return (ctx: RuleContext = {}) => $.OR([
|
|
901
|
+
{ ALT: () => $.SUBRULE($.mediaNot, { ARGS: [ctx] }) },
|
|
902
|
+
{
|
|
903
|
+
ALT: () => {
|
|
904
|
+
const RECORDING_PHASE = $.RECORDING_PHASE;
|
|
905
|
+
$.startRule();
|
|
906
|
+
let nodes: Node[] | undefined;
|
|
907
|
+
if (!RECORDING_PHASE) {
|
|
908
|
+
nodes = [];
|
|
909
|
+
}
|
|
910
|
+
const node = $.SUBRULE($.mediaInParens, { ARGS: [ctx] });
|
|
911
|
+
if (!RECORDING_PHASE) {
|
|
912
|
+
nodes!.push(node);
|
|
913
|
+
}
|
|
914
|
+
$.MANY({
|
|
915
|
+
GATE: () => $.isType(T.And),
|
|
916
|
+
DEF: () => {
|
|
917
|
+
const rule = $.SUBRULE($.mediaAnd, { ARGS: [ctx] });
|
|
918
|
+
if (!RECORDING_PHASE) {
|
|
919
|
+
nodes!.push(...rule);
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
});
|
|
923
|
+
if (RECORDING_PHASE) {
|
|
924
|
+
return;
|
|
925
|
+
}
|
|
926
|
+
if (nodes!.length === 1) {
|
|
927
|
+
$.endRule();
|
|
928
|
+
return nodes![0]!;
|
|
929
|
+
}
|
|
930
|
+
return new QueryCondition(nodes!, undefined, $.endRule(), $.context);
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
]);
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
export function mediaFeature(this: P, T: TokenMap) {
|
|
937
|
+
const $ = this;
|
|
938
|
+
|
|
939
|
+
function createFeatureIdentNode(token: IToken, role: 'property'): Any<'property'> | Interpolated<'property'>;
|
|
940
|
+
function createFeatureIdentNode(token: IToken, role: 'ident'): Any<'ident'> | Interpolated<AnyRole>;
|
|
941
|
+
function createFeatureIdentNode(token: IToken, role: 'ident' | 'property') {
|
|
942
|
+
const location = $.getLocationInfo(token);
|
|
943
|
+
const resolved = getInterpolatedOrString(token.image, location, $.context);
|
|
944
|
+
if (typeof resolved === 'string') {
|
|
945
|
+
return new Any(resolved, { role }, location, $.context);
|
|
946
|
+
}
|
|
947
|
+
return resolved;
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
return (ctx: RuleContext = {}) => $.OR([
|
|
951
|
+
{
|
|
952
|
+
GATE: () => {
|
|
953
|
+
return $.isType(T.InterpolatedIdent) || $.isType(T.Ident);
|
|
954
|
+
},
|
|
955
|
+
ALT: () => {
|
|
956
|
+
const RECORDING_PHASE = $.RECORDING_PHASE;
|
|
957
|
+
$.startRule();
|
|
958
|
+
let rule: Node | undefined;
|
|
959
|
+
const ident = $.LA(1).tokenType === T.InterpolatedIdent
|
|
960
|
+
? $.CONSUME(T.InterpolatedIdent)
|
|
961
|
+
: $.CONSUME(T.Ident);
|
|
962
|
+
$.OPTION(() => {
|
|
963
|
+
rule = $.OR2([
|
|
964
|
+
{
|
|
965
|
+
ALT: () => {
|
|
966
|
+
$.CONSUME(T.Colon);
|
|
967
|
+
const value = $.SUBRULE($.mfValue, { ARGS: [ctx] });
|
|
968
|
+
if (!RECORDING_PHASE) {
|
|
969
|
+
const location = $.endRule();
|
|
970
|
+
const featureName = createFeatureIdentNode(ident, 'property');
|
|
971
|
+
return new Declaration({
|
|
972
|
+
name: featureName instanceof Any ? String(featureName.valueOf()) : featureName,
|
|
973
|
+
value: value
|
|
974
|
+
}, undefined, location, $.context);
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
},
|
|
978
|
+
{
|
|
979
|
+
GATE: () => ($.isTypeAt(1, T.MfLt) || $.isTypeAt(1, T.MfGt))
|
|
980
|
+
&& ($.isTypeAt(2, T.Ident) || $.isTypeAt(2, T.InterpolatedIdent)),
|
|
981
|
+
ALT: () => {
|
|
982
|
+
const seq = $.SUBRULE($.mediaRange, { ARGS: [ctx] });
|
|
983
|
+
|
|
984
|
+
if (!RECORDING_PHASE) {
|
|
985
|
+
const [startOffset, startLine, startColumn] = $.endRule();
|
|
986
|
+
seq.value.unshift(createFeatureIdentNode(ident, 'ident'));
|
|
987
|
+
seq.location.start = startOffset;
|
|
988
|
+
seq.location[1] = startLine;
|
|
989
|
+
seq.location[2] = startColumn;
|
|
990
|
+
return new QueryCondition(seq.value, undefined, seq.location, $.context);
|
|
991
|
+
}
|
|
992
|
+
return seq;
|
|
993
|
+
}
|
|
994
|
+
},
|
|
995
|
+
{
|
|
996
|
+
GATE: () => $.isTypeAt(1, T.MfLt) || $.isTypeAt(1, T.MfGt) || $.LA(1).tokenType === T.Eq,
|
|
997
|
+
ALT: () => {
|
|
998
|
+
const op = $.SUBRULE($.mfComparison, { ARGS: [ctx] });
|
|
999
|
+
const value = $.SUBRULE($.mfNonIdentifierValue, { ARGS: [ctx] });
|
|
1000
|
+
|
|
1001
|
+
if (!RECORDING_PHASE) {
|
|
1002
|
+
const location = $.endRule();
|
|
1003
|
+
return new QueryCondition([
|
|
1004
|
+
createFeatureIdentNode(ident, 'ident'),
|
|
1005
|
+
new Any(op.image, { role: 'operator' }, $.getLocationInfo(op), $.context),
|
|
1006
|
+
value
|
|
1007
|
+
], undefined, location, $.context);
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
1011
|
+
]);
|
|
1012
|
+
});
|
|
1013
|
+
if (!RECORDING_PHASE && !rule) {
|
|
1014
|
+
const location = $.endRule();
|
|
1015
|
+
const identNode = createFeatureIdentNode(ident, 'ident');
|
|
1016
|
+
return new QueryCondition([identNode], undefined, location, $.context);
|
|
1017
|
+
}
|
|
1018
|
+
return rule;
|
|
1019
|
+
}
|
|
1020
|
+
},
|
|
1021
|
+
{
|
|
1022
|
+
ALT: () => {
|
|
1023
|
+
const RECORDING_PHASE = $.RECORDING_PHASE;
|
|
1024
|
+
$.startRule();
|
|
1025
|
+
const left = $.SUBRULE2($.mfNonIdentifierValue, { ARGS: [{ ...ctx }] });
|
|
1026
|
+
return $.OR3([
|
|
1027
|
+
{
|
|
1028
|
+
GATE: () => {
|
|
1029
|
+
const tt2 = $.LA(2).tokenType;
|
|
1030
|
+
if (!(($.isTypeAt(1, T.MfLt) || $.isTypeAt(1, T.MfGt) || $.LA(1).tokenType === T.Eq)
|
|
1031
|
+
&& (tt2 === T.Ident || tt2 === T.InterpolatedIdent))) {
|
|
1032
|
+
return false;
|
|
1033
|
+
}
|
|
1034
|
+
if ($.isTypeAt(3, T.MfLt) || $.isTypeAt(3, T.MfGt)) {
|
|
1035
|
+
return false;
|
|
1036
|
+
}
|
|
1037
|
+
return true;
|
|
1038
|
+
},
|
|
1039
|
+
ALT: () => {
|
|
1040
|
+
const op = $.SUBRULE2($.mfComparison, { ARGS: [{ ...ctx }] });
|
|
1041
|
+
const value = $.LA(1).tokenType === T.Ident
|
|
1042
|
+
? $.CONSUME2(T.Ident)
|
|
1043
|
+
: $.CONSUME2(T.InterpolatedIdent);
|
|
1044
|
+
if (!RECORDING_PHASE) {
|
|
1045
|
+
const location = $.endRule();
|
|
1046
|
+
return new QueryCondition([
|
|
1047
|
+
left,
|
|
1048
|
+
new Any(op.image, { role: 'operator' }, $.getLocationInfo(op), $.context),
|
|
1049
|
+
createFeatureIdentNode(value, 'ident')
|
|
1050
|
+
], undefined, location, $.context);
|
|
1051
|
+
}
|
|
1052
|
+
}
|
|
1053
|
+
},
|
|
1054
|
+
{
|
|
1055
|
+
ALT: () => {
|
|
1056
|
+
const seq = $.SUBRULE2($.mediaRange, { ARGS: [{ ...ctx }] });
|
|
1057
|
+
if (!RECORDING_PHASE) {
|
|
1058
|
+
const [startOffset, startLine, startColumn] = $.endRule();
|
|
1059
|
+
seq.value.unshift(left);
|
|
1060
|
+
seq.location.start = startOffset;
|
|
1061
|
+
seq.location[1] = startLine;
|
|
1062
|
+
seq.location[2] = startColumn;
|
|
1063
|
+
return new QueryCondition(seq.value, undefined, seq.location, $.context);
|
|
1064
|
+
}
|
|
1065
|
+
return seq;
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
1068
|
+
]);
|
|
1069
|
+
}
|
|
1070
|
+
}
|
|
1071
|
+
]);
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
export function mfValue(this: P, T: TokenMap): ProductionRule {
|
|
1075
|
+
const $ = this;
|
|
1076
|
+
return (ctx: RuleContext = {}) => {
|
|
1077
|
+
/**
|
|
1078
|
+
* Like the original Less Parser, we're
|
|
1079
|
+
* going to allow any value expression,
|
|
1080
|
+
* and it's up to the Less author to know
|
|
1081
|
+
* if it's valid.
|
|
1082
|
+
*/
|
|
1083
|
+
const exprCtx: RuleContext = { ...ctx, wrapInExpression: true };
|
|
1084
|
+
const node = $.SUBRULE($.expressionSum, { ARGS: [exprCtx] });
|
|
1085
|
+
return wrapOuterExpressionIfNeeded.call($, node, exprCtx);
|
|
1086
|
+
};
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
export function mfNonIdentifierValue(this: P, T: TokenMap) {
|
|
1090
|
+
const $ = this;
|
|
1091
|
+
return (ctx: RuleContext = {}) => {
|
|
1092
|
+
return $.OR2([
|
|
1093
|
+
{
|
|
1094
|
+
GATE: () => {
|
|
1095
|
+
const next = $.LA(1);
|
|
1096
|
+
return next.tokenType === T.AtKeyword || next.tokenType === T.PropertyReference || next.tokenType === T.NestedReference;
|
|
1097
|
+
},
|
|
1098
|
+
ALT: () => {
|
|
1099
|
+
const node = $.SUBRULE($.valueReference, { ARGS: [{ ...ctx, requireAccessorsAfterMixinCall: true }] });
|
|
1100
|
+
return wrapAtRulePreludeExpression.call($, node, ctx);
|
|
1101
|
+
}
|
|
1102
|
+
},
|
|
1103
|
+
{
|
|
1104
|
+
ALT: () => {
|
|
1105
|
+
$.startRule();
|
|
1106
|
+
let num1 = $.CONSUME(T.Number);
|
|
1107
|
+
let num2: IToken | undefined;
|
|
1108
|
+
$.OPTION(() => {
|
|
1109
|
+
$.CONSUME(T.Slash);
|
|
1110
|
+
num2 = $.CONSUME2(T.Number);
|
|
1111
|
+
});
|
|
1112
|
+
let location = $.endRule();
|
|
1113
|
+
let num1Node = $.processValueToken(num1);
|
|
1114
|
+
if (!num2) {
|
|
1115
|
+
return num1Node;
|
|
1116
|
+
}
|
|
1117
|
+
let num2Node = $.processValueToken(num2);
|
|
1118
|
+
return new List([num1Node, num2Node], { sep: '/' }, location, $.context);
|
|
1119
|
+
}
|
|
1120
|
+
},
|
|
1121
|
+
{
|
|
1122
|
+
ALT: () => {
|
|
1123
|
+
let dim = $.CONSUME(T.Dimension);
|
|
1124
|
+
return $.processValueToken(dim);
|
|
1125
|
+
}
|
|
1126
|
+
}
|
|
1127
|
+
]);
|
|
1128
|
+
};
|
|
1129
|
+
}
|
|
1130
|
+
|
|
1131
|
+
export function wrappedDeclarationList(this: P, T: TokenMap) {
|
|
1132
|
+
const $ = this;
|
|
1133
|
+
return (ctx: RuleContext = {}) => {
|
|
1134
|
+
$.CONSUME(T.LCurly);
|
|
1135
|
+
let rules = $.SUBRULE($.declarationList, { ARGS: [ctx] });
|
|
1136
|
+
$.CONSUME(T.RCurly);
|
|
1137
|
+
return rules;
|
|
1138
|
+
};
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1141
|
+
export function qualifiedRuleBody(this: P, T: TokenMap) {
|
|
1142
|
+
const $ = this;
|
|
1143
|
+
return (ctx: RuleContext = {}) => {
|
|
1144
|
+
let selector!: Selector;
|
|
1145
|
+
let isSelectorList: boolean | undefined;
|
|
1146
|
+
selector = ctx.selector!;
|
|
1147
|
+
isSelectorList = typeof ctx.isSelectorList === 'boolean' ? ctx.isSelectorList : selector instanceof SelectorList;
|
|
1148
|
+
|
|
1149
|
+
let guard: Condition | undefined;
|
|
1150
|
+
if (!isSelectorList) {
|
|
1151
|
+
$.OPTION(() => {
|
|
1152
|
+
guard = $.SUBRULE($.guard, { ARGS: [ctx] });
|
|
1153
|
+
});
|
|
1154
|
+
}
|
|
1155
|
+
$.CONSUME(T.LCurly);
|
|
1156
|
+
let savedExtendNodes: Extend[] | undefined;
|
|
1157
|
+
if (!$.RECORDING_PHASE) {
|
|
1158
|
+
// Save extendNodes before parsing declarationList, so nested rulesets don't inherit them
|
|
1159
|
+
// Make a copy of the array (not just a reference) so mutations during nested parsing don't affect it
|
|
1160
|
+
savedExtendNodes = ctx.extendNodes ? [...ctx.extendNodes] : undefined;
|
|
1161
|
+
ctx.extendNodes = undefined;
|
|
1162
|
+
}
|
|
1163
|
+
let rules = $.SUBRULE2($.declarationList, { ARGS: [ctx] });
|
|
1164
|
+
let end = $.CONSUME(T.RCurly);
|
|
1165
|
+
if (!$.RECORDING_PHASE) {
|
|
1166
|
+
// After declarationList, check if new extends were added (e.g., by ampersandExtend)
|
|
1167
|
+
// If so, merge them with the saved extends; otherwise restore the saved extends
|
|
1168
|
+
const newExtends = ctx.extendNodes;
|
|
1169
|
+
if (newExtends && newExtends.length) {
|
|
1170
|
+
// New extends were added during declarationList (e.g., &:extend())
|
|
1171
|
+
if (savedExtendNodes && savedExtendNodes.length > 0) {
|
|
1172
|
+
// Merge with saved extends
|
|
1173
|
+
ctx.extendNodes = [...savedExtendNodes, ...newExtends];
|
|
1174
|
+
} else {
|
|
1175
|
+
// Keep the new extends
|
|
1176
|
+
ctx.extendNodes = newExtends;
|
|
1177
|
+
}
|
|
1178
|
+
} else {
|
|
1179
|
+
// No new extends, restore saved extends
|
|
1180
|
+
ctx.extendNodes = savedExtendNodes;
|
|
1181
|
+
}
|
|
1182
|
+
let extend = ctx.extendNodes;
|
|
1183
|
+
if (extend?.length) {
|
|
1184
|
+
/** If it's not a selector list, then our only extend does not need to be grouped */
|
|
1185
|
+
if (!isSelectorList) {
|
|
1186
|
+
/** For extends inside rulesets (not bubbled), selector should be undefined
|
|
1187
|
+
* so it defaults to ampersand and resolves to the ruleset's selector */
|
|
1188
|
+
extend = extend.map(e => extendWithSelector(e, undefined, $.context));
|
|
1189
|
+
rules = prependRules(rules, extend, $.context);
|
|
1190
|
+
ctx.extendNodes = undefined;
|
|
1191
|
+
} else {
|
|
1192
|
+
const selectorList = selector instanceof SelectorList ? selector : undefined;
|
|
1193
|
+
if (!selectorList) {
|
|
1194
|
+
return;
|
|
1195
|
+
}
|
|
1196
|
+
const selectorCount = selectorList.value.length;
|
|
1197
|
+
const extendCount = extend.length;
|
|
1198
|
+
|
|
1199
|
+
// Determine if extends should bubble up:
|
|
1200
|
+
// 1. If any value in the list have extends (extendCount < selectorCount)
|
|
1201
|
+
// 2. If all value have extends but their "all" flags don't match
|
|
1202
|
+
let shouldBubble = false;
|
|
1203
|
+
|
|
1204
|
+
if (extendCount < selectorCount) {
|
|
1205
|
+
// Some value have extends, some don't - bubble up
|
|
1206
|
+
shouldBubble = true;
|
|
1207
|
+
} else if (extendCount === selectorCount) {
|
|
1208
|
+
// All value have extends - check if flags match
|
|
1209
|
+
let finalExtends = groupExtendsByTargetAndFlag(extend);
|
|
1210
|
+
if (finalExtends.length === 1) {
|
|
1211
|
+
// All extends have same target and flag - can be inside ruleset
|
|
1212
|
+
let extendNodes = finalExtends[0]!;
|
|
1213
|
+
let finalExtend = isArray(extendNodes) ? extendNodes[0]! : extendNodes;
|
|
1214
|
+
finalExtend = extendWithSelector(finalExtend, undefined, $.context);
|
|
1215
|
+
rules = prependRules(rules, [finalExtend], $.context);
|
|
1216
|
+
ctx.extendNodes = undefined;
|
|
1217
|
+
} else {
|
|
1218
|
+
// Multiple extend groups (different targets/flags) - bubble up
|
|
1219
|
+
shouldBubble = true;
|
|
1220
|
+
}
|
|
1221
|
+
} else {
|
|
1222
|
+
// extendCount > selectorCount - shouldn't happen, but bubble to be safe
|
|
1223
|
+
shouldBubble = true;
|
|
1224
|
+
}
|
|
1225
|
+
|
|
1226
|
+
if (shouldBubble) {
|
|
1227
|
+
// Keep extends in ctx.extendNodes so they bubble up to qualifiedRule
|
|
1228
|
+
// Don't clear ctx.extendNodes - let them bubble
|
|
1229
|
+
// The extends will be prepended above the ruleset in qualifiedRule
|
|
1230
|
+
}
|
|
1231
|
+
}
|
|
1232
|
+
}
|
|
1233
|
+
const hasDefault = Boolean(ctx.hasDefault);
|
|
1234
|
+
let node = new Ruleset(
|
|
1235
|
+
{ selector, rules: rules.rules, guard },
|
|
1236
|
+
guard && hasDefault ? { hasDefault } : undefined,
|
|
1237
|
+
undefined,
|
|
1238
|
+
$.context
|
|
1239
|
+
);
|
|
1240
|
+
ctx.hasDefault = false;
|
|
1241
|
+
let [startOffset, startLine, startColumn] = selector.location!;
|
|
1242
|
+
let { endOffset, endLine, endColumn } = end;
|
|
1243
|
+
node._location = [startOffset!, startLine!, startColumn!, endOffset!, endLine!, endColumn!];
|
|
1244
|
+
|
|
1245
|
+
return node;
|
|
1246
|
+
}
|
|
1247
|
+
};
|
|
1248
|
+
}
|
|
1249
|
+
|
|
1250
|
+
export function qualifiedRule(this: P, T: TokenMap): ProductionRule {
|
|
1251
|
+
const $ = this;
|
|
1252
|
+
return (ctx: RuleContext = {}, altContext?: AltContext) => {
|
|
1253
|
+
let selectorAlt = altContext ?? ((ctx: RuleContext) => [
|
|
1254
|
+
{
|
|
1255
|
+
GATE: () => !ctx.inner,
|
|
1256
|
+
ALT: () => {
|
|
1257
|
+
let initialQualifiedRule = ctx.qualifiedRule;
|
|
1258
|
+
ctx.qualifiedRule = true;
|
|
1259
|
+
try {
|
|
1260
|
+
return $.SUBRULE($.selectorList, { ARGS: [ctx] });
|
|
1261
|
+
} finally {
|
|
1262
|
+
ctx.qualifiedRule = initialQualifiedRule;
|
|
1263
|
+
}
|
|
1264
|
+
}
|
|
1265
|
+
},
|
|
1266
|
+
{
|
|
1267
|
+
GATE: () => !!ctx.inner,
|
|
1268
|
+
ALT: () => {
|
|
1269
|
+
let initialQualifiedRule = ctx.qualifiedRule;
|
|
1270
|
+
let initialFirstSelector = ctx.firstSelector;
|
|
1271
|
+
ctx.firstSelector = true;
|
|
1272
|
+
ctx.qualifiedRule = true;
|
|
1273
|
+
try {
|
|
1274
|
+
return $.SUBRULE2($.forgivingSelectorList, { ARGS: [ctx] });
|
|
1275
|
+
} finally {
|
|
1276
|
+
ctx.qualifiedRule = initialQualifiedRule;
|
|
1277
|
+
ctx.firstSelector = initialFirstSelector;
|
|
1278
|
+
}
|
|
1279
|
+
}
|
|
1280
|
+
}
|
|
1281
|
+
]);
|
|
1282
|
+
// qualifiedRule
|
|
1283
|
+
// : selectorList WS* LCURLY declarationList RCURLY
|
|
1284
|
+
// ;
|
|
1285
|
+
// Save parent's extendNodes before parsing selector (which may set extendNodes)
|
|
1286
|
+
let savedExtendNodes = ctx.extendNodes ? [...ctx.extendNodes] : undefined;
|
|
1287
|
+
// Set extendNodes to a fresh empty array upon entry to this qualifiedRule
|
|
1288
|
+
// so nested rulesets don't inherit extends from parent rulesets
|
|
1289
|
+
ctx.extendNodes = undefined;
|
|
1290
|
+
let selector: Selector = $.OR(selectorAlt(ctx));
|
|
1291
|
+
// Use the same context object so modifications propagate back
|
|
1292
|
+
ctx.selector = selector;
|
|
1293
|
+
// Now extendNodes may have been set by extend() during selector parsing
|
|
1294
|
+
// Save it for this ruleset, then clear it so nested rulesets don't see it
|
|
1295
|
+
let thisExtendNodes = ctx.extendNodes ? [...ctx.extendNodes] : undefined;
|
|
1296
|
+
ctx.extendNodes = undefined;
|
|
1297
|
+
let rule: Node = $.SUBRULE3($.qualifiedRuleBody, { ARGS: [ctx] });
|
|
1298
|
+
// After qualifiedRuleBody returns, ctx.extendNodes may contain:
|
|
1299
|
+
// 1. Extends that should bubble up (from nested rulesets or this ruleset that didn't match)
|
|
1300
|
+
// 2. Nothing (if all extends were processed)
|
|
1301
|
+
// Restore this ruleset's extendNodes (from selector parsing) to process them
|
|
1302
|
+
|
|
1303
|
+
const bubblingExtends = (ctx as { extendNodes?: Extend[] }).extendNodes; // Extends that should bubble up
|
|
1304
|
+
ctx.extendNodes = thisExtendNodes;
|
|
1305
|
+
// Restore parent's extendNodes after processing this ruleset's extends
|
|
1306
|
+
let parentExtendNodes = savedExtendNodes;
|
|
1307
|
+
if (ctx.extendNodes) {
|
|
1308
|
+
let qRuleset = rule;
|
|
1309
|
+
// Set the Extend nodes' selector to the ruleset's selector (not &)
|
|
1310
|
+
// This allows the extends to work correctly when evaluated in the wrapper Rules context
|
|
1311
|
+
for (const extendNode of ctx.extendNodes) {
|
|
1312
|
+
const { selector: extendSelector } = extendNode;
|
|
1313
|
+
if (extendSelector === undefined || extendSelector instanceof Ampersand) {
|
|
1314
|
+
const index = ctx.extendNodes.indexOf(extendNode);
|
|
1315
|
+
ctx.extendNodes[index] = extendWithSelector(extendNode, selector, $.context);
|
|
1316
|
+
}
|
|
1317
|
+
}
|
|
1318
|
+
/** Prepend a rules block */
|
|
1319
|
+
rule = new Rules([
|
|
1320
|
+
...ctx.extendNodes,
|
|
1321
|
+
qRuleset
|
|
1322
|
+
]);
|
|
1323
|
+
// Set location from the ruleset (which has proper location info)
|
|
1324
|
+
if (qRuleset._location) {
|
|
1325
|
+
rule._location = qRuleset._location;
|
|
1326
|
+
}
|
|
1327
|
+
ctx.extendNodes = undefined;
|
|
1328
|
+
}
|
|
1329
|
+
// Restore parent's extendNodes and merge with any bubbling extends
|
|
1330
|
+
const hasBubblingExtends = bubblingExtends && bubblingExtends.length > 0;
|
|
1331
|
+
if (hasBubblingExtends) {
|
|
1332
|
+
// Bubble them up to parent
|
|
1333
|
+
if (parentExtendNodes && parentExtendNodes.length > 0) {
|
|
1334
|
+
ctx.extendNodes = [...parentExtendNodes, ...bubblingExtends];
|
|
1335
|
+
} else {
|
|
1336
|
+
ctx.extendNodes = bubblingExtends;
|
|
1337
|
+
}
|
|
1338
|
+
} else {
|
|
1339
|
+
ctx.extendNodes = parentExtendNodes;
|
|
1340
|
+
}
|
|
1341
|
+
return rule;
|
|
1342
|
+
};
|
|
1343
|
+
}
|
|
1344
|
+
|
|
1345
|
+
/**
|
|
1346
|
+
* In order to not do any backtracking, anything with a class or id selector start
|
|
1347
|
+
* will end up here, and everything else will be shunted to the qualified rule.
|
|
1348
|
+
*/
|
|
1349
|
+
export function mixinOrQualifiedRule(this: P, T: TokenMap) {
|
|
1350
|
+
const $ = this;
|
|
1351
|
+
return (ctx: RuleContext = {}) => {
|
|
1352
|
+
// Helper function to convert Any nodes to VarDeclaration nodes for mixin definition parameters
|
|
1353
|
+
const convertArgsForDefinition = (args: List<Node> | undefined): void => {
|
|
1354
|
+
if (!args || !args.value.length) {
|
|
1355
|
+
return;
|
|
1356
|
+
}
|
|
1357
|
+
|
|
1358
|
+
for (let i = 0; i < args.value.length; i++) {
|
|
1359
|
+
const node = args.value[i]!;
|
|
1360
|
+
|
|
1361
|
+
const location: LocationInfo | undefined = node.location.length ? node.location as LocationInfo : undefined;
|
|
1362
|
+
|
|
1363
|
+
// If it's an Any node with role: 'name', convert it to VarDeclaration for mixin definition parameters
|
|
1364
|
+
if (isNode(node, N.Any) && node.role === 'name') {
|
|
1365
|
+
const replacement = new VarDeclaration({
|
|
1366
|
+
name: String(node.valueOf()),
|
|
1367
|
+
value: new Nil(undefined, undefined, location, $.context)
|
|
1368
|
+
}, { paramVar: true }, location, $.context);
|
|
1369
|
+
args.adopt(replacement);
|
|
1370
|
+
args.value[i] = replacement;
|
|
1371
|
+
}
|
|
1372
|
+
// Rest nodes with string values can stay as-is for mixin definitions
|
|
1373
|
+
}
|
|
1374
|
+
};
|
|
1375
|
+
|
|
1376
|
+
// Helper function to convert Any nodes to Reference nodes for mixin call arguments
|
|
1377
|
+
const convertArgsForCall = (args: List<Node> | undefined): void => {
|
|
1378
|
+
if (!args || !args.value.length) {
|
|
1379
|
+
return;
|
|
1380
|
+
}
|
|
1381
|
+
|
|
1382
|
+
for (let i = 0; i < args.value.length; i++) {
|
|
1383
|
+
const node = args.value[i]!;
|
|
1384
|
+
|
|
1385
|
+
const location: LocationInfo | undefined = node.location.length ? node.location as LocationInfo : undefined;
|
|
1386
|
+
|
|
1387
|
+
// If it's an Any node with role: 'name', convert it to Reference for mixin call arguments
|
|
1388
|
+
let replacement: Node | undefined;
|
|
1389
|
+
if (isNode(node, N.Any) && node.role === 'name') {
|
|
1390
|
+
replacement = new Reference({ key: node.valueOf() }, { type: 'variable' }, location, $.context);
|
|
1391
|
+
} else if (node instanceof Rest) {
|
|
1392
|
+
const restValue = node.value;
|
|
1393
|
+
if (typeof restValue === 'string') {
|
|
1394
|
+
replacement = new Rest(new Reference({ key: restValue }, { type: 'variable' }, location, $.context), undefined, location, $.context);
|
|
1395
|
+
}
|
|
1396
|
+
}
|
|
1397
|
+
if (replacement) {
|
|
1398
|
+
args.adopt(replacement);
|
|
1399
|
+
args.value[i] = replacement;
|
|
1400
|
+
}
|
|
1401
|
+
}
|
|
1402
|
+
};
|
|
1403
|
+
|
|
1404
|
+
// qualifiedRule
|
|
1405
|
+
// : selectorList WS* LCURLY declarationList RCURLY
|
|
1406
|
+
// ;
|
|
1407
|
+
$.startRule();
|
|
1408
|
+
|
|
1409
|
+
let selector: Selector = $.OR([
|
|
1410
|
+
{
|
|
1411
|
+
GATE: () => !ctx.inner,
|
|
1412
|
+
ALT: () => {
|
|
1413
|
+
let initialQualifiedRule = ctx.qualifiedRule;
|
|
1414
|
+
ctx.qualifiedRule = true;
|
|
1415
|
+
try {
|
|
1416
|
+
return $.SUBRULE($.selectorList, { ARGS: [ctx] });
|
|
1417
|
+
} finally {
|
|
1418
|
+
ctx.qualifiedRule = initialQualifiedRule;
|
|
1419
|
+
}
|
|
1420
|
+
}
|
|
1421
|
+
},
|
|
1422
|
+
{
|
|
1423
|
+
GATE: () => !!ctx.inner,
|
|
1424
|
+
ALT: () => {
|
|
1425
|
+
let initialQualifiedRule = ctx.qualifiedRule;
|
|
1426
|
+
let initialFirstSelector = ctx.firstSelector;
|
|
1427
|
+
ctx.firstSelector = true;
|
|
1428
|
+
ctx.qualifiedRule = true;
|
|
1429
|
+
try {
|
|
1430
|
+
return $.SUBRULE2($.forgivingSelectorList, { ARGS: [ctx] });
|
|
1431
|
+
} finally {
|
|
1432
|
+
ctx.qualifiedRule = initialQualifiedRule;
|
|
1433
|
+
ctx.firstSelector = initialFirstSelector;
|
|
1434
|
+
}
|
|
1435
|
+
}
|
|
1436
|
+
}
|
|
1437
|
+
]);
|
|
1438
|
+
|
|
1439
|
+
let isSelectorList = selector instanceof SelectorList;
|
|
1440
|
+
let guard: Condition | undefined;
|
|
1441
|
+
let args: List<Node> | undefined;
|
|
1442
|
+
let important: IToken | undefined;
|
|
1443
|
+
|
|
1444
|
+
const createMixinCall = (location: LocationInfo) => {
|
|
1445
|
+
let leftNode!: Node;
|
|
1446
|
+
|
|
1447
|
+
if (!isSelectorList && (
|
|
1448
|
+
selector instanceof CompoundSelector
|
|
1449
|
+
|| selector instanceof BasicSelector
|
|
1450
|
+
|| selector instanceof InterpolatedSelector
|
|
1451
|
+
|| selector instanceof ComplexSelector
|
|
1452
|
+
)) {
|
|
1453
|
+
const { key, rawKey } = normalizeMixinReferenceKey(selector);
|
|
1454
|
+
leftNode = new Reference({ key, rawKey }, { type: 'mixin-ruleset', role: 'name' }, undefined, $.context);
|
|
1455
|
+
} else {
|
|
1456
|
+
// For other cases (like SelectorList or when we need nested references),
|
|
1457
|
+
// iterate through selector nodes and create nested references
|
|
1458
|
+
for (let s of selector.nodes()) {
|
|
1459
|
+
if (s instanceof BasicSelector) {
|
|
1460
|
+
const target = leftNode instanceof Reference ? leftNode : leftNode instanceof Call ? leftNode : undefined;
|
|
1461
|
+
leftNode = new Reference({ target, key: s.valueOf() }, { type: 'mixin-ruleset', role: 'name' }, undefined, $.context);
|
|
1462
|
+
}
|
|
1463
|
+
}
|
|
1464
|
+
}
|
|
1465
|
+
|
|
1466
|
+
/** Finally, pass this reference into a call */
|
|
1467
|
+
leftNode = new Call({ name: leftNode, args }, { markImportant: !!important }, location, $.context);
|
|
1468
|
+
return leftNode;
|
|
1469
|
+
};
|
|
1470
|
+
|
|
1471
|
+
let isPossibleMixinDefinition = (selector instanceof BasicSelector && (selector.isClass || selector.isId))
|
|
1472
|
+
|| (selector instanceof InterpolatedSelector && (selector.isClass || selector.isId));
|
|
1473
|
+
let isPossibleMixinCall = true;
|
|
1474
|
+
if (!$.RECORDING_PHASE && !isSelectorList && !isPossibleMixinDefinition) {
|
|
1475
|
+
for (let s of selector.nodes()) {
|
|
1476
|
+
/** Keep going until we get to basic value. */
|
|
1477
|
+
if (s instanceof ComplexSelector || s instanceof CompoundSelector) {
|
|
1478
|
+
continue;
|
|
1479
|
+
}
|
|
1480
|
+
if (
|
|
1481
|
+
(s instanceof BasicSelector && (s.isClass || s.isId))
|
|
1482
|
+
|| (s instanceof InterpolatedSelector && (s.isClass || s.isId))
|
|
1483
|
+
|| (s instanceof Combinator && (s.value === '>' || s.value === ' '))
|
|
1484
|
+
) {
|
|
1485
|
+
continue;
|
|
1486
|
+
}
|
|
1487
|
+
isPossibleMixinCall = false;
|
|
1488
|
+
break;
|
|
1489
|
+
}
|
|
1490
|
+
}
|
|
1491
|
+
return $.OR2([
|
|
1492
|
+
{
|
|
1493
|
+
GATE: () => (isPossibleMixinDefinition || isPossibleMixinCall) && $.isType(T.LParen),
|
|
1494
|
+
ALT: () => {
|
|
1495
|
+
args = $.SUBRULE3($.mixinArgs, { ARGS: [ctx] });
|
|
1496
|
+
let next = $.LA(1).tokenType;
|
|
1497
|
+
if (next === T.LCurly || next === T.When) {
|
|
1498
|
+
isPossibleMixinCall = false;
|
|
1499
|
+
}
|
|
1500
|
+
return $.OR3([
|
|
1501
|
+
{
|
|
1502
|
+
GATE: () => isPossibleMixinDefinition,
|
|
1503
|
+
/** Mixin definition */
|
|
1504
|
+
ALT: () => {
|
|
1505
|
+
$.OPTION(() => {
|
|
1506
|
+
guard = $.SUBRULE4($.guard, { ARGS: [ctx] });
|
|
1507
|
+
});
|
|
1508
|
+
$.CONSUME(T.LCurly);
|
|
1509
|
+
|
|
1510
|
+
let rules = $.SUBRULE5($.declarationList, { ARGS: [ctx] });
|
|
1511
|
+
$.CONSUME(T.RCurly);
|
|
1512
|
+
if (!$.RECORDING_PHASE) {
|
|
1513
|
+
// Convert Any nodes to VarDeclaration nodes for mixin definition parameters
|
|
1514
|
+
convertArgsForDefinition(args);
|
|
1515
|
+
const guardText = String(guard?.toString?.() ?? '');
|
|
1516
|
+
const hasDefault = Boolean(ctx.hasDefault) || guardContainsDefaultCall(guard) || guardText.includes('??()');
|
|
1517
|
+
const node = new Mixin(
|
|
1518
|
+
{ name: selector.valueOf(), params: args, rules: rules.rules, guard },
|
|
1519
|
+
guard && hasDefault ? { hasDefault } : undefined,
|
|
1520
|
+
$.endRule(),
|
|
1521
|
+
$.context
|
|
1522
|
+
);
|
|
1523
|
+
ctx.hasDefault = false;
|
|
1524
|
+
return node;
|
|
1525
|
+
}
|
|
1526
|
+
$.endRule();
|
|
1527
|
+
}
|
|
1528
|
+
},
|
|
1529
|
+
{
|
|
1530
|
+
GATE: () => isPossibleMixinCall,
|
|
1531
|
+
/** Mixin call */
|
|
1532
|
+
ALT: () => {
|
|
1533
|
+
$.OPTION2(() => {
|
|
1534
|
+
important = $.CONSUME(T.Important);
|
|
1535
|
+
});
|
|
1536
|
+
let location: LocationInfo = $.endRule();
|
|
1537
|
+
if (!$.RECORDING_PHASE) {
|
|
1538
|
+
// Convert Any nodes to Reference nodes for mixin call arguments
|
|
1539
|
+
convertArgsForCall(args);
|
|
1540
|
+
}
|
|
1541
|
+
let result: Node | undefined;
|
|
1542
|
+
{
|
|
1543
|
+
/** in Less legacy mode, mixin calls can happen without a space. */
|
|
1544
|
+
let noSpace = $.noSep();
|
|
1545
|
+
let next = $.LA(1).tokenType;
|
|
1546
|
+
if ((noSpace && next === T.LSquare) || ((noSpace || $.looseMode) && next === T.LParen)) {
|
|
1547
|
+
result = $.OPTION3(() => $.SUBRULE6($.lookupOrCall, { ARGS: [{ ...ctx, node: $.RECORDING_PHASE ? undefined! : createMixinCall(location) }] }));
|
|
1548
|
+
}
|
|
1549
|
+
}
|
|
1550
|
+
// Note: Mixin calls without parentheses are handled in the semicolon-terminated ALT below
|
|
1551
|
+
return $.RECORDING_PHASE ? undefined : (result ?? createMixinCall(location!));
|
|
1552
|
+
}
|
|
1553
|
+
}
|
|
1554
|
+
]);
|
|
1555
|
+
}
|
|
1556
|
+
},
|
|
1557
|
+
{
|
|
1558
|
+
GATE: () => isPossibleMixinCall && $.isType(T.Semi),
|
|
1559
|
+
ALT: () => {
|
|
1560
|
+
// Call terminated by a semi-colon and not parens, deprecated
|
|
1561
|
+
const semi = $.CONSUME(T.Semi);
|
|
1562
|
+
const location = $.endRule();
|
|
1563
|
+
if (!$.RECORDING_PHASE) {
|
|
1564
|
+
// Mixin call without parentheses - deprecated
|
|
1565
|
+
$.warnDeprecation(
|
|
1566
|
+
'Calling a mixin without parentheses is deprecated',
|
|
1567
|
+
semi,
|
|
1568
|
+
'mixin-call-no-parens'
|
|
1569
|
+
);
|
|
1570
|
+
return createMixinCall(location);
|
|
1571
|
+
}
|
|
1572
|
+
}
|
|
1573
|
+
},
|
|
1574
|
+
{
|
|
1575
|
+
/** Parse as qualified rule */
|
|
1576
|
+
ALT: () => {
|
|
1577
|
+
$.endRule();
|
|
1578
|
+
let initialSelector = ctx.selector;
|
|
1579
|
+
let initialIsSelectorList = ctx.isSelectorList;
|
|
1580
|
+
ctx.selector = selector;
|
|
1581
|
+
ctx.isSelectorList = isSelectorList;
|
|
1582
|
+
let rule: Node;
|
|
1583
|
+
try {
|
|
1584
|
+
rule = $.SUBRULE7($.qualifiedRuleBody, { ARGS: [ctx] });
|
|
1585
|
+
} finally {
|
|
1586
|
+
ctx.selector = initialSelector;
|
|
1587
|
+
ctx.isSelectorList = initialIsSelectorList;
|
|
1588
|
+
}
|
|
1589
|
+
if (ctx.extendNodes) {
|
|
1590
|
+
/** Prepend a rules block */
|
|
1591
|
+
let qRule = rule;
|
|
1592
|
+
// Set the Extend nodes' selector to the ruleset's selector (not &)
|
|
1593
|
+
// This allows the extends to work correctly when evaluated in the wrapper Rules context
|
|
1594
|
+
for (const extendNode of ctx.extendNodes) {
|
|
1595
|
+
const { selector: extendSelector } = extendNode;
|
|
1596
|
+
if (extendSelector === undefined || extendSelector instanceof Ampersand) {
|
|
1597
|
+
const index = ctx.extendNodes.indexOf(extendNode);
|
|
1598
|
+
ctx.extendNodes[index] = extendWithSelector(extendNode, selector, $.context);
|
|
1599
|
+
}
|
|
1600
|
+
}
|
|
1601
|
+
rule = new Rules([
|
|
1602
|
+
...ctx.extendNodes,
|
|
1603
|
+
qRule
|
|
1604
|
+
]);
|
|
1605
|
+
rule._location = qRule._location;
|
|
1606
|
+
ctx.extendNodes = undefined;
|
|
1607
|
+
}
|
|
1608
|
+
return rule;
|
|
1609
|
+
}
|
|
1610
|
+
}
|
|
1611
|
+
]);
|
|
1612
|
+
};
|
|
1613
|
+
}
|