@jesscss/less-parser 2.0.0-alpha.6 → 2.0.0-alpha.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/README.md +133 -5
  2. package/lib/builders.d.ts +381 -0
  3. package/lib/builders.d.ts.map +1 -0
  4. package/lib/cst.cjs +14 -0
  5. package/lib/cst.d.ts +6 -0
  6. package/lib/cst.d.ts.map +1 -0
  7. package/lib/cst.js +12 -0
  8. package/lib/functional-parser.cjs +2653 -0
  9. package/lib/functional-parser.d.ts +18 -0
  10. package/lib/functional-parser.d.ts.map +1 -0
  11. package/lib/functional-parser.js +2588 -0
  12. package/lib/grammar.cjs +30621 -0
  13. package/lib/grammar.d.ts +2 -0
  14. package/lib/grammar.d.ts.map +1 -0
  15. package/lib/grammar.js +30620 -0
  16. package/lib/index.cjs +17 -4096
  17. package/lib/index.d.ts +9 -149
  18. package/lib/index.d.ts.map +1 -1
  19. package/lib/index.js +6 -4091
  20. package/lib/jess.cjs +3968 -0
  21. package/lib/jess.d.ts +7 -0
  22. package/lib/jess.d.ts.map +1 -0
  23. package/lib/jess.js +3962 -0
  24. package/lib/lessParser.d.ts +38 -0
  25. package/lib/lessParser.d.ts.map +1 -0
  26. package/lib/lessRecursiveParser.d.ts +99 -0
  27. package/lib/lessRecursiveParser.d.ts.map +1 -0
  28. package/lib/lessTokens.d.ts +23 -0
  29. package/lib/lessTokens.d.ts.map +1 -0
  30. package/lib/productions/guards.d.ts +113 -0
  31. package/lib/productions/guards.d.ts.map +1 -0
  32. package/lib/productions/index.d.ts +5 -0
  33. package/lib/productions/index.d.ts.map +1 -0
  34. package/lib/productions/root.d.ts +38 -0
  35. package/lib/productions/root.d.ts.map +1 -0
  36. package/lib/productions/selectors.d.ts +41 -0
  37. package/lib/productions/selectors.d.ts.map +1 -0
  38. package/lib/productions/values.d.ts +35 -0
  39. package/lib/productions/values.d.ts.map +1 -0
  40. package/lib/utils.d.ts +9 -0
  41. package/lib/utils.d.ts.map +1 -0
  42. package/package.json +41 -10
  43. package/src/__tests__/debug-log.ts +35 -0
  44. package/src/__tests__/wall5-parse.test.ts +67 -0
  45. package/src/builders.ts +3183 -0
  46. package/src/cst.ts +25 -0
  47. package/src/functional-parser.ts +162 -0
  48. package/src/grammar.ts +869 -0
  49. package/src/index.ts +19 -0
  50. package/src/jess.ts +6 -0
  51. package/src/lessParser.ts +120 -0
  52. package/src/lessRecursiveParser.ts +279 -0
  53. package/src/lessTokens.ts +350 -0
  54. package/src/productions/guards.ts +1066 -0
  55. package/src/productions/index.ts +29 -0
  56. package/src/productions/root.ts +1613 -0
  57. package/src/productions/selectors.ts +1309 -0
  58. package/src/productions/values.ts +1449 -0
  59. package/src/utils.ts +178 -0
  60. package/lib/index.d.cts +0 -150
  61. package/lib/index.d.cts.map +0 -1
  62. package/lib/index.js.map +0 -1
@@ -0,0 +1,2588 @@
1
+ import { lessGrammar } from "./grammar.js";
2
+ import { LexerType, SKIPPED_LABEL, groupCapture, rawCssFragments, rawCssTokens } from "@jesscss/css-parser";
3
+ import { CssParser, buildLazyTriviaMap, runFunctionalParse, spannedComponents, toParseError } from "@jesscss/css-parser/jess";
4
+ import { Ampersand, Any, AtRule, AtRuleStatement, Block, Call, Color, ComplexSelector, CompoundSelector, Condition, CustomDeclaration, Declaration, DefaultGuard, Dimension, Expression, Extend, ExtendFlag, For, INTERPOLATION_PLACEHOLDER, Interpolated, InterpolatedSelector, JsImport, List, Mixin, N, NESTABLE_AT_RULES, Negative, Nil, Num, Operation, Paren, QueryCondition, Quoted, Reference, Rest, Rules, Ruleset, Sequence, StyleImport, Url, VarDeclaration, isNode, isSelectorListLike, nil, selectorListItems, shouldOperateWithMathFrames } from "@jesscss/core";
5
+ //#region src/lessTokens.ts
6
+ const AMPERSAND_TEMPLATE_CONTENTS_REGEX = /(?:[.#-]|\d)(?:[.#\w\u0080-\uffff-]|&)*|(?:[.#\w\u0080-\uffff-]|&)*&(?:[.#\w\u0080-\uffff-]|&)*/;
7
+ function $preBuildFragments() {
8
+ const fragments = rawCssFragments().map((f) => [...f]);
9
+ fragments.unshift(["lineComment", "\\/\\/[^\\n\\r]*"]);
10
+ fragments.push(["interpolated", "[@$]\\{(?:{{nmchar}}*)\\}"]);
11
+ return fragments;
12
+ }
13
+ function $preBuildTokens() {
14
+ const tokens = rawCssTokens();
15
+ /**
16
+ * Keyed by what to insert after
17
+ *
18
+ * @todo - Move merge utility to css-parser
19
+ */
20
+ const merges = {
21
+ Assign: [{
22
+ name: "Ellipsis",
23
+ pattern: /\.\.\./,
24
+ categories: ["BlockMarker"]
25
+ }, (
26
+ /**
27
+ * Less's historical parser unfortunately allows
28
+ * at-keywords that are not valid in CSS. One is
29
+ * that Less allows at-keywords to begin with numbers.
30
+ * Another is that it allows an at-rule that only
31
+ * contains a single dash. So we capture this as
32
+ * a separate token.
33
+ *
34
+ * We also do this later in the token stack so that we
35
+ * don't accidentally grab something like
36
+ * @-webkit-keyframes while looking for @-.
37
+ */
38
+ {
39
+ name: "AtKeywordLessExtension",
40
+ pattern: "@(?:-|\\d(?:{{nmchar}})*)",
41
+ categories: ["BlockMarker", "AtName"]
42
+ })],
43
+ PlainIdent: [
44
+ {
45
+ name: "Interpolated",
46
+ pattern: LexerType.NA
47
+ },
48
+ {
49
+ name: "LineComment",
50
+ pattern: "{{lineComment}}",
51
+ label: SKIPPED_LABEL
52
+ },
53
+ {
54
+ name: "PlusAssign",
55
+ pattern: "\\+{{whitespace}}*:",
56
+ categories: ["BlockMarker", "Assign"]
57
+ },
58
+ {
59
+ name: "UnderscoreAssign",
60
+ pattern: "\\+{{whitespace}}*_{{whitespace}}*:",
61
+ categories: ["BlockMarker", "Assign"]
62
+ },
63
+ {
64
+ name: "AnonMixinStart",
65
+ pattern: /[.#]\(/,
66
+ categories: ["BlockMarker"]
67
+ },
68
+ {
69
+ name: "GtEqAlias",
70
+ pattern: /=>/,
71
+ categories: ["CompareOperator"]
72
+ },
73
+ {
74
+ name: "LtEqAlias",
75
+ pattern: /=</,
76
+ categories: ["CompareOperator"]
77
+ },
78
+ {
79
+ name: "Extend",
80
+ pattern: /:extend\(/,
81
+ categories: ["BlockMarker"]
82
+ },
83
+ {
84
+ name: "VarOrProp",
85
+ pattern: LexerType.NA
86
+ },
87
+ {
88
+ name: "NestedReference",
89
+ pattern: ["([@$]+{{ident}}?){2,}", groupCapture],
90
+ start_chars_hint: ["@", "$"],
91
+ categories: ["VarOrProp"],
92
+ line_breaks: true
93
+ },
94
+ {
95
+ name: "PropertyReference",
96
+ pattern: "\\${{ident}}",
97
+ categories: ["VarOrProp"]
98
+ },
99
+ (
100
+ /** Can be used in unit function or mod operation */
101
+ {
102
+ name: "Percent",
103
+ pattern: /%/
104
+ }),
105
+ {
106
+ name: "DefaultGuardIdent",
107
+ pattern: /default/,
108
+ longer_alt: "PlainIdent",
109
+ categories: ["Ident"]
110
+ },
111
+ {
112
+ name: "DefaultGuardFunc",
113
+ pattern: /default(?:\(\))/
114
+ }
115
+ ],
116
+ Ampersand: [{
117
+ name: "AmpersandLParen",
118
+ pattern: /&\(/,
119
+ push_mode: "AmpersandTemplate",
120
+ categories: [
121
+ "Selector",
122
+ "NestedRuleStart",
123
+ "BlockMarker"
124
+ ]
125
+ }, {
126
+ name: "AllFlag",
127
+ pattern: /!all/,
128
+ categories: ["BlockMarker"]
129
+ }],
130
+ UrlStart: [
131
+ (
132
+ /**
133
+ * Keywords that we don't identify as idents
134
+ * should be manually added to other places where an ident is valid.
135
+ */
136
+ {
137
+ name: "When",
138
+ pattern: /when/i,
139
+ longer_alt: "PlainIdent",
140
+ categories: ["BlockMarker"]
141
+ }),
142
+ {
143
+ name: "FormatFunction",
144
+ pattern: /%\(/,
145
+ categories: ["BlockMarker", "FunctionStart"]
146
+ },
147
+ {
148
+ name: "IfFunction",
149
+ pattern: /if\(/,
150
+ categories: ["BlockMarker", "FunctionStart"]
151
+ },
152
+ {
153
+ name: "BooleanFunction",
154
+ pattern: /boolean\(/,
155
+ categories: ["BlockMarker", "FunctionStart"]
156
+ },
157
+ {
158
+ name: "JavaScript",
159
+ pattern: /~?`[^`]*`/,
160
+ line_breaks: true
161
+ }
162
+ ],
163
+ Signed: [
164
+ {
165
+ name: "InterpolatedIdent",
166
+ pattern: "(?:{{ident}}|-)?{{interpolated}}(?:{{interpolated}}|{{nmchar}})*",
167
+ categories: [
168
+ "Interpolated",
169
+ "Selector",
170
+ "Ident"
171
+ ]
172
+ },
173
+ {
174
+ name: "InterpolatedCustomProperty",
175
+ pattern: "--{{ident}}?{{interpolated}}(?:{{interpolated}}|{{nmchar}})*",
176
+ categories: ["Interpolated"]
177
+ },
178
+ (
179
+ /**
180
+ * Unfortunately, there's grammatical ambiguity between
181
+ * interpolated props and a naked interpolated selector name,
182
+ * making this awkward token necessary.
183
+ */
184
+ {
185
+ name: "InterpolatedSelector",
186
+ pattern: ["[.#]{{ident}}?{{interpolated}}(?:{{interpolated}}|{{nmchar}})*", groupCapture],
187
+ categories: ["Interpolated", "Selector"],
188
+ start_chars_hint: [".", "#"],
189
+ line_breaks: true
190
+ })
191
+ ]
192
+ };
193
+ let defaultTokens = tokens.modes.Default.slice();
194
+ let tokenLength = defaultTokens.length;
195
+ for (let i = 0; i < tokenLength; i++) {
196
+ let token = defaultTokens[i];
197
+ const { name } = token;
198
+ const copyToken = () => {
199
+ token = structuredClone(token);
200
+ };
201
+ let alterations = true;
202
+ switch (name) {
203
+ case "Ampersand":
204
+ copyToken();
205
+ /**
206
+ * Captures not just ampersands, but "ampersand merges", where
207
+ * the intent of the author was to merge the parent selector with a token
208
+ * suffix or prefix.
209
+ *
210
+ * e.g.
211
+ * 1. &-foo
212
+ * 2. &(foo)
213
+ * 3. &1
214
+ * 4. .foo-&
215
+ */
216
+ token.pattern = "(?:[.#](?:{{ident}}-)?&|&){{nmchar}}*";
217
+ token.start_chars_hint = [
218
+ "&",
219
+ ".",
220
+ "#"
221
+ ];
222
+ break;
223
+ case "DotName":
224
+ case "HashName":
225
+ copyToken();
226
+ token.longer_alt = "Ampersand";
227
+ break;
228
+ case "Divide":
229
+ copyToken();
230
+ token.pattern = /\.?\//;
231
+ break;
232
+ case "SingleQuoteStart":
233
+ copyToken();
234
+ token.pattern = /~?'/;
235
+ break;
236
+ case "DoubleQuoteStart":
237
+ copyToken();
238
+ token.pattern = /~?"/;
239
+ break;
240
+ default: alterations = false;
241
+ }
242
+ if (alterations) defaultTokens[i] = token;
243
+ const merge = merges[name];
244
+ if (merge) {
245
+ /** Insert after current token */
246
+ defaultTokens = defaultTokens.slice(0, i + 1).concat(merge, defaultTokens.slice(i + 1));
247
+ tokens.modes.Default = defaultTokens;
248
+ const mergeLength = merge.length;
249
+ tokenLength += mergeLength;
250
+ i += mergeLength;
251
+ }
252
+ }
253
+ tokens.modes.AmpersandTemplate = [
254
+ {
255
+ name: "AmpersandTemplateEnd",
256
+ pattern: /\)/,
257
+ pop_mode: true,
258
+ categories: ["FunctionLikeEnd"]
259
+ },
260
+ {
261
+ name: "AmpersandTemplateContents",
262
+ pattern: AMPERSAND_TEMPLATE_CONTENTS_REGEX,
263
+ categories: ["Selector"]
264
+ },
265
+ "SingleQuoteStart",
266
+ "DoubleQuoteStart",
267
+ "WS"
268
+ ];
269
+ return tokens;
270
+ }
271
+ const Fragments = $preBuildFragments();
272
+ const Tokens = $preBuildTokens();
273
+ const lessFragments = () => Fragments;
274
+ const lessTokens = () => Tokens;
275
+ //#endregion
276
+ //#region src/utils.ts
277
+ const INTERPOLATION_REGEX = /([$@])\{([^}]+)\}/g;
278
+ const createInterpolatedReference = (prefix, varName, location, context) => {
279
+ const isProperty = prefix === "$";
280
+ return new Reference({ key: isProperty ? new Quoted(varName, { quote: "'" }, location) : varName }, {
281
+ type: isProperty ? "index" : "variable",
282
+ role: "ident"
283
+ }, location);
284
+ };
285
+ const getInterpolatedNode = (name, location, context) => {
286
+ const replacements = [];
287
+ let source = name;
288
+ let result;
289
+ INTERPOLATION_REGEX.lastIndex = 0;
290
+ while ((result = INTERPOLATION_REGEX.exec(name)) !== null) {
291
+ const [match, prefix, varName] = result;
292
+ source = source.replace(match, INTERPOLATION_PLACEHOLDER);
293
+ replacements.push(createInterpolatedReference(prefix ?? "", varName ?? "", location, context));
294
+ }
295
+ return new Interpolated({
296
+ source,
297
+ replacements
298
+ }, { role: "ident" }, location);
299
+ };
300
+ const normalizeMixinReferenceKey = (selector) => {
301
+ if (isNode(selector, N.BasicSelector) || selector instanceof InterpolatedSelector) return {
302
+ key: selector.valueOf(),
303
+ rawKey: selector
304
+ };
305
+ if (isNode(selector, N.CompoundSelector)) return {
306
+ key: selector.value.map((node) => node.valueOf()),
307
+ rawKey: selector
308
+ };
309
+ if (isNode(selector, N.ComplexSelector)) {
310
+ const path = [];
311
+ let canUsePath = true;
312
+ for (const node of selector.value) {
313
+ if (isNode(node, N.BasicSelector) || node instanceof InterpolatedSelector) {
314
+ path.push(node.valueOf());
315
+ continue;
316
+ }
317
+ if (isNode(node, N.CompoundSelector)) {
318
+ path.push(...node.value.map((child) => child.valueOf()));
319
+ continue;
320
+ }
321
+ if (isNode(node, N.Combinator) && (node.value === ">" || node.value === " ")) continue;
322
+ canUsePath = false;
323
+ break;
324
+ }
325
+ if (canUsePath && path.length > 0) return {
326
+ key: path,
327
+ rawKey: selector
328
+ };
329
+ }
330
+ return {
331
+ key: selector.valueOf(),
332
+ rawKey: selector
333
+ };
334
+ };
335
+ const getInterpolatedOrString = (name, location, context) => {
336
+ const matches = [];
337
+ INTERPOLATION_REGEX.lastIndex = 0;
338
+ let result;
339
+ while ((result = INTERPOLATION_REGEX.exec(name)) !== null) {
340
+ const [fullMatch, prefix, varName] = result;
341
+ if (varName && prefix) matches.push({
342
+ fullMatch,
343
+ prefix,
344
+ varName,
345
+ index: result.index
346
+ });
347
+ }
348
+ if (matches.length > 0) {
349
+ let source = name;
350
+ const replacements = [];
351
+ let offset = 0;
352
+ for (let i = 0; i < matches.length; i++) {
353
+ const match = matches[i];
354
+ const adjustedIndex = match.index - offset;
355
+ const beforeMatch = source.substring(0, adjustedIndex);
356
+ const afterMatch = source.substring(adjustedIndex + match.fullMatch.length);
357
+ source = beforeMatch + INTERPOLATION_PLACEHOLDER + afterMatch;
358
+ offset += match.fullMatch.length - INTERPOLATION_PLACEHOLDER.length;
359
+ const ref = createInterpolatedReference(match.prefix, match.varName, location, context);
360
+ replacements.push(ref);
361
+ }
362
+ return new Interpolated({
363
+ source,
364
+ replacements
365
+ }, { role: "ident" }, location);
366
+ }
367
+ const atPos = name.indexOf("@", 1);
368
+ const dollarPos = name.indexOf("$", 1);
369
+ if (atPos === -1 && dollarPos === -1) if (name.startsWith("@") || name.startsWith("$")) return name.slice(1);
370
+ else return name;
371
+ const nextPos = atPos !== -1 ? atPos : dollarPos;
372
+ const start = name.slice(1, nextPos);
373
+ const end = name.slice(nextPos);
374
+ const type = end.startsWith("@") ? "variable" : "index";
375
+ const endResult = getInterpolatedOrString(end, location, context);
376
+ if (typeof endResult === "string") {
377
+ const endKey = type === "index" ? new Quoted(endResult, { quote: "'" }, location) : endResult;
378
+ return new Interpolated({
379
+ source: start + INTERPOLATION_PLACEHOLDER,
380
+ replacements: [new Reference({ key: endKey }, {
381
+ type,
382
+ role: "ident"
383
+ }, location)]
384
+ }, { role: "ident" });
385
+ } else
386
+ /**
387
+ * endResult is already an Interpolated node, so we need to handle this
388
+ * differently.
389
+ *
390
+ * @todo - test deep nesting
391
+ */
392
+ return new Interpolated({
393
+ source: start + INTERPOLATION_PLACEHOLDER,
394
+ replacements: [type === "index" ? new Quoted(endResult, { quote: "'" }, location) : endResult]
395
+ }, { role: "ident" });
396
+ };
397
+ //#endregion
398
+ //#region src/builders.ts
399
+ const KNOWN_AT_RULE_VAR_NAME_RE = /^(?:(?:-moz-)?document|(?:-[a-z]+-)?keyframes|(?:-ms-)?viewport|import|media|supports|layer|container|scope|page|font-face|starting-style|property|counter-style|color-profile|font-palette-values|namespace)$/i;
400
+ function spanToLocation(span) {
401
+ return {
402
+ start: span.start,
403
+ end: span.end
404
+ };
405
+ }
406
+ function nodeChildren(children) {
407
+ return children.filter((c) => c._tag === "node");
408
+ }
409
+ var LessGrammar = class LessGrammar extends CssParser {
410
+ /** Math mode governing when arithmetic operates / when `/` divides. Less default. */
411
+ mathMode = "parens-division";
412
+ /** Bare ident/keyword token in value or guard position. */
413
+ _lessKeyword(text, loc) {
414
+ return this._valueKeyword(text, loc);
415
+ }
416
+ _isKeywordLike(node) {
417
+ return !!node && typeof node === "object" && (node.type === "Keyword" || node.type === "Any");
418
+ }
419
+ _isEmptyKeywordLike(node) {
420
+ return !node || this._isKeywordLike(node) && !String(node.value ?? "").trim();
421
+ }
422
+ buildNode(type, span, children, _state, _rawChildren, fields, triviaLog = []) {
423
+ const loc = spanToLocation(span);
424
+ const raw = _rawChildren;
425
+ switch (type) {
426
+ case "VarDeclaration": return this._buildVarDeclaration(children, raw, loc);
427
+ case "Reference": return this._buildReference(children, loc);
428
+ case "LessAmpersand": return this._buildAmpersand(children, loc);
429
+ case "ComplexSelector": return this._buildComplexSelector(raw, loc);
430
+ case "SelectorList": return this._buildSelectorList(raw, loc);
431
+ case "Ruleset": return this._buildRuleset(children, raw, loc);
432
+ case "Declaration":
433
+ this._warnDeprecatedValue(span);
434
+ return this._buildLessDeclaration(raw, loc);
435
+ case "CustomDeclaration":
436
+ this._warnCustomPropVars(span);
437
+ return this._buildLessCustomDecl(children, loc);
438
+ case "Block": return this._buildLessCustomBlock(children, loc);
439
+ case "AtRuleBlock":
440
+ this._warnAtRulePreludeVars(span);
441
+ return this._buildAtRuleBlock(children, loc);
442
+ case "QueryAtRuleBlock":
443
+ this._warnAtRulePreludeVars(span);
444
+ return this._buildLessQueryAtRuleBlock(children, raw, loc);
445
+ case "NamedColor": return this._buildNamedColor(children, loc);
446
+ case "Comparison": return this._buildComparison(raw, loc);
447
+ case "GuardDefault": return new DefaultGuard("default()", {}, loc);
448
+ case "GuardInParens": return this._buildGuardInParens(children, loc);
449
+ case "GuardTerm": return this._buildGuardTerm(raw, loc);
450
+ case "GuardAnd": return this._buildGuardJoin(children, loc, "and");
451
+ case "GuardOr": return this._buildGuardJoin(children, loc, "or");
452
+ case "Guard": return this._buildGuard(children, loc);
453
+ case "CondArgTerm": return this._buildCondArgTerm(raw, loc);
454
+ case "CondArgAnd": return this._buildCondArgJoin(children, loc, "and");
455
+ case "CondArgOr": return this._buildCondArgJoin(children, loc, "or");
456
+ case "UnicodeRange": return this._lessKeyword(this._source.slice(span.start, span.end), loc);
457
+ case "PseudoSelector": return this._buildLessPseudo(type, span, children, _state, raw, fields, triviaLog, loc);
458
+ case "InterpolatedSelector": return this._buildInterpolatedSelector(children, loc);
459
+ case "VarCall": return this._buildVarCall(children, raw, loc);
460
+ case "MixinCall": return this._buildMixinCall(children, raw, loc);
461
+ case "Rest": return this._buildRest(children, loc);
462
+ case "NamedArg": return this._buildNamedArg(raw, loc);
463
+ case "MixinArgs": return this._buildMixinArgs(raw, loc);
464
+ case "AnonymousMixinDefinition": return this._buildAnonMixin(children, loc);
465
+ case "DetachedRuleset": return this._buildDetachedRuleset(children, loc);
466
+ case "For": return this._buildEachFor(children, loc);
467
+ case "FormatCall": return this._buildFormatCall(raw, loc);
468
+ case "MixinOrQualifiedRule": return this._buildMixinOrQualified(children, loc);
469
+ case "Negative": return new Negative(this._negativeOperand(children), void 0, loc);
470
+ case "OperationTop": return this._buildOperation(children, loc, this.mathMode === "always");
471
+ case "EscapedValue": return this._buildEscapedValue(children, loc);
472
+ case "InterpValue": return this._buildInterpValue(raw, loc);
473
+ case "NsAccessor": return this._buildNsAccessor(children, loc);
474
+ case "AtRuleStatement": return this._buildAtRuleStatement(children, loc);
475
+ case "ExtendTarget": return this._buildExtendTarget(children, raw, loc);
476
+ case "ExtendPseudo": return this._buildExtendPseudo(children, loc);
477
+ case "ExtendStatement": return this._buildExtendStatement(children, raw, loc);
478
+ default: return super.buildNode(type, span, children, _state, raw, fields, triviaLog);
479
+ }
480
+ }
481
+ /**
482
+ * The operand of a `Negative` (`-value`). The grammar emits the leading `-`
483
+ * as a leaf followed by the operand, which may itself be a node or a bare
484
+ * string terminal (e.g. `-@color` → `var(--color)`'s inner `-color-accent`).
485
+ * Prefer a node child; otherwise take the operand leaf's text so `Negative`
486
+ * coerces it to the canonical node form rather than receiving `undefined`.
487
+ */
488
+ _negativeOperand(children) {
489
+ const node = nodeChildren(children)[0];
490
+ if (node) return node;
491
+ return children.find((c) => c._tag === "leaf" && c.value !== "-")?.value ?? "";
492
+ }
493
+ _buildVarDeclaration(children, rawChildren, loc) {
494
+ const items = spannedComponents(rawChildren);
495
+ const rawName = typeof items[0]?.comp === "string" ? items[0].comp : "";
496
+ const name = rawName.startsWith("@") ? rawName.slice(1) : rawName;
497
+ if (/^-?\d/.test(name)) this._warn(`Variable name "@${name}" starts with a digit; digit-leading variable names are deprecated.`, "digit-leading-variable");
498
+ const colonIdx = items.findIndex((i) => i.comp === ":");
499
+ if (items[colonIdx + 1]?.comp === "{") {
500
+ const ruleNodes = nodeChildren(children);
501
+ const openBrace = items[colonIdx + 1];
502
+ const closeBrace = items[items.length - 1].comp === "}" ? items[items.length - 1] : void 0;
503
+ if (ruleNodes.length === 0 && closeBrace) {
504
+ if (this._source.slice(openBrace.span.end, closeBrace.span.start).trim() !== "") {
505
+ const rawBlock = this._source.slice(openBrace.span.start, closeBrace.span.end);
506
+ return new VarDeclaration({
507
+ name: (name || void 0) ?? name,
508
+ value: new Quoted(rawBlock, {}, loc)
509
+ }, {}, loc);
510
+ }
511
+ }
512
+ const mixin = new Mixin({ rules: ruleNodes }, {}, loc);
513
+ return new VarDeclaration({
514
+ name: (name || void 0) ?? name,
515
+ value: mixin
516
+ }, {}, loc);
517
+ }
518
+ let end = items.length;
519
+ let bangIdx = -1;
520
+ for (let i = colonIdx + 1; i < items.length; i++) {
521
+ const c = items[i].comp;
522
+ if (c === "!") {
523
+ end = i;
524
+ bangIdx = i;
525
+ break;
526
+ }
527
+ if (c === "important" || c === ";") {
528
+ end = i;
529
+ break;
530
+ }
531
+ }
532
+ const valItems = items.slice(colonIdx + 1, end);
533
+ if (valItems.length) {
534
+ const vText = this._source.slice(valItems[0].span.start, valItems[valItems.length - 1].span.end);
535
+ if (/(?:^|[\s,])\.-?[_a-zA-Z]/.test(vText)) this._warn(`Unquoted selector capture in variable "@${name}" is deprecated; wrap the value in quotes or ~"...".`, "unquoted-selector-capture");
536
+ }
537
+ const nsRef = this._tryParseNamespaceRef(valItems, loc);
538
+ let { value: rawValue } = nsRef ? { value: nsRef } : this._assembleLessValue(valItems, loc);
539
+ if (!nsRef && valItems.length > 0) {
540
+ const lastSpan = valItems[valItems.length - 1].span;
541
+ const afterVal = this._source.slice(lastSpan.end);
542
+ const rv = rawValue;
543
+ const rvKey = rv?.key;
544
+ const isEmptyKey = rvKey === "" || rvKey === void 0 || rvKey && typeof rvKey === "object" && rvKey.type === "Quoted" && (rvKey.value === "" || rvKey.valueOf?.() === "");
545
+ const grammarPartialAccessor = rv && rv.type === "Reference" && rv.target !== void 0 && isEmptyKey;
546
+ const accMatch = /^\s*\[([^\]]+)\]/.exec(afterVal);
547
+ if (accMatch) {
548
+ const accText = accMatch[1].trim();
549
+ const accessorKey = this._decodeAccessorKey(accText, loc);
550
+ const accessorRefOptions = typeof accessorKey === "number" ? { type: "index" } : {};
551
+ if (grammarPartialAccessor) rawValue = new Reference({
552
+ target: rv.target,
553
+ key: accessorKey
554
+ }, accessorRefOptions, loc);
555
+ else rawValue = new Reference({
556
+ target: rawValue,
557
+ key: accessorKey
558
+ }, accessorRefOptions, loc);
559
+ const afterAcc = afterVal.slice(accMatch[0].length);
560
+ if (/^\s*\(\s*\)/.test(afterAcc)) rawValue = new Call({ name: rawValue }, {}, loc);
561
+ }
562
+ }
563
+ const value = typeof rawValue === "string" && rawValue ? this._lessKeyword(rawValue, loc) : rawValue;
564
+ let important;
565
+ if (bangIdx >= 0) {
566
+ const bang = items[bangIdx];
567
+ const kw = items[bangIdx + 1];
568
+ const impEnd = kw && typeof kw.comp === "string" && kw.comp.toLowerCase() === "important" ? kw.span.end : bang.span.end;
569
+ important = this._source.slice(bang.span.start, impEnd);
570
+ }
571
+ return new VarDeclaration({
572
+ name: (name || void 0) ?? name,
573
+ value,
574
+ important
575
+ }, {}, loc);
576
+ }
577
+ /**
578
+ * Build a Less variable reference and its accessor/call chain. Faithful 1:1
579
+ * port of the Chevrotain `varReference` + `lookupOrCall` productions
580
+ * (productions/values.ts, productions/guards.ts): a `@var` base glued to a
581
+ * left-folded chain of `[index]` accessors and `(call)`s. The grammar's
582
+ * noTrivia() guarantees the chain is adjacent (no whitespace between segments).
583
+ */
584
+ _buildReference(children, loc) {
585
+ const ls = children.filter((c) => c._tag === "leaf");
586
+ const varName = ls[0]?.value ?? "";
587
+ const isVar = varName.startsWith("@");
588
+ let base = varName.startsWith("$") ? new Reference({ key: new Quoted(varName.slice(1), { quote: "'" }, loc) }, { type: "index" }, loc) : new Reference(isVar ? varName.slice(1) : varName, isVar ? { type: "variable" } : {}, loc);
589
+ let i = 1;
590
+ while (i < ls.length) {
591
+ const tok = ls[i].value;
592
+ if (tok === "[") if (ls[i + 1]?.value === "]") {
593
+ base = new Reference({
594
+ target: base,
595
+ key: -1
596
+ }, { type: "index" }, loc);
597
+ i += 2;
598
+ } else {
599
+ base = this._applyReferenceAccessor(base, ls[i + 1].value, loc);
600
+ i += 3;
601
+ }
602
+ else if (tok === "(") {
603
+ const payload = { name: base };
604
+ if (ls[i + 1]?.value === ")") i += 2;
605
+ else {
606
+ const args = this._buildRefCallArgs(ls[i + 1].value, loc);
607
+ if (args) payload.args = args;
608
+ i += 3;
609
+ }
610
+ base = new Call(payload, {}, loc);
611
+ } else break;
612
+ }
613
+ return base;
614
+ }
615
+ /**
616
+ * Build a namespace INDEXED-accessor value: a `.`/`#` selector-path head glued to
617
+ * a `[accessor]` (and any further `[accessor]`/`(call)` chain), e.g.
618
+ * `#ns.options[val1]`. The grammar (NsAccessor) captures this as ONE value operand
619
+ * so it survives arithmetic folding; here we reassemble it into the mixin-ruleset
620
+ * name Reference + accessor chain — the SAME shape the declaration-value
621
+ * _assembleSegment path produces for a lone `#ns.options[val1]`. Call-headed forms
622
+ * (`.mixin()`, `.mixin()[k]`) do NOT reach here (they keep the GluedParen path).
623
+ * Leaves: [ headText, '[', key?, ']', '(', content?, ')' … ].
624
+ */
625
+ _buildNsAccessor(children, loc) {
626
+ const ls = children.filter((c) => c._tag === "leaf");
627
+ const headText = (ls[0]?.value ?? "").trim();
628
+ const pathSegs = headText.match(/[#.][^#.>+~\s]*/g) ?? [headText];
629
+ const nameKey = pathSegs.length === 1 ? pathSegs[0] : pathSegs;
630
+ const rawKey = pathSegs.length > 1 ? pathSegs.join("") : void 0;
631
+ let base = new Reference({
632
+ key: nameKey,
633
+ ...rawKey ? { rawKey } : {}
634
+ }, {
635
+ type: "mixin-ruleset",
636
+ role: "name"
637
+ }, loc);
638
+ let i = 1;
639
+ while (i < ls.length) {
640
+ const tok = ls[i].value;
641
+ if (tok === "[") if (ls[i + 1]?.value === "]") {
642
+ base = new Reference({
643
+ target: base,
644
+ key: -1
645
+ }, { type: "index" }, loc);
646
+ i += 2;
647
+ } else {
648
+ base = this._applyReferenceAccessor(base, ls[i + 1].value, loc);
649
+ i += 3;
650
+ }
651
+ else if (tok === "(") {
652
+ const payload = { name: base };
653
+ if (ls[i + 1]?.value === ")") i += 2;
654
+ else {
655
+ const args = this._buildRefCallArgs(ls[i + 1].value, loc);
656
+ if (args) payload.args = args;
657
+ i += 3;
658
+ }
659
+ base = new Call(payload, {}, loc);
660
+ } else break;
661
+ }
662
+ return base;
663
+ }
664
+ /**
665
+ * Apply one `[key]` accessor to `base`, reproducing lookupOrCall's key logic:
666
+ * type is `variable` when the key token starts with `@`, else `index`; the key
667
+ * text runs through getInterpolatedOrString (handling `$@x`/`@{x}` interpolation),
668
+ * and index keys are wrapped in a Quoted.
669
+ */
670
+ _applyReferenceAccessor(base, keyStr, loc) {
671
+ const type = keyStr.startsWith("@") ? "variable" : "index";
672
+ let result = getInterpolatedOrString(keyStr, loc);
673
+ if (type === "index") result = new Quoted(result, { quote: "'" }, loc);
674
+ return new Reference({
675
+ target: base,
676
+ key: result
677
+ }, { type }, loc);
678
+ }
679
+ /**
680
+ * Build mixin-call args for a `(…)` segment in a reference chain from the raw
681
+ * captured content. Comma-separated values become a List; an empty segment
682
+ * yields null (no args). Sub-structure here is intentionally shallow — the
683
+ * accessor-chain call form is rare and no consumer inspects nested arg shape.
684
+ */
685
+ _buildRefCallArgs(content, loc) {
686
+ const trimmed = content.trim();
687
+ if (!trimmed) return null;
688
+ return new List(trimmed.split(",").map((p) => p.trim()).filter(Boolean).map((p) => p.startsWith("@") ? new Reference(p.slice(1), { type: "variable" }, loc) : this._lessKeyword(p, loc)), void 0, loc);
689
+ }
690
+ _buildNamedColor(children, loc) {
691
+ return new Color({ node: children.filter((c) => c._tag === "leaf")[0]?.value ?? "" }, {}, loc);
692
+ }
693
+ _normalizeCompareOp(op) {
694
+ switch (op) {
695
+ case "=>":
696
+ case ">=": return ">=";
697
+ case "=<":
698
+ case "<=": return "<=";
699
+ case ">": return ">";
700
+ case "<": return "<";
701
+ default: return "=";
702
+ }
703
+ }
704
+ /** A guard comparison operator leaf (`=`, `<`, `>=`, `=<`, `=~`, …). */
705
+ _isCompareOpLeaf(text) {
706
+ return />=|<=|=>|=<|=~|[<>=]/.test(text);
707
+ }
708
+ /**
709
+ * Split a guard term's ordered components into `left [op right]`. A bare-keyword
710
+ * operand (`foo`, `true`) is a leaf string — not a node — so `nodeChildren`
711
+ * alone drops it; walk the ordered stream so string operands become real
712
+ * keyword nodes (their guard truthiness is decided at eval by `Condition`).
713
+ */
714
+ _guardComparison(raw, loc) {
715
+ const items = spannedComponents(raw);
716
+ let op;
717
+ const operands = [];
718
+ for (const it of items) if (typeof it.comp === "string") {
719
+ if (this._isCompareOpLeaf(it.comp)) {
720
+ op = this._normalizeCompareOp(it.comp);
721
+ continue;
722
+ }
723
+ operands.push(this._lessKeyword(it.comp, loc));
724
+ } else operands.push(it.comp);
725
+ const left = this._maybeDefaultGuard(operands[0] ?? this._lessKeyword("", loc), loc);
726
+ const right = operands[1] !== void 0 ? this._maybeDefaultGuard(operands[1], loc) : void 0;
727
+ return {
728
+ left,
729
+ op,
730
+ right
731
+ };
732
+ }
733
+ _buildComparison(raw, loc) {
734
+ const { left, op, right } = this._guardComparison(raw, loc);
735
+ if (op && right) return new Condition([
736
+ left,
737
+ op,
738
+ right
739
+ ], {}, loc);
740
+ return new Condition([left], {}, loc);
741
+ }
742
+ /**
743
+ * Coerce a `_assembleValue` result — a single Component or a raw space-group
744
+ * array — into ONE Node, so it can be a Condition operand. A single string
745
+ * becomes a keyword; a space-group array becomes a `Sequence` (the same coercion
746
+ * the List serializer applies to a raw group).
747
+ */
748
+ _condOperandNode(comps, loc) {
749
+ const { value } = this._assembleValue(comps, loc);
750
+ if (Array.isArray(value)) return new Sequence(value.map((c) => typeof c === "string" ? this._lessKeyword(c, loc) : c), void 0, loc);
751
+ if (typeof value === "string") return this._lessKeyword(value, loc);
752
+ return value;
753
+ }
754
+ /**
755
+ * `CondArgTerm` — a name-independent condition-argument term: optional leading
756
+ * `not`, a bounded value operand, and an optional `<op> right` comparison. Builds
757
+ * a `Condition` (comparison → `[left, op, right]`; bare `not` → `{negate:true}`)
758
+ * or, when neither a `not` nor a `compareOp` is present, the plain operand value
759
+ * (byte-identical to an ordinary value arg). Multi-token operands (`1px solid`)
760
+ * survive as a `Sequence`, unlike the single-operand guard path.
761
+ */
762
+ _buildCondArgTerm(raw, loc) {
763
+ const items = spannedComponents(raw);
764
+ const hasNot = items.length > 0 && items[0].comp === "not";
765
+ const rest = hasNot ? items.slice(1) : items;
766
+ const opIdx = rest.findIndex((it) => typeof it.comp === "string" && this._isCompareOpLeaf(it.comp));
767
+ let term;
768
+ if (opIdx >= 0) term = new Condition([
769
+ this._condOperandNode(rest.slice(0, opIdx), loc),
770
+ this._normalizeCompareOp(rest[opIdx].comp),
771
+ this._condOperandNode(rest.slice(opIdx + 1), loc)
772
+ ], {}, loc);
773
+ else term = this._condOperandNode(rest, loc);
774
+ if (hasNot) return new Condition([term], { negate: true }, loc);
775
+ return term;
776
+ }
777
+ /**
778
+ * Fold a left-associative `and`/`or` chain of condition-arg terms into Conditions.
779
+ *
780
+ * Less accepts a bare `and`/`or` join in value-position condition args (`if(@a > 5
781
+ * and @b < 2, …)`) — verified against less@4.6.7 (`if`/`boolean` route their arg
782
+ * through `condition()` with no `needsParens`, so bare comparisons split on `and`/
783
+ * `or`). We keep accepting the bare form for Less parity, but NORMALIZE the AST so
784
+ * each join operand is `Paren`-wrapped: `@a > 5 and @b < 2` builds the SAME tree as
785
+ * the explicitly-parenthesised `(@a > 5) and (@b < 2)`. This is a structural
786
+ * normalisation only — `Paren(Condition)` evaluates to the same boolean as the bare
787
+ * `Condition`, so rendered CSS is byte-identical. A single unjoined operand (one
788
+ * node) is untouched (no synthetic Paren).
789
+ */
790
+ _buildCondArgJoin(children, loc, op) {
791
+ const nodes = nodeChildren(children);
792
+ if (nodes.length === 0) return this._lessKeyword("", loc);
793
+ if (nodes.length === 1) return nodes[0];
794
+ const wrap = (n) => n.type === "Paren" ? n : new Paren(n, {}, loc);
795
+ let left = wrap(nodes[0]);
796
+ for (let i = 1; i < nodes.length; i++) left = new Condition([
797
+ left,
798
+ op,
799
+ wrap(nodes[i])
800
+ ], {}, loc);
801
+ return left;
802
+ }
803
+ /** Coerce a default() call/reference into a DefaultGuard, mirroring isDefaultGuardCall. */
804
+ _maybeDefaultGuard(node, loc) {
805
+ const n = node;
806
+ if (n?.type === "Call") {
807
+ const name = n.name;
808
+ const nameStr = String(typeof name === "object" && name !== null && "valueOf" in name ? name.valueOf() : name ?? "");
809
+ if (nameStr === "default" || nameStr === "??") return new DefaultGuard("default()", {}, loc);
810
+ }
811
+ return node;
812
+ }
813
+ /** guardInParens: `(` guardOr `)` → Paren, or a bare default() → Paren(DefaultGuard). */
814
+ _buildGuardInParens(children, loc) {
815
+ let inner = nodeChildren(children)[0] ?? this._lessKeyword("", loc);
816
+ inner = this._maybeDefaultGuard(inner, loc);
817
+ const innerAny = inner;
818
+ if (innerAny?.type === "Paren" && innerAny.value?.type === "DefaultGuard") return inner;
819
+ return new Paren(inner, {}, loc);
820
+ }
821
+ /** A single guard term: optional `not`, then a paren-guard or a comparison/value. */
822
+ _buildGuardTerm(raw, loc) {
823
+ const hasNot = spannedComponents(raw).some((i) => typeof i.comp === "string" && i.comp === "not");
824
+ const rest = raw.filter((rc) => !(rc._tag === "leaf" && rc.value === "not"));
825
+ const nodes = nodeChildren(rest);
826
+ let term;
827
+ if (nodes.length >= 1 && nodes[0].type === "Paren") term = nodes[0];
828
+ else {
829
+ const { left, op, right } = this._guardComparison(rest, loc);
830
+ term = op && right ? new Condition([
831
+ left,
832
+ op,
833
+ right
834
+ ], {}, loc) : left;
835
+ }
836
+ if (hasNot) return new Condition([term], { negate: true }, loc);
837
+ return term;
838
+ }
839
+ /** Fold a left-associative chain of terms joined by `and` / `or`. */
840
+ _buildGuardJoin(children, loc, op) {
841
+ const nodes = nodeChildren(children);
842
+ if (nodes.length === 0) return this._lessKeyword("", loc);
843
+ let left = nodes[0];
844
+ for (let i = 1; i < nodes.length; i++) left = new Condition([
845
+ left,
846
+ op,
847
+ nodes[i]
848
+ ], {}, loc);
849
+ return left;
850
+ }
851
+ /** guard: `when` guardOr — returns the single guardOr child. */
852
+ _buildGuard(children, loc) {
853
+ return nodeChildren(children)[0] ?? this._lessKeyword("", loc);
854
+ }
855
+ /**
856
+ * Deprecated Less `%(format, args…)` string formatting, LOWERED at build time.
857
+ *
858
+ * Jess already has full string interpolation, so `%()` is redundant Less-4 legacy —
859
+ * we emit a `percent-format` deprecation warning and, when the format is a string
860
+ * literal, splice its `%[sda]` directives into the canonical `Interpolated` node
861
+ * (the same one `@{var}` string interpolation builds), wrapped in a `Quoted` that
862
+ * preserves the literal's quote char / escaped flag:
863
+ * - `%s` → bare interpolation slot (a Quoted arg inserts with its quotes stripped);
864
+ * - `%d`/`%a` → identical bare slot (d/a are the same in Less);
865
+ * - `%S`/`%D`/`%A` → the arg WRAPPED in `escape(…)` (URL-encode);
866
+ * - `%%` → a literal `%`.
867
+ * A dynamic (non-literal) format (`%(hello)`, `%(e("…"), …)`) can't be lowered at
868
+ * parse time, so it falls back to a best-effort runtime `%` Call with the same warning.
869
+ */
870
+ _buildFormatCall(raw, loc) {
871
+ this._warn("%() string formatting is deprecated — use string interpolation", "percent-format");
872
+ const argList = this._assembleArgs(this._betweenParens(spannedComponents(raw)), loc);
873
+ const args = argList.value ?? [];
874
+ const format = args[0];
875
+ const rest = args.slice(1);
876
+ if (format instanceof Quoted && typeof format.value === "string") {
877
+ const { source, replacements } = this._lowerFormatString(format.value, rest, loc);
878
+ return new Quoted(new Interpolated({
879
+ source,
880
+ replacements
881
+ }, { role: "ident" }, loc), {
882
+ quote: format.quote,
883
+ escaped: format.escaped
884
+ }, loc);
885
+ }
886
+ return new Call({
887
+ name: new Reference("%", {
888
+ type: "function",
889
+ fallbackValue: true
890
+ }, loc),
891
+ args: argList
892
+ }, { silentFail: true }, loc);
893
+ }
894
+ /**
895
+ * Turn a printf-style format string into an `Interpolated` source + replacements.
896
+ * `%[sda]` → one `INTERPOLATION_PLACEHOLDER` slot consuming the next positional arg
897
+ * (uppercase → wrapped in `escape(…)` to URL-encode); `%%` → a literal `%`.
898
+ */
899
+ _lowerFormatString(formatText, restArgs, loc) {
900
+ let source = "";
901
+ const replacements = [];
902
+ let argIndex = 0;
903
+ for (let i = 0; i < formatText.length; i++) {
904
+ const ch = formatText[i];
905
+ if (ch !== "%") {
906
+ source += ch;
907
+ continue;
908
+ }
909
+ const next = formatText[i + 1];
910
+ if (next === "%") {
911
+ source += "%";
912
+ i++;
913
+ continue;
914
+ }
915
+ if (next && /[sda]/i.test(next)) {
916
+ const arg = restArgs[argIndex++];
917
+ if (arg) {
918
+ source += INTERPOLATION_PLACEHOLDER;
919
+ if (/[A-Z]/.test(next)) {
920
+ const escapeRef = new Reference("escape", {
921
+ type: "function",
922
+ fallbackValue: true
923
+ }, loc);
924
+ const escapeArgs = new List([arg], {}, loc);
925
+ replacements.push(new Call({
926
+ name: escapeRef,
927
+ args: escapeArgs
928
+ }, { silentFail: true }, loc));
929
+ } else replacements.push(arg);
930
+ } else source += ch + next;
931
+ i++;
932
+ continue;
933
+ }
934
+ source += ch;
935
+ }
936
+ return {
937
+ source,
938
+ replacements
939
+ };
940
+ }
941
+ _buildInterpolatedSelector(children, loc) {
942
+ const ls = children.filter((c) => c._tag === "leaf");
943
+ const replacements = [];
944
+ let source = "";
945
+ for (const l of ls) if (l.value.startsWith("@{")) {
946
+ const varName = l.value.slice(2, -1);
947
+ replacements.push(new Reference(varName, { role: "ident" }, loc));
948
+ source += INTERPOLATION_PLACEHOLDER;
949
+ } else source += l.value;
950
+ return new InterpolatedSelector(new Interpolated({
951
+ source,
952
+ replacements
953
+ }, { role: "ident" }, loc), {}, loc);
954
+ }
955
+ _buildLessPseudo(type, span, children, state, raw, fields, triviaLog, loc) {
956
+ const pseudo = super.buildNode(type, span, children, state, raw, fields, triviaLog);
957
+ let pseudoArg = pseudo.arg;
958
+ if (pseudoArg === void 0) {
959
+ const open = children.findIndex((c) => c._tag === "leaf" && c.value === "(");
960
+ if (open >= 0) {
961
+ const inner = children[open + 1];
962
+ const isClose = inner?._tag === "leaf" && inner.value === ")";
963
+ if (inner !== void 0 && !isClose) {
964
+ const recovered = typeof inner === "string" ? this._lessKeyword(inner, loc) : inner._tag === "leaf" ? this._lessKeyword(inner.value, loc) : inner;
965
+ pseudo.arg = recovered;
966
+ pseudoArg = recovered;
967
+ }
968
+ }
969
+ }
970
+ if (Array.isArray(pseudoArg)) pseudo.arg = pseudoArg.map((item) => typeof item === "string" && item !== " " ? this._lessKeyword(item, loc) : item);
971
+ return pseudo;
972
+ }
973
+ _buildLessDeclaration(raw, loc) {
974
+ const items = spannedComponents(raw);
975
+ const decl = this._buildDeclaration(raw, loc);
976
+ const colonIdx = items.findIndex((i) => i.comp === ":");
977
+ const merge = colonIdx > 0 ? items[colonIdx - 1]?.comp : void 0;
978
+ const assign = merge === "+_" ? "+_:" : merge === "+" ? "+,:" : ":";
979
+ const d = decl;
980
+ d._options = {
981
+ ...d._options ?? {},
982
+ assign
983
+ };
984
+ if (typeof d.name === "string" && d.name) {
985
+ const nameStr = d.name;
986
+ decl.name = nameStr.includes("@{") || nameStr.includes("${") ? getInterpolatedNode(nameStr, loc) : nameStr;
987
+ }
988
+ const dvRaw = decl.value;
989
+ if (dvRaw && typeof dvRaw === "object" && dvRaw.type === "Operation") {
990
+ const f = dvRaw;
991
+ if (shouldOperateWithMathFrames({
992
+ mathMode: this.mathMode,
993
+ parenFrames: [],
994
+ calcFrames: 0
995
+ }, f.operator, f.left, f.right)) decl.value = new Expression(dvRaw, { parens: true }, loc);
996
+ return decl;
997
+ }
998
+ const dvList = decl.value;
999
+ if (dvList && dvList.type === "List" && dvList.options?.sep === "/" && this.mathMode === "always") {
1000
+ const items = dvList.value;
1001
+ if (items.length >= 2 && items.every((it) => this._isDivisionLike(it))) {
1002
+ let op = items[0];
1003
+ for (let i = 1; i < items.length; i++) op = new Operation([
1004
+ op,
1005
+ "/",
1006
+ items[i]
1007
+ ], void 0, loc);
1008
+ const f = op;
1009
+ decl.value = shouldOperateWithMathFrames({
1010
+ mathMode: this.mathMode,
1011
+ parenFrames: [],
1012
+ calcFrames: 0
1013
+ }, f.operator, f.left, f.right) ? new Expression(op, { parens: true }, loc) : op;
1014
+ return decl;
1015
+ }
1016
+ }
1017
+ const dv = decl.value;
1018
+ const dvIsArray = Array.isArray(dv);
1019
+ const dvHasNode = dvIsArray && dv.some((p) => !!p && typeof p === "object" && "type" in p);
1020
+ if (dvIsArray && dvHasNode) {
1021
+ const src = this._source.slice(loc.start, loc.end);
1022
+ const colonPos = src.indexOf(":");
1023
+ const rawVal = colonPos >= 0 ? src.slice(colonPos + 1).trim().replace(/;\s*$/, "").trim() : src;
1024
+ if (/^progid:/i.test(rawVal)) decl.value = this._buildLegacyMSFilter(rawVal, loc);
1025
+ }
1026
+ return decl;
1027
+ }
1028
+ /**
1029
+ * Port of `processLegacyMSFilterToken` (lessRecursiveParser.ts): a `progid:…`
1030
+ * filter value string → Interpolated(role=any) with `@var` runs templated out,
1031
+ * or a plain Keyword when the run has no variables.
1032
+ */
1033
+ _buildLegacyMSFilter(source, loc) {
1034
+ source = source.replace(/\s*=\s*/g, "=");
1035
+ const varRe = /@([_a-zA-Z\xA0-￿][-_a-zA-Z0-9\xA0-￿]*)/g;
1036
+ const matches = [...source.matchAll(varRe)];
1037
+ if (matches.length === 0) return this._lessKeyword(source, loc);
1038
+ return new Interpolated({
1039
+ source: source.replace(varRe, (_full, _name, offset, fullSource) => {
1040
+ const key = fullSource.slice(0, offset).match(/([A-Za-z]+)=$/)?.[1];
1041
+ if (key && /colorstr$/i.test(key)) return `"${INTERPOLATION_PLACEHOLDER}"`;
1042
+ return INTERPOLATION_PLACEHOLDER;
1043
+ }),
1044
+ replacements: matches.map((match) => createInterpolatedReference("@", match[1], loc))
1045
+ }, { role: "any" }, loc);
1046
+ }
1047
+ _buildAmpersand(children, loc) {
1048
+ const ls = children.filter((c) => c._tag === "leaf");
1049
+ if (!ls.some((l) => l.value === "(")) {
1050
+ const image = ls[0]?.value ?? "&";
1051
+ return new Ampersand(this._ampersandTemplateValue(image), {}, loc);
1052
+ }
1053
+ const trimmed = (ls.find((l) => l.value !== "&" && l.value !== "(" && l.value !== ")")?.value ?? "").trim();
1054
+ return new Ampersand(trimmed === "nil" ? "" : trimmed.replace(/^(['"])([\s\S]*)\1$/, "$2"), {}, loc);
1055
+ }
1056
+ /** The append value of a `&`-led ampersand token: the suffix after `&` (`&-bar` →
1057
+ * `-bar`, `&1` → `1`), or undefined for a bare `&`. */
1058
+ _ampersandTemplateValue(image) {
1059
+ return image === "&" ? void 0 : image.slice(1) || void 0;
1060
+ }
1061
+ /**
1062
+ * A single extend target inside `extend( … )`: a complex selector plus its
1063
+ * optional `all` / `!all` flag (selectors.ts `complexSelector`'s OPTION2 flag).
1064
+ * Produced as an `Extend` carrier the surrounding pseudo/statement groups.
1065
+ */
1066
+ _buildExtendTarget(_children, raw, loc) {
1067
+ const comps = spannedComponents(raw);
1068
+ const isFlag = (c) => typeof c === "string" && /^!?all$/.test(c);
1069
+ const flag = comps.some((c) => isFlag(c.comp)) ? ExtendFlag.All : ExtendFlag.Exact;
1070
+ const targetComp = comps.find((c) => !isFlag(c.comp))?.comp;
1071
+ return new Extend({
1072
+ target: typeof targetComp === "string" ? targetComp : targetComp ?? "&",
1073
+ flag
1074
+ }, {}, loc);
1075
+ }
1076
+ /**
1077
+ * `:extend( … )` pseudo form (selectors.ts `extend`): groups its ExtendTarget
1078
+ * children. Targets sharing one flag collapse to a single Extend whose target is
1079
+ * a SelectorList (or the lone selector); mixed flags stay as one Extend each,
1080
+ * returned in a List. Mirrors mergeExtends' target-and-flag grouping.
1081
+ */
1082
+ _buildExtendPseudo(children, loc) {
1083
+ const targets = nodeChildren(children).filter((n) => n.type === "Extend");
1084
+ const firstFlag = targets[0].flag;
1085
+ if (targets.every((t) => t.flag === firstFlag)) return new Extend({
1086
+ target: targets.length === 1 ? targets[0].target : this._makeSelectorList(targets.map((t) => t.target), loc),
1087
+ flag: firstFlag
1088
+ }, {}, loc);
1089
+ return new List(targets.map((t) => new Extend({
1090
+ target: t.target,
1091
+ flag: t.flag
1092
+ }, {}, loc)), {}, loc);
1093
+ }
1094
+ /**
1095
+ * `&:extend( … );` (or bare `:extend( … );`) statement form (selectors.ts
1096
+ * `ampersandExtend`). The ExtendPseudo child already carries the grouped
1097
+ * Extend(s); the leading `&` is just the statement marker.
1098
+ */
1099
+ _buildExtendStatement(children, _raw, _loc) {
1100
+ return nodeChildren(children).find((n) => n.type === "Extend" || n.type === "List");
1101
+ }
1102
+ /**
1103
+ * Decode a single `[key]` accessor into its Less lookup-key AST — the ONE shared
1104
+ * decoder for every accessor-chain builder path (var-decl value, declaration value,
1105
+ * at-rule prelude, namespace ref). Accepts either a raw authored key STRING (the
1106
+ * text inside the brackets, e.g. `@@foo`, `$@x`, `bar`, ``), or a SquareParen `Paren`
1107
+ * node whose inner content the grammar already parsed (a raw string or a `@var`
1108
+ * Reference). Returns the key exactly as the reference lookupOrCall production would:
1109
+ *
1110
+ * `[]` → -1 (index, empty)
1111
+ * `[@@name]` → Reference{type:variable,key:name} (dynamic variable lookup)
1112
+ * `[$@name]` / `[@$name]` → Quoted(Interpolated(@name)) (dynamic property lookup)
1113
+ * `[@name]` → 'name' (static variable lookup; bare string key)
1114
+ * `[$name]` → Quoted('name') (property lookup; `$` marker dropped)
1115
+ * `[name]` → Quoted('name') (index)
1116
+ *
1117
+ * Exactly one `@`/`$` marker is the lookup marker and is never kept.
1118
+ */
1119
+ _decodeAccessorKey(rawTextOrNode, loc) {
1120
+ let rawText;
1121
+ let innerVal;
1122
+ if (typeof rawTextOrNode === "string") rawText = rawTextOrNode.trim();
1123
+ else {
1124
+ innerVal = rawTextOrNode.node ?? rawTextOrNode.value;
1125
+ if (!innerVal || this._isEmptyKeywordLike(innerVal)) return -1;
1126
+ if (typeof innerVal === "string") rawText = innerVal.trim();
1127
+ else if (typeof innerVal === "object" && innerVal.type === "Reference" && typeof innerVal.key === "string") rawText = "@" + innerVal.key;
1128
+ else if (this._isKeywordLike(innerVal) && typeof innerVal.value === "string") rawText = innerVal.value.trim();
1129
+ }
1130
+ if (rawText === void 0 || rawText === "") {
1131
+ if (rawText === "") return -1;
1132
+ return innerVal?.key ?? innerVal;
1133
+ }
1134
+ if (rawText.startsWith("@@")) return new Reference(rawText.slice(2), { type: "variable" }, loc);
1135
+ if (rawText.startsWith("$@") || rawText.startsWith("@$")) return new Quoted(new Interpolated({
1136
+ source: INTERPOLATION_PLACEHOLDER,
1137
+ replacements: [new Reference(rawText.slice(2), { role: "ident" }, loc)]
1138
+ }, { role: "ident" }, loc), {}, loc);
1139
+ if (rawText.startsWith("@")) return rawText.slice(1);
1140
+ return new Quoted(rawText.replace(/^\$/, ""), {}, loc);
1141
+ }
1142
+ _assembleSegment(seg, loc) {
1143
+ const result = super._assembleSegment(seg, loc);
1144
+ const isNsNameEarly = (c) => typeof c === "string" && /^[#.]-?[_a-zA-Z]/.test(c.trim());
1145
+ if (!Array.isArray(result)) {
1146
+ if (isNsNameEarly(result)) {
1147
+ const segs = result.trim().match(/[#.][^#.]*/g) ?? [result.trim()];
1148
+ const nameKey = segs.length === 1 ? segs[0] : segs;
1149
+ const rawKey = segs.length > 1 ? segs.join("") : void 0;
1150
+ return new Reference({
1151
+ key: nameKey,
1152
+ ...rawKey ? { rawKey } : {}
1153
+ }, {
1154
+ type: "mixin-ruleset",
1155
+ role: "name"
1156
+ }, loc);
1157
+ }
1158
+ return result;
1159
+ }
1160
+ if (result.length < 2) return result;
1161
+ const comps = result;
1162
+ const isNsName = (c) => typeof c === "string" && /^[#.]-?[_a-zA-Z€-￿]/.test(c.trim());
1163
+ const isSquareParen = (c) => !!c && typeof c === "object" && c.type === "Paren" && c._options?.delimiter === "square";
1164
+ const isRoundParen = (c) => !!c && typeof c === "object" && c.type === "Paren" && c._options?.delimiter !== "square";
1165
+ if (!isNsName(comps[0])) return comps;
1166
+ const splitNsToken = (s) => s.match(/[#.][^#.]*/g) ?? [s];
1167
+ let i = 0;
1168
+ const pathSegs = [];
1169
+ while (i < comps.length && isNsName(comps[i])) {
1170
+ pathSegs.push(...splitNsToken(comps[i].trim()));
1171
+ i++;
1172
+ }
1173
+ const rawPathText = pathSegs.join("");
1174
+ const nameKey = pathSegs.length === 1 ? pathSegs[0] : pathSegs;
1175
+ const rawKey = pathSegs.length > 1 ? rawPathText : void 0;
1176
+ let base = new Reference({
1177
+ key: nameKey,
1178
+ ...rawKey ? { rawKey } : {}
1179
+ }, {
1180
+ type: "mixin-ruleset",
1181
+ role: "name"
1182
+ }, loc);
1183
+ if (i >= comps.length || !isSquareParen(comps[i]) && !isRoundParen(comps[i])) return i === comps.length ? base : comps;
1184
+ while (i < comps.length) {
1185
+ const item = comps[i];
1186
+ if (isSquareParen(item)) {
1187
+ const innerKey = this._decodeAccessorKey(item, loc);
1188
+ const accType = typeof innerKey === "string" || innerKey != null && typeof innerKey === "object" && innerKey.type === "Reference" ? "variable" : "index";
1189
+ base = new Reference({
1190
+ target: base,
1191
+ key: innerKey
1192
+ }, { type: accType }, loc);
1193
+ i++;
1194
+ } else if (isRoundParen(item)) {
1195
+ const innerContent = item.value ?? item.node;
1196
+ const argsNode = this._isEmptyKeywordLike(innerContent) ? null : this._parenToArgs(item, loc);
1197
+ const callPayload = { name: base };
1198
+ if (argsNode) callPayload.args = argsNode;
1199
+ base = new Call(callPayload, {}, loc);
1200
+ i++;
1201
+ } else break;
1202
+ }
1203
+ return i === comps.length ? base : comps;
1204
+ }
1205
+ _buildEscapedValue(children, loc) {
1206
+ const inner = nodeChildren(children)[0];
1207
+ if (!inner) return this._lessKeyword("", loc);
1208
+ if (inner instanceof Quoted) return new Quoted(inner.value, {
1209
+ quote: inner.quote,
1210
+ escaped: true
1211
+ }, loc);
1212
+ const n = inner;
1213
+ n._options = {
1214
+ ...n._options ?? {},
1215
+ escaped: true
1216
+ };
1217
+ return inner;
1218
+ }
1219
+ _buildInterpValue(raw, loc) {
1220
+ const result = getInterpolatedOrString(spannedComponents(raw).map((i) => typeof i.comp === "string" ? i.comp : "").join(""), loc);
1221
+ if (typeof result === "string") return this._lessKeyword(result, loc);
1222
+ return result;
1223
+ }
1224
+ /**
1225
+ * A quoted string value. Unlike plain CSS, Less interpolates `@{var}` / `${prop}`
1226
+ * inside quoted (and escaped `~"…"`) strings and inside `@import` paths. When the
1227
+ * raw content holds an interpolation, split it into an `Interpolated` value the same
1228
+ * way the reference parser's `processStringInterpolation` does (source with
1229
+ * INTERPOLATION_PLACEHOLDER, `@var`/`$prop` references in `replacements`); otherwise
1230
+ * fall through to the plain CSS builder (bare-string value).
1231
+ */
1232
+ /**
1233
+ * The Less `Url` grammar tokenizes the inner string as bare leaves (no child
1234
+ * node), so the css base builder wraps a quoted url body in a raw-string
1235
+ * `Quoted` — which never interpolates `@{var}`/`${prop}`. Less 4.x DOES resolve
1236
+ * interpolation inside a QUOTED url body (`url("@{base}/@{i}.svg")`), the same as
1237
+ * any other quoted string, so route the quoted inner through the same
1238
+ * interpolation-aware construction `_buildQuoted` uses. Unquoted url bodies stay
1239
+ * verbatim (Less 4.x leaves `url(@{x})` literal), as does a quoted body with no
1240
+ * interpolation.
1241
+ */
1242
+ _buildUrl(children, loc) {
1243
+ if (nodeChildren(children)[0]) return super._buildUrl(children, loc);
1244
+ const inner = children.filter((c) => c._tag === "leaf").filter((l) => !/^url\($/i.test(l.value) && l.value !== ")").map((l) => l.value).join("").trim();
1245
+ const quote = inner[0];
1246
+ if ((quote === "\"" || quote === "'") && inner.at(-1) === quote) {
1247
+ const body = inner.slice(1, -1);
1248
+ if (body.includes("@{") || body.includes("${")) return new Url(new Quoted(this._buildStringInterpolation(body, loc), { quote }, loc), void 0, loc);
1249
+ }
1250
+ return super._buildUrl(children, loc);
1251
+ }
1252
+ _buildQuoted(children, loc) {
1253
+ const text = children.filter((c) => c._tag === "leaf").map((l) => l.value).join("");
1254
+ const inner = text.slice(1, -1);
1255
+ if (inner.includes("@{") || inner.includes("${")) return new Quoted(this._buildStringInterpolation(inner, loc), { quote: text[0] }, loc);
1256
+ return super._buildQuoted(children, loc);
1257
+ }
1258
+ /**
1259
+ * Build an escaped `~'…'` Quoted (at-rule prelude position), interpolating any
1260
+ * `@{var}` / `${prop}` in its body the same way `_buildQuoted` does — so
1261
+ * `~'@{a} / @{b}'` renders its substituted values instead of literal text.
1262
+ */
1263
+ _buildEscapedQuoted(inner, quote, loc) {
1264
+ return new Quoted(inner.includes("@{") || inner.includes("${") ? this._buildStringInterpolation(inner, loc) : inner, {
1265
+ quote,
1266
+ escaped: true
1267
+ }, loc);
1268
+ }
1269
+ /**
1270
+ * Split a quoted-string body on `@{…}` / `${…}` interpolations into an
1271
+ * `Interpolated` (source + reference replacements). Port of the reference parser's
1272
+ * `processStringInterpolation`/`findInterpolations` (productions/values.ts): brace
1273
+ * matching is nesting-aware, and a nested-interpolated name resolves through a
1274
+ * variable Reference wrapped in an Expression.
1275
+ */
1276
+ _buildStringInterpolation(value, loc) {
1277
+ const matches = this._findInterpolations(value);
1278
+ const replacements = [];
1279
+ let source = value;
1280
+ let offset = 0;
1281
+ for (const match of matches) {
1282
+ const adjustedStart = match.start - offset;
1283
+ const adjustedEnd = match.end - offset;
1284
+ source = source.slice(0, adjustedStart) + INTERPOLATION_PLACEHOLDER + source.slice(adjustedEnd);
1285
+ offset += match.end - match.start - INTERPOLATION_PLACEHOLDER.length;
1286
+ if (match.content.includes("@{") || match.content.includes("${")) {
1287
+ const nestedRef = new Reference({ key: this._buildStringInterpolation(match.content, loc) }, {
1288
+ type: "variable",
1289
+ role: "ident"
1290
+ }, loc);
1291
+ replacements.push(new Expression(nestedRef, void 0, loc));
1292
+ } else replacements.push(createInterpolatedReference(match.prefix, match.content, loc));
1293
+ }
1294
+ return new Interpolated({
1295
+ source,
1296
+ replacements
1297
+ }, { role: "ident" }, loc);
1298
+ }
1299
+ /**
1300
+ * Locate `@{…}` / `${…}` interpolation runs in a string, counting nested braces so
1301
+ * `@{@{x}}` and `@{fn(a, b)}` are matched whole. Returns start/end/prefix/content.
1302
+ */
1303
+ _findInterpolations(value) {
1304
+ const matches = [];
1305
+ let i = 0;
1306
+ while (i < value.length) if ((value[i] === "@" || value[i] === "$") && value[i + 1] === "{") {
1307
+ const prefix = value[i];
1308
+ const start = i;
1309
+ i += 2;
1310
+ let braceCount = 1;
1311
+ const contentStart = i;
1312
+ while (i < value.length && braceCount > 0) {
1313
+ if (value[i] === "{") braceCount++;
1314
+ else if (value[i] === "}") braceCount--;
1315
+ i++;
1316
+ }
1317
+ if (braceCount === 0) matches.push({
1318
+ start,
1319
+ end: i,
1320
+ prefix,
1321
+ content: value.slice(contentStart, i - 1)
1322
+ });
1323
+ } else i++;
1324
+ return matches;
1325
+ }
1326
+ _buildCall(rawChildren, loc) {
1327
+ const call = super._buildCall(rawChildren, loc);
1328
+ const key = typeof call.name === "string" ? call.name : "";
1329
+ this._checkMixedArgDelimiters(call.args, "function", loc);
1330
+ const loweredArgs = this._lowerSemiArgs(call.args, loc);
1331
+ return new Call({
1332
+ name: new Reference(key, {
1333
+ type: "function",
1334
+ fallbackValue: true
1335
+ }, loc),
1336
+ args: loweredArgs
1337
+ }, { silentFail: true }, loc);
1338
+ }
1339
+ _buildLessCustomDecl(children, loc) {
1340
+ const ls = children.filter((c) => c._tag === "leaf");
1341
+ const propNameText = ls[0]?.value ?? "";
1342
+ const name = propNameText.includes("@") || propNameText.includes("$") ? getInterpolatedNode(propNameText, loc) : propNameText;
1343
+ const valueNodes = nodeChildren(children);
1344
+ if (valueNodes.length > 0) return new CustomDeclaration({
1345
+ name,
1346
+ value: valueNodes.length === 1 ? valueNodes[0] : valueNodes
1347
+ }, void 0, loc);
1348
+ const valueText = ls.slice(2).filter((l) => l.value !== ";").map((l) => l.value).join("").trim();
1349
+ return new CustomDeclaration({
1350
+ name,
1351
+ value: this._lessKeyword(valueText, loc)
1352
+ }, void 0, loc);
1353
+ }
1354
+ /**
1355
+ * `--foo: { color: @a; }` — a curly-brace custom-property value whose body
1356
+ * opportunistically structured as a declaration list (customCurlyBlock in the
1357
+ * grammar), so nested `@var`/calls evaluate normally instead of staying opaque
1358
+ * text. Wrapped in a Block(type: 'curly') so `{`/`}` re-render around it.
1359
+ */
1360
+ _buildLessCustomBlock(children, loc) {
1361
+ return new Block(new Sequence(nodeChildren(children), void 0, loc), { type: "curly" }, loc);
1362
+ }
1363
+ _warnDeprecatedValue(span) {
1364
+ const text = this._source.slice(span.start, span.end);
1365
+ if (/\d\s*\.\//.test(text)) this._warn("The ./ operator is deprecated and will be removed.", "dot-slash-operator");
1366
+ }
1367
+ _warnCustomPropVars(span) {
1368
+ const text = this._source.slice(span.start, span.end);
1369
+ const colon = text.indexOf(":");
1370
+ const value = colon >= 0 ? text.slice(colon + 1) : text;
1371
+ const at = value.match(/@[a-zA-Z][\w-]*/);
1372
+ if (at && !value.includes("@{")) this._warn(`"${at[0]}" in custom property values is treated as literal text. Use @{${at[0].slice(1)}} for interpolation.`, "variable-in-unknown-value");
1373
+ const dollar = value.match(/\$[a-zA-Z][\w-]*/);
1374
+ if (dollar && !value.includes("${")) this._warn(`"${dollar[0]}" in custom property values is treated as literal text. Use \${${dollar[0].slice(1)}} for interpolation.`, "property-in-unknown-value");
1375
+ }
1376
+ _warnAtRulePreludeVars(span) {
1377
+ const text = this._source.slice(span.start, span.end);
1378
+ const varName = this._firstTopLevelBareAtVar(text);
1379
+ if (varName !== null) this._warn(`A bare "@${varName}" in an at-rule prelude is deprecated. Use @{${varName}} interpolation instead.`, "variable-in-at-rule-prelude");
1380
+ }
1381
+ /**
1382
+ * The first bare `@ident` reference in an at-rule prelude that is deprecated
1383
+ * under Less 4.x PR #4462 (`variable-in-at-rule-prelude`), or null when there
1384
+ * is none. A bare `@var` in a *structural* (top-level) prelude position still
1385
+ * resolves but is deprecated in favour of `@{var}` interpolation; the scan
1386
+ * therefore ignores, mirroring `hasTopLevelBareVariable` / `warnBareAtRuleVariable`:
1387
+ * - the leading at-rule name itself (`@media`, `@-moz-document`, …);
1388
+ * - `@{ident}` interpolation — the supported migration target;
1389
+ * - a `@var` inside `(...)` — a declaration/feature value (e.g. the `@size`
1390
+ * in `@media (min-width: @size)`), which stays valid;
1391
+ * - `@`/`(` characters inside string literals, which are not structural.
1392
+ */
1393
+ _firstTopLevelBareAtVar(text) {
1394
+ let depth = 0;
1395
+ let i = /^\s*@-?[\w-]+/.exec(text)?.[0].length ?? 0;
1396
+ for (; i < text.length; i++) {
1397
+ const c = text[i];
1398
+ if (c === "\"" || c === "'") {
1399
+ i++;
1400
+ while (i < text.length && text[i] !== c) i++;
1401
+ continue;
1402
+ }
1403
+ if (c === "@") {
1404
+ if (text[i + 1] === "{") {
1405
+ i += 2;
1406
+ while (i < text.length && text[i] !== "}") i++;
1407
+ continue;
1408
+ }
1409
+ if (depth === 0) {
1410
+ const m = /^@(-?[a-zA-Z\x80-￿][\w-]*)/.exec(text.slice(i));
1411
+ if (m) return m[1];
1412
+ }
1413
+ continue;
1414
+ }
1415
+ if (c === "{") break;
1416
+ if (c === "(") depth++;
1417
+ else if (c === ")" && depth > 0) depth--;
1418
+ }
1419
+ return null;
1420
+ }
1421
+ _buildMixinCall(children, raw, loc) {
1422
+ const ls = children.filter((c) => c._tag === "leaf");
1423
+ const markImportant = ls.some((l) => l.value === "!");
1424
+ const nameParts = [];
1425
+ for (const l of ls) {
1426
+ if (l.value === "(" || l.value === ";" || l.value === "!") break;
1427
+ nameParts.push(l.value);
1428
+ }
1429
+ const name = nameParts.join("");
1430
+ const argsList = nodeChildren(children).find((n) => n.type === "List");
1431
+ const hasArgs = argsList && argsList.value?.length;
1432
+ if (argsList === void 0) this._warn("Calling a mixin without parentheses is deprecated", "mixin-call-no-parens");
1433
+ else {
1434
+ const src = this._source.slice(loc.start, loc.end);
1435
+ if (/^\S+\s+\(/.test(src)) this._warn("Whitespace between a mixin name and parentheses is deprecated", "mixin-call-whitespace");
1436
+ }
1437
+ return new Call({
1438
+ name: new Reference({ key: name }, {
1439
+ type: "mixin-ruleset",
1440
+ role: "name"
1441
+ }, loc),
1442
+ args: hasArgs ? this._convertArgsForCall(argsList, loc) : void 0
1443
+ }, { markImportant }, loc);
1444
+ }
1445
+ /**
1446
+ * `@name(...)` (no `:`) → a detached-ruleset variable CALL. Faithful port of
1447
+ * `varDeclarationOrCall`'s LParen branch (selectors.ts): build a `Reference`
1448
+ * over the var name (`type: 'variable', role: 'name'`), wrap in a `Call` with
1449
+ * the (optional) args, and wrap THAT in an `Expression` (a top-level variable
1450
+ * call is an expression, not a parenthesized one). `!important` sets
1451
+ * `markImportant` on the Call, mirroring the production.
1452
+ */
1453
+ _buildVarCall(children, raw, loc) {
1454
+ const ls = children.filter((c) => c._tag === "leaf");
1455
+ const markImportant = ls.some((l) => l.value === "!");
1456
+ const rawName = ls.find((l) => l.value.startsWith("@"))?.value ?? "";
1457
+ const name = rawName.startsWith("@") ? rawName.slice(1) : rawName;
1458
+ const nameRef = new Reference({ key: this._lessKeyword(name, loc) }, {
1459
+ type: "variable",
1460
+ role: "name"
1461
+ }, loc);
1462
+ const argsList = nodeChildren(children).find((n) => n.type === "List");
1463
+ const hasArgs = argsList && argsList.value?.length;
1464
+ if (!hasArgs && KNOWN_AT_RULE_VAR_NAME_RE.test(name)) this._warn("Using known at-rule names as variables is deprecated", "at-rule-variable");
1465
+ return new Expression(new Call({
1466
+ name: nameRef,
1467
+ args: hasArgs ? this._convertArgsForCall(argsList, loc) : void 0
1468
+ }, markImportant ? { markImportant: true } : void 0, loc), void 0, loc);
1469
+ }
1470
+ /** `...` or `@name...` variadic arg → `Rest`. Definition-shape (string name); a
1471
+ * CALL turns it into `Rest(Reference)` via `_convertArgsForCall`. */
1472
+ _buildRest(raw, loc) {
1473
+ const nameItem = spannedComponents(raw).find((i) => typeof i.comp === "string" && i.comp.startsWith("@"));
1474
+ return new Rest(nameItem ? String(nameItem.comp).slice(1) : "", {}, loc);
1475
+ }
1476
+ /** `@name: value` named arg/param → `VarDeclaration`. The value is assembled by the
1477
+ * shared value builder (`_assembleValue`) — the same machinery as a declaration
1478
+ * value, so trivia and Keyword-ification are handled and no manual trimming is
1479
+ * needed. Named args flow through function calls too; the runtime decides whether
1480
+ * the target accepts them. */
1481
+ _buildNamedArg(raw, loc) {
1482
+ const items = spannedComponents(raw);
1483
+ const colonIdx = items.findIndex((i) => i.comp === ":");
1484
+ const nameItem = items.find((i) => typeof i.comp === "string" && i.comp.startsWith("@"));
1485
+ const name = String(nameItem?.comp ?? "").slice(1);
1486
+ const valueItems = colonIdx >= 0 ? items.slice(colonIdx + 1) : [];
1487
+ const { value } = this._assembleValue(valueItems, loc);
1488
+ let paramValue;
1489
+ if (typeof value === "string") paramValue = this._valueKeyword(value, loc);
1490
+ else if (Array.isArray(value)) paramValue = new Sequence(value.map((c) => this._argComponent(c, loc)), void 0, loc);
1491
+ else paramValue = value;
1492
+ return new VarDeclaration({
1493
+ name,
1494
+ value: paramValue
1495
+ }, {}, loc);
1496
+ }
1497
+ /** Mixin-call args are assembled by the SAME builder as function-call args
1498
+ * (`_assembleArgs` via `_betweenParens`) — identical comma/semicolon and value
1499
+ * handling. Named args are `VarDeclaration`s and variadic args `Rest`, which pass
1500
+ * through as single components. A bare `@name` is a `Reference` (the call shape);
1501
+ * the mixin-DEFINITION builder reinterprets a lone `@name` as a param. */
1502
+ _buildMixinArgs(raw, loc) {
1503
+ const inner = this._betweenParens(spannedComponents(raw));
1504
+ const args = this._assembleArgs(inner, loc);
1505
+ this._checkMixedArgDelimiters(args, "mixin", loc);
1506
+ return this._lowerSemiArgs(args, loc);
1507
+ }
1508
+ /** Less forbids mixing the COMMA and SEMICOLON argument separators: once a
1509
+ * semicolon separates args, a comma is a value-list separator, so a semicolon-group
1510
+ * may not hold 2+ named params (`@a: 1, @b: 2`). `_assembleArgs` renders such a group
1511
+ * as a List of ≥2 VarDeclarations. (This is purely about the `,` vs `;` argument
1512
+ * separators — a `/` inside a value is unrelated and never checked.) Applies to BOTH
1513
+ * mixin and function calls (args are unified). */
1514
+ _checkMixedArgDelimiters(args, kind, loc) {
1515
+ const list = args;
1516
+ if (list?.type !== "List" || list.options?.sep !== ";" || !Array.isArray(list.value)) return;
1517
+ for (const el of list.value) {
1518
+ const group = el;
1519
+ if (group?.type === "List" && Array.isArray(group.value) && group.value.filter((n) => n?.type === "VarDeclaration").length >= 2) {
1520
+ this._error(`Cannot mix ; and , as delimiter types in ${kind} arguments`, loc.start);
1521
+ break;
1522
+ }
1523
+ }
1524
+ }
1525
+ /**
1526
+ * Lower Less `;`-separated call args to the unified Jess representation: the outer
1527
+ * args `List{ sep: ';' }` becomes comma-separated, and each element that is itself
1528
+ * a comma-`List` (a `;`-group that held a comma-list) is wrapped in an escaped
1529
+ * `Paren` — the same shape Jess authors write as `~(1, 2)`. So
1530
+ * `.mixin(1, 2; 3, 4)` and Jess `mixin(~(1, 2), ~(3, 4))` converge on one AST.
1531
+ *
1532
+ * The escaped `Paren` evaluates to its inner value STRIPPED (paren.ts §escaped),
1533
+ * so `~(1, 2)` binds/renders identically to the bare list `1, 2` — representation
1534
+ * only, semantics unchanged. Scalar (non-List) elements pass through untouched.
1535
+ *
1536
+ * MUST run AFTER `_checkMixedArgDelimiters` (which inspects the `;`-List).
1537
+ */
1538
+ _lowerSemiArgs(args, loc) {
1539
+ const list = args;
1540
+ if (!list || list.type !== "List" || list.options?.sep !== ";" || !Array.isArray(list.value)) return args;
1541
+ return new List(list.value.map((el) => {
1542
+ if (el?.type === "List") return new Paren(el, { escaped: true }, loc);
1543
+ return el;
1544
+ }), void 0, loc);
1545
+ }
1546
+ /**
1547
+ * Mixin-DEFINITION param conversion. With combinator-composed args a bare `@name`
1548
+ * value parses as a `Reference{variable}` (the CALL shape); in a DEFINITION it is a
1549
+ * param, so convert it to `VarDeclaration(name, Nil)`. Named params (`@a: 1`),
1550
+ * variadic (`Rest`) and pattern-match values stay as-is. Returns a NEW List (the
1551
+ * def/call split must not mutate a shared node).
1552
+ */
1553
+ _convertArgsForDefinition(argsList, loc) {
1554
+ if (!argsList || argsList.type !== "List") return argsList;
1555
+ const list = argsList;
1556
+ const value = list.value;
1557
+ if (!value || value.length === 0) return argsList;
1558
+ let changed = false;
1559
+ const converted = value.map((node) => {
1560
+ if (node.type === "Reference" && node.options?.type === "variable") {
1561
+ const key = node.key;
1562
+ const name = typeof key === "string" ? key : String(key?.valueOf?.() ?? "");
1563
+ changed = true;
1564
+ return new VarDeclaration({
1565
+ name,
1566
+ value: new Nil("", {}, loc)
1567
+ }, {}, loc);
1568
+ }
1569
+ return node;
1570
+ });
1571
+ if (!changed) return argsList;
1572
+ return new List(converted, list.options, loc);
1573
+ }
1574
+ /**
1575
+ * Mixin-CALL argument conversion (reference `convertArgsForCall`, root.ts).
1576
+ * `_buildMixinArgs` builds bare `@name` args as definition-style VarDeclarations
1577
+ * (Nil value) — correct for a DEFINITION param, but in a CALL a bare `@name` is a
1578
+ * variable being PASSED, i.e. a `Reference{type:variable}`. Named args (`@a: 1`)
1579
+ * and value args stay as-is; a `Rest('name')` becomes `Rest(Reference{variable})`.
1580
+ * Returns a NEW List (the def/call split must not mutate a shared node).
1581
+ */
1582
+ _convertArgsForCall(argsList, loc) {
1583
+ if (!argsList || argsList.type !== "List") return argsList;
1584
+ const list = argsList;
1585
+ const value = list.value;
1586
+ if (!value || value.length === 0) return argsList;
1587
+ let changed = false;
1588
+ const converted = value.map((node) => {
1589
+ if (node.type === "VarDeclaration") {
1590
+ const decl = node;
1591
+ const val = decl.value;
1592
+ if (!val || val.type === "Nil") {
1593
+ const key = decl.name.valueOf();
1594
+ changed = true;
1595
+ return new Reference({ key }, { type: "variable" }, loc);
1596
+ }
1597
+ return node;
1598
+ }
1599
+ if (node.type === "Rest") {
1600
+ const restVal = node.value;
1601
+ if (typeof restVal === "string") {
1602
+ changed = true;
1603
+ return new Rest(new Reference({ key: restVal }, { type: "variable" }, loc), {}, loc);
1604
+ }
1605
+ return node;
1606
+ }
1607
+ return node;
1608
+ });
1609
+ if (!changed) return argsList;
1610
+ return new List(converted, list.options, loc);
1611
+ }
1612
+ _buildAnonMixin(children, loc) {
1613
+ const nodes = nodeChildren(children);
1614
+ const argsList = nodes.find((n) => n.type === "List");
1615
+ const rules = nodes.filter((n) => n !== argsList);
1616
+ const defParams = this._convertArgsForDefinition(argsList, loc);
1617
+ return new Mixin({
1618
+ params: defParams?.value?.length ? defParams : void 0,
1619
+ rules
1620
+ }, void 0, loc);
1621
+ }
1622
+ /**
1623
+ * `each(<iterable>, { … })` → a `For` control node (the $for shape), not a Call.
1624
+ * The value(s) before the comma are the iterable; the callback block's body becomes
1625
+ * the loop rules. A literal block callback carries no captured params here, so the
1626
+ * pattern defaults to the Less `[value, key, index]` triple.
1627
+ */
1628
+ /** A bare detached ruleset `{ … }` in value / argument position → a Mixin holding
1629
+ * its rules (same shape `@var: { … }` produces in `_buildVarDeclaration`). */
1630
+ _buildDetachedRuleset(children, loc) {
1631
+ return new Mixin({ rules: nodeChildren(children) }, {}, loc);
1632
+ }
1633
+ _buildEachFor(children, loc) {
1634
+ const nodes = nodeChildren(children);
1635
+ const callback = nodes.find((n) => n.type === "Mixin");
1636
+ const iterableNodes = nodes.filter((n) => n !== callback);
1637
+ const paramsList = (callback?.params)?.type === "List" ? callback.params : void 0;
1638
+ const ruleNodes = callback?.rules ?? [];
1639
+ const iterable = iterableNodes.length === 1 ? iterableNodes[0] : new List(iterableNodes, void 0, loc);
1640
+ return new For({
1641
+ pattern: this._eachPattern(paramsList, loc),
1642
+ iterable: {
1643
+ kind: "node",
1644
+ value: iterable
1645
+ },
1646
+ rules: ruleNodes
1647
+ }, void 0, loc);
1648
+ }
1649
+ _eachPattern(paramsList, loc) {
1650
+ const params = (paramsList?.value ?? []).filter((p) => p?.type === "VarDeclaration");
1651
+ if (params.length === 1) return {
1652
+ kind: "single",
1653
+ value: params[0]
1654
+ };
1655
+ if (params.length >= 2) return {
1656
+ kind: "tuple",
1657
+ values: [params[0], ...params.slice(1)]
1658
+ };
1659
+ const paramVar = (name) => new VarDeclaration({
1660
+ name,
1661
+ value: this._lessKeyword("", loc)
1662
+ }, { paramVar: true }, loc);
1663
+ return {
1664
+ kind: "tuple",
1665
+ values: [
1666
+ paramVar("value"),
1667
+ paramVar("key"),
1668
+ paramVar("index")
1669
+ ]
1670
+ };
1671
+ }
1672
+ _buildMixinOrQualified(children, loc) {
1673
+ const ls = children.filter((c) => c._tag === "leaf");
1674
+ const nodes = nodeChildren(children);
1675
+ const hasBlock = ls.some((l) => l.value === "{");
1676
+ const markImportant = ls.some((l) => l.value === "!");
1677
+ const nameParts = [];
1678
+ for (const l of ls) {
1679
+ if (l.value === "(" || l.value === "{" || l.value === "}" || l.value === ";" || l.value === ")" || l.value === "!") break;
1680
+ nameParts.push(l.value);
1681
+ }
1682
+ const name = nameParts.join("");
1683
+ const argsList = nodes.find((n) => n.type === "List");
1684
+ const guard = nodes.find((n) => n.type === "Paren" || n.type === "Condition" || n.type === "DefaultGuard");
1685
+ const hasExplicitParens = argsList !== void 0;
1686
+ if (hasBlock) {
1687
+ const rawRuleNodes = nodes.filter((n) => n !== argsList && n !== guard);
1688
+ const braceIdx = this._source.indexOf("{", loc.start);
1689
+ const bodyStart = braceIdx >= 0 ? braceIdx + 1 : loc.start;
1690
+ const closeIdx = this._source.lastIndexOf("}", loc.end - 1);
1691
+ const bodyEnd = closeIdx >= bodyStart ? closeIdx : loc.end;
1692
+ const ruleNodes = this._liftStandaloneComments(rawRuleNodes, bodyStart, bodyEnd, loc);
1693
+ if (hasExplicitParens) {
1694
+ const hasDefault = (guard !== void 0 ? guard.toTrimmedString?.() ?? "" : "").includes("default");
1695
+ const defParams = this._convertArgsForDefinition(argsList, loc);
1696
+ return new Mixin({
1697
+ name,
1698
+ params: defParams?.value?.length ? defParams : void 0,
1699
+ rules: ruleNodes,
1700
+ guard
1701
+ }, { hasDefault: !!hasDefault }, loc);
1702
+ }
1703
+ const { cleanedSelector, extractedExtends } = this._extractExtendsFromSelectorText(name || "&", loc);
1704
+ const finalRules = extractedExtends.length > 0 ? [...extractedExtends, ...ruleNodes] : ruleNodes;
1705
+ return new Ruleset({
1706
+ selector: cleanedSelector || "&",
1707
+ rules: finalRules,
1708
+ guard
1709
+ }, void 0, loc);
1710
+ }
1711
+ const combinatorValues = new Set([
1712
+ ">",
1713
+ "+",
1714
+ "~"
1715
+ ]);
1716
+ const selectorTokens = nameParts.filter((p) => !combinatorValues.has(p.trim()) && p.trim() !== "");
1717
+ const hasComplexPath = selectorTokens.length > 1;
1718
+ const refKey = hasComplexPath ? selectorTokens : name;
1719
+ const rawKey = hasComplexPath ? new ComplexSelector(nameParts, void 0, loc) : void 0;
1720
+ const ref = name.startsWith(".") || name.startsWith("#") ? new Reference({
1721
+ key: refKey,
1722
+ ...rawKey ? { rawKey } : {}
1723
+ }, {
1724
+ type: "mixin-ruleset",
1725
+ role: "name"
1726
+ }, loc) : new Reference({ key: refKey }, {
1727
+ type: "function",
1728
+ silentFail: true,
1729
+ fallbackValue: true
1730
+ }, loc);
1731
+ const hasArgs2 = argsList && argsList.value?.length;
1732
+ const hasSemi = ls.some((l) => l.value === ";");
1733
+ if (hasSemi && !hasExplicitParens) this._warn("Calling a mixin without parentheses is deprecated", "mixin-call-no-parens");
1734
+ else if (hasSemi && hasExplicitParens) {
1735
+ const src = this._source.slice(loc.start, loc.end);
1736
+ if (/^\S+\s+\(/.test(src)) this._warn("Whitespace between a mixin name and parentheses is deprecated", "mixin-call-whitespace");
1737
+ }
1738
+ return new Call({
1739
+ name: ref,
1740
+ args: hasArgs2 ? this._convertArgsForCall(argsList, loc) : void 0
1741
+ }, { markImportant }, loc);
1742
+ }
1743
+ _extractExtendsFromSelectorText(selectorText, _loc) {
1744
+ return {
1745
+ cleanedSelector: selectorText,
1746
+ extractedExtends: []
1747
+ };
1748
+ }
1749
+ _selectorHasNestedExtend(sel) {
1750
+ if (!sel || typeof sel === "string") return false;
1751
+ if (sel.type === "PseudoSelector") {
1752
+ const arg = sel.arg;
1753
+ return arg ? this._treeHasExtend(arg) : false;
1754
+ }
1755
+ if (sel instanceof CompoundSelector || sel instanceof ComplexSelector) return sel.value.some((p) => this._selectorHasNestedExtend(p));
1756
+ if (isSelectorListLike(sel)) return selectorListItems(sel).some((p) => this._selectorHasNestedExtend(p));
1757
+ return false;
1758
+ }
1759
+ _treeHasExtend(node) {
1760
+ if (node instanceof Extend) return true;
1761
+ if (node instanceof CompoundSelector || node instanceof ComplexSelector) return node.value.some((p) => this._treeHasExtend(p));
1762
+ if (isSelectorListLike(node)) return selectorListItems(node).some((p) => this._treeHasExtend(p));
1763
+ return false;
1764
+ }
1765
+ /**
1766
+ * A guarded ruleset (`sel when …`) is parsed by the shared CSS builder, which
1767
+ * has no `when` concept, so the Guard CST child folds into the body as the
1768
+ * first rule — always a Paren/Condition/DefaultGuard. Lift it into the
1769
+ * ruleset's `guard` field (rebuilt through the canonical Ruleset ctor so the
1770
+ * guard is adopted) so it gates output instead of rendering as a `{ true }`
1771
+ * body. Non-guarded rulesets never begin their body with one of these node
1772
+ * types, so the leading-node check is unambiguous.
1773
+ */
1774
+ _liftRulesetGuard(base, loc) {
1775
+ const rules = base.rules;
1776
+ if (!Array.isArray(rules) || rules.length === 0) return base;
1777
+ const first = rules[0];
1778
+ if (first?.type !== "Paren" && first?.type !== "Condition" && first?.type !== "DefaultGuard") return base;
1779
+ return new Ruleset({
1780
+ selector: base.selector,
1781
+ rules: rules.slice(1),
1782
+ guard: first
1783
+ }, void 0, loc);
1784
+ }
1785
+ _buildRuleset(children, rawChildren, loc) {
1786
+ let base = super._buildRuleset(children, rawChildren, loc);
1787
+ const selector = base.selector;
1788
+ if (!selector) return base;
1789
+ base = this._liftRulesetGuard(base, loc);
1790
+ if (typeof selector === "string") return base;
1791
+ if (this._selectorHasNestedExtend(selector)) this._error(":extend() is not allowed inside a pseudo-class selector", loc.start);
1792
+ const baseRules = Array.isArray(base.rules) ? base.rules : [];
1793
+ const baseGuard = base.guard;
1794
+ const withGuard = (rs) => {
1795
+ if (baseGuard !== void 0) rs.guard = baseGuard;
1796
+ return rs;
1797
+ };
1798
+ const extendKey = (e) => {
1799
+ const ext = e;
1800
+ return `${String(ext.target?.valueOf?.() ?? ext.target)}:${ext.flag}`;
1801
+ };
1802
+ if (!isSelectorListLike(selector)) {
1803
+ const { cleanedSelector, extractedExtends } = this._extractExtendsFromSelector(selector, loc);
1804
+ if (extractedExtends.length === 0) return base;
1805
+ return withGuard(new Ruleset({
1806
+ selector: cleanedSelector,
1807
+ rules: [...extractedExtends, ...baseRules]
1808
+ }, void 0, loc));
1809
+ }
1810
+ const perSelector = [];
1811
+ let anyExtends = false;
1812
+ for (const item of selectorListItems(selector)) {
1813
+ const { cleanedSelector: cs, extractedExtends: ee } = this._extractExtendsFromSelector(item, loc);
1814
+ perSelector.push({
1815
+ clean: cs,
1816
+ extends: ee
1817
+ });
1818
+ if (ee.length > 0) anyExtends = true;
1819
+ }
1820
+ if (!anyExtends) return base;
1821
+ const allExtendKeys = perSelector.map((s) => s.extends.map(extendKey).sort().join("|"));
1822
+ if (allExtendKeys.every((k) => k === allExtendKeys[0])) {
1823
+ const uniqueExtends = perSelector[0].extends;
1824
+ const cleanedItems = perSelector.map((s) => s.clean).filter((c) => c !== void 0);
1825
+ return withGuard(new Ruleset({
1826
+ selector: cleanedItems.length === 1 ? cleanedItems[0] : this._makeSelectorList(cleanedItems, loc),
1827
+ rules: [...uniqueExtends, ...baseRules]
1828
+ }, void 0, loc));
1829
+ }
1830
+ const wrapperRules = [];
1831
+ const cleanedItems = [];
1832
+ for (const { clean, extends: exts } of perSelector) {
1833
+ for (const ext of exts) {
1834
+ const extNode = ext;
1835
+ wrapperRules.push(new Extend({
1836
+ target: extNode.target,
1837
+ flag: extNode.flag,
1838
+ selector: clean
1839
+ }, {}, loc));
1840
+ }
1841
+ if (clean !== void 0) cleanedItems.push(clean);
1842
+ }
1843
+ const combinedSel = cleanedItems.length === 1 ? cleanedItems[0] : this._makeSelectorList(cleanedItems, loc);
1844
+ wrapperRules.push(withGuard(new Ruleset({
1845
+ selector: combinedSel,
1846
+ rules: baseRules
1847
+ }, void 0, loc)));
1848
+ return new Rules(wrapperRules, void 0, loc);
1849
+ }
1850
+ _extractExtendsFromSelector(selector, loc) {
1851
+ if (!selector || typeof selector === "string") return {
1852
+ cleanedSelector: selector,
1853
+ extractedExtends: []
1854
+ };
1855
+ if (selector instanceof CompoundSelector) {
1856
+ const extractedExtends = [];
1857
+ const newParts = [];
1858
+ for (const part of selector.value) if (part instanceof Extend) extractedExtends.push(part);
1859
+ else if (part instanceof List) for (const item of part.value ?? []) if (item instanceof Extend) extractedExtends.push(item);
1860
+ else newParts.push(item);
1861
+ else newParts.push(part);
1862
+ if (extractedExtends.length === 0) return {
1863
+ cleanedSelector: selector,
1864
+ extractedExtends: []
1865
+ };
1866
+ return {
1867
+ cleanedSelector: newParts.length === 0 ? "&" : newParts.length === 1 ? newParts[0] : new CompoundSelector(newParts, void 0, loc),
1868
+ extractedExtends
1869
+ };
1870
+ }
1871
+ if (selector instanceof ComplexSelector) {
1872
+ const allExtends = [];
1873
+ const newParts = [];
1874
+ for (const part of selector.value) if (part instanceof Extend) allExtends.push(part);
1875
+ else if (part instanceof List) for (const item of part.value ?? []) if (item instanceof Extend) allExtends.push(item);
1876
+ else newParts.push(item);
1877
+ else if (part instanceof CompoundSelector) {
1878
+ const { cleanedSelector: cs, extractedExtends: ee } = this._extractExtendsFromSelector(part, loc);
1879
+ allExtends.push(...ee);
1880
+ if (cs !== void 0) newParts.push(cs);
1881
+ } else newParts.push(part);
1882
+ if (allExtends.length === 0) return {
1883
+ cleanedSelector: selector,
1884
+ extractedExtends: []
1885
+ };
1886
+ return {
1887
+ cleanedSelector: newParts.length === 1 ? newParts[0] : new ComplexSelector(newParts, void 0, loc),
1888
+ extractedExtends: allExtends
1889
+ };
1890
+ }
1891
+ if (isSelectorListLike(selector)) {
1892
+ const allExtends = [];
1893
+ const cleanedItems = [];
1894
+ let changed = false;
1895
+ for (const item of selectorListItems(selector)) {
1896
+ const { cleanedSelector: cs, extractedExtends: ee } = this._extractExtendsFromSelector(item, loc);
1897
+ allExtends.push(...ee);
1898
+ if (ee.length > 0) changed = true;
1899
+ if (cs !== void 0) cleanedItems.push(cs);
1900
+ }
1901
+ if (!changed) return {
1902
+ cleanedSelector: selector,
1903
+ extractedExtends: []
1904
+ };
1905
+ return {
1906
+ cleanedSelector: cleanedItems.length === 1 ? cleanedItems[0] : this._makeSelectorList(cleanedItems, loc),
1907
+ extractedExtends: allExtends
1908
+ };
1909
+ }
1910
+ return {
1911
+ cleanedSelector: selector,
1912
+ extractedExtends: []
1913
+ };
1914
+ }
1915
+ static _isCssUrl(url, opts) {
1916
+ if (opts.includes("inline") || opts.includes("less")) return false;
1917
+ return url.endsWith(".css") || url.startsWith("http://") || url.startsWith("https://") || url.startsWith("//");
1918
+ }
1919
+ _buildImportAtRuleFromPrelude(children, raw, loc, name) {
1920
+ const preludeText = this._source.slice(loc.start, loc.end);
1921
+ const optMatch = /^\s*\(([^)]+)\)/.exec(preludeText.replace(/^@import\s*/, ""));
1922
+ const opts = optMatch ? optMatch[1].split(",").map((s) => s.trim()) : [];
1923
+ const builtNodes = nodeChildren(children);
1924
+ const urlNode = builtNodes.find((n) => n.type === "Url");
1925
+ const quotedNode = builtNodes.find((n) => n.type === "Quoted");
1926
+ let pathNode;
1927
+ if (quotedNode) pathNode = new Quoted(quotedNode.value, { quote: quotedNode.quote ?? "\"" }, loc);
1928
+ else {
1929
+ const _qm = preludeText.match(/(['"])([^'"]+)\1/);
1930
+ if (_qm) {
1931
+ const quote = _qm[1] === "'" ? "'" : "\"";
1932
+ const innerNode = _qm[2];
1933
+ pathNode = new Quoted(innerNode, { quote }, loc);
1934
+ }
1935
+ }
1936
+ let mediaNode;
1937
+ {
1938
+ let rest = preludeText.replace(/^@-?[_a-zA-Z][-_a-zA-Z0-9]*\s*/, "");
1939
+ rest = rest.replace(/^\([^)]*\)\s*/, "");
1940
+ rest = urlNode ? rest.replace(/url\(\s*(['"])[^'"]*\1\s*\)\s*/i, "") : rest.replace(/(['"])[^'"]*\1\s*/, "");
1941
+ rest = rest.replace(/\bas\s+[^\s;(]+\s*/g, "");
1942
+ rest = rest.replace(/;\s*$/, "").trim();
1943
+ if (rest) mediaNode = this._buildAtRulePrelude(rest, loc);
1944
+ }
1945
+ const pathMatch2 = /['"]([^'"]+)['"]/.exec(preludeText);
1946
+ const pathStr = pathMatch2 ? pathMatch2[1] : "";
1947
+ const isCssImport = pathStr ? LessGrammar._isCssUrl(pathStr, opts) : false;
1948
+ if (!opts.includes("inline") && (isCssImport || opts.includes("css"))) {
1949
+ const preludeItems = [];
1950
+ const pathPrelude = urlNode ?? pathNode;
1951
+ if (pathPrelude) preludeItems.push(pathPrelude);
1952
+ if (mediaNode) preludeItems.push(mediaNode);
1953
+ let prelude;
1954
+ if (preludeItems.length === 1) prelude = preludeItems[0];
1955
+ else {
1956
+ const joined = preludeItems.map((item) => item.toTrimmedString()).join(" ");
1957
+ prelude = preludeItems.every((item) => item.structuralStaticFlag()) ? new Any(joined, void 0, loc) : joined;
1958
+ }
1959
+ return new AtRuleStatement({
1960
+ name,
1961
+ prelude
1962
+ }, void 0, loc);
1963
+ }
1964
+ const isForward = name === "@-export";
1965
+ const importType = isForward ? "compose" : "import";
1966
+ const importOpts = { once: !opts.includes("multiple") };
1967
+ if (opts.includes("reference")) importOpts.reference = true;
1968
+ if (opts.includes("multiple")) importOpts.multiple = true;
1969
+ if (opts.includes("optional")) importOpts.optional = true;
1970
+ if (opts.includes("inline")) importOpts.inline = true;
1971
+ if (opts.includes("less")) importOpts.type = "less";
1972
+ if (mediaNode) importOpts.postlude = mediaNode;
1973
+ if (isForward) importOpts.forward = true;
1974
+ const namespace = /\bas\s+([^\s;(]+)/.exec(preludeText)?.[1];
1975
+ const styleImportOptions = {
1976
+ type: importType,
1977
+ importOptions: importOpts
1978
+ };
1979
+ if (namespace) styleImportOptions.namespace = namespace;
1980
+ return new StyleImport({ path: pathNode ?? urlNode }, styleImportOptions, loc);
1981
+ }
1982
+ _buildAtRuleBlock(children, loc) {
1983
+ const ls = children.filter((c) => c._tag === "leaf");
1984
+ const nameLf = ls[0];
1985
+ const name = nameLf?.value ?? "";
1986
+ if ([
1987
+ "@import",
1988
+ "@-import",
1989
+ "@-export"
1990
+ ].includes(name)) return this._buildImportAtRuleFromPrelude(children, ls, loc, name);
1991
+ if (["@use", "@-use"].includes(name)) return this._buildUseAtRuleFromPrelude(children, loc, name);
1992
+ const nameSpan = nameLf?.span;
1993
+ const braceStart = ls.find((l) => l.value === "{")?.span?.start;
1994
+ let rawPreludeText;
1995
+ if (nameSpan && typeof braceStart === "number") {
1996
+ const sliced = this._source.slice(nameSpan.end, braceStart).trim();
1997
+ rawPreludeText = sliced.length > 0 ? sliced : void 0;
1998
+ } else rawPreludeText = ls.slice(1).find((l) => l.value !== "{" && l.value !== "}")?.value.trim();
1999
+ return this._buildAtRuleFromParts(name, rawPreludeText, nodeChildren(children), loc);
2000
+ }
2001
+ /**
2002
+ * Shared AtRule assembly used by both the flat `AtRuleBlock` builder and the
2003
+ * structured, committed `QueryAtRuleBlock` builder. `preludeText` is the raw
2004
+ * prelude source (already `{`/`}` stripped); routing it through
2005
+ * `_buildAtRulePrelude` keeps the emitted AST identical regardless of which
2006
+ * grammar rule matched.
2007
+ */
2008
+ _buildAtRuleFromParts(name, preludeText, ruleNodes, loc) {
2009
+ const nestableOpts = NESTABLE_AT_RULES.includes(name) ? { nestable: true } : void 0;
2010
+ return new AtRule({
2011
+ name,
2012
+ prelude: preludeText ? this._buildAtRulePrelude(preludeText, loc) : void 0,
2013
+ rules: ruleNodes
2014
+ }, nestableOpts, loc);
2015
+ }
2016
+ /**
2017
+ * Builder for the structured, committed `@media`/`@container`/`@supports`
2018
+ * query block. The grammar rule parses the prelude with real query structure
2019
+ * (so a stray/unbalanced bracket is rejected instead of swallowed) and commits
2020
+ * on `expect('{')`, but the AST is reconstructed from the prelude source text
2021
+ * via the shared `_buildAtRuleFromParts` path — so well-formed queries emit the
2022
+ * exact same AtRule the flat `AtRuleBlock` builder would.
2023
+ */
2024
+ _buildLessQueryAtRuleBlock(children, raw, loc) {
2025
+ const name = children.filter((c) => c._tag === "leaf")[0]?.value ?? "";
2026
+ const comps = spannedComponents(raw);
2027
+ const keywordEnd = comps[0]?.span.end ?? loc.start;
2028
+ const braceStart = comps.find((c) => c.comp === "{")?.span.start ?? loc.end;
2029
+ const preludeText = this._source.slice(keywordEnd, braceStart).trim();
2030
+ const bodyNodes = comps.filter((c) => typeof c.comp !== "string" && c.span.start >= braceStart).map((c) => c.comp);
2031
+ return this._buildAtRuleFromParts(name, preludeText || void 0, bodyNodes, loc);
2032
+ }
2033
+ _buildAtRulePrelude(text, loc) {
2034
+ const singleVarRe = /^@(-?[_a-zA-Z\x80-￿][-_a-zA-Z0-9\x80-￿]*)$/;
2035
+ const MEDIA_KEYWORDS = new Set([
2036
+ "and",
2037
+ "or",
2038
+ "not",
2039
+ "only",
2040
+ "all",
2041
+ "print",
2042
+ "screen",
2043
+ "speech"
2044
+ ]);
2045
+ const COMPARISON_OPS = new Set([
2046
+ ">",
2047
+ "<",
2048
+ ">=",
2049
+ "<=",
2050
+ "=",
2051
+ "!="
2052
+ ]);
2053
+ const escapedStrRe = /^~(['"])([\s\S]*)\1$/;
2054
+ const buildWord = (w) => {
2055
+ const es = escapedStrRe.exec(w);
2056
+ if (es) return this._buildEscapedQuoted(es[2], es[1], loc);
2057
+ const mv = singleVarRe.exec(w);
2058
+ if (mv) return new Reference(mv[1], {
2059
+ type: "index",
2060
+ role: "ident"
2061
+ }, loc);
2062
+ if (MEDIA_KEYWORDS.has(w.toLowerCase())) return this._lessKeyword(w, loc);
2063
+ if (COMPARISON_OPS.has(w)) return w;
2064
+ return this._lessKeyword(w, loc);
2065
+ };
2066
+ const varAccRe = /^@(-?[_a-zA-Z\x80-￿][-_a-zA-Z0-9\x80-￿]*)\[([^\]]*)\]$/;
2067
+ const buildAccessor = (varName, accInner) => {
2068
+ const varBase = new Reference({ key: varName }, {}, loc);
2069
+ const inner = accInner.trim();
2070
+ let accKey;
2071
+ let accType;
2072
+ if (inner === "") {
2073
+ accKey = -1;
2074
+ accType = "index";
2075
+ } else if (inner.startsWith("@")) {
2076
+ accKey = inner.slice(1);
2077
+ accType = "variable";
2078
+ } else {
2079
+ accKey = new Quoted(inner, {}, loc);
2080
+ accType = "index";
2081
+ }
2082
+ return new Reference({
2083
+ target: varBase,
2084
+ key: accKey
2085
+ }, { type: accType }, loc);
2086
+ };
2087
+ const prodOps = new Set(["*", "/"]);
2088
+ const buildFeatureValue = (raw) => {
2089
+ const propVal = raw.trim();
2090
+ if (escapedStrRe.test(propVal) || singleVarRe.test(propVal)) return buildWord(propVal);
2091
+ const vam = varAccRe.exec(propVal);
2092
+ if (vam) return buildAccessor(vam[1], vam[2] ?? "");
2093
+ const paren = /^\(([\s\S]*)\)$/.exec(propVal);
2094
+ if (paren) {
2095
+ const op = buildMathExpr(paren[1].trim());
2096
+ if (op) return new Expression(op, { parens: true }, loc);
2097
+ }
2098
+ const ratio = /^(\d+(?:\.\d+)?)\s*\/\s*(\d+(?:\.\d+)?)$/.exec(propVal);
2099
+ if (ratio) return this._lessKeyword(`${ratio[1]} / ${ratio[2]}`, loc);
2100
+ return this._lessKeyword(propVal, loc);
2101
+ };
2102
+ const buildMathExpr = (expr) => {
2103
+ const parts = expr.split(/\s+/).filter(Boolean);
2104
+ if (parts.length < 3 || parts.length % 2 === 0) return;
2105
+ const buildOperand = (t) => {
2106
+ const es = escapedStrRe.exec(t);
2107
+ if (es) return this._buildEscapedQuoted(es[2], es[1], loc);
2108
+ const mv = singleVarRe.exec(t);
2109
+ if (mv) return new Reference(mv[1], {
2110
+ type: "index",
2111
+ role: "ident"
2112
+ }, loc);
2113
+ const va = varAccRe.exec(t);
2114
+ if (va) return buildAccessor(va[1], va[2] ?? "");
2115
+ const dim = /^([+-]?(?:\d*\.\d+|\d+))([_a-zA-Z%][-_a-zA-Z0-9%]*)?$/.exec(t);
2116
+ if (dim) return dim[2] ? new Dimension({
2117
+ number: parseFloat(dim[1]),
2118
+ unit: dim[2]
2119
+ }, void 0, loc) : new Num(parseFloat(dim[1]), void 0, loc);
2120
+ };
2121
+ const nodes = parts.map((p, i) => i % 2 === 0 ? buildOperand(p) : /^[-+*/]$/.test(p) ? p : void 0);
2122
+ if (nodes.some((n) => n === void 0)) return;
2123
+ const foldPass = (matchOp) => {
2124
+ for (let i = 1; i < nodes.length - 1; i += 2) {
2125
+ const op = nodes[i];
2126
+ if (matchOp(op)) {
2127
+ const left = nodes[i - 1];
2128
+ const right = nodes[i + 1];
2129
+ const combined = new Operation([
2130
+ left,
2131
+ op,
2132
+ right
2133
+ ], void 0, loc);
2134
+ nodes.splice(i - 1, 3, combined);
2135
+ return true;
2136
+ }
2137
+ }
2138
+ return false;
2139
+ };
2140
+ while (foldPass((op) => prodOps.has(op)));
2141
+ while (foldPass((op) => op === "+" || op === "-"));
2142
+ return nodes.length === 1 ? nodes[0] : void 0;
2143
+ };
2144
+ const buildParen = (inner) => {
2145
+ const trimmed = inner.trim();
2146
+ const colonIdx = trimmed.indexOf(":");
2147
+ if (colonIdx > 0 && !/[><=!]/.test(trimmed.slice(0, colonIdx))) {
2148
+ const propName = trimmed.slice(0, colonIdx).trim();
2149
+ const valueNode = buildFeatureValue(trimmed.slice(colonIdx + 1).trim());
2150
+ if (propName.startsWith("--")) return new Paren(new QueryCondition([this._lessKeyword(`${propName}:`, loc), valueNode], void 0, loc), void 0, loc);
2151
+ return new Paren(new Declaration({
2152
+ name: propName,
2153
+ value: valueNode
2154
+ }, void 0, loc), void 0, loc);
2155
+ }
2156
+ return new Paren(new QueryCondition(trimmed.replace(/\(\s+/g, "(").replace(/\s+\)/g, ")").replace(/\s*(<=|>=|!=|[<>=])\s*/g, " $1 ").trim().split(/\s+/).filter(Boolean).map((w) => buildWord(w)), void 0, loc), void 0, loc);
2157
+ };
2158
+ const tokenize = (t) => {
2159
+ const tokens = [];
2160
+ let i = 0;
2161
+ while (i < t.length) if (t[i] === "(") {
2162
+ let depth = 1;
2163
+ let j = i + 1;
2164
+ while (j < t.length && depth > 0) {
2165
+ if (t[j] === "(") depth++;
2166
+ else if (t[j] === ")") depth--;
2167
+ j++;
2168
+ }
2169
+ tokens.push(buildParen(t.slice(i + 1, j - 1)));
2170
+ i = j;
2171
+ } else if (t[i] === "\"" || t[i] === "'" || t[i] === "~" && (t[i + 1] === "\"" || t[i + 1] === "'")) {
2172
+ const start = i;
2173
+ if (t[i] === "~") i++;
2174
+ const quote = t[i];
2175
+ i++;
2176
+ while (i < t.length && t[i] !== quote) i++;
2177
+ i = Math.min(i + 1, t.length);
2178
+ tokens.push(buildWord(t.slice(start, i)));
2179
+ } else if (/\s/.test(t[i])) i++;
2180
+ else {
2181
+ let j = i;
2182
+ while (j < t.length && !/\s/.test(t[j]) && t[j] !== "(") j++;
2183
+ tokens.push(buildWord(t.slice(i, j)));
2184
+ i = j;
2185
+ }
2186
+ return tokens;
2187
+ };
2188
+ const splitCommas = (t) => {
2189
+ const parts = [];
2190
+ let depth = 0;
2191
+ let start = 0;
2192
+ for (let i = 0; i < t.length; i++) if (t[i] === "(") depth++;
2193
+ else if (t[i] === ")") depth--;
2194
+ else if (t[i] === "," && depth === 0) {
2195
+ parts.push(t.slice(start, i).trim());
2196
+ start = i + 1;
2197
+ }
2198
+ parts.push(t.slice(start).trim());
2199
+ return parts.filter(Boolean);
2200
+ };
2201
+ const nsMediaRe = /^([#.][^(\[,\s]*)(\([^)]*\))?(\[[^\]]*\])?$/;
2202
+ const buildItem = (t) => {
2203
+ const trimmed = t.trim();
2204
+ const mv = singleVarRe.exec(trimmed);
2205
+ if (mv) return new QueryCondition([new Reference(mv[1], {
2206
+ type: "index",
2207
+ role: "ident"
2208
+ }, loc)], void 0, loc);
2209
+ const vam = /^@(-?[_a-zA-Z\x80-￿][-_a-zA-Z0-9\x80-￿]*)(\[([^\]]*)\])$/.exec(trimmed);
2210
+ if (vam) {
2211
+ const varBase = new Reference({ key: vam[1] }, {}, loc);
2212
+ const accInner = (vam[3] ?? "").trim();
2213
+ let accKey;
2214
+ let accType;
2215
+ if (accInner === "") {
2216
+ accKey = -1;
2217
+ accType = "index";
2218
+ } else if (accInner.startsWith("@")) {
2219
+ accKey = accInner.slice(1);
2220
+ accType = "variable";
2221
+ } else {
2222
+ accKey = new Quoted(accInner, {}, loc);
2223
+ accType = "index";
2224
+ }
2225
+ return new Expression(new Reference({
2226
+ target: varBase,
2227
+ key: accKey
2228
+ }, { type: accType }, loc), void 0, loc);
2229
+ }
2230
+ const nsm = nsMediaRe.exec(trimmed);
2231
+ if (nsm) {
2232
+ const nsPath = nsm[1];
2233
+ const argsText = nsm[2];
2234
+ const accText = nsm[3];
2235
+ const segments = nsPath.match(/[#.][^#.]*/g) ?? [nsPath];
2236
+ const nameKey = segments.length === 1 ? segments[0] : segments;
2237
+ const rawKey = segments.length > 1 ? nsPath : void 0;
2238
+ let base = new Reference({
2239
+ key: nameKey,
2240
+ ...rawKey ? { rawKey } : {}
2241
+ }, {
2242
+ type: "mixin-ruleset",
2243
+ role: "name"
2244
+ }, loc);
2245
+ if (argsText) {
2246
+ const argsInner = argsText.slice(1, -1).trim();
2247
+ let argsNode = null;
2248
+ if (argsInner) {
2249
+ const argRefMatch = /^([.#][^\[\]()\s]+)(\[([^\]]*)\])?$/.exec(argsInner);
2250
+ if (argRefMatch) {
2251
+ let argBase = new Reference({ key: argRefMatch[1] }, { role: "name" }, loc);
2252
+ if (argRefMatch[2] !== void 0) {
2253
+ const argAcc = argRefMatch[3] ?? "";
2254
+ argBase = new Reference({
2255
+ target: argBase,
2256
+ key: argAcc === "" ? -1 : argAcc
2257
+ }, {}, loc);
2258
+ }
2259
+ argsNode = new List([argBase], void 0, loc);
2260
+ }
2261
+ }
2262
+ const callPayload = { name: base };
2263
+ if (argsNode) callPayload.args = argsNode;
2264
+ base = new Call(callPayload, {}, loc);
2265
+ }
2266
+ if (accText) {
2267
+ const inner = accText.slice(1, -1).trim();
2268
+ const key = inner.startsWith("@") ? inner.slice(1) : inner === "" ? -1 : inner;
2269
+ base = new Reference({
2270
+ target: base,
2271
+ key
2272
+ }, { type: "variable" }, loc);
2273
+ }
2274
+ return new Expression(base, void 0, loc);
2275
+ }
2276
+ const tokens = tokenize(t);
2277
+ const nameMatch = /^(@?-?[_a-zA-Z\x80-\uffff][-_a-zA-Z0-9\x80-\uffff]*)\s+(?:\(|not(?![-\w]))/i.exec(trimmed);
2278
+ return new QueryCondition(tokens, !!nameMatch && tokens.length > 1 && ![
2279
+ "not",
2280
+ "and",
2281
+ "or",
2282
+ "only"
2283
+ ].includes(nameMatch[1].replace(/^@/, "").toLowerCase()) ? { leadingContainerName: true } : void 0, loc);
2284
+ };
2285
+ const commaItems = splitCommas(text);
2286
+ if (commaItems.length === 1) return buildItem(commaItems[0]);
2287
+ return new List(commaItems.map(buildItem), void 0, loc);
2288
+ }
2289
+ _buildAtRuleStatement(children, loc) {
2290
+ const ls = children.filter((c) => c._tag === "leaf");
2291
+ const name = ls[0]?.value ?? "";
2292
+ if ([
2293
+ "@import",
2294
+ "@-import",
2295
+ "@-export"
2296
+ ].includes(name)) return this._buildImportAtRuleFromPrelude(children, ls, loc, name);
2297
+ if (["@use", "@-use"].includes(name)) return this._buildUseAtRuleFromPrelude(children, loc, name);
2298
+ return super._buildAtRuleStatement(children, loc);
2299
+ }
2300
+ _buildUseAtRuleFromPrelude(children, loc, name) {
2301
+ const preludeText = this._source.slice(loc.start, loc.end);
2302
+ const quotedNode = nodeChildren(children).find((n) => n.type === "Quoted");
2303
+ let rawPath = "";
2304
+ let pathNode;
2305
+ if (quotedNode) {
2306
+ const quote = quotedNode.quote ?? "\"";
2307
+ const innerVal = quotedNode.value;
2308
+ const inner = typeof innerVal === "string" ? innerVal : innerVal?.value ?? String(innerVal?.valueOf?.() ?? "");
2309
+ rawPath = inner;
2310
+ pathNode = new Quoted(inner, { quote }, loc);
2311
+ } else {
2312
+ const qm = /(['"])((?:[^'"\\]|\\.)*)\1/.exec(preludeText);
2313
+ if (qm) {
2314
+ const quote = qm[1] === "'" ? "'" : "\"";
2315
+ const inner = qm[2];
2316
+ rawPath = inner;
2317
+ pathNode = new Quoted(inner, { quote }, loc);
2318
+ }
2319
+ }
2320
+ const explicitNs = /\bas\s+([^\s;]+)/.exec(preludeText)?.[1];
2321
+ if (/\.[cm]?[jt]sx?$/.test(rawPath) || rawPath.startsWith("#")) {
2322
+ let ns = explicitNs;
2323
+ if (!ns) ns = (rawPath.split("/").pop() ?? "").replace(/\.[^.]+$/, "").replace(/[^a-zA-Z0-9_$]/g, "_");
2324
+ return new JsImport({ path: pathNode }, { namespace: ns }, loc);
2325
+ }
2326
+ return new AtRule({
2327
+ name,
2328
+ prelude: pathNode,
2329
+ rules: []
2330
+ }, void 0, loc);
2331
+ }
2332
+ _parenToArgs(paren, loc) {
2333
+ const inner = paren.value ?? paren.node;
2334
+ if (!inner) return null;
2335
+ const items = [];
2336
+ const isSeq = inner && typeof inner === "object" && inner.type === "Sequence";
2337
+ if (isSeq || Array.isArray(inner)) {
2338
+ const seqItems = (isSeq ? inner.value ?? [] : inner).map((it) => typeof it === "string" ? this._lessKeyword(it.trim(), loc) : it);
2339
+ let j = 0;
2340
+ while (j < seqItems.length) {
2341
+ const item = seqItems[j];
2342
+ if (item.type === "Reference" && j + 2 < seqItems.length && seqItems[j + 1]?.value === ":") {
2343
+ const nameAny = item.key ?? "";
2344
+ const valNode = seqItems[j + 2];
2345
+ const vd = new VarDeclaration({
2346
+ name: nameAny,
2347
+ value: this._isKeywordLike(valNode) ? this._lessKeyword(String(valNode.value ?? "").trim(), loc) : this._lessKeyword(String(valNode.value ?? ""), loc)
2348
+ }, {}, loc);
2349
+ items.push(vd);
2350
+ j += 3;
2351
+ } else {
2352
+ items.push(item);
2353
+ j++;
2354
+ }
2355
+ }
2356
+ } else if (inner && inner.type === "List") for (const it of inner.value ?? []) items.push(it);
2357
+ else if (inner) {
2358
+ if (!this._isEmptyKeywordLike(inner)) items.push(inner);
2359
+ }
2360
+ if (items.length === 0) return null;
2361
+ return new List(items, void 0, loc);
2362
+ }
2363
+ _tryParseNamespaceRef(valItems, loc) {
2364
+ const isSel = (s) => typeof s === "string" && /^[#.]-?[_a-zA-Z\u0080-\uffff]/.test(s.trim());
2365
+ const isCombinator = (s) => typeof s === "string" && /^[>+~|]$|^\|\|$/.test(String(s).trim());
2366
+ const isJessNodeVal = (x) => !!x && typeof x === "object" && "type" in x;
2367
+ const first = valItems[0]?.comp;
2368
+ const isVarRef = (x) => !!x && typeof x === "object" && x.type === "Reference";
2369
+ if (!isSel(first) && !isVarRef(first)) return null;
2370
+ if (isVarRef(first) && valItems.length < 2) return null;
2371
+ const hasSquareAfterVar = isVarRef(first) && valItems.slice(1).some((vi) => isJessNodeVal(vi.comp) && vi.comp.type === "Paren" && vi.comp._options?.delimiter === "square");
2372
+ if (isVarRef(first) && !hasSquareAfterVar) return null;
2373
+ let i = 0;
2374
+ let base = null;
2375
+ let pendingSegments = [];
2376
+ let hasMidCall = false;
2377
+ const flushPendingAsRef = () => {
2378
+ const k = pendingSegments.length === 1 ? pendingSegments[0] : pendingSegments;
2379
+ pendingSegments = [];
2380
+ if (base === null) return new Reference({ key: k }, {
2381
+ type: "mixin-ruleset",
2382
+ role: "name"
2383
+ }, loc);
2384
+ return new Reference({
2385
+ target: base,
2386
+ key: k
2387
+ }, {
2388
+ type: "mixin-ruleset",
2389
+ role: "name"
2390
+ }, loc);
2391
+ };
2392
+ while (i < valItems.length) {
2393
+ const c = valItems[i].comp;
2394
+ if (isVarRef(c) && base === null && pendingSegments.length === 0) {
2395
+ const rv = c;
2396
+ base = rv.target !== void 0 && (rv.key === "" || rv.key === void 0 || rv.key && typeof rv.key === "object" && rv.key.type === "Quoted" && !String(rv.key.value ?? "").trim()) ? rv.target : c;
2397
+ i++;
2398
+ } else if (isSel(c)) {
2399
+ const seg = c.trim();
2400
+ const splitSeg = seg.match(/[#.][^#.]*/g) ?? [seg];
2401
+ for (const s of splitSeg) pendingSegments.push(s);
2402
+ i++;
2403
+ } else if (isCombinator(c) && i + 1 < valItems.length && isSel(valItems[i + 1]?.comp)) i++;
2404
+ else if (isJessNodeVal(c) && c.type === "Paren" && c._options?.delimiter === "square") {
2405
+ if (pendingSegments.length > 0) base = flushPendingAsRef();
2406
+ if (base === null) break;
2407
+ const innerKey = this._decodeAccessorKey(c, loc);
2408
+ base = new Reference({
2409
+ target: base,
2410
+ key: innerKey
2411
+ }, { type: typeof innerKey === "number" ? "index" : "variable" }, loc);
2412
+ i++;
2413
+ } else if (isJessNodeVal(c) && c.type === "Paren") {
2414
+ if (pendingSegments.length === 0 && base === null) break;
2415
+ if (pendingSegments.length > 0) base = flushPendingAsRef();
2416
+ const argsNode = this._parenToArgs(c, loc);
2417
+ const callPayload = { name: base };
2418
+ if (argsNode) callPayload.args = argsNode;
2419
+ base = new Call(callPayload, {}, loc);
2420
+ hasMidCall = true;
2421
+ i++;
2422
+ } else break;
2423
+ }
2424
+ if (i !== valItems.length) return null;
2425
+ if (pendingSegments.length === 0 && base === null) return null;
2426
+ if (pendingSegments.length > 0) base = flushPendingAsRef();
2427
+ if (valItems.length > 0) {
2428
+ const lastSpan = valItems[valItems.length - 1].span;
2429
+ let afterVal = this._source.slice(lastSpan.end).trimStart();
2430
+ if (!hasMidCall && afterVal.startsWith("(")) {
2431
+ const closeIdx = afterVal.indexOf(")", 1);
2432
+ if (closeIdx > 0) {
2433
+ const callInner = afterVal.slice(1, closeIdx).trim();
2434
+ afterVal = afterVal.slice(closeIdx + 1).trimStart();
2435
+ let argsNode = null;
2436
+ if (callInner) {
2437
+ const argRefMatch = /^([.#][^\[\]()\s]+)(\[([^\]]*)\])?$/.exec(callInner);
2438
+ if (argRefMatch) {
2439
+ const argSel = argRefMatch[1];
2440
+ const argAccContent = argRefMatch[3];
2441
+ let argBase = new Reference({ key: argSel }, { role: "name" }, loc);
2442
+ if (argRefMatch[2] !== void 0) {
2443
+ const argAccKey = argAccContent === void 0 || argAccContent === "" ? -1 : argAccContent.startsWith("@") ? argAccContent.slice(1) : argAccContent;
2444
+ argBase = new Reference({
2445
+ target: argBase,
2446
+ key: argAccKey
2447
+ }, {}, loc);
2448
+ }
2449
+ argsNode = new List([argBase], void 0, loc);
2450
+ }
2451
+ }
2452
+ const callPayload = { name: base };
2453
+ if (argsNode) callPayload.args = argsNode;
2454
+ base = new Call(callPayload, {}, loc);
2455
+ }
2456
+ }
2457
+ const accMatch = /^\[([^\]]*)\]/.exec(afterVal);
2458
+ if (accMatch) {
2459
+ const accText = accMatch[1].trim();
2460
+ let accessorKey;
2461
+ if (accText === "") accessorKey = -1;
2462
+ else if (accText.startsWith("@")) accessorKey = accText.slice(1);
2463
+ else accessorKey = new Quoted(accText, {}, loc);
2464
+ base = new Reference({
2465
+ target: base,
2466
+ key: accessorKey
2467
+ }, {}, loc);
2468
+ const afterAcc = afterVal.slice(accMatch[0].length).trimStart();
2469
+ if (/^\(\s*\)/.test(afterAcc)) base = new Call({ name: base }, {}, loc);
2470
+ } else if (!hasMidCall) {
2471
+ if (/^\(\s*\)/.exec(afterVal)) base = new Call({ name: base }, {}, loc);
2472
+ }
2473
+ }
2474
+ return base;
2475
+ }
2476
+ _assembleLessValue(valItems, loc) {
2477
+ const parts = [];
2478
+ for (const item of valItems) {
2479
+ const c = item.comp;
2480
+ if (typeof c === "string") {
2481
+ if (c.trim()) parts.push(this._lessKeyword(c.trim(), loc));
2482
+ } else parts.push(c);
2483
+ }
2484
+ if (parts.length === 0) return { value: "" };
2485
+ if (parts.length === 1) return { value: parts[0] };
2486
+ return { value: parts };
2487
+ }
2488
+ };
2489
+ //#endregion
2490
+ //#region src/functional-parser.ts
2491
+ var BuilderHost = class extends LessGrammar {
2492
+ /**
2493
+ * The TreeContext threaded in by the caller (the plugin's per-file context),
2494
+ * held for the duration of one parse. Currently a pass-through: it's carried
2495
+ * back out in the result so a caller can hand one context in and read it back.
2496
+ * Grammar rules (e.g. a future `@compose`/`@use` rule) can read/mutate it —
2497
+ * e.g. set `context.opts.strict` — and the mutation is visible to the caller
2498
+ * since it's the same object.
2499
+ */
2500
+ context;
2501
+ setSource(src) {
2502
+ this._source = src;
2503
+ }
2504
+ resetWarnings() {
2505
+ this._warnings = [];
2506
+ this._errors = [];
2507
+ this._liftedCommentRanges = [];
2508
+ }
2509
+ getWarnings() {
2510
+ return this._warnings.slice();
2511
+ }
2512
+ getErrors() {
2513
+ return this._errors.slice();
2514
+ }
2515
+ /** `ctx.build` host: every structural `node(type, …)` builds through this,
2516
+ * reusing LessGrammar's (Less + inherited CSS) `buildNode` verbatim. */
2517
+ captureTriviaForNode(type) {
2518
+ return type === "CompoundSelector";
2519
+ }
2520
+ build(type, children, fields, span, rawChildren, triviaLog) {
2521
+ return this.buildNode(type, {
2522
+ start: span.start,
2523
+ end: span.end
2524
+ }, children, void 0, rawChildren, fields, triviaLog);
2525
+ }
2526
+ };
2527
+ const host = new BuilderHost();
2528
+ function parseLessFn(input, rule = "Stylesheet", mathMode = "parens-division", context) {
2529
+ host.mathMode = context?.options?.mathMode ?? mathMode;
2530
+ host.context = context;
2531
+ const g = lessGrammar;
2532
+ const result = runFunctionalParse(input, g[rule], host, { trivia: g.rw });
2533
+ const threaded = host.context;
2534
+ host.context = void 0;
2535
+ return {
2536
+ ...result,
2537
+ context: threaded
2538
+ };
2539
+ }
2540
+ /**
2541
+ * Index of the first backtick in CODE position (i.e. real inline JS), or -1.
2542
+ * Skips `//` line comments, `/* … *​/` block comments, and quoted strings so a
2543
+ * backtick inside a comment/string (common in Less doc comments) is not
2544
+ * mistaken for inline JavaScript.
2545
+ */
2546
+ function firstInlineJsBacktick(text) {
2547
+ for (let i = 0; i < text.length; i++) {
2548
+ const c = text[i];
2549
+ if (c === "`") return i;
2550
+ if (c === "/" && text[i + 1] === "/") {
2551
+ const nl = text.indexOf("\n", i + 2);
2552
+ if (nl === -1) return -1;
2553
+ i = nl;
2554
+ } else if (c === "/" && text[i + 1] === "*") {
2555
+ const end = text.indexOf("*/", i + 2);
2556
+ if (end === -1) return -1;
2557
+ i = end + 1;
2558
+ } else if (c === "\"" || c === "'") {
2559
+ i++;
2560
+ while (i < text.length && text[i] !== c) {
2561
+ if (text[i] === "\\") i++;
2562
+ i++;
2563
+ }
2564
+ }
2565
+ }
2566
+ return -1;
2567
+ }
2568
+ /** Functional Less parser — call .parse(text) to get a Jess AST. */
2569
+ var LessParser = class {
2570
+ _mathMode;
2571
+ constructor(config) {
2572
+ this._mathMode = config?.mathMode ?? "parens-division";
2573
+ }
2574
+ parse = (text, rule = "Stylesheet", options) => {
2575
+ const backtick = firstInlineJsBacktick(text);
2576
+ if (backtick !== -1) return {
2577
+ tree: nil(),
2578
+ errors: [toParseError("Inline JavaScript using backticks is not supported. Use @use / @-use to import a script module instead.", backtick, text)],
2579
+ warnings: [],
2580
+ trivia: buildLazyTriviaMap([], text),
2581
+ liftedCommentRanges: [],
2582
+ context: options?.context
2583
+ };
2584
+ return parseLessFn(text, rule, this._mathMode, options?.context);
2585
+ };
2586
+ };
2587
+ //#endregion
2588
+ export { getInterpolatedNode as a, Fragments as c, lessTokens as d, createInterpolatedReference as i, Tokens as l, parseLessFn as n, getInterpolatedOrString as o, LessGrammar as r, normalizeMixinReferenceKey as s, LessParser as t, lessFragments as u };