Skip to content

Commit 699c7a9

Browse files
authored
feat: never quote single attribute expressions in Svelte 5 (#451)
In Svelte 5, attributes are never quoted, because this will mean "stringify this attribute value" in a future Svelte version Related to sveltejs/svelte#7925
1 parent 10caec6 commit 699c7a9

File tree

8 files changed

+54
-16
lines changed

8 files changed

+54
-16
lines changed

README.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,12 +99,16 @@ More strict HTML syntax: Quotes in attributes and no self-closing DOM elements (
9999

100100
> In version 2 this overruled `svelteAllowShorthand`, which is no longer the case.
101101
102+
> In Svelte 5, attributes are never quoted, because this will mean "stringify this attribute value" in a future Svelte version
103+
102104
Example:
103105

104106
<!-- prettier-ignore -->
105107
```html
106-
<!-- svelteStrictMode: true -->
108+
<!-- svelteStrictMode: true (Svelte 3 and 4) -->
107109
<div foo="{bar}"></div>
110+
<!-- svelteStrictMode: true (Svelte 5) -->
111+
<div foo={bar}></div>
108112

109113
<!-- svelteStrictMode: false -->
110114
<div foo={bar} />

src/embed.ts

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,13 @@ export function embed(path: FastPath, _options: Options) {
7878
const parent: Node = path.getParentNode();
7979
const printJsExpression = () =>
8080
(parent as any).expression
81-
? printJS(parent, options.svelteStrictMode ?? false, false, false, 'expression')
81+
? printJS(
82+
parent,
83+
(options.svelteStrictMode && !options._svelte_is5Plus) ?? false,
84+
false,
85+
false,
86+
'expression',
87+
)
8288
: undefined;
8389
const printSvelteBlockJS = (name: string) => printJS(parent, false, true, false, name);
8490

@@ -110,7 +116,13 @@ export function embed(path: FastPath, _options: Options) {
110116
}
111117
break;
112118
case 'Element':
113-
printJS(parent, options.svelteStrictMode ?? false, false, false, 'tag');
119+
printJS(
120+
parent,
121+
(options.svelteStrictMode && !options._svelte_is5Plus) ?? false,
122+
false,
123+
false,
124+
'tag',
125+
);
114126
break;
115127
case 'MustacheTag':
116128
printJS(parent, isInsideQuotedAttribute(path, options), false, false, 'expression');

src/index.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { ASTNode } from './print/nodes';
55
import { embed, getVisitorKeys } from './embed';
66
import { snipScriptAndStyleTagContent } from './lib/snipTagContent';
77
import { parse, VERSION } from 'svelte/compiler';
8+
import { ParserOptions } from './options';
89

910
const babelParser = prettierPluginBabel.parsers.babel;
1011
const typescriptParser = prettierPluginBabel.parsers['babel-ts']; // TODO use TypeScript parser in next major?
@@ -46,7 +47,7 @@ export const parsers: Record<string, Parser> = {
4647
throw err;
4748
}
4849
},
49-
preprocess: (text, options) => {
50+
preprocess: (text, options: ParserOptions) => {
5051
const result = snipScriptAndStyleTagContent(text);
5152
text = result.text.trim();
5253
// Prettier sets the preprocessed text as the originalText in case
@@ -56,7 +57,8 @@ export const parsers: Record<string, Parser> = {
5657
// Therefore we do it ourselves here.
5758
options.originalText = text;
5859
// Only Svelte 5 can have TS in the template
59-
(options as any)._svelte_ts = isSvelte5Plus && result.isTypescript;
60+
options._svelte_ts = isSvelte5Plus && result.isTypescript;
61+
options._svelte_is5Plus = isSvelte5Plus;
6062
return text;
6163
},
6264
locStart,

src/options.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,15 @@ import { SortOrder, PluginConfig } from '..';
33

44
export interface ParserOptions<T = any> extends PrettierParserOptions<T>, Partial<PluginConfig> {
55
_svelte_ts?: boolean;
6+
_svelte_asFunction?: boolean;
7+
/**
8+
* Used for
9+
* - deciding what quote behavior to use in the printer:
10+
* A future version of Svelte treats quoted expressions as strings, so never use quotes in that case.
11+
* Since Svelte 5 does still treat them equally, it's safer to remove quotes in all cases and in a future
12+
* version of this plugin instead leave it up to the user to decide.
13+
*/
14+
_svelte_is5Plus?: boolean;
615
}
716

817
function makeChoice(choice: string) {

src/print/index.ts

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@ import {
2626
isIgnoreEndDirective,
2727
isIgnoreStartDirective,
2828
isInlineElement,
29-
isInsideQuotedAttribute,
3029
isLoneMustacheTag,
3130
isNodeSupportedLanguage,
3231
isNodeTopLevelHTML,
@@ -89,7 +88,8 @@ export function print(path: FastPath, options: ParserOptions, print: PrintFn): D
8988
return printTopLevelParts(n, options, path, print);
9089
}
9190

92-
const [open, close] = options.svelteStrictMode ? ['"{', '}"'] : ['{', '}'];
91+
const [open, close] =
92+
options.svelteStrictMode && !options._svelte_is5Plus ? ['"{', '}"'] : ['{', '}'];
9393
const printJsExpression = () => [open, printJS(path, print, 'expression'), close];
9494
const node = n as Node;
9595

@@ -438,18 +438,17 @@ export function print(path: FastPath, options: ParserOptions, print: PrintFn): D
438438
if (isOrCanBeConvertedToShorthand(node)) {
439439
if (options.svelteAllowShorthand) {
440440
return ['{', node.name, '}'];
441-
} else if (options.svelteStrictMode) {
442-
return [node.name, '="{', node.name, '}"'];
443441
} else {
444-
return [node.name, '={', node.name, '}'];
442+
return [node.name, `=${open}`, node.name, close];
445443
}
446444
} else {
447445
if (node.value === true) {
448446
return [node.name];
449447
}
450448

451449
const quotes =
452-
!isLoneMustacheTag(node.value) || (options.svelteStrictMode ?? false);
450+
!isLoneMustacheTag(node.value) ||
451+
((options.svelteStrictMode && !options._svelte_is5Plus) ?? false);
453452
const attrNodeValue = printAttributeNodeValue(path, print, quotes, node);
454453
if (quotes) {
455454
return [node.name, '=', '"', attrNodeValue, '"'];
@@ -649,14 +648,13 @@ export function print(path: FastPath, options: ParserOptions, print: PrintFn): D
649648
if (isOrCanBeConvertedToShorthand(node) || node.value === true) {
650649
if (options.svelteAllowShorthand) {
651650
return [...prefix];
652-
} else if (options.svelteStrictMode) {
653-
return [...prefix, '="{', node.name, '}"'];
654651
} else {
655-
return [...prefix, '={', node.name, '}'];
652+
return [...prefix, `=${open}`, node.name, close];
656653
}
657654
} else {
658655
const quotes =
659-
!isLoneMustacheTag(node.value) || (options.svelteStrictMode ?? false);
656+
!isLoneMustacheTag(node.value) ||
657+
((options.svelteStrictMode && !options._svelte_is5Plus) ?? false);
660658
const attrNodeValue = printAttributeNodeValue(path, print, quotes, node);
661659
if (quotes) {
662660
return [...prefix, '=', '"', attrNodeValue, '"'];

src/print/node-helpers.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -534,7 +534,8 @@ export function isInsideQuotedAttribute(path: FastPath, options: ParserOptions):
534534
return stack.some(
535535
(node) =>
536536
node.type === 'Attribute' &&
537-
(!isLoneMustacheTag(node.value) || options.svelteStrictMode),
537+
(!isLoneMustacheTag(node.value) ||
538+
(options.svelteStrictMode && !options._svelte_is5Plus)),
538539
);
539540
}
540541

test/formatting/index.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
import test from 'ava';
22
import { readdirSync, readFileSync, existsSync } from 'fs';
33
import { format } from 'prettier';
4+
import { VERSION } from 'svelte/compiler';
45
import * as SveltePlugin from '../../src';
56

7+
const isSvelte5Plus = Number(VERSION.split('.')[0]) >= 5;
8+
69
let dirs = readdirSync('test/formatting/samples');
710
const printerFilesHaveOnly = readdirSync('test/printer/samples').some(
811
(f) => f.endsWith('.only.html') || f.endsWith('.only.md'),
@@ -26,6 +29,9 @@ for (const dir of dirs) {
2629
).replace(/\r?\n/g, '\n');
2730
const options = readOptions(`test/formatting/samples/${dir}/options.json`);
2831

32+
// Tests attribute quoting changes, which are different in Svelte 5
33+
if (dir.endsWith('strict-mode-true') && isSvelte5Plus) continue;
34+
2935
test(`formatting: ${dir}`, async (t) => {
3036
let onTestCompleted;
3137

test/printer/index.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
import test from 'ava';
22
import { readdirSync, readFileSync, existsSync } from 'fs';
33
import { format } from 'prettier';
4+
import { VERSION } from 'svelte/compiler';
45
import * as SveltePlugin from '../../src';
56

7+
const isSvelte5Plus = Number(VERSION.split('.')[0]) >= 5;
8+
69
let files = readdirSync('test/printer/samples').filter(
710
(name) => name.endsWith('.html') || name.endsWith('.md'),
811
);
@@ -24,6 +27,9 @@ for (const file of files) {
2427
`test/printer/samples/${file.replace('.only', '').replace(`.${ending}`, '.options.json')}`,
2528
);
2629

30+
// Tests attribute quoting changes, which are different in Svelte 5
31+
if (file.endsWith('attribute-quoted.html') && isSvelte5Plus) continue;
32+
2733
test(`printer: ${file.slice(0, file.length - `.${ending}`.length)}`, async (t) => {
2834
const actualOutput = await format(input, {
2935
parser: ending === 'html' ? 'svelte' : 'markdown',

0 commit comments

Comments
 (0)