Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: never quote single attribute expressions in Svelte 5 #451

Merged
merged 1 commit into from
Jul 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,12 +99,16 @@ More strict HTML syntax: Quotes in attributes and no self-closing DOM elements (

> In version 2 this overruled `svelteAllowShorthand`, which is no longer the case.

> In Svelte 5, attributes are never quoted, because this will mean "stringify this attribute value" in a future Svelte version

Example:

<!-- prettier-ignore -->
```html
<!-- svelteStrictMode: true -->
<!-- svelteStrictMode: true (Svelte 3 and 4) -->
<div foo="{bar}"></div>
<!-- svelteStrictMode: true (Svelte 5) -->
<div foo={bar}></div>

<!-- svelteStrictMode: false -->
<div foo={bar} />
Expand Down
16 changes: 14 additions & 2 deletions src/embed.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,13 @@ export function embed(path: FastPath, _options: Options) {
const parent: Node = path.getParentNode();
const printJsExpression = () =>
(parent as any).expression
? printJS(parent, options.svelteStrictMode ?? false, false, false, 'expression')
? printJS(
parent,
(options.svelteStrictMode && !options._svelte_is5Plus) ?? false,
false,
false,
'expression',
)
: undefined;
const printSvelteBlockJS = (name: string) => printJS(parent, false, true, false, name);

Expand Down Expand Up @@ -110,7 +116,13 @@ export function embed(path: FastPath, _options: Options) {
}
break;
case 'Element':
printJS(parent, options.svelteStrictMode ?? false, false, false, 'tag');
printJS(
parent,
(options.svelteStrictMode && !options._svelte_is5Plus) ?? false,
false,
false,
'tag',
);
break;
case 'MustacheTag':
printJS(parent, isInsideQuotedAttribute(path, options), false, false, 'expression');
Expand Down
6 changes: 4 additions & 2 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { ASTNode } from './print/nodes';
import { embed, getVisitorKeys } from './embed';
import { snipScriptAndStyleTagContent } from './lib/snipTagContent';
import { parse, VERSION } from 'svelte/compiler';
import { ParserOptions } from './options';

const babelParser = prettierPluginBabel.parsers.babel;
const typescriptParser = prettierPluginBabel.parsers['babel-ts']; // TODO use TypeScript parser in next major?
Expand Down Expand Up @@ -46,7 +47,7 @@ export const parsers: Record<string, Parser> = {
throw err;
}
},
preprocess: (text, options) => {
preprocess: (text, options: ParserOptions) => {
const result = snipScriptAndStyleTagContent(text);
text = result.text.trim();
// Prettier sets the preprocessed text as the originalText in case
Expand All @@ -56,7 +57,8 @@ export const parsers: Record<string, Parser> = {
// Therefore we do it ourselves here.
options.originalText = text;
// Only Svelte 5 can have TS in the template
(options as any)._svelte_ts = isSvelte5Plus && result.isTypescript;
options._svelte_ts = isSvelte5Plus && result.isTypescript;
options._svelte_is5Plus = isSvelte5Plus;
return text;
},
locStart,
Expand Down
9 changes: 9 additions & 0 deletions src/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,15 @@ import { SortOrder, PluginConfig } from '..';

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

function makeChoice(choice: string) {
Expand Down
18 changes: 8 additions & 10 deletions src/print/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@ import {
isIgnoreEndDirective,
isIgnoreStartDirective,
isInlineElement,
isInsideQuotedAttribute,
isLoneMustacheTag,
isNodeSupportedLanguage,
isNodeTopLevelHTML,
Expand Down Expand Up @@ -89,7 +88,8 @@ export function print(path: FastPath, options: ParserOptions, print: PrintFn): D
return printTopLevelParts(n, options, path, print);
}

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

Expand Down Expand Up @@ -438,18 +438,17 @@ export function print(path: FastPath, options: ParserOptions, print: PrintFn): D
if (isOrCanBeConvertedToShorthand(node)) {
if (options.svelteAllowShorthand) {
return ['{', node.name, '}'];
} else if (options.svelteStrictMode) {
return [node.name, '="{', node.name, '}"'];
} else {
return [node.name, '={', node.name, '}'];
return [node.name, `=${open}`, node.name, close];
}
} else {
if (node.value === true) {
return [node.name];
}

const quotes =
!isLoneMustacheTag(node.value) || (options.svelteStrictMode ?? false);
!isLoneMustacheTag(node.value) ||
((options.svelteStrictMode && !options._svelte_is5Plus) ?? false);
const attrNodeValue = printAttributeNodeValue(path, print, quotes, node);
if (quotes) {
return [node.name, '=', '"', attrNodeValue, '"'];
Expand Down Expand Up @@ -649,14 +648,13 @@ export function print(path: FastPath, options: ParserOptions, print: PrintFn): D
if (isOrCanBeConvertedToShorthand(node) || node.value === true) {
if (options.svelteAllowShorthand) {
return [...prefix];
} else if (options.svelteStrictMode) {
return [...prefix, '="{', node.name, '}"'];
} else {
return [...prefix, '={', node.name, '}'];
return [...prefix, `=${open}`, node.name, close];
}
} else {
const quotes =
!isLoneMustacheTag(node.value) || (options.svelteStrictMode ?? false);
!isLoneMustacheTag(node.value) ||
((options.svelteStrictMode && !options._svelte_is5Plus) ?? false);
const attrNodeValue = printAttributeNodeValue(path, print, quotes, node);
if (quotes) {
return [...prefix, '=', '"', attrNodeValue, '"'];
Expand Down
3 changes: 2 additions & 1 deletion src/print/node-helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -534,7 +534,8 @@ export function isInsideQuotedAttribute(path: FastPath, options: ParserOptions):
return stack.some(
(node) =>
node.type === 'Attribute' &&
(!isLoneMustacheTag(node.value) || options.svelteStrictMode),
(!isLoneMustacheTag(node.value) ||
(options.svelteStrictMode && !options._svelte_is5Plus)),
);
}

Expand Down
6 changes: 6 additions & 0 deletions test/formatting/index.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import test from 'ava';
import { readdirSync, readFileSync, existsSync } from 'fs';
import { format } from 'prettier';
import { VERSION } from 'svelte/compiler';
import * as SveltePlugin from '../../src';

const isSvelte5Plus = Number(VERSION.split('.')[0]) >= 5;

let dirs = readdirSync('test/formatting/samples');
const printerFilesHaveOnly = readdirSync('test/printer/samples').some(
(f) => f.endsWith('.only.html') || f.endsWith('.only.md'),
Expand All @@ -26,6 +29,9 @@ for (const dir of dirs) {
).replace(/\r?\n/g, '\n');
const options = readOptions(`test/formatting/samples/${dir}/options.json`);

// Tests attribute quoting changes, which are different in Svelte 5
if (dir.endsWith('strict-mode-true') && isSvelte5Plus) continue;

test(`formatting: ${dir}`, async (t) => {
let onTestCompleted;

Expand Down
6 changes: 6 additions & 0 deletions test/printer/index.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import test from 'ava';
import { readdirSync, readFileSync, existsSync } from 'fs';
import { format } from 'prettier';
import { VERSION } from 'svelte/compiler';
import * as SveltePlugin from '../../src';

const isSvelte5Plus = Number(VERSION.split('.')[0]) >= 5;

let files = readdirSync('test/printer/samples').filter(
(name) => name.endsWith('.html') || name.endsWith('.md'),
);
Expand All @@ -24,6 +27,9 @@ for (const file of files) {
`test/printer/samples/${file.replace('.only', '').replace(`.${ending}`, '.options.json')}`,
);

// Tests attribute quoting changes, which are different in Svelte 5
if (file.endsWith('attribute-quoted.html') && isSvelte5Plus) continue;

test(`printer: ${file.slice(0, file.length - `.${ending}`.length)}`, async (t) => {
const actualOutput = await format(input, {
parser: ending === 'html' ? 'svelte' : 'markdown',
Expand Down
Loading