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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) 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 +18 -0
  17. package/lib/index.d.ts +9 -32
  18. package/lib/index.d.ts.map +1 -0
  19. package/lib/index.js +7 -75
  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 +4 -3
  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 +8 -1
  41. package/lib/utils.d.ts.map +1 -0
  42. package/package.json +49 -16
  43. package/{lib/__tests__/debug-log.js → src/__tests__/debug-log.ts} +20 -19
  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/__tests__/debug-log.d.ts +0 -1
  61. package/lib/__tests__/debug-log.js.map +0 -1
  62. package/lib/index.js.map +0 -1
  63. package/lib/lessActionsParser.d.ts +0 -156
  64. package/lib/lessActionsParser.js +0 -145
  65. package/lib/lessActionsParser.js.map +0 -1
  66. package/lib/lessErrorMessageProvider.d.ts +0 -3
  67. package/lib/lessErrorMessageProvider.js +0 -4
  68. package/lib/lessErrorMessageProvider.js.map +0 -1
  69. package/lib/lessTokens.js +0 -249
  70. package/lib/lessTokens.js.map +0 -1
  71. package/lib/productions.d.ts +0 -181
  72. package/lib/productions.js +0 -3521
  73. package/lib/productions.js.map +0 -1
  74. package/lib/utils.js +0 -88
  75. package/lib/utils.js.map +0 -1
package/src/grammar.ts ADDED
@@ -0,0 +1,869 @@
1
+ /**
2
+ * Functional Less grammar — the macro-compiled counterpart to the class-based
3
+ * LessGrammar. This file is JUST the grammar: `lessGrammar = compose([cssGrammar,
4
+ * <Less delta>])`. Most returned rules are structural `node(parser)` entries that build via
5
+ * the injected `ctx.build` host. The host + parse entry (`parseLessFn`,
6
+ * `LessParser`) live in ./functional-parser.ts; the shared driver in
7
+ * @jesscss/css-parser.
8
+ */
9
+ import {
10
+ rules, compose,
11
+ node, regex, literal, sequence, choice, many, oneOrMore, optional,
12
+ not, scanTo, balanced, parser, trivia, noTrivia, expect, sepBy, label
13
+ } from 'parseman' with { type: 'macro' };
14
+ import { cssGrammar } from '@jesscss/css-parser/grammar';
15
+
16
+ // ---------------------------------------------------------------------------
17
+ // Grammar — Less = CSS + the Less delta. `compose` fuses the imported compiled
18
+ // `cssGrammar` (its linkable pieces travel on the value — no source) with the
19
+ // inline Less delta: the delta's rules win by name, and its references to CSS
20
+ // value rules (Num/Quoted/Paren/query) resolve into the fused set. One grammar =
21
+ // one `rules()`; no fragment spreads.
22
+ // ---------------------------------------------------------------------------
23
+
24
+ // Trivia (`rw`) is declared ONCE on the grammar via `rules({ trivia: rw }, …)`,
25
+ // honored through `compose()`, making it ambient in every rule — no per-rule
26
+ // trivia-establisher wrappers are needed. Hoisted to module scope (mirroring
27
+ // css-parser) so the options-first `rules({ trivia: rw }, …)` call below can
28
+ // reference it. Same shape as CSS (whitespace + block + `//` line comments).
29
+ const ws = regex(/[ \t\n\r\f]+/);
30
+ const comment = regex(/\/\*(?:[^*]|\*(?!\/))*\*\//);
31
+ const lineComment = regex(/\/\/[^\n\r]*/);
32
+ const rw = trivia(oneOrMore(choice(label('whitespace', ws), label('blockComment', comment), label('lineComment', lineComment))));
33
+
34
+ export const lessGrammar = compose([cssGrammar, rules({ trivia: rw }, (g: any) => {
35
+ // ---------------------------------------------------------------------------
36
+ // Terminals (CSS base + Less @var / @{interp}).
37
+ // ---------------------------------------------------------------------------
38
+
39
+ // Whitespace-only trivia for url() bodies: inside `url(…)`, `//` and `/*` are URL
40
+ // characters, not comments (`url(//host/x)` is protocol-relative), so the normal
41
+ // `rw` (which skips line/block comments) must not apply there.
42
+ const urlWs = trivia(ws);
43
+
44
+ const ident = regex(/-?(?:[_a-zA-Z€-￿]|\\(?:[0-9a-fA-F]{1,6}[ \t\n\r\f]?|[^\n]))(?:[-_a-zA-Z0-9€-￿]|\\(?:[0-9a-fA-F]{1,6}[ \t\n\r\f]?|[^\n]))*/);
45
+ // Selectors / mixin names / idents include CSS escapes (\hex, \char) — same
46
+ // definition as css-parser grammar.ts (a mixin call is just a selector).
47
+ const basicSel = regex(/(?:[.#]?-?(?:[_a-zA-Z€-￿]|\\(?:[0-9a-fA-F]{1,6}[ \t\n\r\f]?|[^\n]))(?:[-_a-zA-Z0-9€-￿]|\\(?:[0-9a-fA-F]{1,6}[ \t\n\r\f]?|[^\n]))*|\d+(?:\.\d+)?%|\*)/);
48
+ const combinator = choice(literal('||'), literal('>'), literal('+'), literal('~'), literal('|'));
49
+ const pseudoColon = regex(/::?/);
50
+ const attrOp = regex(/[*~|^$]?=/);
51
+ // Only `i` / `s` are defined today; for forwards-compatibility any single ASCII
52
+ // letter is accepted (`[a=b c]`). A digit / underscore / other non-letter is
53
+ // still rejected.
54
+ const attrMod = regex(/[a-zA-Z]/);
55
+ const nth = regex(/even|odd|[-+]?\d*n(?:[ \t\n\r\f]*[+-][ \t\n\r\f]*\d+)?|[-+]?\d+/i);
56
+ // Same pattern as shared-value-rules.ts `singleStr`/`doubleStr` — local so the macro
57
+ // can statically evaluate regex(); `\\` + newline is valid CSS line continuation.
58
+ const singleStr = regex(/'(?:[^'\\]|\\[\s\S])*'/);
59
+ const doubleStr = regex(/"(?:[^"\\]|\\[\s\S])*"/);
60
+ // Balanced bracket scans that treat strings as opaque holes — a bracket inside
61
+ // a string (`(foo: "(" x ")")`) takes token precedence and must NOT affect depth.
62
+ const strHole = [singleStr, doubleStr];
63
+ const bParen = balanced('(', ')', { skip: strHole });
64
+ const bSquare = balanced('[', ']', { skip: strHole });
65
+ const bCurly = balanced('{', '}', { skip: strHole });
66
+ const customProp = regex(/--[-_a-zA-Z0-9\u0080-\uffff]*/);
67
+ // Interpolated custom-property name (`--@{key}`, `--foo-@{key}-bar`). Port of the
68
+ // reference's InterpolatedCustomProperty token: `--` + optional ident run, then
69
+ // one-or-more `@{...}` interpolations interleaved with further ident runs. Tried
70
+ // before the plain `customProp` regex, which stops at `@` (not an ident char) and
71
+ // would otherwise match only `--`, failing the declaration outright.
72
+ const customPropInterp = regex(/--(?:-?[_a-zA-Z\u0080-\uffff][-_a-zA-Z0-9\u0080-\uffff]*|-)?@\{-?[_a-zA-Z\u0080-\uffff][-_a-zA-Z0-9\u0080-\uffff]*\}(?:@\{-?[_a-zA-Z\u0080-\uffff][-_a-zA-Z0-9\u0080-\uffff]*\}|[-_a-zA-Z0-9\u0080-\uffff])*/);
73
+ const atKeyword = regex(/@-?[_a-zA-Z\u0080-\uffff][-_a-zA-Z0-9\u0080-\uffff]*/);
74
+ const numPart = regex(/[+-]?(?:\d*\.\d+(?:[eE][+-]?\d+)?|\d+(?:[eE][+-]?\d+)?|\d+)/);
75
+ const urlOpen = regex(/url\(/i);
76
+ // Unquoted url() body: any run of non-delimiter chars, with CSS escapes (\" \( …)
77
+ // so escaped quotes/parens inside the URL don't terminate it.
78
+ const urlInner = regex(/(?:\\.|[^)"'\s])+/);
79
+ const anyValueTok = regex(/[+\-*/=<>|~^]+|[^\s;{}\[\]()'",!]+/);
80
+
81
+ // Less-specific terminals.
82
+ // First char may be a digit \u2014 Less allows numeric variable names (`@3`, `@{3}`).
83
+ // `@` + one or more name chars (dash included), so a dash-only name like `@-` is
84
+ // valid (Less accepts it). Digits are allowed anywhere (`@3` \u2014 flagged, not rejected).
85
+ const lessVar = regex(/@[-_a-zA-Z0-9\u0080-\uffff]+/);
86
+ const lessInterp = regex(/@\{-?[_a-zA-Z0-9\u0080-\uffff][-_a-zA-Z0-9\u0080-\uffff]*\}/);
87
+
88
+ // ---------------------------------------------------------------------------
89
+ // Grammar — CSS base rules + Less overrides/additions (mirrors LessGrammar).
90
+ // ---------------------------------------------------------------------------
91
+
92
+ // ── Root (Less: + VarDeclaration, MixinCall, detached Call) ─────────────────
93
+ // No catch-all: unmatched input stops `many`; the driver reports the unconsumed
94
+ // offset as one syntax error (parseLessFn). Bare `;` is an empty statement.
95
+ // The per-statement choice used by the root `many(...)`. Exposed as a named
96
+ // rule so grammars that EXTEND Less (e.g. SCSS) can inject their own
97
+ // statements ahead of it — `many(choice(g.ScssIf, …, g.stylesheetItem))` —
98
+ // without re-listing the whole set. Keeps the extension seam in one place.
99
+ const stylesheetItem = choice(
100
+ g.VarDeclaration, g.VarCall, g.QueryAtRuleBlock, g.AtRuleBlock, g.ImportAtRuleStatement, g.AtRuleStatement, g.ExtendStatement, g.Ruleset, g.MixinOrQualifiedRule, g.EachFor,
101
+ sequence(g.Call, optional(literal(';'))), literal(';')
102
+ );
103
+ const Stylesheet = node(
104
+ many(g.stylesheetItem));
105
+
106
+ // Plain helper consts referenced before their section must be defined up-front
107
+ // (Phase-1 evaluation is sequential; only g.* refs resolve lazily).
108
+ const important = sequence(literal('!'), literal('important'));
109
+
110
+ // ── Less variable declaration / reference ───────────────────────────────────
111
+ // A detached ruleset assigned to a variable: `@name: { … }`. The structured branch
112
+ // parses the body as a declaration list (→ Mixin). If that fails — e.g. bootstrap's
113
+ // `@escaped-characters: { <: %3c; … }`, whose keys (`<`, `>`, `(`, `)`) are not valid
114
+ // property names — the raw fallback captures the balanced `{ … }` verbatim. Historical
115
+ // Less treats such a block as a raw string (only re-parsed on interpolation); the
116
+ // builder then produces a `Quoted` so `@plugin` functions (e.g. escape-svg) read it as
117
+ // a string. The fallback is a plain balanced scan (no node wrapper); the builder
118
+ // distinguishes it by the absence of structured child nodes.
119
+ const rawDetachedBlock = sequence(literal('{'), noTrivia(scanTo(literal('}'), { skip: [bParen, bSquare, bCurly, singleStr, doubleStr] })), literal('}'));
120
+ const detachedBlock = choice(
121
+ sequence(literal('{'), g.declarationList, literal('}')),
122
+ rawDetachedBlock
123
+ );
124
+ // Var-decl colon. Spaces around it are fine (`@x : y` is a declaration). It is
125
+ // NOT a declaration only in the pseudo pattern `<space>:<word>` — the colon has a
126
+ // space before AND clings to the following ident (e.g. `@page :first { … }` is an
127
+ // at-rule prelude, not `@page: first`). So: colon adjacent to the name (noTrivia),
128
+ // OR colon not immediately followed by an ident-start.
129
+ const varColon = choice(
130
+ noTrivia(sequence(lessVar, literal(':'))),
131
+ sequence(lessVar, regex(/:(?![-_a-zA-Z€-￿])/))
132
+ );
133
+ const VarDeclaration = node(
134
+ sequence(varColon, choice(detachedBlock, sequence(g.valueList, optional(important), optional(literal(';'))))));
135
+ const mixinArgsContent = scanTo(literal(')'), { skip: [bParen, bSquare, bCurly, singleStr, doubleStr] });
136
+ // Accessor key tokens, in lookupOrCall's OR2 order: NestedReference ($@x / @@x),
137
+ // AtKeyword (@x), PropertyReference ($x), InterpolatedIdent (…@{x}…), Ident.
138
+ // The builder applies the index/variable typing + Quoted-wrap (see _buildReference).
139
+ const nestedRef = regex(/(?:[@$]+(?:-?[_a-zA-Z€-￿][-_a-zA-Z0-9€-￿]*)?){2,}/);
140
+ const propRef = regex(/\$-?[_a-zA-Z€-￿][-_a-zA-Z0-9€-￿]*/);
141
+ const interpKey = regex(/(?:-?[_a-zA-Z€-￿][-_a-zA-Z0-9€-￿]*|-)?[@$]\{-?[_a-zA-Z€-￿][-_a-zA-Z0-9€-￿]*\}(?:[@$]\{-?[_a-zA-Z€-￿][-_a-zA-Z0-9€-￿]*\}|[-_a-zA-Z0-9€-￿])*/);
142
+ // A purely-numeric name (`100:`) is a Less detached-ruleset map key, e.g.
143
+ // `@grays: { 100: @gray-100; }` (Bootstrap). Not valid CSS, but Less accepts it
144
+ // and `@grays[100]` reads it back; the whole-number alternative is tried first.
145
+ const declPropName = regex(/[0-9]+|\*?-?(?:[_a-zA-Z\u0080-\uffff]|\\(?:[0-9a-fA-F]{1,6}[ \t\n\r\f]?|[^\n])|[@$]\{[^}]*\})(?:[-_a-zA-Z0-9\u0080-\uffff]|\\(?:[0-9a-fA-F]{1,6}[ \t\n\r\f]?|[^\n])|[@$]\{[^}]*\})*/);
146
+ const refKey = choice(nestedRef, lessVar, propRef, interpKey, ident);
147
+ // One accessor: glued '[' / '(', trivia re-enabled inside the brackets/parens.
148
+ const refIndex = sequence(literal('['), optional(refKey), literal(']'));
149
+ const refCall = sequence(literal('('), optional(mixinArgsContent), literal(')'));
150
+ // varReference + lookupOrCall: a @variable OR $property glued to a chain of
151
+ // [accessor]/(call). `$color` is a bare property reference (read declaration
152
+ // `color`); `@a[k]` is a variable + accessor chain. noTrivia() forbids trivia
153
+ // (whitespace/comments) between the head and '[' / '(', keeping the chain
154
+ // contiguous (production's noSep()). The builder types `$`-headed refs as
155
+ // `property` and `@`-headed as `variable` (see _buildReference).
156
+ const Reference = node(
157
+ noTrivia(sequence(choice(lessVar, propRef), many(choice(refIndex, refCall)))));
158
+
159
+ // ── Detached-ruleset variable call ─────────────────────────────────────────
160
+ // `@name(...)` (no `:`) is a variable CALL of a detached ruleset, not a var
161
+ // decl or an at-rule. Faithful port of `varDeclarationOrCall`'s LParen branch
162
+ // (selectors.ts) + the `isVariableLike` disambiguation (root.ts): a KNOWN
163
+ // at-rule name (@media, @supports, …) followed by NON-empty parens stays an
164
+ // at-rule — `@media() ` (empty parens) is a deprecated var call, and any other
165
+ // `@var(...)` is a var call regardless of parens content. The builder emits the
166
+ // production's `Expression(Call{ name: Reference[role=name], args })` shape.
167
+ // A var name that is NOT a known at-rule name (negative lookahead asserts the
168
+ // known name is not the COMPLETE ident before the call parens).
169
+ const nonKnownAtVar = regex(/@-?(?!(?:(?:-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)(?![-_a-zA-Z0-9]))[_a-zA-Z0-9€-￿][-_a-zA-Z0-9€-￿]*/);
170
+ const knownAtVar = regex(/@(?:(?:-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)(?![-_a-zA-Z0-9])/);
171
+ const VarCall = node(
172
+ choice(
173
+ // A var call is `@name(...)` with the `(` ADJACENT to the name (no space).
174
+ // `@foo (bar)` — space before `(` — is never a var call; it's an unknown
175
+ // at-rule prelude, so noTrivia makes this branch defer to AtRuleBlock.
176
+ sequence(noTrivia(sequence(nonKnownAtVar, regex(/(?=\()/))), g.MixinArgs, optional(important), optional(literal(';'))),
177
+ // Known at-rule name with EMPTY parens only.
178
+ sequence(knownAtVar, literal('('), literal(')'), optional(important), optional(literal(';')))
179
+ ));
180
+
181
+ // ── Mixins ───────────────────────────────────────────────────────────────
182
+ // Mixin arguments are composed from the SAME value combinators as function-call
183
+ // args (`callArgSeq`) — NOT captured as raw text — so an arithmetic arg like
184
+ // `@a * 2` is a real Operation, not a Reference whose key is the raw string. The
185
+ // `MixinArgs` production lives next to `callArgSeq`/`callArgList` (below) so it can
186
+ // reuse them directly. See `_buildMixinArgs` (which reuses the shared `_assembleArgs`).
187
+ const mixinNamePath = sequence(basicSel, many(sequence(optional(combinator), basicSel)));
188
+ // MixinCall names must start with . or # — plain idents are properties, not mixins.
189
+ const mixinCallBasicSel = regex(/[.#]-?(?:[_a-zA-Z€-￿]|\\(?:[0-9a-fA-F]{1,6}[ \t\n\r\f]?|[^\n]))(?:[-_a-zA-Z0-9€-￿]|\\(?:[0-9a-fA-F]{1,6}[ \t\n\r\f]?|[^\n]))*/);
190
+ const mixinCallPath = sequence(g.mixinCallBasicSel, many(sequence(optional(combinator), basicSel)));
191
+ const MixinCall = node(
192
+ sequence(g.mixinCallPath, optional(g.MixinArgs), optional(important), optional(literal(';'))));
193
+ // Anonymous mixin callback: `.(…){…}` OR `#(…){…}` — the Chevrotain
194
+ // AnonMixinStart token is `/[.#]\(/`, so both prefixes are valid.
195
+ const AnonymousMixinDefinition = node(
196
+ sequence(regex(/[.#]/), g.MixinArgs, literal('{'), g.declarationList, literal('}')));
197
+ // A bare name with nothing after it is NOT a statement — require args (a mixin
198
+ // call), or a `{}` body / `;` (a qualified rule or mixin call). Otherwise a lone
199
+ // ident like `x` or `nonsense` would be silently accepted.
200
+ // Two combinator-distinguished forms — the PARSER decides validity, not a
201
+ // post-hoc name check:
202
+ // block form: `name [args] [guard] { … }` — a mixin definition or qualified
203
+ // rule (name path may be a bare selector); a `{}` body is required.
204
+ // call form: `path [args] [guard] [;]` — a mixin CALL; the path MUST start
205
+ // with `.`/`#` (mixinCallPath). A bare ident like `nonsense;` is
206
+ // not a `.`/`#` path and has no block, so it matches NEITHER and is
207
+ // reported as unconsumed input.
208
+ const MixinOrQualifiedRule = node(
209
+ choice(
210
+ sequence(g.mixinNamePath, optional(g.MixinArgs), optional(g.Guard), literal('{'), g.declarationList, literal('}')),
211
+ sequence(g.mixinCallPath, optional(g.MixinArgs), optional(g.Guard), optional(important), optional(literal(';')))
212
+ ));
213
+
214
+ // ── Guards / comparisons ───────────────────────────────────────────────────
215
+ // Faithful port of the Chevrotain guard productions (src/productions/guards.ts):
216
+ // guard → 'when' guardOr
217
+ // guardOr → guardAnd ( ('or' | ',') guardAnd )* (left-assoc, 'or')
218
+ // guardAnd → guardTerm ( 'and' guardTerm )* (left-assoc, 'and')
219
+ // guardTerm → [not] ( guardInParens | comparison/value )
220
+ // guardInParens → guardDefault | '(' guardOr ')' (wrapped in Paren)
221
+ // guardDefault → 'default()' → DefaultGuard
222
+ // Precedence: 'or' loops over 'and'; parens nest a fresh guardOr. `not` negates a
223
+ // single term, producing a Condition(negate:true). Comparisons are a left operand
224
+ // followed by an optional `<op> right`.
225
+ const compareOp = regex(/>=|<=|=>|=<|=~|[<>=]/);
226
+ // A single comparison operand (mirrors expressionSum's value role here).
227
+ // `NsAccessor` (`#ns.opts[key]`) is a valid guard operand — `when (#ns.opts[flag])`
228
+ // / `when (#ns.opts[flag] = true)` — so it must parse as ONE operand (ordered
229
+ // before Reference/Paren) rather than falling to the value-Paren, which would
230
+ // swallow `= true` into a Sequence instead of a comparison.
231
+ // `EscapedValue` (`~"…"`, `~(…)`) is a valid comparison operand — `when (~"a" = @s)`
232
+ // — so it must parse as ONE operand (ordered before Quoted/anyValue), else the bare
233
+ // `Quoted` alt fails on the leading `~` and `anyValue` swallows `~"…" = @s` into a
234
+ // Sequence, dropping the comparison.
235
+ const guardOperand = choice(g.NsAccessor, g.Reference, g.Dimension, g.Num, g.Color, g.NamedColor, g.EscapedValue, g.Quoted, g.Call, g.Paren, g.anyValue);
236
+ const Comparison = node(
237
+ sequence(g.Reference, compareOp, choice(g.Reference, g.Dimension, g.Num, g.Color, g.NamedColor, g.EscapedValue, g.Quoted, g.anyValue)));
238
+ const GuardDefault = node(
239
+ regex(/default(?:[ \t\n\r\f]*\([ \t\n\r\f]*\))?(?![-\w])/));
240
+ // '(' guardOr ')' → Paren; or a bare default(). Wrapped in a Paren node.
241
+ const GuardInParens = node(
242
+ choice(
243
+ g.GuardDefault,
244
+ sequence(literal('('), g.GuardOr, literal(')'))
245
+ ));
246
+ // A single guard term: optional `not`, then either a parenthesized guard or a
247
+ // bare comparison (`left <op> right`) / value.
248
+ const GuardTerm = node(
249
+ sequence(
250
+ optional(regex(/not(?![-\w])/)),
251
+ choice(
252
+ g.GuardInParens,
253
+ sequence(guardOperand, optional(sequence(compareOp, guardOperand)))
254
+ )
255
+ ));
256
+ // 'and' chain of terms (left-associative).
257
+ const GuardAnd = node(
258
+ sequence(g.GuardTerm, many(sequence(regex(/and(?![-\w])/), g.GuardTerm))));
259
+ // 'or' / ',' chain of and-expressions (left-associative).
260
+ const GuardOr = node(
261
+ sequence(g.GuardAnd, many(sequence(choice(regex(/or(?![-\w])/), literal(',')), g.GuardAnd))));
262
+ const Guard = node(
263
+ sequence(regex(/when(?![-\w])/), g.GuardOr));
264
+
265
+ // ── Less ampersand / interpolated / extend ──────────────────────────────────
266
+ // `&` (the parent reference) optionally glued to a SUFFIX (`&1`, `&-bar`), which
267
+ // Less appends to the parent's trailing selector (`.rule` + `-bar` → `.rule-bar`).
268
+ // A number/`-` suffix reads as a merge (elements can't start with those). A `.`/`#`
269
+ // PREFIX is NOT part of the ampersand: `.foo-` is a complete, valid dash-ending class,
270
+ // so `.foo-&` is a COMPOUND of the BasicSelector `.foo-` and a plain `&` — it parses
271
+ // as `['.foo-', &]` (two independent simple selectors), never one merge node. The
272
+ // `&(…)` form keeps its paren scan.
273
+ const ampToken = regex(/&[-_a-zA-Z0-9€-￿]*/);
274
+ const LessAmpersand = node(
275
+ sequence(ampToken, optional(sequence(literal('('), scanTo(literal(')'), { skip: [bParen, bSquare, bCurly, singleStr, doubleStr] }), literal(')')))));
276
+ // Selector interpolation: an ident/`.`/`#` run interleaved with `@{…}`, with at
277
+ // least one interpolation. Three dispatch heads so the compiled first-set routes
278
+ // every leading form (a single sequence starting with two empty-matchable runs
279
+ // never exposed `@` as a first token, so a bare/leading `@{…}` was unreachable):
280
+ // • `.`/`#`-prefixed → `.a-@{n}`, `.@{n}`, `#id-@{x}`
281
+ // • ident-prefixed → `div@{n}`, `a@{parent}` (interp right after a type sel)
282
+ // • bare interpolation → `@{parent}` (interp is the whole simple selector)
283
+ const interpPart = choice(lessInterp, regex(/[-_a-zA-Z0-9]+/));
284
+ const InterpolatedSelector = node(
285
+ choice(
286
+ sequence(regex(/[.#]/), many(regex(/[-_a-zA-Z0-9]+/)), lessInterp, many(interpPart)),
287
+ sequence(oneOrMore(regex(/[-_a-zA-Z0-9]+/)), lessInterp, many(interpPart)),
288
+ sequence(lessInterp, many(interpPart))
289
+ ));
290
+
291
+ // ── Selectors (Less: + ampersand/interp, relative combinator) ───────────────
292
+ // `sel when …` is a guarded ruleset: the `when` KEYWORD is the guard boundary,
293
+ // never a selector token — stop the selector run before it (the reference gets
294
+ // this from its lexer's `When` token; scannerless, we assert the keyword). The
295
+ // guard body (`(…)`, `not (…)`, `default()`, `and`/`or` chains) is then parsed
296
+ // by the atomic Guard rule — so the boundary must be just `when`, NOT `when (`
297
+ // (which missed `when not (…)`, `when default()`, …).
298
+ const whenAhead = regex(/when(?![-\w])/i);
299
+ // `:extend(` lookahead — keeps the generic PseudoSelector from claiming extend
300
+ // (extend goes through ExtendPseudo) and lets the compound run stop before it.
301
+ const extendAhead = regex(/::?extend[ \t\n\r\f]*\(/);
302
+ // The two selector-run boundaries (`when` guard keyword, `:extend(`) combined
303
+ // into ONE lookahead regex: `not(selectorBoundary)` ran two regex
304
+ // execs at every simple/compound iteration — ~8% of parse on a selector-dense
305
+ // file. `not(selectorBoundary)` is equivalent (not A ∧ not B = not(A∨B)) at one
306
+ // exec. Used only in the `many(...)` run stops; the standalone extendAhead gate
307
+ // before PseudoSelector is unchanged.
308
+ const selectorBoundary = regex(/when(?![-\w])|::?extend[ \t\n\r\f]*\(/i);
309
+ const simpleSelector = choice(g.AttributeSelector, sequence(not(extendAhead), g.PseudoSelector), g.LessAmpersand, g.InterpolatedSelector, basicSel);
310
+ // unwrap: a single simple selector (76% of compounds — `.btn`, `a`, `:hover`)
311
+ // IS that token; skip the build+frame and pass the child straight through. The
312
+ // builder's single-child path already returned the bare component, so this is
313
+ // byte-identical — a 2+-simple / whitespace-descendant run still builds.
314
+ const CompoundSelector = node(
315
+ sequence(g.simpleSelector, many(sequence(not(selectorBoundary), g.simpleSelector))), undefined, { unwrap: true });
316
+ // A complex selector, optionally terminated by a single `:extend(...)` pseudo.
317
+ // Mirrors Chevrotain's `complexSelector`, which consumes extend (OPTION3) AFTER
318
+ // the whole compound/combinator run — so extend is the LAST thing in the
319
+ // selector, and `.a:extend(.b).c` leaves `.c` unconsumed → parse error
320
+ // (extend-must-be-last). The compound run also stops at `:extend(` (extendAhead).
321
+ // unwrap: single compound (no combinator, no extend) IS the compound.
322
+ const ComplexSelector = node(
323
+ sequence(optional(combinator), g.CompoundSelector, many(sequence(optional(combinator), not(selectorBoundary), g.CompoundSelector)), optional(g.ExtendPseudo)), undefined, { unwrap: true });
324
+ // unwrap: single complex selector (no comma) IS that selector.
325
+ const SelectorList = node(
326
+ sequence(g.ComplexSelector, many(sequence(literal(','), g.ComplexSelector))), undefined, { unwrap: true });
327
+ // Attribute name may carry a CSS namespace prefix (`ns|attr`, `*|attr`,
328
+ // `|attr`). Less also allows interpolation in the name and value: `[@{n}=@{v}]`,
329
+ // `[data=@{attr-data}]`. interpKey matches a run containing `@{…}`.
330
+ // `|` is a namespace separator (`ns|attr`, `*|attr`, `|attr`) ONLY when not
331
+ // followed by `=` — `[prop|="x"]` is the `|=` dash-match operator, not a namespace.
332
+ const attrNsPrefix = optional(sequence(optional(choice(literal('*'), ident)), regex(/\|(?!=)/)));
333
+ const AttributeSelector = node(
334
+ sequence(literal('['), attrNsPrefix, choice(interpKey, ident), optional(sequence(attrOp, choice(singleStr, doubleStr, interpKey, ident), optional(attrMod))), literal(']')));
335
+ // pseudoArg: content inside pseudo parens (used in ExtendStatement too).
336
+ // PseudoSelector uses a two-branch outer choice so PEG backtracking works when
337
+ // SelectorList succeeds internally but ')' doesn't follow (e.g. "!all" suffix).
338
+ const pseudoArg = choice(nth, g.SelectorList, scanTo(literal(')'), { skip: [bParen, bSquare, bCurly, singleStr, doubleStr] }));
339
+ const pseudoSelectorParens = choice(
340
+ sequence(literal('('), choice(nth, g.SelectorList), literal(')')),
341
+ sequence(literal('('), scanTo(literal(')'), { skip: [bParen, bSquare, bCurly, singleStr, doubleStr] }), literal(')'))
342
+ );
343
+ const PseudoSelector = node(
344
+ sequence(pseudoColon, choice(interpKey, ident), optional(g.pseudoSelectorParens)));
345
+
346
+ // ── Extend grammar (faithful port of selectors.ts `extend`/`ampersandExtend`)
347
+ // Chevrotain models extend as: `:extend(` selectorList[inExtend] `)` where each
348
+ // complexSelector inside consumes an optional trailing `all` flag (OPTION2 on
349
+ // T.All / T.AllFlag). The statement form `&:extend(...)` ends with `;`. Here
350
+ // each piece is real grammar (comma list + per-target flag) rather than
351
+ // re-parsing the source text.
352
+ //
353
+ // A per-target `all` / `!all` flag (Chevrotain's T.All / T.AllFlag); both
354
+ // collapse to ExtendFlag.All in the builder.
355
+ const extendFlag = regex(/!?all(?![-\w])/);
356
+ // Lookahead used to stop the target's compound/complex run before the flag, so
357
+ // `all` is consumed as the flag — not swallowed as a trailing ident selector.
358
+ const extendFlagAhead = regex(/!?all(?![-\w])[ \t\n\r\f]*[,)]/);
359
+ // Extend-local compound/complex selectors: identical to the normal ones but they
360
+ // halt before a trailing flag (so `.x all` parses as target `.x` + flag `all`).
361
+ const extendCompound = node('CompoundSelector',
362
+ sequence(g.simpleSelector, many(sequence(not(selectorBoundary), not(extendFlagAhead), g.simpleSelector))));
363
+ const extendComplex = node('ComplexSelector',
364
+ sequence(optional(combinator), g.extendCompound, many(sequence(optional(combinator), not(whenAhead), not(extendFlagAhead), g.extendCompound))));
365
+ // A single extend target: a complex selector + its optional flag.
366
+ const ExtendTarget = node(
367
+ sequence(g.extendComplex, optional(extendFlag)));
368
+ // The comma-separated target list inside `extend( … )` (selectorList[inExtend]).
369
+ const extendBody = sepBy(g.ExtendTarget, literal(','));
370
+ // `:extend(` body `)` — the in-selector pseudo form (selectors.ts `extend`).
371
+ const ExtendPseudo = node(
372
+ sequence(pseudoColon, literal('extend'), literal('('), extendBody, expect(literal(')'), ')')));
373
+ // `&:extend(...)` statement, terminated by `;` (selectors.ts `ampersandExtend`).
374
+ // The leading `&` is REQUIRED: the reference's `ampersandExtend` does an
375
+ // unconditional `$.CONSUME(T.Ampersand)`, so a bare `:extend(...)` is NOT a valid
376
+ // standalone statement. Making `&` mandatory keeps `.a:extend(.b).c { … }` from
377
+ // mis-splitting into `.a` (mixin call) + bare `:extend(.b)` + `.c { … }`; instead
378
+ // the leftover `:extend(` after the `.a` mixin call is unconsumed input → one
379
+ // parse error (faithful: extend must be the last thing in its selector).
380
+ const ExtendStatement = node(
381
+ sequence(g.LessAmpersand, g.ExtendPseudo, optional(literal(';'))));
382
+
383
+ // ── Ruleset / declarations (Less-aware) ─────────────────────────────────────
384
+ const Ruleset = node(
385
+ sequence(g.SelectorList, optional(g.Guard), literal('{'), g.declarationList, expect(literal('}'), '}')));
386
+ // A nested mixin DEFINITION inside a rule body: `.name(args) [guard] { … }`.
387
+ // Strict — requires the `()` arg list AND a `{}` body, so it never matches a
388
+ // plain declaration or a `.name { }` ruleset. (declarationList only had MixinCall,
389
+ // which has no body, so nested definitions e.g. `.vars(){…}` were unmodelled.)
390
+ const NestedMixinDefinition = node('MixinOrQualifiedRule',
391
+ sequence(g.mixinCallPath, g.MixinArgs, optional(g.Guard), literal('{'), g.declarationList, literal('}')));
392
+ // The per-statement choice for a `{ … }` body (ruleset body + at-rule body).
393
+ // Exposed as a named rule so extending grammars (SCSS) can inject their own
394
+ // block statements ahead of it — `many(choice(g.ScssIf, …, g.blockItem))`.
395
+ // `NestedMixinDefinition` stays a local const referenced here (Less-only).
396
+ const blockItem = choice(
397
+ g.VarDeclaration, g.VarCall, g.QueryAtRuleBlock, g.AtRuleBlock, g.ImportAtRuleStatement, g.AtRuleStatement, g.ExtendStatement, g.Ruleset, NestedMixinDefinition, g.EachFor, g.MixinCall, g.Declaration, g.CustomDeclaration,
398
+ // A bare function-call statement in a body, e.g. `each(@list, { … });`. Needs
399
+ // `ident(` so it never shadows a Declaration (which needs `:`).
400
+ sequence(g.Call, optional(literal(';'))), literal(';')
401
+ );
402
+ const declarationList = many(g.blockItem);
403
+ // Property name may itself be interpolated (`@{prop}: …`, `pre-@{x}-post: …`).
404
+ // Chevrotain lexes the name as a single Ident/InterpolatedIdent token whose image
405
+ // carries the `@{…}` runs; `declaration` then routes an image containing `@`/`$`
406
+ // through getInterpolatedNode. We mirror that: try the interpolated-ident regex
407
+ // first (it requires at least one `@{…}`), else a plain ident.
408
+ const Declaration = node(
409
+ sequence(declPropName, optional(choice(literal('+_'), literal('+'))), literal(':'), optional(g.valueList), optional(important), optional(literal(';'))));
410
+ const customValue = sequence(g.valueList, not(regex(/[^\s;}]/)));
411
+ // Opportunistic structuring for a `{ … }` custom-property value: try it as a
412
+ // real declaration body (so nested `@var`/calls evaluate normally, same as the
413
+ // `[…]` case already does via customValue's valueList), tolerant of anything
414
+ // that isn't CSS-shaped. No `expect()` on the closing `}` — a non-declaration
415
+ // body (arbitrary tokens) simply fails this alt with no error recorded, and
416
+ // `choice` falls through to the raw-text cpValue capture below.
417
+ const customCurlyBlock = node('Block',
418
+ sequence(literal('{'), g.declarationList, literal('}')));
419
+ // Predictive custom-property value region — NO scanTo, NO skip. Content runs
420
+ // interleaved with recursively-balanced ()/[]/{} groups, each closed by expect().
421
+ // So an unmatched, stray, or CROSS-TYPE bracket (`({[ })`) surfaces a syntax error
422
+ // instead of being swallowed. noTrivia keeps the value verbatim; inside a group a
423
+ // `;` is content (only the group's own close ends it), at top level `;`/`}` end it.
424
+ // Custom-property value = CSS `<declaration-value>` (spec-close, not Less 4.x's
425
+ // permissive pass): opaque tokens with ()/[]/{} balanced; `/* … */` comments are
426
+ // preserved and their contents NOT tokenized (`/* { ; } */`); strings are
427
+ // line-bounded, so a quote left unclosed before the newline is a
428
+ // `<bad-string-token>` → hard error (matches browsers — `--x: don't` is invalid).
429
+ // `//` is NOT a comment here (CSS), just delim content. A `{ … }` value that fits
430
+ // a declaration list is structured upstream by customCurlyBlock; this is the
431
+ // fallback for non-CSS-shaped values.
432
+ const cpSingleStr = regex(/'(?:[^'\n\\]|\\.)*'/);
433
+ const cpDoubleStr = regex(/"(?:[^"\n\\]|\\.)*"/);
434
+ // Content runs include CSS escapes (`\'`, `\(`, `\;`): `\` + any non-newline is an
435
+ // escaped code point (§4.3.7), so an escaped quote/bracket/semicolon is literal
436
+ // content, NOT a string/bracket/terminator. A lone `/` (division) is content;
437
+ // `/*` is left for the comment alt.
438
+ const cpInnerContent = regex(/(?:\\[^\n]|[^(){}[\]'"\/\\])+|\/(?!\*)/);
439
+ const cpOuterContent = regex(/(?:\\[^\n]|[^(){}[\];'"\/\\])+|\/(?!\*)/);
440
+ const cpInner = many(choice(cpInnerContent, comment, g.cpParen, g.cpSquare, g.cpCurly, cpSingleStr, cpDoubleStr));
441
+ const cpParen = sequence(literal('('), g.cpInner, expect(literal(')')));
442
+ const cpSquare = sequence(literal('['), g.cpInner, expect(literal(']')));
443
+ const cpCurly = sequence(literal('{'), g.cpInner, expect(literal('}')));
444
+ const cpValue = noTrivia(many(choice(cpOuterContent, comment, g.cpParen, g.cpSquare, g.cpCurly, cpSingleStr, cpDoubleStr)));
445
+ const CustomDeclaration = node(
446
+ sequence(choice(customPropInterp, customProp), literal(':'),
447
+ choice(g.customCurlyBlock, g.customValue, g.cpValue),
448
+ optional(literal(';'))));
449
+ const declaration = choice(g.VarDeclaration, g.CustomDeclaration, g.Declaration);
450
+
451
+ // ── Values (Less: + Reference, NamedColor, EscapedValue) ────────────────────
452
+ // A comma must be followed by a value — a trailing comma (`a, b,`) is a parse
453
+ // error in Less v5 (stricter than Less 4.x, which tolerated it). The dangling
454
+ // comma is left unconsumed and surfaces as one syntax error via the net.
455
+ const valueList = sequence(g.valueSequence, many(sequence(literal(','), g.valueSequence)));
456
+ // A space-separated value sequence: each item is a full top-level EXPRESSION
457
+ // (topSum), so arithmetic folds into the grammar (`1 + 2` → one Operation) while
458
+ // non-operator items stay a list (`1px 2px 3px`). topSum collapses to the bare
459
+ // operand when there is no operator, so a plain list is byte-identical to before.
460
+ const valueSequence = oneOrMore(g.topSum);
461
+ // Interpolated value token (`@{colorVar}`, `pre-@{x}`). Chevrotain lexes this as
462
+ // InterpolatedIdent and `processValueToken` runs it through getInterpolatedOrString
463
+ // → Interpolated (role=ident). Ordered before Reference: `@{` cannot match lessVar,
464
+ // and anyValueTok excludes `{`, so this is the only rule that accepts it.
465
+ const InterpValue = node(
466
+ interpKey);
467
+ // A namespace INDEXED-accessor reference in value position: a `.`/`#` compound
468
+ // selector-path head (`#ns.options`, `.mixin`) glued (noTrivia) to a `[accessor]`
469
+ // and then any further `[accessor]`/`(call)` chain. This must parse as ONE value
470
+ // operand BEFORE arithmetic folding — otherwise `#ns.options[val1] + 5px` splits
471
+ // into the bare string `#ns.options` plus an Operation whose left operand is the
472
+ // lone `[val1]` SquareParen, so the accessor never binds to the namespace path.
473
+ // Ordered before Color/SquareParen/anyValue in `value`: a hex-color-shaped head
474
+ // (`#DEF.colors[primary]`) would otherwise be eaten by Color as a bare `#DEF`,
475
+ // stranding `.colors[primary]` as a separate single-segment accessor that loses
476
+ // the `#DEF` namespace hop. NsAccessor requires a glued `[` (refIndex first), so a
477
+ // plain color `#DEF` — no bracket — still falls through to Color unchanged.
478
+ // Requiring the FIRST segment be
479
+ // a `[` (not a `(`) keeps every call-headed form — `.mixin()`, `.mixin()[k]`,
480
+ // `#ns.x(.a[])[k]`, chained `.a() > .b()` — on the existing GluedParen /
481
+ // _tryParseNamespaceRef reassembly paths, which structure call args richly.
482
+ // The builder (_buildNsAccessor) reuses the same mixin-ruleset assembly as the
483
+ // declaration-value _assembleSegment path.
484
+ const nsHead = regex(/(?<![>+~|][ \t]?)[.#]-?(?:[_a-zA-Z€-￿][-_a-zA-Z0-9€-￿]*)(?:[.#]-?[_a-zA-Z€-￿][-_a-zA-Z0-9€-￿]*)*/);
485
+ const NsAccessor = node(
486
+ noTrivia(sequence(nsHead, refIndex, many(choice(refIndex, refCall)))));
487
+ // A CSS `unicode-range` token (`U+A5`, `U+0-7F`, `U+0???`, `U+??????`). Ordered
488
+ // before Dimension/Num/anyValue so the whole `U+…` run is one verbatim value — a
489
+ // bare `ident` would stop at the `+` and leave `+0???`/`0-7F` to be mis-folded as
490
+ // arithmetic. @see https://drafts.csswg.org/css-syntax/#urange-syntax
491
+ const UnicodeRange = node(
492
+ regex(/[Uu]\+[0-9A-Fa-f?]{1,6}(?:-[0-9A-Fa-f]{1,6})?/));
493
+ const value = choice(g.InterpValue, g.Reference, g.UnicodeRange, g.Dimension, g.Num, g.NsAccessor, g.Color, g.NamedColor, g.Url, g.CalcCall, g.FormatCall, g.Call, g.EscapedValue, g.GluedParen, g.Paren, g.SquareParen, g.Quoted, g.anyValue);
494
+ // ── Math expressions — precedence in the grammar (port of expressionSum /
495
+ // expressionProduct). `* / %` bind tighter than `+ -`; left-associative. The
496
+ // `collapse` option makes a single-operand level pass its operand straight
497
+ // through (no Operation wrapper), so a plain value is byte-identical to the
498
+ // pre-expression grammar. The build folds the flat `operand op operand …`
499
+ // children into Operation nodes (see _buildOperation).
500
+ //
501
+ // `+`/`-` operator token: a sign NOT glued to a following number. A glued
502
+ // `-23`/`+5` is ONE signed operand (Num/Dimension eats the sign), mirroring the
503
+ // lexer's Plus/Minus-vs-Signed split — so `1 - 2` subtracts, but `1 -23` (space
504
+ // before, glued) does NOT continue the sum: the sign belongs to the next
505
+ // operand. At top level that trailing operand makes a space-list; inside a bare
506
+ // paren it has no operator before it, so the paren's `)` fails (a parse error,
507
+ // matching Less 4.x on `(12 (13))` / `(… 5 -23)`).
508
+ // The deprecated `./` dot-slash operator is intentionally NOT accepted — it was
509
+ // obscure, rarely used, and removed in v5. A `./` in a math context therefore
510
+ // leaves the `.` unconsumed and surfaces as a parse error (wrap division in
511
+ // parens instead).
512
+ const prodOp = regex(/[*\/%]/);
513
+ // A `+`/`-` operator fires when it is NOT a signed operand glued after a space:
514
+ // • `[-+](?![0-9.])` — standalone (space / non-number after): `8 + 4`, `8 - (…)`.
515
+ // • `(?<=\S)[-+](?=[0-9.])` — glued with NO space before (port of the Signed
516
+ // branch's noSep gate): `8+4`, `8-4` are arithmetic. `8 +4` (space before,
517
+ // glued) matches NEITHER — the `+4` is a separate signed operand (a list at
518
+ // top level, a paren error inside `( … )`).
519
+ const sumOp = regex(/[-+](?![0-9.])|(?<=\S)[-+](?=[0-9.])/);
520
+ // Leading unary minus → Negative (port of expressionValue's OPTION(Minus)). Only
521
+ // a STANDALONE `-` (not glued to a number — that's a signed operand) at an operand
522
+ // position: `-(@a * 2)`, `-@var`. The sum level consumes a binary `-` first, so
523
+ // this only fires where an operand is expected.
524
+ const Negative = node(
525
+ sequence(regex(/-(?![0-9.])/), g.value));
526
+ const operand = choice(g.Negative, g.value);
527
+ const mathProduct = node('Operation',
528
+ sequence(operand, many(sequence(prodOp, operand))), undefined, { collapse: true });
529
+ const mathSum = node('Operation',
530
+ sequence(g.mathProduct, many(sequence(sumOp, g.mathProduct))), undefined, { collapse: true });
531
+ // Top-level (declaration / space-list) variant of the same precedence grammar.
532
+ // Identical shape, but built as `OperationTop`, whose slash-vs-list decision uses
533
+ // the DECLARATION context: `/` divides only under `math: always` (default
534
+ // `parens-division` keeps a top-level `/` a slash-List, e.g. `font: 12px/1.5`).
535
+ // A math paren nested inside a top-level value still uses the `Operation` variant
536
+ // (slash divides), since being in-parens turns division on.
537
+ const topProduct = node('OperationTop',
538
+ sequence(operand, many(sequence(prodOp, operand))), undefined, { collapse: true });
539
+ const topSum = node('OperationTop',
540
+ sequence(g.topProduct, many(sequence(sumOp, g.topProduct))), undefined, { collapse: true });
541
+ // An escaped paren `~( … )` is a RAW list, not a math expression: it holds an
542
+ // arbitrary space / comma / `;`-separated value sequence (`~(1 2 3)`, `~(1; 2)`),
543
+ // so it uses the permissive body — unlike a bare `( … )`, which is one expression.
544
+ const escapedParen = node('Paren', sequence(literal('('), g.permissiveParenBody));
545
+ const EscapedValue = node(
546
+ sequence(literal('~'), choice(escapedParen, g.Quoted)));
547
+ const NamedColor = node(regex(/(?:lightgoldenrodyellow|mediumspringgreen|mediumaquamarine|mediumslateblue|mediumturquoise|mediumvioletred|blanchedalmond|cornflowerblue|darkolivegreen|lightslategray|lightslategrey|lightsteelblue|mediumseagreen|darkgoldenrod|darkslateblue|darkslategray|darkslategrey|darkturquoise|lavenderblush|lightseagreen|palegoldenrod|paleturquoise|palevioletred|rebeccapurple|antiquewhite|currentcolor|darkseagreen|lemonchiffon|lightskyblue|mediumorchid|mediumpurple|midnightblue|darkmagenta|deepskyblue|floralwhite|forestgreen|greenyellow|lightsalmon|lightyellow|navajowhite|saddlebrown|springgreen|transparent|yellowgreen|aquamarine|blueviolet|chartreuse|darkorange|darkorchid|darksalmon|darkviolet|dodgerblue|ghostwhite|lightcoral|lightgreen|mediumblue|papayawhip|powderblue|sandybrown|whitesmoke|aliceblue|burlywood|cadetblue|chocolate|darkgreen|darkkhaki|firebrick|gainsboro|goldenrod|indianred|lawngreen|lightblue|lightcyan|lightgray|lightgrey|lightpink|limegreen|mintcream|mistyrose|olivedrab|orangered|palegreen|peachpuff|rosybrown|royalblue|slateblue|slategray|slategrey|steelblue|turquoise|cornsilk|darkblue|darkcyan|darkgray|darkgrey|deeppink|honeydew|lavender|moccasin|seagreen|seashell|crimson|darkred|dimgray|dimgrey|fuchsia|hotpink|magenta|oldlace|skyblue|thistle|bisque|indigo|maroon|orange|orchid|purple|salmon|sienna|silver|tomato|violet|yellow|azure|beige|black|brown|coral|green|ivory|khaki|linen|olive|wheat|white|aqua|blue|cyan|gold|gray|grey|lime|navy|peru|pink|plum|snow|teal|red|tan)(?![-_a-zA-Z0-9(])/i));
548
+ // unit collapsed to one regex (Dimension still reads number + unit as two leaves).
549
+ // number + unit must be contiguous \u2014 the surrounding valueSequence runs with
550
+ // trivia enabled, so without noTrivia() a space (`1 %`, `10 px`) would still be
551
+ // glued into a Dimension. Chevrotain lexes those as Num + a separate token.
552
+ const Dimension = node(noTrivia(sequence(numPart, regex(/-?[_a-zA-Z\u0080-\uffff][-_a-zA-Z0-9\u0080-\uffff]*|%/))));
553
+ // `Num` and `Color` now come from the shared `numericRules` fragment, spread into
554
+ // the return object below (identical to the CSS grammar's definitions).
555
+ const Url = node(parser({ trivia: urlWs }, sequence(urlOpen, optional(choice(singleStr, doubleStr, urlInner)), literal(')'))));
556
+ // A bare paren holds ONE expression per comma-segment (a Sum), NOT a
557
+ // space-separated value sequence — `(12 13)` / `(12 (13))` are incoherent (two
558
+ // operands, no operator) and error, matching Less 4.x. `;`-separated segments
559
+ // (used by `~( … ; … )` escapes) and commas are still lists. The closing `)` is
560
+ // committed (`expect`), so a leftover operand surfaces as `Expected ')'` at the
561
+ // offending token rather than being silently left unconsumed.
562
+ // A paren item is one expression, optionally followed by a single comparison
563
+ // (`(@i > 5)`) OR a declaration-form `feature: value` pair (`(min-width: @val)` —
564
+ // a media condition stored for reuse). The separator/operator stays a raw leaf in
565
+ // the stream (not folded into an Operation), so the Paren builder sees the same
566
+ // flat `left op right` it always did. A bare `12 (13)` has NO separator, so it
567
+ // still fails the `)`.
568
+ const parenSep = choice(compareOp, literal(':'));
569
+ const parenExpr = sequence(g.mathSum, optional(sequence(parenSep, g.mathSum)));
570
+ // A paren whose content BEGINS with a `#`/`.` namespace selector is a
571
+ // namespace-lookup reference (`(#ns.options[option])`, `(.mixin()[key])`), not an
572
+ // arithmetic expression — its `[…]`/`(…)` accessor chain is captured as a value
573
+ // sequence and the Paren builder reassembles it into a Reference/Call
574
+ // (_tryParseNamespaceRef). The lookahead requires a selector START (`.`/`#` + a
575
+ // name char), so `.5` (a number) and a bare `12 (13)` are NOT namespace refs and
576
+ // stay strict expressions — the incoherent `12 (13)` still fails the `)`.
577
+ const namespaceAhead = regex('(?=[.#]-?[_a-zA-Z\\u0080-\\uffff])');
578
+ const parenItem = choice(sequence(namespaceAhead, g.valueSequence), parenExpr);
579
+ const parenExprList = sequence(parenItem, many(sequence(literal(','), parenItem)));
580
+ const parenBody = sequence(optional(sequence(g.parenExprList, many(sequence(literal(';'), optional(g.parenExprList))))), expect(literal(')')));
581
+ // Permissive paren body (the pre-expression valueList form). Used ONLY by
582
+ // GluedParen — a `(` glued (no space) to a preceding selector/accessor token,
583
+ // i.e. mixin-reference ARGS (`.mixin1(@foo: bar)`, `#ns.x(.valToGet[])`), which
584
+ // hold arbitrary named args / accessor chains, not arithmetic. A `(` with space
585
+ // before it (or at value start) is a real value paren and takes the strict
586
+ // single-expression `parenBody` above, so `(12 (13))` still errors.
587
+ const permissiveParenBody = sequence(optional(sequence(g.valueList, many(sequence(literal(';'), optional(g.valueList))))), expect(literal(')')));
588
+ // A bare detached ruleset `{ … }` in value / function-argument position → a Mixin.
589
+ const DetachedRuleset = node(sequence(literal('{'), g.declarationList, literal('}')));
590
+ // Function-call arguments are their OWN production (parity with the Chevrotain
591
+ // functionCallArgs/callArgument rules), NOT `parenBody`: unlike a parenthesized
592
+ // value, a function argument may be an anonymous mixin `.(…){…}` or a bare
593
+ // detached ruleset `{…}` — e.g. `each(@list, { … })`, `func({a:1}, {b:2})`. The
594
+ // comma phase takes value SEQUENCES (comma is the arg separator); after a `;` the
595
+ // args become value LISTS (comma allowed within an arg).
596
+ // Function-call args and mixin-call args share ONE set of arg productions, so an
597
+ // arithmetic arg like `@a * 2` is a real Operation in both, and values are
598
+ // assembled by the shared `_assembleArgs` builder (Keyword-ification + trivia — no
599
+ // raw text, no manual trimming). Beyond values / anon-mixin / detached-ruleset,
600
+ // the args admit the `@x: value` NAMED form and the `...` / `@x...` VARIADIC form.
601
+ // Named args flow through function calls too (dispatch to a named-param function,
602
+ // e.g. a Sass fn); the runtime rejects them if the target declares no names.
603
+ // Ordered choice: `...`/`:` lookahead lets variadic/named win, else the value
604
+ // combinator consumes the whole expression (so `@a * 2` is never truncated at
605
+ // `@a`). A bare `@a` is a Reference (the CALL shape); the mixin-DEFINITION builder
606
+ // reinterprets a lone `@name` as a param.
607
+ const argRest = node('Rest', choice(sequence(lessVar, literal('...')), literal('...')));
608
+ const argNamedSeq = node('NamedArg', sequence(lessVar, literal(':'), choice(DetachedRuleset, g.valueSequence)));
609
+ // ── Name-independent condition arguments ─────────────────────────────────────
610
+ // A top-level condition operator (`> < >= <= = and or not`) inside ANY call's
611
+ // argument makes that argument a `Condition` — no name dispatch on `if`/`boolean`.
612
+ // The condition operators layer ON TOP of the ordinary value production, so nesting
613
+ // (`not(2 < 1)`, `true and isnumber(6)`) falls out of the grammar's own recursion:
614
+ // the `(…)` value-Paren already parses an inner comparison (parenSep = compareOp),
615
+ // and `and`/`or` split terms so a bare keyword never swallows the operator.
616
+ //
617
+ // The whole layer is GATED to only match when a real operator is present: each
618
+ // `ArgCondition` alternative's distinguishing token past the operand is an operator
619
+ // (leading `not`, a `compareOp`, or `and`/`or`), so a plain value / space-list arg
620
+ // matches NONE and falls through to the unchanged `valueSequence` below — the
621
+ // pre-existing arg is byte-identical, and mixin-DEFINITION params (never a top-level
622
+ // condition) are unaffected.
623
+ const notKw = regex(/not(?![-\w])/i);
624
+ const andKw = regex(/and(?![-\w])/i);
625
+ const orKw = regex(/or(?![-\w])/i);
626
+ // A standalone top-level condition operator — used as a negative lookahead so the
627
+ // bounded value operand stops before it instead of eating it as a keyword/anyValue.
628
+ const condStopAhead = regex(/(?:>=|<=|=>|=<|=~|[<>=]|(?:and|or)(?![-\w]))/i);
629
+ // A bounded value/space-list operand: a `valueSequence` that stops at a top-level
630
+ // condition operator (so `@a > 5 and @b` splits into operands, not one space-list).
631
+ const condOperand = oneOrMore(sequence(not(condStopAhead), g.topSum));
632
+ // A PARENTHESIZED sub-condition operand: `( CondArgOr )`. Necessary because a `(`
633
+ // glued to a preceding word (`not(…)`) takes the permissive mixin-arg Paren, whose
634
+ // body is a raw value list — it would NOT parse the inner `2 > 1` as a comparison.
635
+ // Parsing the paren body as a full `CondArgOr` restores the guard-grammar behaviour
636
+ // (`not(2 > 1)`, `(@a > 0)`, `(true)`), built into a `Paren` wrapping the condition.
637
+ const CondArgParen = node('GuardInParens',
638
+ sequence(literal('('), g.CondArgOr, literal(')')));
639
+ // A single-operand core: a parenthesized sub-condition OR a bounded value, with an
640
+ // optional trailing `<op> right` comparison.
641
+ const condCore = sequence(
642
+ choice(g.CondArgParen, condOperand),
643
+ optional(sequence(compareOp, choice(g.CondArgParen, condOperand))));
644
+ // A single condition term: optional leading `not`, then the operand core. `not`
645
+ // negates the term into a `Condition{negate}`; a bare comparison folds into
646
+ // `Condition[left, op, right]`; a plain operand passes through.
647
+ const CondArgTerm = node(
648
+ sequence(optional(notKw), condCore));
649
+ const CondArgAnd = node('CondArgAnd',
650
+ sequence(g.CondArgTerm, many(sequence(andKw, g.CondArgTerm))));
651
+ const CondArgOr = node('CondArgOr',
652
+ sequence(g.CondArgAnd, many(sequence(orKw, g.CondArgAnd))));
653
+ // An OPERATOR-BEARING term: a leading `not`, OR a comparison (`left <op> right`).
654
+ // (A bare operand with no `not`/`compareOp` is NOT operator-bearing — that path is
655
+ // reserved for the plain `valueSequence` arg.) Built via the same `CondArgTerm`
656
+ // builder — the tag is shared, so `not`/comparison fold into a `Condition`.
657
+ const CondArgTermOp = node('CondArgTerm',
658
+ choice(
659
+ sequence(notKw, condCore),
660
+ sequence(choice(g.CondArgParen, condOperand), compareOp, choice(g.CondArgParen, condOperand))
661
+ ));
662
+ // An `and`-group that carries ≥1 operator: either its FIRST term is operator-bearing,
663
+ // or it has an explicit `and`. Built via the shared `CondArgAnd` fold.
664
+ const CondArgAndOp = node('CondArgAnd',
665
+ choice(
666
+ sequence(g.CondArgTermOp, many(sequence(andKw, g.CondArgTerm))),
667
+ sequence(g.CondArgTerm, oneOrMore(sequence(andKw, g.CondArgTerm)))
668
+ ));
669
+ // GATE — `ArgCondition` matches ONLY an arg that carries a REAL top-level condition
670
+ // operator (a `not`, a comparison, or an `and`/`or`); a plain value / space-list
671
+ // matches NEITHER alternative and falls through to the unchanged `valueSequence`,
672
+ // so ordinary args (and mixin-def params, never a top-level condition) build
673
+ // byte-identically. No paren-aware lookahead scan: the operator requirement is
674
+ // structural (`CondArgAndOp` / a mandatory `oneOrMore` `or`), and nesting
675
+ // (`not(2 < 1)`, `true and isnumber(6)`) falls out of the grammar's own recursion —
676
+ // the value-Paren parses its inner comparison; `and`/`or` split terms. Built as a
677
+ // `CondArgOr` (shared fold): a leading op-bearing and-group, or a bare-headed `or`.
678
+ const ArgCondition = node('CondArgOr',
679
+ choice(
680
+ sequence(g.CondArgAndOp, many(sequence(orKw, g.CondArgAnd))),
681
+ sequence(g.CondArgAnd, oneOrMore(sequence(orKw, g.CondArgAnd)))
682
+ ));
683
+ const callArgSeq = choice(argRest, argNamedSeq, g.AnonymousMixinDefinition, DetachedRuleset, ArgCondition, g.valueSequence);
684
+ // Function-call args and mixin-call args are now IDENTICAL — one `argsInner`. After
685
+ // a semicolon, commas keep splitting args (`sepBy(callArgSeq, ',')`), so both `.m(…)`
686
+ // and `foo(…)` catch the one illegal case: mixing the comma and semicolon ARG
687
+ // separators — i.e. two named params in a single semicolon-group (`@a: 1, @b: 2`) —
688
+ // rather than mis-parsing it as one list-valued param. (This is only about the comma
689
+ // vs semicolon argument separators; a `/` inside a value — `16px/1.5`, `1fr / 2fr` —
690
+ // is a value-internal separator and is never involved.) Value assembly is identical
691
+ // (`_assembleArgs` folds a comma run into a List). Named args + spreads flow through
692
+ // FUNCTION calls too — a `.jess` extension; validity is a dialect concern
693
+ // (Less-4-compat flags them; the runtime rejects a target that declares no names).
694
+ const argsInner = optional(sequence(sepBy(callArgSeq, literal(',')), many(sequence(literal(';'), optional(sepBy(callArgSeq, literal(',')))))));
695
+ const functionCallArgs = sequence(argsInner, literal(')'));
696
+ const MixinArgs = node(sequence(literal('('), argsInner, literal(')')));
697
+ // `calc(…)` follows the CSS math grammar, whose only operators are `+ - * /` — a
698
+ // bare `%` operand (e.g. `calc(1 %)`) is a syntax error (Chevrotain: mathProduct
699
+ // has no `%` alt, so the trailing `%` fails the closing `)`). We model calc as a
700
+ // Call whose body excludes a standalone `%` token, so `1 %` leaves the `%`
701
+ // unconsumed and the `)` fails → one parse error. A percentage glued to a number
702
+ // (`100%`) is a Dimension and unaffected.
703
+ const calcAnyTok = regex(/[+\-*/=<>|~^]+|[^\s;{}\[\]()'",!%]+/);
704
+ const calcAnyValue = choice(ident, calcAnyTok);
705
+ const calcValue = choice(g.InterpValue, g.Reference, g.Dimension, g.Num, g.Color, g.NamedColor, g.Url, g.Call, g.EscapedValue, g.Paren, g.SquareParen, g.Quoted, calcAnyValue);
706
+ // calc math grammar (port of mathSum/mathProduct): operators are ONLY `+ - * /` —
707
+ // NO `%` (a standalone `%` stays unconsumed → the `)` fails → syntax error, per
708
+ // CSS calc). `/` always divides here (calc is a math context), built as
709
+ // `Operation`. Precedence + collapse identical to the value-position rules.
710
+ const calcProdOp = regex(/[*\/]/);
711
+ const calcProduct = node('Operation',
712
+ sequence(calcValue, many(sequence(calcProdOp, calcValue))), undefined, { collapse: true });
713
+ const calcSum = node('Operation',
714
+ sequence(calcProduct, many(sequence(sumOp, calcProduct))), undefined, { collapse: true });
715
+ const calcSequence = oneOrMore(calcSum);
716
+ const calcList = sequence(calcSequence, many(sequence(literal(','), calcSequence)));
717
+ const calcBody = sequence(optional(sequence(calcList, many(sequence(literal(';'), optional(calcList))))), expect(literal(')')));
718
+ // `CalcCall` (calc(…)) and the plain value-position `Paren` come from the shared
719
+ // `parenRules` fragment (spread below); they defer to g.calcBody / g.parenBody here.
720
+ const Call = node(sequence(ident, literal('('), functionCallArgs));
721
+ // A bare value paren `( … )`. Defined locally (not inherited from CSS) so the `(`→body
722
+ // trivia uses Less `rw`, which skips `//` line comments — CSS `rw` does not, so a `//`
723
+ // right after `(` (e.g. `(@a * // c\n @b)`) would otherwise not be consumed as trivia.
724
+ const Paren = node(sequence(literal('('), g.parenBody));
725
+ // Mixin-argument paren: `(` immediately preceded (lookbehind, no trivia) by a
726
+ // selector / accessor char — the args of a `.name(…)` / `#ns.x(…)` reference.
727
+ // Parsed permissively; the Declaration builder reassembles the selector +
728
+ // round-paren-args + square-paren-accessor items into a Reference/Call chain.
729
+ // A trailing `-` counts ONLY when it terminates an identifier (`.my-mixin-(…)`),
730
+ // never a standalone unary minus — `-(@a / 2)` is a Negative around a math Paren,
731
+ // so its `(` must fall through to the strict `g.Paren` (slash divides in-parens).
732
+ const GluedParen = node('Paren', sequence(regex('(?<=[)\\]\\w.#\\u0080-\\uffff]|[\\w.#\\u0080-\\uffff]-)\\('), g.permissiveParenBody));
733
+ const squareParenBody = sequence(optional(g.valueList), literal(']'));
734
+ const SquareParen = node(sequence(literal('['), g.squareParenBody));
735
+ const anyValue = choice(ident, anyValueTok);
736
+
737
+ // `each(<iterable>, { … })` (or `.(@p) { … }`) is a $for control form, not a
738
+ // function call — parse it straight into a `For` node. The callback is a literal
739
+ // detached ruleset / anonymous mixin; a bare `each(list)` with no block callback
740
+ // falls through to a normal Call.
741
+ // `each(<iterable>, { … })` builds a `For` directly (not a throwaway Call). Its
742
+ // ARGUMENTS reuse the shared `functionCallArgs` — same args any function accepts,
743
+ // so the iterable + detached-ruleset / `.(…){…}` callback parse uniformly. The
744
+ // builder pulls the callback (a Mixin) out of the parsed args.
745
+ const EachFor = node('For',
746
+ sequence(
747
+ regex(/each(?![-\w])/i), literal('('), functionCallArgs, optional(literal(';'))
748
+ ));
749
+
750
+ // ── Logical / conditional functions (Less) ──────────────────────────────────
751
+ // `if(cond, then[, else])` and `boolean(cond)` are NOT name-dispatched in the
752
+ // grammar: they are ordinary function `Call`s whose condition argument parses
753
+ // through the name-independent `ArgCondition` layer (a top-level `> < >= <= = and
754
+ // or not` in ANY call's argument becomes a `Condition`). Eval already registers
755
+ // `if`/`boolean` as ordinary functions that consume the parsed condition — so this
756
+ // is a parse-only unification: `if`/`boolean`/`#ns.if`/`.if`/`foo` all route through
757
+ // one `Call` production. The `and`/`or`/`not`/comparison sub-grammar the `when`
758
+ // guard uses (GuardOr) is unchanged; only the value-position call dispatch merged.
759
+
760
+ // ── Deprecated Less `%()` string-format function ─────────────────────────────
761
+ // `%(format, args…)` is printf-style formatting. We LOWER it at build time into a
762
+ // `Quoted(Interpolated)` — the canonical string-interpolation node — with a
763
+ // deprecation warning (see `_buildFormatCall`). The `%(?=\()` lookahead matches
764
+ // ONLY when the `(` follows immediately, so the bare `%` mod operator (`10 % 3`,
765
+ // parsed by `prodOp`) is UNAFFECTED. Ordered before the generic `Call` in `value`.
766
+ const FormatCall = node(
767
+ sequence(regex(/%(?=\()/), literal('('), functionCallArgs));
768
+
769
+ // ── At-rules ───────────────────────────────────────────────────────────────
770
+ const atPrelude = optional(scanTo(choice(literal('{'), literal(';')), { skip: [bParen, bSquare, bCurly, singleStr, doubleStr] }));
771
+
772
+ // ── Structured, committed query block (@media / @container / @supports) ──────
773
+ // The flat `atPrelude` above walks past ANY bracket content to the first
774
+ // top-level `{`/`;`, so a stray/unbalanced bracket (`@media (extra: bracket))`)
775
+ // is silently swallowed — 0 errors. This structured prelude mirrors the CSS
776
+ // query grammar (grammar.ts QueryCondition/QueryInParens/QueryFeature): each
777
+ // `(…)` is a real balanced group, so a top-level stray `)` is NOT consumed by
778
+ // the prelude, and the committed `expect('{')` then fails ON that `)` → 1 error.
779
+ // Because the query keyword IS consumed, this rule does not fall through to the
780
+ // swallowing generic AtRuleBlock. Well-formed Less-specific preludes that this
781
+ // structured shape can't parse (bare `@var`, `#ns.x[@k]`, `~"…"`, `@media
782
+ // screen`) fail the prelude BEFORE the commit point, so the sequence backtracks
783
+ // cleanly and the generic AtRuleBlock (→ `_buildAtRulePrelude`) handles them.
784
+ // @see https://www.w3.org/TR/mediaqueries-5/#mq-syntax
785
+ // The condition sub-grammar (QueryFeature / QueryInParens / QueryCondition) is
786
+ // inherited from CSS verbatim. `queryPrelude` is overridden locally (below) so a
787
+ // comma-separated list may carry bare <media-type> items alongside conditions.
788
+ // Only this block wrapper differs (Less commits its opening brace via `expect`),
789
+ // so it stays here and reads `g.queryPrelude`.
790
+ //
791
+ // ── Media-query list (CSS Media Queries L4 <media-query-list>) ────────────────
792
+ // The inherited CSS `queryPrelude` models a @supports/@container-flavoured list
793
+ // whose every comma item is a parenthesised / `not`-led <media-condition>. It
794
+ // rejects a bare <media-type> (`all`, `print`, `screen`) as a list item, so a
795
+ // prelude whose FIRST item parses structurally but whose tail is a bare type —
796
+ // `@media ((color) and (hover)), all`, `@media (min-width: 100px), print` —
797
+ // hard-errors at the committed `{`: the first item is consumed, the `, all` tail
798
+ // is not, and the structured rule has already passed the point where it could
799
+ // backtrack to the (bracket-swallowing) generic AtRuleBlock. Per the L4 grammar a
800
+ // <media-query> list item is EITHER a <media-condition> OR
801
+ // [ not | only ]? <media-type> [ and <media-condition-without-or> ]?
802
+ // so each comma-list position also admits the media-type form. The emitted AST is
803
+ // unaffected: the Less QueryAtRuleBlock builder reconstructs the prelude from
804
+ // SOURCE TEXT (identical to the generic AtRuleBlock path — both converge on
805
+ // `_buildAtRulePrelude`), so the grammar only has to CONSUME a well-formed prelude
806
+ // and reach the commit point; no stray/unbalanced bracket is ever swallowed (the
807
+ // media-type form's `and` sub-conditions are balanced `QueryInParens`).
808
+ // @see https://www.w3.org/TR/mediaqueries-5/#media-query-list
809
+ // A <media-type> is an <ident> other than the query keywords (`not only and or`,
810
+ // plus `layer` — reserved). Mirrors the CSS `containerName` exclusion set.
811
+ const mediaType = regex(/(?!(?:not|only|and|or|layer)(?![-\w]))-?[_a-zA-Z€-￿][-_a-zA-Z0-9€-￿]*/i);
812
+ const containerName = mediaType;
813
+ // `[ not | only ]? <media-type> [ and <media-in-parens> ]*`.
814
+ const mediaTypeQuery = sequence(
815
+ optional(regex(/(?:not|only)(?![-\w])/i)),
816
+ mediaType,
817
+ many(sequence(regex(/and(?![-\w])/i), g.QueryInParens)));
818
+ const mediaQueryItem = choice(g.QueryCondition, mediaTypeQuery);
819
+ const queryPrelude = sequence(
820
+ optional(containerName), g.QueryCondition, many(sequence(literal(','), mediaQueryItem)));
821
+ const queryAtKeyword = regex(/@(?:media|container|supports)(?![-\w])/i);
822
+ const QueryAtRuleBlock = node(
823
+ sequence(queryAtKeyword, g.queryPrelude, expect(literal('{'), '{'), g.atRuleBody, expect(literal('}'), '}')));
824
+
825
+ const AtRuleBlock = node(
826
+ sequence(atKeyword, atPrelude, literal('{'), g.atRuleBody, expect(literal('}'), '}')));
827
+
828
+ // ── Structured, committed import statement (@import / @-import / @-export) ────
829
+ // The flat `atPrelude` also swallows a bare ident before the path, so
830
+ // `@import malformed "x.less";` is accepted with 0 errors. This rule requires,
831
+ // right after the keyword and an optional `(options)` paren, a quoted string or
832
+ // `url(...)` as the path — committed via `expect`. For `@import malformed "…"`,
833
+ // the token after the keyword is the bare ident `malformed` (neither `(` nor
834
+ // Quoted/Url), so the committed `expect(choice(Quoted, Url))` fails → 1 error.
835
+ // Ordered before the generic AtRuleStatement; the existing
836
+ // `_buildImportAtRuleFromPrelude` builder reconstructs the AST from source.
837
+ const importKeyword = regex(/@(?:-import|-export|import)(?![-\w])/i);
838
+ const importOptionsParen = sequence(literal('('), scanTo(literal(')'), { skip: [bParen, bSquare, bCurly, singleStr, doubleStr] }), literal(')'));
839
+ const importMedia = scanTo(literal(';'), { skip: [bParen, bSquare, bCurly, singleStr, doubleStr] });
840
+ const ImportAtRuleStatement = node('AtRuleStatement',
841
+ sequence(
842
+ importKeyword, optional(importOptionsParen),
843
+ expect(choice(g.Url, g.Quoted), 'import path'),
844
+ optional(importMedia), expect(literal(';'))
845
+ ));
846
+
847
+ const AtRuleStatement = node(
848
+ sequence(atKeyword, atPrelude, literal(';')));
849
+ // An at-rule body (@media / @supports / @starting-style / …) holds the SAME
850
+ // statements as a ruleset body — nested rules, mixin calls, each(), extends,
851
+ // var calls — not just declarations. Mirror declarationList's choice set.
852
+ // Same statement set as a ruleset body (shares `blockItem`).
853
+ const atRuleBody = many(g.blockItem);
854
+
855
+ return {
856
+ rw,
857
+ stylesheetItem, blockItem,
858
+ Stylesheet, VarDeclaration, VarCall, Reference, MixinArgs, mixinNamePath, mixinCallBasicSel, mixinCallPath, MixinCall,
859
+ AnonymousMixinDefinition, MixinOrQualifiedRule, Comparison, GuardDefault, GuardInParens, GuardTerm, GuardAnd, GuardOr, Guard,
860
+ CondArgParen, CondArgTerm, CondArgAnd, CondArgOr, CondArgTermOp, CondArgAndOp, ArgCondition,
861
+ LessAmpersand, InterpolatedSelector, ExtendStatement, ExtendPseudo, ExtendTarget, extendCompound, extendComplex, simpleSelector,
862
+ CompoundSelector, ComplexSelector, SelectorList, AttributeSelector, PseudoSelector, pseudoArg, pseudoSelectorParens,
863
+ Ruleset, declarationList, Declaration, customValue, customCurlyBlock, cpInner, cpParen, cpSquare, cpCurly, cpValue, CustomDeclaration, declaration,
864
+ valueList, valueSequence, value, UnicodeRange, Negative, mathProduct, mathSum, topProduct, topSum, parenExprList, InterpValue, NsAccessor, EscapedValue, NamedColor, Dimension, Url,
865
+ parenBody, permissiveParenBody, Paren, GluedParen, DetachedRuleset, functionCallArgs, squareParenBody, calcBody, Call, FormatCall, SquareParen, anyValue, EachFor,
866
+ queryPrelude, QueryAtRuleBlock, ImportAtRuleStatement,
867
+ AtRuleBlock, AtRuleStatement, atRuleBody
868
+ };
869
+ })]);