From 3303415d837d0e15e79ee62a3135948a3fc82aff Mon Sep 17 00:00:00 2001 From: Simon Holthausen Date: Wed, 17 Jul 2024 15:09:03 +0200 Subject: [PATCH] feat: never quote single attribute expressions in Svelte 5 In Svelte 5, attributes are never quoted, because this will mean "stringify this attribute value" in a future Svelte version Related to https://github.com/sveltejs/svelte/issues/7925 --- README.md | 6 +++++- src/embed.ts | 16 ++++++++++++++-- src/index.ts | 6 ++++-- src/options.ts | 9 +++++++++ src/print/index.ts | 18 ++++++++---------- src/print/node-helpers.ts | 3 ++- test/formatting/index.ts | 6 ++++++ test/printer/index.ts | 6 ++++++ 8 files changed, 54 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index a799ac1..6951113 100644 --- a/README.md +++ b/README.md @@ -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: ```html - +
+ +
diff --git a/src/embed.ts b/src/embed.ts index 93c4471..e739185 100644 --- a/src/embed.ts +++ b/src/embed.ts @@ -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); @@ -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'); diff --git a/src/index.ts b/src/index.ts index 602455c..e661d5f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -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? @@ -46,7 +47,7 @@ export const parsers: Record = { 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 @@ -56,7 +57,8 @@ export const parsers: Record = { // 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, diff --git a/src/options.ts b/src/options.ts index 87a8791..18d65fd 100644 --- a/src/options.ts +++ b/src/options.ts @@ -3,6 +3,15 @@ import { SortOrder, PluginConfig } from '..'; export interface ParserOptions extends PrettierParserOptions, Partial { _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) { diff --git a/src/print/index.ts b/src/print/index.ts index 44ad01f..531da04 100644 --- a/src/print/index.ts +++ b/src/print/index.ts @@ -26,7 +26,6 @@ import { isIgnoreEndDirective, isIgnoreStartDirective, isInlineElement, - isInsideQuotedAttribute, isLoneMustacheTag, isNodeSupportedLanguage, isNodeTopLevelHTML, @@ -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; @@ -438,10 +438,8 @@ 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) { @@ -449,7 +447,8 @@ export function print(path: FastPath, options: ParserOptions, print: PrintFn): D } 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, '"']; @@ -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, '"']; diff --git a/src/print/node-helpers.ts b/src/print/node-helpers.ts index 842b713..abf1cb5 100644 --- a/src/print/node-helpers.ts +++ b/src/print/node-helpers.ts @@ -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)), ); } diff --git a/test/formatting/index.ts b/test/formatting/index.ts index 9bb5b89..8a1c124 100644 --- a/test/formatting/index.ts +++ b/test/formatting/index.ts @@ -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'), @@ -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; diff --git a/test/printer/index.ts b/test/printer/index.ts index 2d2dc2c..97c6130 100644 --- a/test/printer/index.ts +++ b/test/printer/index.ts @@ -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'), ); @@ -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',