@appthreat/atom-parsetools 1.5.0 → 1.6.0
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 +32 -103
- package/astgen.js +148 -11
- package/package.json +6 -4
- package/plugins/composer/installed.php +6 -6
- package/svelteAst.js +1291 -0
package/svelteAst.js
ADDED
|
@@ -0,0 +1,1291 @@
|
|
|
1
|
+
// Svelte single-file component support for astgen.
|
|
2
|
+
//
|
|
3
|
+
// A `.svelte` file mixes a JavaScript/TypeScript `<script>` block with an HTML
|
|
4
|
+
// template that uses `{...}` expression tags and `{#if}`/`{#each}`/... logic
|
|
5
|
+
// blocks. Neither half parses as the other, so a single Babel parse of the raw
|
|
6
|
+
// document cannot work. Instead `svelte/compiler`'s `parse(src, { modern: true })`
|
|
7
|
+
// is used purely as a *segmenter*: it returns a tree whose every node carries
|
|
8
|
+
// an absolute byte range into the original file. From that tree we produce a
|
|
9
|
+
// regular Babel `File` AST in three steps:
|
|
10
|
+
//
|
|
11
|
+
// 1. Script blocks are parsed by Babel over a masked buffer - a same-length
|
|
12
|
+
// copy of the file where every character outside the script bodies is
|
|
13
|
+
// blanked to a space (newlines preserved). Because nothing moves, the
|
|
14
|
+
// resulting statement offsets are already absolute.
|
|
15
|
+
// 2. The template is walked and re-emitted as standard Babel JSX nodes
|
|
16
|
+
// (JSXElement, JSXExpressionContainer, JSXFragment, ...). Zero new node
|
|
17
|
+
// types are introduced, so downstream consumers that understand Babel JSX
|
|
18
|
+
// need no changes.
|
|
19
|
+
// 3. Every template expression/pattern/declaration is sub-parsed with Babel
|
|
20
|
+
// from its own source substring and its offsets shifted back into file
|
|
21
|
+
// coordinates.
|
|
22
|
+
//
|
|
23
|
+
// The result keeps two invariants everywhere:
|
|
24
|
+
//
|
|
25
|
+
// * every `start`/`end` is an absolute byte offset into the `.svelte` source
|
|
26
|
+
// and `loc` is rebuilt from those offsets (`loc.start.index === start`), so
|
|
27
|
+
// `src.slice(node.start, node.end)` is always the node's original text;
|
|
28
|
+
// * every emitted `type` is a stock Babel node type.
|
|
29
|
+
//
|
|
30
|
+
// Synthesized (non-Babel-parsed) nodes additionally carry two additive keys for
|
|
31
|
+
// traceability: `svelteKind` (the originating Svelte node type, e.g.
|
|
32
|
+
// "EachBlock") and, where meaningful, `svelteName` (e.g. the tag name).
|
|
33
|
+
// Consumers ignore unknown keys, so these are safe to emit.
|
|
34
|
+
//
|
|
35
|
+
// Limitations, by design: `<style>` blocks and HTML comments are dropped, and
|
|
36
|
+
// `{@const}` is modelled as an assignment rather than a declaration (see
|
|
37
|
+
// docs/ASTGEN.md, "Svelte" for the full list of accepted losses).
|
|
38
|
+
|
|
39
|
+
import { parse as svelteCompilerParse } from "svelte/compiler";
|
|
40
|
+
import { parse as babelParse, parseExpression } from "@babel/parser";
|
|
41
|
+
|
|
42
|
+
// Identifier substituted for a template expression that Babel could not
|
|
43
|
+
// sub-parse. The file is never aborted over one bad expression; the failure is
|
|
44
|
+
// recorded in `File.errors` instead.
|
|
45
|
+
const UNPARSED_IDENTIFIER_NAME = "__astgen_unparsed";
|
|
46
|
+
|
|
47
|
+
const TRANSITION_DIRECTIVE_PREFIXES = ["transition", "in", "out"];
|
|
48
|
+
|
|
49
|
+
// `name_loc` in Svelte's modern AST is a { line, column, character } pair, not
|
|
50
|
+
// a plain offset pair; the byte offsets live under `.character`.
|
|
51
|
+
const nameLocStart = (node) => node.name_loc?.start?.character ?? node.start;
|
|
52
|
+
const nameLocEnd = (node) => node.name_loc?.end?.character ?? node.end;
|
|
53
|
+
|
|
54
|
+
/** Per-file parse context threaded through all helpers below. */
|
|
55
|
+
class SvelteParseContext {
|
|
56
|
+
constructor(file, src, options, errors) {
|
|
57
|
+
this.file = file;
|
|
58
|
+
this.src = src;
|
|
59
|
+
// Babel options for every sub-parse; supplied by the caller so there is a
|
|
60
|
+
// single option set shared with regular JS/TS parsing in astgen.js.
|
|
61
|
+
this.options = options;
|
|
62
|
+
this.errors = errors;
|
|
63
|
+
// Offsets of the first character of every line, for rebuilding `loc`.
|
|
64
|
+
this.lineStarts = [0];
|
|
65
|
+
for (let i = 0; i < src.length; i++) {
|
|
66
|
+
if (src[i] === "\n") {
|
|
67
|
+
this.lineStarts.push(i + 1);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** { line (1-based), column (0-based), index } for an absolute offset. */
|
|
73
|
+
posOf(offset) {
|
|
74
|
+
const lineStarts = this.lineStarts;
|
|
75
|
+
let lo = 0;
|
|
76
|
+
let hi = lineStarts.length - 1;
|
|
77
|
+
while (lo < hi) {
|
|
78
|
+
const mid = (lo + hi + 1) >> 1;
|
|
79
|
+
if (lineStarts[mid] <= offset) {
|
|
80
|
+
lo = mid;
|
|
81
|
+
} else {
|
|
82
|
+
hi = mid - 1;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return { line: lo + 1, column: offset - lineStarts[lo], index: offset };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
slice(start, end) {
|
|
89
|
+
return this.src.slice(start, end);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
recordSubParseFailure(message, start, end) {
|
|
93
|
+
this.errors.push({ svelteSubParse: true, message, start, end });
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Fresh loc object for a synthesized node spanning [start, end). */
|
|
97
|
+
locBetween(start, end) {
|
|
98
|
+
return { start: this.posOf(start), end: this.posOf(end) };
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Recursively add `delta` to every start/end/range on `node` (in place) and
|
|
104
|
+
* rebuild `loc` from the original file's line index. Value fields such as
|
|
105
|
+
* `extra.raw` or `TemplateElement.value` hold source strings, not offsets, and
|
|
106
|
+
* are left untouched.
|
|
107
|
+
*/
|
|
108
|
+
const shiftNode = (ctx, node, delta) => {
|
|
109
|
+
if (Array.isArray(node)) {
|
|
110
|
+
for (const child of node) {
|
|
111
|
+
shiftNode(ctx, child, delta);
|
|
112
|
+
}
|
|
113
|
+
return node;
|
|
114
|
+
}
|
|
115
|
+
if (!node || typeof node !== "object") {
|
|
116
|
+
return node;
|
|
117
|
+
}
|
|
118
|
+
if (typeof node.start === "number" && typeof node.end === "number") {
|
|
119
|
+
node.start += delta;
|
|
120
|
+
node.end += delta;
|
|
121
|
+
if (Array.isArray(node.range) && node.range.length === 2) {
|
|
122
|
+
node.range = [node.range[0] + delta, node.range[1] + delta];
|
|
123
|
+
}
|
|
124
|
+
if (node.loc) {
|
|
125
|
+
node.loc = { start: ctx.posOf(node.start), end: ctx.posOf(node.end) };
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
for (const key of Object.keys(node)) {
|
|
129
|
+
if (key === "loc" || key === "range" || key === "start" || key === "end") {
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
shiftNode(ctx, node[key], delta);
|
|
133
|
+
}
|
|
134
|
+
return node;
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
/** Placeholder emitted when a sub-parse fails; keeps ranges sane for consumers. */
|
|
138
|
+
const unparsedIdentifier = (ctx, start, end) => ({
|
|
139
|
+
type: "Identifier",
|
|
140
|
+
name: UNPARSED_IDENTIFIER_NAME,
|
|
141
|
+
start,
|
|
142
|
+
end,
|
|
143
|
+
loc: ctx.locBetween(start, end)
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Run a sub-parse, converting any throw into a recorded error plus an
|
|
148
|
+
* `__astgen_unparsed` identifier. One malformed expression costs exactly that
|
|
149
|
+
* expression, never the file.
|
|
150
|
+
*/
|
|
151
|
+
const guardedSubParse = (ctx, start, end, parseFn) => {
|
|
152
|
+
try {
|
|
153
|
+
return parseFn();
|
|
154
|
+
} catch (err) {
|
|
155
|
+
ctx.recordSubParseFailure(err?.message || String(err), start, end);
|
|
156
|
+
return unparsedIdentifier(ctx, start, end);
|
|
157
|
+
}
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Rebuild every `loc` on a Babel-parsed tree with fresh position objects.
|
|
162
|
+
* Babel shares position objects between neighbouring nodes (`nodeA.loc.end ===
|
|
163
|
+
* nodeB.loc.start`), and the JSON writer's circular-reference guard drops a
|
|
164
|
+
* repeated object on its second appearance - leaving some nodes with a
|
|
165
|
+
* half-missing `loc` (this also affects plain `.js` output). Rebuilding from
|
|
166
|
+
* the node's own offsets makes the Svelte output fully self-consistent:
|
|
167
|
+
* `loc.start.index === start` and `loc.end.index === end` hold everywhere.
|
|
168
|
+
*/
|
|
169
|
+
const relocTree = (ctx, node) => {
|
|
170
|
+
if (Array.isArray(node)) {
|
|
171
|
+
for (const child of node) {
|
|
172
|
+
relocTree(ctx, child);
|
|
173
|
+
}
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
if (!node || typeof node !== "object") {
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
if (typeof node.start === "number" && typeof node.end === "number") {
|
|
180
|
+
node.loc = ctx.locBetween(node.start, node.end);
|
|
181
|
+
}
|
|
182
|
+
for (const key of Object.keys(node)) {
|
|
183
|
+
if (key === "loc" || key === "range" || key === "start" || key === "end") {
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
186
|
+
relocTree(ctx, node[key]);
|
|
187
|
+
}
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
/** Sub-parse a standalone expression such as `count > 10` or `'<b>' + x`. */
|
|
191
|
+
const subExpr = (ctx, start, end) =>
|
|
192
|
+
guardedSubParse(ctx, start, end, () =>
|
|
193
|
+
shiftNode(
|
|
194
|
+
ctx,
|
|
195
|
+
parseExpression(ctx.slice(start, end), ctx.options),
|
|
196
|
+
start
|
|
197
|
+
)
|
|
198
|
+
);
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Sub-parse a binding pattern such as `item`, `{ a, b }` or `[x, ...rest]`.
|
|
202
|
+
* Patterns are not standalone expressions, so the text is embedded as the
|
|
203
|
+
* parameter of a throwaway arrow function; the pattern starts at index 1 of
|
|
204
|
+
* that snippet, hence the `start - 1` shift.
|
|
205
|
+
*/
|
|
206
|
+
const subPattern = (ctx, start, end) =>
|
|
207
|
+
guardedSubParse(ctx, start, end, () => {
|
|
208
|
+
const fn = babelParse(`(${ctx.slice(start, end)})=>0`, ctx.options);
|
|
209
|
+
return shiftNode(ctx, fn.program.body[0].expression.params[0], start - 1);
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
// ---------------------------------------------------------------------------
|
|
214
|
+
// Source-range helpers for synthesized element structure
|
|
215
|
+
// ---------------------------------------------------------------------------
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* End of an element's opening tag (index just past its `>`), derived from the
|
|
219
|
+
* source. Svelte reports the tag-name range and attribute ranges but not the
|
|
220
|
+
* tag boundary itself. Scanning for `>` from the last attribute's end is safe
|
|
221
|
+
* against quoted `>` characters because a quoted value belongs to an attribute
|
|
222
|
+
* node and therefore ends before the scan starts.
|
|
223
|
+
*/
|
|
224
|
+
const openTagEnd = (ctx, node) => {
|
|
225
|
+
let from = nameLocEnd(node);
|
|
226
|
+
for (const attribute of node.attributes ?? []) {
|
|
227
|
+
if (attribute.end > from) {
|
|
228
|
+
from = attribute.end;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
const gt = ctx.src.indexOf(">", from);
|
|
232
|
+
return gt === -1 ? node.end : gt + 1;
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* True for `<br/>` and for unclosed void elements such as `<br>` or
|
|
237
|
+
* `<img src=x>` whose node ends at the opening tag because they have no
|
|
238
|
+
* children and no closing tag.
|
|
239
|
+
*/
|
|
240
|
+
const isSelfClosing = (ctx, node) => {
|
|
241
|
+
const end = openTagEnd(ctx, node);
|
|
242
|
+
return ctx.src[end - 2] === "/" || end >= node.end;
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
/** Range of the closing tag (`</div>`), or null for self-closing/void elements. */
|
|
246
|
+
const closeTagRange = (ctx, node) => {
|
|
247
|
+
const openEnd = openTagEnd(ctx, node);
|
|
248
|
+
if (isSelfClosing(ctx, node) || node.end <= openEnd) {
|
|
249
|
+
return null;
|
|
250
|
+
}
|
|
251
|
+
// An element with children ends exactly at its closing tag's `>`. Scanning
|
|
252
|
+
// backwards for `</` alone is not enough: the next sibling's closing tag can
|
|
253
|
+
// start precisely at this element's end, so each candidate is validated by
|
|
254
|
+
// requiring its `>` to land on node.end - 1.
|
|
255
|
+
let lt = ctx.src.lastIndexOf("</", node.end - 1);
|
|
256
|
+
while (lt >= openEnd) {
|
|
257
|
+
const gt = ctx.src.indexOf(">", lt);
|
|
258
|
+
if (gt === node.end - 1) {
|
|
259
|
+
return { start: lt, end: node.end };
|
|
260
|
+
}
|
|
261
|
+
lt = ctx.src.lastIndexOf("</", lt - 1);
|
|
262
|
+
}
|
|
263
|
+
return null;
|
|
264
|
+
};
|
|
265
|
+
|
|
266
|
+
// ---------------------------------------------------------------------------
|
|
267
|
+
// Names
|
|
268
|
+
// ---------------------------------------------------------------------------
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Map a tag/attribute name onto Babel JSX name nodes:
|
|
272
|
+
* "div" -> JSXIdentifier
|
|
273
|
+
* "svelte:head" -> JSXNamespacedName(svelte, head)
|
|
274
|
+
* "Foo.Bar" -> JSXMemberExpression(Foo, Bar)
|
|
275
|
+
* Offsets come from the source so every part spans its real text; chen never
|
|
276
|
+
* visits these nodes, but keeping ranges honest costs nothing.
|
|
277
|
+
*/
|
|
278
|
+
const jsxName = (ctx, name, start, end) => {
|
|
279
|
+
const identifier = (idName, s, e) => ({
|
|
280
|
+
type: "JSXIdentifier",
|
|
281
|
+
name: idName,
|
|
282
|
+
start: s,
|
|
283
|
+
end: e,
|
|
284
|
+
loc: ctx.locBetween(s, e)
|
|
285
|
+
});
|
|
286
|
+
if (name.includes(":")) {
|
|
287
|
+
const [namespaceName, localName] = name.split(":");
|
|
288
|
+
const colon = ctx.src.indexOf(":", start);
|
|
289
|
+
const namespaceEnd =
|
|
290
|
+
colon === -1 || colon >= end ? start + namespaceName.length : colon;
|
|
291
|
+
const localStart = namespaceEnd + 1;
|
|
292
|
+
return {
|
|
293
|
+
type: "JSXNamespacedName",
|
|
294
|
+
namespace: identifier(namespaceName, start, namespaceEnd),
|
|
295
|
+
name: identifier(localName, localStart, Math.max(localStart, end)),
|
|
296
|
+
start,
|
|
297
|
+
end,
|
|
298
|
+
loc: ctx.locBetween(start, end)
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
if (name.includes(".")) {
|
|
302
|
+
const lastDot = name.lastIndexOf(".");
|
|
303
|
+
const dot = ctx.src.lastIndexOf(".", end - 1);
|
|
304
|
+
const objectEnd = dot <= start || dot >= end ? start + lastDot : dot;
|
|
305
|
+
return {
|
|
306
|
+
type: "JSXMemberExpression",
|
|
307
|
+
object: jsxName(ctx, name.slice(0, lastDot), start, objectEnd),
|
|
308
|
+
property: identifier(name.slice(lastDot + 1), objectEnd + 1, end),
|
|
309
|
+
start,
|
|
310
|
+
end,
|
|
311
|
+
loc: ctx.locBetween(start, end)
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
return identifier(name, start, end);
|
|
315
|
+
};
|
|
316
|
+
|
|
317
|
+
// ---------------------------------------------------------------------------
|
|
318
|
+
// Attributes and directives
|
|
319
|
+
// ---------------------------------------------------------------------------
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* Map an attribute/directive value onto the `value` of a Babel JSXAttribute.
|
|
323
|
+
*
|
|
324
|
+
* Svelte shapes: `true` (boolean shorthand), a single node (ExpressionTag for
|
|
325
|
+
* `{expr}`), or an array of Text/ExpressionTag parts (mixed content such as
|
|
326
|
+
* class="a {b} c", or a plain quoted string which arrives as [Text]).
|
|
327
|
+
*/
|
|
328
|
+
const mapAttributeValue = (ctx, value, valueStart, valueEnd) => {
|
|
329
|
+
if (value === true || value == null) {
|
|
330
|
+
return null;
|
|
331
|
+
}
|
|
332
|
+
if (!Array.isArray(value)) {
|
|
333
|
+
if (value.type === "ExpressionTag") {
|
|
334
|
+
return expressionTagContainer(ctx, value, value.expression);
|
|
335
|
+
}
|
|
336
|
+
if (value.type === "Text") {
|
|
337
|
+
return stringLiteralForText(ctx, value, value.start, value.end);
|
|
338
|
+
}
|
|
339
|
+
return subExpr(ctx, value.start, value.end);
|
|
340
|
+
}
|
|
341
|
+
if (value.length === 1 && value[0].type === "Text") {
|
|
342
|
+
return stringLiteralForText(ctx, value[0], valueStart, valueEnd);
|
|
343
|
+
}
|
|
344
|
+
if (value.length === 1 && value[0].type === "ExpressionTag") {
|
|
345
|
+
return expressionTagContainer(ctx, value[0], value[0].expression);
|
|
346
|
+
}
|
|
347
|
+
// Mixed text/interpolation: a JSXExpressionContainer over the whole (quoted)
|
|
348
|
+
// value wrapping a TemplateLiteral built from the parts - the shape a Babel
|
|
349
|
+
// parse of the equivalent JSX attribute would produce.
|
|
350
|
+
const [regionStart, regionEnd] = trimmedValueRange(ctx, valueStart, valueEnd);
|
|
351
|
+
const template = mixedValueTemplateLiteral(ctx, value);
|
|
352
|
+
return {
|
|
353
|
+
type: "JSXExpressionContainer",
|
|
354
|
+
start: regionStart,
|
|
355
|
+
end: regionEnd,
|
|
356
|
+
loc: ctx.locBetween(regionStart, regionEnd),
|
|
357
|
+
expression: template
|
|
358
|
+
};
|
|
359
|
+
};
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* The attribute value's own region: after the `=`, trimmed of surrounding
|
|
363
|
+
* whitespace, so a quoted value keeps both quotes (`class="a"` -> `"a"`) the
|
|
364
|
+
* way Babel's own attribute values do.
|
|
365
|
+
*/
|
|
366
|
+
const trimmedValueRange = (ctx, valueStart, valueEnd) => {
|
|
367
|
+
let start = valueStart;
|
|
368
|
+
let end = valueEnd;
|
|
369
|
+
while (start < end && /\s/.test(ctx.src[start])) {
|
|
370
|
+
start++;
|
|
371
|
+
}
|
|
372
|
+
while (end > start && /\s/.test(ctx.src[end - 1])) {
|
|
373
|
+
end--;
|
|
374
|
+
}
|
|
375
|
+
return [start, end];
|
|
376
|
+
};
|
|
377
|
+
|
|
378
|
+
/** A plain quoted value as a StringLiteral spanning the trimmed value region. */
|
|
379
|
+
const stringLiteralForText = (ctx, text, regionStart, regionEnd) => {
|
|
380
|
+
const [start, end] = trimmedValueRange(ctx, regionStart, regionEnd);
|
|
381
|
+
return {
|
|
382
|
+
type: "StringLiteral",
|
|
383
|
+
value: text.data,
|
|
384
|
+
start,
|
|
385
|
+
end,
|
|
386
|
+
loc: ctx.locBetween(start, end)
|
|
387
|
+
};
|
|
388
|
+
};
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* Build the TemplateLiteral for a mixed text/expression attribute value. Babel
|
|
392
|
+
* requires `quasis.length === expressions.length + 1`, so empty zero-width
|
|
393
|
+
* quasis are inserted where two expressions are adjacent and at the head/tail
|
|
394
|
+
* when the value starts/ends with an expression.
|
|
395
|
+
*/
|
|
396
|
+
const mixedValueTemplateLiteral = (ctx, parts) => {
|
|
397
|
+
const expressions = [];
|
|
398
|
+
const quasis = [];
|
|
399
|
+
for (const part of parts) {
|
|
400
|
+
if (part.type === "Text") {
|
|
401
|
+
if (quasis.length === expressions.length) {
|
|
402
|
+
// No quasi is pending (start of the value, or an expression was just
|
|
403
|
+
// pushed): this Text opens a new quasi.
|
|
404
|
+
quasis.push(templateElement(ctx, part.data, part.start, part.end, false));
|
|
405
|
+
} else if (part.data.length > 0) {
|
|
406
|
+
// Svelte can split text runs (e.g. around decoded entities); merge the
|
|
407
|
+
// run into the trailing quasi so the arity invariant holds.
|
|
408
|
+
const previous = quasis[quasis.length - 1];
|
|
409
|
+
previous.value = {
|
|
410
|
+
raw: previous.value.raw + part.data,
|
|
411
|
+
cooked: previous.value.cooked + part.data
|
|
412
|
+
};
|
|
413
|
+
previous.end = part.end;
|
|
414
|
+
previous.loc = ctx.locBetween(previous.start, part.end);
|
|
415
|
+
}
|
|
416
|
+
} else if (part.type === "ExpressionTag") {
|
|
417
|
+
if (quasis.length === expressions.length) {
|
|
418
|
+
// Two adjacent expressions (or a leading one): insert an empty
|
|
419
|
+
// separator quasi at this position.
|
|
420
|
+
quasis.push(templateElement(ctx, "", part.start, part.start, false));
|
|
421
|
+
}
|
|
422
|
+
expressions.push(subExpr(ctx, part.expression.start, part.expression.end));
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
if (quasis.length === expressions.length) {
|
|
426
|
+
// The value ends with an expression: close with an empty tail quasi. Its
|
|
427
|
+
// position - and the literal's overall end - is the last *part*'s end (an
|
|
428
|
+
// ExpressionTag's end includes its closing `}`); using the inner
|
|
429
|
+
// expression's end would truncate the literal by one character.
|
|
430
|
+
const at = parts[parts.length - 1].end;
|
|
431
|
+
quasis.push(templateElement(ctx, "", at, at, false));
|
|
432
|
+
}
|
|
433
|
+
quasis[quasis.length - 1].tail = true;
|
|
434
|
+
const start = quasis[0].start;
|
|
435
|
+
const end = parts[parts.length - 1].end;
|
|
436
|
+
return {
|
|
437
|
+
type: "TemplateLiteral",
|
|
438
|
+
quasis,
|
|
439
|
+
expressions,
|
|
440
|
+
start,
|
|
441
|
+
end,
|
|
442
|
+
loc: ctx.locBetween(start, end)
|
|
443
|
+
};
|
|
444
|
+
};
|
|
445
|
+
|
|
446
|
+
const templateElement = (ctx, raw, start, end, tail) => ({
|
|
447
|
+
type: "TemplateElement",
|
|
448
|
+
start,
|
|
449
|
+
end,
|
|
450
|
+
tail,
|
|
451
|
+
value: { raw, cooked: raw },
|
|
452
|
+
loc: ctx.locBetween(start, end)
|
|
453
|
+
});
|
|
454
|
+
|
|
455
|
+
/**
|
|
456
|
+
* A JSXExpressionContainer that spans a `{...}` tag's brace range and wraps the
|
|
457
|
+
* sub-parsed inner expression. `tag` supplies the outer range (which for
|
|
458
|
+
* shorthand attributes is narrower than the braces themselves); `expression`
|
|
459
|
+
* supplies the inner range.
|
|
460
|
+
*/
|
|
461
|
+
const expressionTagContainer = (ctx, tag, expression) => {
|
|
462
|
+
const inner = subExpr(ctx, expression.start, expression.end);
|
|
463
|
+
return {
|
|
464
|
+
type: "JSXExpressionContainer",
|
|
465
|
+
start: tag.start,
|
|
466
|
+
end: tag.end,
|
|
467
|
+
loc: ctx.locBetween(tag.start, tag.end),
|
|
468
|
+
expression: inner
|
|
469
|
+
};
|
|
470
|
+
};
|
|
471
|
+
|
|
472
|
+
/** Container whose range is exactly the braces around [start, end). */
|
|
473
|
+
const bracedContainer = (ctx, expressionNode, start, end) => ({
|
|
474
|
+
type: "JSXExpressionContainer",
|
|
475
|
+
start,
|
|
476
|
+
end,
|
|
477
|
+
loc: ctx.locBetween(start, end),
|
|
478
|
+
expression: expressionNode
|
|
479
|
+
});
|
|
480
|
+
|
|
481
|
+
/** Zero-width node at `offset` - for invented identifiers and markers. */
|
|
482
|
+
const zeroWidth = (ctx, offset, fields) => ({
|
|
483
|
+
start: offset,
|
|
484
|
+
end: offset,
|
|
485
|
+
loc: ctx.locBetween(offset, offset),
|
|
486
|
+
...fields
|
|
487
|
+
});
|
|
488
|
+
|
|
489
|
+
/**
|
|
490
|
+
* Directives (`on:click={h}`, `bind:value={q}`, ...) become JSXAttributes whose
|
|
491
|
+
* name is a JSXNamespacedName, which Babel's JSX parser produces natively for
|
|
492
|
+
* `on:click`-style names. Unlike the Vue masking path nothing is rewritten, so
|
|
493
|
+
* the attribute's [start, end) slices to the directive's exact source text -
|
|
494
|
+
* including modifiers such as `on:click|preventDefault`.
|
|
495
|
+
*/
|
|
496
|
+
const mapDirective = (ctx, node) => {
|
|
497
|
+
const prefix = directivePrefix(ctx, node);
|
|
498
|
+
const nameStart = node.start;
|
|
499
|
+
const colon = ctx.src.indexOf(":", nameStart);
|
|
500
|
+
// The local name stops at the first modifier pipe, `=`, `{` or whitespace.
|
|
501
|
+
const afterColon = ctx.slice(colon + 1, node.end);
|
|
502
|
+
const stop = afterColon.search(/[\s={|]/);
|
|
503
|
+
const nameEnd =
|
|
504
|
+
stop === -1 ? colon + 1 + node.name.length : colon + 1 + stop;
|
|
505
|
+
const name = {
|
|
506
|
+
type: "JSXNamespacedName",
|
|
507
|
+
namespace: {
|
|
508
|
+
type: "JSXIdentifier",
|
|
509
|
+
name: prefix,
|
|
510
|
+
start: nameStart,
|
|
511
|
+
end: colon,
|
|
512
|
+
loc: ctx.locBetween(nameStart, colon)
|
|
513
|
+
},
|
|
514
|
+
name: {
|
|
515
|
+
type: "JSXIdentifier",
|
|
516
|
+
name: node.name,
|
|
517
|
+
start: colon + 1,
|
|
518
|
+
end: nameEnd,
|
|
519
|
+
loc: ctx.locBetween(colon + 1, nameEnd)
|
|
520
|
+
},
|
|
521
|
+
start: nameStart,
|
|
522
|
+
end: nameEnd,
|
|
523
|
+
loc: ctx.locBetween(nameStart, nameEnd)
|
|
524
|
+
};
|
|
525
|
+
return {
|
|
526
|
+
type: "JSXAttribute",
|
|
527
|
+
start: node.start,
|
|
528
|
+
end: node.end,
|
|
529
|
+
loc: ctx.locBetween(node.start, node.end),
|
|
530
|
+
svelteKind: node.type,
|
|
531
|
+
name,
|
|
532
|
+
value: directiveValue(ctx, node)
|
|
533
|
+
};
|
|
534
|
+
};
|
|
535
|
+
|
|
536
|
+
/**
|
|
537
|
+
* `transition:`/`in:`/`out:` share one Svelte node type; the actual keyword is
|
|
538
|
+
* only in the source text, so read it back rather than guessing.
|
|
539
|
+
*/
|
|
540
|
+
const directivePrefix = (ctx, node) => {
|
|
541
|
+
if (node.type === "TransitionDirective") {
|
|
542
|
+
for (const prefix of TRANSITION_DIRECTIVE_PREFIXES) {
|
|
543
|
+
if (ctx.src.startsWith(prefix, node.start)) {
|
|
544
|
+
return prefix;
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
return {
|
|
549
|
+
OnDirective: "on",
|
|
550
|
+
BindDirective: "bind",
|
|
551
|
+
ClassDirective: "class",
|
|
552
|
+
StyleDirective: "style",
|
|
553
|
+
UseDirective: "use",
|
|
554
|
+
AnimateDirective: "animate",
|
|
555
|
+
LetDirective: "let"
|
|
556
|
+
}[node.type];
|
|
557
|
+
};
|
|
558
|
+
|
|
559
|
+
const directiveValue = (ctx, node) => {
|
|
560
|
+
if (node.type === "LetDirective") {
|
|
561
|
+
// `let:` values are binding patterns (`let:item`, `let:{a, b}`).
|
|
562
|
+
if (!node.expression) {
|
|
563
|
+
return null;
|
|
564
|
+
}
|
|
565
|
+
return bracedContainer(
|
|
566
|
+
ctx,
|
|
567
|
+
subPattern(ctx, node.expression.start, node.expression.end),
|
|
568
|
+
node.expression.start - 1,
|
|
569
|
+
node.expression.end + 1
|
|
570
|
+
);
|
|
571
|
+
}
|
|
572
|
+
if (node.type === "StyleDirective") {
|
|
573
|
+
// StyleDirective keeps its payload in `value`, with the same shapes an
|
|
574
|
+
// Attribute value can have.
|
|
575
|
+
return mapAttributeValue(ctx, node.value, nameLocEnd(node) + 1, node.end);
|
|
576
|
+
}
|
|
577
|
+
if (!node.expression) {
|
|
578
|
+
return null;
|
|
579
|
+
}
|
|
580
|
+
return bracedContainer(
|
|
581
|
+
ctx,
|
|
582
|
+
subExpr(ctx, node.expression.start, node.expression.end),
|
|
583
|
+
node.expression.start - 1,
|
|
584
|
+
node.expression.end + 1
|
|
585
|
+
);
|
|
586
|
+
};
|
|
587
|
+
|
|
588
|
+
/** `Attribute` -> JSXAttribute; `SpreadAttribute` -> JSXSpreadAttribute. */
|
|
589
|
+
const mapAttribute = (ctx, node) => {
|
|
590
|
+
if (node.type === "SpreadAttribute") {
|
|
591
|
+
return {
|
|
592
|
+
type: "JSXSpreadAttribute",
|
|
593
|
+
start: node.start,
|
|
594
|
+
end: node.end,
|
|
595
|
+
loc: ctx.locBetween(node.start, node.end),
|
|
596
|
+
svelteKind: "SpreadAttribute",
|
|
597
|
+
argument: subExpr(ctx, node.expression.start, node.expression.end)
|
|
598
|
+
};
|
|
599
|
+
}
|
|
600
|
+
if (node.type === "AttachTag") {
|
|
601
|
+
// `{@attach fn}` rides along in the attributes array in Svelte's AST.
|
|
602
|
+
const name = zeroWidth(ctx, node.start, {
|
|
603
|
+
type: "JSXIdentifier",
|
|
604
|
+
name: "attach"
|
|
605
|
+
});
|
|
606
|
+
return {
|
|
607
|
+
type: "JSXAttribute",
|
|
608
|
+
start: node.start,
|
|
609
|
+
end: node.end,
|
|
610
|
+
loc: ctx.locBetween(node.start, node.end),
|
|
611
|
+
svelteKind: "AttachTag",
|
|
612
|
+
name,
|
|
613
|
+
value: node.expression
|
|
614
|
+
? bracedContainer(
|
|
615
|
+
ctx,
|
|
616
|
+
subExpr(ctx, node.expression.start, node.expression.end),
|
|
617
|
+
node.start,
|
|
618
|
+
node.end
|
|
619
|
+
)
|
|
620
|
+
: null
|
|
621
|
+
};
|
|
622
|
+
}
|
|
623
|
+
if (node.type === "Attribute") {
|
|
624
|
+
return {
|
|
625
|
+
type: "JSXAttribute",
|
|
626
|
+
start: node.start,
|
|
627
|
+
end: node.end,
|
|
628
|
+
loc: ctx.locBetween(node.start, node.end),
|
|
629
|
+
svelteKind: "Attribute",
|
|
630
|
+
name: jsxName(ctx, node.name, nameLocStart(node), nameLocEnd(node)),
|
|
631
|
+
value: mapAttributeValue(ctx, node.value, nameLocEnd(node) + 1, node.end)
|
|
632
|
+
};
|
|
633
|
+
}
|
|
634
|
+
return mapDirective(ctx, node);
|
|
635
|
+
};
|
|
636
|
+
|
|
637
|
+
// ---------------------------------------------------------------------------
|
|
638
|
+
// Elements
|
|
639
|
+
// ---------------------------------------------------------------------------
|
|
640
|
+
|
|
641
|
+
/**
|
|
642
|
+
* Every element-like Svelte node (`RegularElement`, `Component`, `SlotElement`,
|
|
643
|
+
* `TitleElement`, `SvelteHead`, `SvelteWindow`, `SvelteBody`,
|
|
644
|
+
* `SvelteDocument`, `SvelteFragment`, `SvelteSelf`, `SvelteBoundary`,
|
|
645
|
+
* `SvelteOptions`, `SvelteElement`, `SvelteComponent`) maps to a JSXElement.
|
|
646
|
+
*/
|
|
647
|
+
const mapElement = (ctx, node) => {
|
|
648
|
+
const attributes = node.attributes?.map((a) => mapAttribute(ctx, a)) ?? [];
|
|
649
|
+
// `<svelte:element this={...}>` and `<svelte:component this={...}>` keep their
|
|
650
|
+
// dynamic tag outside `attributes`; surface it as a `this` attribute so the
|
|
651
|
+
// expression is not lost.
|
|
652
|
+
const dynamicThis = node.type === "SvelteElement" ? node.tag : node.type === "SvelteComponent" ? node.expression : null;
|
|
653
|
+
if (dynamicThis) {
|
|
654
|
+
attributes.unshift(thisAttribute(ctx, node, dynamicThis));
|
|
655
|
+
}
|
|
656
|
+
const openEnd = openTagEnd(ctx, node);
|
|
657
|
+
const closeRange = closeTagRange(ctx, node);
|
|
658
|
+
const children = (node.fragment?.nodes ?? [])
|
|
659
|
+
.map((child) => mapChild(ctx, child))
|
|
660
|
+
.flat()
|
|
661
|
+
.filter(Boolean);
|
|
662
|
+
return {
|
|
663
|
+
type: "JSXElement",
|
|
664
|
+
start: node.start,
|
|
665
|
+
end: node.end,
|
|
666
|
+
loc: ctx.locBetween(node.start, node.end),
|
|
667
|
+
svelteKind: node.type,
|
|
668
|
+
svelteName: node.name,
|
|
669
|
+
openingElement: {
|
|
670
|
+
type: "JSXOpeningElement",
|
|
671
|
+
start: node.start,
|
|
672
|
+
end: openEnd,
|
|
673
|
+
loc: ctx.locBetween(node.start, openEnd),
|
|
674
|
+
svelteKind: node.type,
|
|
675
|
+
svelteName: node.name,
|
|
676
|
+
name: jsxName(ctx, node.name, nameLocStart(node), nameLocEnd(node)),
|
|
677
|
+
attributes,
|
|
678
|
+
selfClosing: isSelfClosing(ctx, node)
|
|
679
|
+
},
|
|
680
|
+
closingElement: closeRange
|
|
681
|
+
? {
|
|
682
|
+
type: "JSXClosingElement",
|
|
683
|
+
start: closeRange.start,
|
|
684
|
+
end: closeRange.end,
|
|
685
|
+
loc: ctx.locBetween(closeRange.start, closeRange.end),
|
|
686
|
+
svelteKind: node.type,
|
|
687
|
+
svelteName: node.name,
|
|
688
|
+
name: jsxName(ctx, node.name, closeRange.start + 2, closeRange.end - 1)
|
|
689
|
+
}
|
|
690
|
+
: null,
|
|
691
|
+
children
|
|
692
|
+
};
|
|
693
|
+
};
|
|
694
|
+
|
|
695
|
+
/** Synthesized `this={expr}` attribute for svelte:element / svelte:component. */
|
|
696
|
+
const thisAttribute = (ctx, node, expression) => {
|
|
697
|
+
const openEnd = openTagEnd(ctx, node);
|
|
698
|
+
const nameStart = nameLocEnd(node);
|
|
699
|
+
const thisIndex = ctx.slice(nameStart, openEnd).indexOf("this");
|
|
700
|
+
const attrStart = thisIndex === -1 ? nameStart : nameStart + thisIndex;
|
|
701
|
+
const attrEnd = expression.end + 1;
|
|
702
|
+
return {
|
|
703
|
+
type: "JSXAttribute",
|
|
704
|
+
start: attrStart,
|
|
705
|
+
end: attrEnd,
|
|
706
|
+
loc: ctx.locBetween(attrStart, attrEnd),
|
|
707
|
+
svelteKind: node.type,
|
|
708
|
+
name: {
|
|
709
|
+
type: "JSXIdentifier",
|
|
710
|
+
name: "this",
|
|
711
|
+
start: attrStart,
|
|
712
|
+
end: attrStart + (thisIndex === -1 ? 0 : 4),
|
|
713
|
+
loc: ctx.locBetween(attrStart, attrStart + (thisIndex === -1 ? 0 : 4))
|
|
714
|
+
},
|
|
715
|
+
value: bracedContainer(
|
|
716
|
+
ctx,
|
|
717
|
+
subExpr(ctx, expression.start, expression.end),
|
|
718
|
+
expression.start - 1,
|
|
719
|
+
expression.end + 1
|
|
720
|
+
)
|
|
721
|
+
};
|
|
722
|
+
};
|
|
723
|
+
|
|
724
|
+
// ---------------------------------------------------------------------------
|
|
725
|
+
// Template children
|
|
726
|
+
// ---------------------------------------------------------------------------
|
|
727
|
+
|
|
728
|
+
/**
|
|
729
|
+
* Map one template child onto Babel JSX. Returns a node, an array of nodes
|
|
730
|
+
* (blocks that emit siblings), or null for dropped content (comments).
|
|
731
|
+
*/
|
|
732
|
+
const mapChild = (ctx, node) => {
|
|
733
|
+
switch (node.type) {
|
|
734
|
+
case "Text":
|
|
735
|
+
return {
|
|
736
|
+
type: "JSXText",
|
|
737
|
+
value: node.data,
|
|
738
|
+
start: node.start,
|
|
739
|
+
end: node.end,
|
|
740
|
+
loc: ctx.locBetween(node.start, node.end)
|
|
741
|
+
};
|
|
742
|
+
case "Comment":
|
|
743
|
+
// HTML comments carry no code semantics; dropped by design.
|
|
744
|
+
return null;
|
|
745
|
+
case "ExpressionTag":
|
|
746
|
+
return { ...expressionTagContainer(ctx, node, node.expression), svelteKind: "ExpressionTag" };
|
|
747
|
+
case "HtmlTag":
|
|
748
|
+
return {
|
|
749
|
+
...expressionTagContainer(ctx, node, node.expression),
|
|
750
|
+
svelteKind: "HtmlTag"
|
|
751
|
+
};
|
|
752
|
+
case "RenderTag":
|
|
753
|
+
return {
|
|
754
|
+
type: "JSXExpressionContainer",
|
|
755
|
+
start: node.start,
|
|
756
|
+
end: node.end,
|
|
757
|
+
loc: ctx.locBetween(node.start, node.end),
|
|
758
|
+
svelteKind: "RenderTag",
|
|
759
|
+
expression: node.expression
|
|
760
|
+
? subExpr(ctx, node.expression.start, node.expression.end)
|
|
761
|
+
: zeroWidth(ctx, node.start, { type: "JSXEmptyExpression" })
|
|
762
|
+
};
|
|
763
|
+
case "DebugTag":
|
|
764
|
+
return debugTagContainer(ctx, node);
|
|
765
|
+
case "ConstTag":
|
|
766
|
+
return constTagContainer(ctx, node);
|
|
767
|
+
case "IfBlock":
|
|
768
|
+
return ifBlockContainer(ctx, node);
|
|
769
|
+
case "EachBlock":
|
|
770
|
+
return eachBlockNodes(ctx, node);
|
|
771
|
+
case "AwaitBlock":
|
|
772
|
+
return awaitBlockNodes(ctx, node);
|
|
773
|
+
case "KeyBlock":
|
|
774
|
+
return keyBlockNodes(ctx, node);
|
|
775
|
+
case "SnippetBlock":
|
|
776
|
+
return snippetBlockContainer(ctx, node);
|
|
777
|
+
default:
|
|
778
|
+
// Remaining child types are the element-like nodes (and any future
|
|
779
|
+
// element kind, which still has a name and a fragment).
|
|
780
|
+
if (node.fragment || node.name_loc) {
|
|
781
|
+
return mapElement(ctx, node);
|
|
782
|
+
}
|
|
783
|
+
return null;
|
|
784
|
+
}
|
|
785
|
+
};
|
|
786
|
+
|
|
787
|
+
/**
|
|
788
|
+
* `{@debug a, b}` -> a container over a SequenceExpression of the identifiers
|
|
789
|
+
* (or the bare identifier when there is only one).
|
|
790
|
+
*/
|
|
791
|
+
const debugTagContainer = (ctx, node) => {
|
|
792
|
+
const identifiers = node.identifiers ?? [];
|
|
793
|
+
const start = identifiers[0]?.start ?? node.start;
|
|
794
|
+
const end = identifiers[identifiers.length - 1]?.end ?? node.end;
|
|
795
|
+
const parsed = identifiers.map((i) => subExpr(ctx, i.start, i.end));
|
|
796
|
+
const expression =
|
|
797
|
+
parsed.length > 1
|
|
798
|
+
? {
|
|
799
|
+
type: "SequenceExpression",
|
|
800
|
+
expressions: parsed,
|
|
801
|
+
start,
|
|
802
|
+
end,
|
|
803
|
+
loc: ctx.locBetween(start, end)
|
|
804
|
+
}
|
|
805
|
+
: parsed[0] ?? zeroWidth(ctx, node.start, { type: "JSXEmptyExpression" });
|
|
806
|
+
return {
|
|
807
|
+
type: "JSXExpressionContainer",
|
|
808
|
+
start: node.start,
|
|
809
|
+
end: node.end,
|
|
810
|
+
loc: ctx.locBetween(node.start, node.end),
|
|
811
|
+
svelteKind: "DebugTag",
|
|
812
|
+
expression
|
|
813
|
+
};
|
|
814
|
+
};
|
|
815
|
+
|
|
816
|
+
/**
|
|
817
|
+
* `{@const label = expr}` -> a container over `pattern = expr`. A declaration
|
|
818
|
+
* is not legal in a JSX child position, so the binding is modelled as an
|
|
819
|
+
* assignment; the trade-off (the binding looks like an implicit global
|
|
820
|
+
* downstream) is documented in docs/ASTGEN.md.
|
|
821
|
+
*/
|
|
822
|
+
const constTagContainer = (ctx, node) => {
|
|
823
|
+
const declaration = node.declaration;
|
|
824
|
+
const declarator = declaration?.declarations?.[0];
|
|
825
|
+
if (!declarator) {
|
|
826
|
+
return null;
|
|
827
|
+
}
|
|
828
|
+
return {
|
|
829
|
+
type: "JSXExpressionContainer",
|
|
830
|
+
start: node.start,
|
|
831
|
+
end: node.end,
|
|
832
|
+
loc: ctx.locBetween(node.start, node.end),
|
|
833
|
+
svelteKind: "ConstTag",
|
|
834
|
+
expression: {
|
|
835
|
+
type: "AssignmentExpression",
|
|
836
|
+
operator: "=",
|
|
837
|
+
start: declaration.start,
|
|
838
|
+
end: declaration.end,
|
|
839
|
+
loc: ctx.locBetween(declaration.start, declaration.end),
|
|
840
|
+
left: subPattern(ctx, declarator.id.start, declarator.id.end),
|
|
841
|
+
right: subExpr(ctx, declarator.init.start, declarator.init.end)
|
|
842
|
+
}
|
|
843
|
+
};
|
|
844
|
+
};
|
|
845
|
+
|
|
846
|
+
/**
|
|
847
|
+
* `{#if test}A{:else if t2}B{:else}C{/if}` -> a container over a chain of
|
|
848
|
+
* ConditionalExpressions. An `{:else if}` alternate holds exactly one nested
|
|
849
|
+
* IfBlock; that block's conditional is spliced in directly so the chain is a
|
|
850
|
+
* proper `a ? ... : b ? ... : ...` nesting rather than a fragment wrapper.
|
|
851
|
+
*/
|
|
852
|
+
const ifBlockContainer = (ctx, node) => ({
|
|
853
|
+
type: "JSXExpressionContainer",
|
|
854
|
+
start: node.start,
|
|
855
|
+
end: node.end,
|
|
856
|
+
loc: ctx.locBetween(node.start, node.end),
|
|
857
|
+
svelteKind: "IfBlock",
|
|
858
|
+
expression: ifBlockConditional(ctx, node)
|
|
859
|
+
});
|
|
860
|
+
|
|
861
|
+
const ifBlockConditional = (ctx, node) => {
|
|
862
|
+
let alternate;
|
|
863
|
+
const alternateNodes = node.alternate?.nodes ?? [];
|
|
864
|
+
const nestedElseIf =
|
|
865
|
+
alternateNodes.length === 1 &&
|
|
866
|
+
alternateNodes[0].type === "IfBlock" &&
|
|
867
|
+
alternateNodes[0].elseif === true;
|
|
868
|
+
if (!node.alternate) {
|
|
869
|
+
alternate = zeroWidth(ctx, node.end, { type: "NullLiteral" });
|
|
870
|
+
} else if (nestedElseIf) {
|
|
871
|
+
alternate = ifBlockConditional(ctx, alternateNodes[0]);
|
|
872
|
+
} else {
|
|
873
|
+
alternate = fragmentToJsx(ctx, node.alternate, node.end);
|
|
874
|
+
}
|
|
875
|
+
return {
|
|
876
|
+
type: "ConditionalExpression",
|
|
877
|
+
start: node.test.start,
|
|
878
|
+
end: node.end,
|
|
879
|
+
loc: ctx.locBetween(node.test.start, node.end),
|
|
880
|
+
test: subExpr(ctx, node.test.start, node.test.end),
|
|
881
|
+
consequent: fragmentToJsx(ctx, node.consequent, node.start),
|
|
882
|
+
alternate
|
|
883
|
+
};
|
|
884
|
+
};
|
|
885
|
+
|
|
886
|
+
/**
|
|
887
|
+
* `{#each list as item, i (key)}body{:else}fallback{/each}` ->
|
|
888
|
+
* a `list.map((item, i) => body)` container. The `{:else}` fallback is
|
|
889
|
+
* deliberately emitted as a second sibling fragment rather than folded into a
|
|
890
|
+
* conditional: re-using `list` in a conditional test would sub-parse the same
|
|
891
|
+
* expression twice and double-count its identifiers downstream.
|
|
892
|
+
*
|
|
893
|
+
* `node.index` is a plain string in Svelte's AST; its source range is located
|
|
894
|
+
* by searching between the context pattern and the key/group end.
|
|
895
|
+
*/
|
|
896
|
+
const eachBlockNodes = (ctx, node) => {
|
|
897
|
+
const mapCall = {
|
|
898
|
+
type: "CallExpression",
|
|
899
|
+
start: node.expression.start,
|
|
900
|
+
end: node.end,
|
|
901
|
+
loc: ctx.locBetween(node.expression.start, node.end),
|
|
902
|
+
callee: {
|
|
903
|
+
type: "MemberExpression",
|
|
904
|
+
computed: false,
|
|
905
|
+
object: subExpr(ctx, node.expression.start, node.expression.end),
|
|
906
|
+
property: zeroWidth(ctx, node.expression.end, {
|
|
907
|
+
type: "Identifier",
|
|
908
|
+
name: "map"
|
|
909
|
+
}),
|
|
910
|
+
start: node.expression.start,
|
|
911
|
+
end: node.expression.end,
|
|
912
|
+
loc: ctx.locBetween(node.expression.start, node.expression.end)
|
|
913
|
+
},
|
|
914
|
+
arguments: [eachArrowFunction(ctx, node)]
|
|
915
|
+
};
|
|
916
|
+
const container = {
|
|
917
|
+
type: "JSXExpressionContainer",
|
|
918
|
+
start: node.start,
|
|
919
|
+
end: node.end,
|
|
920
|
+
loc: ctx.locBetween(node.start, node.end),
|
|
921
|
+
svelteKind: "EachBlock",
|
|
922
|
+
expression: mapCall
|
|
923
|
+
};
|
|
924
|
+
return node.fallback ? [container, fragmentToJsx(ctx, node.fallback, node.end)] : container;
|
|
925
|
+
};
|
|
926
|
+
|
|
927
|
+
const eachArrowFunction = (ctx, node) => {
|
|
928
|
+
const params = [subPattern(ctx, node.context.start, node.context.end)];
|
|
929
|
+
if (node.index) {
|
|
930
|
+
params.push(eachIndexIdentifier(ctx, node));
|
|
931
|
+
}
|
|
932
|
+
const body = fragmentToJsx(ctx, node.body, node.start);
|
|
933
|
+
// The `(key)` expression becomes the first child of the arrow's fragment so
|
|
934
|
+
// its identifiers stay reachable without polluting the map() signature.
|
|
935
|
+
if (node.key) {
|
|
936
|
+
body.children.unshift(
|
|
937
|
+
bracedContainer(
|
|
938
|
+
ctx,
|
|
939
|
+
subExpr(ctx, node.key.start, node.key.end),
|
|
940
|
+
node.key.start - 1,
|
|
941
|
+
node.key.end + 1
|
|
942
|
+
)
|
|
943
|
+
);
|
|
944
|
+
}
|
|
945
|
+
return {
|
|
946
|
+
type: "ArrowFunctionExpression",
|
|
947
|
+
start: node.context.start,
|
|
948
|
+
end: node.end,
|
|
949
|
+
loc: ctx.locBetween(node.context.start, node.end),
|
|
950
|
+
id: null,
|
|
951
|
+
async: false,
|
|
952
|
+
generator: false,
|
|
953
|
+
params,
|
|
954
|
+
body,
|
|
955
|
+
expression: false
|
|
956
|
+
};
|
|
957
|
+
};
|
|
958
|
+
|
|
959
|
+
const eachIndexIdentifier = (ctx, node) => {
|
|
960
|
+
const searchEnd = node.key ? node.key.start : node.end;
|
|
961
|
+
const region = ctx.slice(node.context.end, searchEnd);
|
|
962
|
+
const match = new RegExp(
|
|
963
|
+
`[,\\s]${node.index.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}[\\s({]|[,\\s]${node.index.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}$`
|
|
964
|
+
).exec(region);
|
|
965
|
+
if (match) {
|
|
966
|
+
const start = node.context.end + match.index + 1;
|
|
967
|
+
const end = start + node.index.length;
|
|
968
|
+
return {
|
|
969
|
+
type: "Identifier",
|
|
970
|
+
name: node.index,
|
|
971
|
+
start,
|
|
972
|
+
end,
|
|
973
|
+
loc: ctx.locBetween(start, end)
|
|
974
|
+
};
|
|
975
|
+
}
|
|
976
|
+
return zeroWidth(ctx, node.context.end, {
|
|
977
|
+
type: "Identifier",
|
|
978
|
+
name: node.index
|
|
979
|
+
});
|
|
980
|
+
};
|
|
981
|
+
|
|
982
|
+
/**
|
|
983
|
+
* `{#await expr}pending{:then value}then{:catch error}catch{/await}` ->
|
|
984
|
+
* the pending children emitted as plain preceding siblings (they are static
|
|
985
|
+
* DOM), followed by a container over `expr.then(onFulfilled, onRejected?)`.
|
|
986
|
+
* A `NullLiteral` keeps the rejection handler in argument position when only
|
|
987
|
+
* `{:catch}` is present.
|
|
988
|
+
*/
|
|
989
|
+
const awaitBlockNodes = (ctx, node) => {
|
|
990
|
+
const pendingChildren = (node.pending?.nodes ?? [])
|
|
991
|
+
.map((child) => mapChild(ctx, child))
|
|
992
|
+
.flat()
|
|
993
|
+
.filter(Boolean);
|
|
994
|
+
const argumentsList = [];
|
|
995
|
+
if (node.then) {
|
|
996
|
+
argumentsList.push(awaitHandler(ctx, node.value, node.then, node));
|
|
997
|
+
} else if (node.catch) {
|
|
998
|
+
argumentsList.push(zeroWidth(ctx, node.end, { type: "NullLiteral" }));
|
|
999
|
+
}
|
|
1000
|
+
if (node.catch) {
|
|
1001
|
+
argumentsList.push(awaitHandler(ctx, node.error, node.catch, node));
|
|
1002
|
+
}
|
|
1003
|
+
const call = {
|
|
1004
|
+
type: "CallExpression",
|
|
1005
|
+
start: node.expression.start,
|
|
1006
|
+
end: node.end,
|
|
1007
|
+
loc: ctx.locBetween(node.expression.start, node.end),
|
|
1008
|
+
callee: {
|
|
1009
|
+
type: "MemberExpression",
|
|
1010
|
+
computed: false,
|
|
1011
|
+
object: subExpr(ctx, node.expression.start, node.expression.end),
|
|
1012
|
+
property: zeroWidth(ctx, node.expression.end, {
|
|
1013
|
+
type: "Identifier",
|
|
1014
|
+
name: "then"
|
|
1015
|
+
}),
|
|
1016
|
+
start: node.expression.start,
|
|
1017
|
+
end: node.expression.end,
|
|
1018
|
+
loc: ctx.locBetween(node.expression.start, node.expression.end)
|
|
1019
|
+
},
|
|
1020
|
+
arguments: argumentsList
|
|
1021
|
+
};
|
|
1022
|
+
const container = {
|
|
1023
|
+
type: "JSXExpressionContainer",
|
|
1024
|
+
start: node.start,
|
|
1025
|
+
end: node.end,
|
|
1026
|
+
loc: ctx.locBetween(node.start, node.end),
|
|
1027
|
+
svelteKind: "AwaitBlock",
|
|
1028
|
+
expression: call
|
|
1029
|
+
};
|
|
1030
|
+
return [...pendingChildren, container];
|
|
1031
|
+
};
|
|
1032
|
+
|
|
1033
|
+
const awaitHandler = (ctx, pattern, fragment, owner) => ({
|
|
1034
|
+
type: "ArrowFunctionExpression",
|
|
1035
|
+
start: pattern?.start ?? fragment.nodes[0]?.start ?? owner.start,
|
|
1036
|
+
end: fragment.end ?? owner.end,
|
|
1037
|
+
loc: ctx.locBetween(
|
|
1038
|
+
pattern?.start ?? fragment.nodes[0]?.start ?? owner.start,
|
|
1039
|
+
fragment.end ?? owner.end
|
|
1040
|
+
),
|
|
1041
|
+
id: null,
|
|
1042
|
+
async: false,
|
|
1043
|
+
generator: false,
|
|
1044
|
+
params: pattern ? [subPattern(ctx, pattern.start, pattern.end)] : [],
|
|
1045
|
+
body: fragmentToJsx(ctx, fragment, owner.start),
|
|
1046
|
+
expression: false
|
|
1047
|
+
});
|
|
1048
|
+
|
|
1049
|
+
/**
|
|
1050
|
+
* `{#key expr}fragment{/key}` -> the key expression container followed by the
|
|
1051
|
+
* fragment as a sibling, mirroring how the each-block fallback is emitted.
|
|
1052
|
+
*/
|
|
1053
|
+
const keyBlockNodes = (ctx, node) => [
|
|
1054
|
+
bracedContainer(
|
|
1055
|
+
ctx,
|
|
1056
|
+
subExpr(ctx, node.expression.start, node.expression.end),
|
|
1057
|
+
node.expression.start - 1,
|
|
1058
|
+
node.expression.end + 1
|
|
1059
|
+
),
|
|
1060
|
+
fragmentToJsx(ctx, node.fragment, node.start)
|
|
1061
|
+
];
|
|
1062
|
+
|
|
1063
|
+
/**
|
|
1064
|
+
* `{#snippet name(params)}body{/snippet}` -> an assignment of an arrow function
|
|
1065
|
+
* to the snippet's name, so the name binding and the closure body are both
|
|
1066
|
+
* visible downstream.
|
|
1067
|
+
*/
|
|
1068
|
+
const snippetBlockContainer = (ctx, node) => ({
|
|
1069
|
+
type: "JSXExpressionContainer",
|
|
1070
|
+
start: node.start,
|
|
1071
|
+
end: node.end,
|
|
1072
|
+
loc: ctx.locBetween(node.start, node.end),
|
|
1073
|
+
svelteKind: "SnippetBlock",
|
|
1074
|
+
svelteName: node.expression?.name,
|
|
1075
|
+
expression: {
|
|
1076
|
+
type: "AssignmentExpression",
|
|
1077
|
+
operator: "=",
|
|
1078
|
+
start: node.expression.start,
|
|
1079
|
+
end: node.end,
|
|
1080
|
+
loc: ctx.locBetween(node.expression.start, node.end),
|
|
1081
|
+
left: {
|
|
1082
|
+
type: "Identifier",
|
|
1083
|
+
name: node.expression.name,
|
|
1084
|
+
start: node.expression.start,
|
|
1085
|
+
end: node.expression.end,
|
|
1086
|
+
loc: ctx.locBetween(node.expression.start, node.expression.end)
|
|
1087
|
+
},
|
|
1088
|
+
right: {
|
|
1089
|
+
type: "ArrowFunctionExpression",
|
|
1090
|
+
start: node.expression.start,
|
|
1091
|
+
end: node.end,
|
|
1092
|
+
loc: ctx.locBetween(node.expression.start, node.end),
|
|
1093
|
+
id: null,
|
|
1094
|
+
async: false,
|
|
1095
|
+
generator: false,
|
|
1096
|
+
params: (node.parameters ?? []).map((p) =>
|
|
1097
|
+
subPattern(ctx, p.start, p.end)
|
|
1098
|
+
),
|
|
1099
|
+
body: fragmentToJsx(ctx, node.body, node.start),
|
|
1100
|
+
expression: false
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
1103
|
+
});
|
|
1104
|
+
|
|
1105
|
+
/**
|
|
1106
|
+
* A Svelte fragment (a list of sibling template nodes) becomes a JSXFragment
|
|
1107
|
+
* spanning its first..last mapped child. Empty fragments get a zero-width
|
|
1108
|
+
* range at `fallbackOffset` (the enclosing block's start/end) so consumers
|
|
1109
|
+
* never see an unset position. Takes already-mapped children so a fragment is
|
|
1110
|
+
* never mapped twice (sub-parses have side effects on the error list).
|
|
1111
|
+
*/
|
|
1112
|
+
const jsxFragmentFromChildren = (ctx, children, fallbackOffset) => {
|
|
1113
|
+
const start = children.length
|
|
1114
|
+
? children[0].start
|
|
1115
|
+
: (fallbackOffset ?? 0);
|
|
1116
|
+
const end = children.length
|
|
1117
|
+
? children[children.length - 1].end
|
|
1118
|
+
: (fallbackOffset ?? 0);
|
|
1119
|
+
return {
|
|
1120
|
+
type: "JSXFragment",
|
|
1121
|
+
start,
|
|
1122
|
+
end,
|
|
1123
|
+
loc: ctx.locBetween(start, end),
|
|
1124
|
+
svelteKind: "Fragment",
|
|
1125
|
+
openingFragment: {
|
|
1126
|
+
type: "JSXOpeningFragment",
|
|
1127
|
+
start,
|
|
1128
|
+
end: start,
|
|
1129
|
+
loc: ctx.locBetween(start, start),
|
|
1130
|
+
svelteKind: "Fragment"
|
|
1131
|
+
},
|
|
1132
|
+
closingFragment: {
|
|
1133
|
+
type: "JSXClosingFragment",
|
|
1134
|
+
start: end,
|
|
1135
|
+
end,
|
|
1136
|
+
loc: ctx.locBetween(end, end),
|
|
1137
|
+
svelteKind: "Fragment"
|
|
1138
|
+
},
|
|
1139
|
+
children
|
|
1140
|
+
};
|
|
1141
|
+
};
|
|
1142
|
+
|
|
1143
|
+
const fragmentToJsx = (ctx, fragment, fallbackOffset) =>
|
|
1144
|
+
jsxFragmentFromChildren(
|
|
1145
|
+
ctx,
|
|
1146
|
+
(fragment?.nodes ?? [])
|
|
1147
|
+
.map((child) => mapChild(ctx, child))
|
|
1148
|
+
.flat()
|
|
1149
|
+
.filter(Boolean),
|
|
1150
|
+
fallbackOffset
|
|
1151
|
+
);
|
|
1152
|
+
|
|
1153
|
+
// ---------------------------------------------------------------------------
|
|
1154
|
+
// File assembly
|
|
1155
|
+
// ---------------------------------------------------------------------------
|
|
1156
|
+
|
|
1157
|
+
/**
|
|
1158
|
+
* Build the masked buffer for script parsing: a same-length copy of the file
|
|
1159
|
+
* where only the script bodies keep their bytes. Newlines survive so line
|
|
1160
|
+
* numbers are preserved for every other region.
|
|
1161
|
+
*/
|
|
1162
|
+
const maskedScriptBuffer = (root, src) => {
|
|
1163
|
+
const buffer = src.replace(/[^\r\n]/g, " ").split("");
|
|
1164
|
+
for (const script of [root.instance, root.module].filter(Boolean)) {
|
|
1165
|
+
const content = script.content;
|
|
1166
|
+
for (let i = content.start; i < content.end; i++) {
|
|
1167
|
+
buffer[i] = src[i];
|
|
1168
|
+
}
|
|
1169
|
+
}
|
|
1170
|
+
return buffer.join("");
|
|
1171
|
+
};
|
|
1172
|
+
|
|
1173
|
+
/**
|
|
1174
|
+
* Parse a `.svelte` source into a Babel `File` AST with absolute offsets.
|
|
1175
|
+
* Throws if `svelte/compiler` cannot segment the file (the caller falls back
|
|
1176
|
+
* to the legacy masking parser) or if the script buffer cannot be parsed.
|
|
1177
|
+
*
|
|
1178
|
+
* @param {string} file absolute path, used for sourceFilename
|
|
1179
|
+
* @param {string} src file content
|
|
1180
|
+
* @param {object} options Babel parser options shared with astgen.js
|
|
1181
|
+
*/
|
|
1182
|
+
const assembleFile = (ctx, src, body) => ({
|
|
1183
|
+
type: "File",
|
|
1184
|
+
start: 0,
|
|
1185
|
+
end: src.length,
|
|
1186
|
+
loc: { start: ctx.posOf(0), end: ctx.posOf(src.length) },
|
|
1187
|
+
errors: ctx.errors,
|
|
1188
|
+
comments: [],
|
|
1189
|
+
program: {
|
|
1190
|
+
type: "Program",
|
|
1191
|
+
start: 0,
|
|
1192
|
+
end: src.length,
|
|
1193
|
+
loc: { start: ctx.posOf(0), end: ctx.posOf(src.length) },
|
|
1194
|
+
sourceType: "module",
|
|
1195
|
+
interpreter: null,
|
|
1196
|
+
directives: [],
|
|
1197
|
+
body
|
|
1198
|
+
}
|
|
1199
|
+
});
|
|
1200
|
+
|
|
1201
|
+
export const parseSvelteFile = (file, src, options) => {
|
|
1202
|
+
const root = svelteCompilerParse(src, {
|
|
1203
|
+
modern: true,
|
|
1204
|
+
filename: file
|
|
1205
|
+
});
|
|
1206
|
+
const babelOptions = { ...options, sourceFilename: file };
|
|
1207
|
+
const errors = [];
|
|
1208
|
+
const ctx = new SvelteParseContext(file, src, babelOptions, errors);
|
|
1209
|
+
|
|
1210
|
+
// Step A: script statements via one Babel parse of the masked buffer. Both
|
|
1211
|
+
// the instance and module scripts land in the same Program, flattened; the
|
|
1212
|
+
// distinction is lost and documented.
|
|
1213
|
+
const scriptFile = babelParse(maskedScriptBuffer(root, src), babelOptions);
|
|
1214
|
+
if (Array.isArray(scriptFile.errors)) {
|
|
1215
|
+
errors.push(...scriptFile.errors);
|
|
1216
|
+
}
|
|
1217
|
+
const scriptStatements = scriptFile.program.body;
|
|
1218
|
+
relocTree(ctx, scriptStatements);
|
|
1219
|
+
|
|
1220
|
+
// Step B: the template as one JSXFragment expression statement appended
|
|
1221
|
+
// after the scripts; body is sorted by start because `<script>` may legally
|
|
1222
|
+
// follow the markup. A template with no mapped content (or whitespace text
|
|
1223
|
+
// only) contributes nothing.
|
|
1224
|
+
const templateChildren = (root.fragment?.nodes ?? [])
|
|
1225
|
+
.map((child) => mapChild(ctx, child))
|
|
1226
|
+
.flat()
|
|
1227
|
+
.filter(Boolean);
|
|
1228
|
+
const hasTemplateContent = templateChildren.some(
|
|
1229
|
+
(child) => !(child.type === "JSXText" && child.value.trim() === "")
|
|
1230
|
+
);
|
|
1231
|
+
// Script statements keep source order among themselves; the template statement is
|
|
1232
|
+
// always appended last, never sorted in by offset.
|
|
1233
|
+
//
|
|
1234
|
+
// A component's markup renders after its instance script has run - Svelte hoists the
|
|
1235
|
+
// script regardless of where the `<script>` tag sits textually - so "template last" is
|
|
1236
|
+
// the execution order. Sorting by offset instead was actively wrong: the root fragment's
|
|
1237
|
+
// first child is whatever text precedes `<script>`, so a single leading newline gave the
|
|
1238
|
+
// template statement start=0 and placed it ahead of every script statement. Downstream
|
|
1239
|
+
// that inverts the CFG (method entry -> template -> script), which means no script-side
|
|
1240
|
+
// definition reaches a template use and every template dataflow query silently returns
|
|
1241
|
+
// nothing.
|
|
1242
|
+
const body = [...scriptStatements].sort((a, b) => a.start - b.start);
|
|
1243
|
+
if (hasTemplateContent) {
|
|
1244
|
+
const start = templateChildren[0].start;
|
|
1245
|
+
const end = templateChildren[templateChildren.length - 1].end;
|
|
1246
|
+
body.push({
|
|
1247
|
+
type: "ExpressionStatement",
|
|
1248
|
+
start,
|
|
1249
|
+
end,
|
|
1250
|
+
loc: ctx.locBetween(start, end),
|
|
1251
|
+
expression: jsxFragmentFromChildren(ctx, templateChildren, 0)
|
|
1252
|
+
});
|
|
1253
|
+
}
|
|
1254
|
+
|
|
1255
|
+
return assembleFile(ctx, src, body);
|
|
1256
|
+
};
|
|
1257
|
+
|
|
1258
|
+
/**
|
|
1259
|
+
* Fallback for files `svelte/compiler` rejects outright: parse only the script
|
|
1260
|
+
* blocks, over a position-preserving masked buffer (every byte outside the
|
|
1261
|
+
* `<script>` contents blanked to a space, newlines kept - the same masking
|
|
1262
|
+
* astgen.js applies to build virtual type sources). Because nothing moves, the
|
|
1263
|
+
* statement offsets are absolute byte positions into the original file, so a
|
|
1264
|
+
* scanner keeps correct line numbers for the script even though the template
|
|
1265
|
+
* is unrecoverable. The whole-file failure is recorded on `File.errors`.
|
|
1266
|
+
*
|
|
1267
|
+
* @param {string} file absolute path, used for sourceFilename
|
|
1268
|
+
* @param {string} src original file content (for loc computation)
|
|
1269
|
+
* @param {string} maskedSource same-length buffer with script contents verbatim
|
|
1270
|
+
* @param {object} options Babel parser options shared with astgen.js
|
|
1271
|
+
* @param {string} parseErrorMessage the svelte/compiler failure to record
|
|
1272
|
+
*/
|
|
1273
|
+
export const parseSvelteScriptBuffer = (
|
|
1274
|
+
file,
|
|
1275
|
+
src,
|
|
1276
|
+
maskedSource,
|
|
1277
|
+
options,
|
|
1278
|
+
parseErrorMessage
|
|
1279
|
+
) => {
|
|
1280
|
+
const babelOptions = { ...options, sourceFilename: file };
|
|
1281
|
+
const errors = [];
|
|
1282
|
+
const ctx = new SvelteParseContext(file, src, babelOptions, errors);
|
|
1283
|
+
const scriptFile = babelParse(maskedSource, babelOptions);
|
|
1284
|
+
if (Array.isArray(scriptFile.errors)) {
|
|
1285
|
+
errors.push(...scriptFile.errors);
|
|
1286
|
+
}
|
|
1287
|
+
errors.push({ svelteParse: true, message: parseErrorMessage });
|
|
1288
|
+
const statements = scriptFile.program.body;
|
|
1289
|
+
relocTree(ctx, statements);
|
|
1290
|
+
return assembleFile(ctx, src, statements);
|
|
1291
|
+
};
|