@markuplint/svelte-parser 5.4.0 → 5.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/lib/component-scanner.js +2 -1
- package/lib/parser.d.ts +1 -1
- package/lib/parser.js +88 -16
- package/lib/svelte-parser/index.d.ts +27 -0
- package/lib/svelte-parser/index.js +210 -0
- package/package.json +7 -7
package/lib/component-scanner.js
CHANGED
|
@@ -115,7 +115,8 @@ function extractAttrs(el) {
|
|
|
115
115
|
}
|
|
116
116
|
const value = attr.value.raw;
|
|
117
117
|
if (attr.isDynamicValue) {
|
|
118
|
-
|
|
118
|
+
// `disabled="{false}"` is the expression `false` in quotes, which the parser keeps as `potentialValue`
|
|
119
|
+
const expression = (attr.potentialValue ?? value).trim();
|
|
119
120
|
addAttr(attrs, { name: attr.nodeName, dynamic: true, ...(expression === '' ? {} : { expression }) });
|
|
120
121
|
}
|
|
121
122
|
else if (INTERPOLATION.test(value)) {
|
package/lib/parser.d.ts
CHANGED
|
@@ -56,6 +56,7 @@ export declare class SvelteParser extends Parser<SvelteNode> {
|
|
|
56
56
|
visitAttr(token: Token): (import("@markuplint/ml-ast").MLASTSpreadAttr & {
|
|
57
57
|
__rightText?: string;
|
|
58
58
|
}) | {
|
|
59
|
+
potentialValue?: string;
|
|
59
60
|
isDynamicValue: true | undefined;
|
|
60
61
|
potentialName: string | undefined;
|
|
61
62
|
type: "attr";
|
|
@@ -69,7 +70,6 @@ export declare class SvelteParser extends Parser<SvelteNode> {
|
|
|
69
70
|
value: import("@markuplint/ml-ast").MLASTToken;
|
|
70
71
|
endQuote: import("@markuplint/ml-ast").MLASTToken;
|
|
71
72
|
isDirective?: true;
|
|
72
|
-
potentialValue?: string;
|
|
73
73
|
valueType?: "string" | "number" | "boolean" | "code";
|
|
74
74
|
candidate?: string;
|
|
75
75
|
isDuplicatable: boolean;
|
package/lib/parser.js
CHANGED
|
@@ -1,6 +1,20 @@
|
|
|
1
|
-
import { ParserError, Parser, AttrState } from '@markuplint/parser-utils';
|
|
1
|
+
import { ParserError, Parser, AttrState, isBracedExpression } from '@markuplint/parser-utils';
|
|
2
2
|
import { parseBlock } from './parse-block.js';
|
|
3
|
-
import {
|
|
3
|
+
import { svelteParseComponent } from './svelte-parser/index.js';
|
|
4
|
+
/**
|
|
5
|
+
* The opening of an attachment (`{@attach expr}`), up to the keyword, with the braces and spaces as
|
|
6
|
+
* Svelte allows. A shorthand (`{attach}`) is not one: the keyword is followed by a space.
|
|
7
|
+
*/
|
|
8
|
+
const ATTACHMENT = /^(\s*\{\s*)@attach(?=\s)/;
|
|
9
|
+
/**
|
|
10
|
+
* Replaces the keyword of an attachment with spaces of the same length.
|
|
11
|
+
*
|
|
12
|
+
* @param raw - The source that starts with an attachment
|
|
13
|
+
* @returns The source whose keyword is spaces
|
|
14
|
+
*/
|
|
15
|
+
function maskAttachKeyword(raw) {
|
|
16
|
+
return raw.replace(ATTACHMENT, (_match, opening) => opening + ' '.repeat('@attach'.length));
|
|
17
|
+
}
|
|
4
18
|
/**
|
|
5
19
|
* Parser implementation for Svelte component templates.
|
|
6
20
|
* Extends the base Parser to handle Svelte elements, text, comments,
|
|
@@ -13,26 +27,34 @@ export class SvelteParser extends Parser {
|
|
|
13
27
|
super({
|
|
14
28
|
endTagType: 'xml',
|
|
15
29
|
tagNameCaseSensitive: true,
|
|
30
|
+
// A value in `{…}` is JavaScript
|
|
31
|
+
attrExpressionLanguage: 'javascript',
|
|
16
32
|
// A static attribute is in the HTML string that the compiler hands to the browser's parser,
|
|
17
33
|
// and a dynamic one is set by `set_attribute` under the name as written.
|
|
18
34
|
attrNameRoute: 'dom-api-when-dynamic',
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
],
|
|
30
|
-
maskChar: '-',
|
|
35
|
+
// `<slot>` is replaced with what the parent component passes to it, not the HTML `slot` element
|
|
36
|
+
// of a shadow tree
|
|
37
|
+
// @see https://svelte.dev/docs/svelte/legacy-slots
|
|
38
|
+
slotElement: 'framework',
|
|
39
|
+
// No `ignoreTags`:
|
|
40
|
+
// - The script tag needs to be passed with the lang attribute to the parser, so instead of
|
|
41
|
+
// masking it, it is converted to PSBlock in visitText.
|
|
42
|
+
// - The top-level style is kept out of Svelte's CSS parse in `svelteParseComponent`; nested ones stay elements.
|
|
43
|
+
// `svelte/compiler` strips a leading BOM and counts positions from the next character
|
|
44
|
+
leadingBom: 'mask',
|
|
31
45
|
});
|
|
32
46
|
}
|
|
47
|
+
/**
|
|
48
|
+
* A top-level `<style>` is masked as a comment before Svelte parses it (see `svelteParseComponent`),
|
|
49
|
+
* so `nodeize` needs to know which comments stand for one. The parser object is reused, so this is
|
|
50
|
+
* set again by every `tokenize`.
|
|
51
|
+
*/
|
|
52
|
+
#topLevelStyleStarts = new Set();
|
|
33
53
|
tokenize() {
|
|
54
|
+
const { nodes, topLevelStyleStarts } = svelteParseComponent(this.rawCode);
|
|
55
|
+
this.#topLevelStyleStarts = topLevelStyleStarts;
|
|
34
56
|
return {
|
|
35
|
-
ast:
|
|
57
|
+
ast: nodes,
|
|
36
58
|
isFragment: true,
|
|
37
59
|
};
|
|
38
60
|
}
|
|
@@ -63,6 +85,16 @@ export class SvelteParser extends Parser {
|
|
|
63
85
|
});
|
|
64
86
|
}
|
|
65
87
|
case 'Comment': {
|
|
88
|
+
// The mask of a top-level `<style>`, the CSS of the component (see `svelteParseComponent`)
|
|
89
|
+
if (this.#topLevelStyleStarts.has(originNode.start)) {
|
|
90
|
+
return this.visitPsBlock({
|
|
91
|
+
...token,
|
|
92
|
+
depth,
|
|
93
|
+
parentNode,
|
|
94
|
+
nodeName: 'Style',
|
|
95
|
+
isFragment: false,
|
|
96
|
+
});
|
|
97
|
+
}
|
|
66
98
|
return this.visitComment({
|
|
67
99
|
...token,
|
|
68
100
|
depth,
|
|
@@ -273,7 +305,11 @@ export class SvelteParser extends Parser {
|
|
|
273
305
|
* @returns The parsed attribute node with Svelte-specific metadata
|
|
274
306
|
*/
|
|
275
307
|
visitAttr(token) {
|
|
276
|
-
|
|
308
|
+
// `@attach` is not JavaScript, so the tokenizer, which finds the end of `{…}` by reading it as
|
|
309
|
+
// JavaScript, cannot read it. Spaces of the same length stand for it, which makes the attachment
|
|
310
|
+
// a shorthand (`{expr}`) whose end the tokenizer finds, and every offset stays the source's.
|
|
311
|
+
const isAttachment = ATTACHMENT.test(token.raw);
|
|
312
|
+
const attr = super.visitAttr(isAttachment ? { ...token, raw: maskAttachKeyword(token.raw) } : token, {
|
|
277
313
|
quoteSet: [
|
|
278
314
|
{ start: '"', end: '"', type: 'string' },
|
|
279
315
|
{ start: "'", end: "'", type: 'string' },
|
|
@@ -286,8 +322,25 @@ export class SvelteParser extends Parser {
|
|
|
286
322
|
if (attr.type === 'spread') {
|
|
287
323
|
return attr;
|
|
288
324
|
}
|
|
325
|
+
// `{@attach expr}`: "Attachments are functions that run in an effect when an
|
|
326
|
+
// element is mounted". The function gets the element and can do anything to it, its attributes
|
|
327
|
+
// included, so it is read as a spread.
|
|
328
|
+
if (isAttachment) {
|
|
329
|
+
// The token of the attribute starts at its `{`: the spaces before it are not in `attr.raw`, nor in
|
|
330
|
+
// `source`, so the length of the attribute is the length of the text of `source` that it takes
|
|
331
|
+
const source = token.raw.trimStart();
|
|
332
|
+
const attachmentLength = attr.raw.length;
|
|
333
|
+
const rightText = source.slice(attachmentLength);
|
|
334
|
+
return {
|
|
335
|
+
...this.createToken(source.slice(0, attachmentLength), attr.offset, attr.line, attr.col),
|
|
336
|
+
type: 'spread',
|
|
337
|
+
nodeName: '#spread',
|
|
338
|
+
...(rightText === '' ? {} : { __rightText: rightText }),
|
|
339
|
+
};
|
|
340
|
+
}
|
|
289
341
|
let isDynamicValue = attr.startQuote.raw === '{' || undefined;
|
|
290
342
|
let potentialName;
|
|
343
|
+
let potentialValue;
|
|
291
344
|
// Shorthand {name} → potentialName = value
|
|
292
345
|
if (isDynamicValue && attr.name.raw === '') {
|
|
293
346
|
potentialName = attr.value.raw;
|
|
@@ -296,10 +349,29 @@ export class SvelteParser extends Parser {
|
|
|
296
349
|
if (attr.startQuote.raw === '{' && attr.endQuote.raw === '}') {
|
|
297
350
|
isDynamicValue = true;
|
|
298
351
|
}
|
|
352
|
+
// A single expression in quotes is the expression: `disabled="{false}"` is `disabled={false}`:
|
|
353
|
+
//
|
|
354
|
+
// > Quoting a singular expression does not affect how the value is parsed, but in Svelte 6 it
|
|
355
|
+
// > will cause the value to be coerced to a string:
|
|
356
|
+
//
|
|
357
|
+
// @see https://svelte.dev/docs/svelte/basic-markup#Element-attributes
|
|
358
|
+
//
|
|
359
|
+
// A value with text around the expression (`page/{p}`) or several expressions is a string. The
|
|
360
|
+
// tokens stay those of the source (the quotes are the quotes), and the expression is
|
|
361
|
+
// `potentialValue`.
|
|
362
|
+
if (!isDynamicValue &&
|
|
363
|
+
attr.name.raw !== '' &&
|
|
364
|
+
(attr.startQuote.raw === '"' || attr.startQuote.raw === "'") &&
|
|
365
|
+
attr.endQuote.raw === attr.startQuote.raw &&
|
|
366
|
+
isBracedExpression(attr.value.raw)) {
|
|
367
|
+
isDynamicValue = true;
|
|
368
|
+
potentialValue = attr.value.raw.slice(1, -1);
|
|
369
|
+
}
|
|
299
370
|
return {
|
|
300
371
|
...attr,
|
|
301
372
|
isDynamicValue,
|
|
302
373
|
potentialName,
|
|
374
|
+
...(potentialValue == null ? {} : { potentialValue }),
|
|
303
375
|
};
|
|
304
376
|
}
|
|
305
377
|
/**
|
|
@@ -9,6 +9,33 @@ export type SvelteAwaitBlock = AST.AwaitBlock;
|
|
|
9
9
|
* `SnippetBlock`, `RenderTag`, and `SvelteBoundary`.
|
|
10
10
|
*/
|
|
11
11
|
export declare function svelteParse(template: string): SvelteNode[];
|
|
12
|
+
/**
|
|
13
|
+
* Parses a component, keeping the top-level `<style>` out of Svelte's CSS parser and the nested one in
|
|
14
|
+
* the markup.
|
|
15
|
+
*
|
|
16
|
+
* > There can only be one top-level <style> tag per component.
|
|
17
|
+
* >
|
|
18
|
+
* > However, it is possible to have a <style> tag nested inside other elements or logic blocks.
|
|
19
|
+
* >
|
|
20
|
+
* > In that case, the <style> tag will be inserted as-is into the DOM; no scoping or processing will
|
|
21
|
+
* > be done on the <style> tag.
|
|
22
|
+
*
|
|
23
|
+
* The top-level one is the CSS of the component, and Svelte reads it as CSS (`lang="scss"` is not CSS and
|
|
24
|
+
* fails: "Expected a valid CSS identifier"), so it is masked. The nested one is an element whose content
|
|
25
|
+
* is text, which Svelte reads without parsing it. What is top-level cannot be told from the source by a
|
|
26
|
+
* pattern. The source is tried with Svelte as-is first (`ast.css` for the top-level one). When CSS parsing
|
|
27
|
+
* fails (`css_*`), only that style block is masked and parsing is retried. When that is still not enough,
|
|
28
|
+
* every `<style>` is masked and the ones that are children of the fragment are the top-level ones.
|
|
29
|
+
*
|
|
30
|
+
* @see https://svelte.dev/docs/svelte/nested-style-elements#main
|
|
31
|
+
* @param template - The source of the component
|
|
32
|
+
* @returns The nodes, and the offsets in `template` where a top-level `<style>` starts: the nodes have
|
|
33
|
+
* a comment (`Comment`) there, and the positions of every node are positions in `template`
|
|
34
|
+
*/
|
|
35
|
+
export declare function svelteParseComponent(template: string): {
|
|
36
|
+
readonly nodes: SvelteNode[];
|
|
37
|
+
readonly topLevelStyleStarts: ReadonlySet<number>;
|
|
38
|
+
};
|
|
12
39
|
export type SvelteDirective = Directive | AST.Attribute | AST.SpreadAttribute;
|
|
13
40
|
export type SvelteBlock = AST.EachBlock | AST.IfBlock | AST.AwaitBlock | AST.KeyBlock | AST.SnippetBlock | AST.SvelteBoundary;
|
|
14
41
|
type Directive = AST.AnimateDirective | AST.BindDirective | AST.ClassDirective | AST.LetDirective | AST.OnDirective | AST.StyleDirective | AST.TransitionDirective | AST.UseDirective;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isFatalError } from '@markuplint/shared';
|
|
1
2
|
import { parse } from 'svelte/compiler';
|
|
2
3
|
/**
|
|
3
4
|
* The `modern: true` option opts into Svelte 5's modern AST format,
|
|
@@ -8,3 +9,212 @@ export function svelteParse(template) {
|
|
|
8
9
|
const ast = parse(template, { modern: true });
|
|
9
10
|
return ast.fragment.nodes ?? [];
|
|
10
11
|
}
|
|
12
|
+
/**
|
|
13
|
+
* What the source is read in order as, so that a `<style` is a `<style>` block only where one can start: a
|
|
14
|
+
* comment (`<!-- … -->`) and a `<script>` block are consumed whole, and a `<style` in their text is not
|
|
15
|
+
* one. A `<style>` block runs from `<style` to the first `</style>`, or to the end of the source when it
|
|
16
|
+
* is not closed; the name ends there (`<style-guide>` is a custom element).
|
|
17
|
+
*
|
|
18
|
+
* It does not read an attribute value or an expression (`{…}`), so a `<style` in one is still taken as
|
|
19
|
+
* a block. `svelteParseComponent` tries Svelte first so that case is handled without this mask; only the
|
|
20
|
+
* last fallback still uses `SOURCE_BLOCK`.
|
|
21
|
+
*/
|
|
22
|
+
const SOURCE_BLOCK = /<!--[\s\S]*?(?:-->|$)|<script(?=[\s/>])[\s\S]+?(?:<\/script>|$)|<style(?=[\s/>])[\s\S]+?(?:<\/style>|$)/g;
|
|
23
|
+
/**
|
|
24
|
+
* Replaces each `<style>` block of the source with what `replacer` returns, and leaves everything else.
|
|
25
|
+
*
|
|
26
|
+
* @param template - The source of the component
|
|
27
|
+
* @param replacer - Gives the replacement of a `<style>` block, from the block and its offset in `template`
|
|
28
|
+
* @returns The source with the replaced blocks
|
|
29
|
+
*/
|
|
30
|
+
function replaceStyleBlocks(template, replacer) {
|
|
31
|
+
return template.replaceAll(SOURCE_BLOCK, (block, offset) => block.startsWith('<style') ? replacer(block, offset) : block);
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* The shortest block that a comment (`<!---->`) can mask. A shorter one is `<style` cut off by the end of
|
|
35
|
+
* the source, which no comment of the same length can stand for.
|
|
36
|
+
*/
|
|
37
|
+
const MIN_MASKABLE_LENGTH = '<!---->'.length;
|
|
38
|
+
/**
|
|
39
|
+
* Replaces a `<style>` block with a comment (`<!---…--->`) of the same length and line breaks, so that
|
|
40
|
+
* every offset in the source stays the offset in the result. A block that is too short to be a comment
|
|
41
|
+
* is left as it is, so that Svelte reports the syntax error of the source, not one of a mask.
|
|
42
|
+
*
|
|
43
|
+
* @param block - A `<style>` block
|
|
44
|
+
* @returns The comment
|
|
45
|
+
*/
|
|
46
|
+
function maskStyleBlock(block) {
|
|
47
|
+
if (block.length < MIN_MASKABLE_LENGTH) {
|
|
48
|
+
return block;
|
|
49
|
+
}
|
|
50
|
+
return '<!' + block.slice(2, -1).replaceAll(/[^\n]/g, '-') + '>';
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* `afterCssError` must not treat markup or script errors as a failed top-level style block: only Svelte's
|
|
54
|
+
* own CSS parse step (`css_*`) qualifies.
|
|
55
|
+
*/
|
|
56
|
+
function isSvelteCssParseError(error) {
|
|
57
|
+
return (typeof error === 'object' &&
|
|
58
|
+
error !== null &&
|
|
59
|
+
'code' in error &&
|
|
60
|
+
typeof error.code === 'string' &&
|
|
61
|
+
error.code.startsWith('css_'));
|
|
62
|
+
}
|
|
63
|
+
const STYLE_OPENER = /<style(?=[\s/>])/g;
|
|
64
|
+
/**
|
|
65
|
+
* The `<style>` block that contains `index`. When a `<style` in an attribute value or an expression opens
|
|
66
|
+
* a spurious range to the next `</style>`, the real block is the one that starts last among the openers
|
|
67
|
+
* before `index`.
|
|
68
|
+
*
|
|
69
|
+
* @param template - The source of the component
|
|
70
|
+
* @param index - A character offset in `template`
|
|
71
|
+
* @returns The block span, if any
|
|
72
|
+
*/
|
|
73
|
+
function styleBlockAroundIndex(template, index) {
|
|
74
|
+
let best = null;
|
|
75
|
+
for (const match of template.matchAll(STYLE_OPENER)) {
|
|
76
|
+
if (match.index === undefined) {
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
const start = match.index;
|
|
80
|
+
const close = template.indexOf('</style>', start);
|
|
81
|
+
const end = close === -1 ? template.length : close + '</style>'.length;
|
|
82
|
+
if (index >= start && index < end && (best === null || start > best.start)) {
|
|
83
|
+
best = { start, end };
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return best;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Masks the top-level `<style>` that made Svelte throw `css_*`, then parses again with
|
|
90
|
+
* {@link svelteParseComponentDirect}.
|
|
91
|
+
*
|
|
92
|
+
* @param template - The source of the component
|
|
93
|
+
* @param error - The error from the first `parse` of `template`
|
|
94
|
+
* @returns The nodes when masking one block is enough, otherwise `null`
|
|
95
|
+
*/
|
|
96
|
+
function svelteParseComponentAfterCssError(template, error) {
|
|
97
|
+
if (!isSvelteCssParseError(error) || !('start' in error)) {
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
const { character } = error.start;
|
|
101
|
+
const block = styleBlockAroundIndex(template, character);
|
|
102
|
+
if (!block) {
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
const masked = template.slice(0, block.start) +
|
|
106
|
+
maskStyleBlock(template.slice(block.start, block.end)) +
|
|
107
|
+
template.slice(block.end);
|
|
108
|
+
const parsed = svelteParseComponentDirect(masked);
|
|
109
|
+
if (!parsed) {
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
return {
|
|
113
|
+
nodes: parsed.nodes,
|
|
114
|
+
topLevelStyleStarts: new Set([...parsed.topLevelStyleStarts, block.start]),
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Builds nodes from an already-parsed component. The top-level `<style>` is `ast.css`, not in
|
|
119
|
+
* `ast.fragment`, so a `Comment` at the same span stands in for `SvelteParser` `nodeize`.
|
|
120
|
+
*
|
|
121
|
+
* @param ast - The component root from `svelte/compiler` `parse`
|
|
122
|
+
* @returns The fragment nodes and top-level style offsets in the source
|
|
123
|
+
*/
|
|
124
|
+
function nodesFromParsedComponent(ast) {
|
|
125
|
+
const topLevelStyleStarts = new Set();
|
|
126
|
+
let nodes = ast.fragment.nodes ?? [];
|
|
127
|
+
if (ast.css) {
|
|
128
|
+
topLevelStyleStarts.add(ast.css.start);
|
|
129
|
+
const styleNode = {
|
|
130
|
+
type: 'Comment',
|
|
131
|
+
start: ast.css.start,
|
|
132
|
+
end: ast.css.end,
|
|
133
|
+
data: '',
|
|
134
|
+
};
|
|
135
|
+
const index = nodes.findIndex((node) => node.start > ast.css.start);
|
|
136
|
+
if (index === -1) {
|
|
137
|
+
nodes = [...nodes, styleNode];
|
|
138
|
+
}
|
|
139
|
+
else {
|
|
140
|
+
nodes = [...nodes.slice(0, index), styleNode, ...nodes.slice(index)];
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return { nodes, topLevelStyleStarts };
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Parses `template` with Svelte when the source is valid markup and CSS.
|
|
147
|
+
*
|
|
148
|
+
* @param template - The source of the component
|
|
149
|
+
* @returns The nodes and offsets, or `null` when `parse` fails and the failure is not fatal
|
|
150
|
+
*/
|
|
151
|
+
function svelteParseComponentDirect(template) {
|
|
152
|
+
try {
|
|
153
|
+
return nodesFromParsedComponent(parse(template, { modern: true }));
|
|
154
|
+
}
|
|
155
|
+
catch (error) {
|
|
156
|
+
if (isFatalError(error)) {
|
|
157
|
+
throw error;
|
|
158
|
+
}
|
|
159
|
+
return null;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Parses a component, keeping the top-level `<style>` out of Svelte's CSS parser and the nested one in
|
|
164
|
+
* the markup.
|
|
165
|
+
*
|
|
166
|
+
* > There can only be one top-level <style> tag per component.
|
|
167
|
+
* >
|
|
168
|
+
* > However, it is possible to have a <style> tag nested inside other elements or logic blocks.
|
|
169
|
+
* >
|
|
170
|
+
* > In that case, the <style> tag will be inserted as-is into the DOM; no scoping or processing will
|
|
171
|
+
* > be done on the <style> tag.
|
|
172
|
+
*
|
|
173
|
+
* The top-level one is the CSS of the component, and Svelte reads it as CSS (`lang="scss"` is not CSS and
|
|
174
|
+
* fails: "Expected a valid CSS identifier"), so it is masked. The nested one is an element whose content
|
|
175
|
+
* is text, which Svelte reads without parsing it. What is top-level cannot be told from the source by a
|
|
176
|
+
* pattern. The source is tried with Svelte as-is first (`ast.css` for the top-level one). When CSS parsing
|
|
177
|
+
* fails (`css_*`), only that style block is masked and parsing is retried. When that is still not enough,
|
|
178
|
+
* every `<style>` is masked and the ones that are children of the fragment are the top-level ones.
|
|
179
|
+
*
|
|
180
|
+
* @see https://svelte.dev/docs/svelte/nested-style-elements#main
|
|
181
|
+
* @param template - The source of the component
|
|
182
|
+
* @returns The nodes, and the offsets in `template` where a top-level `<style>` starts: the nodes have
|
|
183
|
+
* a comment (`Comment`) there, and the positions of every node are positions in `template`
|
|
184
|
+
*/
|
|
185
|
+
export function svelteParseComponent(template) {
|
|
186
|
+
let caught;
|
|
187
|
+
try {
|
|
188
|
+
return nodesFromParsedComponent(parse(template, { modern: true }));
|
|
189
|
+
}
|
|
190
|
+
catch (error) {
|
|
191
|
+
if (isFatalError(error)) {
|
|
192
|
+
throw error;
|
|
193
|
+
}
|
|
194
|
+
caught = error;
|
|
195
|
+
}
|
|
196
|
+
const afterCssError = svelteParseComponentAfterCssError(template, caught);
|
|
197
|
+
if (afterCssError) {
|
|
198
|
+
return afterCssError;
|
|
199
|
+
}
|
|
200
|
+
const blocks = [];
|
|
201
|
+
const everyStyleMasked = replaceStyleBlocks(template, (block, offset) => {
|
|
202
|
+
blocks.push({ start: offset, end: offset + block.length });
|
|
203
|
+
return maskStyleBlock(block);
|
|
204
|
+
});
|
|
205
|
+
if (blocks.length === 0) {
|
|
206
|
+
return { nodes: svelteParse(template), topLevelStyleStarts: new Set() };
|
|
207
|
+
}
|
|
208
|
+
const topLevelStyleStarts = new Set();
|
|
209
|
+
const nodesOfEveryMasked = svelteParse(everyStyleMasked);
|
|
210
|
+
for (const node of nodesOfEveryMasked) {
|
|
211
|
+
if (node.type === 'Comment' && blocks.some(block => block.start === node.start && block.end === node.end)) {
|
|
212
|
+
topLevelStyleStarts.add(node.start);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
if (topLevelStyleStarts.size === blocks.length) {
|
|
216
|
+
return { nodes: nodesOfEveryMasked, topLevelStyleStarts };
|
|
217
|
+
}
|
|
218
|
+
const onlyTopLevelMasked = replaceStyleBlocks(template, (block, offset) => topLevelStyleStarts.has(offset) ? maskStyleBlock(block) : block);
|
|
219
|
+
return { nodes: svelteParse(onlyTopLevelMasked), topLevelStyleStarts };
|
|
220
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@markuplint/svelte-parser",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.6.0",
|
|
4
4
|
"description": "Svelte parser for markuplint",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -40,11 +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
|
-
"@markuplint/shared": "5.
|
|
47
|
-
"svelte": "^5.57.
|
|
43
|
+
"@markuplint/html-parser": "5.6.0",
|
|
44
|
+
"@markuplint/ml-ast": "5.6.0",
|
|
45
|
+
"@markuplint/parser-utils": "5.6.0",
|
|
46
|
+
"@markuplint/shared": "5.6.0",
|
|
47
|
+
"svelte": "^5.57.3"
|
|
48
48
|
},
|
|
49
|
-
"gitHead": "
|
|
49
|
+
"gitHead": "eade3d7202682e567f3c1b6985478f1d4b5c5481"
|
|
50
50
|
}
|