@jesscss/scss-parser 2.0.0-alpha.7 → 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/builders.ts DELETED
@@ -1,1408 +0,0 @@
1
- /**
2
- * ScssGrammar — Parséman-based SCSS parser, extending LessGrammar.
3
- *
4
- * Adds SCSS-specific grammar on top of Less (which in turn extends CSS):
5
- * - Variable declarations: $var: value [!default|!global]; → VarDeclaration
6
- * - Variable references: $var → Reference
7
- * - Line comments: // ... (added to rw trivia)
8
- *
9
- * Inherits from LessGrammar:
10
- * - Nested rulesets, & ampersand, relative selectors
11
- * - anyDeclaration entry point
12
- * - atRuleBody, declarationList, Stylesheet overrides
13
- * - Less merge operators on Declaration (harmless for SCSS)
14
- *
15
- * Chevrotain note: in the Chevrotain architecture, ScssRecursiveParser
16
- * extends CssRecursiveParser independently of LessRecursiveParser.
17
- * Here we take the Parséman inheritance chain
18
- * CssParser → LessGrammar → ScssGrammar to maximise code reuse.
19
- */
20
-
21
- import {
22
- sequence,
23
- choice,
24
- optional,
25
- regex,
26
- literal
27
- } from 'parseman';
28
- import type { FieldMap, Span } from 'parseman';
29
- import type { CSTLeaf, CSTError } from 'parseman';
30
- import { LessGrammar } from '@jesscss/less-parser/jess';
31
- import { spannedComponents } from '@jesscss/css-parser/jess';
32
-
33
- import {
34
- type Node,
35
- type LocationInfo,
36
- type TreeContext,
37
- Any,
38
- VarDeclaration, type VarDeclarationOptions, type AssignmentType,
39
- Reference,
40
- Rules,
41
- Condition, type ConditionOperator,
42
- Paren,
43
- If,
44
- For,
45
- While,
46
- Nil,
47
- Sequence,
48
- Mixin,
49
- Call,
50
- Rest,
51
- List,
52
- F_VISIBLE,
53
- Func,
54
- Interpolated,
55
- InterpolatedSelector,
56
- CustomDeclaration,
57
- Quoted,
58
- INTERPOLATION_PLACEHOLDER,
59
- isNode,
60
- N,
61
- Collection,
62
- Declaration,
63
- Expression,
64
- Operation,
65
- StyleImport,
66
- JsImport,
67
- Extend,
68
- ExtendFlag,
69
- AtRuleStatement,
70
- AtRule,
71
- Log,
72
- Ruleset,
73
- SelectorCapture,
74
- sourceSpanOf,
75
- type Selector
76
- } from '@jesscss/core';
77
- import {
78
- buildScssInterpolatedFromString,
79
- isValidScssSelectorList,
80
- toInterpReplacement
81
- } from './interp.js';
82
- import {
83
- quotedLike,
84
- isPlainCssImportPrelude,
85
- checkImportPreludeOrder,
86
- validateExtendTarget,
87
- checkForwardPreludeErrors,
88
- isPlaceholderExtendTarget,
89
- isScriptUsePath,
90
- defaultNamespaceFromPath
91
- } from './scss-atrule-helpers.js';
92
- import {
93
- lowerPlainAtRootRules,
94
- prefixAtRootSelector
95
- } from './scss-atroot-helpers.js';
96
- import {
97
- desugarMapLookup,
98
- desugarNamespacedCall,
99
- makeNamespacedReference,
100
- toDeclKey
101
- } from './scss-value-helpers.js';
102
-
103
- // ---------------------------------------------------------------------------
104
- // Types
105
- // ---------------------------------------------------------------------------
106
-
107
- type JessNode = Node<any, any>;
108
- type Child = JessNode | CSTLeaf | CSTError;
109
-
110
- // ---------------------------------------------------------------------------
111
- // Helpers
112
- // ---------------------------------------------------------------------------
113
-
114
- function spanToLocation(span: Span): LocationInfo {
115
- return { start: span.start, end: span.end };
116
- }
117
-
118
- function nodeChildren(children: ReadonlyArray<Child>): JessNode[] {
119
- return children.filter((c): c is JessNode => c != null && c._tag === 'node') as JessNode[];
120
- }
121
-
122
- // ---------------------------------------------------------------------------
123
- // ScssGrammar
124
- // ---------------------------------------------------------------------------
125
-
126
- export class ScssGrammar extends LessGrammar {
127
- // ── Override rw to include // line comments ───────────────────────────────
128
- // Must be declared BEFORE _trivia so the field initializer captures this rw.
129
- rw = regex(/(?:[ \t\n\r\f]+|\/\/[^\n\r]*|\/\*(?:[^*]|\*(?!\/))*\*\/)+/);
130
- protected _trivia = this.rw;
131
- protected _parseContext?: TreeContext;
132
-
133
- setContext(context?: TreeContext) {
134
- this._parseContext = context;
135
- }
136
-
137
- // ── SCSS $variable token ──────────────────────────────────────────────────
138
- scssVar = regex(/\$-?[_a-zA-Z-￿][-_a-zA-Z0-9-￿]*/);
139
-
140
- // ── VarDeclaration: $color: value [!default|!global]; ────────────────────
141
- // Overrides LessGrammar.VarDeclaration (which uses g.lessVar).
142
- VarDeclaration = (g: any) => sequence(
143
- g.scssVar,
144
- literal(':'),
145
- g.valueList,
146
- optional(choice(literal('!default'), literal('!global'))),
147
- optional(literal(';'))
148
- );
149
-
150
- // ── Reference: $var in value positions ───────────────────────────────────
151
- // Overrides LessGrammar.Reference (which used g.lessVar + optional accessor).
152
- Reference = (g: any) => g.scssVar;
153
-
154
- // ── buildNode ─────────────────────────────────────────────────────────────
155
- /* eslint-disable @typescript-eslint/naming-convention */
156
-
157
- protected override buildNode(
158
- type: string,
159
- span: Span,
160
- children: ReadonlyArray<JessNode | CSTLeaf | CSTError>,
161
- _state: unknown,
162
- _rawChildren: ReadonlyArray<{ _tag: string }>,
163
- fields?: FieldMap,
164
- triviaLog: readonly number[] = []
165
- ): JessNode {
166
- const loc = spanToLocation(span);
167
- switch (type) {
168
- case 'VarDeclaration': return this._buildScssVarDeclaration(_rawChildren, loc);
169
- case 'NsVarDeclaration': return this._buildScssNsVarDeclaration(_rawChildren, loc);
170
- case 'Reference': return this._buildScssReference(children, loc);
171
- case 'ScssComparison': return this._buildScssComparison(children, loc);
172
- case 'ScssCondInParens': return this._buildScssCondInParens(children, loc);
173
- case 'ScssCondTerm': return this._buildScssCondTerm(children, loc);
174
- case 'ScssCondAnd': return this._buildScssCondJoin(children, loc, 'and');
175
- case 'ScssCondOr': return this._buildScssCondJoin(children, loc, 'or');
176
- case 'ScssRules': return this._buildScssRules(children, loc);
177
- case 'ScssIf': return this._buildScssIf(children, loc);
178
- case 'ScssEach': return this._buildScssEach(children, loc);
179
- case 'ScssFor': return this._buildScssFor(children, loc);
180
- case 'ScssWhile': return this._buildScssWhile(children, loc);
181
- case 'ScssCallArg': return this._buildScssCallArg(children, loc);
182
- case 'ScssCallArgsInner': return this._buildScssCallArgsInner(children, loc);
183
- case 'ScssMixinParam': return this._buildScssMixinParam(children, loc);
184
- case 'ScssMixinParams': return this._buildScssMixinParams(children, loc);
185
- case 'ScssMixinName': return this._buildScssMixinName(children, loc);
186
- case 'ScssDeclBody': return this._buildScssRules(children, loc);
187
- case 'ScssMixin': return this._buildScssMixin(children, loc);
188
- case 'ScssIncludeUsing': return this._buildScssIncludeUsing(children, loc);
189
- case 'ScssInclude': return this._buildScssInclude(children, loc);
190
- case 'ScssContent': return this._buildScssContent(children, loc);
191
- case 'ScssFunction': return this._buildScssFunction(children, loc);
192
- case 'ScssReturn': return this._buildScssReturn(children, _rawChildren, loc);
193
- case 'ScssInterpBare': return this._buildScssInterpBare(children, loc);
194
- case 'ScssInterpolatedName': return this._buildScssInterpolatedName(children, loc);
195
- case 'InterpValue': return this._buildScssInterpValue(_rawChildren, loc);
196
- case 'InterpolatedSelector': return this._buildScssInterpolatedSelector(children, loc);
197
- case 'Declaration': return this._buildScssDeclaration(children, loc, () =>
198
- super.buildNode(type, span, children, _state, _rawChildren, fields, triviaLog));
199
- case 'CustomDeclaration': return this._buildScssCustomDeclaration(children, loc, () =>
200
- super.buildNode(type, span, children, _state, _rawChildren, fields, triviaLog));
201
- case 'Quoted': return this._buildQuoted(children, loc);
202
- case 'ScssMapPair': return this._buildScssMapPair(children, loc);
203
- case 'ScssMapLiteral': return this._buildScssMapLiteral(children, loc);
204
- case 'ScssIdentValue': return this._buildScssIdentValue(children, _rawChildren, loc);
205
- case 'ScssWithConfigEntry': return this._buildScssWithConfigEntry(_rawChildren, loc);
206
- case 'ScssWithConfig': return this._buildScssWithConfig(children, loc);
207
- case 'ScssUseAs': return this._buildScssUseAs(children, loc);
208
- case 'ScssUse': return this._buildScssUse(children, loc);
209
- case 'ScssForward': return this._buildScssForward(children, _rawChildren, loc);
210
- case 'ScssPlaceholderSelector': return this._buildScssPlaceholderSelector(children, loc);
211
- case 'ScssPlaceholderRuleset': return this._buildRuleset(children, _rawChildren, loc);
212
- case 'ScssExtendTarget': return this._buildScssExtendTarget(children, _rawChildren, loc);
213
- case 'ScssExtend': return this._buildScssExtend(children, _rawChildren, loc);
214
- case 'ScssImportItem': return this._buildScssImportItem(children, _rawChildren, loc);
215
- case 'ScssImportAtRule': return this._buildScssImportAtRule(children, loc);
216
- case 'ScssNestedProps': return this._buildScssNestedProps(children, loc);
217
- case 'ScssDiagnostic': return this._buildScssDiagnostic(children, loc);
218
- case 'ScssAtRootFilter': return this._buildScssAtRootFilter(children, loc);
219
- case 'ScssAtRootSelector': return this._buildScssAtRootSelector(children, loc);
220
- case 'ScssAtRootPlain': return this._buildScssAtRootPlain(children, loc);
221
- case 'ScssScopeBlock': return this._buildScssPermissiveAtRule(children, loc);
222
- case 'ScssLayerBlock': return this._buildScssLayerBlock(children, loc);
223
- case 'Call': return this._buildCall(_rawChildren, loc);
224
- case 'SquareParen': return this._buildSquareParen(_rawChildren, loc);
225
- case 'Paren': return this._buildScssParen(_rawChildren, loc);
226
- default: return super.buildNode(type, span, children, _state, _rawChildren, fields, triviaLog);
227
- }
228
- }
229
-
230
- /* eslint-enable @typescript-eslint/naming-convention */
231
-
232
- // ── Private SCSS AST builders ─────────────────────────────────────────────
233
- /* eslint-disable @typescript-eslint/no-unsafe-type-assertion */
234
-
235
- private _buildScssVarDeclaration(rawChildren: ReadonlyArray<{ _tag: string }>, loc: LocationInfo) {
236
- // strings-not-nodes: name is the bare ident ($ stripped); value via the
237
- // shared CSS string-AST value builder.
238
- const items = spannedComponents(rawChildren);
239
- const rawName = typeof items[0]?.comp === 'string' ? items[0]!.comp : '';
240
- const name = rawName.startsWith('$') ? rawName.slice(1) : rawName;
241
- const colonIdx = items.findIndex(i => i.comp === ':');
242
- let end = items.length;
243
- for (let i = colonIdx + 1; i < items.length; i++) {
244
- const c = items[i]!.comp;
245
- if (c === '!' || c === '!default' || c === '!global' || c === ';') {
246
- end = i;
247
- break;
248
- }
249
- }
250
- const { value } = this._assembleValue(items.slice(colonIdx + 1, end), loc);
251
- const hasImportant = items.some(i => i.comp === '!' || i.comp === '!default' || i.comp === '!global');
252
- return new VarDeclaration(
253
- { name, value, important: hasImportant || undefined } as any,
254
- {} as VarDeclarationOptions,
255
- loc
256
- );
257
- }
258
-
259
- /**
260
- * `ns.$member: value [!default|!global];` — a namespaced variable ASSIGNMENT.
261
- * Built as a `VarDeclaration` whose name carries the namespace (`ns.member`);
262
- * `!default` → conditional-assign, `!global` → `setDefined`. Mirrors the
263
- * member-read shape (`Reference{ target, key }`) on the write side while
264
- * staying within the `string | Interpolated` declaration-name contract.
265
- */
266
- private _buildScssNsVarDeclaration(rawChildren: ReadonlyArray<{ _tag: string }>, loc: LocationInfo) {
267
- const items = spannedComponents(rawChildren);
268
- const ns = typeof items[0]?.comp === 'string' ? items[0]!.comp : '';
269
- const memberItem = items.find(i => typeof i.comp === 'string' && i.comp.startsWith('$'));
270
- const memberRaw = typeof memberItem?.comp === 'string' ? memberItem.comp : '';
271
- const member = memberRaw.startsWith('$') ? memberRaw.slice(1) : memberRaw;
272
- const colonIdx = items.findIndex(i => i.comp === ':');
273
- let end = items.length;
274
- for (let i = colonIdx + 1; i < items.length; i++) {
275
- const c = items[i]!.comp;
276
- if (c === '!' || c === '!default' || c === '!global' || c === ';') {
277
- end = i;
278
- break;
279
- }
280
- }
281
- const { value } = this._assembleValue(items.slice(colonIdx + 1, end), loc);
282
- const sawDefault = items.slice(end).some(i => i.comp === '!default');
283
- const sawGlobal = items.slice(end).some(i => i.comp === '!global');
284
- return new VarDeclaration(
285
- { name: `${ns}.${member}`, value: value as Node },
286
- {
287
- assign: (sawDefault ? '?:' : ':') as AssignmentType,
288
- setDefined: sawGlobal
289
- },
290
- loc
291
- ) as unknown as JessNode;
292
- }
293
-
294
- private _buildScssReference(children: ReadonlyArray<Child>, loc: LocationInfo) {
295
- const ls = children.filter((c): c is CSTLeaf => c._tag === 'leaf');
296
- const varName = ls[0]?.value ?? '';
297
- const key = varName.startsWith('$') ? varName.slice(1) : varName;
298
- return new Reference(key, { type: 'variable' }, loc);
299
- }
300
-
301
- // ── @if / @else conditions ─────────────────────────────────────────────────
302
-
303
- /**
304
- * `left [op right]` → Condition, or a bare operand when there is no operator.
305
- * `!=` desugars to `=` + negate (matches the Chevrotain scssComparison).
306
- */
307
- private _buildScssComparison(children: ReadonlyArray<Child>, loc: LocationInfo) {
308
- const nodes = nodeChildren(children);
309
- const ls = children.filter((c): c is CSTLeaf => c._tag === 'leaf');
310
- const left = nodes[0] ?? new Any('', {}, loc);
311
- const opLeaf = ls.find(l => /^(?:==|!=|>=|<=|=|>|<)$/.test(l.value));
312
- if (!opLeaf || !nodes[1]) {
313
- return left as unknown as JessNode;
314
- }
315
- let op: string = opLeaf.value;
316
- let negate = false;
317
- if (op === '!=') {
318
- op = '=';
319
- negate = true;
320
- } else if (op === '==') {
321
- op = '=';
322
- }
323
- return new Condition(
324
- [left, op as ConditionOperator, nodes[1]],
325
- negate ? { negate: true } : {},
326
- loc
327
- ) as unknown as JessNode;
328
- }
329
-
330
- /**
331
- * Every condition term is wrapped in a Paren, matching the Chevrotain
332
- * `scssConditionInParens` production (both the `( … )` group and the bare
333
- * comparison / value branch wrap their result in a single Paren).
334
- */
335
- private _buildScssCondInParens(children: ReadonlyArray<Child>, loc: LocationInfo) {
336
- const inner = nodeChildren(children)[0] ?? new Any('', {}, loc);
337
- return new Paren(inner as any, {}, loc) as unknown as JessNode;
338
- }
339
-
340
- /** Optional leading `not` negates the term. */
341
- private _buildScssCondTerm(children: ReadonlyArray<Child>, loc: LocationInfo) {
342
- const ls = children.filter((c): c is CSTLeaf => c._tag === 'leaf');
343
- const inner = nodeChildren(children)[0] ?? new Any('', {}, loc);
344
- if (ls.some(l => /^not$/i.test(l.value))) {
345
- return new Condition([inner as any], { negate: true }, loc) as unknown as JessNode;
346
- }
347
- return inner as unknown as JessNode;
348
- }
349
-
350
- /** Fold a left-associative `and` / `or` chain of terms into Conditions. */
351
- private _buildScssCondJoin(children: ReadonlyArray<Child>, loc: LocationInfo, op: ConditionOperator) {
352
- const nodes = nodeChildren(children);
353
- if (nodes.length === 0) {
354
- return new Any('', {}, loc) as unknown as JessNode;
355
- }
356
- let left = nodes[0]!;
357
- for (let i = 1; i < nodes.length; i++) {
358
- left = new Condition([left, op, nodes[i]!], {}, loc) as unknown as Node;
359
- }
360
- return left as unknown as JessNode;
361
- }
362
-
363
- /** A `{ … }` control-block body → Rules. */
364
- private _buildScssRules(children: ReadonlyArray<Child>, loc: LocationInfo) {
365
- const rules = this._flattenScssImportLists(nodeChildren(children));
366
- return new Rules(rules, undefined, loc) as unknown as JessNode;
367
- }
368
-
369
- /**
370
- * `@if cond { … } (@else if cond { … })* (@else { … })?` → nested `If` chain.
371
- * Children arrive as alternating condition / Rules nodes, with an optional
372
- * trailing bare Rules (the final `@else`). Fold from the last branch inward.
373
- */
374
- private _buildScssIf(children: ReadonlyArray<Child>, loc: LocationInfo) {
375
- const nodes = nodeChildren(children);
376
- const conditions: Node[] = [];
377
- const bodies: Rules[] = [];
378
- let elseBranch: Rules | undefined;
379
- let pendingCond: Node | undefined;
380
- for (const n of nodes) {
381
- if (n instanceof Rules) {
382
- if (pendingCond !== undefined) {
383
- conditions.push(pendingCond);
384
- bodies.push(n);
385
- pendingCond = undefined;
386
- } else {
387
- elseBranch = n;
388
- }
389
- } else {
390
- pendingCond = n;
391
- }
392
- }
393
- let elseNode: If | Rules | undefined = elseBranch;
394
- for (let i = conditions.length - 1; i >= 0; i--) {
395
- elseNode = new If(
396
- { condition: conditions[i]!, rules: bodies[i]!.rules, else: elseNode },
397
- undefined,
398
- loc
399
- );
400
- }
401
- return (elseNode ?? new Any('', {}, loc)) as unknown as JessNode;
402
- }
403
-
404
- // ── @each / @for / @while loops ───────────────────────────────────────────
405
-
406
- /** A `$name` loop-binding with no value (`paramVar` — prints as `$name`). */
407
- private _scssParamVar(varName: string, loc: LocationInfo): VarDeclaration {
408
- return new VarDeclaration(
409
- { name: varName, value: new Nil() },
410
- { paramVar: true },
411
- loc
412
- );
413
- }
414
-
415
- /**
416
- * `@each $a[, $b …] in <expr> { … }` → `For` with a node iterable.
417
- * Normalizes to Jess `$for ($a of …)` / `$for ([$a, $b] of …)`.
418
- */
419
- private _buildScssEach(children: ReadonlyArray<Child>, loc: LocationInfo) {
420
- const ls = children.filter((c): c is CSTLeaf => c._tag === 'leaf');
421
- const nodes = nodeChildren(children);
422
- const body = nodes.find((n): n is Rules => n instanceof Rules)!;
423
-
424
- const vars: string[] = [];
425
- let pastEach = false;
426
- for (const l of ls) {
427
- if (/^@each/i.test(l.value)) {
428
- pastEach = true;
429
- continue;
430
- }
431
- if (pastEach && l.value === 'in') {
432
- break;
433
- }
434
- if (pastEach && l.value.startsWith('$')) {
435
- vars.push(l.value.slice(1));
436
- }
437
- }
438
-
439
- const iterableNodes = nodes.filter(n => n !== body);
440
- let iterable: Node = iterableNodes.length === 1
441
- ? iterableNodes[0]!
442
- : new Sequence(iterableNodes as any, undefined, loc);
443
- if ((iterable as any).type === 'Expression') {
444
- iterable = (iterable as any).value;
445
- }
446
-
447
- const decls = vars.map(v => this._scssParamVar(v, loc));
448
- const pattern = decls.length === 1
449
- ? { kind: 'single' as const, value: decls[0]! }
450
- : { kind: 'tuple' as const, values: decls as [VarDeclaration, ...VarDeclaration[]] };
451
-
452
- return new For(
453
- { pattern, iterable: { kind: 'node', value: iterable }, rules: body.rules },
454
- undefined,
455
- loc
456
- ) as unknown as JessNode;
457
- }
458
-
459
- /**
460
- * `@for $i from <start> (to|through) <end> { … }` → `For` with a range iterable.
461
- * `through` is inclusive end; `to` is exclusive (`includeEnd: false`).
462
- */
463
- private _buildScssFor(children: ReadonlyArray<Child>, loc: LocationInfo) {
464
- const ls = children.filter((c): c is CSTLeaf => c._tag === 'leaf');
465
- const nodes = nodeChildren(children);
466
- const includeEnd = ls.some(l => l.value === 'through');
467
-
468
- const varLeaf = ls.find(l => l.value.startsWith('$'));
469
- const varDecl = this._scssParamVar(varLeaf?.value.slice(1) ?? '', loc);
470
-
471
- const body = nodes.find((n): n is Rules => n instanceof Rules)!;
472
- const exprNodes = nodes.filter(n => n !== body);
473
- const startExpr = exprNodes[0] ?? new Any('', {}, loc);
474
- const endExpr = exprNodes[1] ?? new Any('', {}, loc);
475
-
476
- return new For(
477
- {
478
- pattern: { kind: 'single', value: varDecl },
479
- iterable: { kind: 'range', start: startExpr, end: endExpr, includeStart: true, includeEnd },
480
- rules: body.rules
481
- },
482
- undefined,
483
- loc
484
- ) as unknown as JessNode;
485
- }
486
-
487
- /** `@while <cond> { … }` → `While`. */
488
- private _buildScssWhile(children: ReadonlyArray<Child>, loc: LocationInfo) {
489
- const nodes = nodeChildren(children);
490
- const body = nodes.find((n): n is Rules => n instanceof Rules)!;
491
- const condition = nodes.find(n => n !== body) ?? new Any('', {}, loc);
492
- return new While({ condition, rules: body.rules }, undefined, loc) as unknown as JessNode;
493
- }
494
-
495
- // ── @mixin / @include / @content ───────────────────────────────────────────
496
-
497
- /** Build a module-qualified or plain mixin `Reference`. */
498
- private _buildScssMixinName(children: ReadonlyArray<Child>, loc: LocationInfo) {
499
- const nodes = nodeChildren(children);
500
- const interp = nodes.find(n => isNode(n, N.Interpolated));
501
- if (interp) {
502
- return new Reference({ key: interp }, { type: 'mixin', role: 'name' }, loc) as unknown as JessNode;
503
- }
504
- const parts = children
505
- .filter((c): c is CSTLeaf => c._tag === 'leaf')
506
- .map(l => l.value)
507
- .filter(v => v !== '.');
508
- if (parts.length >= 2) {
509
- let ref: Reference = new Reference(parts[0]!, { type: 'variable' }, loc);
510
- for (let i = 1; i < parts.length; i++) {
511
- const isFinal = i === parts.length - 1;
512
- ref = new Reference(
513
- { target: ref, key: parts[i]! },
514
- { type: isFinal ? 'mixin' : 'index', ...(isFinal ? { role: 'name' as const } : {}) },
515
- loc
516
- );
517
- }
518
- return ref as unknown as JessNode;
519
- }
520
- return new Reference({ key: parts[0] ?? '' }, { type: 'mixin', role: 'name' }, loc) as unknown as JessNode;
521
- }
522
-
523
- /** `$x: val` keyword arg, `val...` spread, or plain value. */
524
- private _buildScssCallArg(children: ReadonlyArray<Child>, loc: LocationInfo) {
525
- const ls = children.filter((c): c is CSTLeaf => c._tag === 'leaf');
526
- const nodes = nodeChildren(children);
527
- const varLeaf = ls.find(l => l.value.startsWith('$') && l.value !== '$');
528
- const hasColon = ls.some(l => l.value === ':');
529
- const hasSpread = ls.some(l => l.value === '...');
530
- if (varLeaf && hasColon) {
531
- const name = varLeaf.value.slice(1);
532
- const value = nodes.find(n => n !== undefined && !ls.includes(n as any)) ?? nodes[0] ?? new Nil();
533
- return new VarDeclaration(
534
- { name, value: value as Node },
535
- {},
536
- loc
537
- ) as unknown as JessNode;
538
- }
539
- const value = nodes[0] ?? new Any('', {}, loc);
540
- if (hasSpread) {
541
- return new Rest(value as Node, undefined, loc) as unknown as JessNode;
542
- }
543
- return value as unknown as JessNode;
544
- }
545
-
546
- private _buildScssCallArgsInner(children: ReadonlyArray<Child>, loc: LocationInfo) {
547
- const nodes = nodeChildren(children);
548
- if (nodes.length === 0) {
549
- return undefined as unknown as JessNode;
550
- }
551
- return new List(nodes as any, undefined, loc) as unknown as JessNode;
552
- }
553
-
554
- /** Mixin param: `...$rest`, `$rest...`, `$a: default`, or bare `$a`. */
555
- private _buildScssMixinParam(children: ReadonlyArray<Child>, loc: LocationInfo) {
556
- const ls = children.filter((c): c is CSTLeaf => c._tag === 'leaf');
557
- const nodes = nodeChildren(children);
558
- const varLeaf = ls.find(l => l.value.startsWith('$'));
559
- const varName = varLeaf?.value.slice(1) ?? '';
560
- const hasPrefixEllipsis = ls[0]?.value === '...';
561
- const hasSuffixEllipsis = ls.some(l => l.value === '...' && ls.indexOf(l) > 0);
562
- if (hasPrefixEllipsis || hasSuffixEllipsis) {
563
- return new Rest(varName, undefined, loc) as unknown as JessNode;
564
- }
565
- const hasColon = ls.some(l => l.value === ':');
566
- if (hasColon && nodes[0]) {
567
- return new VarDeclaration(
568
- { name: varName, value: nodes[0] as Node },
569
- { paramVar: true },
570
- loc
571
- ) as unknown as JessNode;
572
- }
573
- return new Any(varName, { role: 'property' }, loc) as unknown as JessNode;
574
- }
575
-
576
- private _buildScssMixinParams(children: ReadonlyArray<Child>, loc: LocationInfo) {
577
- const nodes = nodeChildren(children);
578
- return new List(nodes as any, undefined, loc) as unknown as JessNode;
579
- }
580
-
581
- /** `@mixin name($params) { … }` → `Mixin` (inner vars default to private). */
582
- private _buildScssMixin(children: ReadonlyArray<Child>, loc: LocationInfo) {
583
- const ls = children.filter((c): c is CSTLeaf => c._tag === 'leaf');
584
- const nodes = nodeChildren(children);
585
- const interpName = nodes.find(n => isNode(n, N.Interpolated)) as Interpolated<'name'> | undefined;
586
- const nameLeaf = ls.find(l => !l.value.startsWith('@') && l.value !== '(' && l.value !== ')'
587
- && l.value !== '{' && l.value !== '}' && l.value !== ',');
588
- const name = interpName ?? (nameLeaf?.value ?? '');
589
- const params = nodes.find(n => n.type === 'List') as List | undefined;
590
- const body = nodes.find((n): n is Rules => n instanceof Rules)!;
591
- return new Mixin(
592
- { name, params, rules: body.rules },
593
- undefined,
594
- loc
595
- ) as unknown as JessNode;
596
- }
597
-
598
- /** `using ($c, $n)` param list for `@include … using (…)`. */
599
- private _buildScssIncludeUsing(children: ReadonlyArray<Child>, loc: LocationInfo) {
600
- const ls = children.filter((c): c is CSTLeaf => c._tag === 'leaf');
601
- const vars = ls.filter(l => l.value.startsWith('$')).map(l => this._scssParamVar(l.value.slice(1), loc));
602
- return new List(vars as any, undefined, loc) as unknown as JessNode;
603
- }
604
-
605
- /**
606
- * `@include name(args) [using (…)] [ { … } ];` → `Call(Reference(type=mixin))`.
607
- * An optional content block becomes an anonymous visible `Mixin` on the call.
608
- */
609
- private _buildScssInclude(children: ReadonlyArray<Child>, loc: LocationInfo) {
610
- const ls = children.filter((c): c is CSTLeaf => c?._tag === 'leaf');
611
- const nodes = nodeChildren(children);
612
- const nameRef = nodes.find(n => n.type === 'Reference') as Reference | undefined;
613
- const lists = nodes.filter(n => n.type === 'List') as List[];
614
- const hasUsing = ls.some(l => l.value === 'using');
615
- let args: List | undefined;
616
- let usingParams: List | undefined;
617
- if (lists.length === 2) {
618
- args = lists[0];
619
- usingParams = lists[1];
620
- } else if (lists.length === 1) {
621
- if (hasUsing) {
622
- usingParams = lists[0];
623
- } else {
624
- args = lists[0];
625
- }
626
- }
627
- const contentRules = nodes.find((n): n is Rules => n instanceof Rules);
628
- let contentNode: Mixin | undefined;
629
- if (contentRules) {
630
- contentNode = new Mixin(
631
- { rules: contentRules.rules, params: usingParams },
632
- undefined,
633
- loc
634
- );
635
- contentNode.addFlags(F_VISIBLE);
636
- }
637
- return new Call(
638
- { name: nameRef ?? new Reference({ key: '' }, { type: 'mixin', role: 'name' }, loc), args, contentNode: contentNode as Node | undefined },
639
- undefined,
640
- loc
641
- ) as unknown as JessNode;
642
- }
643
-
644
- /** `@content[(args)];` → `Call(Reference('content', type=mixin))`. */
645
- private _buildScssContent(children: ReadonlyArray<Child>, loc: LocationInfo) {
646
- const nodes = nodeChildren(children);
647
- const args = nodes.find(n => n.type === 'List') as List | undefined;
648
- const ref = new Reference({ key: 'content' }, { type: 'mixin', role: 'name' }, loc);
649
- return new Call({ name: ref, args }, undefined, loc) as unknown as JessNode;
650
- }
651
-
652
- /** `@function name($params) { … }` → `Func` with `returnName: 'result'`. */
653
- private _buildScssFunction(children: ReadonlyArray<Child>, loc: LocationInfo) {
654
- const ls = children.filter((c): c is CSTLeaf => c._tag === 'leaf');
655
- const nodes = nodeChildren(children);
656
- const interpName = nodes.find(n => isNode(n, N.Interpolated)) as Interpolated<'name'> | undefined;
657
- const nameLeaf = ls.find(l => !l.value.startsWith('@') && l.value !== '(' && l.value !== ')'
658
- && l.value !== '{' && l.value !== '}' && l.value !== ',');
659
- const name = interpName ?? (nameLeaf?.value ?? '');
660
- const params = nodes.find(n => n.type === 'List') as List | undefined;
661
- const body = nodes.find((n): n is Rules => n instanceof Rules)!;
662
- return new Func(
663
- { name, params, body },
664
- { returnName: 'result' },
665
- loc
666
- ) as unknown as JessNode;
667
- }
668
-
669
- /** `@return <value>;` → `$result: <value>;` */
670
- private _buildScssReturn(
671
- children: ReadonlyArray<Child>,
672
- rawChildren: ReadonlyArray<{ _tag: string }>,
673
- loc: LocationInfo
674
- ) {
675
- const items = spannedComponents(rawChildren);
676
- const semiIdx = items.findIndex(i => i.comp === ';');
677
- const valueItems = items.filter((i, idx) =>
678
- idx > 0 && i.comp !== '@return' && (semiIdx < 0 || idx < semiIdx)
679
- );
680
- const { value } = this._assembleValue(valueItems, loc);
681
- const name = 'result';
682
- return new VarDeclaration({ name, value: value as Node }, undefined, loc) as unknown as JessNode;
683
- }
684
-
685
- // ── Interpolation (#{…}) ───────────────────────────────────────────────────
686
-
687
- private _buildScssInterpBare(children: ReadonlyArray<Child>, loc: LocationInfo) {
688
- const expr = nodeChildren(children)[0] ?? new Any('', {}, loc);
689
- return new Interpolated(
690
- { source: INTERPOLATION_PLACEHOLDER, replacements: [toInterpReplacement(expr as Node, loc)] },
691
- { role: 'any' },
692
- loc
693
- ) as unknown as JessNode;
694
- }
695
-
696
- /** `foo-#{$bar}` name segments → Interpolated(role=name) or plain Any. */
697
- private _buildScssInterpolatedName(children: ReadonlyArray<Child>, loc: LocationInfo) {
698
- let source = '';
699
- const replacements: Node[] = [];
700
- for (const c of children) {
701
- if (c._tag === 'leaf') {
702
- const v = (c as CSTLeaf).value;
703
- if (v === '#{' || v === '}' || v === '.') {
704
- continue;
705
- }
706
- source += v;
707
- } else if (c._tag === 'node' && isNode(c as JessNode, N.Interpolated)) {
708
- source += INTERPOLATION_PLACEHOLDER;
709
- replacements.push(...(c as Interpolated).replacements);
710
- }
711
- }
712
- if (replacements.length === 0) {
713
- return new Any(source, { role: 'name' }, loc) as unknown as JessNode;
714
- }
715
- return new Interpolated({ source, replacements }, { role: 'name' }, loc) as unknown as JessNode;
716
- }
717
-
718
- private _buildScssInterpValue(raw: ReadonlyArray<{ _tag: string }>, loc: LocationInfo) {
719
- const items = spannedComponents(raw);
720
- const image = items.map(i => (typeof i.comp === 'string' ? i.comp : '')).join('');
721
- const result = buildScssInterpolatedFromString(image, loc, 'ident');
722
- return result as unknown as JessNode;
723
- }
724
-
725
- private _buildScssInterpolatedSelector(children: ReadonlyArray<Child>, loc: LocationInfo) {
726
- let source = '';
727
- const replacements: Node[] = [];
728
- for (const c of children) {
729
- if (c._tag === 'leaf') {
730
- const v = (c as CSTLeaf).value;
731
- if (v === '#{' || v === '}') {
732
- continue;
733
- }
734
- source += v;
735
- } else if (c._tag === 'node' && isNode(c as JessNode, N.Interpolated)) {
736
- source += INTERPOLATION_PLACEHOLDER;
737
- replacements.push(...(c as Interpolated).replacements);
738
- }
739
- }
740
- const interp = new Interpolated({ source, replacements }, { role: 'ident' }, loc);
741
- return new InterpolatedSelector(interp as any, {}, loc) as unknown as JessNode;
742
- }
743
-
744
- private _scssInterpDeclName(name: unknown, loc: LocationInfo): unknown {
745
- if (typeof name !== 'string') {
746
- return name;
747
- }
748
- if (name.includes('#{')) {
749
- return buildScssInterpolatedFromString(name, loc, 'property');
750
- }
751
- return name;
752
- }
753
-
754
- private _buildScssDeclaration(
755
- children: ReadonlyArray<Child>,
756
- loc: LocationInfo,
757
- buildLess: () => JessNode
758
- ) {
759
- const decl = buildLess() as Declaration;
760
- const d = decl as { name?: unknown; value?: unknown };
761
- if (d.name !== undefined) {
762
- d.name = this._scssInterpDeclName(d.name, loc);
763
- }
764
- const valueNodes = nodeChildren(children).filter(n =>
765
- isNode(n, N.Collection) || isNode(n, N.Sequence) || isNode(n, N.Keyword)
766
- || isNode(n, N.Reference) || isNode(n, N.Num) || isNode(n, N.Paren) || isNode(n, N.List)
767
- );
768
- const collection = valueNodes.find(n => isNode(n, N.Collection));
769
- if (collection && valueNodes.length > 1) {
770
- const base = valueNodes.find(n => n !== collection);
771
- if (base) {
772
- d.value = new Sequence([base as Node, collection as Node], undefined, loc);
773
- }
774
- } else if (collection) {
775
- d.value = collection;
776
- }
777
- return decl as unknown as JessNode;
778
- }
779
-
780
- private _buildScssCustomDeclaration(
781
- children: ReadonlyArray<Child>,
782
- loc: LocationInfo,
783
- buildLess: () => JessNode
784
- ) {
785
- const decl = buildLess() as CustomDeclaration;
786
- const d = decl as { name?: unknown };
787
- if (d.name !== undefined) {
788
- d.name = this._scssInterpDeclName(d.name, loc);
789
- }
790
- return decl as unknown as JessNode;
791
- }
792
-
793
- protected override _buildQuoted(children: ReadonlyArray<Child>, loc: LocationInfo) {
794
- const ls = children.filter((c): c is CSTLeaf => c._tag === 'leaf');
795
- const text = ls.map(l => l.value).join('');
796
- const inner = text.slice(1, -1);
797
- const quote = text[0] as '"' | '\'';
798
- if (inner.includes('#{')) {
799
- const value = buildScssInterpolatedFromString(inner, loc, 'any');
800
- return new Quoted(value, { quote }, loc) as unknown as JessNode;
801
- }
802
- return super._buildQuoted(children, loc);
803
- }
804
-
805
- /** `("k": v, …)` pair inside a map literal. */
806
- private _buildScssMapPair(children: ReadonlyArray<Child>, loc: LocationInfo) {
807
- const nodes = nodeChildren(children);
808
- const keyNode = nodes[0] ?? new Any('', { role: 'property' }, loc);
809
- const valueNode = nodes[1] ?? new Any('', {}, loc);
810
- const keyStr = toDeclKey(keyNode as Node);
811
- return new Declaration(
812
- { name: keyStr, value: valueNode as Node },
813
- undefined,
814
- loc
815
- ) as unknown as JessNode;
816
- }
817
-
818
- private _buildScssMapLiteral(children: ReadonlyArray<Child>, loc: LocationInfo) {
819
- const decls = nodeChildren(children) as Declaration[];
820
- return new Collection(decls as any, undefined, loc) as unknown as JessNode;
821
- }
822
-
823
- /** `ns.$var`, `ns.fn(…)`, `ns.\#foo(…)`, or a plain ident. */
824
- private _buildScssIdentValue(
825
- children: ReadonlyArray<Child>,
826
- raw: ReadonlyArray<{ _tag: string }>,
827
- loc: LocationInfo
828
- ) {
829
- const ls = children.filter((c): c is CSTLeaf => c?._tag === 'leaf');
830
- const identLeaf = ls.find(l => !l.value.startsWith('.') && l.value !== '(' && l.value !== ')'
831
- && l.value !== '\\');
832
- const ident = identLeaf?.value ?? '';
833
- const varLeaf = ls.find(l => l.value.startsWith('$'));
834
- const dotLeaf = ls.find(l => l.value.startsWith('.') && !l.value.startsWith('$'));
835
- const hashLeaf = ls.find(l => l.value.startsWith('#'));
836
- const hasCall = ls.some(l => l.value === '(');
837
- const hasEscape = ls.some(l => l.value === '\\');
838
-
839
- if (varLeaf && dotLeaf) {
840
- const nsRef = new Reference(ident, { type: 'variable' }, loc);
841
- const key = varLeaf.value.slice(1);
842
- return new Reference({ target: nsRef, key }, { type: 'variable' }, loc) as unknown as JessNode;
843
- }
844
-
845
- if (hasEscape && hashLeaf && hasCall) {
846
- const key = hashLeaf.value.slice(1);
847
- const args = nodeChildren(children).find(n => isNode(n, N.List)) as List | undefined;
848
- const ref = makeNamespacedReference([ident, key], 'mixin-ruleset', loc);
849
- const call = new Call({ name: ref, args }, undefined, loc);
850
- return new Expression(call, undefined, loc) as unknown as JessNode;
851
- }
852
-
853
- if (dotLeaf && hasCall) {
854
- const fnName = dotLeaf.value.slice(1);
855
- if (ident === 'selector' && fnName === 'parse') {
856
- const items = spannedComponents(raw);
857
- const open = items.findIndex(i => i.comp === '(');
858
- let close = items.length;
859
- for (let i = items.length - 1; i >= 0; i--) {
860
- if (items[i]!.comp === ')') {
861
- close = i;
862
- break;
863
- }
864
- }
865
- const { value: argValue } = this._assembleValue(items.slice(open + 1, close), loc);
866
- const firstArg = isNode(argValue as Node, N.List)
867
- ? (argValue as List).value[0]
868
- : argValue;
869
- const selectorText = firstArg && isNode(firstArg as Node, N.Quoted)
870
- ? typeof (firstArg as Quoted).value === 'string'
871
- ? (firstArg as Quoted).value as string
872
- : isNode((firstArg as Quoted).value, N.Any)
873
- ? String((firstArg as Quoted).value.valueOf())
874
- : undefined
875
- : undefined;
876
- if (selectorText !== undefined && isValidScssSelectorList(selectorText)) {
877
- // SelectorCapture keeps the lean bare-string payload; validation above
878
- // rejects malformed input and falls through to default call desugaring.
879
- return new SelectorCapture(selectorText, undefined, loc) as unknown as JessNode;
880
- }
881
- }
882
- const items = spannedComponents(raw);
883
- const open = items.findIndex(i => i.comp === '(');
884
- let close = items.length;
885
- for (let i = items.length - 1; i >= 0; i--) {
886
- if (items[i]!.comp === ')') {
887
- close = i;
888
- break;
889
- }
890
- }
891
- const { value: argValue } = this._assembleValue(items.slice(open + 1, close), loc);
892
- let args: List | undefined;
893
- if (argValue !== undefined) {
894
- args = isNode(argValue as Node, N.List)
895
- ? argValue as List
896
- : new List([argValue as Node], undefined, loc);
897
- }
898
- const dottedName = `${ident}.${fnName}`;
899
- const lookupCall = new Call({ name: dottedName, args }, undefined, loc);
900
- const mapped = desugarMapLookup(lookupCall, loc);
901
- if (isNode(mapped, N.Reference)) {
902
- return mapped as unknown as JessNode;
903
- }
904
- const memberType = fnName.startsWith('#') ? 'mixin-ruleset' : 'function';
905
- const memberKey = fnName.startsWith('#') ? fnName.slice(1) : fnName;
906
- const ref = makeNamespacedReference([ident, memberKey], memberType, loc);
907
- const call = new Call({ name: ref, args }, undefined, loc);
908
- if (memberType === 'mixin-ruleset') {
909
- return new Expression(call, undefined, loc) as unknown as JessNode;
910
- }
911
- return new Expression(desugarNamespacedCall(call, loc), undefined, loc) as unknown as JessNode;
912
- }
913
-
914
- return new Any(ident, { role: 'ident' }, loc) as unknown as JessNode;
915
- }
916
-
917
- protected override _buildStylesheet(children: ReadonlyArray<Child>, loc: LocationInfo) {
918
- const nodes = this._flattenScssImportLists(nodeChildren(children));
919
- const lifted = this._liftStandaloneComments(nodes, loc.start, loc.end, loc);
920
- return new Rules(lifted, undefined, loc);
921
- }
922
-
923
- private _flattenScssImportLists(nodes: JessNode[]): JessNode[] {
924
- const flat: JessNode[] = [];
925
- for (const n of nodes) {
926
- if (isNode(n, N.List) && ((n as List).options?.role === 'scss-imports'
927
- || (n as List).options?.role === 'scss-at-root')) {
928
- flat.push(...(n as List).value);
929
- } else {
930
- flat.push(n);
931
- }
932
- }
933
- return flat;
934
- }
935
-
936
- private _buildScssNestedProps(children: ReadonlyArray<Child>, loc: LocationInfo) {
937
- // Keep sub-declarations plus any control flow / namespaced-assignment nodes
938
- // Sass permits inside a nested-properties block (dropping them would silently
939
- // lose statements).
940
- const kept: Node[] = nodeChildren(children).filter(n =>
941
- isNode(n, N.Declaration) || isNode(n, N.VarDeclaration)
942
- || n instanceof If || n instanceof For || n instanceof While
943
- );
944
- return new Collection(kept, undefined, loc) as unknown as JessNode;
945
- }
946
-
947
- private _buildScssDiagnostic(children: ReadonlyArray<Child>, loc: LocationInfo) {
948
- const ls = children.filter((c): c is CSTLeaf => c?._tag === 'leaf');
949
- const atLeaf = ls.find(l => l.value.startsWith('@'));
950
- const level = (atLeaf?.value.slice(1) ?? 'debug') as 'debug' | 'warn' | 'error';
951
- const message = nodeChildren(children).find(n => !isNode(n, N.Any) || (n as Any).options?.role !== 'atkeyword')
952
- ?? nodeChildren(children)[0]
953
- ?? new Any('', {}, loc);
954
- return new Log({ level, message: message as Node }, undefined, loc) as unknown as JessNode;
955
- }
956
-
957
- private _buildScssAtRootFilter(children: ReadonlyArray<Child>, loc: LocationInfo) {
958
- const nodes = nodeChildren(children);
959
- const prelude = nodes.find(n => !(n instanceof Rules)) ?? nodes[0];
960
- const body = nodes.find((n): n is Rules => n instanceof Rules)!;
961
- const name = new Any('@at-root', { role: 'atkeyword' }, loc);
962
- this._error(
963
- '@at-root prelude/filter forms are not yet supported in Jess. Write the hoisted rules directly instead.',
964
- loc.start,
965
- loc.end
966
- );
967
- return new AtRule(
968
- { name, prelude: prelude as Node, rules: body.rules },
969
- undefined,
970
- loc
971
- ) as unknown as JessNode;
972
- }
973
-
974
- private _buildScssAtRootSelector(children: ReadonlyArray<Child>, loc: LocationInfo) {
975
- const nodes = nodeChildren(children);
976
- const selector = nodes.find(n => !(n instanceof Rules)) as Selector;
977
- const body = nodes.find((n): n is Rules => n instanceof Rules)!;
978
- const context = this._parseContext;
979
- return new Ruleset(
980
- {
981
- selector: prefixAtRootSelector(selector, context),
982
- rules: body.rules
983
- },
984
- undefined,
985
- loc
986
- ) as unknown as JessNode;
987
- }
988
-
989
- private _buildScssAtRootPlain(children: ReadonlyArray<Child>, loc: LocationInfo) {
990
- const body = nodeChildren(children).find((n): n is Rules => n instanceof Rules)!;
991
- const context = this._parseContext;
992
- const lowered = new Rules([...body.rules], undefined, loc);
993
- lowerPlainAtRootRules(lowered, context);
994
- if (lowered.rules.length === 0) {
995
- return new Nil(undefined, undefined, loc) as unknown as JessNode;
996
- }
997
- if (lowered.rules.length === 1) {
998
- return lowered.rules[0]! as unknown as JessNode;
999
- }
1000
- return new List(lowered.rules, { role: 'scss-at-root' }, loc) as unknown as JessNode;
1001
- }
1002
-
1003
- private _buildScssWithConfigEntry(rawChildren: ReadonlyArray<{ _tag: string }>, loc: LocationInfo) {
1004
- const items = spannedComponents(rawChildren);
1005
- const rawName = typeof items[0]?.comp === 'string' ? items[0]!.comp : '';
1006
- const name = rawName.startsWith('$') ? rawName.slice(1) : rawName;
1007
- const colonIdx = items.findIndex(i => i.comp === ':');
1008
- let end = items.length;
1009
- for (let i = colonIdx + 1; i < items.length; i++) {
1010
- const c = items[i]!.comp;
1011
- if (c === '!' || c === '!default' || c === '!global' || c === ',' || c === ')') {
1012
- end = i;
1013
- break;
1014
- }
1015
- }
1016
- const { value } = this._assembleValue(items.slice(colonIdx + 1, end), loc);
1017
- const sawDefault = items.slice(end).some(i => i.comp === '!default');
1018
- const sawGlobal = items.slice(end).some(i => i.comp === '!global');
1019
- return new VarDeclaration(
1020
- { name, value: value as Node },
1021
- {
1022
- assign: (sawDefault ? '?:' : ':') as AssignmentType,
1023
- setDefined: sawGlobal
1024
- },
1025
- loc
1026
- ) as unknown as JessNode;
1027
- }
1028
-
1029
- private _buildScssWithConfig(children: ReadonlyArray<Child>, loc: LocationInfo) {
1030
- const decls = nodeChildren(children).filter(n => isNode(n, N.VarDeclaration));
1031
- return new Collection(decls as Node[], undefined, loc) as unknown as JessNode;
1032
- }
1033
-
1034
- private _buildScssUseAs(children: ReadonlyArray<Child>, loc: LocationInfo) {
1035
- const ls = children.filter((c): c is CSTLeaf => c?._tag === 'leaf');
1036
- const nsLeaf = ls.find(l => l.value !== 'as');
1037
- return new Any(nsLeaf?.value ?? '', { role: 'ident' }, loc) as unknown as JessNode;
1038
- }
1039
-
1040
- private _buildScssUse(children: ReadonlyArray<Child>, loc: LocationInfo) {
1041
- const pathNode = nodeChildren(children).find(n => isNode(n, N.Quoted)) as Quoted | undefined;
1042
- const withConfig = nodeChildren(children).find(n => isNode(n, N.Collection)) as Collection | undefined;
1043
- const useAs = nodeChildren(children).find(n => isNode(n, N.Any) && (n as Any).options?.role === 'ident');
1044
- const namespace = useAs ? String((useAs as Any).valueOf()) : undefined;
1045
- const rawPath = pathNode?.valueOf() ?? '';
1046
-
1047
- if (rawPath.startsWith('sass:')) {
1048
- const mod = rawPath.slice('sass:'.length);
1049
- const rewritten = `#sass/${mod}`;
1050
- const q = quotedLike(pathNode!, rewritten, loc);
1051
- return new JsImport(
1052
- { path: q },
1053
- { namespace: namespace ?? defaultNamespaceFromPath(rawPath) },
1054
- loc
1055
- ) as unknown as JessNode;
1056
- }
1057
-
1058
- if (isScriptUsePath(rawPath)) {
1059
- return new JsImport(
1060
- { path: pathNode! },
1061
- { namespace: namespace ?? defaultNamespaceFromPath(rawPath) },
1062
- loc
1063
- ) as unknown as JessNode;
1064
- }
1065
-
1066
- return new StyleImport(
1067
- {
1068
- path: pathNode!,
1069
- with: withConfig ? { node: withConfig, type: 'set' } : undefined
1070
- },
1071
- {
1072
- type: 'compose',
1073
- namespace,
1074
- importOptions: {}
1075
- },
1076
- loc
1077
- ) as unknown as JessNode;
1078
- }
1079
-
1080
- private _buildScssForward(
1081
- children: ReadonlyArray<Child>,
1082
- _raw: ReadonlyArray<{ _tag: string }>,
1083
- loc: LocationInfo
1084
- ) {
1085
- const pathNode = nodeChildren(children).find(n => isNode(n, N.Quoted)) as Quoted | undefined;
1086
- const withConfig = nodeChildren(children).find(n => isNode(n, N.Collection)) as Collection | undefined;
1087
- const preludeText = this._source.slice(loc.start, loc.end);
1088
- const pathMatch = /(['"])([^'"]+)\1/.exec(preludeText);
1089
- const afterPath = pathMatch
1090
- ? preludeText.slice(preludeText.indexOf(pathMatch[0]) + pathMatch[0].length)
1091
- : '';
1092
- const preludeExtra = afterPath.replace(/\bwith\s*\([^)]*\)\s*;?\s*$/, '').replace(/;\s*$/, '').trim();
1093
- checkForwardPreludeErrors(preludeExtra, msg => this._error(msg, loc.start, loc.end));
1094
-
1095
- return new StyleImport(
1096
- {
1097
- path: pathNode!,
1098
- with: withConfig ? { node: withConfig, type: 'set' } : undefined
1099
- },
1100
- {
1101
- type: 'compose',
1102
- importOptions: { forward: true }
1103
- },
1104
- loc
1105
- ) as unknown as JessNode;
1106
- }
1107
-
1108
- private _buildScssPlaceholderSelector(children: ReadonlyArray<Child>, loc: LocationInfo) {
1109
- const ls = children.filter((c): c is CSTLeaf => c?._tag === 'leaf');
1110
- const raw = ls[0]?.value ?? '';
1111
- const name = `\\${raw.slice(1)}`;
1112
- return this._makeBasicSelector(name, loc);
1113
- }
1114
-
1115
- private _buildScssPermissiveAtRule(children: ReadonlyArray<Child>, loc: LocationInfo) {
1116
- const ls = children.filter((c): c is CSTLeaf => c?._tag === 'leaf');
1117
- const name = ls[0]?.value ?? '';
1118
- const braceIdx = children.findIndex(c => c._tag === 'leaf' && (c as CSTLeaf).value === '{');
1119
- const preludeChildren = braceIdx >= 0 ? children.slice(1, braceIdx) : children.slice(1);
1120
- const bodyChildren = braceIdx >= 0 ? children.slice(braceIdx + 1) : [];
1121
- const prelude = new Sequence(nodeChildren(preludeChildren) as Node[], undefined, loc);
1122
- return new AtRule(
1123
- { name, prelude, rules: nodeChildren(bodyChildren) },
1124
- undefined,
1125
- loc
1126
- ) as unknown as JessNode;
1127
- }
1128
-
1129
- private _buildScssLayerBlock(children: ReadonlyArray<Child>, loc: LocationInfo) {
1130
- const ls = children.filter((c): c is CSTLeaf => c?._tag === 'leaf');
1131
- const name = ls[0]?.value ?? '';
1132
- const braceIdx = children.findIndex(c => c._tag === 'leaf' && (c as CSTLeaf).value === '{');
1133
- const preludeChildren = braceIdx >= 0 ? children.slice(1, braceIdx) : children.slice(1);
1134
- const bodyChildren = braceIdx >= 0 ? children.slice(braceIdx + 1) : [];
1135
- const preludeNodes = nodeChildren(preludeChildren);
1136
- const prelude = preludeNodes.length === 1
1137
- ? preludeNodes[0]
1138
- : preludeNodes.length > 0
1139
- ? new Sequence(preludeNodes as Node[], undefined, loc)
1140
- : undefined;
1141
- return new AtRule(
1142
- { name, prelude, rules: nodeChildren(bodyChildren) },
1143
- undefined,
1144
- loc
1145
- ) as unknown as JessNode;
1146
- }
1147
-
1148
- protected override _buildQueryAtRuleBlock(children: ReadonlyArray<Child>, loc: LocationInfo) {
1149
- const ls = children.filter((c): c is CSTLeaf => c?._tag === 'leaf');
1150
- const name = ls[0]?.value ?? '';
1151
- const braceIdx = children.findIndex(c => c._tag === 'leaf' && (c as CSTLeaf).value === '{');
1152
- const preludeChildren = braceIdx >= 0 ? children.slice(1, braceIdx) : children.slice(1);
1153
- const bodyChildren = braceIdx >= 0 ? children.slice(braceIdx + 1) : [];
1154
- const prelude = new Sequence(nodeChildren(preludeChildren) as Node[], undefined, loc);
1155
- return new AtRule(
1156
- { name, prelude, rules: nodeChildren(bodyChildren) },
1157
- undefined,
1158
- loc
1159
- ) as unknown as JessNode;
1160
- }
1161
-
1162
- protected _buildScssParen(rawChildren: ReadonlyArray<{ _tag: string }>, loc: LocationInfo) {
1163
- const inner = this._betweenParens(spannedComponents(rawChildren));
1164
- const { value } = this._assembleValue(inner, loc);
1165
- if (value && isNode(value as Node, N.Operation)) {
1166
- return new Expression(value as Node, undefined, loc) as unknown as JessNode;
1167
- }
1168
- if (value && isNode(value as Node, N.List) && (value as List).options?.sep === '/'
1169
- && (value as List).value.length === 2) {
1170
- const [left, right] = (value as List).value;
1171
- const operation = new Operation([left!, '/', right!], undefined, loc);
1172
- return new Expression(operation, undefined, loc) as unknown as JessNode;
1173
- }
1174
- return new Paren(value as unknown as Node, undefined, loc) as unknown as JessNode;
1175
- }
1176
-
1177
- private _buildScssExtendTarget(
1178
- children: ReadonlyArray<Child>,
1179
- raw: ReadonlyArray<{ _tag: string }>,
1180
- loc: LocationInfo
1181
- ) {
1182
- for (const c of children) {
1183
- if (typeof c === 'string') {
1184
- return c as unknown as JessNode;
1185
- }
1186
- }
1187
- const placeholderLeaf = children.find((c): c is CSTLeaf =>
1188
- c?._tag === 'leaf' && typeof (c as CSTLeaf).value === 'string' && (c as CSTLeaf).value.startsWith('%')
1189
- );
1190
- if (placeholderLeaf) {
1191
- return `\\${placeholderLeaf.value.slice(1)}` as unknown as JessNode;
1192
- }
1193
- const items = nodeChildren(children);
1194
- if (items.length === 1) {
1195
- return items[0]!;
1196
- }
1197
- if (items.length > 1) {
1198
- return this._makeSelectorList(items, loc);
1199
- }
1200
- const spanItems = spannedComponents(raw).filter(i => i.comp !== ',');
1201
- if (spanItems.length === 1 && typeof spanItems[0]!.comp === 'string') {
1202
- const sel = spanItems[0]!.comp as string;
1203
- return (sel.startsWith('%') ? `\\${sel.slice(1)}` : sel) as unknown as JessNode;
1204
- }
1205
- return items[0] as unknown as JessNode;
1206
- }
1207
-
1208
- private _scssExtendTargetFrom(
1209
- children: ReadonlyArray<Child>,
1210
- raw: ReadonlyArray<{ _tag: string }>,
1211
- _loc: LocationInfo
1212
- ): Selector | string {
1213
- for (const c of children) {
1214
- if (typeof c === 'string') {
1215
- return c;
1216
- }
1217
- if (c != null && typeof c === 'object' && '_tag' in c && (c as { _tag: string })._tag === 'node') {
1218
- const n = c as JessNode;
1219
- if (['SelectorList', 'BasicSelector', 'CompoundSelector', 'ComplexSelector'].includes(n.type)) {
1220
- return n as unknown as Selector;
1221
- }
1222
- }
1223
- }
1224
- const items = spannedComponents(raw).filter(i => i.comp !== '@extend' && i.comp !== ';' && i.comp !== '!optional');
1225
- if (items.length === 1 && typeof items[0]!.comp === 'string') {
1226
- const sel = items[0]!.comp as string;
1227
- if (sel.startsWith('%')) {
1228
- return `\\${sel.slice(1)}`;
1229
- }
1230
- return sel;
1231
- }
1232
- return nodeChildren(children)[0] as unknown as Selector;
1233
- }
1234
-
1235
- private _buildScssExtend(
1236
- children: ReadonlyArray<Child>,
1237
- raw: ReadonlyArray<{ _tag: string }>,
1238
- loc: LocationInfo
1239
- ) {
1240
- const target = this._scssExtendTargetFrom(children, raw, loc);
1241
- validateExtendTarget(
1242
- target as Node,
1243
- this._parseContext?.opts?.allowExtendSelectors,
1244
- msg => this._error(msg, loc.start, loc.end)
1245
- );
1246
- const prelude = this._source.slice(loc.start, loc.end);
1247
- const namespace = /@extend\s+%/.test(prelude) || isPlaceholderExtendTarget(target)
1248
- ? '*'
1249
- : undefined;
1250
- return new Extend(
1251
- { target: target as unknown as Selector, flag: ExtendFlag.All, namespace },
1252
- undefined,
1253
- loc
1254
- ) as unknown as JessNode;
1255
- }
1256
-
1257
- private _buildScssImportItem(
1258
- children: ReadonlyArray<Child>,
1259
- raw: ReadonlyArray<{ _tag: string }>,
1260
- loc: LocationInfo
1261
- ) {
1262
- const prelude = nodeChildren(children).find(n => isNode(n, N.Quoted) || isNode(n, N.Url)) as Node | undefined;
1263
- const pathSpan = spannedComponents(raw).find(i =>
1264
- isNode(i.comp as Node, N.Quoted) || isNode(i.comp as Node, N.Url) || (typeof i.comp === 'string' && (i.comp.startsWith('"') || i.comp.startsWith('\'') || i.comp.startsWith('url')))
1265
- );
1266
- let extraText: string | undefined;
1267
- if (pathSpan) {
1268
- const tail = raw.filter(c => c._tag === 'leaf' && (c as CSTLeaf).value !== '@import')
1269
- .map(c => (c as CSTLeaf).value)
1270
- .join('');
1271
- const pathText = typeof pathSpan.comp === 'string' ? pathSpan.comp : '';
1272
- const idx = tail.indexOf(pathText);
1273
- if (idx >= 0) {
1274
- extraText = tail.slice(idx + pathText.length).replace(/^[\s,]+/, '').replace(/[,;]\s*$/, '').trim() || undefined;
1275
- }
1276
- }
1277
- const seqItems: Node[] = [];
1278
- if (prelude) {
1279
- seqItems.push(prelude);
1280
- }
1281
- if (extraText) {
1282
- seqItems.push(new Any(extraText, { role: 'ident' }, loc) as unknown as Node);
1283
- }
1284
- return new Sequence(seqItems, undefined, loc) as unknown as JessNode;
1285
- }
1286
-
1287
- private _buildScssImportAtRule(children: ReadonlyArray<Child>, loc: LocationInfo) {
1288
- // Reject the CSS `@import` ordering violations Sass parse-rejects. Run on the
1289
- // raw prelude (everything after `@import`, without the trailing `;`).
1290
- const preludeText = this._source.slice(loc.start, loc.end)
1291
- .replace(/^@import\b/i, '')
1292
- .replace(/;\s*$/, '');
1293
- checkImportPreludeOrder(preludeText, msg => this._error(msg, loc.start, loc.end));
1294
- const items = nodeChildren(children).filter(n => isNode(n, N.Sequence));
1295
- const importName = new Any('@import', { role: 'atkeyword' }, loc) as unknown as Node;
1296
- const built: JessNode[] = [];
1297
- for (const item of items) {
1298
- const seq = item as Sequence;
1299
- const prelude = seq.value[0];
1300
- const extra = seq.value[1];
1301
- const extraText = extra && isNode(extra, N.Any) ? String((extra as Any).valueOf()).trim() : undefined;
1302
- const itemLoc = sourceSpanOf(seq) ?? loc;
1303
- if (!prelude) {
1304
- continue;
1305
- }
1306
- if (!isPlainCssImportPrelude(prelude as Node, extraText) && isNode(prelude as Node, N.Quoted)) {
1307
- built.push(new StyleImport(
1308
- { path: prelude as Quoted },
1309
- { type: 'import', importOptions: { multiple: true } },
1310
- itemLoc
1311
- ) as unknown as JessNode);
1312
- continue;
1313
- }
1314
- const preludeNodes = [prelude as Node];
1315
- if (extraText) {
1316
- preludeNodes.push(new Any(extraText, { role: 'ident' }, itemLoc) as unknown as Node);
1317
- }
1318
- built.push(new AtRuleStatement(
1319
- {
1320
- name: importName,
1321
- prelude: new Sequence(preludeNodes, undefined, itemLoc)
1322
- },
1323
- undefined,
1324
- itemLoc
1325
- ) as unknown as JessNode);
1326
- }
1327
- if (built.length === 1) {
1328
- return built[0]!;
1329
- }
1330
- return new List(built, { role: 'scss-imports' }, loc) as unknown as JessNode;
1331
- }
1332
-
1333
- protected override _buildCall(rawChildren: ReadonlyArray<{ _tag: string }>, loc: LocationInfo) {
1334
- const call = super._buildCall(rawChildren, loc) as Call;
1335
- const nameNode = call.name;
1336
- const stringName = typeof nameNode === 'string'
1337
- ? nameNode
1338
- : isNode(nameNode, N.Reference) && typeof nameNode.key === 'string'
1339
- ? nameNode.key
1340
- : '';
1341
-
1342
- const mapped = desugarMapLookup(
1343
- new Call({ name: stringName, args: call.args }, call.options, loc),
1344
- loc
1345
- );
1346
- if (isNode(mapped, N.Reference)) {
1347
- return mapped as unknown as JessNode;
1348
- }
1349
-
1350
- const desugared = desugarNamespacedCall(
1351
- new Call({ name: stringName, args: call.args }, call.options, loc),
1352
- loc
1353
- );
1354
- const name = desugared.name;
1355
-
1356
- if (stringName === 'selector.parse') {
1357
- const argValues = isNode(desugared.args, N.List) ? desugared.args.value : [];
1358
- const firstArg = argValues[0];
1359
- const selectorText = firstArg && isNode(firstArg, N.Quoted)
1360
- ? typeof firstArg.value === 'string'
1361
- ? firstArg.value
1362
- : isNode(firstArg.value, N.Any)
1363
- ? String(firstArg.value.valueOf())
1364
- : undefined
1365
- : undefined;
1366
- if (selectorText !== undefined && isValidScssSelectorList(selectorText)) {
1367
- // SelectorCapture keeps the lean bare-string payload; malformed input
1368
- // falls through to the desugared namespaced call below.
1369
- return new SelectorCapture(selectorText, undefined, loc) as unknown as JessNode;
1370
- }
1371
- return desugared as unknown as JessNode;
1372
- }
1373
-
1374
- if (typeof name === 'string' && name.includes('.')) {
1375
- return new Expression(desugared, undefined, loc) as unknown as JessNode;
1376
- }
1377
-
1378
- if (isNode(name, N.Reference) && name.options?.type === 'function') {
1379
- return new Call({ name, args: desugared.args }, undefined, loc) as unknown as JessNode;
1380
- }
1381
-
1382
- if (typeof name === 'string') {
1383
- const ref = new Reference(
1384
- { key: name },
1385
- { type: 'function', fallbackValue: true },
1386
- loc
1387
- );
1388
- return new Call(
1389
- { name: ref, args: desugared.args },
1390
- undefined,
1391
- loc
1392
- ) as unknown as JessNode;
1393
- }
1394
-
1395
- return desugared as unknown as JessNode;
1396
- }
1397
-
1398
- protected override _buildSquareParen(rawChildren: ReadonlyArray<{ _tag: string }>, loc: LocationInfo) {
1399
- const paren = super._buildSquareParen(rawChildren, loc) as Paren;
1400
- const inner = (paren as unknown as { value?: Node }).value;
1401
- const delimiter = isNode(inner as Node, N.Any) && (inner as Any).options?.role === 'ident'
1402
- ? 'square'
1403
- : 'paren';
1404
- return new Paren(inner as Node, { delimiter }, loc) as unknown as JessNode;
1405
- }
1406
-
1407
- /* eslint-enable @typescript-eslint/no-unsafe-type-assertion */
1408
- }