@jesscss/less-parser 2.0.0-alpha.5 → 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 +18 -0
- package/lib/index.d.ts +9 -32
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +7 -75
- 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 +4 -3
- 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 +8 -1
- package/lib/utils.d.ts.map +1 -0
- package/package.json +49 -16
- package/{lib/__tests__/debug-log.js → src/__tests__/debug-log.ts} +20 -19
- 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/__tests__/debug-log.d.ts +0 -1
- package/lib/__tests__/debug-log.js.map +0 -1
- package/lib/index.js.map +0 -1
- package/lib/lessActionsParser.d.ts +0 -156
- package/lib/lessActionsParser.js +0 -145
- package/lib/lessActionsParser.js.map +0 -1
- package/lib/lessErrorMessageProvider.d.ts +0 -3
- package/lib/lessErrorMessageProvider.js +0 -4
- package/lib/lessErrorMessageProvider.js.map +0 -1
- package/lib/lessTokens.js +0 -249
- package/lib/lessTokens.js.map +0 -1
- package/lib/productions.d.ts +0 -181
- package/lib/productions.js +0 -3521
- package/lib/productions.js.map +0 -1
- package/lib/utils.js +0 -88
- package/lib/utils.js.map +0 -1
|
@@ -0,0 +1,1309 @@
|
|
|
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
|
+
// Selector-related production rules for LessRecursiveParser
|
|
7
|
+
// Converted from Chevrotain-based productions.ts lines 1145-2060
|
|
8
|
+
|
|
9
|
+
import type { RuleContext, ExtendTarget, TokenMap } from '../lessRecursiveParser.js';
|
|
10
|
+
import type { IToken, IOrAlt } from 'chevrotain';
|
|
11
|
+
import {
|
|
12
|
+
Node,
|
|
13
|
+
Ampersand,
|
|
14
|
+
Block,
|
|
15
|
+
Any,
|
|
16
|
+
type LocationInfo,
|
|
17
|
+
type TreeContext,
|
|
18
|
+
BasicSelector,
|
|
19
|
+
Combinator,
|
|
20
|
+
type Combinators,
|
|
21
|
+
List,
|
|
22
|
+
Sequence,
|
|
23
|
+
Call,
|
|
24
|
+
Quoted,
|
|
25
|
+
AtRule,
|
|
26
|
+
AtRuleStatement,
|
|
27
|
+
Interpolated,
|
|
28
|
+
InterpolatedSelector,
|
|
29
|
+
AttributeSelector,
|
|
30
|
+
Reference,
|
|
31
|
+
Extend,
|
|
32
|
+
Mixin,
|
|
33
|
+
VarDeclaration,
|
|
34
|
+
Declaration,
|
|
35
|
+
Expression,
|
|
36
|
+
SelectorCapture,
|
|
37
|
+
ComplexSelector,
|
|
38
|
+
CompoundSelector,
|
|
39
|
+
Selector,
|
|
40
|
+
SelectorList,
|
|
41
|
+
Url,
|
|
42
|
+
Nil,
|
|
43
|
+
Collection,
|
|
44
|
+
type ComplexSelectorValue,
|
|
45
|
+
type ComplexSelectorComponent,
|
|
46
|
+
type SimpleSelector,
|
|
47
|
+
isNode,
|
|
48
|
+
N,
|
|
49
|
+
StyleImport
|
|
50
|
+
} from '@jesscss/core';
|
|
51
|
+
|
|
52
|
+
import { createInterpolatedReference, getInterpolatedNode, getInterpolatedOrString } from '../utils.js';
|
|
53
|
+
import { all } from 'known-css-properties';
|
|
54
|
+
|
|
55
|
+
/** Use `any` for `this` to avoid structural incompatibility between LessRecursiveParser and CssRecursiveParser */
|
|
56
|
+
type P = any;
|
|
57
|
+
type Alt = IOrAlt<any>[];
|
|
58
|
+
type AltContext = (ctx?: RuleContext) => Alt;
|
|
59
|
+
type ProductionRule = (...args: any[]) => any;
|
|
60
|
+
const COMBINATORS = new Set<string>([' ', '>', '+', '~', '|', '||']);
|
|
61
|
+
|
|
62
|
+
function toCombinator(image: string): Combinators {
|
|
63
|
+
if (COMBINATORS.has(image)) {
|
|
64
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
|
65
|
+
return image as Combinators;
|
|
66
|
+
}
|
|
67
|
+
throw new Error(`Unexpected selector combinator "${image}".`);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
type ComplexSelectorComponentNode = Exclude<ComplexSelectorComponent, string>;
|
|
71
|
+
|
|
72
|
+
function isComplexSelectorComponentNode(node: Node | undefined): node is ComplexSelectorComponentNode {
|
|
73
|
+
return node instanceof Call
|
|
74
|
+
|| (node instanceof Selector
|
|
75
|
+
&& !(node instanceof SelectorList)
|
|
76
|
+
&& !(node instanceof ComplexSelector));
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function attributeSelector(this: P, T: TokenMap, valueAlt?: AltContext) {
|
|
80
|
+
const $ = this;
|
|
81
|
+
|
|
82
|
+
valueAlt ??= () => [
|
|
83
|
+
{
|
|
84
|
+
GATE: () => !$.isType(T.InterpolatedIdent),
|
|
85
|
+
ALT: () => {
|
|
86
|
+
const token = $.CONSUME5(T.Ident);
|
|
87
|
+
if ($.RECORDING_PHASE) {
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
return new Any(token.image, { role: 'ident' }, $.getLocationInfo(token), $.context);
|
|
91
|
+
}
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
GATE: () => $.isType(T.InterpolatedIdent),
|
|
95
|
+
ALT: () => {
|
|
96
|
+
const token = $.CONSUME(T.InterpolatedIdent);
|
|
97
|
+
if ($.RECORDING_PHASE) {
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
const match = /([$@])\{([^}]+)\}/.exec(token.image);
|
|
101
|
+
if (match && match[0] === token.image) {
|
|
102
|
+
return createInterpolatedReference(
|
|
103
|
+
match[1]!,
|
|
104
|
+
match[2]!,
|
|
105
|
+
$.getLocationInfo(token),
|
|
106
|
+
$.context
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
const result = getInterpolatedOrString(token.image, $.getLocationInfo(token), $.context);
|
|
110
|
+
return typeof result === 'string'
|
|
111
|
+
? new Any(result, { role: 'ident' }, $.getLocationInfo(token), $.context)
|
|
112
|
+
: result;
|
|
113
|
+
}
|
|
114
|
+
},
|
|
115
|
+
{ ALT: () => $.SUBRULE($.string) }
|
|
116
|
+
];
|
|
117
|
+
|
|
118
|
+
return (ctx: RuleContext = {}) => {
|
|
119
|
+
const RECORDING_PHASE = $.RECORDING_PHASE;
|
|
120
|
+
$.startRule();
|
|
121
|
+
|
|
122
|
+
$.CONSUME2(T.LSquare);
|
|
123
|
+
const key: Any = $.SUBRULE2($.attributeName);
|
|
124
|
+
let op: IToken | undefined;
|
|
125
|
+
let value: Node | undefined;
|
|
126
|
+
let mod: IToken | undefined;
|
|
127
|
+
$.OPTION(() => {
|
|
128
|
+
op = $.OR([
|
|
129
|
+
{ ALT: () => $.CONSUME4(T.Eq) },
|
|
130
|
+
{ ALT: () => $.CONSUME6(T.AttrMatch) }
|
|
131
|
+
]);
|
|
132
|
+
value = $.OR2(valueAlt!(ctx));
|
|
133
|
+
});
|
|
134
|
+
$.OPTION2(() => mod = $.CONSUME7(T.AttrFlag));
|
|
135
|
+
$.CONSUME8(T.RSquare);
|
|
136
|
+
|
|
137
|
+
if (!RECORDING_PHASE) {
|
|
138
|
+
const location = $.endRule();
|
|
139
|
+
return new AttributeSelector({
|
|
140
|
+
name: key.valueOf(),
|
|
141
|
+
op: op?.image,
|
|
142
|
+
value,
|
|
143
|
+
mod: mod?.image
|
|
144
|
+
}, undefined, location, $.context);
|
|
145
|
+
}
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// ── Helper: getAmpersandTemplateValue ────────────────────────────────
|
|
150
|
+
|
|
151
|
+
function getAmpersandTemplateValue(image: string): string | undefined {
|
|
152
|
+
if (image === '&') {
|
|
153
|
+
return undefined;
|
|
154
|
+
}
|
|
155
|
+
if (image.startsWith('&')) {
|
|
156
|
+
return image.slice(1) || undefined;
|
|
157
|
+
}
|
|
158
|
+
if (image.includes('&')) {
|
|
159
|
+
return image;
|
|
160
|
+
}
|
|
161
|
+
return undefined;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// ── Helper: wrapOuterExpressionIfNeeded ──────────────────────────────
|
|
165
|
+
// Imported from root.ts where it's defined.
|
|
166
|
+
import { wrapOuterExpressionIfNeeded } from './root.js';
|
|
167
|
+
|
|
168
|
+
// ── Helper: groupExtendsByTargetAndFlag ──────────────────────────────
|
|
169
|
+
|
|
170
|
+
const { isArray } = Array;
|
|
171
|
+
type ExtendSelectorKind = 'simple' | 'basic' | 'pseudo' | 'complex' | 'compound';
|
|
172
|
+
|
|
173
|
+
function getAllowedExtendSelectors(context: TreeContext): ExtendSelectorKind[] | undefined {
|
|
174
|
+
const val: ExtendSelectorKind[] | undefined = context.opts.allowExtendSelectors;
|
|
175
|
+
return val;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function findDisallowedExtendSelector(selector: Selector | string, allowed?: readonly ExtendSelectorKind[]): { kind: ExtendSelectorKind; selector: Selector } | undefined {
|
|
179
|
+
// A bare-string selector (strings-not-nodes) is a plain simple selector — not a
|
|
180
|
+
// disallowed extend target.
|
|
181
|
+
if (typeof selector === 'string') {
|
|
182
|
+
return undefined;
|
|
183
|
+
}
|
|
184
|
+
if (!allowed) {
|
|
185
|
+
return undefined;
|
|
186
|
+
}
|
|
187
|
+
if (isNode(selector, N.SelectorList)) {
|
|
188
|
+
for (const item of selector.value) {
|
|
189
|
+
const disallowed = findDisallowedExtendSelector(item, allowed);
|
|
190
|
+
if (disallowed) {
|
|
191
|
+
return disallowed;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
return undefined;
|
|
195
|
+
}
|
|
196
|
+
const kinds: ExtendSelectorKind[] = isNode(selector, N.BasicSelector)
|
|
197
|
+
? ['simple', 'basic']
|
|
198
|
+
: isNode(selector, N.PseudoSelector)
|
|
199
|
+
? ['simple', 'pseudo']
|
|
200
|
+
: isNode(selector, N.CompoundSelector)
|
|
201
|
+
? ['compound']
|
|
202
|
+
: isNode(selector, N.ComplexSelector)
|
|
203
|
+
? ['complex']
|
|
204
|
+
: ['simple'];
|
|
205
|
+
if (kinds.some(kind => allowed.includes(kind))) {
|
|
206
|
+
return undefined;
|
|
207
|
+
}
|
|
208
|
+
return {
|
|
209
|
+
kind: kinds[0]!,
|
|
210
|
+
selector
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function formatAllowedExtendSelectors(allowed: readonly ExtendSelectorKind[]) {
|
|
215
|
+
if (allowed.length === 0) {
|
|
216
|
+
return 'no selector kinds';
|
|
217
|
+
}
|
|
218
|
+
if (allowed.length === 1) {
|
|
219
|
+
return `${allowed[0]} value`;
|
|
220
|
+
}
|
|
221
|
+
const head = allowed.slice(0, -1).join(', ');
|
|
222
|
+
return `${head}, or ${allowed[allowed.length - 1]} value`;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function validateExtendTarget($: P, selector: Selector, source: ':extend()' | '&:extend()') {
|
|
226
|
+
const allowed = getAllowedExtendSelectors($.context);
|
|
227
|
+
const disallowed = findDisallowedExtendSelector(selector, allowed);
|
|
228
|
+
if (!disallowed || !allowed) {
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
throw new Error(
|
|
232
|
+
`${source} only allows ${formatAllowedExtendSelectors(allowed)}, but found ${disallowed.kind} selector "${disallowed.selector.valueOf()}".`
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Groups extends by target (using valueOf()) and flag.
|
|
238
|
+
* Returns an array of grouped Extend nodes where extends with the same target and flag
|
|
239
|
+
* are combined into a single Extend node with a SelectorList of all matching value.
|
|
240
|
+
*
|
|
241
|
+
* @todo Group complex value into selector lists
|
|
242
|
+
*/
|
|
243
|
+
function groupExtendsByTargetAndFlag(
|
|
244
|
+
extendNodes: Extend[]
|
|
245
|
+
): Array<Extend | Extend[]> {
|
|
246
|
+
// Group extends by target and flag
|
|
247
|
+
const groups = new Map<string, Extend | Extend[]>();
|
|
248
|
+
|
|
249
|
+
for (const ext of extendNodes) {
|
|
250
|
+
const { target, flag = 1 } = ext; // ExtendFlag.Exact = 1
|
|
251
|
+
// Create a key from target valueOf() and flag
|
|
252
|
+
const key = `${target.valueOf()}|${flag}`;
|
|
253
|
+
|
|
254
|
+
let group = groups.get(key);
|
|
255
|
+
if (!group) {
|
|
256
|
+
groups.set(key, ext);
|
|
257
|
+
} else if (isArray(group)) {
|
|
258
|
+
group.push(ext);
|
|
259
|
+
} else {
|
|
260
|
+
groups.set(key, [group, ext]);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
return Array.from(groups.values());
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// ── Helper: mergeExtends ─────────────────────────────────────────────
|
|
268
|
+
|
|
269
|
+
function mergeExtends(
|
|
270
|
+
selector: Selector | undefined,
|
|
271
|
+
extendTargets: ExtendTarget[],
|
|
272
|
+
location: LocationInfo,
|
|
273
|
+
context: TreeContext,
|
|
274
|
+
flag: IToken | undefined
|
|
275
|
+
): Extend | Extend[] {
|
|
276
|
+
let extendNodes: Extend[] | undefined;
|
|
277
|
+
let currentTarget = extendTargets[0]!.target;
|
|
278
|
+
let currentFlag = (extendTargets[0]!.flag ?? flag) ? 0 : 1; // ExtendFlag.All = 0, ExtendFlag.Exact = 1
|
|
279
|
+
let currentNode = new Extend({
|
|
280
|
+
selector,
|
|
281
|
+
target: currentTarget,
|
|
282
|
+
flag: currentFlag
|
|
283
|
+
}, undefined, location, context);
|
|
284
|
+
const flushCurrent = (): void => {
|
|
285
|
+
(extendNodes ??= []).push(currentNode);
|
|
286
|
+
};
|
|
287
|
+
for (let i = 1; i < extendTargets.length; i++) {
|
|
288
|
+
let ext = extendTargets[i]!;
|
|
289
|
+
let thisFlag = (ext.flag ?? flag) ? 0 : 1;
|
|
290
|
+
/**
|
|
291
|
+
* Merge extends. We do this instead of merging earlier so that
|
|
292
|
+
* selector lists with different flags are not merged.
|
|
293
|
+
*/
|
|
294
|
+
if (thisFlag === currentFlag) {
|
|
295
|
+
const { target } = currentNode;
|
|
296
|
+
let nextTarget: Selector;
|
|
297
|
+
if (!(target instanceof SelectorList)) {
|
|
298
|
+
nextTarget = new SelectorList([target, ext.target], undefined, location, context);
|
|
299
|
+
} else {
|
|
300
|
+
nextTarget = new SelectorList([...target.value, ext.target], undefined, location, context);
|
|
301
|
+
}
|
|
302
|
+
currentNode = new Extend({
|
|
303
|
+
selector,
|
|
304
|
+
target: nextTarget,
|
|
305
|
+
flag: currentFlag
|
|
306
|
+
}, undefined, location, context);
|
|
307
|
+
} else {
|
|
308
|
+
flushCurrent();
|
|
309
|
+
currentFlag = thisFlag;
|
|
310
|
+
currentTarget = ext.target;
|
|
311
|
+
currentNode = new Extend({
|
|
312
|
+
selector,
|
|
313
|
+
target: currentTarget,
|
|
314
|
+
flag: currentFlag
|
|
315
|
+
}, undefined, location, context);
|
|
316
|
+
}
|
|
317
|
+
};
|
|
318
|
+
if (!extendNodes) {
|
|
319
|
+
return currentNode;
|
|
320
|
+
}
|
|
321
|
+
flushCurrent();
|
|
322
|
+
if (extendNodes.length === 1) {
|
|
323
|
+
return extendNodes[0]!;
|
|
324
|
+
}
|
|
325
|
+
return extendNodes;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
function extendWithSelector(node: Extend, selector: Selector | undefined, location: LocationInfo | undefined, context: TreeContext): Extend {
|
|
329
|
+
return new Extend({
|
|
330
|
+
selector,
|
|
331
|
+
target: node.target,
|
|
332
|
+
namespace: node.namespace,
|
|
333
|
+
flag: node.flag
|
|
334
|
+
}, undefined, location, context);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// ── Helper: isSelectorLikeListItem / isLegacySelectorLikeValue ───────
|
|
338
|
+
|
|
339
|
+
/** True for a node that could be one item in the old unquoted selector list (e.g. .a or #id). */
|
|
340
|
+
function isSelectorLikeListItem(node: Node): boolean {
|
|
341
|
+
if (node.type === 'SelectorCapture' || isNode(node, N.Call)) {
|
|
342
|
+
return false;
|
|
343
|
+
}
|
|
344
|
+
if (isNode(node, N.Reference)) {
|
|
345
|
+
return node.options.type === 'mixin-ruleset';
|
|
346
|
+
}
|
|
347
|
+
if (node instanceof List) {
|
|
348
|
+
return node.value.length > 0 && node.value.every(isSelectorLikeListItem);
|
|
349
|
+
}
|
|
350
|
+
if (node instanceof Sequence) {
|
|
351
|
+
return node.value.length > 0 && node.value.every(isSelectorLikeListItem);
|
|
352
|
+
}
|
|
353
|
+
return false;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/** True only for the legacy unquoted selector-list form (e.g. @var: .a, .b, .c), not @var: .a; */
|
|
357
|
+
function isLegacySelectorLikeValue(node: Node): boolean {
|
|
358
|
+
if (node.type === 'SelectorCapture' || isNode(node, N.Call)) {
|
|
359
|
+
return false;
|
|
360
|
+
}
|
|
361
|
+
if (isNode(node, N.Reference)) {
|
|
362
|
+
return false;
|
|
363
|
+
}
|
|
364
|
+
if (node instanceof List) {
|
|
365
|
+
return node.value.length > 1 && node.value.every(isSelectorLikeListItem);
|
|
366
|
+
}
|
|
367
|
+
if (node instanceof Sequence) {
|
|
368
|
+
return node.value.length > 1 && node.value.every(isSelectorLikeListItem);
|
|
369
|
+
}
|
|
370
|
+
return false;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// ══════════════════════════════════════════════════════════════════════
|
|
374
|
+
// PRODUCTION RULES
|
|
375
|
+
// ══════════════════════════════════════════════════════════════════════
|
|
376
|
+
|
|
377
|
+
/**
|
|
378
|
+
* We need to now handle a returned `Extend` node from the complexSelector rule
|
|
379
|
+
*/
|
|
380
|
+
export function relativeSelector(this: P, T: TokenMap) {
|
|
381
|
+
const $ = this;
|
|
382
|
+
return (ctx: RuleContext = {}) => {
|
|
383
|
+
return $.OR([
|
|
384
|
+
{
|
|
385
|
+
ALT: () => {
|
|
386
|
+
let co = $.CONSUME(T.Combinator);
|
|
387
|
+
let node: ComplexSelector | Extend = $.SUBRULE2($.complexSelector, { ARGS: [ctx] });
|
|
388
|
+
if ($.RECORDING_PHASE) {
|
|
389
|
+
return node;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
let combinator = new Combinator(toCombinator(co.image), undefined, $.getLocationInfo(co), $.context);
|
|
393
|
+
let targetNode =
|
|
394
|
+
node instanceof Extend
|
|
395
|
+
? node.selector
|
|
396
|
+
: node;
|
|
397
|
+
if (targetNode instanceof ComplexSelector) {
|
|
398
|
+
const nodes = [combinator, ...targetNode.value] as ComplexSelectorValue;
|
|
399
|
+
const complex = new ComplexSelector(nodes, undefined, $.getLocationFromNodes(nodes), $.context);
|
|
400
|
+
if (node instanceof Extend) {
|
|
401
|
+
const location = node.location.length === 6
|
|
402
|
+
? ([...node.location] as LocationInfo)
|
|
403
|
+
: undefined;
|
|
404
|
+
if (location) {
|
|
405
|
+
location[0] = co.startOffset!;
|
|
406
|
+
location[1] = co.startLine!;
|
|
407
|
+
location[2] = co.startColumn!;
|
|
408
|
+
}
|
|
409
|
+
node = extendWithSelector(node, complex, location, $.context);
|
|
410
|
+
} else {
|
|
411
|
+
node = complex;
|
|
412
|
+
}
|
|
413
|
+
} else {
|
|
414
|
+
if (!isComplexSelectorComponentNode(targetNode)) {
|
|
415
|
+
const targetType = typeof targetNode === 'string' ? 'string' : (targetNode as Node | undefined)?.type ?? 'none';
|
|
416
|
+
throw new Error(`Expected selector component after relative combinator; got ${targetType}.`);
|
|
417
|
+
}
|
|
418
|
+
let nodes = [combinator, targetNode];
|
|
419
|
+
let complex = new ComplexSelector(nodes, undefined, $.getLocationFromNodes(nodes), $.context);
|
|
420
|
+
if (node instanceof Extend) {
|
|
421
|
+
const location = node.location.length === 6
|
|
422
|
+
? ([...node.location] as LocationInfo)
|
|
423
|
+
: undefined;
|
|
424
|
+
if (location) {
|
|
425
|
+
location[0] = co.startOffset!;
|
|
426
|
+
location[1] = co.startLine!;
|
|
427
|
+
location[2] = co.startColumn!;
|
|
428
|
+
}
|
|
429
|
+
node = extendWithSelector(node, complex, location, $.context);
|
|
430
|
+
} else {
|
|
431
|
+
node = complex;
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
return node;
|
|
435
|
+
}
|
|
436
|
+
},
|
|
437
|
+
{
|
|
438
|
+
ALT: () => $.SUBRULE3($.complexSelector, { ARGS: [ctx] })
|
|
439
|
+
}
|
|
440
|
+
]);
|
|
441
|
+
};
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
export function forgivingSelectorList(this: P, T: TokenMap) {
|
|
445
|
+
const $ = this;
|
|
446
|
+
return (ctx: RuleContext = {}) => {
|
|
447
|
+
const RECORDING_PHASE = $.RECORDING_PHASE;
|
|
448
|
+
$.startRule();
|
|
449
|
+
|
|
450
|
+
let sequences: ComplexSelector[] | undefined;
|
|
451
|
+
let i = 0;
|
|
452
|
+
|
|
453
|
+
if (!RECORDING_PHASE) {
|
|
454
|
+
sequences = [];
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
$.AT_LEAST_ONE_SEP({
|
|
458
|
+
SEP: T.Comma,
|
|
459
|
+
DEF: () => {
|
|
460
|
+
const selector = $.SUBRULE($.relativeSelector, { ARGS: [ctx] });
|
|
461
|
+
if (!RECORDING_PHASE) {
|
|
462
|
+
i++;
|
|
463
|
+
if (i === 1 && ctx.qualifiedRule) {
|
|
464
|
+
sequences!.push(selector);
|
|
465
|
+
} else {
|
|
466
|
+
sequences!.push(selector);
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
});
|
|
471
|
+
|
|
472
|
+
if (RECORDING_PHASE) {
|
|
473
|
+
return;
|
|
474
|
+
}
|
|
475
|
+
const location = $.endRule();
|
|
476
|
+
if (sequences!.length === 1) {
|
|
477
|
+
return sequences![0];
|
|
478
|
+
}
|
|
479
|
+
return new SelectorList(sequences!, undefined, location, $.context);
|
|
480
|
+
};
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
export function selectorList(this: P, T: TokenMap) {
|
|
484
|
+
const $ = this;
|
|
485
|
+
return (ctx: RuleContext = {}) => {
|
|
486
|
+
const RECORDING_PHASE = $.RECORDING_PHASE;
|
|
487
|
+
$.startRule();
|
|
488
|
+
|
|
489
|
+
let sequences: ComplexSelector[] | undefined;
|
|
490
|
+
let i = 0;
|
|
491
|
+
|
|
492
|
+
if (!RECORDING_PHASE) {
|
|
493
|
+
sequences = [];
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
$.AT_LEAST_ONE_SEP({
|
|
497
|
+
SEP: T.Comma,
|
|
498
|
+
DEF: () => {
|
|
499
|
+
const selector = $.SUBRULE2($.complexSelector, { ARGS: [ctx] });
|
|
500
|
+
if (!RECORDING_PHASE) {
|
|
501
|
+
i++;
|
|
502
|
+
if (i === 1 && ctx.qualifiedRule) {
|
|
503
|
+
sequences!.push(selector);
|
|
504
|
+
} else {
|
|
505
|
+
sequences!.push(selector);
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
});
|
|
510
|
+
|
|
511
|
+
if (RECORDING_PHASE) {
|
|
512
|
+
return;
|
|
513
|
+
}
|
|
514
|
+
const location = $.endRule();
|
|
515
|
+
if (sequences!.length === 1) {
|
|
516
|
+
return sequences![0];
|
|
517
|
+
}
|
|
518
|
+
return new SelectorList(sequences!, undefined, location, $.context);
|
|
519
|
+
};
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
export function compoundSelector(this: P, T: TokenMap): ProductionRule {
|
|
523
|
+
const $ = this;
|
|
524
|
+
return (ctx: RuleContext = {}) => {
|
|
525
|
+
/**
|
|
526
|
+
A sequence of simple value that are not separated by
|
|
527
|
+
a combinator.
|
|
528
|
+
.e.g. `a#selected`
|
|
529
|
+
*/
|
|
530
|
+
// compoundSelector
|
|
531
|
+
// : simpleSelector+
|
|
532
|
+
// ;
|
|
533
|
+
let RECORDING_PHASE = $.RECORDING_PHASE;
|
|
534
|
+
let value: SimpleSelector[];
|
|
535
|
+
if (!RECORDING_PHASE) {
|
|
536
|
+
value = [];
|
|
537
|
+
}
|
|
538
|
+
let sel: SimpleSelector = $.SUBRULE($.simpleSelector, { ARGS: [ctx] });
|
|
539
|
+
if (!RECORDING_PHASE) {
|
|
540
|
+
value!.push(sel);
|
|
541
|
+
}
|
|
542
|
+
$.MANY({
|
|
543
|
+
/** Make sure we don't ignore space combinators */
|
|
544
|
+
GATE: () => !$.hasWS() && !(ctx.inExtend && $.isType(T.All)),
|
|
545
|
+
DEF: () => {
|
|
546
|
+
let sel: SimpleSelector = $.SUBRULE2($.simpleSelector, { ARGS: [ctx] });
|
|
547
|
+
if (!RECORDING_PHASE) {
|
|
548
|
+
value!.push(sel);
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
});
|
|
552
|
+
if (RECORDING_PHASE) {
|
|
553
|
+
return;
|
|
554
|
+
}
|
|
555
|
+
if (value!.length === 1) {
|
|
556
|
+
return value![0]!;
|
|
557
|
+
}
|
|
558
|
+
return new CompoundSelector(value!, undefined, $.getLocationFromNodes(value!), $.context);
|
|
559
|
+
};
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
/**
|
|
563
|
+
* Extended with :extend
|
|
564
|
+
*/
|
|
565
|
+
export function complexSelector(this: P, T: TokenMap): ProductionRule {
|
|
566
|
+
const $ = this;
|
|
567
|
+
return (ctx: RuleContext = {}) => {
|
|
568
|
+
const RECORDING_PHASE = $.RECORDING_PHASE;
|
|
569
|
+
$.startRule();
|
|
570
|
+
|
|
571
|
+
let value: ComplexSelectorValue | undefined;
|
|
572
|
+
if (!RECORDING_PHASE) {
|
|
573
|
+
const first: ComplexSelectorComponent = $.SUBRULE($.compoundSelector, { ARGS: [ctx] });
|
|
574
|
+
value = [first];
|
|
575
|
+
} else {
|
|
576
|
+
$.SUBRULE($.compoundSelector, { ARGS: [ctx] });
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
$.MANY({
|
|
580
|
+
GATE: () => {
|
|
581
|
+
if (ctx.inExtend && $.isType(T.All)) {
|
|
582
|
+
return false;
|
|
583
|
+
}
|
|
584
|
+
return $.hasWS() || $.isType(T.Combinator);
|
|
585
|
+
},
|
|
586
|
+
DEF: () => {
|
|
587
|
+
let co: IToken | undefined;
|
|
588
|
+
let combinator: Combinator | undefined;
|
|
589
|
+
|
|
590
|
+
$.OPTION(() => {
|
|
591
|
+
co = $.CONSUME(T.Combinator);
|
|
592
|
+
});
|
|
593
|
+
|
|
594
|
+
if (!RECORDING_PHASE) {
|
|
595
|
+
if (co) {
|
|
596
|
+
const coImg = toCombinator(co.image);
|
|
597
|
+
combinator = new Combinator(coImg, undefined, $.getLocationInfo(co), $.context);
|
|
598
|
+
} else {
|
|
599
|
+
const ws = $.claimSpaceCombinator($.LA(1).startOffset);
|
|
600
|
+
combinator = new Combinator(' ', undefined, ws ? $.getLocationInfo(ws) : undefined, $.context);
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
const compound: ComplexSelectorComponent = $.SUBRULE2($.compoundSelector, { ARGS: [ctx] });
|
|
605
|
+
if (!RECORDING_PHASE) {
|
|
606
|
+
value!.push(combinator!, compound);
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
});
|
|
610
|
+
|
|
611
|
+
let selector: Selector | undefined;
|
|
612
|
+
if (!RECORDING_PHASE) {
|
|
613
|
+
const location = $.endRule();
|
|
614
|
+
selector = value!.length === 1
|
|
615
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
|
616
|
+
? value![0] as Selector | undefined
|
|
617
|
+
: new ComplexSelector(value!, undefined, location, $.context);
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
let flag: IToken | undefined;
|
|
621
|
+
|
|
622
|
+
/** Inside :extend(...), only consume the optional trailing "all" keyword. */
|
|
623
|
+
$.OPTION2({
|
|
624
|
+
GATE: () => !!ctx.inExtend && $.isType(T.All),
|
|
625
|
+
DEF: () => {
|
|
626
|
+
flag = $.CONSUME(T.All);
|
|
627
|
+
}
|
|
628
|
+
});
|
|
629
|
+
|
|
630
|
+
/**
|
|
631
|
+
* Outside :extend(...), only enter the extend production when the next token
|
|
632
|
+
* is actually :extend(. Do not commit based on context alone.
|
|
633
|
+
*/
|
|
634
|
+
$.OPTION3({
|
|
635
|
+
GATE: () => !ctx.inExtend && !!ctx.qualifiedRule && $.isType(T.Extend),
|
|
636
|
+
DEF: () => {
|
|
637
|
+
const initialSelector = ctx.selector;
|
|
638
|
+
if (!RECORDING_PHASE) {
|
|
639
|
+
ctx.selector = selector;
|
|
640
|
+
}
|
|
641
|
+
try {
|
|
642
|
+
$.SUBRULE($.extend, { ARGS: [ctx] });
|
|
643
|
+
} finally {
|
|
644
|
+
ctx.selector = initialSelector;
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
});
|
|
648
|
+
|
|
649
|
+
if (ctx.inExtend) {
|
|
650
|
+
validateExtendTarget($, selector!, ':extend()');
|
|
651
|
+
(ctx.extendTargets ??= []).push({ selector: ctx.selector, target: selector!, flag });
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
return selector;
|
|
655
|
+
};
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
/**
|
|
659
|
+
* &:extend(...) statement ending with a semicolon.
|
|
660
|
+
* This is the only valid standalone extend statement in Less.
|
|
661
|
+
*/
|
|
662
|
+
export function ampersandExtend(this: P, T: TokenMap) {
|
|
663
|
+
const $ = this;
|
|
664
|
+
return (ctx: RuleContext = {}) => {
|
|
665
|
+
$.startRule();
|
|
666
|
+
|
|
667
|
+
$.CONSUME(T.Ampersand);
|
|
668
|
+
$.CONSUME(T.Extend);
|
|
669
|
+
ctx.inExtend = true;
|
|
670
|
+
$.SUBRULE($.selectorList, { ARGS: [ctx] });
|
|
671
|
+
ctx.inExtend = false;
|
|
672
|
+
let extendTargets = ctx.extendTargets!;
|
|
673
|
+
let flag = $.OPTION(() => $.CONSUME(T.AllFlag));
|
|
674
|
+
$.CONSUME(T.RParen);
|
|
675
|
+
$.CONSUME(T.Semi);
|
|
676
|
+
|
|
677
|
+
let location = $.endRule();
|
|
678
|
+
if (!$.RECORDING_PHASE) {
|
|
679
|
+
let result = mergeExtends(undefined, extendTargets, location, $.context, flag);
|
|
680
|
+
/** We've converted these extend targets to nodes, so we can reset extend targets */
|
|
681
|
+
ctx.extendTargets = undefined;
|
|
682
|
+
if (ctx.extendNodes) {
|
|
683
|
+
if (isArray(result)) {
|
|
684
|
+
ctx.extendNodes = [...ctx.extendNodes, ...result];
|
|
685
|
+
} else {
|
|
686
|
+
ctx.extendNodes.push(result);
|
|
687
|
+
}
|
|
688
|
+
} else {
|
|
689
|
+
if (isArray(result)) {
|
|
690
|
+
ctx.extendNodes = result;
|
|
691
|
+
} else {
|
|
692
|
+
ctx.extendNodes = [result];
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
// Return Nil instead of the extend node - extends are handled via ctx.extendNodes
|
|
696
|
+
// pathway to avoid duplicates (cssMain collects returned nodes into rules.rules).
|
|
697
|
+
// Nil is needed because undefined confuses cssMain (treats it as semicolon).
|
|
698
|
+
return new Nil(undefined, undefined, location, $.context);
|
|
699
|
+
}
|
|
700
|
+
};
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
export function extend(this: P, T: TokenMap) {
|
|
704
|
+
const $ = this;
|
|
705
|
+
return (ctx: RuleContext = {}) => {
|
|
706
|
+
$.startRule();
|
|
707
|
+
$.CONSUME(T.Extend);
|
|
708
|
+
|
|
709
|
+
ctx.inExtend = true;
|
|
710
|
+
$.SUBRULE($.selectorList, { ARGS: [ctx] });
|
|
711
|
+
let extendTargets = ctx.extendTargets;
|
|
712
|
+
ctx.inExtend = false;
|
|
713
|
+
|
|
714
|
+
let selector = ctx.selector;
|
|
715
|
+
let flag = $.OPTION(() => $.CONSUME(T.AllFlag));
|
|
716
|
+
$.CONSUME(T.RParen);
|
|
717
|
+
|
|
718
|
+
let location = $.endRule();
|
|
719
|
+
if (!$.RECORDING_PHASE) {
|
|
720
|
+
// When .c:extend(...) is parsed, selector is .c
|
|
721
|
+
// The extend will be processed in qualifiedRuleBody where selector: undefined is set
|
|
722
|
+
// for extends that stay inside the ruleset (not bubbled)
|
|
723
|
+
// Bubbled extends keep their selector and get it set correctly in qualifiedRule
|
|
724
|
+
let merged = mergeExtends(selector, extendTargets!, location, $.context, flag);
|
|
725
|
+
/**
|
|
726
|
+
* If we don't have as many extends as we have value, we need a way to signal
|
|
727
|
+
* that these should be bumped above the ruleset.
|
|
728
|
+
*/
|
|
729
|
+
/** We've converted these extend targets to nodes, so we can reset extend targets */
|
|
730
|
+
ctx.extendTargets = undefined;
|
|
731
|
+
if (ctx.extendNodes) {
|
|
732
|
+
if (isArray(merged)) {
|
|
733
|
+
ctx.extendNodes = [...ctx.extendNodes, ...merged];
|
|
734
|
+
} else {
|
|
735
|
+
ctx.extendNodes.push(merged);
|
|
736
|
+
}
|
|
737
|
+
} else {
|
|
738
|
+
if (isArray(merged)) {
|
|
739
|
+
ctx.extendNodes = merged;
|
|
740
|
+
} else {
|
|
741
|
+
ctx.extendNodes = [merged];
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
};
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
export function simpleSelector(this: P, T: TokenMap): ProductionRule {
|
|
749
|
+
const $ = this;
|
|
750
|
+
return (ctx: RuleContext = {}) => {
|
|
751
|
+
let selectorAlt: Alt = [
|
|
752
|
+
{
|
|
753
|
+
GATE: () => (
|
|
754
|
+
(!ctx.inExtend || $.LA(1).tokenType !== T.All)
|
|
755
|
+
&& $.LA(1).tokenType !== T.InterpolatedIdent
|
|
756
|
+
),
|
|
757
|
+
/**
|
|
758
|
+
* In Less/Sass (and now CSS), the first inner selector can be an identifier
|
|
759
|
+
*/
|
|
760
|
+
ALT: () => $.CONSUME(T.Ident)
|
|
761
|
+
},
|
|
762
|
+
{
|
|
763
|
+
/**
|
|
764
|
+
* Unlike CSS Nesting, Less allows outer qualified rules
|
|
765
|
+
* to have `&`, and it is just silently absorbed if there
|
|
766
|
+
* is no parent selector.
|
|
767
|
+
*/
|
|
768
|
+
ALT: () => {
|
|
769
|
+
let amp = $.CONSUME(T.Ampersand);
|
|
770
|
+
const value = getAmpersandTemplateValue(amp.image);
|
|
771
|
+
return new Ampersand({ appendValue: value }, undefined, $.getLocationInfo(amp), $.context);
|
|
772
|
+
}
|
|
773
|
+
},
|
|
774
|
+
{
|
|
775
|
+
ALT: () => {
|
|
776
|
+
$.startRule();
|
|
777
|
+
$.CONSUME(T.AmpersandLParen);
|
|
778
|
+
const parts: string[] = [];
|
|
779
|
+
let sawQuoted = false;
|
|
780
|
+
$.MANY(() => {
|
|
781
|
+
$.OR2([
|
|
782
|
+
{
|
|
783
|
+
GATE: () => $.isType(T.QuoteStart),
|
|
784
|
+
ALT: () => {
|
|
785
|
+
const quoted: Quoted = $.SUBRULE($.string, { ARGS: [ctx] });
|
|
786
|
+
parts.push(quoted.valueOf());
|
|
787
|
+
sawQuoted = true;
|
|
788
|
+
}
|
|
789
|
+
},
|
|
790
|
+
{
|
|
791
|
+
GATE: () => $.isType(T.WS),
|
|
792
|
+
ALT: () => {
|
|
793
|
+
parts.push($.CONSUME(T.WS).image);
|
|
794
|
+
}
|
|
795
|
+
},
|
|
796
|
+
{
|
|
797
|
+
ALT: () => {
|
|
798
|
+
parts.push($.CONSUME(T.AmpersandTemplateContents).image);
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
]);
|
|
802
|
+
});
|
|
803
|
+
$.CONSUME(T.AmpersandTemplateEnd);
|
|
804
|
+
const location = $.endRule();
|
|
805
|
+
const value = parts.join('');
|
|
806
|
+
const appendValue = sawQuoted && value === ''
|
|
807
|
+
? ''
|
|
808
|
+
: value === 'nil'
|
|
809
|
+
? ''
|
|
810
|
+
: value;
|
|
811
|
+
return new Ampersand({ appendValue }, undefined, location, $.context);
|
|
812
|
+
}
|
|
813
|
+
},
|
|
814
|
+
{ ALT: () => $.CONSUME(T.InterpolatedIdent) },
|
|
815
|
+
{ ALT: () => $.CONSUME(T.InterpolatedSelector) },
|
|
816
|
+
{ ALT: () => $.SUBRULE($.classSelector, { ARGS: [ctx] }) },
|
|
817
|
+
{ ALT: () => $.SUBRULE($.idSelector, { ARGS: [ctx] }) },
|
|
818
|
+
{ ALT: () => $.CONSUME(T.Star) },
|
|
819
|
+
{ ALT: () => {
|
|
820
|
+
let initialIsQualifiedRule = ctx.qualifiedRule;
|
|
821
|
+
ctx.qualifiedRule = false;
|
|
822
|
+
/** Make sure we prevent things like :extend() inside pseudo-value */
|
|
823
|
+
try {
|
|
824
|
+
let pseudo = $.SUBRULE($.pseudoSelector, { ARGS: [ctx] });
|
|
825
|
+
return pseudo;
|
|
826
|
+
} finally {
|
|
827
|
+
ctx.qualifiedRule = initialIsQualifiedRule;
|
|
828
|
+
}
|
|
829
|
+
} },
|
|
830
|
+
/** @todo - replicate this fix we made with the Jess parser
|
|
831
|
+
*
|
|
832
|
+
* { ALT: () => $.attributeSelector(ctx, () => [
|
|
833
|
+
{
|
|
834
|
+
ALT: () => {
|
|
835
|
+
let token = $.CONSUME($.T.InterpolatedIdent);
|
|
836
|
+
let location = $.getLocationInfo(token);
|
|
837
|
+
let image = token.image;
|
|
838
|
+
let match = interpolatedRegex.exec(image);
|
|
839
|
+
interpolatedRegex.lastIndex = 0;
|
|
840
|
+
if (match && match[0] === image) {
|
|
841
|
+
return new Reference(
|
|
842
|
+
{ key: new Keyword(match[2]!, undefined, location, $.context) },
|
|
843
|
+
{ type: 'index' },
|
|
844
|
+
location,
|
|
845
|
+
$.context
|
|
846
|
+
);
|
|
847
|
+
}
|
|
848
|
+
return getInterpolatedNode(image, location, $.context);
|
|
849
|
+
}
|
|
850
|
+
},
|
|
851
|
+
*/
|
|
852
|
+
{ ALT: () => $.SUBRULE($.attributeSelector, { ARGS: [ctx] }) },
|
|
853
|
+
/** Supports keyframes value */
|
|
854
|
+
{ ALT: () => $.CONSUME(T.DimensionInt) },
|
|
855
|
+
{ ALT: () => $.CONSUME(T.DimensionNum) }
|
|
856
|
+
];
|
|
857
|
+
|
|
858
|
+
let selector = $.OR(selectorAlt);
|
|
859
|
+
|
|
860
|
+
if ($.isToken(selector)) {
|
|
861
|
+
if (selector.tokenType.name === 'Ampersand') {
|
|
862
|
+
const value = getAmpersandTemplateValue(selector.image);
|
|
863
|
+
return new Ampersand({ appendValue: value }, undefined, $.getLocationInfo(selector), $.context);
|
|
864
|
+
}
|
|
865
|
+
if (
|
|
866
|
+
selector.tokenType.name === 'InterpolatedSelector'
|
|
867
|
+
|| selector.tokenType.name === 'InterpolatedIdent'
|
|
868
|
+
) {
|
|
869
|
+
// Create an InterpolatedSelector wrapper for interpolated value
|
|
870
|
+
let nameValue = selector.image;
|
|
871
|
+
let interpolatedNode = getInterpolatedNode(nameValue, $.getLocationInfo(selector), $.context);
|
|
872
|
+
|
|
873
|
+
return new InterpolatedSelector(interpolatedNode, undefined, $.getLocationInfo(selector), $.context);
|
|
874
|
+
}
|
|
875
|
+
return new BasicSelector(selector.image, undefined, $.getLocationInfo(selector), $.context);
|
|
876
|
+
}
|
|
877
|
+
const result: Node = selector;
|
|
878
|
+
return result;
|
|
879
|
+
};
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
export function anonymousMixinDefinition(this: P, T: TokenMap) {
|
|
883
|
+
const $ = this;
|
|
884
|
+
return (ctx: RuleContext = {}) => {
|
|
885
|
+
$.startRule();
|
|
886
|
+
let params: List | undefined;
|
|
887
|
+
let anonToken: IToken | undefined;
|
|
888
|
+
$.OPTION(() => {
|
|
889
|
+
anonToken = $.CONSUME(T.AnonMixinStart);
|
|
890
|
+
$.OPTION2(() => {
|
|
891
|
+
params = $.SUBRULE($.mixinArgList, { ARGS: [{ ...ctx, isDefinition: true }] });
|
|
892
|
+
});
|
|
893
|
+
$.CONSUME(T.RParen);
|
|
894
|
+
});
|
|
895
|
+
let rules = $.SUBRULE($.wrappedDeclarationList, { ARGS: [ctx] });
|
|
896
|
+
|
|
897
|
+
if ($.RECORDING_PHASE) {
|
|
898
|
+
return;
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
// Set rulesVisibility for detached rulesets based on leakyScope
|
|
902
|
+
// Less, for whatever reason, has slightly different lookup rules for
|
|
903
|
+
// "detached rulesets".
|
|
904
|
+
|
|
905
|
+
// Parse as Anonymous mixin
|
|
906
|
+
if (!rules.options.rulesVisibility) {
|
|
907
|
+
rules.options.rulesVisibility = {};
|
|
908
|
+
}
|
|
909
|
+
if ($.leakyScope) {
|
|
910
|
+
rules.options.rulesVisibility.Mixin = 'public';
|
|
911
|
+
rules.options.rulesVisibility.VarDeclaration = 'private';
|
|
912
|
+
} else {
|
|
913
|
+
rules.options.rulesVisibility.Mixin = 'private';
|
|
914
|
+
rules.options.rulesVisibility.VarDeclaration = 'private';
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
if (!anonToken) {
|
|
918
|
+
/** To Less, this is a "detached ruleset" */
|
|
919
|
+
// Check if this should be parsed as Collection or Rules
|
|
920
|
+
const shouldBeCollection = (() => {
|
|
921
|
+
let properties: Declaration[] = [];
|
|
922
|
+
for (const node of rules.rules) {
|
|
923
|
+
if (node.type === 'Declaration') {
|
|
924
|
+
properties.push(node);
|
|
925
|
+
} else if (node.type === 'Comment' || node.type === 'VarDeclaration') {
|
|
926
|
+
continue;
|
|
927
|
+
} else {
|
|
928
|
+
/** Not a valid collection, parse as anonymous mixin */
|
|
929
|
+
return false;
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
if (properties.length === 0) {
|
|
934
|
+
/** If just var declarations and/or comments, parse as collection */
|
|
935
|
+
return true;
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
const validPropertyCount = properties.filter((decl) => {
|
|
939
|
+
const { name } = decl;
|
|
940
|
+
const propName = typeof name === 'string' ? name : name?.valueOf();
|
|
941
|
+
if (!propName) {
|
|
942
|
+
return false;
|
|
943
|
+
}
|
|
944
|
+
// Skip custom properties (--*)
|
|
945
|
+
if (propName.startsWith('--')) {
|
|
946
|
+
return true; // Custom properties are always valid
|
|
947
|
+
}
|
|
948
|
+
return all.includes(propName);
|
|
949
|
+
}).length;
|
|
950
|
+
|
|
951
|
+
// Majority means more than 50%
|
|
952
|
+
const majorityValid = validPropertyCount > properties.length / 2;
|
|
953
|
+
/** If this looks like mostly CSS properties, parse as mixin instead */
|
|
954
|
+
return !majorityValid;
|
|
955
|
+
})();
|
|
956
|
+
|
|
957
|
+
const usage = (ctx as RuleContext).detachedRulesetUsage ?? 'none';
|
|
958
|
+
const forceMixinForDynamicUsage =
|
|
959
|
+
usage === 'function-arg'
|
|
960
|
+
|| usage === 'mixin-arg'
|
|
961
|
+
|| usage === 'default-param';
|
|
962
|
+
const shouldBeCollectionFinal = shouldBeCollection && !forceMixinForDynamicUsage;
|
|
963
|
+
if (shouldBeCollectionFinal) {
|
|
964
|
+
return new Collection(rules.rules, rules.options, $.endRule(), $.context);
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
// If anonToken exists, it's an anonymous mixin with (optional) parameters, return as Mixin
|
|
969
|
+
return new Mixin({ params, rules: rules.rules }, undefined, $.endRule(), $.context);
|
|
970
|
+
};
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
/**
|
|
974
|
+
* Mostly copied from css importAtRule, but it maps
|
|
975
|
+
* differently to Jess nodes depending on if it's meant
|
|
976
|
+
* to be a Jess-style import or just an at-rule
|
|
977
|
+
*/
|
|
978
|
+
export function importAtRule(this: P, T: TokenMap) {
|
|
979
|
+
const $ = this;
|
|
980
|
+
return (ctx: RuleContext = {}) => {
|
|
981
|
+
const isCssUrl = (url: string, options: string[]) => {
|
|
982
|
+
if (options.includes('inline')) {
|
|
983
|
+
return false;
|
|
984
|
+
}
|
|
985
|
+
const lower = url.toLowerCase();
|
|
986
|
+
const forcedLess = options.includes('less');
|
|
987
|
+
if (forcedLess) {
|
|
988
|
+
return false;
|
|
989
|
+
}
|
|
990
|
+
if (options.includes('css')) {
|
|
991
|
+
return true;
|
|
992
|
+
}
|
|
993
|
+
if (/\.css([?#].*)?$/.test(lower)) {
|
|
994
|
+
return true;
|
|
995
|
+
}
|
|
996
|
+
if (/\.less([?#].*)?$/.test(lower)) {
|
|
997
|
+
return false;
|
|
998
|
+
}
|
|
999
|
+
// Remote imports default to CSS.
|
|
1000
|
+
if (lower.startsWith('http://') || lower.startsWith('https://') || lower.startsWith('//')) {
|
|
1001
|
+
return true;
|
|
1002
|
+
}
|
|
1003
|
+
return false;
|
|
1004
|
+
};
|
|
1005
|
+
|
|
1006
|
+
$.startRule();
|
|
1007
|
+
|
|
1008
|
+
let name = $.CONSUME(T.AtImport);
|
|
1009
|
+
|
|
1010
|
+
let options: string[] = [];
|
|
1011
|
+
|
|
1012
|
+
$.OPTION(() => {
|
|
1013
|
+
$.CONSUME(T.LParen);
|
|
1014
|
+
$.AT_LEAST_ONE_SEP({
|
|
1015
|
+
SEP: T.Comma,
|
|
1016
|
+
DEF: () => {
|
|
1017
|
+
let opt = $.CONSUME(T.PlainIdent);
|
|
1018
|
+
options.push(opt.image);
|
|
1019
|
+
}
|
|
1020
|
+
});
|
|
1021
|
+
$.CONSUME(T.RParen);
|
|
1022
|
+
});
|
|
1023
|
+
|
|
1024
|
+
let urlNode: Quoted | Url = $.OR([
|
|
1025
|
+
{ ALT: () => $.SUBRULE($.urlFunction, { ARGS: [ctx] }) },
|
|
1026
|
+
{ ALT: () => $.SUBRULE($.string, { ARGS: [ctx] }) }
|
|
1027
|
+
]);
|
|
1028
|
+
|
|
1029
|
+
let extraNodes: Node[] | undefined;
|
|
1030
|
+
$.OPTION2(() => {
|
|
1031
|
+
extraNodes = $.SUBRULE($.importPostlude, { ARGS: [{}] });
|
|
1032
|
+
});
|
|
1033
|
+
|
|
1034
|
+
$.CONSUME(T.Semi);
|
|
1035
|
+
|
|
1036
|
+
if (!$.RECORDING_PHASE) {
|
|
1037
|
+
let isAtRule: boolean | undefined;
|
|
1038
|
+
let postludeNode: Node | undefined;
|
|
1039
|
+
|
|
1040
|
+
let url = urlNode.valueOf();
|
|
1041
|
+
isAtRule = isCssUrl(url, options);
|
|
1042
|
+
|
|
1043
|
+
let preludeNodes: Node[] = [urlNode];
|
|
1044
|
+
|
|
1045
|
+
if (extraNodes && extraNodes.length) {
|
|
1046
|
+
if (isAtRule) {
|
|
1047
|
+
isAtRule = true;
|
|
1048
|
+
for (const n of extraNodes) {
|
|
1049
|
+
preludeNodes.push(n);
|
|
1050
|
+
}
|
|
1051
|
+
} else {
|
|
1052
|
+
const postludeLoc = $.getLocationFromNodes(extraNodes);
|
|
1053
|
+
postludeNode = new Sequence(extraNodes, undefined, postludeLoc, $.context);
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
let location = $.endRule();
|
|
1058
|
+
if (isAtRule) {
|
|
1059
|
+
const prelude = new Sequence(preludeNodes, undefined, $.getLocationFromNodes(preludeNodes), $.context);
|
|
1060
|
+
const atRule = new AtRuleStatement({
|
|
1061
|
+
name: name.image,
|
|
1062
|
+
prelude: prelude
|
|
1063
|
+
}, undefined, location, $.context);
|
|
1064
|
+
return atRule;
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
return new StyleImport({
|
|
1068
|
+
path: urlNode
|
|
1069
|
+
}, {
|
|
1070
|
+
type: 'import',
|
|
1071
|
+
importOptions: {
|
|
1072
|
+
type: options.includes('less') ? 'less' : undefined,
|
|
1073
|
+
reference: options.includes('reference'),
|
|
1074
|
+
once: !options.includes('multiple'),
|
|
1075
|
+
multiple: options.includes('multiple'),
|
|
1076
|
+
optional: options.includes('optional'),
|
|
1077
|
+
inline: options.includes('inline'),
|
|
1078
|
+
postlude: postludeNode
|
|
1079
|
+
}
|
|
1080
|
+
}, location, $.context);
|
|
1081
|
+
}
|
|
1082
|
+
};
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
/** Less variables */
|
|
1086
|
+
export function varDeclarationOrCall(this: P, T: TokenMap) {
|
|
1087
|
+
const $ = this;
|
|
1088
|
+
return (ctx: RuleContext = {}) => {
|
|
1089
|
+
$.startRule();
|
|
1090
|
+
|
|
1091
|
+
let name = $.SUBRULE($.varName, { ARGS: [{}] });
|
|
1092
|
+
let value: Node | undefined;
|
|
1093
|
+
let args: List | undefined;
|
|
1094
|
+
let important: IToken | undefined;
|
|
1095
|
+
|
|
1096
|
+
$.OR([
|
|
1097
|
+
{
|
|
1098
|
+
/**
|
|
1099
|
+
* This is a variable declaration
|
|
1100
|
+
* Disallows `@atrule :foo;` because it resembles a pseudo-selector
|
|
1101
|
+
*/
|
|
1102
|
+
ALT: () => {
|
|
1103
|
+
$.CONSUME(T.Colon);
|
|
1104
|
+
return $.OR2([
|
|
1105
|
+
{
|
|
1106
|
+
GATE: () => {
|
|
1107
|
+
const type = $.LA(1).tokenType;
|
|
1108
|
+
return type === T.AnonMixinStart || type === T.LCurly;
|
|
1109
|
+
},
|
|
1110
|
+
ALT: () => {
|
|
1111
|
+
value = $.SUBRULE($.anonymousMixinDefinition, { ARGS: [ctx] });
|
|
1112
|
+
$.OPTION(() => $.CONSUME(T.Semi));
|
|
1113
|
+
return value;
|
|
1114
|
+
}
|
|
1115
|
+
},
|
|
1116
|
+
{
|
|
1117
|
+
GATE: () => {
|
|
1118
|
+
const type = $.LA(1).tokenType;
|
|
1119
|
+
return type !== T.AnonMixinStart && type !== T.LCurly;
|
|
1120
|
+
},
|
|
1121
|
+
ALT: () => {
|
|
1122
|
+
value = $.SUBRULE($.valueList, { ARGS: [{ ...ctx, allowMixinCallWithoutAccessor: true }] });
|
|
1123
|
+
$.OPTION2(() => {
|
|
1124
|
+
important = $.CONSUME(T.Important);
|
|
1125
|
+
});
|
|
1126
|
+
return value;
|
|
1127
|
+
}
|
|
1128
|
+
}
|
|
1129
|
+
]);
|
|
1130
|
+
}
|
|
1131
|
+
},
|
|
1132
|
+
/** This is a variable call. Allow optional whitespace between name and (. */
|
|
1133
|
+
{
|
|
1134
|
+
GATE: () => $.isType(T.LParen),
|
|
1135
|
+
/**
|
|
1136
|
+
* This is a change from Less 1.x-4.x
|
|
1137
|
+
* e.g.
|
|
1138
|
+
* ```
|
|
1139
|
+
* @dr: #(@var1, @var2) {
|
|
1140
|
+
* // ...
|
|
1141
|
+
* }
|
|
1142
|
+
* @dr(arg1, arg2);
|
|
1143
|
+
*/
|
|
1144
|
+
ALT: () => {
|
|
1145
|
+
args = $.SUBRULE($.mixinArgs, { ARGS: [ctx] });
|
|
1146
|
+
return args;
|
|
1147
|
+
}
|
|
1148
|
+
}
|
|
1149
|
+
]);
|
|
1150
|
+
|
|
1151
|
+
let location = $.endRule();
|
|
1152
|
+
if ($.RECORDING_PHASE) {
|
|
1153
|
+
return;
|
|
1154
|
+
}
|
|
1155
|
+
let nameVal = getInterpolatedOrString(name!.image);
|
|
1156
|
+
let nameNode: Any | Interpolated;
|
|
1157
|
+
if (!(nameVal instanceof Interpolated)) {
|
|
1158
|
+
nameNode = new Any(nameVal, { role: 'ident' }, $.getLocationInfo(name!), $.context);
|
|
1159
|
+
} else {
|
|
1160
|
+
nameNode = nameVal;
|
|
1161
|
+
}
|
|
1162
|
+
/** An anonymous mixin call */
|
|
1163
|
+
if (!value) {
|
|
1164
|
+
// When @variable() is called, look up the variable first
|
|
1165
|
+
// The variable's value (which may be a Call node) will be executed
|
|
1166
|
+
const nameRef = new Reference({ key: nameNode }, { type: 'variable', role: 'name' });
|
|
1167
|
+
// Pass markImportant in options if !important is present
|
|
1168
|
+
const callOptions = important ? { markImportant: true } : undefined;
|
|
1169
|
+
const callNode = new Call({ name: nameRef, args }, callOptions, location, $.context);
|
|
1170
|
+
// Clear important since it's now on the Call
|
|
1171
|
+
if (important) {
|
|
1172
|
+
important = undefined;
|
|
1173
|
+
}
|
|
1174
|
+
// Variable calls are expressions at the outermost level (but not parenthesized).
|
|
1175
|
+
// e.g. `$media()`, NOT `$(media())`
|
|
1176
|
+
return new Expression(callNode, undefined, location, $.context);
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1179
|
+
// If the value is a Call node and we have !important, set markImportant on the Call
|
|
1180
|
+
// instead of on the VarDeclaration (mixin call semantics)
|
|
1181
|
+
if (important && value instanceof Call) {
|
|
1182
|
+
value.options = value.options || {};
|
|
1183
|
+
value.options.markImportant = true;
|
|
1184
|
+
important = undefined;
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
if (value && isLegacySelectorLikeValue(value)) {
|
|
1188
|
+
const varName = String(nameNode.valueOf());
|
|
1189
|
+
$.warnDeprecation(
|
|
1190
|
+
`Unquoted selector capture in '${varName}' is no longer supported. Use '*[ ... ]' (e.g. ${varName}: *[.a, .b]).`,
|
|
1191
|
+
$.LA(1),
|
|
1192
|
+
'unquoted-selector-capture'
|
|
1193
|
+
);
|
|
1194
|
+
}
|
|
1195
|
+
|
|
1196
|
+
return new VarDeclaration({
|
|
1197
|
+
name: nameNode instanceof Any ? String(nameNode.valueOf()) : nameNode,
|
|
1198
|
+
value: value,
|
|
1199
|
+
important: important ? new Any(important.image, { role: 'flag' }, $.getLocationInfo(important), $.context) : undefined
|
|
1200
|
+
}, undefined, location, $.context);
|
|
1201
|
+
};
|
|
1202
|
+
}
|
|
1203
|
+
|
|
1204
|
+
export function selectorCapture(this: P, T: TokenMap) {
|
|
1205
|
+
const $ = this;
|
|
1206
|
+
return (ctx: RuleContext = {}) => {
|
|
1207
|
+
$.startRule();
|
|
1208
|
+
$.CONSUME(T.Star);
|
|
1209
|
+
// Use $.OR with a gate for a positive assertion
|
|
1210
|
+
const selector = $.OR([
|
|
1211
|
+
{
|
|
1212
|
+
GATE: $.noSep.bind($),
|
|
1213
|
+
ALT: () => {
|
|
1214
|
+
$.CONSUME(T.LSquare);
|
|
1215
|
+
const selector = $.SUBRULE($.forgivingSelectorList, { ARGS: [{ ...ctx, inner: true }] });
|
|
1216
|
+
$.CONSUME(T.RSquare);
|
|
1217
|
+
return selector;
|
|
1218
|
+
}
|
|
1219
|
+
}
|
|
1220
|
+
]);
|
|
1221
|
+
|
|
1222
|
+
const location = $.endRule();
|
|
1223
|
+
if ($.RECORDING_PHASE) {
|
|
1224
|
+
return;
|
|
1225
|
+
}
|
|
1226
|
+
return new SelectorCapture(
|
|
1227
|
+
selector,
|
|
1228
|
+
undefined,
|
|
1229
|
+
location,
|
|
1230
|
+
$.context
|
|
1231
|
+
);
|
|
1232
|
+
};
|
|
1233
|
+
}
|
|
1234
|
+
|
|
1235
|
+
export function valueSequence(this: P, _T: TokenMap): ProductionRule {
|
|
1236
|
+
const $ = this;
|
|
1237
|
+
return (ctx: RuleContext = {}) => {
|
|
1238
|
+
const RECORDING_PHASE = $.RECORDING_PHASE;
|
|
1239
|
+
$.startRule();
|
|
1240
|
+
let nodes: Node[] | undefined;
|
|
1241
|
+
if (!RECORDING_PHASE) {
|
|
1242
|
+
nodes = [];
|
|
1243
|
+
}
|
|
1244
|
+
|
|
1245
|
+
{
|
|
1246
|
+
const exprCtx: RuleContext = { ...ctx, wrapInExpression: true };
|
|
1247
|
+
let value = $.SUBRULE2($.expressionSum, { ARGS: [exprCtx] });
|
|
1248
|
+
if (!RECORDING_PHASE) {
|
|
1249
|
+
value = wrapOuterExpressionIfNeeded.call($, value, exprCtx);
|
|
1250
|
+
nodes!.push(value);
|
|
1251
|
+
}
|
|
1252
|
+
}
|
|
1253
|
+
|
|
1254
|
+
$.MANY(() => {
|
|
1255
|
+
const exprCtx: RuleContext = { ...ctx, wrapInExpression: true };
|
|
1256
|
+
let value = $.SUBRULE3($.expressionSum, { ARGS: [exprCtx] });
|
|
1257
|
+
if (!RECORDING_PHASE) {
|
|
1258
|
+
value = wrapOuterExpressionIfNeeded.call($, value, exprCtx);
|
|
1259
|
+
nodes!.push(value);
|
|
1260
|
+
}
|
|
1261
|
+
});
|
|
1262
|
+
|
|
1263
|
+
if (RECORDING_PHASE) {
|
|
1264
|
+
return;
|
|
1265
|
+
}
|
|
1266
|
+
|
|
1267
|
+
let location = $.endRule();
|
|
1268
|
+
if (nodes!.length === 1) {
|
|
1269
|
+
const single = nodes![0]!;
|
|
1270
|
+
return single;
|
|
1271
|
+
}
|
|
1272
|
+
const seq = new Sequence(nodes!, undefined, location, $.context);
|
|
1273
|
+
return seq;
|
|
1274
|
+
};
|
|
1275
|
+
}
|
|
1276
|
+
|
|
1277
|
+
export function squareValue(this: P, T: TokenMap) {
|
|
1278
|
+
const $ = this;
|
|
1279
|
+
return (ctx: RuleContext = {}) => {
|
|
1280
|
+
$.startRule();
|
|
1281
|
+
$.CONSUME(T.LSquare);
|
|
1282
|
+
let node: Node = $.OR([
|
|
1283
|
+
{
|
|
1284
|
+
GATE: () => !$.looseMode,
|
|
1285
|
+
ALT: () => {
|
|
1286
|
+
let ident = $.CONSUME(T.Ident);
|
|
1287
|
+
return new Any(ident.image, { role: 'ident' }, $.getLocationInfo(ident), $.context);
|
|
1288
|
+
}
|
|
1289
|
+
},
|
|
1290
|
+
{
|
|
1291
|
+
GATE: () => !!$.looseMode,
|
|
1292
|
+
ALT: () => {
|
|
1293
|
+
let nodes: Node[] = [];
|
|
1294
|
+
$.MANY(() => {
|
|
1295
|
+
let node = $.SUBRULE($.anyInnerValue, { ARGS: [ctx] });
|
|
1296
|
+
const wrapped = node;
|
|
1297
|
+
nodes.push(wrapped);
|
|
1298
|
+
});
|
|
1299
|
+
const seq = new Sequence(nodes, undefined, $.getLocationFromNodes(nodes), $.context);
|
|
1300
|
+
return seq;
|
|
1301
|
+
}
|
|
1302
|
+
}
|
|
1303
|
+
]);
|
|
1304
|
+
$.CONSUME(T.RSquare);
|
|
1305
|
+
let location = $.endRule();
|
|
1306
|
+
const blk = new Block(node, { type: 'square' }, location, $.context);
|
|
1307
|
+
return blk;
|
|
1308
|
+
};
|
|
1309
|
+
}
|