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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/grammar.ts DELETED
@@ -1,651 +0,0 @@
1
- /**
2
- * Functional SCSS grammar — the macro-compiled counterpart to the class-based
3
- * ScssGrammar. This file is JUST the grammar: `scssGrammar = compose([lessGrammar,
4
- * <SCSS delta>])`. Most returned rules are structural `node(parser)` entries that build via
5
- * the injected `ctx.build` host. The host + parse entry (`parseScssFn`,
6
- * `ScssParser`) 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, optional, trivia,
12
- many, expect, sepBy, oneOrMore, scanTo, balanced, label, not
13
- } from 'parseman' with { type: 'macro' };
14
- import { lessGrammar } from '@jesscss/less-parser/grammar';
15
-
16
- // ---------------------------------------------------------------------------
17
- // Grammar — SCSS = Less + the SCSS delta. `compose` fuses the imported compiled
18
- // `lessGrammar` (pieces travel on the value — no source) with the inline SCSS
19
- // delta; the delta's rules win by name (its `Stylesheet` etc. override Less's),
20
- // and its references to Less/CSS rules 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 Less/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 scssGrammar = compose([lessGrammar, rules({ trivia: rw }, (g: any) => {
35
- // SCSS `$variable` token — first char may be a letter or `-` after `$`.
36
- const scssVar = regex(/\$-?[_a-zA-Z\u0080-\uffff][-_a-zA-Z0-9\u0080-\uffff]*/);
37
- const plainIdent = regex(/-?[_a-zA-Z\u0080-\uffff][-_a-zA-Z0-9\u0080-\uffff]*/);
38
-
39
- const VarDeclaration = node(
40
- sequence(
41
- scssVar,
42
- literal(':'),
43
- g.valueList,
44
- optional(choice(literal('!default'), literal('!global'))),
45
- optional(literal(';'))
46
- ));
47
-
48
- // SCSS references are bare `$var` (no Less accessor-chain syntax) — a single
49
- // token, so no trivia handling is needed.
50
- const Reference = node(scssVar);
51
-
52
- // Namespaced variable ASSIGNMENT — `ns.$var: value [!default|!global];`. Writes
53
- // into another module's variable (distinct from the member READ `ns.get-x()`).
54
- // The `plainIdent '.' scssVar` head can't be confused with a Declaration
55
- // (`scssDeclPropName` stops before `.`) or a class-selector ruleset (`.` there
56
- // is followed by an ident, not `$`), so this is safe at statement head.
57
- const NsVarDeclaration = node(
58
- sequence(
59
- plainIdent,
60
- literal('.'),
61
- scssVar,
62
- literal(':'),
63
- g.valueList,
64
- optional(choice(literal('!default'), literal('!global'))),
65
- optional(literal(';'))
66
- ));
67
-
68
- // ── Interpolation (#{…}) ───────────────────────────────────────────────────
69
- // SCSS uses `#{expr}` (not Less `@{var}`). Override the Less interpolation
70
- // hooks: bare `#{…}` values, interpolated idents in names/selectors/strings.
71
- const scssInterpKey = 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])*/);
72
- const scssCustomPropInterp = regex(/--(?:[-_a-zA-Z0-9\u0080-\uffff]|#\{[^}]*\})+/);
73
- const customProp = regex(/--[-_a-zA-Z0-9\u0080-\uffff]*/);
74
- const scssDeclPropName = regex(/\*?-?(?:[_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])|#\{[^}]*\})*/);
75
- const important = sequence(literal('!'), literal('important'));
76
-
77
- const ScssInterpBare = node(
78
- sequence(literal('#'), literal('{'), g.valueSequence, expect(literal('}'), '}')));
79
-
80
- const InterpValue = node(
81
- scssInterpKey);
82
-
83
- // ── Sass map literals + module-qualified idents ────────────────────────────
84
- const dotName = regex(/\.-?[_a-zA-Z\u0080-\uffff][-_a-zA-Z0-9\u0080-\uffff]*/);
85
- const ScssMapPair = node('ScssMapPair',
86
- sequence(g.value, literal(':'), g.valueSequence));
87
- // A Sass map literal REQUIRES at least one `key: value` pair. `expect(')')`
88
- // recovers in place (zero-width success), so if this rule matched an empty or
89
- // pairless `(…)` it would swallow every parenthesized value before the value
90
- // paren rule is tried. Requiring a real pair (the `:` is a soft `literal`) lets a
91
- // non-map paren like `(15px/30px)` or `(1 + 2)` fail here and fall through.
92
- const ScssMapLiteral = node(
93
- sequence(
94
- literal('('),
95
- ScssMapPair,
96
- many(sequence(literal(','), ScssMapPair)),
97
- optional(literal(',')),
98
- expect(literal(')'))
99
- ));
100
- const scssHashName = regex(/#-?[_a-zA-Z\u0080-\uffff][-_a-zA-Z0-9\u0080-\uffff]*/);
101
- const ScssIdentValue = node(
102
- sequence(
103
- plainIdent,
104
- optional(choice(
105
- sequence(
106
- literal('.'), literal('\\'), choice(scssHashName, dotName),
107
- literal('('), optional(g.ScssCallArgsInner), expect(literal(')'))
108
- ),
109
- sequence(literal('.'), scssVar),
110
- sequence(dotName, literal('('), optional(g.ScssCallArgsInner), expect(literal(')')))
111
- ))
112
- ));
113
-
114
- // Value-position paren. Unlike Less's strict single-expression `Paren`, SCSS
115
- // allows space/comma-separated value lists inside parens (e.g.
116
- // `(bold 15px/30px sans-serif)`). We parse permissively and let `_buildScssParen`
117
- // decide: an isolated arithmetic form (`(15px/30px)`, `(1 + 2)`) becomes an
118
- // `Expression(Operation)`; anything else stays a grouped `Paren`.
119
- const ScssValueParen = node('Paren',
120
- sequence(literal('('), g.permissiveParenBody));
121
-
122
- // Sass allows trailing commas in comma-separated lists (as does Less v5, matching Less 4.x).
123
- const valueList = sequence(
124
- g.valueSequence,
125
- many(sequence(literal(','), g.valueSequence)),
126
- optional(literal(','))
127
- );
128
- const callArgSeq = choice(g.AnonymousMixinDefinition, g.DetachedRuleset, g.valueSequence);
129
- const callArgList = choice(g.AnonymousMixinDefinition, g.DetachedRuleset, valueList);
130
- const functionCallArgs = sequence(
131
- optional(sequence(
132
- callArgSeq,
133
- many(sequence(literal(','), callArgSeq)),
134
- optional(literal(',')),
135
- many(sequence(literal(';'), optional(callArgList)))
136
- )),
137
- literal(')')
138
- );
139
- const fnIdent = regex(/-?(?:[_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]))*/);
140
- const Call = node(
141
- sequence(fnIdent, literal('('), functionCallArgs));
142
-
143
- const value = choice(
144
- ScssInterpBare, InterpValue, g.Reference, g.Dimension, g.Num, g.Color, g.NamedColor,
145
- g.Url, g.CalcCall, g.Call, ScssIdentValue, g.EscapedValue, g.GluedParen, ScssMapLiteral,
146
- ScssValueParen, g.SquareParen, g.Quoted, g.anyValue
147
- );
148
-
149
- const staticSeg = regex(/[-_a-zA-Z0-9]+/);
150
- const nameSegment = choice(staticSeg, ScssInterpBare);
151
- const ScssInterpolatedName = node(
152
- oneOrMore(nameSegment));
153
-
154
- const InterpolatedSelector = node(
155
- sequence(
156
- optional(regex(/[.#]/)),
157
- oneOrMore(nameSegment)
158
- ));
159
-
160
- const CustomDeclaration = node(
161
- sequence(
162
- choice(scssCustomPropInterp, customProp),
163
- literal(':'),
164
- choice(g.customCurlyBlock, g.customValue, g.cpValue),
165
- optional(literal(';'))
166
- ));
167
-
168
- // A nested prop (`size: 1rem`) is built AS a `Declaration` (structural node →
169
- // ctx.build('Declaration')); `_buildScssNestedProps` filters children for
170
- // Declaration nodes. The rule's own name stays local (`many(ScssNestedDecl)`).
171
- const ScssNestedDecl = node('Declaration',
172
- sequence(
173
- scssDeclPropName,
174
- literal(':'),
175
- g.valueList,
176
- optional(literal(';'))
177
- ));
178
-
179
- // A nested-properties block (`font: { … }`) normally holds inner
180
- // sub-declarations, but Sass also allows control flow (`@for`, `@if`, …) and
181
- // namespaced variable ASSIGNMENTS inside it. Try those before the plain
182
- // sub-declaration.
183
- const ScssNestedProps = node(
184
- sequence(
185
- literal('{'),
186
- many(choice(
187
- g.ScssIf, g.ScssEach, g.ScssFor, g.ScssWhile,
188
- g.NsVarDeclaration, g.VarDeclaration,
189
- ScssNestedDecl
190
- )),
191
- expect(literal('}'))
192
- ));
193
-
194
- const Declaration = node(
195
- sequence(
196
- scssDeclPropName,
197
- optional(choice(literal('+_'), literal('+'))),
198
- literal(':'),
199
- choice(
200
- ScssNestedProps,
201
- sequence(
202
- optional(g.valueList),
203
- optional(ScssNestedProps)
204
- )
205
- ),
206
- optional(important),
207
- optional(literal(';'))
208
- ));
209
-
210
- // ── Control flow: @if / @else if / @else ───────────────────────────────────
211
- // Faithful port of the Chevrotain scssCondition* / scssIfAtRule productions
212
- // (productions/conditions.ts, productions/atRules.ts). The condition sub-
213
- // grammar is structurally the Less guard grammar (or → and → term → parens /
214
- // comparison), so the SCSS builders mirror LessGrammar's guard builders.
215
- const scssCompareOp = regex(/==|!=|>=|<=|=|>|</);
216
- const kwOr = regex(/or(?![-\w])/i);
217
- const kwAnd = regex(/and(?![-\w])/i);
218
- const kwNot = regex(/not(?![-\w])/i);
219
-
220
- // A single comparison operand. Specific value rules first; `anyValue` last —
221
- // it stops at whitespace so it cannot swallow a spaced ` == ` / `and` / `{`.
222
- const condOperand = choice(g.Reference, g.Dimension, g.Num, g.Color, g.NamedColor, g.Quoted, g.Call, g.Paren, g.anyValue);
223
- const ScssComparison = node(
224
- sequence(condOperand, optional(sequence(scssCompareOp, condOperand))));
225
- // `(` condOr `)` (Paren-wrapped) OR a bare comparison.
226
- const ScssCondInParens = node(
227
- choice(
228
- sequence(literal('('), g.ScssCondOr, literal(')')),
229
- g.ScssComparison
230
- ));
231
- // A term: optional `not`, then a paren-group or a comparison.
232
- const ScssCondTerm = node(
233
- sequence(optional(kwNot), g.ScssCondInParens));
234
- // 'and' chain (left-associative).
235
- const ScssCondAnd = node(
236
- sequence(g.ScssCondTerm, many(sequence(kwAnd, g.ScssCondTerm))));
237
- // 'or' / ',' chain (left-associative). `,` is allowed in @if (legacy syntax).
238
- const ScssCondOr = node(
239
- sequence(g.ScssCondAnd, many(sequence(choice(kwOr, literal(',')), g.ScssCondAnd))));
240
-
241
- // A `{ … }` block body → Rules (statements come from atRuleBody).
242
- const ScssRules = node(
243
- sequence(literal('{'), g.atRuleBody, expect(literal('}'), '}')));
244
-
245
- const ifKw = regex(/@if(?![-\w])/i);
246
- const elseKw = regex(/@else(?![-\w])/i);
247
- const ifWord = regex(/if(?![-\w])/i);
248
- // A REQUIRED condition. `@if { … }` (no condition) is a real error. `not('{')`
249
- // asserts we are not sitting directly on the block opener; `expect` reports the
250
- // missing condition and RECOVERS IN PLACE (zero-width) so the `{ … }` block still
251
- // parses (as an `@if` with a recovered error) rather than the whole rule failing
252
- // and `@if` falling through to the opaque unknown-at-rule handler. Structural,
253
- // context-free — no `withCtx`/`guard`, so the grammar stays macro-compiled.
254
- const reqIfCond = expect(sequence(not(literal('{')), g.ScssCondOr), 'condition');
255
- const ScssIf = node(
256
- sequence(
257
- ifKw, reqIfCond, g.ScssRules,
258
- many(sequence(elseKw, choice(
259
- sequence(ifWord, g.ScssCondOr, g.ScssRules),
260
- g.ScssRules
261
- )))
262
- ));
263
-
264
- // ── Control flow: @each / @for / @while ────────────────────────────────────
265
- // Faithful ports of scssEachAtRule / scssForAtRule / scssWhileAtRule
266
- // (productions/atRules.ts). All normalize to Jess `For` / `While` nodes.
267
- const inKw = regex(/\bin\b/);
268
- const fromKw = regex(/\bfrom\b/);
269
- const forThrough = regex(/\bthrough\b/);
270
- const forTo = regex(/\bto\b/);
271
- const eachKw = regex(/@each(?![-\w])/i);
272
- const forKw = regex(/@for(?![-\w])/i);
273
- const whileKw = regex(/@while(?![-\w])/i);
274
-
275
- // A REQUIRED loop variable. `@each in $list { … }` (no variable) is a real error.
276
- // `not(inKw | '{')` asserts a variable is actually present (missing → we are at
277
- // `in` or the block); `expect` reports it and recovers zero-width so `in $list
278
- // { … }` still parses as an `@each` with a recovered error.
279
- const reqEachVars = expect(
280
- sequence(not(choice(inKw, literal('{'))), sepBy(scssVar, literal(','))), 'variable');
281
- const ScssEach = node(
282
- sequence(
283
- eachKw,
284
- reqEachVars,
285
- inKw,
286
- g.valueSequence,
287
- g.ScssRules
288
- ));
289
-
290
- // A REQUIRED `from … through/to …` range. `@for $i { … }` (no range) is a real
291
- // error. The whole range tail is wrapped in one `expect`: on failure it recovers
292
- // zero-width (as a unit — `fromKw` is a hard token that fails at `{` without
293
- // consuming) so the trailing `{ … }` block still parses as a `@for` with a
294
- // recovered error rather than the rule failing and falling through.
295
- const forRangeTail = sequence(fromKw, g.topSum, choice(forThrough, forTo), g.topSum);
296
- const ScssFor = node(
297
- sequence(
298
- forKw,
299
- expect(scssVar, 'variable'),
300
- expect(forRangeTail, '"from"'),
301
- g.ScssRules
302
- ));
303
-
304
- const ScssWhile = node(
305
- sequence(whileKw, g.ScssCondOr, g.ScssRules));
306
-
307
- // ── Mixins: @mixin / @include / @content ───────────────────────────────────
308
- // Faithful ports of scssMixinAtRule / scssIncludeAtRule / scssContentAtRule.
309
- const mixinKw = regex(/@mixin(?![-\w])/i);
310
- const includeKw = regex(/@include(?![-\w])/i);
311
- const contentKw = regex(/@content(?![-\w])/i);
312
- const usingKw = regex(/\busing\b/);
313
-
314
- // SCSS call/mixin argument: `$x: val`, `val...`, or a plain value.
315
- const ScssCallArg = node(
316
- choice(
317
- sequence(scssVar, literal(':'), g.valueSequence),
318
- sequence(g.value, literal('...')),
319
- sequence(g.valueSequence, literal('...')),
320
- g.valueSequence
321
- ));
322
- const ScssCallArgsInner = node(
323
- optional(sequence(
324
- g.ScssCallArg,
325
- many(sequence(literal(','), optional(g.ScssCallArg)))
326
- )));
327
- const optionalCallParens = optional(sequence(
328
- literal('('), g.ScssCallArgsInner, expect(literal(')'))
329
- ));
330
-
331
- // Mixin parameter: `...$rest`, `$rest...`, `$a: default`, or bare `$a`.
332
- const ScssMixinParam = node(
333
- choice(
334
- sequence(literal('...'), scssVar),
335
- sequence(scssVar, literal('...')),
336
- sequence(scssVar, optional(sequence(literal(':'), g.valueSequence)))
337
- ));
338
- const ScssMixinParams = node(
339
- sequence(
340
- literal('('),
341
- optional(sequence(
342
- g.ScssMixinParam,
343
- many(sequence(literal(','), optional(g.ScssMixinParam)))
344
- )),
345
- expect(literal(')'))
346
- ));
347
-
348
- // Mixin/include name: `foo`, module-qualified `ns.foo`, or `foo-#{$bar}`.
349
- const scssMixinIdent = choice(ScssInterpolatedName, plainIdent);
350
- const ScssMixinName = node(
351
- choice(
352
- sequence(plainIdent, literal('.'), plainIdent),
353
- ScssInterpolatedName,
354
- plainIdent
355
- ));
356
-
357
- const ScssDeclBody = node(
358
- sequence(literal('{'), g.declarationList, expect(literal('}'), '}')));
359
-
360
- // A REQUIRED mixin name. `@mixin { … }` (no name) is a real error. `expect` reports
361
- // the missing name and recovers zero-width so the `{ … }` body still parses (as a
362
- // `@mixin` with a recovered error) rather than the rule falling through.
363
- const ScssMixin = node(
364
- sequence(
365
- mixinKw, expect(scssMixinIdent, 'name'), optional(g.ScssMixinParams), g.ScssDeclBody
366
- ));
367
-
368
- const ScssIncludeUsing = node(
369
- sequence(
370
- usingKw, literal('('), sepBy(scssVar, literal(',')), expect(literal(')'))
371
- ));
372
-
373
- // A REQUIRED mixin name. `@include ;` and `.a { @include }` (no name) are real
374
- // errors. `expect` reports the missing name and recovers zero-width so the trailing
375
- // `;`/`}` still closes the statement rather than the rule falling through.
376
- const ScssInclude = node(
377
- sequence(
378
- includeKw, expect(g.ScssMixinName, 'name'), optionalCallParens,
379
- optional(g.ScssIncludeUsing), optional(g.ScssRules), optional(literal(';'))
380
- ));
381
-
382
- const ScssContent = node(
383
- sequence(contentKw, optionalCallParens, optional(literal(';'))));
384
-
385
- // ── @function / @return ─────────────────────────────────────────────────────
386
- const functionKw = regex(/@function(?![-\w])/i);
387
- const returnKw = regex(/@return(?![-\w])/i);
388
-
389
- const ScssFunction = node(
390
- sequence(
391
- functionKw, scssMixinIdent, optional(g.ScssMixinParams), g.ScssDeclBody
392
- ));
393
-
394
- // A REQUIRED return value. `@return }` / `@return ;` (no expression) is a real
395
- // error. `not('}' | ';')` asserts a value is actually present (valueList can match
396
- // zero-width); `expect` reports it and recovers zero-width so the enclosing block's
397
- // `}` still closes.
398
- const ScssReturn = node(
399
- sequence(
400
- returnKw,
401
- expect(sequence(not(choice(literal('}'), literal(';'))), g.valueList), 'expression'),
402
- optional(literal(';'))
403
- ));
404
-
405
- // ── @use / @forward / @import / @extend ───────────────────────────────────
406
- // Faithful ports of scssUseAtRule / scssForwardAtRule / importAtRule /
407
- // scssExtendAtRule (productions/atRules.ts).
408
- const singleStr = regex(/'(?:[^'\\]|\\[\s\S])*'/);
409
- const doubleStr = regex(/"(?:[^"\\]|\\[\s\S])*"/);
410
- const strHole = [singleStr, doubleStr];
411
- const bParen = balanced('(', ')', { skip: strHole });
412
- const bSquare = balanced('[', ']', { skip: strHole });
413
- const bCurly = balanced('{', '}', { skip: strHole });
414
- const scanSkip = [bParen, bSquare, bCurly, singleStr, doubleStr];
415
-
416
- const kwAs = regex(/\bas\b/);
417
- const kwWith = regex(/\bwith\b/);
418
- const useKw = regex(/@use(?![-\w])/i);
419
- const forwardKw = regex(/@forward(?![-\w])/i);
420
- const extendKw = regex(/@extend(?![-\w])/i);
421
- const extendOptional = regex(/!optional\b/);
422
- const importKw = regex(/@import(?![-\w])/i);
423
-
424
- const ScssWithConfigEntry = node(
425
- sequence(
426
- scssVar, literal(':'), g.valueSequence,
427
- optional(choice(literal('!default'), literal('!global')))
428
- ));
429
- const ScssWithConfig = node(
430
- sequence(
431
- literal('('),
432
- optional(sequence(
433
- sepBy(ScssWithConfigEntry, literal(',')),
434
- optional(literal(','))
435
- )),
436
- expect(literal(')'))
437
- ));
438
-
439
- const ScssUseAs = node(
440
- sequence(kwAs, choice(literal('*'), plainIdent)));
441
-
442
- const ScssUse = node(
443
- sequence(
444
- useKw, g.Quoted,
445
- optional(ScssUseAs),
446
- optional(sequence(kwWith, ScssWithConfig)),
447
- optional(literal(';'))
448
- ));
449
-
450
- // Capture the post-path prelude (`as *`, `show …`, `hide …`, `as prefix-*`) up
451
- // to `with (`, `;`, `}`, or EOF — so an unterminated `@forward "x" as a-*` (the
452
- // owner-rejected prefix form, which the sass-spec corpus writes without a `;`,
453
- // sometimes with comments/newlines around `as`) still reaches the builder's
454
- // "will never be" check rather than dangling as unparsed input. `{` bounds the
455
- // scan so a following ruleset is never swallowed.
456
- const forwardExtra = optional(scanTo(
457
- choice(sequence(kwWith, literal('(')), literal(';'), literal('{'), literal('}')),
458
- { skip: scanSkip, orEOF: true }
459
- ));
460
- const ScssForward = node(
461
- sequence(
462
- forwardKw, g.Quoted,
463
- forwardExtra,
464
- optional(sequence(kwWith, ScssWithConfig)),
465
- optional(literal(';'))
466
- ));
467
-
468
- const scssPlaceholder = regex(/%-?[_a-zA-Z\u0080-\uffff][-_a-zA-Z0-9\u0080-\uffff]*/);
469
- const ScssPlaceholderSelector = node(
470
- scssPlaceholder);
471
- const scssExtendComplex = choice(ScssPlaceholderSelector, g.ComplexSelector);
472
- const ScssExtendTarget = node(
473
- sequence(
474
- scssExtendComplex,
475
- many(sequence(literal(','), scssExtendComplex))
476
- ));
477
- const ScssExtend = node(
478
- sequence(
479
- extendKw, ScssExtendTarget,
480
- optional(extendOptional),
481
- optional(literal(';'))
482
- ));
483
-
484
- const importOptionsParen = sequence(
485
- literal('('),
486
- scanTo(literal(')'), { skip: scanSkip }),
487
- literal(')')
488
- );
489
- // An `@import` prelude is a comma-separated list of items, each `<path>
490
- // <modifiers>?`. The modifiers are a CSS media-query list / `supports(...)`
491
- // that MAY itself contain commas (`@import "a" b, (c: d), e;` is ONE import
492
- // with a three-part media list). So a comma only begins a NEW import when the
493
- // token after it is another path (a string / url) — `not(not(...))` is the
494
- // positive lookahead. The modifier scan skips balanced groups, strings, and
495
- // comments, and terminates at `;`, `}`, EOF, or such a new-import comma.
496
- const importPathStart = choice(g.Url, g.Quoted);
497
- // `#{ … }` interpolation hole (handles `#{$a}` and `#{"(a: b)"}` with a string
498
- // that may itself carry braces). Kept ahead of the generic brace skip so the
499
- // leading `#` is consumed together with the group.
500
- const scssInterpHole = regex(/#\{(?:[^{}'"]|'(?:[^'\\]|\\.)*'|"(?:[^"\\]|\\.)*")*\}/);
501
- const importSkip = [scssInterpHole, bParen, bSquare, bCurly, singleStr, doubleStr, comment, lineComment];
502
- const newImportComma = sequence(literal(','), not(not(importPathStart)));
503
- const importModifier = scanTo(
504
- choice(literal(';'), literal('}'), newImportComma),
505
- { skip: importSkip, orEOF: true }
506
- );
507
- const ScssImportItem = node(
508
- sequence(
509
- expect(importPathStart, 'import path'),
510
- optional(importModifier)
511
- ));
512
- const ImportAtRuleStatement = node('ScssImportAtRule',
513
- sequence(
514
- importKw,
515
- optional(importOptionsParen),
516
- ScssImportItem,
517
- many(sequence(literal(','), ScssImportItem)),
518
- optional(literal(';'))
519
- ));
520
-
521
- const atRootKw = regex(/@at-root(?![-\w])/i);
522
- const debugKw = regex(/@debug(?![-\w])/i);
523
- const warnKw = regex(/@warn(?![-\w])/i);
524
- const errorKw = regex(/@error(?![-\w])/i);
525
-
526
- const ScssDiagnostic = node(
527
- sequence(
528
- choice(debugKw, warnKw, errorKw),
529
- g.valueSequence,
530
- optional(literal(';'))
531
- ));
532
-
533
- const ScssAtRootFilter = node(
534
- sequence(
535
- atRootKw,
536
- literal('('),
537
- g.valueSequence,
538
- literal(')'),
539
- ScssRules
540
- ));
541
-
542
- const ScssAtRootSelector = node(
543
- sequence(
544
- atRootKw,
545
- g.SelectorList,
546
- ScssDeclBody
547
- ));
548
-
549
- const ScssAtRootPlain = node(
550
- sequence(atRootKw, ScssRules));
551
-
552
- // ── SCSS at-rule prelude interpolation (segments) ────────────────────────
553
- const scssPreludeText = regex(/(?:[^{#]|#(?!\{))+/);
554
- const scssPreludeSegment = choice(ScssInterpBare, scssPreludeText);
555
- const scssPermissivePrelude = oneOrMore(scssPreludeSegment);
556
-
557
- // Generic unknown at-rule statement (`@charset "x";`, or a bare `@c` used as a
558
- // content placeholder in the sass-spec corpus). Overrides Less's
559
- // `AtRuleStatement`, whose `;` is mandatory and whose prelude scan stops only
560
- // at `{`/`;`: Sass allows omitting the terminator before `}`/EOF, so the
561
- // prelude also stops at `}`/EOF and the trailing `;` is optional.
562
- const scssAtKeyword = regex(/@-?[_a-zA-Z€-￿][-_a-zA-Z0-9€-￿]*/);
563
- const scssAtPrelude = optional(scanTo(
564
- choice(literal('{'), literal(';'), literal('}')),
565
- { skip: scanSkip, orEOF: true }
566
- ));
567
- const AtRuleStatement = node('AtRuleStatement',
568
- sequence(scssAtKeyword, scssAtPrelude, optional(literal(';'))));
569
-
570
- // ── Statement injection ─────────────────────────────────────────────────
571
- // Override Less's containers to try the SCSS control statements first, then
572
- // fall back to Less's full statement set (`g.stylesheetItem` / `g.blockItem`).
573
- const scssStatement = choice(
574
- g.ScssIf, g.ScssEach, g.ScssFor, g.ScssWhile,
575
- g.ScssMixin, g.ScssInclude, g.ScssContent,
576
- g.ScssFunction, g.ScssReturn,
577
- g.ScssUse, g.ScssForward,
578
- // Tried ahead of Less's `blockItem`, so an `@import` whose modifier carries
579
- // `#{ … }` interpolation is handled by the SCSS import rule rather than being
580
- // misread by Less's generic `AtRuleBlock` (which would treat the `{` in `#{`
581
- // as a block opener).
582
- g.ImportAtRuleStatement,
583
- g.NsVarDeclaration,
584
- ScssDiagnostic,
585
- ScssAtRootFilter, ScssAtRootSelector, ScssAtRootPlain
586
- );
587
- const declarationList = many(choice(
588
- scssStatement, g.ScssExtend, g.ScssPlaceholderRuleset, Declaration, CustomDeclaration, g.blockItem
589
- ));
590
- const atRuleBody = many(choice(scssStatement, g.ScssPlaceholderRuleset, g.blockItem));
591
-
592
- const ScssPlaceholderRuleset = node(
593
- sequence(
594
- ScssPlaceholderSelector,
595
- optional(g.Guard),
596
- literal('{'),
597
- declarationList,
598
- expect(literal('}'))
599
- ));
600
- const queryAtKeyword = regex(/@(?:media|container|supports)(?![-\w])/i);
601
- const QueryAtRuleBlock = node(
602
- sequence(
603
- queryAtKeyword,
604
- scssPermissivePrelude,
605
- expect(literal('{')),
606
- atRuleBody,
607
- expect(literal('}'))
608
- ));
609
- const scopeKw = regex(/@scope(?![-\w])/i);
610
- const ScssScopeBlock = node(
611
- sequence(
612
- scopeKw,
613
- scssPermissivePrelude,
614
- literal('{'),
615
- atRuleBody,
616
- expect(literal('}'))
617
- ));
618
- const layerKw = regex(/@layer(?![-\w])/i);
619
- const ScssLayerBlock = node(
620
- sequence(
621
- layerKw,
622
- optional(ScssInterpolatedName),
623
- literal('{'),
624
- atRuleBody,
625
- expect(literal('}'))
626
- ));
627
- const Stylesheet = node(
628
- many(choice(
629
- scssStatement, ScssPlaceholderRuleset, ScssScopeBlock, ScssLayerBlock, g.stylesheetItem
630
- )));
631
-
632
- return {
633
- VarDeclaration, Reference, NsVarDeclaration, AtRuleStatement,
634
- ScssInterpBare, InterpValue, value, valueList, functionCallArgs, Call,
635
- ScssMapLiteral, ScssIdentValue,
636
- ScssInterpolatedName, InterpolatedSelector,
637
- Declaration, CustomDeclaration,
638
- ScssComparison, ScssCondInParens, ScssCondTerm, ScssCondAnd, ScssCondOr, ScssRules, ScssIf,
639
- ScssEach, ScssFor, ScssWhile,
640
- ScssCallArg, ScssCallArgsInner, ScssMixinParam, ScssMixinParams, ScssMixinName,
641
- ScssDeclBody, ScssMixin, ScssIncludeUsing, ScssInclude, ScssContent,
642
- ScssFunction, ScssReturn,
643
- ScssWithConfigEntry, ScssWithConfig, ScssUseAs, ScssUse, ScssForward,
644
- ScssPlaceholderSelector, ScssPlaceholderRuleset, ScssExtendTarget, ScssExtend,
645
- ScssImportItem, ImportAtRuleStatement,
646
- ScssNestedProps,
647
- ScssDiagnostic, ScssAtRootFilter, ScssAtRootSelector, ScssAtRootPlain,
648
- QueryAtRuleBlock, ScssScopeBlock, ScssLayerBlock,
649
- Stylesheet, declarationList, atRuleBody
650
- };
651
- })]);
package/src/index.ts DELETED
@@ -1,14 +0,0 @@
1
- // The legacy Chevrotain parser has been removed — the functional macro parser
2
- // (ScssParser / scssGrammar) IS the parser now.
3
-
4
- export { ScssGrammar } from './builders.js';
5
- export { scssGrammar } from './grammar.js';
6
-
7
- import { ScssParser } from './functional-parser.js';
8
- export { ScssParser, parseScssFn, type ScssFnParseResult, type ScssFnParseOptions } from './functional-parser.js';
9
-
10
- export const Parser = ScssParser;
11
- export { parseScssCst, parseScssDoc } from './cst.js';
12
- export type {
13
- ScssCstChild, ScssCstError, ScssCstLeaf, ScssCstNode, ScssCstParseResult, ScssCstType
14
- } from './cst.js';