@markuplint/svelte-parser 5.2.0 → 5.4.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.
@@ -58,6 +58,20 @@ export interface ComponentScanScriptSource {
58
58
  readonly content: string;
59
59
  readonly offset: number;
60
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
+ }
61
75
  /**
62
76
  * Component scanner for Svelte component files.
63
77
  *
@@ -68,4 +82,5 @@ export interface ComponentScanScriptSource {
68
82
  export declare const componentScanner: {
69
83
  scanComponent(sourceCode: string): ComponentScanResult | null;
70
84
  extractScriptSource(sourceCode: string): ComponentScanScriptSource | null;
85
+ extractTemplateImports(sourceCode: string): ComponentScanTemplateImport[];
71
86
  };
@@ -1,10 +1,20 @@
1
1
  import { getNamespaceBelow } from '@markuplint/parser-utils';
2
+ import { isFatalError } from '@markuplint/shared';
2
3
  import { parser } from './parser.js';
4
+ import { svelteParse } from './svelte-parser/index.js';
3
5
  function extractComponentInfo(doc) {
4
6
  const root = findRoot(doc);
5
7
  if (!root) {
6
8
  return null;
7
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
+ }
8
18
  const spreads = extractSpreads(root);
9
19
  return {
10
20
  rootElement: root.nodeName,
@@ -291,6 +301,68 @@ function extractSvelteScript(source) {
291
301
  }
292
302
  return moduleBlock;
293
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
+ }
294
366
  /**
295
367
  * Component scanner for Svelte component files.
296
368
  *
@@ -321,4 +393,7 @@ export const componentScanner = {
321
393
  extractScriptSource(sourceCode) {
322
394
  return extractSvelteScript(sourceCode);
323
395
  },
396
+ extractTemplateImports(sourceCode) {
397
+ return extractAwaitBlocks(sourceCode);
398
+ },
324
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
- thenToken = this.sliceFragment(token.offset + thenExpStart, thenExpEndCharOffset);
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
- : (pendingEnd ?? awaitExpEnd);
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
- catchToken = this.sliceFragment(token.offset + catchExpStart, catchExpEndCharOffset);
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.2.0",
3
+ "version": "5.4.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
- "import": "./lib/index.js",
19
- "types": "./lib/index.d.ts"
18
+ "types": "./lib/index.d.ts",
19
+ "default": "./lib/index.js"
20
20
  },
21
21
  "./kit": {
22
- "import": "./lib/sveltekit-parser.js",
23
- "types": "./lib/sveltekit-parser.d.ts"
22
+ "types": "./lib/sveltekit-parser.d.ts",
23
+ "default": "./lib/sveltekit-parser.js"
24
24
  },
25
25
  "./component-scanner": {
26
- "import": "./lib/component-scanner.js",
27
- "types": "./lib/component-scanner.d.ts"
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.2.0",
44
- "@markuplint/ml-ast": "5.1.1",
45
- "@markuplint/parser-utils": "5.2.0",
46
- "svelte": "^5.57.1"
43
+ "@markuplint/html-parser": "5.4.0",
44
+ "@markuplint/ml-ast": "5.3.0",
45
+ "@markuplint/parser-utils": "5.4.0",
46
+ "@markuplint/shared": "5.4.0",
47
+ "svelte": "^5.57.2"
47
48
  },
48
- "gitHead": "d8b2feab77c9c862513615bea6b80d2817431878"
49
+ "gitHead": "647f6fb5d5743f060678599c97e9c84b58970747"
49
50
  }