diff --git a/package-lock.json b/package-lock.json index 2b7590cc..db08bcdc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,14 +17,14 @@ "prettier": "^3.0.0", "rollup": "2.36.0", "rollup-plugin-typescript": "1.0.1", - "svelte": "^4.2.7", + "svelte": "^5.0.0-next.192", "ts-node": "^10.1.1", "tslib": "^2.6.0", "typescript": "5.1.3" }, "peerDependencies": { "prettier": "^3.0.0", - "svelte": "^3.2.0 || ^4.0.0-next.0 || ^5.0.0-next.0" + "svelte": "^5.0.0-next.111" } }, "node_modules/@ampproject/remapping": { @@ -338,6 +338,18 @@ "rollup": "^1.20.0||^2.0.0" } }, + "node_modules/@rollup/pluginutils/node_modules/@types/estree": { + "version": "0.0.39", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.39.tgz", + "integrity": "sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw==", + "dev": true + }, + "node_modules/@rollup/pluginutils/node_modules/estree-walker": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-1.0.1.tgz", + "integrity": "sha512-1fMXF3YP4pZZVozF8j/ZLfvnR8NSIljt56UhbZ5PeeDmmGHpgpdwQt7ITlGvYaQukCvuBRMLEiKiYC+oeIg4cg==", + "dev": true + }, "node_modules/@sindresorhus/is": { "version": "0.14.0", "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-0.14.0.tgz", @@ -384,9 +396,9 @@ "dev": true }, "node_modules/@types/estree": { - "version": "0.0.39", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.39.tgz", - "integrity": "sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw==", + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", + "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==", "dev": true }, "node_modules/@types/node": { @@ -411,9 +423,9 @@ } }, "node_modules/acorn": { - "version": "8.10.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.10.0.tgz", - "integrity": "sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==", + "version": "8.11.3", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz", + "integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==", "dev": true, "bin": { "acorn": "bin/acorn" @@ -422,6 +434,15 @@ "node": ">=0.4.0" } }, + "node_modules/acorn-typescript": { + "version": "1.4.13", + "resolved": "https://registry.npmjs.org/acorn-typescript/-/acorn-typescript-1.4.13.tgz", + "integrity": "sha512-xsc9Xv0xlVfwp2o7sQ+GCQ1PgbkdcpWdTzrwXxO3xDMTAywVS3oXVOcOHuRjAPkS4P9b+yc/qNF15460v+jp4Q==", + "dev": true, + "peerDependencies": { + "acorn": ">=8.9.0" + } + }, "node_modules/acorn-walk": { "version": "8.2.0", "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.2.0.tgz", @@ -1120,34 +1141,6 @@ "node": ">=10" } }, - "node_modules/code-red": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/code-red/-/code-red-1.0.4.tgz", - "integrity": "sha512-7qJWqItLA8/VPVlKJlFXU+NBlo/qyfs39aJcuMT/2ere32ZqvF5OSxgdM5xOfJJ7O429gg2HM47y8v9P+9wrNw==", - "dev": true, - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.4.15", - "@types/estree": "^1.0.1", - "acorn": "^8.10.0", - "estree-walker": "^3.0.3", - "periscopic": "^3.1.0" - } - }, - "node_modules/code-red/node_modules/@types/estree": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", - "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==", - "dev": true - }, - "node_modules/code-red/node_modules/estree-walker": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", - "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", - "dev": true, - "dependencies": { - "@types/estree": "^1.0.0" - } - }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -1253,19 +1246,6 @@ "node": ">=8" } }, - "node_modules/css-tree": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.3.1.tgz", - "integrity": "sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw==", - "dev": true, - "dependencies": { - "mdn-data": "2.0.30", - "source-map-js": "^1.0.1" - }, - "engines": { - "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" - } - }, "node_modules/currently-unhandled": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/currently-unhandled/-/currently-unhandled-0.4.1.tgz", @@ -1500,6 +1480,12 @@ "node": ">=0.8.0" } }, + "node_modules/esm-env": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/esm-env/-/esm-env-1.0.0.tgz", + "integrity": "sha512-Cf6VksWPsTuW01vU9Mk/3vRue91Zevka5SjyNf3nEpokFRuqt/KjUQoGAwq9qMmhpLTHmXzSIrFRw8zxWzmFBA==", + "dev": true + }, "node_modules/esprima": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", @@ -1513,6 +1499,17 @@ "node": ">=4" } }, + "node_modules/esrap": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/esrap/-/esrap-1.2.2.tgz", + "integrity": "sha512-F2pSJklxx1BlQIQgooczXCPHmcWpn6EsP5oo73LQfonG9fIlIENQ8vMmfGXeojP9MrkzUNAfyU5vdFlR9shHAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.4.15", + "@types/estree": "^1.0.1" + } + }, "node_modules/estree-walker": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-1.0.1.tgz", @@ -2323,12 +2320,6 @@ "node": ">=8" } }, - "node_modules/mdn-data": { - "version": "2.0.30", - "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.30.tgz", - "integrity": "sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==", - "dev": true - }, "node_modules/mem": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/mem/-/mem-8.0.0.tgz", @@ -2690,41 +2681,6 @@ "node": ">=8" } }, - "node_modules/periscopic": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/periscopic/-/periscopic-3.1.0.tgz", - "integrity": "sha512-vKiQ8RRtkl9P+r/+oefh25C3fhybptkHKCZSPlcXiJux2tJF55GnEj3BVn4A5gKfq9NWWXXrxkHBwVPUfH0opw==", - "dev": true, - "dependencies": { - "@types/estree": "^1.0.0", - "estree-walker": "^3.0.0", - "is-reference": "^3.0.0" - } - }, - "node_modules/periscopic/node_modules/@types/estree": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", - "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==", - "dev": true - }, - "node_modules/periscopic/node_modules/estree-walker": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", - "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", - "dev": true, - "dependencies": { - "@types/estree": "^1.0.0" - } - }, - "node_modules/periscopic/node_modules/is-reference": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.2.tgz", - "integrity": "sha512-v3rht/LgVcsdZa3O2Nqs+NMowLOxeOm7Ay9+/ARQ2F+qEoANRcqrjAZKGN0v8ymUetZGgkp26LTnGT7H0Qo9Pg==", - "dev": true, - "dependencies": { - "@types/estree": "*" - } - }, "node_modules/picomatch": { "version": "2.2.2", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.2.2.tgz", @@ -3273,15 +3229,6 @@ "node": ">=0.10.0" } }, - "node_modules/source-map-js": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", - "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/sourcemap-codec": { "version": "1.4.8", "resolved": "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz", @@ -3458,53 +3405,28 @@ } }, "node_modules/svelte": { - "version": "4.2.12", - "resolved": "https://registry.npmjs.org/svelte/-/svelte-4.2.12.tgz", - "integrity": "sha512-d8+wsh5TfPwqVzbm4/HCXC783/KPHV60NvwitJnyTA5lWn1elhXMNWhXGCJ7PwPa8qFUnyJNIyuIRt2mT0WMug==", + "version": "5.0.0-next.192", + "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.0.0-next.192.tgz", + "integrity": "sha512-UgjiqTCsEWyQ157x5YNbmx859vBVFfznKaxuiMCPqHS3HRZ1iqTsSyO3LI/4BHjqPrtxwrOn1Z63VwoJkYBBDA==", "dev": true, + "license": "MIT", "dependencies": { "@ampproject/remapping": "^2.2.1", "@jridgewell/sourcemap-codec": "^1.4.15", - "@jridgewell/trace-mapping": "^0.3.18", - "@types/estree": "^1.0.1", - "acorn": "^8.9.0", + "@types/estree": "^1.0.5", + "acorn": "^8.11.3", + "acorn-typescript": "^1.4.13", "aria-query": "^5.3.0", "axobject-query": "^4.0.0", - "code-red": "^1.0.3", - "css-tree": "^2.3.1", - "estree-walker": "^3.0.3", - "is-reference": "^3.0.1", + "esm-env": "^1.0.0", + "esrap": "^1.2.2", + "is-reference": "^3.0.2", "locate-character": "^3.0.0", - "magic-string": "^0.30.4", - "periscopic": "^3.1.0" + "magic-string": "^0.30.5", + "zimmerframe": "^1.1.2" }, "engines": { - "node": ">=16" - } - }, - "node_modules/svelte/node_modules/@jridgewell/trace-mapping": { - "version": "0.3.23", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.23.tgz", - "integrity": "sha512-9/4foRoUKp8s96tSkh8DlAAc5A0Ty8vLXld+l9gjKKY6ckwI8G15f0hskGmuLZu78ZlGa1vtsfOa+lnB4vG6Jg==", - "dev": true, - "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" - } - }, - "node_modules/svelte/node_modules/@types/estree": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", - "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==", - "dev": true - }, - "node_modules/svelte/node_modules/estree-walker": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", - "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", - "dev": true, - "dependencies": { - "@types/estree": "^1.0.0" + "node": ">=18" } }, "node_modules/svelte/node_modules/is-reference": { @@ -3881,6 +3803,12 @@ "engines": { "node": ">=6" } + }, + "node_modules/zimmerframe": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/zimmerframe/-/zimmerframe-1.1.2.tgz", + "integrity": "sha512-rAbqEGa8ovJy4pyBxZM70hg4pE6gDgaQ0Sl9M3enG3I0d6H4XSAM3GeNGLKnsBpuijUow064sf7ww1nutC5/3w==", + "dev": true } } } diff --git a/package.json b/package.json index 1ea7e13d..827670d0 100644 --- a/package.json +++ b/package.json @@ -49,13 +49,13 @@ "prettier": "^3.0.0", "rollup": "2.36.0", "rollup-plugin-typescript": "1.0.1", - "svelte": "^4.2.7", + "svelte": "^5.0.0-next.192", "ts-node": "^10.1.1", "tslib": "^2.6.0", "typescript": "5.1.3" }, "peerDependencies": { "prettier": "^3.0.0", - "svelte": "^3.2.0 || ^4.0.0-next.0 || ^5.0.0-next.0" + "svelte": "^5.0.0-next.111" } } diff --git a/src/embed.ts b/src/embed.ts index e7391853..203168e8 100644 --- a/src/embed.ts +++ b/src/embed.ts @@ -1,4 +1,4 @@ -import { Doc, doc, FastPath, Options } from 'prettier'; +import { Doc, doc, AstPath, Options } from 'prettier'; import { getText } from './lib/getText'; import { snippedTagContentAttribute } from './lib/snipTagContent'; import { isBracketSameLine, ParserOptions } from './options'; @@ -19,8 +19,7 @@ import { isTypeScript, printRaw, } from './print/node-helpers'; -import { BaseNode, CommentNode, ElementNode, Node, ScriptNode, StyleNode } from './print/nodes'; -import { extractAttributes } from './lib/extractAttributes'; +import { BaseNode, Script, Root, Comment, RegularElement, SvelteElement } from './print/nodes'; import { base64ToString } from './base64-string'; const { @@ -30,7 +29,7 @@ const { const leaveAlone = new Set([ 'Script', - 'Style', + 'StyleSheet', 'Identifier', 'MemberExpression', 'CallExpression', @@ -49,7 +48,7 @@ export function getVisitorKeys(node: any, nonTraversableKeys: Set): stri // - deepest property is calling embed first // - if embed returns a function, it will be called after the traversal in a second pass, in the same order (deepest first) // For performance reasons we try to only return functions when we're sure we need to transform something. -export function embed(path: FastPath, _options: Options) { +export function embed(path: AstPath, _options: Options) { const node: Node = path.getNode(); const options = _options as ParserOptions; if (!options.locStart || !options.locEnd || !options.originalText) { @@ -58,16 +57,10 @@ export function embed(path: FastPath, _options: Options) { if (isASTNode(node)) { assignCommentsToNodes(node); - if (node.module) { - node.module.type = 'Script'; - node.module.attributes = extractAttributes(getText(node.module, options)); - } - if (node.instance) { - node.instance.type = 'Script'; - node.instance.attributes = extractAttributes(getText(node.instance, options)); + if (node.options) { + node.options.type = 'SvelteOptions'; } if (node.css) { - node.css.type = 'Style'; node.css.content.type = 'StyleProgram'; } return null; @@ -77,19 +70,12 @@ export function embed(path: FastPath, _options: Options) { // check the parent to see if we are inside an expression that should be embedded. const parent: Node = path.getParentNode(); const printJsExpression = () => - (parent as any).expression - ? printJS( - parent, - (options.svelteStrictMode && !options._svelte_is5Plus) ?? false, - false, - false, - 'expression', - ) - : undefined; + (parent as any).expression ? printJS(parent, false, false, false, 'expression') : undefined; const printSvelteBlockJS = (name: string) => printJS(parent, false, true, false, name); switch (parent.type) { case 'IfBlock': + printSvelteBlockJS('test'); case 'ElseBlock': case 'AwaitBlock': case 'KeyBlock': @@ -115,26 +101,21 @@ export function embed(path: FastPath, _options: Options) { node.asFunction = true; } break; - case 'Element': - printJS( - parent, - (options.svelteStrictMode && !options._svelte_is5Plus) ?? false, - false, - false, - 'tag', - ); + case 'RegularElement': + case 'SvelteElement': + printJS(parent, false, false, false, 'tag'); break; - case 'MustacheTag': + case 'ExpressionTag': printJS(parent, isInsideQuotedAttribute(path, options), false, false, 'expression'); break; - case 'RawMustacheTag': + case 'HtmlTag': printJS(parent, false, false, false, 'expression'); break; - case 'Spread': + case 'SpreadAttribute': printJS(parent, false, false, false, 'expression'); break; case 'ConstTag': - printJS(parent, false, false, true, 'expression'); + printJS(parent, false, false, true, 'declaration'); break; case 'RenderTag': if (node === parent.expression) { @@ -153,14 +134,16 @@ export function embed(path: FastPath, _options: Options) { printJS(parent, false, false, false, 'expression'); } break; - case 'EventHandler': - case 'Binding': - case 'Class': - case 'Let': - case 'Transition': - case 'Action': - case 'Animation': - case 'InlineComponent': + case 'OnDirective': + case 'BindDirective': + case 'ClassDirective': + case 'LetDirective': + case 'TransitionDirective': + case 'UseDirective': + case 'AnimateDirective': + case 'SvelteSelf': + case 'SvelteComponent': + case 'Component': printJsExpression(); break; } @@ -246,9 +229,9 @@ export function embed(path: FastPath, _options: Options) { switch (node.type) { case 'Script': return embedScript(true); - case 'Style': + case 'StyleSheet': return embedStyle(true); - case 'Element': { + case 'RegularElement': { if (node.name === 'script') { return embedScript(false); } else if (node.name === 'style') { @@ -343,21 +326,21 @@ async function formatBodyContent( async function embedTag( tag: 'script' | 'style' | 'template', text: string, - path: FastPath, + path: AstPath, formatBodyContent: (content: string) => Promise, print: PrintFn, isTopLevel: boolean, options: ParserOptions, ) { - const node: ScriptNode | StyleNode | ElementNode = path.getNode(); + const node: Script | StyleSheet | RegularElement | SvelteElement = path.getNode(); const content = - tag === 'template' ? printRaw(node as ElementNode, text) : getSnippedContent(node); + tag === 'template' ? printRaw(node as RegularElement, text) : getSnippedContent(node); const previousComments = - node.type === 'Script' || node.type === 'Style' + node.type === 'Script' || node.type === 'StyleSheet' ? node.comments : [getLeadingComment(path)] .filter(Boolean) - .map((comment) => ({ comment: comment as CommentNode, emptyLineAfter: false })); + .map((comment) => ({ comment: comment as Comment, emptyLineAfter: false })); const canFormat = isNodeSupportedLanguage(node) && diff --git a/src/index.ts b/src/index.ts index e661d5fe..7c43130c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,15 +1,13 @@ import { SupportLanguage, Parser, Printer } from 'prettier'; import * as prettierPluginBabel from 'prettier/plugins/babel'; import { hasPragma, print } from './print'; -import { ASTNode } from './print/nodes'; import { embed, getVisitorKeys } from './embed'; import { snipScriptAndStyleTagContent } from './lib/snipTagContent'; -import { parse, VERSION } from 'svelte/compiler'; +import { parse } 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? -const isSvelte5Plus = Number(VERSION.split('.')[0]) >= 5; function locStart(node: any) { return node.start; @@ -33,7 +31,7 @@ export const parsers: Record = { hasPragma, parse: (text) => { try { - return { ...parse(text), __isRoot: true }; + return parse(text, { modern: true }); } catch (err: any) { if (err.start != null && err.end != null) { // Prettier expects error objects to have loc.start and loc.end fields. @@ -57,8 +55,7 @@ export const parsers: Record = { // Therefore we do it ourselves here. options.originalText = text; // Only Svelte 5 can have TS in the template - options._svelte_ts = isSvelte5Plus && result.isTypescript; - options._svelte_is5Plus = isSvelte5Plus; + options._svelte_ts = result.isTypescript; return text; }, locStart, diff --git a/src/lib/extractAttributes.ts b/src/lib/extractAttributes.ts deleted file mode 100644 index 8bd83f15..00000000 --- a/src/lib/extractAttributes.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { AttributeNode, TextNode } from '../print/nodes'; - -const extractAttributesRegex = - /<[a-z]+((?:\s+[^=>'"\/]+=(?:"[^"]*"|'[^']*'|[^>\s]+)|\s+[^=>'"\/]+)*\s*)>/im; -const attributeRegex = /([^\s=]+)(?:=(?:(?:("|')([\s\S]*?)\2)|(?:([^>\s]+?)(?:\s|>|$))))?/gim; - -export function extractAttributes(html: string): AttributeNode[] { - const [, attributesString] = html.match(extractAttributesRegex)!; - - const attrs: AttributeNode[] = []; - - let match: RegExpMatchArray | null; - while ((match = attributeRegex.exec(attributesString))) { - const [all, name, quotes, valueQuoted, valueUnquoted] = match; - const value = valueQuoted || valueUnquoted; - const attrStart = match.index!; - - let valueNode: AttributeNode['value']; - if (!value) { - valueNode = true; - } else { - let valueStart = attrStart + name.length; - if (quotes) { - valueStart += 2; - } - - valueNode = [ - { - type: 'Text', - data: value, - start: valueStart, - end: valueStart + value.length, - } as TextNode, - ]; - } - - attrs.push({ - type: 'Attribute', - name, - value: valueNode, - start: attrStart, - end: attrStart + all.length, - }); - } - - return attrs; -} diff --git a/src/options.ts b/src/options.ts index 18d65fdb..adcc36ac 100644 --- a/src/options.ts +++ b/src/options.ts @@ -4,14 +4,6 @@ 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/helpers.ts b/src/print/helpers.ts index 68ee12f8..085e3833 100644 --- a/src/print/helpers.ts +++ b/src/print/helpers.ts @@ -1,23 +1,25 @@ -import { Doc, doc, FastPath } from 'prettier'; +import { Doc, doc, AstPath } from 'prettier'; import { PrintFn } from '.'; import { formattableAttributes } from '../lib/elements'; import { snippedTagContentAttribute } from '../lib/snipTagContent'; import { - ASTNode, - AttributeNode, - BodyNode, - DocumentNode, - ElementNode, - HeadNode, - InlineComponentNode, - Node, - OptionsNode, - ScriptNode, - SlotNode, - SlotTemplateNode, - StyleNode, - TitleNode, - WindowNode + BaseElement, + BaseNode, + Component, + ElementLike, + RegularElement, + Root, + SlotElement, + StyleSheet, + SvelteBody, + SvelteDocument, + SvelteElement, + SvelteFragment, + SvelteHead, + SvelteOptions, + SvelteSelf, + SvelteWindow, + TitleElement, } from './nodes'; import { ParserOptions } from '../options'; @@ -25,16 +27,16 @@ import { ParserOptions } from '../options'; * Determines whether or not given node * is the root of the Svelte AST. */ -export function isASTNode(n: any): n is ASTNode { - return n && n.__isRoot; +export function isASTNode(n: any): n is Root { + return n && n.type === 'Root'; } -export function isPreTagContent(path: FastPath): boolean { +export function isPreTagContent(path: AstPath): boolean { const stack = path.stack as Node[]; return stack.some( (node) => - (node.type === 'Element' && node.name.toLowerCase() === 'pre') || + (node.type === 'RegularElement' && node.name.toLowerCase() === 'pre') || (node.type === 'Attribute' && !formattableAttributes.includes(node.name)), ); } @@ -70,26 +72,26 @@ export function replaceEndOfLineWith(text: string, replacement: Doc) { export function getAttributeLine( node: - | ElementNode - | InlineComponentNode - | SlotNode - | WindowNode - | HeadNode - | TitleNode - | StyleNode - | ScriptNode - | BodyNode - | DocumentNode - | OptionsNode - | SlotTemplateNode, + | RegularElement + | SvelteElement + | SvelteSelf + | Component + | SlotElement + | SvelteFragment + | SvelteWindow + | SvelteHead + | TitleElement + | StyleSheet +// | ScriptNode + | SvelteBody + | SvelteDocument + | SvelteOptions, options: ParserOptions, ) { const { hardline, line } = doc.builders; - const hasThisBinding = - (node.type === 'InlineComponent' && !!node.expression) || - (node.type === 'Element' && !!node.tag); + const hasThisBinding = node.type === 'SvelteComponent' || node.type === 'SvelteElement'; - const attributes = (node.attributes as Array).filter( + const attributes = node.attributes.filter( (attribute) => attribute.name !== snippedTagContentAttribute, ); return options.singleAttributePerLine && @@ -100,18 +102,20 @@ export function getAttributeLine( export function printWithPrependedAttributeLine( node: - | ElementNode - | InlineComponentNode - | SlotNode - | WindowNode - | HeadNode - | TitleNode - | StyleNode - | ScriptNode - | BodyNode - | DocumentNode - | OptionsNode - | SlotTemplateNode, + | RegularElement + | SvelteElement + | SvelteSelf + | Component + | SlotElement + | SvelteFragment + | SvelteWindow + | SvelteHead + | TitleElement + | StyleSheet + // | ScriptNode + | SvelteBody + | SvelteDocument + | SvelteOptions, options: ParserOptions, print: PrintFn, ): PrintFn { diff --git a/src/print/index.ts b/src/print/index.ts index 531da046..a0c5ddaf 100644 --- a/src/print/index.ts +++ b/src/print/index.ts @@ -1,4 +1,4 @@ -import { Doc, doc, FastPath } from 'prettier'; +import { Doc, doc, AstPath } from 'prettier'; import { formattableAttributes, selfClosingTags } from '../lib/elements'; import { hasSnippedContent, unsnipContent } from '../lib/snipTagContent'; import { isBracketSameLine, ParserOptions, parseSortOrder, SortOrderPart } from '../options'; @@ -13,8 +13,8 @@ import { } from './helpers'; import { canOmitSoftlineBeforeClosingTag, - checkWhitespaceAtEndOfSvelteBlock, - checkWhitespaceAtStartOfSvelteBlock, + checkWhitespaceAtEndOfFragment, + checkWhitespaceAtStartOfFragment, doesEmbedStartAfterNode, endsWithLinebreak, getChildren, @@ -26,7 +26,7 @@ import { isIgnoreEndDirective, isIgnoreStartDirective, isInlineElement, - isLoneMustacheTag, + isLoneExpressionTag, isNodeSupportedLanguage, isNodeTopLevelHTML, isOrCanBeConvertedToShorthand, @@ -42,21 +42,12 @@ import { trimTextNodeLeft, trimTextNodeRight, } from './node-helpers'; -import { - ASTNode, - AttributeNode, - CommentNode, - IfBlockNode, - Node, - OptionsNode, - StyleDirectiveNode, - TextNode, -} from './nodes'; +import { Fragment, Root, SvelteNode, Text } from './nodes'; const { join, line, group, indent, dedent, softline, hardline, fill, breakParent, literalline } = doc.builders; -export type PrintFn = (path: FastPath) => Doc; +export type PrintFn = (path: AstPath) => Doc; declare module 'prettier' { export namespace doc { @@ -74,9 +65,8 @@ export function hasPragma(text: string) { let ignoreNext = false; let ignoreRange = false; -let svelteOptionsDoc: Doc | undefined; -export function print(path: FastPath, options: ParserOptions, print: PrintFn): Doc { +export function print(path: AstPath, options: ParserOptions, print: PrintFn): Doc { const bracketSameLine = isBracketSameLine(options); const n = path.getValue(); @@ -88,10 +78,9 @@ export function print(path: FastPath, options: ParserOptions, print: PrintFn): D return printTopLevelParts(n, options, path, print); } - const [open, close] = - options.svelteStrictMode && !options._svelte_is5Plus ? ['"{', '}"'] : ['{', '}']; + const [open, close] = ['{', '}']; const printJsExpression = () => [open, printJS(path, print, 'expression'), close]; - const node = n as Node; + const node = n as SvelteNode; if ( (ignoreNext || (ignoreRange && !isIgnoreEndDirective(node))) && @@ -110,26 +99,32 @@ export function print(path: FastPath, options: ParserOptions, print: PrintFn): D switch (node.type) { case 'Fragment': - const children = node.children; + const children = node.nodes; if (children.length === 0 || children.every(isEmptyTextNode)) { return ''; } if (!isPreTagContent(path)) { - trimChildren(node.children, path); - const output = trim( - [printChildren(path, print, options)], - (n) => - isLine(n) || - (typeof n === 'string' && n.trim() === '') || - // Because printChildren may append this at the end and - // may hide other lines before it - n === breakParent, - ); + trimChildren(node.nodes, path); + let shouldBreakParent = false; + const output = trim([printChildren(path, print, options)], (n) => { + // Because printChildren may append this at the end and + // may hide other lines before it + if (n === breakParent) { + shouldBreakParent = true; + return true; + } + + return isLine(n) || (typeof n === 'string' && n.trim() === ''); + }); + if (shouldBreakParent) { + output.push(breakParent); + } if (output.every((doc) => isEmptyDoc(doc))) { return ''; } - return group([...output, hardline]); + // return group([...output, hardline]); + return group(output); } else { return group(path.map(print, 'children')); } @@ -165,7 +160,10 @@ export function print(path: FastPath, options: ParserOptions, print: PrintFn): D if (parent.type === 'Attribute') { // Direct child of attribute value -> add literallines at end of lines // so that other things don't break in unexpected places - if (parent.name === 'class' && path.getParentNode(1).type === 'Element') { + if ( + parent.name === 'class' && + path.getParentNode(1).type === 'RegularElement' + ) { // Special treatment for class attribute on html elements. Prettier // will force everything into one line, we deviate from that and preserve lines. rawText = rawText.replace( @@ -184,7 +182,7 @@ export function print(path: FastPath, options: ParserOptions, print: PrintFn): D ? match : characterBeforeWhitespace + (isEndOfLine ? endOfLine : ' '), ); - // Shrink trailing whitespace in case it's followed by a mustache tag + // Shrink trailing whitespace in case it's followed by a expression tag // and remove it completely if it's at the end of the string, but not // if it's on its own line rawText = rawText.replace( @@ -196,30 +194,36 @@ export function print(path: FastPath, options: ParserOptions, print: PrintFn): D } return rawText; } - case 'Element': - case 'InlineComponent': - case 'Slot': - case 'SlotTemplate': - case 'Window': - case 'Head': - case 'Title': { + case 'RegularElement': + case 'SvelteElement': + case 'SvelteSelf': + case 'Component': + case 'SvelteComponent': + case 'SlotElement': + case 'SvelteFragment': + case 'SvelteWindow': + case 'SvelteHead': + case 'TitleElement': { const isSupportedLanguage = !( node.name === 'template' && !isNodeSupportedLanguage(node) ); - const isEmpty = node.children.every((child) => isEmptyTextNode(child)); + const isEmpty = node.fragment.nodes.every((child) => isEmptyTextNode(child)); const isDoctypeTag = node.name.toUpperCase() === '!DOCTYPE'; const didSelfClose = options.originalText[node.end - 2] === '/'; const isSelfClosingTag = isEmpty && - ((((node.type === 'Element' && !options.svelteStrictMode) || - node.type === 'Head' || - node.type === 'InlineComponent' || - node.type === 'Slot' || - node.type === 'SlotTemplate' || - node.type === 'Title') && + (((((node.type === 'RegularElement' || node.type === 'SvelteElement') && + !options.svelteStrictMode) || + node.type === 'SvelteHead' || + node.type === 'SvelteSelf' || + node.type === 'SvelteComponent' || + node.type === 'Component' || + node.type === 'SlotElement' || + node.type === 'SvelteFragment' || + node.type === 'TitleElement') && didSelfClose) || - node.type === 'Window' || + node.type === 'SvelteWindow' || selfClosingTags.indexOf(node.name) !== -1 || isDoctypeTag); @@ -230,14 +234,14 @@ export function print(path: FastPath, options: ParserOptions, print: PrintFn): D ); const attributeLine = getAttributeLine(node, options); const possibleThisBinding = - node.type === 'InlineComponent' && node.expression + node.type === 'SvelteComponent' ? [attributeLine, 'this=', ...printJsExpression()] - : node.type === 'Element' && node.tag + : node.type === 'SvelteElement' ? [ attributeLine, 'this=', - ...(typeof node.tag === 'string' - ? [`"${node.tag}"`] + ...(node.tag.type === 'Literal' && typeof node.tag.loc === 'undefined' + ? [`"${node.tag.value}"`] : [open, printJS(path, print, 'tag'), close]), ] : ''; @@ -259,10 +263,11 @@ export function print(path: FastPath, options: ParserOptions, print: PrintFn): D ]); } - const children = node.children; + const children = node.fragment.nodes; const firstChild = children[0]; const lastChild = children[children.length - 1]; + //todo // Is a function which is invoked later because printChildren will manipulate child nodes // which would wrongfully change the other checks about hugging etc done beforehand let body: () => Doc; @@ -273,8 +278,8 @@ export function print(path: FastPath, options: ParserOptions, print: PrintFn): D if (isEmpty) { body = isInlineElement(path, options, node) && - node.children.length && - isTextNodeStartingWithWhitespace(node.children[0]) && + node.fragment.nodes.length && + isTextNodeStartingWithWhitespace(node.fragment.nodes[0]) && !isPreTagContent(path) ? () => line : () => (bracketSameLine ? softline : ''); @@ -283,9 +288,9 @@ export function print(path: FastPath, options: ParserOptions, print: PrintFn): D } else if (!isSupportedLanguage) { body = () => printRaw(node, options.originalText, true); } else if (isInlineElement(path, options, node) && !isPreTagContent(path)) { - body = () => printChildren(path, print, options); + body = () => path.call(print, 'fragment'); } else { - body = () => printChildren(path, print, options); + body = () => path.call(print, 'fragment'); } const openingTag = [ @@ -393,28 +398,38 @@ export function print(path: FastPath, options: ParserOptions, print: PrintFn): D ``, ]); } - case 'Options': - if (options.svelteSortOrder !== 'none') { - throw new Error('Options tags should have been handled by prepareChildren'); + case 'SvelteOptions': + const comments = []; + for (const comment of node.comments) { + comments.push(''); + comments.push(hardline); + if (comment.emptyLineAfter) { + comments.push(hardline); + } } - // else fall through to Body - case 'Body': - case 'Document': - return group([ - '<', - node.name, - indent( - group([ - ...path.map( - printWithPrependedAttributeLine(node, options, print), - 'attributes', + + return [ + comments, + group([ + [ + ''], - ]); - case 'Document': + ...[bracketSameLine ? ' ' : '', '/>'], + ], + // hardline, + ]), + ]; + // else fall through to Body + case 'SvelteBody': + case 'SvelteDocument': return group([ '<', node.name, @@ -431,9 +446,6 @@ export function print(path: FastPath, options: ParserOptions, print: PrintFn): D ]); case 'Identifier': return node.name; - case 'AttributeShorthand': { - return (node.expression as any).name; - } case 'Attribute': { if (isOrCanBeConvertedToShorthand(node)) { if (options.svelteAllowShorthand) { @@ -446,9 +458,7 @@ export function print(path: FastPath, options: ParserOptions, print: PrintFn): D return [node.name]; } - const quotes = - !isLoneMustacheTag(node.value) || - ((options.svelteStrictMode && !options._svelte_is5Plus) ?? false); + const quotes = !isLoneExpressionTag(node.value); const attrNodeValue = printAttributeNodeValue(path, print, quotes, node); if (quotes) { return [node.name, '=', '"', attrNodeValue, '"']; @@ -457,56 +467,37 @@ export function print(path: FastPath, options: ParserOptions, print: PrintFn): D } } } - case 'MustacheTag': + case 'ExpressionTag': return ['{', printJS(path, print, 'expression'), '}']; case 'IfBlock': { - const def: Doc[] = [ - '{#if ', - printJS(path, print, 'expression'), + let def: Doc[] = [ + node.elseif ? '{:else ' : '{#', + 'if ', + printJS(path, print, 'test'), '}', - printSvelteBlockChildren(path, print, options), + printSvelteBlockFragment(path, print, 'consequent'), ]; - if (node.else) { - def.push(path.call(print, 'else')); - } - - def.push('{/if}'); + if (node.alternate) { + const alternateNodes = node.alternate.nodes; + if ( + alternateNodes.length !== 1 || + alternateNodes[0].type !== 'IfBlock' || + !alternateNodes[0].elseif + ) { + def.push('{:else}'); + } - return group([def, breakParent]); - } - case 'ElseBlock': { - // Else if - const parent = path.getParentNode() as Node; + def.push(printSvelteBlockFragment(path, print, 'alternate', node.elseif)); + } - if ( - node.children.length === 1 && - node.children[0].type === 'IfBlock' && - parent.type !== 'EachBlock' - ) { - const ifNode = node.children[0] as IfBlockNode; - const def: Doc[] = [ - '{:else if ', - path.map((ifPath) => printJS(ifPath, print, 'expression'), 'children')[0], - '}', - path.map( - (ifPath) => printSvelteBlockChildren(ifPath, print, options), - 'children', - )[0], - ]; - - if (ifNode.else) { - def.push( - path.map( - (ifPath: FastPath) => ifPath.call(print, 'else'), - 'children', - )[0], - ); - } - return def; + if (node.elseif) { + def = dedent(def); + } else { + def.push('{/if}'); } - return ['{:else}', printSvelteBlockChildren(path, print, options)]; + return group([def, breakParent]); } case 'EachBlock': { const def: Doc[] = [ @@ -524,10 +515,10 @@ export function print(path: FastPath, options: ParserOptions, print: PrintFn): D def.push(' (', printJS(path, print, 'key'), ')'); } - def.push('}', printSvelteBlockChildren(path, print, options)); + def.push('}', printSvelteBlockFragment(path, print, 'body')); - if (node.else) { - def.push(path.call(print, 'else')); + if (node.fallback) { + def.push('{:else}', printSvelteBlockFragment(path, print, 'fallback')); } def.push('{/each}'); @@ -535,9 +526,9 @@ export function print(path: FastPath, options: ParserOptions, print: PrintFn): D return group([def, breakParent]); } case 'AwaitBlock': { - const hasPendingBlock = node.pending.children.some((n) => !isEmptyTextNode(n)); - const hasThenBlock = node.then.children.some((n) => !isEmptyTextNode(n)); - const hasCatchBlock = node.catch.children.some((n) => !isEmptyTextNode(n)); + const hasPendingBlock = (node.pending?.nodes ?? []).some((n) => !isEmptyTextNode(n)); + const hasThenBlock = (node.then?.nodes ?? []).some((n) => !isEmptyTextNode(n)); + const hasCatchBlock = (node.catch?.nodes ?? []).some((n) => !isEmptyTextNode(n)); let block = []; @@ -550,7 +541,7 @@ export function print(path: FastPath, options: ParserOptions, print: PrintFn): D expandNode(node.value, options.originalText), '}', ]), - path.call(print, 'then'), + printSvelteBlockFragment(path, print, 'then'), ); } else if (!hasPendingBlock && hasCatchBlock) { block.push( @@ -561,19 +552,19 @@ export function print(path: FastPath, options: ParserOptions, print: PrintFn): D expandNode(node.error, options.originalText), '}', ]), - path.call(print, 'catch'), + printSvelteBlockFragment(path, print, 'catch'), ); } else { block.push(group(['{#await ', printJS(path, print, 'expression'), '}'])); if (hasPendingBlock) { - block.push(path.call(print, 'pending')); + block.push(printSvelteBlockFragment(path, print, 'pending')); } if (hasThenBlock) { block.push( group(['{:then', expandNode(node.value, options.originalText), '}']), - path.call(print, 'then'), + printSvelteBlockFragment(path, print, 'then'), ); } } @@ -581,7 +572,7 @@ export function print(path: FastPath, options: ParserOptions, print: PrintFn): D if ((hasPendingBlock || hasThenBlock) && hasCatchBlock) { block.push( group(['{:catch', expandNode(node.error, options.originalText), '}']), - path.call(print, 'catch'), + printSvelteBlockFragment(path, print, 'catch'), ); } @@ -594,31 +585,26 @@ export function print(path: FastPath, options: ParserOptions, print: PrintFn): D '{#key ', printJS(path, print, 'expression'), '}', - printSvelteBlockChildren(path, print, options), + printSvelteBlockFragment(path, print, 'fragment'), ]; def.push('{/key}'); return group([def, breakParent]); } - case 'ThenBlock': - case 'PendingBlock': - case 'CatchBlock': - return printSvelteBlockChildren(path, print, options); - // Svelte 5 only case 'SnippetBlock': { const snippet = ['{#snippet ', printJS(path, print, 'expression')]; - snippet.push('}', printSvelteBlockChildren(path, print, options), '{/snippet}'); + snippet.push('}', printSvelteBlockFragment(path, print, 'body'), '{/snippet}'); return snippet; } - case 'EventHandler': + case 'OnDirective': return [ 'on:', node.name, node.modifiers && node.modifiers.length ? ['|', join('|', node.modifiers)] : '', node.expression ? ['=', ...printJsExpression()] : '', ]; - case 'Binding': + case 'BindDirective': return [ 'bind:', node.name, @@ -628,7 +614,7 @@ export function print(path: FastPath, options: ParserOptions, print: PrintFn): D ? '' : ['=', ...printJsExpression()], ]; - case 'Class': + case 'ClassDirective': return [ 'class:', node.name, @@ -652,9 +638,7 @@ export function print(path: FastPath, options: ParserOptions, print: PrintFn): D return [...prefix, `=${open}`, node.name, close]; } } else { - const quotes = - !isLoneMustacheTag(node.value) || - ((options.svelteStrictMode && !options._svelte_is5Plus) ?? false); + const quotes = !isLoneExpressionTag(node.value); const attrNodeValue = printAttributeNodeValue(path, print, quotes, node); if (quotes) { return [...prefix, '=', '"', attrNodeValue, '"']; @@ -662,7 +646,7 @@ export function print(path: FastPath, options: ParserOptions, print: PrintFn): D return [...prefix, '=', attrNodeValue]; } } - case 'Let': + case 'LetDirective': return [ 'let:', node.name, @@ -680,8 +664,6 @@ export function print(path: FastPath, options: ParserOptions, print: PrintFn): D : '', '}', ]; - case 'Ref': - return ['ref:', node.name]; case 'Comment': { const nodeAfterComment = getNextNode(path); @@ -704,9 +686,15 @@ export function print(path: FastPath, options: ParserOptions, print: PrintFn): D ignoreNext = true; } - return printComment(node); + let text = node.data; + + if (hasSnippedContent(text)) { + text = unsnipContent(text); + } + + return group(['']); } - case 'Transition': + case 'TransitionDirective': const kind = node.intro && node.outro ? 'transition' : node.intro ? 'in' : 'out'; return [ kind, @@ -715,36 +703,33 @@ export function print(path: FastPath, options: ParserOptions, print: PrintFn): D node.modifiers && node.modifiers.length ? ['|', join('|', node.modifiers)] : '', node.expression ? ['=', ...printJsExpression()] : '', ]; - case 'Action': + case 'UseDirective': return ['use:', node.name, node.expression ? ['=', ...printJsExpression()] : '']; - case 'Animation': + case 'AnimateDirective': return ['animate:', node.name, node.expression ? ['=', ...printJsExpression()] : '']; - case 'RawMustacheTag': + case 'HtmlTag': return ['{@html ', printJS(path, print, 'expression'), '}']; - // Svelte 5 only case 'RenderTag': { const render = ['{@render ', printJS(path, print, 'expression'), '}']; return render; } - case 'Spread': + case 'SpreadAttribute': return ['{...', printJS(path, print, 'expression'), '}']; case 'ConstTag': - return ['{@const ', printJS(path, print, 'expression'), '}']; + return ['{@', printJS(path, print, 'declaration'), '}']; } console.error(JSON.stringify(node, null, 4)); throw new Error('unknown node type: ' + node.type); } -function printTopLevelParts( - n: ASTNode, - options: ParserOptions, - path: FastPath, - print: PrintFn, -): Doc { +function printTopLevelParts(n: Root, options: ParserOptions, path: AstPath, print: PrintFn): Doc { if (options.svelteSortOrder === 'none') { const topLevelPartsByEnd: Record = {}; + if (n.options) { + topLevelPartsByEnd[n.options.end] = n.options; + } if (n.module) { topLevelPartsByEnd[n.module.end] = n.module; } @@ -755,7 +740,7 @@ function printTopLevelParts( topLevelPartsByEnd[n.css.end] = n.css; } - const children = getChildren(n.html); + const children = getChildren(n); for (let i = 0; i < children.length; i++) { const node = children[i]; if (topLevelPartsByEnd[node.start]) { @@ -764,7 +749,7 @@ function printTopLevelParts( } } - const result = path.call(print, 'html'); + const result = [path.call(print, 'fragment'), hardline]; if (options.insertPragma && !hasPragma(options.originalText)) { return [``, hardline, result]; } else { @@ -779,7 +764,11 @@ function printTopLevelParts( styles: [], }; - // scripts + if (n.options) { + const svelteOptionsDoc = [path.call(print, 'options'), hardline]; + parts.options.push(svelteOptionsDoc); + } + if (n.module) { parts.scripts.push(path.call(print, 'module')); } @@ -787,18 +776,13 @@ function printTopLevelParts( parts.scripts.push(path.call(print, 'instance')); } - // styles - if (n.css) { - parts.styles.push(path.call(print, 'css')); - } - - // markup - const htmlDoc = path.call(print, 'html'); + const htmlDoc = path.call(print, 'fragment'); if (htmlDoc) { - parts.markup.push(htmlDoc); + parts.markup.push([htmlDoc, hardline]); } - if (svelteOptionsDoc) { - parts.options.push(svelteOptionsDoc); + + if (n.css) { + parts.styles.push(path.call(print, 'css')); } const docs = flatten(parseSortOrder(options.svelteSortOrder).map((p) => parts[p])); @@ -806,7 +790,6 @@ function printTopLevelParts( // Need to reset these because they are global and could affect the next formatting run ignoreNext = false; ignoreRange = false; - svelteOptionsDoc = undefined; // If this is invoked as an embed of markdown, remove the last hardline. // The markdown parser tries this, too, but fails because it does not @@ -825,7 +808,7 @@ function printTopLevelParts( } function printAttributeNodeValue( - path: FastPath, + path: AstPath, print: PrintFn, quotes: boolean, node: AttributeNode | StyleDirectiveNode, @@ -839,25 +822,31 @@ function printAttributeNodeValue( } } -function printSvelteBlockChildren(path: FastPath, print: PrintFn, options: ParserOptions): Doc { - const node = path.getValue(); - const children = node.children; +function printSvelteBlockFragment( + path: AstPath, + print: PrintFn, + name: string, + shouldIndent = true, +): Doc { + const node = path.node[name] as Fragment; + + const children = node.nodes; if (!children || children.length === 0) { return ''; } - const whitespaceAtStartOfBlock = checkWhitespaceAtStartOfSvelteBlock(node, options); - const whitespaceAtEndOfBlock = checkWhitespaceAtEndOfSvelteBlock(node, options); + const whitespaceAtStartOfFragment = checkWhitespaceAtStartOfFragment(node); + const whitespaceAtEndOfFragment = checkWhitespaceAtEndOfFragment(node); const startline = - whitespaceAtStartOfBlock === 'none' + whitespaceAtStartOfFragment === 'none' ? '' - : whitespaceAtEndOfBlock === 'line' || whitespaceAtStartOfBlock === 'line' + : whitespaceAtEndOfFragment === 'line' || whitespaceAtStartOfFragment === 'line' ? hardline : line; const endline = - whitespaceAtEndOfBlock === 'none' + whitespaceAtEndOfFragment === 'none' ? '' - : whitespaceAtEndOfBlock === 'line' || whitespaceAtStartOfBlock === 'line' + : whitespaceAtEndOfFragment === 'line' || whitespaceAtStartOfFragment === 'line' ? hardline : line; @@ -870,19 +859,22 @@ function printSvelteBlockChildren(path: FastPath, print: PrintFn, options: Parse trimTextNodeRight(lastChild); } - return [indent([startline, group(printChildren(path, print, options))]), endline]; + // return [indent([startline, group(printChildren(path, print, options))]), endline]; + return shouldIndent + ? [indent([startline, group(path.call(print, name))]), endline] + : [startline, group(path.call(print, name)), endline]; } function printPre( node: Parameters[0], originalText: string, - path: FastPath, + path: AstPath, print: PrintFn, ): Doc { const result: Doc = []; - const length = node.children.length; + const length = node.fragment.nodes.length; for (let i = 0; i < length; i++) { - const child = node.children[i]; + const child = node.fragment.nodes[i]; if (child.type === 'Text') { const lines = originalText.substring(child.start, child.end).split(/\r?\n/); lines.forEach((line, j) => { @@ -890,20 +882,19 @@ function printPre( result.push(line); }); } else { - result.push(path.call(print, 'children', i)); + result.push(path.call(print, 'fragment', 'nodes', i)); } } return result; } -function printChildren(path: FastPath, print: PrintFn, options: ParserOptions): Doc { +//should get fragment +function printChildren(path: AstPath, print: PrintFn, options: ParserOptions): Doc { if (isPreTagContent(path)) { - return path.map(print, 'children'); + return path.map(print, 'nodes'); } - const childNodes: Node[] = prepareChildren(path.getValue().children, path, print, options); - // modify original array because it's accessed later through map(print, 'children', idx) - path.getValue().children = childNodes; + const childNodes = path.getValue().nodes; if (childNodes.length === 0) { return ''; } @@ -935,7 +926,7 @@ function printChildren(path: FastPath, print: PrintFn, options: ParserOptions): return childDocs; function printChild(idx: number): Doc { - return path.call(print, 'children', idx); + return path.call(print, 'nodes', idx); } /** @@ -995,7 +986,7 @@ function printChildren(path: FastPath, print: PrintFn, options: ParserOptions): * subsequent (inline)block element to alter its printing logic * to check if they need to hug or print lines themselves. */ - function handleTextChild(idx: number, childNode: TextNode) { + function handleTextChild(idx: number, childNode: Text) { handleWhitespaceOfPrevTextNode = false; if (idx === 0 || idx === childNodes.length - 1) { @@ -1043,122 +1034,13 @@ function printChildren(path: FastPath, print: PrintFn, options: ParserOptions): } } -/** - * `svelte:options` is part of the html part but needs to be snipped out and handled - * separately to reorder it as configured. The comment above it should be moved with it. - * Do that here. - */ -function prepareChildren( - children: Node[], - path: FastPath, - print: PrintFn, - options: ParserOptions, -): Node[] { - let svelteOptionsComment: Doc | undefined; - const childrenWithoutOptions = []; - const bracketSameLine = isBracketSameLine(options); - - for (let idx = 0; idx < children.length; idx++) { - const currentChild = children[idx]; - - if (currentChild.type === 'Text' && getUnencodedText(currentChild) === '') { - continue; - } - - if (isEmptyTextNode(currentChild) && doesEmbedStartAfterNode(currentChild, path)) { - continue; - } - - if (options.svelteSortOrder !== 'none') { - if (isCommentFollowedByOptions(currentChild, idx)) { - svelteOptionsComment = printComment(currentChild); - const nextChild = children[idx + 1]; - idx += nextChild && isEmptyTextNode(nextChild) ? 1 : 0; - continue; - } - - if (currentChild.type === 'Options') { - printSvelteOptions(currentChild, idx, path, print); - continue; - } - } - - childrenWithoutOptions.push(currentChild); - } - - const mergedChildrenWithoutOptions = []; - - for (let idx = 0; idx < childrenWithoutOptions.length; idx++) { - const currentChild = childrenWithoutOptions[idx]; - const nextChild = childrenWithoutOptions[idx + 1]; - - if (currentChild.type === 'Text' && nextChild && nextChild.type === 'Text') { - // A tag was snipped out (f.e. svelte:options). Join text - currentChild.raw += nextChild.raw; - currentChild.data += nextChild.data; - idx++; - } - - mergedChildrenWithoutOptions.push(currentChild); - } - - return mergedChildrenWithoutOptions; - - function printSvelteOptions( - node: OptionsNode, - idx: number, - path: FastPath, - print: PrintFn, - ): void { - svelteOptionsDoc = group([ - [ - '<', - node.name, - indent( - group([ - ...path.map( - printWithPrependedAttributeLine(node, options, print), - 'children', - idx, - 'attributes', - ), - bracketSameLine ? '' : dedent(line), - ]), - ), - ...[bracketSameLine ? ' ' : '', '/>'], - ], - hardline, - ]); - if (svelteOptionsComment) { - svelteOptionsDoc = group([svelteOptionsComment, hardline, svelteOptionsDoc]); - } - } - - function isCommentFollowedByOptions(node: Node, idx: number): node is CommentNode { - if (node.type !== 'Comment' || isIgnoreEndDirective(node) || isIgnoreStartDirective(node)) { - return false; - } - - const nextChild = children[idx + 1]; - if (nextChild) { - if (isEmptyTextNode(nextChild)) { - const afterNext = children[idx + 2]; - return afterNext && afterNext.type === 'Options'; - } - return nextChild.type === 'Options'; - } - - return false; - } -} - /** * Split the text into words separated by whitespace. Replace the whitespaces by lines, * collapsing multiple whitespaces into a single line. * * If the text starts or ends with multiple newlines, two of those should be kept. */ -function splitTextToDocs(node: TextNode): Doc[] { +function splitTextToDocs(node: Text): Doc[] { const text = getUnencodedText(node); const lines = text.split(/[\t\n\f\r ]+/); @@ -1181,7 +1063,7 @@ function splitTextToDocs(node: TextNode): Doc[] { return docs; } -function printJS(path: FastPath, print: PrintFn, name: string) { +function printJS(path: AstPath, print: PrintFn, name: string) { return path.call(print, name); } @@ -1235,13 +1117,3 @@ function _expandNode(node: any, parent?: any): string { console.error(JSON.stringify(node, null, 4)); throw new Error('unknown node type: ' + node.type); } - -function printComment(node: CommentNode) { - let text = node.data; - - if (hasSnippedContent(text)) { - text = unsnipContent(text); - } - - return group(['']); -} diff --git a/src/print/node-helpers.ts b/src/print/node-helpers.ts index abf1cb5a..59326add 100644 --- a/src/print/node-helpers.ts +++ b/src/print/node-helpers.ts @@ -1,46 +1,55 @@ import { - Node, - ElementNode, - TextNode, - AttributeNode, - MustacheTagNode, - AttributeShorthandNode, - HeadNode, - InlineComponentNode, - SlotNode, - TitleNode, - WindowNode, - IfBlockNode, - AwaitBlockNode, - CatchBlockNode, - EachBlockNode, - ElseBlockNode, - KeyBlockNode, - PendingBlockNode, - ThenBlockNode, - CommentNode, - SlotTemplateNode, - StyleDirectiveNode, - ASTNode, + Attribute, + AwaitBlock, + Block, + Comment, CommentInfo, + Component, + Css, + EachBlock, + ElementLike, + ExpressionTag, + Fragment, + IfBlock, + KeyBlock, + RegularElement, + Root, + Script, + SlotElement, + SnippetBlock, + StyleDirective, + SvelteComponent, + SvelteElement, + SvelteFragment, + SvelteHead, + SvelteNode, + SvelteOptions, + SvelteSelf, + SvelteWindow, + Tag, + Text, + TitleElement, } from './nodes'; import { blockElements, TagName } from '../lib/elements'; -import { FastPath } from 'prettier'; -import { findLastIndex, isASTNode, isPreTagContent } from './helpers'; +import { AstPath } from 'prettier'; +import { findLastIndex, isPreTagContent } from './helpers'; import { ParserOptions, isBracketSameLine } from '../options'; const unsupportedLanguages = ['coffee', 'coffeescript', 'styl', 'stylus', 'sass']; -export function isInlineElement(path: FastPath, options: ParserOptions, node: Node) { +export function isInlineElement(path: AstPath, options: ParserOptions, node: SvelteNode) { return ( - node && node.type === 'Element' && !isBlockElement(node, options) && !isPreTagContent(path) + node && + node.type === 'RegularElement' && + !isBlockElement(node, options) && + !isPreTagContent(path) ); } -export function isBlockElement(node: Node, options: ParserOptions): node is ElementNode { +export function isBlockElement(node: SvelteNode, options: ParserOptions): node is RegularElement { return ( node && - node.type === 'Element' && + node.type === 'RegularElement' && options.htmlWhitespaceSensitivity !== 'strict' && (options.htmlWhitespaceSensitivity === 'ignore' || blockElements.includes(node.name as TagName)) @@ -48,73 +57,47 @@ export function isBlockElement(node: Node, options: ParserOptions): node is Elem } export function isSvelteBlock( - node: Node, -): node is - | IfBlockNode - | AwaitBlockNode - | CatchBlockNode - | EachBlockNode - | ElseBlockNode - | KeyBlockNode - | PendingBlockNode - | ThenBlockNode { - return [ - 'IfBlock', - 'SnippetBlock', - 'AwaitBlock', - 'CatchBlock', - 'EachBlock', - 'ElseBlock', - 'KeyBlock', - 'PendingBlock', - 'ThenBlock', - ].includes(node.type); -} - -export function isNodeWithChildren(node: Node): node is Node & { children: Node[] } { - return (node as any).children; -} - -export function getChildren(node: Node): Node[] { - return isNodeWithChildren(node) ? node.children : []; + node: SvelteNode, +): node is IfBlock | SnippetBlock | AwaitBlock | EachBlock | KeyBlock { + return ['IfBlock', 'SnippetBlock', 'AwaitBlock', 'EachBlock', 'KeyBlock'].includes(node.type); +} + +export function isNodeWithChildren(node: SvelteNode): node is SvelteNode & { fragment: Fragment } { + return (node as any).fragment?.type === 'Fragment'; +} + +export function getChildren(node: SvelteNode): Array { + return isNodeWithChildren(node) ? node.fragment.nodes : []; } /** * Returns siblings, that is, the children of the parent. */ -export function getSiblings(path: FastPath): Node[] { - let parent: Node = path.getParentNode(); +export function getSiblings(path: AstPath): SvelteNode[] { + let parent: SvelteNode = path.getParentNode(); - if (isASTNode(parent)) { - parent = parent.html; - } + if (parent.type === 'Fragment') return parent.nodes; return getChildren(parent); } -/** - * Returns the previous sibling node. - */ -export function getPreviousNode(path: FastPath): Node | undefined { - const node: Node = path.getNode(); - return getSiblings(path).find((child) => child.end === node.start); -} - /** * Returns the next sibling node. */ -export function getNextNode(path: FastPath, node: Node = path.getNode()): Node | undefined { +export function getNextNode(path: AstPath): SvelteNode | undefined { + const node: SvelteNode = path.getNode(); + return getSiblings(path).find((child) => child.start === node.end); } /** * Returns the comment that is above the current node. */ -export function getLeadingComment(path: FastPath): CommentNode | undefined { +export function getLeadingComment(path: AstPath): Comment | undefined { const siblings = getSiblings(path); - let node: Node = path.getNode(); - let prev: Node | undefined = siblings.find((child) => child.end === node.start); + let node: SvelteNode = path.getNode(); + let prev: SvelteNode | undefined = siblings.find((child) => child.end === node.start); while (prev) { if ( prev.type === 'Comment' && @@ -135,7 +118,7 @@ export function getLeadingComment(path: FastPath): CommentNode | undefined { * Did there use to be any embedded object (that has been snipped out of the AST to be moved) * at the specified position? */ -export function doesEmbedStartAfterNode(node: Node, path: FastPath, siblings = getSiblings(path)) { +export function doesEmbedStartAfterNode(node: SvelteNode, path: AstPath) { // If node is not at the top level of html, an embed cannot start after it, // because embeds are only at the top level if (!isNodeTopLevelHTML(node, path)) { @@ -143,53 +126,62 @@ export function doesEmbedStartAfterNode(node: Node, path: FastPath, siblings = g } const position = node.end; - const root = path.stack[0]; - - const embeds = [root.css, root.html, root.instance, root.js, root.module] as Node[]; - + const root = path.stack[0] as Root; + + const embeds = [ + root.css, + root.fragment, + root.instance, + root.module, + root.options, + ] as SvelteNode[]; + const siblings = getSiblings(path); const nextNode = siblings[siblings.indexOf(node) + 1]; return embeds.find((n) => n && n.start >= position && (!nextNode || n.end <= nextNode.start)); } -export function isNodeTopLevelHTML(node: Node, path: FastPath): boolean { - const root = path.stack[0]; - return !!root.html && !!root.html.children && root.html.children.includes(node); +export function isNodeTopLevelHTML(node: SvelteNode, path: AstPath): boolean { + const root = path.stack[0] as Root | undefined; + return !!root && root.fragment.nodes.includes(node); } -export function isEmptyTextNode(node: Node | undefined): node is TextNode { +export function isEmptyTextNode(node: SvelteNode | undefined): node is Text { return !!node && node.type === 'Text' && getUnencodedText(node).trim() === ''; } -export function isIgnoreDirective(node: Node | undefined | null): boolean { +export function isIgnoreDirective(node: SvelteNode | undefined | null): boolean { return !!node && node.type === 'Comment' && node.data.trim() === 'prettier-ignore'; } -export function isIgnoreStartDirective(node: Node | undefined | null): boolean { +export function isIgnoreStartDirective(node: SvelteNode | undefined | null): boolean { return !!node && node.type === 'Comment' && node.data.trim() === 'prettier-ignore-start'; } -export function isIgnoreEndDirective(node: Node | undefined | null): boolean { +export function isIgnoreEndDirective(node: SvelteNode | undefined | null): boolean { return !!node && node.type === 'Comment' && node.data.trim() === 'prettier-ignore-end'; } export function printRaw( node: - | ElementNode - | InlineComponentNode - | SlotNode - | WindowNode - | HeadNode - | TitleNode - | SlotTemplateNode, + | RegularElement + | SvelteElement + | SvelteSelf + | SvelteComponent + | Component + | SlotElement + | SvelteWindow + | SvelteHead + | TitleElement + | SvelteFragment, originalText: string, stripLeadingAndTrailingNewline: boolean = false, ): string { - if (node.children.length === 0) { + if (node.fragment.nodes.length === 0) { return ''; } - const firstChild = node.children[0]; - const lastChild = node.children[node.children.length - 1]; + const firstChild = node.fragment.nodes[0]; + const lastChild = node.fragment.nodes[node.fragment.nodes.length - 1]; let raw = originalText.substring(firstChild.start, lastChild.end); @@ -210,19 +202,19 @@ export function printRaw( return raw; } -function isTextNode(node: Node): node is TextNode { +function isTextNode(node: SvelteNode): node is Text { return node.type === 'Text'; } -function getAttributeValue(attributeName: string, node: Node) { - const attributes = ((node as ElementNode).attributes ?? []) as AttributeNode[]; +function getAttributeValue(attributeName: string, node: SvelteNode) { + const attributes = (node as RegularElement).attributes ?? []; const langAttribute = attributes.find((attribute) => attribute.name === attributeName); return langAttribute && langAttribute.value; } -export function getAttributeTextValue(attributeName: string, node: Node): string | null { +export function getAttributeTextValue(attributeName: string, node: SvelteNode): string | null { const value = getAttributeValue(attributeName, node); if (value != null && typeof value === 'object') { @@ -236,7 +228,7 @@ export function getAttributeTextValue(attributeName: string, node: Node): string return null; } -function getLangAttribute(node: Node): string | null { +function getLangAttribute(node: SvelteNode): string | null { const value = getAttributeTextValue('lang', node) || getAttributeTextValue('type', node); if (value != null) { @@ -251,7 +243,7 @@ function getLangAttribute(node: Node): string | null { * a language we cannot format. This might for example be `