diff --git a/package-lock.json b/package-lock.json index be52543c..06ddd15d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "prettier-plugin-svelte", - "version": "2.7.0", + "version": "2.7.1", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "prettier-plugin-svelte", - "version": "2.7.0", + "version": "2.7.1", "license": "MIT", "devDependencies": { "@prettier/plugin-pug": "^1.16.0", @@ -15,7 +15,7 @@ "@types/node": "^10.12.18", "@types/prettier": "^2.4.1", "ava": "3.15.0", - "prettier": "^2.4.1", + "prettier": "^2.7.1", "rollup": "2.36.0", "rollup-plugin-typescript": "1.0.1", "svelte": "^3.47.0", @@ -2730,15 +2730,18 @@ } }, "node_modules/prettier": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.4.1.tgz", - "integrity": "sha512-9fbDAXSBcc6Bs1mZrDYb3XKzDLm4EXXL9sC1LqKP5rZkT6KRr/rf9amVUcODVXgguK/isJz0d0hP72WeaKWsvA==", + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.7.1.tgz", + "integrity": "sha512-ujppO+MkdPqoVINuDFDRLClm7D78qbDt0/NR+wp5FqEZOoTNAjPHWj17QRhu7geIHJfcNhRk1XVQmF8Bp3ye+g==", "dev": true, "bin": { "prettier": "bin-prettier.js" }, "engines": { "node": ">=10.13.0" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" } }, "node_modules/pretty-ms": { @@ -5746,9 +5749,9 @@ "dev": true }, "prettier": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.4.1.tgz", - "integrity": "sha512-9fbDAXSBcc6Bs1mZrDYb3XKzDLm4EXXL9sC1LqKP5rZkT6KRr/rf9amVUcODVXgguK/isJz0d0hP72WeaKWsvA==", + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.7.1.tgz", + "integrity": "sha512-ujppO+MkdPqoVINuDFDRLClm7D78qbDt0/NR+wp5FqEZOoTNAjPHWj17QRhu7geIHJfcNhRk1XVQmF8Bp3ye+g==", "dev": true }, "pretty-ms": { diff --git a/package.json b/package.json index 0c365326..850ddbec 100644 --- a/package.json +++ b/package.json @@ -34,7 +34,7 @@ "@types/node": "^10.12.18", "@types/prettier": "^2.4.1", "ava": "3.15.0", - "prettier": "^2.4.1", + "prettier": "^2.7.1", "rollup": "2.36.0", "rollup-plugin-typescript": "1.0.1", "svelte": "^3.47.0", diff --git a/src/embed.ts b/src/embed.ts index fca38e3e..cdf78b3a 100644 --- a/src/embed.ts +++ b/src/embed.ts @@ -1,8 +1,10 @@ import { Doc, doc, FastPath, ParserOptions } from 'prettier'; import { getText } from './lib/getText'; import { snippedTagContentAttribute } from './lib/snipTagContent'; +import { isBracketSameLine } from './options'; import { PrintFn } from './print'; import { isLine, removeParentheses, trimRight } from './print/doc-helpers'; +import { groupConcat, printWithPrependedAttributeLine } from './print/helpers'; import { getAttributeTextValue, getLeadingComment, @@ -12,10 +14,10 @@ import { isTypeScript, printRaw, } from './print/node-helpers'; -import { ElementNode, Node } from './print/nodes'; +import { ElementNode, Node, ScriptNode, StyleNode } from './print/nodes'; const { - builders: { concat, hardline, group, indent, literalline }, + builders: { concat, hardline, softline, indent, dedent, literalline }, utils: { removeLines }, } = doc; @@ -188,7 +190,7 @@ function embedTag( isTopLevel: boolean, options: ParserOptions, ) { - const node: Node = path.getNode(); + const node: ScriptNode | StyleNode | ElementNode = path.getNode(); const content = tag === 'template' ? printRaw(node as ElementNode, text) : getSnippedContent(node); const previousComment = getLeadingComment(path); @@ -208,19 +210,18 @@ function embedTag( : hardline : preformattedBody(content); - const attributes = concat( - path.map( - (childPath) => - childPath.getNode().name !== snippedTagContentAttribute - ? childPath.call(print) - : '', - 'attributes', + const openingTag = groupConcat([ + '<', + tag, + indent( + groupConcat([ + ...path.map(printWithPrependedAttributeLine(node, options, print), 'attributes'), + isBracketSameLine(options) ? '' : dedent(softline), + ]), ), - ); - - let result: Doc = group( - concat(['<', tag, indent(group(attributes)), '>', body, '']), - ); + '>', + ]); + let result = groupConcat([openingTag, body, '']); if (isTopLevel) { // top level embedded nodes have been moved from their normal position in the diff --git a/src/lib/extractAttributes.ts b/src/lib/extractAttributes.ts index d46ca66f..bd5208a8 100644 --- a/src/lib/extractAttributes.ts +++ b/src/lib/extractAttributes.ts @@ -1,8 +1,8 @@ import { AttributeNode, TextNode } from '../print/nodes'; export function extractAttributes(html: string): AttributeNode[] { - const extractAttributesRegex = /<[a-z]+\s*(.*?)>/i; - const attributeRegex = /([^\s=]+)(?:=("|')(.*?)\2)?/gi; + const extractAttributesRegex = /<[a-z]+[\s\n]*([\s\S]*?)>/im; + const attributeRegex = /([^\s=]+)(?:=("|')([\s\S]*?)\2)?/gim; const [, attributesString] = html.match(extractAttributesRegex)!; diff --git a/src/print/helpers.ts b/src/print/helpers.ts index 35192dab..904d03cf 100644 --- a/src/print/helpers.ts +++ b/src/print/helpers.ts @@ -1,6 +1,23 @@ -import { ASTNode, Node } from './nodes'; -import { Doc, FastPath } from 'prettier'; +import { + ASTNode, + AttributeNode, + BodyNode, + ElementNode, + HeadNode, + InlineComponentNode, + Node, + OptionsNode, + ScriptNode, + SlotNode, + SlotTemplateNode, + StyleNode, + TitleNode, + WindowNode, +} from './nodes'; +import { Doc, doc, FastPath, ParserOptions } from 'prettier'; import { formattableAttributes } from '../lib/elements'; +import { PrintFn } from '.'; +import { snippedTagContentAttribute } from '../lib/snipTagContent'; /** * Determines whether or not given node @@ -48,3 +65,59 @@ export function replaceEndOfLineWith(text: string, replacement: Doc) { } return parts; } + +export function groupConcat(contents: doc.builders.Doc[]): doc.builders.Doc { + const { concat, group } = doc.builders; + return group(concat(contents)); +} + +export function getAttributeLine( + node: + | ElementNode + | InlineComponentNode + | SlotNode + | WindowNode + | HeadNode + | TitleNode + | StyleNode + | ScriptNode + | BodyNode + | OptionsNode + | SlotTemplateNode, + options: ParserOptions, +) { + const { hardline, line } = doc.builders; + const hasThisBinding = + (node.type === 'InlineComponent' && !!node.expression) || + (node.type === 'Element' && !!node.tag); + + const attributes = (node.attributes as Array).filter( + (attribute) => attribute.name !== snippedTagContentAttribute, + ); + return options.singleAttributePerLine && + (attributes.length > 1 || (attributes.length && hasThisBinding)) + ? hardline + : line; +} + +export function printWithPrependedAttributeLine( + node: + | ElementNode + | InlineComponentNode + | SlotNode + | WindowNode + | HeadNode + | TitleNode + | StyleNode + | ScriptNode + | BodyNode + | OptionsNode + | SlotTemplateNode, + options: ParserOptions, + print: PrintFn, +): PrintFn { + return (path) => + path.getNode().name !== snippedTagContentAttribute + ? doc.builders.concat([getAttributeLine(node, options), path.call(print)]) + : ''; +} diff --git a/src/print/index.ts b/src/print/index.ts index b5915ba1..0b8af275 100644 --- a/src/print/index.ts +++ b/src/print/index.ts @@ -5,7 +5,15 @@ import { getText } from '../lib/getText'; import { hasSnippedContent, unsnipContent } from '../lib/snipTagContent'; import { isBracketSameLine, parseSortOrder, SortOrderPart } from '../options'; import { isEmptyDoc, isLine, trim, trimRight } from './doc-helpers'; -import { flatten, isASTNode, isPreTagContent, replaceEndOfLineWith } from './helpers'; +import { + flatten, + getAttributeLine, + groupConcat, + isASTNode, + isPreTagContent, + printWithPrependedAttributeLine, + replaceEndOfLineWith, +} from './helpers'; import { checkWhitespaceAtEndOfSvelteBlock, checkWhitespaceAtStartOfSvelteBlock, @@ -78,10 +86,6 @@ let ignoreNext = false; let ignoreRange = false; let svelteOptionsDoc: Doc | undefined; -function groupConcat(contents: doc.builders.Doc[]): doc.builders.Doc { - return group(concat(contents)); -} - export function print(path: FastPath, options: ParserOptions, print: PrintFn): Doc { const bracketSameLine = isBracketSameLine(options); @@ -200,13 +204,17 @@ export function print(path: FastPath, options: ParserOptions, print: PrintFn): D isDoctypeTag); // Order important: print attributes first - const attributes = path.map((childPath) => childPath.call(print), 'attributes'); + const attributes = path.map( + printWithPrependedAttributeLine(node, options, print), + 'attributes', + ); + const attributeLine = getAttributeLine(node, options); const possibleThisBinding = node.type === 'InlineComponent' && node.expression - ? concat([line, 'this=', ...printJsExpression()]) + ? concat([attributeLine, 'this=', ...printJsExpression()]) : node.type === 'Element' && node.tag ? concat([ - line, + attributeLine, 'this=', ...(typeof node.tag === 'string' ? [`"${node.tag}"`] @@ -385,10 +393,16 @@ export function print(path: FastPath, options: ParserOptions, print: PrintFn): D return groupConcat([ '<', node.name, - - indent(groupConcat(path.map((childPath) => childPath.call(print), 'attributes'))), - - ' />', + indent( + groupConcat([ + ...path.map( + printWithPrependedAttributeLine(node, options, print), + 'attributes', + ), + bracketSameLine ? '' : dedent(line), + ]), + ), + ...[bracketSameLine ? ' ' : '', '/>'], ]); case 'Identifier': return node.name; @@ -398,23 +412,23 @@ export function print(path: FastPath, options: ParserOptions, print: PrintFn): D case 'Attribute': { if (isOrCanBeConvertedToShorthand(node)) { if (options.svelteStrictMode) { - return concat([line, node.name, '="{', node.name, '}"']); + return concat([node.name, '="{', node.name, '}"']); } else if (options.svelteAllowShorthand) { - return concat([line, '{', node.name, '}']); + return concat(['{', node.name, '}']); } else { - return concat([line, node.name, '={', node.name, '}']); + return concat([node.name, '={', node.name, '}']); } } else { if (node.value === true) { - return concat([line, node.name]); + return concat([node.name]); } const quotes = !isLoneMustacheTag(node.value) || options.svelteStrictMode; const attrNodeValue = printAttributeNodeValue(path, print, quotes, node); if (quotes) { - return concat([line, node.name, '=', '"', attrNodeValue, '"']); + return concat([node.name, '=', '"', attrNodeValue, '"']); } else { - return concat([line, node.name, '=', attrNodeValue]); + return concat([node.name, '=', attrNodeValue]); } } } @@ -568,7 +582,6 @@ export function print(path: FastPath, options: ParserOptions, print: PrintFn): D return printSvelteBlockChildren(path, print, options); case 'EventHandler': return concat([ - line, 'on:', node.name, node.modifiers && node.modifiers.length @@ -578,7 +591,6 @@ export function print(path: FastPath, options: ParserOptions, print: PrintFn): D ]); case 'Binding': return concat([ - line, 'bind:', node.name, node.expression.type === 'Identifier' && node.expression.name === node.name @@ -587,7 +599,6 @@ export function print(path: FastPath, options: ParserOptions, print: PrintFn): D ]); case 'Class': return concat([ - line, 'class:', node.name, node.expression.type === 'Identifier' && node.expression.name === node.name @@ -597,28 +608,27 @@ export function print(path: FastPath, options: ParserOptions, print: PrintFn): D case 'StyleDirective': if (isOrCanBeConvertedToShorthand(node)) { if (options.svelteStrictMode) { - return concat([line, 'style:', node.name, '="{', node.name, '}"']); + return concat(['style:', node.name, '="{', node.name, '}"']); } else if (options.svelteAllowShorthand) { - return concat([line, 'style:', node.name]); + return concat(['style:', node.name]); } else { - return concat([line, 'style:', node.name, '={', node.name, '}']); + return concat(['style:', node.name, '={', node.name, '}']); } } else { if (node.value === true) { - return concat([line, 'style:', node.name]); + return concat(['style:', node.name]); } const quotes = !isLoneMustacheTag(node.value) || options.svelteStrictMode; const attrNodeValue = printAttributeNodeValue(path, print, quotes, node); if (quotes) { - return concat([line, 'style:', node.name, '=', '"', attrNodeValue, '"']); + return concat(['style:', node.name, '=', '"', attrNodeValue, '"']); } else { - return concat([line, 'style:', node.name, '=', attrNodeValue]); + return concat(['style:', node.name, '=', attrNodeValue]); } } case 'Let': return concat([ - line, 'let:', node.name, // shorthand let directives have `null` expressions @@ -636,7 +646,7 @@ export function print(path: FastPath, options: ParserOptions, print: PrintFn): D '}', ]); case 'Ref': - return concat([line, 'ref:', node.name]); + return concat(['ref:', node.name]); case 'Comment': { const nodeAfterComment = getNextNode(path); @@ -664,7 +674,6 @@ export function print(path: FastPath, options: ParserOptions, print: PrintFn): D case 'Transition': const kind = node.intro && node.outro ? 'transition' : node.intro ? 'in' : 'out'; return concat([ - line, kind, ':', node.name, @@ -675,14 +684,12 @@ export function print(path: FastPath, options: ParserOptions, print: PrintFn): D ]); case 'Action': return concat([ - line, 'use:', node.name, node.expression ? concat(['=', ...printJsExpression()]) : '', ]); case 'Animation': return concat([ - line, 'animate:', node.name, node.expression ? concat(['=', ...printJsExpression()]) : '', @@ -694,12 +701,7 @@ export function print(path: FastPath, options: ParserOptions, print: PrintFn): D '}', ]); case 'Spread': - return concat([ - line, - '{...', - printJS(path, print, false, false, false, 'expression'), - '}', - ]); + return concat(['{...', printJS(path, print, false, false, false, 'expression'), '}']); case 'ConstTag': return concat([ '{@const ', @@ -852,7 +854,7 @@ function printChildren(path: FastPath, print: PrintFn, options: ParserOptions): return concat(path.map(print, 'children')); } - const childNodes: Node[] = prepareChildren(path.getValue().children, path, print); + 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; if (childNodes.length === 0) { @@ -999,9 +1001,15 @@ function printChildren(path: FastPath, print: PrintFn, options: ParserOptions): * 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): Node[] { +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]; @@ -1057,10 +1065,18 @@ function prepareChildren(children: Node[], path: FastPath, print: PrintFn): Node groupConcat([ '<', node.name, - - indent(groupConcat(path.map(print, 'children', idx, 'attributes'))), - - ' />', + indent( + groupConcat([ + ...path.map( + printWithPrependedAttributeLine(node, options, print), + 'children', + idx, + 'attributes', + ), + bracketSameLine ? '' : dedent(line), + ]), + ), + ...[bracketSameLine ? ' ' : '', '/>'], ]), hardline, ]); diff --git a/test/formatting/samples/single-attribute-per-line/input.html b/test/formatting/samples/single-attribute-per-line/input.html new file mode 100644 index 00000000..971d726e --- /dev/null +++ b/test/formatting/samples/single-attribute-per-line/input.html @@ -0,0 +1,38 @@ + + + + + + +
Copy
+ +
Copy
+ +Copy + +
+ + + + + + + + + Foo + + + +

hi

+
+ + diff --git a/test/formatting/samples/single-attribute-per-line/options.json b/test/formatting/samples/single-attribute-per-line/options.json new file mode 100644 index 00000000..c4d8ac54 --- /dev/null +++ b/test/formatting/samples/single-attribute-per-line/options.json @@ -0,0 +1,3 @@ +{ + "singleAttributePerLine": true +} diff --git a/test/formatting/samples/single-attribute-per-line/output.html b/test/formatting/samples/single-attribute-per-line/output.html new file mode 100644 index 00000000..129f9fae --- /dev/null +++ b/test/formatting/samples/single-attribute-per-line/output.html @@ -0,0 +1,70 @@ + + + + + + +
+ Copy +
+ +
Copy
+ +Copy + +
+ + + + + + + + + Foo + + + +

hi

+
+ + diff --git a/test/printer/samples/single-attribute-per-line-bracket-no-new-line.html b/test/printer/samples/single-attribute-per-line-bracket-no-new-line.html new file mode 100644 index 00000000..c4aa1b24 --- /dev/null +++ b/test/printer/samples/single-attribute-per-line-bracket-no-new-line.html @@ -0,0 +1,58 @@ + + + + + + +
+ Copy +
+ +
Copy
+ +Copy + +
+ + + + + + + + + Foo + + +

hi

+ + diff --git a/test/printer/samples/single-attribute-per-line-bracket-no-new-line.options.json b/test/printer/samples/single-attribute-per-line-bracket-no-new-line.options.json new file mode 100644 index 00000000..fae8329b --- /dev/null +++ b/test/printer/samples/single-attribute-per-line-bracket-no-new-line.options.json @@ -0,0 +1,4 @@ +{ + "singleAttributePerLine": true, + "bracketSameLine": true +} diff --git a/test/printer/samples/single-attribute-per-line.html b/test/printer/samples/single-attribute-per-line.html new file mode 100644 index 00000000..28ed30e6 --- /dev/null +++ b/test/printer/samples/single-attribute-per-line.html @@ -0,0 +1,68 @@ + + + + + + +
+ Copy +
+ +
Copy
+ +Copy + +
+ + + + + + + + + Foo + + +

hi

+ + diff --git a/test/printer/samples/single-attribute-per-line.options.json b/test/printer/samples/single-attribute-per-line.options.json new file mode 100644 index 00000000..c4d8ac54 --- /dev/null +++ b/test/printer/samples/single-attribute-per-line.options.json @@ -0,0 +1,3 @@ +{ + "singleAttributePerLine": true +}