Repository: sveltejs/prettier-plugin-svelte (ts)

Issue:
[feat] Break down big prop objects into separate lines
**Is your feature request related to a problem? Please describe.**
Currently prettier plugin does not break down big props/objects into multiple lines if it is placed in template. More than that, it puts everything into one huge line, ignoring the `printWidth` setting.

**Describe the solution you'd like**
It should work exactly like it does in other places.

Take this code as an example:

```svelte
<input
  class="input"
  type="password"
  bind:value={$passwordStore.rawValue}
  use:trackChanges={{ store: passwordStore, clean: [ensureString], validate: [minLength(6), maxLength(25)] }} />
  // Cannot be broken down into multiple lines no matter what
```

It should be formatted into something like this:

```svelte
<input
  class="input"
  type="password"
  bind:value={$passwordStore.rawValue}
  use:trackChanges={{
    store: passwordStore,
    clean: [ensureString],
    validate: [minLength(6), maxLength(25)]
  }} />
```

Pull request:
Keep lines inside embedded expression
Possibly break all JS expressions except Svelte Blocks

Fixes #170

### BREAKING CHANGE
If the js content inside a mustache tag is too long, break it up if possible. Only exception are Svelte Blocks (#if/#await/etc) because that looks stupid.

Patch:
diff --git a/src/embed.ts b/src/embed.ts
index 31bb433..995a534 100644
--- a/src/embed.ts
+++ b/src/embed.ts
@@ -32,9 +32,8 @@ export function embed(
                 embeddedOptions.singleQuote = true;
             }
 
-            return removeLines(
-                textToDoc(forceIntoExpression(getText(node, options)), embeddedOptions),
-            );
+            const docs = textToDoc(forceIntoExpression(getText(node, options)), embeddedOptions);
+            return node.forceSingleLine ? removeLines(docs) : docs;
         } catch (e) {
             return getText(node, options);
         }
diff --git a/src/print/index.ts b/src/print/index.ts
index 5d1c54a..22a6e7b 100644
--- a/src/print/index.ts
+++ b/src/print/index.ts
@@ -109,7 +109,7 @@ export function print(path: FastPath, options: ParserOptions, print: PrintFn): D
     const [open, close] = options.svelteStrictMode ? ['"{', '}"'] : ['{', '}'];
     const printJsExpression = () => [
         open,
-        printJS(path, print, options.svelteStrictMode, 'expression'),
+        printJS(path, print, options.svelteStrictMode, false, 'expression'),
         close,
     ];
     const node = n as Node;
@@ -385,13 +385,13 @@ export function print(path: FastPath, options: ParserOptions, print: PrintFn): D
         case 'MustacheTag':
             return concat([
                 '{',
-                printJS(path, print, isInsideQuotedAttribute(path, options), 'expression'),
+                printJS(path, print, isInsideQuotedAttribute(path, options), false, 'expression'),
                 '}',
             ]);
         case 'IfBlock': {
             const def: Doc[] = [
                 '{#if ',
-                printJS(path, print, false, 'expression'),
+                printSvelteBlockJS(path, print, 'expression'),
                 '}',
                 printSvelteBlockChildren(path, print, options),
             ];
@@ -417,7 +417,7 @@ export function print(path: FastPath, options: ParserOptions, print: PrintFn): D
                 const def: Doc[] = [
                     '{:else if ',
                     path.map(
-                        (ifPath) => printJS(ifPath, print, false, 'expression'),
+                        (ifPath) => printSvelteBlockJS(ifPath, print, 'expression'),
                         'children',
                     )[0],
                     '}',
@@ -438,9 +438,9 @@ export function print(path: FastPath, options: ParserOptions, print: PrintFn): D
         case 'EachBlock': {
             const def: Doc[] = [
                 '{#each ',
-                printJS(path, print, false, 'expression'),
+                printSvelteBlockJS(path, print, 'expression'),
                 ' as ',
-                printJS(path, print, false, 'context'),
+                printSvelteBlockJS(path, print, 'context'),
             ];
 
             if (node.index) {
@@ -448,7 +448,7 @@ export function print(path: FastPath, options: ParserOptions, print: PrintFn): D
             }
 
             if (node.key) {
-                def.push(' (', printJS(path, print, false, 'key'), ')');
+                def.push(' (', printSvelteBlockJS(path, print, 'key'), ')');
             }
 
             def.push('}', printSvelteBlockChildren(path, print, options));
@@ -473,7 +473,7 @@ export function print(path: FastPath, options: ParserOptions, print: PrintFn): D
                     group(
                         concat([
                             '{#await ',
-                            printJS(path, print, false, 'expression'),
+                            printSvelteBlockJS(path, print, 'expression'),
                             ' then',
                             expandNode(node.value),
                             '}',
@@ -483,7 +483,7 @@ export function print(path: FastPath, options: ParserOptions, print: PrintFn): D
                 );
             } else {
                 block.push(
-                    group(concat(['{#await ', printJS(path, print, false, 'expression'), '}'])),
+                    group(concat(['{#await ', printSvelteBlockJS(path, print, 'expression'), '}'])),
                 );
 
                 if (hasPendingBlock) {
@@ -512,7 +512,7 @@ export function print(path: FastPath, options: ParserOptions, print: PrintFn): D
         case 'KeyBlock': {
             const def: Doc[] = [
                 '{#key ',
-                printJS(path, print, false, 'expression'),
+                printSvelteBlockJS(path, print, 'expression'),
                 '}',
                 printSvelteBlockChildren(path, print, options),
             ];
@@ -623,9 +623,9 @@ export function print(path: FastPath, options: ParserOptions, print: PrintFn): D
                 node.expression ? concat(['=', ...printJsExpression()]) : '',
             ]);
         case 'RawMustacheTag':
-            return concat(['{@html ', printJS(path, print, false, 'expression'), '}']);
+            return concat(['{@html ', printJS(path, print, false, false, 'expression'), '}']);
         case 'Spread':
-            return concat([line, '{...', printJS(path, print, false, 'expression'), '}']);
+            return concat([line, '{...', printJS(path, print, false, false, 'expression'), '}']);
     }
 
     console.error(JSON.stringify(node, null, 4));
@@ -853,15 +853,20 @@ function splitTextToDocs(node: TextNode): Doc[] {
     return docs;
 }
 
-function printJS(path: FastPath, print: PrintFn, forceSingleQuote: boolean, name?: string) {
-    if (!name) {
-        path.getValue().isJS = true;
-        path.getValue().forceSingleQuote = forceSingleQuote;
-        return path.call(print);
-    }
+function printSvelteBlockJS(path: FastPath, print: PrintFn, name: string) {
+    return printJS(path, print, false, true, name);
+}
 
+function printJS(
+    path: FastPath,
+    print: PrintFn,
+    forceSingleQuote: boolean,
+    forceSingleLine: boolean,
+    name: string,
+) {
     path.getValue()[name].isJS = true;
     path.getValue()[name].forceSingleQuote = forceSingleQuote;
+    path.getValue()[name].forceSingleLine = forceSingleLine;
     return path.call(print, name);
 }
 
diff --git a/src/print/nodes.ts b/src/print/nodes.ts
index d7396ce..11bf7db 100644
--- a/src/print/nodes.ts
+++ b/src/print/nodes.ts
@@ -3,6 +3,7 @@ export interface BaseNode {
     end: number;
     isJS?: boolean;
     forceSingleQuote?: boolean;
+    forceSingleLine?: boolean;
 }
 
 export interface FragmentNode extends BaseNode {


Test patch:
diff --git a/test/formatting/samples/long-attribute-value/input.html b/test/formatting/samples/long-attribute-value/input.html
new file mode 100644
index 0000000..2323030
--- /dev/null
+++ b/test/formatting/samples/long-attribute-value/input.html
@@ -0,0 +1,17 @@
+<input
+  type="password"
+  use:fits={{ store: passwordStore, clean: [ensureString] }}
+  use:breaks={{ store: passwordStore, clean: [ensureString], validate: [minLength(6), maxLength(25)] }}
+  on:fits={() => ''}
+  on:breaks={() => { const a = ''; a.toLowerCase(); }}
+  in:fits={{ store: passwordStore, clean: [ensureString] }}
+  in:breaks={{ store: passwordStore, clean: [ensureString], validate: [minLength(6), maxLength(25)] }}
+  out:fits={{ store: passwordStore, clean: [ensureString] }}
+  out:breaks={{ store: passwordStore, clean: [ensureString], validate: [minLength(6), maxLength(25)] }}
+  transition:fits={{ store: passwordStore, clean: [ensureString] }}
+  transition:breaks={{ store: passwordStore, clean: [ensureString], validate: [minLength(6), maxLength(25)] }}
+  animate:fits={{ store: passwordStore, clean: [ensureString] }}
+  animate:breaks={{ store: passwordStore, clean: [ensureString], validate: [minLength(6), maxLength(25)] }}
+  class:fits={'short check that fits in one line' ? true : false}
+  class:breaks={'some really long condition check happening here' ? true : false}
+   />
diff --git a/test/formatting/samples/long-attribute-value/output.html b/test/formatting/samples/long-attribute-value/output.html
new file mode 100644
index 0000000..769029f
--- /dev/null
+++ b/test/formatting/samples/long-attribute-value/output.html
@@ -0,0 +1,41 @@
+<input
+    type="password"
+    use:fits={{ store: passwordStore, clean: [ensureString] }}
+    use:breaks={{
+        store: passwordStore,
+        clean: [ensureString],
+        validate: [minLength(6), maxLength(25)],
+    }}
+    on:fits={() => ""}
+    on:breaks={() => {
+        const a = "";
+        a.toLowerCase();
+    }}
+    in:fits={{ store: passwordStore, clean: [ensureString] }}
+    in:breaks={{
+        store: passwordStore,
+        clean: [ensureString],
+        validate: [minLength(6), maxLength(25)],
+    }}
+    out:fits={{ store: passwordStore, clean: [ensureString] }}
+    out:breaks={{
+        store: passwordStore,
+        clean: [ensureString],
+        validate: [minLength(6), maxLength(25)],
+    }}
+    transition:fits={{ store: passwordStore, clean: [ensureString] }}
+    transition:breaks={{
+        store: passwordStore,
+        clean: [ensureString],
+        validate: [minLength(6), maxLength(25)],
+    }}
+    animate:fits={{ store: passwordStore, clean: [ensureString] }}
+    animate:breaks={{
+        store: passwordStore,
+        clean: [ensureString],
+        validate: [minLength(6), maxLength(25)],
+    }}
+    class:fits={"short check that fits in one line" ? true : false}
+    class:breaks={"some really long condition check happening here"
+        ? true
+        : false} />
diff --git a/test/formatting/samples/long-mustache-value/input.html b/test/formatting/samples/long-mustache-value/input.html
new file mode 100644
index 0000000..1330013
--- /dev/null
+++ b/test/formatting/samples/long-mustache-value/input.html
@@ -0,0 +1,6 @@
+<input
+  type="password"
+  class="attributes are not broken up by default which is the prettier behavior but {'breakable' + 'mustache tags'} can be broken up"
+   />
+
+<p>Text with spaces inbetween should break at the most fitting point and not wait for a {'breakable' + 'mustache tags'} to be broken up. {'If the mustache tag itself is very long, however' + 'it should be broken uuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuup'} {andIiiiiiiiiiiiiiiiiiiiiiiiiiiWillNeverBreaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaakaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa}</p>
diff --git a/test/formatting/samples/long-mustache-value/output.html b/test/formatting/samples/long-mustache-value/output.html
new file mode 100644
index 0000000..89f7552
--- /dev/null
+++ b/test/formatting/samples/long-mustache-value/output.html
@@ -0,0 +1,11 @@
+<input
+    type="password"
+    class="attributes are not broken up by default which is the prettier behavior but {'breakable' +
+        'mustache tags'} can be broken up" />
+
+<p>
+    Text with spaces inbetween should break at the most fitting point and not
+    wait for a {"breakable" + "mustache tags"} to be broken up. {"If the mustache tag itself is very long, however" +
+        "it should be broken uuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuup"}
+    {andIiiiiiiiiiiiiiiiiiiiiiiiiiiWillNeverBreaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaakaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa}
+</p>
diff --git a/test/printer/samples/long-control-blocks-dont-break.html b/test/printer/samples/long-control-blocks-dont-break.html
new file mode 100644
index 0000000..934cff1
--- /dev/null
+++ b/test/printer/samples/long-control-blocks-dont-break.html
@@ -0,0 +1,17 @@
+{#if andIiiiiiiiiiiiiiiiiiiiiiiiiiiWillNeverBreaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaak === true && cool}
+    a
+{:else if andIiiiiiiiiiiiiiiiiiiiiiiiiiiWillNeverBreaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaak === true && cool}
+    b
+{/if}
+
+{#each [1, andIiiiiiiiiiiiiiiiiiiiiiiiiiiWillNeverBreaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaak] as { cool, veryCool }}
+    a
+{/each}
+
+{#key andIiiiiiiiiiiiiiiiiiiiiiiiiiiWillNeverBreaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaak.no.no.no}
+    a
+{/key}
+
+{#await Promise.resolve(andIiiiiiiiiiiiiiiiiiiiiiiiiiiWillNeverBreaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaak) then cool}
+    a
+{/await}