@jesscss/less-parser 2.0.0-alpha.8 → 2.0.0-alpha.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/README.md +27 -10
  2. package/lib/ast/grammar.d.ts +2 -0
  3. package/lib/ast/grammar.d.ts.map +1 -0
  4. package/lib/grammar.cjs +39855 -31054
  5. package/lib/grammar.d.ts.map +1 -1
  6. package/lib/grammar.js +39855 -31054
  7. package/lib/index.cjs +49609 -15
  8. package/lib/index.d.ts +4 -9
  9. package/lib/index.d.ts.map +1 -1
  10. package/lib/index.js +49607 -5
  11. package/lib/parse-error.d.ts +15 -0
  12. package/lib/parse-error.d.ts.map +1 -0
  13. package/package.json +9 -24
  14. package/lib/builders.d.ts +0 -381
  15. package/lib/builders.d.ts.map +0 -1
  16. package/lib/functional-parser.cjs +0 -2654
  17. package/lib/functional-parser.d.ts +0 -18
  18. package/lib/functional-parser.d.ts.map +0 -1
  19. package/lib/functional-parser.js +0 -2589
  20. package/lib/jess.cjs +0 -3968
  21. package/lib/jess.d.ts +0 -7
  22. package/lib/jess.d.ts.map +0 -1
  23. package/lib/jess.js +0 -3962
  24. package/lib/lessParser.d.ts +0 -38
  25. package/lib/lessParser.d.ts.map +0 -1
  26. package/lib/lessRecursiveParser.d.ts +0 -99
  27. package/lib/lessRecursiveParser.d.ts.map +0 -1
  28. package/lib/lessTokens.d.ts +0 -23
  29. package/lib/lessTokens.d.ts.map +0 -1
  30. package/lib/productions/guards.d.ts +0 -113
  31. package/lib/productions/guards.d.ts.map +0 -1
  32. package/lib/productions/index.d.ts +0 -5
  33. package/lib/productions/index.d.ts.map +0 -1
  34. package/lib/productions/root.d.ts +0 -38
  35. package/lib/productions/root.d.ts.map +0 -1
  36. package/lib/productions/selectors.d.ts +0 -41
  37. package/lib/productions/selectors.d.ts.map +0 -1
  38. package/lib/productions/values.d.ts +0 -35
  39. package/lib/productions/values.d.ts.map +0 -1
  40. package/lib/utils.d.ts +0 -9
  41. package/lib/utils.d.ts.map +0 -1
  42. package/src/__tests__/debug-log.ts +0 -35
  43. package/src/__tests__/wall5-parse.test.ts +0 -67
  44. package/src/builders.ts +0 -3190
  45. package/src/cst.ts +0 -25
  46. package/src/functional-parser.ts +0 -162
  47. package/src/grammar.ts +0 -870
  48. package/src/index.ts +0 -19
  49. package/src/jess.ts +0 -6
  50. package/src/lessParser.ts +0 -120
  51. package/src/lessRecursiveParser.ts +0 -279
  52. package/src/lessTokens.ts +0 -350
  53. package/src/productions/guards.ts +0 -1066
  54. package/src/productions/index.ts +0 -29
  55. package/src/productions/root.ts +0 -1613
  56. package/src/productions/selectors.ts +0 -1309
  57. package/src/productions/values.ts +0 -1449
  58. package/src/utils.ts +0 -178
@@ -1,1613 +0,0 @@
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
- }