@markuplint/svelte-parser 4.7.12 → 5.0.0-alpha.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/index.d.ts CHANGED
@@ -1 +1,8 @@
1
+ /**
2
+ * @module
3
+ * Svelte component parser for markuplint. Provides a parser that transforms Svelte
4
+ * template syntax into markuplint's AST, supporting Svelte-specific constructs such as
5
+ * `{#if}`, `{#each}`, `{#await}`, `{#key}`, `{#snippet}`, expression tags,
6
+ * and bind/class/event directives.
7
+ */
1
8
  export { parser } from './parser.js';
package/lib/index.js CHANGED
@@ -1 +1,8 @@
1
+ /**
2
+ * @module
3
+ * Svelte component parser for markuplint. Provides a parser that transforms Svelte
4
+ * template syntax into markuplint's AST, supporting Svelte-specific constructs such as
5
+ * `{#if}`, `{#each}`, `{#await}`, `{#key}`, `{#snippet}`, expression tags,
6
+ * and bind/class/event directives.
7
+ */
1
8
  export { parser } from './parser.js';
@@ -1,6 +1,17 @@
1
1
  import type { SvelteParser } from './parser.js';
2
2
  import type { ChildToken, Token } from '@markuplint/parser-utils';
3
3
  import type { SvelteBlock } from './svelte-parser/index.js';
4
+ /**
5
+ * Extracts the open and close tag tokens from a Svelte block construct
6
+ * (e.g., `{#each}...{/each}`, `{#key}...{/key}`).
7
+ * Locates the closing `{/xxx}` tag via regex and computes the opening token
8
+ * based on the block's child fragment boundaries.
9
+ *
10
+ * @param parser - The SvelteParser instance used to slice source fragments
11
+ * @param token - The child token representing the entire block range
12
+ * @param originBlockNode - The Svelte AST block node being parsed
13
+ * @returns An object containing the `openToken` and `closeToken` for the block
14
+ */
4
15
  export declare function parseBlock(parser: SvelteParser, token: ChildToken, originBlockNode: SvelteBlock): {
5
16
  openToken: Token;
6
17
  closeToken: Token;
@@ -1,3 +1,14 @@
1
+ /**
2
+ * Extracts the open and close tag tokens from a Svelte block construct
3
+ * (e.g., `{#each}...{/each}`, `{#key}...{/key}`).
4
+ * Locates the closing `{/xxx}` tag via regex and computes the opening token
5
+ * based on the block's child fragment boundaries.
6
+ *
7
+ * @param parser - The SvelteParser instance used to slice source fragments
8
+ * @param token - The child token representing the entire block range
9
+ * @param originBlockNode - The Svelte AST block node being parsed
10
+ * @returns An object containing the `openToken` and `closeToken` for the block
11
+ */
1
12
  export function parseBlock(
2
13
  // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
3
14
  parser, token,
@@ -16,7 +27,7 @@ originBlockNode) {
16
27
  /**
17
28
  * `{/xxx}`
18
29
  */
19
- const closeToken = parser.sliceFragment(token.startOffset + eachCloseStartIndex, originBlockNode.end);
30
+ const closeToken = parser.sliceFragment(token.offset + eachCloseStartIndex, originBlockNode.end);
20
31
  const fragment = originBlockNode.type === 'IfBlock'
21
32
  ? originBlockNode.consequent.nodes
22
33
  : originBlockNode.type === 'AwaitBlock'
@@ -37,10 +48,10 @@ originBlockNode) {
37
48
  */
38
49
  let openToken;
39
50
  if (fragStart != null && fragEnd != null) {
40
- openToken = parser.sliceFragment(token.startOffset, fragStart);
51
+ openToken = parser.sliceFragment(token.offset, fragStart);
41
52
  }
42
53
  else {
43
- openToken = parser.sliceFragment(token.startOffset, eachCloseStartIndex);
54
+ openToken = parser.sliceFragment(token.offset, eachCloseStartIndex);
44
55
  }
45
56
  return {
46
57
  openToken,
package/lib/parser.d.ts CHANGED
@@ -1,10 +1,16 @@
1
1
  import type { SvelteNode } from './svelte-parser/index.js';
2
- import type { MLASTNodeTreeItem, MLASTParentNode, MLASTPreprocessorSpecificBlock, MLASTPreprocessorSpecificBlockConditionalType } from '@markuplint/ml-ast';
2
+ import type { MLASTNodeTreeItem, MLASTParentNode, MLASTPreprocessorSpecificBlock, MLASTBlockBehavior } from '@markuplint/ml-ast';
3
3
  import type { ChildToken, ParseOptions, Token } from '@markuplint/parser-utils';
4
4
  import { ParserError, Parser } from '@markuplint/parser-utils';
5
+ /**
6
+ * Parser implementation for Svelte component templates.
7
+ * Extends the base Parser to handle Svelte elements, text, comments,
8
+ * expression tags, control flow blocks (`{#if}`, `{#each}`, `{#await}`,
9
+ * `{#key}`, `{#snippet}`), directives (`bind:`, `class:`, `on:`),
10
+ * and shorthand attribute syntax.
11
+ */
5
12
  export declare class SvelteParser extends Parser<SvelteNode> {
6
13
  #private;
7
- readonly specificBindDirective: ReadonlySet<string>;
8
14
  constructor();
9
15
  tokenize(): {
10
16
  ast: SvelteNode[];
@@ -12,19 +18,64 @@ export declare class SvelteParser extends Parser<SvelteNode> {
12
18
  };
13
19
  parse(raw: string, options?: ParseOptions): import("@markuplint/ml-ast").MLASTDocument;
14
20
  parseError(error: any): ParserError;
21
+ /**
22
+ * Converts a Svelte AST node into markuplint node tree items.
23
+ * Dispatches on the node type to handle Text, Comment, ExpressionTag,
24
+ * elements (Component, RegularElement), and control flow blocks
25
+ * (IfBlock, EachBlock, AwaitBlock, KeyBlock, SnippetBlock).
26
+ *
27
+ * @param originNode - The Svelte AST node to convert
28
+ * @param parentNode - The parent node in the markuplint tree, or null for root nodes
29
+ * @param depth - The nesting depth of the node
30
+ * @returns An array of markuplint node tree items
31
+ */
15
32
  nodeize(originNode: SvelteNode, parentNode: MLASTParentNode | null, depth: number): readonly MLASTNodeTreeItem[];
33
+ /**
34
+ * Visits a text token, converting `<script>` tags embedded in Svelte template
35
+ * text into preprocessor-specific blocks rather than treating them as raw text.
36
+ *
37
+ * @param token - The child token representing the text content
38
+ * @returns An array of markuplint node tree items
39
+ */
16
40
  visitText(token: ChildToken): readonly MLASTNodeTreeItem[];
41
+ /**
42
+ * Visits a preprocessor-specific block token and enforces that exactly one
43
+ * block node is produced. Throws a ParserError if the result is empty
44
+ * or contains multiple nodes.
45
+ *
46
+ * @param token - The child token with node name and fragment flag
47
+ * @param childNodes - The child Svelte AST nodes within the block
48
+ * @param blockBehavior - The block behavior, or null
49
+ * @returns A single-element tuple containing the preprocessor-specific block
50
+ */
17
51
  visitPsBlock(token: ChildToken & {
18
52
  readonly nodeName: string;
19
53
  readonly isFragment: boolean;
20
- }, childNodes?: readonly SvelteNode[], conditionalType?: MLASTPreprocessorSpecificBlockConditionalType): readonly [MLASTPreprocessorSpecificBlock];
54
+ }, childNodes?: readonly SvelteNode[], blockBehavior?: MLASTBlockBehavior | null): readonly [MLASTPreprocessorSpecificBlock];
55
+ /**
56
+ * Visits child nodes and verifies that no sibling nodes with differing
57
+ * hierarchy levels are produced. Throws a ParserError if unexpected
58
+ * sibling nodes are discovered.
59
+ *
60
+ * @param children - The child Svelte AST nodes to visit
61
+ * @param parentNode - The parent node in the markuplint tree
62
+ * @returns An empty array (all children are attached via the visitor)
63
+ */
21
64
  visitChildren(children: readonly SvelteNode[], parentNode: MLASTParentNode | null): never[];
65
+ /**
66
+ * Visits an attribute token, handling Svelte-specific syntax including
67
+ * curly-brace expression values and shorthand attributes (`{name}`).
68
+ * Directive resolution (`bind:`, `class:`, `on:`, etc.) and IDL attribute
69
+ * mapping are now handled declaratively by svelte-spec's directivePatterns
70
+ * and ml-core's useIDLAttributeNames.
71
+ *
72
+ * @param token - The token representing the attribute
73
+ * @returns The parsed attribute node with Svelte-specific metadata
74
+ */
22
75
  visitAttr(token: Token): (import("@markuplint/ml-ast").MLASTSpreadAttr & {
23
76
  __rightText?: string;
24
77
  }) | {
25
78
  isDynamicValue: true | undefined;
26
- isDirective: true | undefined;
27
- isDuplicatable: boolean;
28
79
  potentialName: string | undefined;
29
80
  type: "attr";
30
81
  nodeName: string;
@@ -36,17 +87,16 @@ export declare class SvelteParser extends Parser<SvelteNode> {
36
87
  startQuote: import("@markuplint/ml-ast").MLASTToken;
37
88
  value: import("@markuplint/ml-ast").MLASTToken;
38
89
  endQuote: import("@markuplint/ml-ast").MLASTToken;
90
+ isDirective?: true;
39
91
  potentialValue?: string;
40
92
  valueType?: "string" | "number" | "boolean" | "code";
41
93
  candidate?: string;
94
+ isDuplicatable: boolean;
42
95
  uuid: string;
43
96
  raw: string;
44
- startOffset: number;
45
- endOffset: number;
46
- startLine: number;
47
- endLine: number;
48
- startCol: number;
49
- endCol: number;
97
+ offset: number;
98
+ line: number;
99
+ col: number;
50
100
  __rightText?: string;
51
101
  };
52
102
  /**
package/lib/parser.js CHANGED
@@ -1,13 +1,13 @@
1
- var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) {
2
- if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter");
3
- if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
4
- return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
5
- };
6
- var _SvelteParser_instances, _SvelteParser_parseAwaitBlock, _SvelteParser_parseEachBlock, _SvelteParser_traverseIfBlock;
7
- import { getNamespace } from '@markuplint/html-parser';
8
1
  import { ParserError, Parser, AttrState } from '@markuplint/parser-utils';
9
2
  import { parseBlock } from './parse-block.js';
10
3
  import { svelteParse } from './svelte-parser/index.js';
4
+ /**
5
+ * Parser implementation for Svelte component templates.
6
+ * Extends the base Parser to handle Svelte elements, text, comments,
7
+ * expression tags, control flow blocks (`{#if}`, `{#each}`, `{#await}`,
8
+ * `{#key}`, `{#snippet}`), directives (`bind:`, `class:`, `on:`),
9
+ * and shorthand attribute syntax.
10
+ */
11
11
  export class SvelteParser extends Parser {
12
12
  constructor() {
13
13
  super({
@@ -26,8 +26,6 @@ export class SvelteParser extends Parser {
26
26
  ],
27
27
  maskChar: '-',
28
28
  });
29
- _SvelteParser_instances.add(this);
30
- this.specificBindDirective = new Set(['group', 'this']);
31
29
  }
32
30
  tokenize() {
33
31
  return {
@@ -49,11 +47,21 @@ export class SvelteParser extends Parser {
49
47
  }
50
48
  return super.parseError(error);
51
49
  }
50
+ /**
51
+ * Converts a Svelte AST node into markuplint node tree items.
52
+ * Dispatches on the node type to handle Text, Comment, ExpressionTag,
53
+ * elements (Component, RegularElement), and control flow blocks
54
+ * (IfBlock, EachBlock, AwaitBlock, KeyBlock, SnippetBlock).
55
+ *
56
+ * @param originNode - The Svelte AST node to convert
57
+ * @param parentNode - The parent node in the markuplint tree, or null for root nodes
58
+ * @param depth - The nesting depth of the node
59
+ * @returns An array of markuplint node tree items
60
+ */
52
61
  nodeize(
53
62
  // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
54
63
  originNode, parentNode, depth) {
55
64
  const token = this.sliceFragment(originNode.start, originNode.end);
56
- const parentNamespace = parentNode && 'namespace' in parentNode ? parentNode.namespace : 'http://www.w3.org/1999/xhtml';
57
65
  switch (originNode.type) {
58
66
  case 'Text': {
59
67
  return this.visitText({
@@ -84,14 +92,13 @@ export class SvelteParser extends Parser {
84
92
  const reEndTag = new RegExp(`</${originNode.name}\\s*>$`, 'i');
85
93
  const startTagEndOffset = children.length > 0
86
94
  ? (children[0]?.start ?? 0)
87
- : token.raw.replace(reEndTag, '').length + token.startOffset;
88
- const startTagLocation = this.sliceFragment(token.startOffset, startTagEndOffset);
95
+ : token.raw.replace(reEndTag, '').length + token.offset;
96
+ const startTagLocation = this.sliceFragment(token.offset, startTagEndOffset);
89
97
  return this.visitElement({
90
98
  ...startTagLocation,
91
99
  depth,
92
100
  parentNode,
93
101
  nodeName: originNode.name,
94
- namespace: getNamespace(originNode.name, parentNamespace),
95
102
  }, originNode.fragment.nodes, {
96
103
  createEndTagToken: () => {
97
104
  if (!reEndTag.test(token.raw)) {
@@ -102,7 +109,7 @@ export class SvelteParser extends Parser {
102
109
  throw new Error('Parse error');
103
110
  }
104
111
  const endTagRaw = endTagRawMatched[0];
105
- const endTagStartOffset = token.startOffset + token.raw.lastIndexOf(endTagRaw);
112
+ const endTagStartOffset = token.offset + token.raw.lastIndexOf(endTagRaw);
106
113
  const endTagEndOffset = endTagStartOffset + endTagRaw.length;
107
114
  const endTagLocation = this.sliceFragment(endTagStartOffset, endTagEndOffset);
108
115
  return {
@@ -115,7 +122,7 @@ export class SvelteParser extends Parser {
115
122
  }
116
123
  case 'IfBlock': {
117
124
  const expressions = [];
118
- const ifElseBlocks = __classPrivateFieldGet(this, _SvelteParser_instances, "m", _SvelteParser_traverseIfBlock).call(this, originNode, token.startOffset);
125
+ const ifElseBlocks = this.#traverseIfBlock(originNode, token.offset);
119
126
  for (const ifElseBlock of ifElseBlocks) {
120
127
  const expression = this.visitPsBlock({
121
128
  ...ifElseBlock,
@@ -124,24 +131,27 @@ export class SvelteParser extends Parser {
124
131
  nodeName: ifElseBlock.type,
125
132
  isFragment: false,
126
133
  }, ifElseBlock.children, {
127
- if: 'if',
128
- elseif: 'if:elseif',
129
- else: 'if:else',
130
- '/if': 'end',
131
- }[ifElseBlock.type])[0];
134
+ type: {
135
+ if: 'if',
136
+ elseif: 'if:elseif',
137
+ else: 'if:else',
138
+ '/if': 'end',
139
+ }[ifElseBlock.type],
140
+ expression: ifElseBlock.raw,
141
+ })[0];
132
142
  expressions.push(expression);
133
143
  }
134
144
  return expressions;
135
145
  }
136
146
  case 'EachBlock': {
137
- return __classPrivateFieldGet(this, _SvelteParser_instances, "m", _SvelteParser_parseEachBlock).call(this, {
147
+ return this.#parseEachBlock({
138
148
  ...token,
139
149
  depth,
140
150
  parentNode,
141
151
  }, originNode);
142
152
  }
143
153
  case 'AwaitBlock': {
144
- return __classPrivateFieldGet(this, _SvelteParser_instances, "m", _SvelteParser_parseAwaitBlock).call(this, {
154
+ return this.#parseAwaitBlock({
145
155
  ...token,
146
156
  depth,
147
157
  parentNode,
@@ -205,6 +215,13 @@ export class SvelteParser extends Parser {
205
215
  }
206
216
  }
207
217
  }
218
+ /**
219
+ * Visits a text token, converting `<script>` tags embedded in Svelte template
220
+ * text into preprocessor-specific blocks rather than treating them as raw text.
221
+ *
222
+ * @param token - The child token representing the text content
223
+ * @returns An array of markuplint node tree items
224
+ */
208
225
  visitText(token) {
209
226
  const nodes = super.visitText(token, {
210
227
  researchTags: false,
@@ -221,8 +238,18 @@ export class SvelteParser extends Parser {
221
238
  return node;
222
239
  });
223
240
  }
224
- visitPsBlock(token, childNodes = [], conditionalType = null) {
225
- const nodes = super.visitPsBlock(token, childNodes, conditionalType);
241
+ /**
242
+ * Visits a preprocessor-specific block token and enforces that exactly one
243
+ * block node is produced. Throws a ParserError if the result is empty
244
+ * or contains multiple nodes.
245
+ *
246
+ * @param token - The child token with node name and fragment flag
247
+ * @param childNodes - The child Svelte AST nodes within the block
248
+ * @param blockBehavior - The block behavior, or null
249
+ * @returns A single-element tuple containing the preprocessor-specific block
250
+ */
251
+ visitPsBlock(token, childNodes = [], blockBehavior = null) {
252
+ const nodes = super.visitPsBlock(token, childNodes, blockBehavior);
226
253
  const block = nodes.at(0);
227
254
  if (!block || block.type !== 'psblock') {
228
255
  throw new ParserError('Parse error', token);
@@ -232,6 +259,15 @@ export class SvelteParser extends Parser {
232
259
  }
233
260
  return [block];
234
261
  }
262
+ /**
263
+ * Visits child nodes and verifies that no sibling nodes with differing
264
+ * hierarchy levels are produced. Throws a ParserError if unexpected
265
+ * sibling nodes are discovered.
266
+ *
267
+ * @param children - The child Svelte AST nodes to visit
268
+ * @param parentNode - The parent node in the markuplint tree
269
+ * @returns An empty array (all children are attached via the visitor)
270
+ */
235
271
  visitChildren(
236
272
  // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
237
273
  children, parentNode) {
@@ -241,6 +277,16 @@ export class SvelteParser extends Parser {
241
277
  }
242
278
  return [];
243
279
  }
280
+ /**
281
+ * Visits an attribute token, handling Svelte-specific syntax including
282
+ * curly-brace expression values and shorthand attributes (`{name}`).
283
+ * Directive resolution (`bind:`, `class:`, `on:`, etc.) and IDL attribute
284
+ * mapping are now handled declaratively by svelte-spec's directivePatterns
285
+ * and ml-core's useIDLAttributeNames.
286
+ *
287
+ * @param token - The token representing the attribute
288
+ * @returns The parsed attribute node with Svelte-specific metadata
289
+ */
244
290
  visitAttr(token) {
245
291
  const attr = super.visitAttr(token, {
246
292
  quoteSet: [
@@ -257,35 +303,17 @@ export class SvelteParser extends Parser {
257
303
  }
258
304
  let isDynamicValue = attr.startQuote.raw === '{' || undefined;
259
305
  let potentialName;
260
- let isDirective;
261
- let isDuplicatable = false;
306
+ // Shorthand {name} → potentialName = value
262
307
  if (isDynamicValue && attr.name.raw === '') {
263
308
  potentialName = attr.value.raw;
264
309
  }
265
- const [baseName, subName] = attr.name.raw.split(':');
266
- if (subName) {
267
- isDirective = true;
268
- if (baseName === 'bind' && !this.specificBindDirective.has(subName)) {
269
- potentialName = subName;
270
- isDirective = undefined;
271
- isDynamicValue = true;
272
- }
273
- }
274
- if (baseName?.toLowerCase() === 'class') {
275
- isDuplicatable = true;
276
- if (subName) {
277
- potentialName = 'class';
278
- isDynamicValue = true;
279
- }
280
- }
310
+ // Final curly brace check
281
311
  if (attr.startQuote.raw === '{' && attr.endQuote.raw === '}') {
282
312
  isDynamicValue = true;
283
313
  }
284
314
  return {
285
315
  ...attr,
286
316
  isDynamicValue,
287
- isDirective,
288
- isDuplicatable,
289
317
  potentialName,
290
318
  };
291
319
  }
@@ -300,217 +328,260 @@ export class SvelteParser extends Parser {
300
328
  detectElementType(nodeName) {
301
329
  return super.detectElementType(nodeName, /^[A-Z]|\./);
302
330
  }
303
- }
304
- _SvelteParser_instances = new WeakSet(), _SvelteParser_parseAwaitBlock = function _SvelteParser_parseAwaitBlock(token,
305
- // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
306
- originBlockNode) {
307
- const { closeToken } = parseBlock(this, token, originBlockNode);
308
- const pendingNodes = originBlockNode.pending?.nodes ?? [];
309
- const thenNodes = originBlockNode.then?.nodes ?? [];
310
- const pendingEnd = pendingNodes.at(-1)?.end;
311
- const thenEnd = thenNodes.at(-1)?.end;
312
- // @ts-ignore - new Svelte Compiler Type doesn't support `start` and `end` yet.
313
- const awaitConditionEnd = originBlockNode.expression.end;
314
331
  /**
315
- * `{#await expression}...{:then name}...{:catch name}...{/await}`
316
- * find___^ and cut
332
+ * Parses a Svelte `{#await}` block into its constituent preprocessor-specific blocks:
333
+ * the await expression, optional `{:then}` branch, optional `{:catch}` branch,
334
+ * and the closing `{/await}` tag.
317
335
  *
318
- * `}...{:then name}...{:catch name}...{/await}`
319
- */
320
- const rawAwaitConditionBelow = this.rawCode.slice(awaitConditionEnd, originBlockNode.end);
321
- /**
322
- * `}...{:then name}...{:catch name}...{/await}`
323
- * ^___find
324
- */
325
- const awaitExpEnd = awaitConditionEnd + rawAwaitConditionBelow.indexOf('}') + 1;
326
- /**
327
- * `{#await expression}`
328
- */
329
- const awaitExpToken = this.sliceFragment(token.startOffset, awaitExpEnd);
330
- let thenToken = null;
331
- /**
332
- * `{#await expression}...{:then name}...{:catch name}...{/await}`
333
- * find___^
334
- */
335
- const thenExpStart = pendingEnd ?? awaitExpEnd;
336
- /**
337
- * `{:then name}...{:catch name}...{/await}`
336
+ * @param token - The child token representing the entire await block
337
+ * @param originBlockNode - The Svelte AST AwaitBlock node
338
+ * @returns An array of preprocessor-specific block nodes
338
339
  */
339
- const rawPendingNodesBelow = this.rawCode.slice(thenExpStart, originBlockNode.end);
340
- if (
341
- // eslint-disable-next-line regexp/strict
342
- /^{\s*:then[\s|}]/.test(rawPendingNodesBelow)) {
343
- let thenExpEndCharOffset;
344
- if (originBlockNode.value) {
345
- const thenIdentifierEnd =
346
- // @ts-ignore - new Svelte Compiler Type doesn't support `start` and `end` yet.
347
- originBlockNode.value.end;
348
- const rawThenExpCloseCharAndBelow = this.rawCode.slice(thenIdentifierEnd, originBlockNode.end);
349
- const thenExpEndCharIndex = rawThenExpCloseCharAndBelow.indexOf('}') + 1;
350
- thenExpEndCharOffset = thenIdentifierEnd + thenExpEndCharIndex;
340
+ #parseAwaitBlock(token,
341
+ // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
342
+ originBlockNode) {
343
+ const { closeToken } = parseBlock(this, token, originBlockNode);
344
+ const pendingNodes = originBlockNode.pending?.nodes ?? [];
345
+ const thenNodes = originBlockNode.then?.nodes ?? [];
346
+ const pendingEnd = pendingNodes.at(-1)?.end;
347
+ const thenEnd = thenNodes.at(-1)?.end;
348
+ // @ts-ignore - new Svelte Compiler Type doesn't support `start` and `end` yet.
349
+ const awaitConditionEnd = originBlockNode.expression.end;
350
+ /**
351
+ * `{#await expression}...{:then name}...{:catch name}...{/await}`
352
+ * find___^ and cut
353
+ *
354
+ * `}...{:then name}...{:catch name}...{/await}`
355
+ */
356
+ const rawAwaitConditionBelow = this.rawCode.slice(awaitConditionEnd, originBlockNode.end);
357
+ /**
358
+ * `}...{:then name}...{:catch name}...{/await}`
359
+ * ^___find
360
+ */
361
+ const awaitExpEnd = awaitConditionEnd + rawAwaitConditionBelow.indexOf('}') + 1;
362
+ /**
363
+ * `{#await expression}`
364
+ */
365
+ const awaitExpToken = this.sliceFragment(token.offset, awaitExpEnd);
366
+ let thenToken = null;
367
+ /**
368
+ * `{#await expression}...{:then name}...{:catch name}...{/await}`
369
+ * find___^
370
+ */
371
+ const thenExpStart = pendingEnd ?? awaitExpEnd;
372
+ /**
373
+ * `{:then name}...{:catch name}...{/await}`
374
+ */
375
+ const rawPendingNodesBelow = this.rawCode.slice(thenExpStart, originBlockNode.end);
376
+ if (
377
+ // eslint-disable-next-line regexp/strict
378
+ /^{\s*:then[\s|}]/.test(rawPendingNodesBelow)) {
379
+ let thenExpEndCharOffset;
380
+ if (originBlockNode.value) {
381
+ const thenIdentifierEnd =
382
+ // @ts-ignore - new Svelte Compiler Type doesn't support `start` and `end` yet.
383
+ originBlockNode.value.end;
384
+ const rawThenExpCloseCharAndBelow = this.rawCode.slice(thenIdentifierEnd, originBlockNode.end);
385
+ const thenExpEndCharIndex = rawThenExpCloseCharAndBelow.indexOf('}') + 1;
386
+ thenExpEndCharOffset = thenIdentifierEnd + thenExpEndCharIndex;
387
+ }
388
+ else {
389
+ thenExpEndCharOffset = thenExpStart + rawPendingNodesBelow.indexOf('}') + 1;
390
+ }
391
+ thenToken = this.sliceFragment(token.offset + thenExpStart, thenExpEndCharOffset);
351
392
  }
352
- else {
353
- thenExpEndCharOffset = thenExpStart + rawPendingNodesBelow.indexOf('}') + 1;
393
+ let catchToken = null;
394
+ /**
395
+ * `{#await expression}...{:then name}...{:catch name}...{/await}`
396
+ * find___^
397
+ *
398
+ * If `then` block is not found:
399
+ *
400
+ * `{#await expression}...{:catch name}...{/await}`
401
+ * find___^
402
+ */
403
+ const catchExpStart = thenToken
404
+ ? (thenEnd ?? thenToken.offset + thenToken.raw.length)
405
+ : (pendingEnd ?? awaitExpEnd);
406
+ /**
407
+ * `{:catch name}...{/await}`
408
+ */
409
+ const rawThenNodesBelow = this.rawCode.slice(catchExpStart, originBlockNode.end);
410
+ if (
411
+ // eslint-disable-next-line regexp/strict
412
+ /^{\s*:catch[\s|}]/.test(rawThenNodesBelow)) {
413
+ let catchExpEndCharOffset;
414
+ if (originBlockNode.error) {
415
+ const catchIdentifierEnd =
416
+ // @ts-ignore - new Svelte Compiler Type doesn't support `start` and `end` yet.
417
+ originBlockNode.error.end;
418
+ const rawCatchExpCloseCharAndBelow = this.rawCode.slice(catchIdentifierEnd, originBlockNode.end);
419
+ const catchExpEndCharIndex = rawCatchExpCloseCharAndBelow.indexOf('}') + 1;
420
+ catchExpEndCharOffset = catchIdentifierEnd + catchExpEndCharIndex;
421
+ }
422
+ else {
423
+ catchExpEndCharOffset = catchExpStart + rawThenNodesBelow.indexOf('}') + 1;
424
+ }
425
+ catchToken = this.sliceFragment(token.offset + catchExpStart, catchExpEndCharOffset);
354
426
  }
355
- thenToken = this.sliceFragment(token.startOffset + thenExpStart, thenExpEndCharOffset);
356
- }
357
- let catchToken = null;
358
- /**
359
- * `{#await expression}...{:then name}...{:catch name}...{/await}`
360
- * find___^
361
- *
362
- * If `then` block is not found:
363
- *
364
- * `{#await expression}...{:catch name}...{/await}`
365
- * find___^
366
- */
367
- const catchExpStart = thenToken
368
- ? (thenEnd ?? thenToken.startOffset + thenToken.raw.length)
369
- : (pendingEnd ?? awaitExpEnd);
370
- /**
371
- * `{:catch name}...{/await}`
372
- */
373
- const rawThenNodesBelow = this.rawCode.slice(catchExpStart, originBlockNode.end);
374
- if (
375
- // eslint-disable-next-line regexp/strict
376
- /^{\s*:catch[\s|}]/.test(rawThenNodesBelow)) {
377
- let catchExpEndCharOffset;
378
- if (originBlockNode.error) {
379
- const catchIdentifierEnd =
380
- // @ts-ignore - new Svelte Compiler Type doesn't support `start` and `end` yet.
381
- originBlockNode.error.end;
382
- const rawCatchExpCloseCharAndBelow = this.rawCode.slice(catchIdentifierEnd, originBlockNode.end);
383
- const catchExpEndCharIndex = rawCatchExpCloseCharAndBelow.indexOf('}') + 1;
384
- catchExpEndCharOffset = catchIdentifierEnd + catchExpEndCharIndex;
427
+ const expressions = [
428
+ this.visitPsBlock({
429
+ ...awaitExpToken,
430
+ depth: token.depth,
431
+ parentNode: token.parentNode,
432
+ nodeName: 'await',
433
+ isFragment: false,
434
+ }, originBlockNode.pending?.nodes, {
435
+ type: 'await',
436
+ expression: awaitExpToken.raw,
437
+ })[0],
438
+ ];
439
+ if (thenToken) {
440
+ expressions.push(this.visitPsBlock({
441
+ ...thenToken,
442
+ depth: token.depth,
443
+ parentNode: token.parentNode,
444
+ nodeName: 'await:then',
445
+ isFragment: false,
446
+ }, originBlockNode.then?.nodes, {
447
+ type: 'await:then',
448
+ expression: thenToken.raw,
449
+ })[0]);
385
450
  }
386
- else {
387
- catchExpEndCharOffset = catchExpStart + rawThenNodesBelow.indexOf('}') + 1;
451
+ if (catchToken) {
452
+ expressions.push(this.visitPsBlock({
453
+ ...catchToken,
454
+ depth: token.depth,
455
+ parentNode: token.parentNode,
456
+ nodeName: 'await:catch',
457
+ isFragment: false,
458
+ }, originBlockNode.catch?.nodes, {
459
+ type: 'await:catch',
460
+ expression: catchToken.raw,
461
+ })[0]);
388
462
  }
389
- catchToken = this.sliceFragment(token.startOffset + catchExpStart, catchExpEndCharOffset);
390
- }
391
- const expressions = [];
392
- expressions.push(this.visitPsBlock({
393
- ...awaitExpToken,
394
- depth: token.depth,
395
- parentNode: token.parentNode,
396
- nodeName: 'await',
397
- isFragment: false,
398
- }, originBlockNode.pending?.nodes, 'await')[0]);
399
- if (thenToken) {
400
463
  expressions.push(this.visitPsBlock({
401
- ...thenToken,
464
+ ...closeToken,
402
465
  depth: token.depth,
403
466
  parentNode: token.parentNode,
404
- nodeName: 'await:then',
467
+ nodeName: '/await',
405
468
  isFragment: false,
406
- }, originBlockNode.then?.nodes, 'await:then')[0]);
469
+ }, undefined, {
470
+ type: 'end',
471
+ expression: closeToken.raw,
472
+ })[0]);
473
+ return expressions;
407
474
  }
408
- if (catchToken) {
475
+ /**
476
+ * Parses a Svelte `{#each}` block into its constituent preprocessor-specific blocks:
477
+ * the each expression, optional `{:else}` fallback branch,
478
+ * and the closing `{/each}` tag.
479
+ *
480
+ * @param token - The child token representing the entire each block
481
+ * @param originBlockNode - The Svelte AST EachBlock node
482
+ * @returns An array of preprocessor-specific block nodes
483
+ */
484
+ #parseEachBlock(token,
485
+ // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
486
+ originBlockNode) {
487
+ const expressions = [];
488
+ /**
489
+ * `{/each}`
490
+ */
491
+ const { closeToken } = parseBlock(this, token, originBlockNode);
492
+ /**
493
+ * `{#each expression as name}...{:else}...{/each}`
494
+ * find___^
495
+ */
496
+ const bodyStart = originBlockNode.body.nodes.at(0)?.start ?? closeToken.offset;
497
+ /**
498
+ * `{#each expression as name}...{:else}...{/each}`
499
+ * find___^
500
+ */
501
+ const fallbackScopeStart = originBlockNode.fallback?.nodes.at(0)?.start ?? closeToken.offset;
502
+ /**
503
+ * `{#each expression as name}...{:else}`
504
+ */
505
+ const rawUntilFallbackScope = this.rawCode.slice(token.offset, fallbackScopeStart);
506
+ let elseToken = null;
507
+ /**
508
+ * `{#each expression as name}...{:else}`
509
+ * find___^
510
+ */
511
+ // eslint-disable-next-line regexp/strict
512
+ const elseTokenStart = rawUntilFallbackScope.match(/{\s*:else\s*}$/)?.index;
513
+ if (elseTokenStart != null) {
514
+ elseToken = this.sliceFragment(token.offset + elseTokenStart, fallbackScopeStart);
515
+ }
516
+ const eachToken = this.sliceFragment(token.offset, bodyStart);
409
517
  expressions.push(this.visitPsBlock({
410
- ...catchToken,
518
+ ...eachToken,
411
519
  depth: token.depth,
412
520
  parentNode: token.parentNode,
413
- nodeName: 'await:catch',
521
+ nodeName: 'each',
414
522
  isFragment: false,
415
- }, originBlockNode.catch?.nodes, 'await:catch')[0]);
416
- }
417
- expressions.push(this.visitPsBlock({
418
- ...closeToken,
419
- depth: token.depth,
420
- parentNode: token.parentNode,
421
- nodeName: '/await',
422
- isFragment: false,
423
- }, undefined, 'end')[0]);
424
- return expressions;
425
- }, _SvelteParser_parseEachBlock = function _SvelteParser_parseEachBlock(token,
426
- // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
427
- originBlockNode) {
428
- const expressions = [];
429
- /**
430
- * `{/each}`
431
- */
432
- const { closeToken } = parseBlock(this, token, originBlockNode);
433
- /**
434
- * `{#each expression as name}...{:else}...{/each}`
435
- * find___^
436
- */
437
- const bodyStart = originBlockNode.body.nodes.at(0)?.start ?? closeToken.startOffset;
438
- /**
439
- * `{#each expression as name}...{:else}...{/each}`
440
- * find___^
441
- */
442
- const fallbackScopeStart = originBlockNode.fallback?.nodes.at(0)?.start ?? closeToken.startOffset;
443
- /**
444
- * `{#each expression as name}...{:else}`
445
- */
446
- const rawUntilFallbackScope = this.rawCode.slice(token.startOffset, fallbackScopeStart);
447
- let elseToken = null;
448
- /**
449
- * `{#each expression as name}...{:else}`
450
- * find___^
451
- */
452
- // eslint-disable-next-line regexp/strict
453
- const elseTokenStart = rawUntilFallbackScope.match(/{\s*:else\s*}$/)?.index;
454
- if (elseTokenStart != null) {
455
- elseToken = this.sliceFragment(token.startOffset + elseTokenStart, fallbackScopeStart);
456
- }
457
- const eachToken = this.sliceFragment(token.startOffset, bodyStart);
458
- expressions.push(this.visitPsBlock({
459
- ...eachToken,
460
- depth: token.depth,
461
- parentNode: token.parentNode,
462
- nodeName: 'each',
463
- isFragment: false,
464
- }, originBlockNode.body.nodes, 'each')[0]);
465
- if (elseToken) {
523
+ }, originBlockNode.body.nodes, { type: 'each', expression: eachToken.raw })[0]);
524
+ if (elseToken) {
525
+ expressions.push(this.visitPsBlock({
526
+ ...elseToken,
527
+ depth: token.depth,
528
+ parentNode: token.parentNode,
529
+ nodeName: 'each:empty',
530
+ isFragment: false,
531
+ }, originBlockNode.fallback?.nodes, { type: 'each:empty', expression: elseToken.raw })[0]);
532
+ }
466
533
  expressions.push(this.visitPsBlock({
467
- ...elseToken,
534
+ ...closeToken,
468
535
  depth: token.depth,
469
536
  parentNode: token.parentNode,
470
- nodeName: 'each:empty',
537
+ nodeName: '/each',
471
538
  isFragment: false,
472
- }, originBlockNode.fallback?.nodes, 'each:empty')[0]);
539
+ }, undefined, { type: 'end', expression: closeToken.raw })[0]);
540
+ return expressions;
473
541
  }
474
- expressions.push(this.visitPsBlock({
475
- ...closeToken,
476
- depth: token.depth,
477
- parentNode: token.parentNode,
478
- nodeName: '/each',
479
- isFragment: false,
480
- }, undefined, 'end')[0]);
481
- return expressions;
482
- }, _SvelteParser_traverseIfBlock = function _SvelteParser_traverseIfBlock(
483
- // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
484
- originBlockNode, start, type = 'if') {
485
- const result = [];
486
- const end = originBlockNode.consequent.nodes?.[0]?.start ?? originBlockNode.end;
487
- const tag = this.sliceFragment(start, end);
488
- const children = originBlockNode.consequent.nodes;
489
- result.push({ ...tag, children, type });
490
- if (originBlockNode.alternate) {
491
- if (originBlockNode.alternate.nodes?.[0]?.type === 'IfBlock') {
492
- const elseif = __classPrivateFieldGet(this, _SvelteParser_instances, "m", _SvelteParser_traverseIfBlock).call(this, originBlockNode.alternate.nodes[0], children.at(-1)?.end ?? start, 'elseif');
493
- result.push(...elseif);
542
+ /**
543
+ * Recursively traverses a Svelte `{#if}` block and its chained `{:else if}` / `{:else}`
544
+ * branches, producing a flat list of token segments with their conditional type labels
545
+ * and child node arrays.
546
+ *
547
+ * @param originBlockNode - The Svelte AST IfBlock node to traverse
548
+ * @param start - The source offset where this block segment begins
549
+ * @param type - The conditional branch type: 'if', 'elseif', or 'else'
550
+ * @returns A flat array of token segments with children and type labels
551
+ */
552
+ #traverseIfBlock(
553
+ // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
554
+ originBlockNode, start, type = 'if') {
555
+ const result = [];
556
+ const end = originBlockNode.consequent.nodes?.[0]?.start ?? originBlockNode.end;
557
+ const tag = this.sliceFragment(start, end);
558
+ const children = originBlockNode.consequent.nodes;
559
+ result.push({ ...tag, children, type });
560
+ if (originBlockNode.alternate) {
561
+ if (originBlockNode.alternate.nodes?.[0]?.type === 'IfBlock') {
562
+ const elseif = this.#traverseIfBlock(originBlockNode.alternate.nodes[0], children.at(-1)?.end ?? start, 'elseif');
563
+ result.push(...elseif);
564
+ }
565
+ else {
566
+ const start = children.at(-1)?.end ?? originBlockNode.end;
567
+ const end = originBlockNode.alternate.nodes?.[0]?.start;
568
+ const tag = this.sliceFragment(start, end);
569
+ result.push({
570
+ ...tag,
571
+ children: originBlockNode.alternate.nodes,
572
+ type: 'else',
573
+ });
574
+ }
494
575
  }
495
- else {
496
- const start = children.at(-1)?.end ?? originBlockNode.end;
497
- const end = originBlockNode.alternate.nodes?.[0]?.start;
576
+ {
577
+ const start = result.at(-1)?.children.at(-1)?.end ?? originBlockNode.end;
578
+ const end = originBlockNode.end;
498
579
  const tag = this.sliceFragment(start, end);
499
- result.push({
500
- ...tag,
501
- children: originBlockNode.alternate.nodes,
502
- type: 'else',
503
- });
504
- }
505
- }
506
- {
507
- const start = result.at(-1)?.children.at(-1)?.end ?? originBlockNode.end;
508
- const end = originBlockNode.end;
509
- const tag = this.sliceFragment(start, end);
510
- if (tag.raw) {
511
- result.push({ ...tag, children: [], type: '/if' });
580
+ if (tag.raw) {
581
+ result.push({ ...tag, children: [], type: '/if' });
582
+ }
512
583
  }
584
+ return result;
513
585
  }
514
- return result;
515
- };
586
+ }
516
587
  export const parser = new SvelteParser();
@@ -1,10 +1,23 @@
1
1
  import type { AST } from 'svelte/compiler';
2
+ /** Union of Svelte AST node types that can appear as children in a Svelte template fragment. */
2
3
  export type SvelteNode = AST.Text | AST.Comment | AST.Tag | AST.ElementLike | AST.Block;
4
+ /** Represents a Svelte `{#if}` block with consequent, alternate, and elseif branches. */
3
5
  export type SvelteIfBlock = AST.IfBlock;
6
+ /** Represents a Svelte `{#each}` block with iteration body and optional fallback. */
4
7
  export type SvelteEachBlock = AST.EachBlock;
8
+ /** Represents a Svelte `{#await}` block with pending, then, and catch branches. */
5
9
  export type SvelteAwaitBlock = AST.AwaitBlock;
10
+ /**
11
+ * Parses a Svelte template string into an array of top-level AST nodes
12
+ * using the Svelte compiler's modern parser mode.
13
+ *
14
+ * @param template - The raw Svelte template source code
15
+ * @returns An array of top-level Svelte AST nodes from the template fragment
16
+ */
6
17
  export declare function svelteParse(template: string): SvelteNode[];
18
+ /** Union of all Svelte directive and attribute types that can appear on elements. */
7
19
  export type SvelteDirective = Directive | AST.Attribute | AST.SpreadAttribute;
20
+ /** Union of all Svelte block types that have opening/closing tag syntax. */
8
21
  export type SvelteBlock = AST.EachBlock | AST.IfBlock | AST.AwaitBlock | AST.KeyBlock | AST.SnippetBlock | AST.SvelteBoundary;
9
22
  type Directive = AST.AnimateDirective | AST.BindDirective | AST.ClassDirective | AST.LetDirective | AST.OnDirective | AST.StyleDirective | AST.TransitionDirective | AST.UseDirective;
10
23
  export {};
@@ -1,4 +1,11 @@
1
1
  import { parse } from 'svelte/compiler';
2
+ /**
3
+ * Parses a Svelte template string into an array of top-level AST nodes
4
+ * using the Svelte compiler's modern parser mode.
5
+ *
6
+ * @param template - The raw Svelte template source code
7
+ * @returns An array of top-level Svelte AST nodes from the template fragment
8
+ */
2
9
  export function svelteParse(template) {
3
10
  const ast = parse(template, { modern: true });
4
11
  return ast.fragment.nodes ?? [];
@@ -1,4 +1,10 @@
1
1
  import { HtmlParser } from '@markuplint/html-parser';
2
+ /**
3
+ * Parser for SvelteKit app template files (e.g., `app.html`).
4
+ * Extends the standard HTML parser to handle SvelteKit placeholder tags
5
+ * such as `%sveltekit.head%` and `%sveltekit.body%`, which are treated
6
+ * as opaque preprocessor-specific blocks.
7
+ */
2
8
  declare class SvelteKitTemplateParser extends HtmlParser {
3
9
  constructor();
4
10
  }
@@ -1,4 +1,10 @@
1
1
  import { HtmlParser } from '@markuplint/html-parser';
2
+ /**
3
+ * Parser for SvelteKit app template files (e.g., `app.html`).
4
+ * Extends the standard HTML parser to handle SvelteKit placeholder tags
5
+ * such as `%sveltekit.head%` and `%sveltekit.body%`, which are treated
6
+ * as opaque preprocessor-specific blocks.
7
+ */
2
8
  class SvelteKitTemplateParser extends HtmlParser {
3
9
  constructor() {
4
10
  super({
package/package.json CHANGED
@@ -1,10 +1,13 @@
1
1
  {
2
2
  "name": "@markuplint/svelte-parser",
3
- "version": "4.7.12",
3
+ "version": "5.0.0-alpha.0",
4
4
  "description": "Svelte parser for markuplint",
5
5
  "repository": "git@github.com:markuplint/markuplint.git",
6
6
  "author": "Yusuke Hirao <yusukehirao@me.com>",
7
7
  "license": "MIT",
8
+ "engines": {
9
+ "node": ">=22"
10
+ },
8
11
  "type": "module",
9
12
  "exports": {
10
13
  ".": {
@@ -29,10 +32,10 @@
29
32
  "clean": "tsc --build --clean tsconfig.build.json"
30
33
  },
31
34
  "dependencies": {
32
- "@markuplint/html-parser": "4.6.22",
33
- "@markuplint/ml-ast": "4.4.10",
34
- "@markuplint/parser-utils": "4.8.10",
35
- "svelte": "5.38.10"
35
+ "@markuplint/html-parser": "5.0.0-alpha.0",
36
+ "@markuplint/ml-ast": "5.0.0-alpha.0",
37
+ "@markuplint/parser-utils": "5.0.0-alpha.0",
38
+ "svelte": "5.53.0"
36
39
  },
37
- "gitHead": "6213ea30269ef404f030e67bbcc7fc7443ec1060"
40
+ "gitHead": "13dcfc84ec83d87360c720e253383b60767e1b56"
38
41
  }