@jesscss/less-parser 2.0.0-alpha.6 → 2.0.0-alpha.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +133 -5
- package/lib/builders.d.ts +381 -0
- package/lib/builders.d.ts.map +1 -0
- package/lib/cst.cjs +14 -0
- package/lib/cst.d.ts +6 -0
- package/lib/cst.d.ts.map +1 -0
- package/lib/cst.js +12 -0
- package/lib/functional-parser.cjs +2653 -0
- package/lib/functional-parser.d.ts +18 -0
- package/lib/functional-parser.d.ts.map +1 -0
- package/lib/functional-parser.js +2588 -0
- package/lib/grammar.cjs +30621 -0
- package/lib/grammar.d.ts +2 -0
- package/lib/grammar.d.ts.map +1 -0
- package/lib/grammar.js +30620 -0
- package/lib/index.cjs +17 -4096
- package/lib/index.d.ts +9 -149
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +6 -4091
- package/lib/jess.cjs +3968 -0
- package/lib/jess.d.ts +7 -0
- package/lib/jess.d.ts.map +1 -0
- package/lib/jess.js +3962 -0
- package/lib/lessParser.d.ts +38 -0
- package/lib/lessParser.d.ts.map +1 -0
- package/lib/lessRecursiveParser.d.ts +99 -0
- package/lib/lessRecursiveParser.d.ts.map +1 -0
- package/lib/lessTokens.d.ts +23 -0
- package/lib/lessTokens.d.ts.map +1 -0
- package/lib/productions/guards.d.ts +113 -0
- package/lib/productions/guards.d.ts.map +1 -0
- package/lib/productions/index.d.ts +5 -0
- package/lib/productions/index.d.ts.map +1 -0
- package/lib/productions/root.d.ts +38 -0
- package/lib/productions/root.d.ts.map +1 -0
- package/lib/productions/selectors.d.ts +41 -0
- package/lib/productions/selectors.d.ts.map +1 -0
- package/lib/productions/values.d.ts +35 -0
- package/lib/productions/values.d.ts.map +1 -0
- package/lib/utils.d.ts +9 -0
- package/lib/utils.d.ts.map +1 -0
- package/package.json +41 -10
- package/src/__tests__/debug-log.ts +35 -0
- package/src/__tests__/wall5-parse.test.ts +67 -0
- package/src/builders.ts +3183 -0
- package/src/cst.ts +25 -0
- package/src/functional-parser.ts +162 -0
- package/src/grammar.ts +869 -0
- package/src/index.ts +19 -0
- package/src/jess.ts +6 -0
- package/src/lessParser.ts +120 -0
- package/src/lessRecursiveParser.ts +279 -0
- package/src/lessTokens.ts +350 -0
- package/src/productions/guards.ts +1066 -0
- package/src/productions/index.ts +29 -0
- package/src/productions/root.ts +1613 -0
- package/src/productions/selectors.ts +1309 -0
- package/src/productions/values.ts +1449 -0
- package/src/utils.ts +178 -0
- package/lib/index.d.cts +0 -150
- package/lib/index.d.cts.map +0 -1
- package/lib/index.js.map +0 -1
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
|
+
})]);
|