@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.
@@ -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
  };
@@ -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
- 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.1.1",
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
- "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.1.1",
44
- "@markuplint/ml-ast": "5.1.1",
45
- "@markuplint/parser-utils": "5.1.1",
46
- "svelte": "5.55.7"
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": "a940c8526e971d590d97395ce924910221208820"
49
+ "gitHead": "f587cf0206c8426e57369a48d6d5fb22306eafde"
49
50
  }