@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/README.md +33 -8
- package/lib/ast/grammar.cjs +36610 -0
- package/lib/ast/grammar.d.ts +1 -0
- package/lib/ast/grammar.js +36609 -0
- package/lib/ast/lower-user-function-calls.d.ts +6 -0
- package/lib/cst.cjs +1 -1
- package/lib/cst.js +1 -1
- package/lib/grammar.cjs +2 -60347
- package/lib/grammar.js +1 -60346
- package/lib/grammar2.cjs +88858 -0
- package/lib/grammar2.js +88853 -0
- package/lib/index.cjs +108 -11
- package/lib/index.d.ts +10 -7
- package/lib/index.js +107 -5
- package/package.json +10 -20
- package/lib/builders.d.ts +0 -154
- package/lib/functional-parser.cjs +0 -3034
- package/lib/functional-parser.d.ts +0 -46
- package/lib/functional-parser.js +0 -3017
- package/lib/interp.d.ts +0 -26
- package/lib/jess.cjs +0 -6
- package/lib/jess.d.ts +0 -2
- package/lib/jess.js +0 -2
- package/lib/scss-atroot-helpers.d.ts +0 -4
- package/lib/scss-atrule-helpers.d.ts +0 -36
- package/lib/scss-value-helpers.d.ts +0 -11
- package/src/builders.ts +0 -1408
- package/src/cst.ts +0 -25
- package/src/functional-parser.ts +0 -135
- package/src/grammar.ts +0 -651
- package/src/index.ts +0 -14
- package/src/interp.ts +0 -158
- package/src/jess.ts +0 -11
- package/src/scss-atroot-helpers.ts +0 -105
- package/src/scss-atrule-helpers.ts +0 -191
- package/src/scss-value-helpers.ts +0 -105
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
|
-
}
|