@markuplint/svelte-parser 5.1.1 → 5.3.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/lib/component-scanner.d.ts +18 -0
- package/lib/component-scanner.js +98 -0
- package/lib/parser.js +32 -4
- package/package.json +13 -12
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { NamespaceURI } from '@markuplint/ml-ast';
|
|
1
2
|
/**
|
|
2
3
|
* Result of scanning a single component file for its root element information.
|
|
3
4
|
*/
|
|
@@ -47,6 +48,8 @@ export interface ComponentScanSlotWrapper {
|
|
|
47
48
|
readonly isRoot: boolean;
|
|
48
49
|
readonly attrs: readonly ComponentScanAttr[];
|
|
49
50
|
readonly contents: readonly ComponentScanContent[];
|
|
51
|
+
/** The namespace of the element when the elements between it and the root element decide it. */
|
|
52
|
+
readonly namespace?: NamespaceURI;
|
|
50
53
|
}
|
|
51
54
|
/**
|
|
52
55
|
* A script/ESM source block extracted from a component file.
|
|
@@ -55,6 +58,20 @@ export interface ComponentScanScriptSource {
|
|
|
55
58
|
readonly content: string;
|
|
56
59
|
readonly offset: number;
|
|
57
60
|
}
|
|
61
|
+
/**
|
|
62
|
+
* An expression of the template that is read as script, with the pattern that binds its result:
|
|
63
|
+
* the `import('./C.svelte')` of `{#await import('./C.svelte') then { default: C }}`, as the source
|
|
64
|
+
* text of each.
|
|
65
|
+
*
|
|
66
|
+
* It has the shape of the type of the same name of `@markuplint/pretenders`
|
|
67
|
+
* (`ComponentScanner#extractTemplateImports`), which this package cannot import (the scanner is
|
|
68
|
+
* loaded by it dynamically); keep the two the same.
|
|
69
|
+
*/
|
|
70
|
+
export interface ComponentScanTemplateImport {
|
|
71
|
+
readonly expression: string;
|
|
72
|
+
/** The pattern of the `then` of the block (`{ default: C }`, `m`); absent when the block has none */
|
|
73
|
+
readonly value?: string;
|
|
74
|
+
}
|
|
58
75
|
/**
|
|
59
76
|
* Component scanner for Svelte component files.
|
|
60
77
|
*
|
|
@@ -65,4 +82,5 @@ export interface ComponentScanScriptSource {
|
|
|
65
82
|
export declare const componentScanner: {
|
|
66
83
|
scanComponent(sourceCode: string): ComponentScanResult | null;
|
|
67
84
|
extractScriptSource(sourceCode: string): ComponentScanScriptSource | null;
|
|
85
|
+
extractTemplateImports(sourceCode: string): ComponentScanTemplateImport[];
|
|
68
86
|
};
|
package/lib/component-scanner.js
CHANGED
|
@@ -1,9 +1,20 @@
|
|
|
1
|
+
import { getNamespaceBelow } from '@markuplint/parser-utils';
|
|
2
|
+
import { isFatalError } from '@markuplint/shared';
|
|
1
3
|
import { parser } from './parser.js';
|
|
4
|
+
import { svelteParse } from './svelte-parser/index.js';
|
|
2
5
|
function extractComponentInfo(doc) {
|
|
3
6
|
const root = findRoot(doc);
|
|
4
7
|
if (!root) {
|
|
5
8
|
return null;
|
|
6
9
|
}
|
|
10
|
+
// The parser says the root is authored, yet its name is not the one of a component
|
|
11
|
+
// (`component`, `slot` of Vue): it renders what it is given, so there is no element
|
|
12
|
+
// to pretend to be. `pretenders` would otherwise emit that name as the target, and
|
|
13
|
+
// nothing tells it from the name of an element. What a component's name is, is the
|
|
14
|
+
// parser's rule (`Parser#isComponentName`).
|
|
15
|
+
if (root.elementType === 'authored' && !parser.isComponentName(root.nodeName)) {
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
7
18
|
const spreads = extractSpreads(root);
|
|
8
19
|
return {
|
|
9
20
|
rootElement: root.nodeName,
|
|
@@ -151,6 +162,24 @@ function findWrapper(n, nodes) {
|
|
|
151
162
|
}
|
|
152
163
|
return null;
|
|
153
164
|
}
|
|
165
|
+
/**
|
|
166
|
+
* The names of the elements from the child of `root` down to `el`, for the namespace of `el`
|
|
167
|
+
* (`getNamespaceBelow`). A component is `null`, as it does not enter a namespace by its name.
|
|
168
|
+
*/
|
|
169
|
+
function getNamesBelow(el, root, nodes) {
|
|
170
|
+
const names = [];
|
|
171
|
+
let current = el;
|
|
172
|
+
while (current && current.uuid !== root.uuid) {
|
|
173
|
+
if (isElement(current) && !current.isFragment && current.nodeName !== 'template') {
|
|
174
|
+
names.unshift(getNamespaceSubject(current));
|
|
175
|
+
}
|
|
176
|
+
current = current.parentNodeUuid ? nodes.get(current.parentNodeUuid) : undefined;
|
|
177
|
+
}
|
|
178
|
+
return names;
|
|
179
|
+
}
|
|
180
|
+
function getNamespaceSubject(el) {
|
|
181
|
+
return el.elementType === 'authored' ? null : el.nodeName;
|
|
182
|
+
}
|
|
154
183
|
function isWithin(n, root, nodes) {
|
|
155
184
|
let current = n;
|
|
156
185
|
while (current) {
|
|
@@ -230,11 +259,15 @@ function scanSlotContent(doc) {
|
|
|
230
259
|
return {
|
|
231
260
|
slotWrappers: [...wrappers.values()].map(wrapper => {
|
|
232
261
|
const isNative = wrapper.elementType === 'html';
|
|
262
|
+
const namespace = isNative
|
|
263
|
+
? getNamespaceBelow(getNamespaceSubject(root), getNamesBelow(wrapper, root, nodes))
|
|
264
|
+
: null;
|
|
233
265
|
return {
|
|
234
266
|
element: wrapper.nodeName,
|
|
235
267
|
isRoot: wrapper.uuid === root.uuid,
|
|
236
268
|
attrs: isNative ? extractAttrs(wrapper) : [],
|
|
237
269
|
contents: isNative ? extractContents(wrapper) : [],
|
|
270
|
+
...(namespace ? { namespace } : {}),
|
|
238
271
|
};
|
|
239
272
|
}),
|
|
240
273
|
rootContents: extractContents(root),
|
|
@@ -268,6 +301,68 @@ function extractSvelteScript(source) {
|
|
|
268
301
|
}
|
|
269
302
|
return moduleBlock;
|
|
270
303
|
}
|
|
304
|
+
/**
|
|
305
|
+
* The source text of a node of the script AST that Svelte parses an expression or a pattern of the
|
|
306
|
+
* template into. The types of its nodes (`estree`) do not declare the offsets, though the parser
|
|
307
|
+
* (acorn) sets them on every node.
|
|
308
|
+
*/
|
|
309
|
+
function textOf(source, node) {
|
|
310
|
+
const { start, end } = node;
|
|
311
|
+
return source.slice(start, end);
|
|
312
|
+
}
|
|
313
|
+
/**
|
|
314
|
+
* The `#await` blocks of the template, in source order, wherever they are (in another block, an
|
|
315
|
+
* element, a component, a snippet). Svelte documents `{#await import('./C.svelte') then { default: C }}`
|
|
316
|
+
* as the way to render a component lazily (https://svelte.dev/docs/svelte/await), and the script block
|
|
317
|
+
* does not hold it: whatever the expression of a block is, it is returned as written, and the
|
|
318
|
+
* reader decides whether it loads a module.
|
|
319
|
+
*/
|
|
320
|
+
function extractAwaitBlocks(source) {
|
|
321
|
+
// A source with no `{#await` has none, and is not parsed a second time for that
|
|
322
|
+
if (!/\{\s*#await\b/.test(source)) {
|
|
323
|
+
return [];
|
|
324
|
+
}
|
|
325
|
+
let nodes;
|
|
326
|
+
try {
|
|
327
|
+
nodes = svelteParse(source);
|
|
328
|
+
}
|
|
329
|
+
catch (error) {
|
|
330
|
+
if (isFatalError(error)) {
|
|
331
|
+
throw error;
|
|
332
|
+
}
|
|
333
|
+
// The source is not Svelte (the compiler reports it as an `Error` of its own),
|
|
334
|
+
// which `scanComponent` reports as no component
|
|
335
|
+
return [];
|
|
336
|
+
}
|
|
337
|
+
const found = [];
|
|
338
|
+
const visit = (node) => {
|
|
339
|
+
if (Array.isArray(node)) {
|
|
340
|
+
for (const child of node) {
|
|
341
|
+
visit(child);
|
|
342
|
+
}
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
if (typeof node !== 'object' || node === null) {
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
const block = node;
|
|
349
|
+
if (block.type === 'AwaitBlock' && block.expression) {
|
|
350
|
+
const { value } = block;
|
|
351
|
+
found.push([
|
|
352
|
+
block.start,
|
|
353
|
+
{
|
|
354
|
+
expression: textOf(source, block.expression),
|
|
355
|
+
...(value ? { value: textOf(source, value) } : {}),
|
|
356
|
+
},
|
|
357
|
+
]);
|
|
358
|
+
}
|
|
359
|
+
for (const child of Object.values(node)) {
|
|
360
|
+
visit(child);
|
|
361
|
+
}
|
|
362
|
+
};
|
|
363
|
+
visit(nodes);
|
|
364
|
+
return found.toSorted(([a], [b]) => a - b).map(([, block]) => block);
|
|
365
|
+
}
|
|
271
366
|
/**
|
|
272
367
|
* Component scanner for Svelte component files.
|
|
273
368
|
*
|
|
@@ -298,4 +393,7 @@ export const componentScanner = {
|
|
|
298
393
|
extractScriptSource(sourceCode) {
|
|
299
394
|
return extractSvelteScript(sourceCode);
|
|
300
395
|
},
|
|
396
|
+
extractTemplateImports(sourceCode) {
|
|
397
|
+
return extractAwaitBlocks(sourceCode);
|
|
398
|
+
},
|
|
301
399
|
};
|
package/lib/parser.js
CHANGED
|
@@ -13,6 +13,9 @@ export class SvelteParser extends Parser {
|
|
|
13
13
|
super({
|
|
14
14
|
endTagType: 'xml',
|
|
15
15
|
tagNameCaseSensitive: true,
|
|
16
|
+
// A static attribute is in the HTML string that the compiler hands to the browser's parser,
|
|
17
|
+
// and a dynamic one is set by `set_attribute` under the name as written.
|
|
18
|
+
attrNameRoute: 'dom-api-when-dynamic',
|
|
16
19
|
ignoreTags: [
|
|
17
20
|
// Fixed(Resolved) #2505
|
|
18
21
|
// The script tag needs to be passed with the lang attribute to the parser,
|
|
@@ -361,8 +364,16 @@ export class SvelteParser extends Parser {
|
|
|
361
364
|
else {
|
|
362
365
|
thenExpEndCharOffset = thenExpStart + rawPendingNodesBelow.indexOf('}') + 1;
|
|
363
366
|
}
|
|
364
|
-
|
|
367
|
+
// `thenExpStart` is a position in the whole source, as `thenExpEndCharOffset` is
|
|
368
|
+
thenToken = this.sliceFragment(thenExpStart, thenExpEndCharOffset);
|
|
365
369
|
}
|
|
370
|
+
/**
|
|
371
|
+
* `{#await expression then name}...{:catch name}...{/await}`
|
|
372
|
+
*
|
|
373
|
+
* The `then` is in the head of the block (the shorthand), so no `{:then}` marks it: Svelte
|
|
374
|
+
* reads what follows the head as the `then` fragment, which then belongs to the head.
|
|
375
|
+
*/
|
|
376
|
+
const hasThenInHead = !thenToken && !!originBlockNode.then;
|
|
366
377
|
let catchToken = null;
|
|
367
378
|
/**
|
|
368
379
|
* `{#await expression}...{:then name}...{:catch name}...{/await}`
|
|
@@ -372,10 +383,17 @@ export class SvelteParser extends Parser {
|
|
|
372
383
|
*
|
|
373
384
|
* `{#await expression}...{:catch name}...{/await}`
|
|
374
385
|
* find___^
|
|
386
|
+
*
|
|
387
|
+
* If `then` is in the head of the block:
|
|
388
|
+
*
|
|
389
|
+
* `{#await expression then name}...{:catch name}...{/await}`
|
|
390
|
+
* find___^
|
|
375
391
|
*/
|
|
376
392
|
const catchExpStart = thenToken
|
|
377
393
|
? (thenEnd ?? thenToken.offset + thenToken.raw.length)
|
|
378
|
-
:
|
|
394
|
+
: hasThenInHead
|
|
395
|
+
? (thenEnd ?? awaitExpEnd)
|
|
396
|
+
: (pendingEnd ?? awaitExpEnd);
|
|
379
397
|
/**
|
|
380
398
|
* `{:catch name}...{/await}`
|
|
381
399
|
*/
|
|
@@ -395,8 +413,18 @@ export class SvelteParser extends Parser {
|
|
|
395
413
|
else {
|
|
396
414
|
catchExpEndCharOffset = catchExpStart + rawThenNodesBelow.indexOf('}') + 1;
|
|
397
415
|
}
|
|
398
|
-
|
|
416
|
+
// `catchExpStart` is a position in the whole source, as `catchExpEndCharOffset` is
|
|
417
|
+
catchToken = this.sliceFragment(catchExpStart, catchExpEndCharOffset);
|
|
399
418
|
}
|
|
419
|
+
/**
|
|
420
|
+
* What follows the head of the block when no `{:then}` or `{:catch}` marks it: the `then` or the
|
|
421
|
+
* `catch` of the shorthand (`{#await expression then name}`, `{#await expression catch name}`).
|
|
422
|
+
*/
|
|
423
|
+
const headContent = hasThenInHead
|
|
424
|
+
? originBlockNode.then?.nodes
|
|
425
|
+
: !thenToken && !catchToken
|
|
426
|
+
? originBlockNode.catch?.nodes
|
|
427
|
+
: undefined;
|
|
400
428
|
const expressions = [
|
|
401
429
|
this.visitPsBlock({
|
|
402
430
|
...awaitExpToken,
|
|
@@ -404,7 +432,7 @@ export class SvelteParser extends Parser {
|
|
|
404
432
|
parentNode: token.parentNode,
|
|
405
433
|
nodeName: 'await',
|
|
406
434
|
isFragment: false,
|
|
407
|
-
}, originBlockNode.pending?.nodes, {
|
|
435
|
+
}, originBlockNode.pending?.nodes ?? headContent, {
|
|
408
436
|
type: 'await',
|
|
409
437
|
expression: awaitExpToken.raw,
|
|
410
438
|
})[0],
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@markuplint/svelte-parser",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.3.0",
|
|
4
4
|
"description": "Svelte parser for markuplint",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -15,16 +15,16 @@
|
|
|
15
15
|
"type": "module",
|
|
16
16
|
"exports": {
|
|
17
17
|
".": {
|
|
18
|
-
"
|
|
19
|
-
"
|
|
18
|
+
"types": "./lib/index.d.ts",
|
|
19
|
+
"default": "./lib/index.js"
|
|
20
20
|
},
|
|
21
21
|
"./kit": {
|
|
22
|
-
"
|
|
23
|
-
"
|
|
22
|
+
"types": "./lib/sveltekit-parser.d.ts",
|
|
23
|
+
"default": "./lib/sveltekit-parser.js"
|
|
24
24
|
},
|
|
25
25
|
"./component-scanner": {
|
|
26
|
-
"
|
|
27
|
-
"
|
|
26
|
+
"types": "./lib/component-scanner.d.ts",
|
|
27
|
+
"default": "./lib/component-scanner.js"
|
|
28
28
|
}
|
|
29
29
|
},
|
|
30
30
|
"types": "lib/index.d.ts",
|
|
@@ -40,10 +40,11 @@
|
|
|
40
40
|
"clean": "tsc --build --clean tsconfig.build.json"
|
|
41
41
|
},
|
|
42
42
|
"dependencies": {
|
|
43
|
-
"@markuplint/html-parser": "5.
|
|
44
|
-
"@markuplint/ml-ast": "5.
|
|
45
|
-
"@markuplint/parser-utils": "5.
|
|
46
|
-
"
|
|
43
|
+
"@markuplint/html-parser": "5.3.0",
|
|
44
|
+
"@markuplint/ml-ast": "5.3.0",
|
|
45
|
+
"@markuplint/parser-utils": "5.3.0",
|
|
46
|
+
"@markuplint/shared": "5.3.0",
|
|
47
|
+
"svelte": "^5.57.2"
|
|
47
48
|
},
|
|
48
|
-
"gitHead": "
|
|
49
|
+
"gitHead": "f587cf0206c8426e57369a48d6d5fb22306eafde"
|
|
49
50
|
}
|