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

Issue:
Quotes inside if blocks
Inside if blocks, double quotes are replaced by single quotes when running prettier with this plugin. For example,

    {#if foo === "bar"}
      <div>baz</div>
    {/if}

is changed to

    {#if foo === 'bar'}
      <div>baz</div>
    {/if}

Most (all?) other places double quotes are enforced. Is this intentional, or can quotes inside if blocks also be enforced as double?

Pull request description:
Don't force single quotes
Fixes #94, closes #101

### BREAKING CHANGE
Single quotes are no longer forced inside markup except inside quoted html attributes.

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 33b45bd..e9c8b71 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,6 +6,7 @@
 * Default sort order is now `scripts-markup-styles`. This is a breaking change.
 * Fix formatting of fenced Svelte code blocks inside markdown ([#129](https://github.com/sveltejs/prettier-plugin-svelte/issues/129))
 * Everything that is not explicitely a block element is now treated as an inline element, including components. This is a breaking change ([#159](https://github.com/sveltejs/prettier-plugin-svelte/issues/159))
+* Single quotes are no longer forced except inside quoted attributes/events/etc. This is a breaking change ([#94](https://github.com/sveltejs/prettier-plugin-svelte/issues/94))
 
 ## 1.4.2
 
diff --git a/src/embed.ts b/src/embed.ts
index cb2c3d1..31bb433 100644
--- a/src/embed.ts
+++ b/src/embed.ts
@@ -25,11 +25,15 @@ export function embed(
 
     if (node.isJS) {
         try {
+            const embeddedOptions: any = {
+                parser: expressionParser,
+            };
+            if (node.forceSingleQuote) {
+                embeddedOptions.singleQuote = true;
+            }
+
             return removeLines(
-                textToDoc(forceIntoExpression(getText(node, options)), {
-                    parser: expressionParser,
-                    singleQuote: true,
-                }),
+                textToDoc(forceIntoExpression(getText(node, options)), embeddedOptions),
             );
         } catch (e) {
             return getText(node, options);
diff --git a/src/print/index.ts b/src/print/index.ts
index fbbc872..5d1c54a 100644
--- a/src/print/index.ts
+++ b/src/print/index.ts
@@ -16,6 +16,7 @@ import {
     isEmptyNode,
     isIgnoreDirective,
     isInlineElement,
+    isInsideQuotedAttribute,
     isLoneMustacheTag,
     isNodeSupportedLanguage,
     isOrCanBeConvertedToShorthand,
@@ -106,6 +107,11 @@ export function print(path: FastPath, options: ParserOptions, print: PrintFn): D
     }
 
     const [open, close] = options.svelteStrictMode ? ['"{', '}"'] : ['{', '}'];
+    const printJsExpression = () => [
+        open,
+        printJS(path, print, options.svelteStrictMode, 'expression'),
+        close,
+    ];
     const node = n as Node;
 
     if (ignoreNext && (node.type !== 'Text' || !isEmptyNode(node))) {
@@ -196,7 +202,7 @@ export function print(path: FastPath, options: ParserOptions, print: PrintFn): D
             const attributes = path.map((childPath) => childPath.call(print), 'attributes');
             const possibleThisBinding =
                 node.type === 'InlineComponent' && node.expression
-                    ? concat([line, 'this=', open, printJS(path, print, 'expression'), close])
+                    ? concat([line, 'this=', ...printJsExpression()])
                     : '';
 
             if (isSelfClosingTag) {
@@ -377,11 +383,15 @@ export function print(path: FastPath, options: ParserOptions, print: PrintFn): D
             }
         }
         case 'MustacheTag':
-            return concat(['{', printJS(path, print, 'expression'), '}']);
+            return concat([
+                '{',
+                printJS(path, print, isInsideQuotedAttribute(path, options), 'expression'),
+                '}',
+            ]);
         case 'IfBlock': {
             const def: Doc[] = [
                 '{#if ',
-                printJS(path, print, 'expression'),
+                printJS(path, print, false, 'expression'),
                 '}',
                 printSvelteBlockChildren(path, print, options),
             ];
@@ -406,7 +416,10 @@ export function print(path: FastPath, options: ParserOptions, print: PrintFn): D
                 const ifNode = node.children[0] as IfBlockNode;
                 const def: Doc[] = [
                     '{:else if ',
-                    path.map((ifPath) => printJS(ifPath, print, 'expression'), 'children')[0],
+                    path.map(
+                        (ifPath) => printJS(ifPath, print, false, 'expression'),
+                        'children',
+                    )[0],
                     '}',
                     path.map(
                         (ifPath) => printSvelteBlockChildren(ifPath, print, options),
@@ -425,9 +438,9 @@ export function print(path: FastPath, options: ParserOptions, print: PrintFn): D
         case 'EachBlock': {
             const def: Doc[] = [
                 '{#each ',
-                printJS(path, print, 'expression'),
+                printJS(path, print, false, 'expression'),
                 ' as ',
-                printJS(path, print, 'context'),
+                printJS(path, print, false, 'context'),
             ];
 
             if (node.index) {
@@ -435,7 +448,7 @@ export function print(path: FastPath, options: ParserOptions, print: PrintFn): D
             }
 
             if (node.key) {
-                def.push(' (', printJS(path, print, 'key'), ')');
+                def.push(' (', printJS(path, print, false, 'key'), ')');
             }
 
             def.push('}', printSvelteBlockChildren(path, print, options));
@@ -460,7 +473,7 @@ export function print(path: FastPath, options: ParserOptions, print: PrintFn): D
                     group(
                         concat([
                             '{#await ',
-                            printJS(path, print, 'expression'),
+                            printJS(path, print, false, 'expression'),
                             ' then',
                             expandNode(node.value),
                             '}',
@@ -469,7 +482,9 @@ export function print(path: FastPath, options: ParserOptions, print: PrintFn): D
                     path.call(print, 'then'),
                 );
             } else {
-                block.push(group(concat(['{#await ', printJS(path, print, 'expression'), '}'])));
+                block.push(
+                    group(concat(['{#await ', printJS(path, print, false, 'expression'), '}'])),
+                );
 
                 if (hasPendingBlock) {
                     block.push(path.call(print, 'pending'));
@@ -497,7 +512,7 @@ export function print(path: FastPath, options: ParserOptions, print: PrintFn): D
         case 'KeyBlock': {
             const def: Doc[] = [
                 '{#key ',
-                printJS(path, print, 'expression'),
+                printJS(path, print, false, 'expression'),
                 '}',
                 printSvelteBlockChildren(path, print, options),
             ];
@@ -518,9 +533,7 @@ export function print(path: FastPath, options: ParserOptions, print: PrintFn): D
                 node.modifiers && node.modifiers.length
                     ? concat(['|', join('|', node.modifiers)])
                     : '',
-                node.expression
-                    ? concat(['=', open, printJS(path, print, 'expression'), close])
-                    : '',
+                node.expression ? concat(['=', ...printJsExpression()]) : '',
             ]);
         case 'Binding':
             return concat([
@@ -529,7 +542,7 @@ export function print(path: FastPath, options: ParserOptions, print: PrintFn): D
                 node.name,
                 node.expression.type === 'Identifier' && node.expression.name === node.name
                     ? ''
-                    : concat(['=', open, printJS(path, print, 'expression'), close]),
+                    : concat(['=', ...printJsExpression()]),
             ]);
         case 'Class':
             return concat([
@@ -538,7 +551,7 @@ export function print(path: FastPath, options: ParserOptions, print: PrintFn): D
                 node.name,
                 node.expression.type === 'Identifier' && node.expression.name === node.name
                     ? ''
-                    : concat(['=', open, printJS(path, print, 'expression'), close]),
+                    : concat(['=', ...printJsExpression()]),
             ]);
         case 'Let':
             return concat([
@@ -549,7 +562,7 @@ export function print(path: FastPath, options: ParserOptions, print: PrintFn): D
                 !node.expression ||
                 (node.expression.type === 'Identifier' && node.expression.name === node.name)
                     ? ''
-                    : concat(['=', open, printJS(path, print, 'expression'), close]),
+                    : concat(['=', ...printJsExpression()]),
             ]);
         case 'DebugTag':
             return concat([
@@ -593,32 +606,26 @@ export function print(path: FastPath, options: ParserOptions, print: PrintFn): D
                 node.modifiers && node.modifiers.length
                     ? concat(['|', join('|', node.modifiers)])
                     : '',
-                node.expression
-                    ? concat(['=', open, printJS(path, print, 'expression'), close])
-                    : '',
+                node.expression ? concat(['=', ...printJsExpression()]) : '',
             ]);
         case 'Action':
             return concat([
                 line,
                 'use:',
                 node.name,
-                node.expression
-                    ? concat(['=', open, printJS(path, print, 'expression'), close])
-                    : '',
+                node.expression ? concat(['=', ...printJsExpression()]) : '',
             ]);
         case 'Animation':
             return concat([
                 line,
                 'animate:',
                 node.name,
-                node.expression
-                    ? concat(['=', open, printJS(path, print, 'expression'), close])
-                    : '',
+                node.expression ? concat(['=', ...printJsExpression()]) : '',
             ]);
         case 'RawMustacheTag':
-            return concat(['{@html ', printJS(path, print, 'expression'), '}']);
+            return concat(['{@html ', printJS(path, print, false, 'expression'), '}']);
         case 'Spread':
-            return concat([line, '{...', printJS(path, print, 'expression'), '}']);
+            return concat([line, '{...', printJS(path, print, false, 'expression'), '}']);
     }
 
     console.error(JSON.stringify(node, null, 4));
@@ -846,13 +853,15 @@ function splitTextToDocs(node: TextNode): Doc[] {
     return docs;
 }
 
-function printJS(path: FastPath, print: PrintFn, name?: string) {
+function printJS(path: FastPath, print: PrintFn, forceSingleQuote: boolean, name?: string) {
     if (!name) {
         path.getValue().isJS = true;
+        path.getValue().forceSingleQuote = forceSingleQuote;
         return path.call(print);
     }
 
     path.getValue()[name].isJS = true;
+    path.getValue()[name].forceSingleQuote = forceSingleQuote;
     return path.call(print, name);
 }
 
diff --git a/src/print/node-helpers.ts b/src/print/node-helpers.ts
index 5551d06..9f10b2b 100644
--- a/src/print/node-helpers.ts
+++ b/src/print/node-helpers.ts
@@ -382,3 +382,13 @@ export function checkWhitespaceAtEndOfSvelteBlock(
 
     return 'none';
 }
+
+export function isInsideQuotedAttribute(path: FastPath, options: ParserOptions): boolean {
+    const stack = path.stack as Node[];
+
+    return stack.some(
+        (node) =>
+            node.type === 'Attribute' &&
+            (!isLoneMustacheTag(node.value) || options.svelteStrictMode),
+    );
+}
diff --git a/src/print/nodes.ts b/src/print/nodes.ts
index 38861ef..d7396ce 100644
--- a/src/print/nodes.ts
+++ b/src/print/nodes.ts
@@ -2,6 +2,7 @@ export interface BaseNode {
     start: number;
     end: number;
     isJS?: boolean;
+    forceSingleQuote?: boolean;
 }
 
 export interface FragmentNode extends BaseNode {


Test patch:
diff --git a/test/printer/samples/attribute-quoted.html b/test/printer/samples/attribute-quoted.html
index 9a2abea..224849f 100644
--- a/test/printer/samples/attribute-quoted.html
+++ b/test/printer/samples/attribute-quoted.html
@@ -4,6 +4,18 @@
 
 <div class:awesome="{isOk}"></div>
 
+<div class="asd {otherclass + 'b'}"></div>
+
 <input bind:value="{fooValue}" on:change="{onChange}" />
 
-<a use:link="{linkValues}">Link</a>
+<a use:link="{linkValues + 'asd'}">Link</a>
+
+<a href="{linkValues + 'asd'}">Link</a>
+
+<button on:click="{() => console.log('hi')}">hi</button>
+
+<div in:asd="{foo + 'bar'}" out:asd="{foo + 'bar'}"></div>
+
+<div transistion:asd="{foo + 'bar'}"></div>
+
+<div animate:asd="{foo + 'bar'}"></div>
diff --git a/test/printer/samples/each-block-else-with-nested-if-block-else.html b/test/printer/samples/each-block-else-with-nested-if-block-else.html
index a6cee17..1164a0f 100644
--- a/test/printer/samples/each-block-else-with-nested-if-block-else.html
+++ b/test/printer/samples/each-block-else-with-nested-if-block-else.html
@@ -1,7 +1,7 @@
 {#each animals as animal}
     <p>{animal}</p>
 {:else}
-    {#if type === 'dog'}
+    {#if type === "dog"}
         <p>no dogs</p>
     {:else}
         <p>no animals</p>
diff --git a/test/printer/samples/each-block-else-with-nested-if-block-elseif-else.html b/test/printer/samples/each-block-else-with-nested-if-block-elseif-else.html
index 73f2fd0..3494988 100644
--- a/test/printer/samples/each-block-else-with-nested-if-block-elseif-else.html
+++ b/test/printer/samples/each-block-else-with-nested-if-block-elseif-else.html
@@ -1,9 +1,9 @@
 {#each animals as animal}
     <p>{animal}</p>
 {:else}
-    {#if type === 'dog'}
+    {#if type === "dog"}
         <p>no dogs</p>
-    {:else if type === 'cat'}
+    {:else if type === "cat"}
         <p>no cats</p>
     {:else}
         <p>no animals</p>
diff --git a/test/printer/samples/each-block-else-with-nested-if-block-elseif.html b/test/printer/samples/each-block-else-with-nested-if-block-elseif.html
index 3fac47d..71eef35 100644
--- a/test/printer/samples/each-block-else-with-nested-if-block-elseif.html
+++ b/test/printer/samples/each-block-else-with-nested-if-block-elseif.html
@@ -1,9 +1,9 @@
 {#each animals as animal}
     <p>{animal}</p>
 {:else}
-    {#if type === 'dog'}
+    {#if type === "dog"}
         <p>no dogs</p>
-    {:else if type === 'cat'}
+    {:else if type === "cat"}
         <p>no cats</p>
     {/if}
 {/each}
diff --git a/test/printer/samples/each-block-else-with-nested-if-block.html b/test/printer/samples/each-block-else-with-nested-if-block.html
index 8c0f7aa..f97b8aa 100644
--- a/test/printer/samples/each-block-else-with-nested-if-block.html
+++ b/test/printer/samples/each-block-else-with-nested-if-block.html
@@ -1,7 +1,7 @@
 {#each animals as animal}
     <p>{animal}</p>
 {:else}
-    {#if type === 'dog'}
+    {#if type === "dog"}
         <p>no dogs</p>
     {/if}
 {/each}
diff --git a/test/printer/samples/each-block-else-with-nested-if-inline-else.html b/test/printer/samples/each-block-else-with-nested-if-inline-else.html
index c6465f1..fe1f8c0 100644
--- a/test/printer/samples/each-block-else-with-nested-if-inline-else.html
+++ b/test/printer/samples/each-block-else-with-nested-if-inline-else.html
@@ -1,5 +1,5 @@
 {#each animals as animal}
     <p>{animal}</p>
 {:else}
-    {#if type === 'dog'}no dogs{:else}no animals{/if}
+    {#if type === "dog"}no dogs{:else}no animals{/if}
 {/each}
diff --git a/test/printer/samples/each-block-else-with-nested-if-inline-elseif-else.html b/test/printer/samples/each-block-else-with-nested-if-inline-elseif-else.html
index 616f63c..2a41e79 100644
--- a/test/printer/samples/each-block-else-with-nested-if-inline-elseif-else.html
+++ b/test/printer/samples/each-block-else-with-nested-if-inline-elseif-else.html
@@ -1,7 +1,7 @@
 {#each animals as animal}
     <p>{animal}</p>
 {:else}
-    {#if type === 'dog'}
+    {#if type === "dog"}
         no dogs
-    {:else if type === 'cat'}no cats{:else}no animals{/if}
+    {:else if type === "cat"}no cats{:else}no animals{/if}
 {/each}
diff --git a/test/printer/samples/each-block-else-with-nested-if-inline-elseif.html b/test/printer/samples/each-block-else-with-nested-if-inline-elseif.html
index 967189c..683e864 100644
--- a/test/printer/samples/each-block-else-with-nested-if-inline-elseif.html
+++ b/test/printer/samples/each-block-else-with-nested-if-inline-elseif.html
@@ -1,5 +1,5 @@
 {#each animals as animal}
     <p>{animal}</p>
 {:else}
-    {#if type === 'dog'}no dogs{:else if type === 'cat'}no cats{/if}
+    {#if type === "dog"}no dogs{:else if type === "cat"}no cats{/if}
 {/each}
diff --git a/test/printer/samples/each-block-else-with-nested-if-inline.html b/test/printer/samples/each-block-else-with-nested-if-inline.html
index ea30eb2..fc86c50 100644
--- a/test/printer/samples/each-block-else-with-nested-if-inline.html
+++ b/test/printer/samples/each-block-else-with-nested-if-inline.html
@@ -1,5 +1,5 @@
 {#each animals as animal}
     <p>{animal}</p>
 {:else}
-    {#if type === 'dog'}no dogs{/if}
+    {#if type === "dog"}no dogs{/if}
 {/each}
diff --git a/test/printer/samples/single-quote.html b/test/printer/samples/single-quote.html
new file mode 100644
index 0000000..6610919
--- /dev/null
+++ b/test/printer/samples/single-quote.html
@@ -0,0 +1,17 @@
+<div class={'asd' + boo}>asd</div>
+
+{#if 'foo'}a{:else if 'bar'}b{/if}
+
+{#await Promise.resolve('hi')}a{/await}
+
+{#each ['a'] as b}a{/each}
+
+<p>"{'hello'}" world</p>
+
+<button on:click={() => console.log('hi')}>hi</button>
+
+<div in:asd={foo + 'bar'} out:asd={foo + 'bar'} />
+
+<div transistion:asd={foo + 'bar'} />
+
+<div animate:asd={foo + 'bar'} />
diff --git a/test/printer/samples/single-quote.options.json b/test/printer/samples/single-quote.options.json
new file mode 100644
index 0000000..8db60ca
--- /dev/null
+++ b/test/printer/samples/single-quote.options.json
@@ -0,0 +1,3 @@
+{
+    "singleQuote": true
+}
diff --git a/test/printer/samples/unicode-mustache.html b/test/printer/samples/unicode-mustache.html
index 7eed02b..a3ef93e 100644
--- a/test/printer/samples/unicode-mustache.html
+++ b/test/printer/samples/unicode-mustache.html
@@ -1,1 +1,1 @@
-<div>{'Lønn'}</div>
+<div>{"Lønn"}</div>