Repository: prettier/prettier (JavaScript)
Date: 2019-12-30 13:47:44+00:00
Task type: Bug Fix

Issue:
Extra spaces in html tagged templates
**Prettier 1.19.1**
[Playground link](https://prettier.io/playground/#N4Igxg9gdgLgprEAuc0DOMAEAbAhgIzm0wF5MALGAW2wAMAeNAB1ygD4ASYeADxgF96Aemas2tANwAdKCAA0ICExgBLdMlC4ATlogB3AAraEaZCFwA3CCoAm8kPi24wAazgwAyizAqoAc2QYLQBXOAVKGgB1chV4UTA4DxNYlQtYgE8zMDRTBV80OC0YAyc-KlxkADNcbAKFACs0HgAhJ1d3D1wqOAAZXzgqmrqQRp4PXz9sOABFYIh4QdqwkBYtAq0zfAIieyYtXxhI2xhyZAAOAAYFPYgCyKcmMz24dYsBhQBHOfgSpVMUXBoAC0UDgcBs4PsWjgXxU0JKuDKFSQ1SWCgKVBUgRCyzQEyms3mAxRQ2WMAIRxsJ2QACYFEFcCpsBMAMIQKjlMxQaDvEDBAoAFQI-1RwwsoQAklAIbAPGB9soAILSjwwdJTRYFfj8IA)
```sh
--parser babel
```

**Input:**
```jsx
const label = html`<span>${text}</span>`;

```

**Output:**
```jsx
const label = html`
  <span>${text}</span>
`;

```

**Expected behavior:**

Adding whitespace inside the tagged template literal affects the output. To avoid the spaces around my components, the only way right now is to add ignore comments.


Issue discussion:
/cc @saschanaz please don't repeat issues, put examples here
@evilebottnawi For me #7101 wasn't exactly this issue though, because hyperHTML automatically trim out the newlines *if single-rooted* and thus won't make a breaking change.

Edit: But anyway it looks similar, and 100% sorry for #7100 which is exactly same as this one 😞
What should be done here? Any formatting outside a tag is potentially breaking. I think we should introduce one of these option to:

* turn off the whole HTML formatting
* turn off formatting outside of any tags
* or, not insert newlines before and after the root elements.
@saschanaz I'd vote to keep it simple: if the user tries to have it as one-liner and the line is not too long, avoid adding the line breaks. Otherwise leave the default formatting behavior.

This will create an incentive to the user to simplify their content, possibly extracting it into simple variables that would be easier to read.

It then will behave in a similar way as this example:

```js
// the line below is left as-is
const obj = [{ test: true }];

// the code below will be reformatted with line breaks
// input:
const obj = [{ 
  test: true }];
// output:
const obj = [
  {
    test: true
  }
];
```

What do you think?
That doesn't even need an option then! Sounds good to me 👍
@evilebottnawi Do prettier maintainers think https://github.com/prettier/prettier/issues/7096#issuecomment-564444006 is good? Is it possible to implement that?
I would really like to see this as well.
@saschanaz it is out of scope this bug, we should not add extra space in this case, so PR welcome should be not hard

Solution patch:
diff --git a/changelog_unreleased/javascript/pr-7208.md b/changelog_unreleased/javascript/pr-7208.md
new file mode 100644
index 000000000000..722d231c3af7
--- /dev/null
+++ b/changelog_unreleased/javascript/pr-7208.md
@@ -0,0 +1,24 @@
+#### Do not add hard linebreak for html template ([#7208](https://github.com/prettier/prettier/pull/7208) by [@saschanaz](https://github.com/saschanaz))
+
+Prettier had been adding newlines for every HTML template string, which can cause
+unexpected whitespaces in rendered HTML. #7208 prevents this unless
+`htmlWhitespaceSensitivity: ignore` option is given.
+
+<!-- prettier-ignore -->
+```jsx
+// Input
+html`<div>`;
+html`  <span>TEXT</span>  `;
+
+// Prettier stable
+html`
+  <div></div>
+`;
+html`
+  <span>TEXT</span>
+`;
+
+// Prettier master
+html`<div></div>`;
+html` <span>TEXT</span> `;
+```
diff --git a/src/language-html/printer-html.js b/src/language-html/printer-html.js
index a7f523bdb8f7..d8d4ece435aa 100644
--- a/src/language-html/printer-html.js
+++ b/src/language-html/printer-html.js
@@ -172,6 +172,9 @@ function genericPrint(path, options, print) {
   const node = path.getValue();
   switch (node.type) {
     case "root":
+      if (options.__onHtmlRoot) {
+        options.__onHtmlRoot(node);
+      }
       // use original concat to not break stripTrailingHardline
       return builders.concat([
         group(printChildren(path, options, print)),
diff --git a/src/language-js/embed.js b/src/language-js/embed.js
index 2860060754d8..263e134d82a4 100644
--- a/src/language-js/embed.js
+++ b/src/language-js/embed.js
@@ -6,6 +6,7 @@ const {
   builders: {
     indent,
     join,
+    line,
     hardline,
     softline,
     literalline,
@@ -147,7 +148,7 @@ function embed(path, print, textToDoc, options) {
           print,
           textToDoc,
           htmlParser,
-          options.embeddedInHtml
+          options
         );
       }
 
@@ -549,13 +550,7 @@ function isHtml(path) {
 // The counter is needed to distinguish nested embeds.
 let htmlTemplateLiteralCounter = 0;
 
-function printHtmlTemplateLiteral(
-  path,
-  print,
-  textToDoc,
-  parser,
-  escapeClosingScriptTag
-) {
+function printHtmlTemplateLiteral(path, print, textToDoc, parser, options) {
   const node = path.getValue();
 
   const counter = htmlTemplateLiteralCounter;
@@ -579,9 +574,17 @@ function printHtmlTemplateLiteral(
   }
 
   const placeholderRegex = new RegExp(composePlaceholder("(\\d+)"), "g");
+  let topLevelCount = 0;
 
   const contentDoc = mapDoc(
-    stripTrailingHardline(textToDoc(text, { parser })),
+    stripTrailingHardline(
+      textToDoc(text, {
+        parser,
+        __onHtmlRoot(root) {
+          topLevelCount = root.children.length;
+        }
+      })
+    ),
     doc => {
       if (typeof doc !== "string") {
         return doc;
@@ -596,7 +599,7 @@ function printHtmlTemplateLiteral(
         if (i % 2 === 0) {
           if (component) {
             component = uncook(component);
-            if (escapeClosingScriptTag) {
+            if (options.embeddedInHtml) {
               component = component.replace(/<\/(script)\b/gi, "<\\/$1");
             }
             parts.push(component);
@@ -614,8 +617,35 @@ function printHtmlTemplateLiteral(
     }
   );
 
+  const leadingWhitespace = /^\s/.test(text) ? " " : "";
+  const trailingWhitespace = /\s$/.test(text) ? " " : "";
+
+  const linebreak =
+    options.htmlWhitespaceSensitivity === "ignore"
+      ? hardline
+      : leadingWhitespace && trailingWhitespace
+      ? line
+      : null;
+
+  if (linebreak) {
+    return group(
+      concat([
+        "`",
+        indent(concat([linebreak, group(contentDoc)])),
+        linebreak,
+        "`"
+      ])
+    );
+  }
+
   return group(
-    concat(["`", indent(concat([hardline, group(contentDoc)])), softline, "`"])
+    concat([
+      "`",
+      leadingWhitespace,
+      topLevelCount > 1 ? indent(group(contentDoc)) : group(contentDoc),
+      trailingWhitespace,
+      "`"
+    ])
   );
 }
 


Test patch:
diff --git a/tests/angular_component_examples/__snapshots__/jsfmt.spec.js.snap b/tests/angular_component_examples/__snapshots__/jsfmt.spec.js.snap
index 21ae6bd3d3b7..d72d36c36313 100644
--- a/tests/angular_component_examples/__snapshots__/jsfmt.spec.js.snap
+++ b/tests/angular_component_examples/__snapshots__/jsfmt.spec.js.snap
@@ -28,11 +28,9 @@ class     TestComponent {}
 =====================================output=====================================
 @Component({
   selector: "app-test",
-  template: \`
-    <ul>
-      <li>test</li>
-    </ul>
-  \`,
+  template: \`<ul>
+    <li>test</li>
+  </ul> \`,
   styles: [
     \`
       :host {
@@ -76,11 +74,9 @@ class     TestComponent {}
 =====================================output=====================================
 @Component({
   selector: "app-test",
-  template: \`
-    <ul>
-      <li>test</li>
-    </ul>
-  \`,
+  template: \`<ul>
+    <li>test</li>
+  </ul> \`,
   styles: [
     \`
       :host {
diff --git a/tests/decorators-ts/__snapshots__/jsfmt.spec.js.snap b/tests/decorators-ts/__snapshots__/jsfmt.spec.js.snap
index 87971cfab700..189725a3bb76 100644
--- a/tests/decorators-ts/__snapshots__/jsfmt.spec.js.snap
+++ b/tests/decorators-ts/__snapshots__/jsfmt.spec.js.snap
@@ -66,9 +66,7 @@ export class HeroButtonComponent {
 =====================================output=====================================
 @Component({
   selector: "toh-hero-button",
-  template: \`
-    <button>{{ label }}</button>
-  \`,
+  template: \`<button>{{ label }}</button>\`,
 })
 export class HeroButtonComponent {
   @Output() change = new EventEmitter<any>();
diff --git a/tests/multiparser_html_js/__snapshots__/jsfmt.spec.js.snap b/tests/multiparser_html_js/__snapshots__/jsfmt.spec.js.snap
index 30c347dfca9a..2b29d9263e8e 100644
--- a/tests/multiparser_html_js/__snapshots__/jsfmt.spec.js.snap
+++ b/tests/multiparser_html_js/__snapshots__/jsfmt.spec.js.snap
@@ -60,11 +60,9 @@ printWidth: 80
         <!-- foo1 -->
         <script>
           document.write(
-            /* HTML */ \\\\\\\`
-              <!-- bar1 -->
+            /* HTML */ \\\\\\\`<!-- bar1 -->
               bar
-              <!-- bar2 -->
-            \\\\\\\`
+              <!-- bar2 -->\\\\\\\`
           );
         <\\\\/script>
         <!-- foo2 -->
diff --git a/tests/multiparser_js_html/__snapshots__/jsfmt.spec.js.snap b/tests/multiparser_js_html/__snapshots__/jsfmt.spec.js.snap
index d22ee17ee45a..8d469cc939bd 100644
--- a/tests/multiparser_js_html/__snapshots__/jsfmt.spec.js.snap
+++ b/tests/multiparser_js_html/__snapshots__/jsfmt.spec.js.snap
@@ -1,5 +1,172 @@
 // Jest Snapshot v1, https://goo.gl/fbAQLP
 
+exports[`html-template-literals.js 1`] = `
+====================================options=====================================
+parsers: ["babel", "flow", "typescript"]
+printWidth: 80
+                                                                                | printWidth
+=====================================input======================================
+const nestedFun = /* HTML */ \`\${outerExpr(1)}
+  <script>
+    const tpl = html\\\`<div>\\\${innerExpr(1)} \${outerExpr(2)}</div>\\\`;
+  </script>\`;
+
+const nestedFun2 = /* HTML */ \`\${outerExpr(1)}
+  <script>
+    const tpl = html\\\`\\\\n<div>\\\${innerExpr(1)} \${outerExpr(2)}</div>\\\\n\\\`;
+  </script>\`;
+
+setFoo(
+  html\`<div>one</div>
+    <div>two</div>
+    <div>three</div>\`,
+  secondArgument
+);
+
+setFoo(
+  html\`<div>
+      <div>nested</div>
+    </div>
+    <div>two</div>
+    <div>three</div>\`,
+  secondArgument
+);
+
+setFoo(
+  html\`<div>
+    <div>nested</div>
+  </div>\`,
+  secondArgument
+);
+
+=====================================output=====================================
+const nestedFun = /* HTML */ \`\${outerExpr(1)}
+  <script>
+    const tpl = html\\\`<div>\\\${innerExpr(1)} \${outerExpr(2)}</div>\\\`;
+  </script>\`;
+
+const nestedFun2 = /* HTML */ \`\${outerExpr(1)}
+  <script>
+    const tpl = html\\\` <div>\\\${innerExpr(1)} \${outerExpr(2)}</div> \\\`;
+  </script>\`;
+
+setFoo(
+  html\`<div>one</div>
+    <div>two</div>
+    <div>three</div>\`,
+  secondArgument
+);
+
+setFoo(
+  html\`<div>
+      <div>nested</div>
+    </div>
+    <div>two</div>
+    <div>three</div>\`,
+  secondArgument
+);
+
+setFoo(
+  html\`<div>
+    <div>nested</div>
+  </div>\`,
+  secondArgument
+);
+
+================================================================================
+`;
+
+exports[`html-template-literals.js 2`] = `
+====================================options=====================================
+htmlWhitespaceSensitivity: "ignore"
+parsers: ["babel", "flow", "typescript"]
+printWidth: 80
+                                                                                | printWidth
+=====================================input======================================
+const nestedFun = /* HTML */ \`\${outerExpr(1)}
+  <script>
+    const tpl = html\\\`<div>\\\${innerExpr(1)} \${outerExpr(2)}</div>\\\`;
+  </script>\`;
+
+const nestedFun2 = /* HTML */ \`\${outerExpr(1)}
+  <script>
+    const tpl = html\\\`\\\\n<div>\\\${innerExpr(1)} \${outerExpr(2)}</div>\\\\n\\\`;
+  </script>\`;
+
+setFoo(
+  html\`<div>one</div>
+    <div>two</div>
+    <div>three</div>\`,
+  secondArgument
+);
+
+setFoo(
+  html\`<div>
+      <div>nested</div>
+    </div>
+    <div>two</div>
+    <div>three</div>\`,
+  secondArgument
+);
+
+setFoo(
+  html\`<div>
+    <div>nested</div>
+  </div>\`,
+  secondArgument
+);
+
+=====================================output=====================================
+const nestedFun = /* HTML */ \`
+  \${outerExpr(1)}
+  <script>
+    const tpl = html\\\`
+      <div>\\\${innerExpr(1)} \${outerExpr(2)}</div>
+    \\\`;
+  </script>
+\`;
+
+const nestedFun2 = /* HTML */ \`
+  \${outerExpr(1)}
+  <script>
+    const tpl = html\\\`
+      <div>\\\${innerExpr(1)} \${outerExpr(2)}</div>
+    \\\`;
+  </script>
+\`;
+
+setFoo(
+  html\`
+    <div>one</div>
+    <div>two</div>
+    <div>three</div>
+  \`,
+  secondArgument
+);
+
+setFoo(
+  html\`
+    <div>
+      <div>nested</div>
+    </div>
+    <div>two</div>
+    <div>three</div>
+  \`,
+  secondArgument
+);
+
+setFoo(
+  html\`
+    <div>
+      <div>nested</div>
+    </div>
+  \`,
+  secondArgument
+);
+
+================================================================================
+`;
+
 exports[`lit-html.js 1`] = `
 ====================================options=====================================
 parsers: ["babel", "flow", "typescript"]
@@ -59,6 +226,12 @@ html\`  <\${Footer}  >footer      content<//     >  \`
 
 html\`  <div />  \`
 
+html\`
+  <div />
+\`
+
+html\`<span>one</span><span>two</span><span>three</span>\`;
+
 function HelloWorld() {
   return html\`
     <h3>Bar List</h3>
@@ -109,6 +282,167 @@ class MyElement extends LitElement {
 
 customElements.define("my-element", MyElement);
 
+const someHtml1 = html\`<div>hello \${world}</div>\`;
+const someHtml2 = /* HTML */ \`<div>hello \${world}</div>\`;
+
+html\`\`;
+
+html\`<my-element obj=\${obj}></my-element>\`;
+
+html\` <\${Footer}>footer content<//> \`;
+
+html\` <div /> \`;
+
+html\` <div /> \`;
+
+html\`<span>one</span><span>two</span><span>three</span>\`;
+
+function HelloWorld() {
+  return html\`
+    <h3>Bar List</h3>
+    \${bars.map((bar) => html\` <p>\${bar}</p> \`)}
+  \`;
+}
+
+const trickyParens = html\`<script>
+  f((\${expr}) / 2);
+</script>\`;
+const nestedFun = /* HTML */ \`\${outerExpr(1)}
+  <script>
+    const tpl = html\\\`<div>\\\${innerExpr(1)} \${outerExpr(2)}</div>\\\`;
+  </script>\`;
+
+const closingScriptTagShouldBeEscapedProperly = /* HTML */ \`
+  <script>
+    const html = /* HTML */ \\\`<script><\\\\/script>\\\`;
+  </script>
+\`;
+
+const closingScriptTag2 = /* HTML */ \`<script>
+  const scriptTag = "<\\\\/script>";
+</script>\`;
+
+================================================================================
+`;
+
+exports[`lit-html.js 2`] = `
+====================================options=====================================
+htmlWhitespaceSensitivity: "ignore"
+parsers: ["babel", "flow", "typescript"]
+printWidth: 80
+                                                                                | printWidth
+=====================================input======================================
+import { LitElement, html } from '@polymer/lit-element';
+
+class MyElement extends LitElement {
+  static get properties() {
+    return {
+      mood: { type: String }
+    };
+  }
+
+  constructor() {
+    super();
+    this.mood = 'happy';
+  }
+
+  render() {
+    return html\`
+      <style
+      
+      
+      >
+                  .mood { color: green; }
+      </style
+      
+      
+      
+      >
+
+         Web            Components         are     <span 
+      
+      
+      class="mood"      >\${
+        this.mood
+      
+      }</span
+      
+           >!
+    \`;
+  }
+}
+
+customElements.define('my-element', MyElement);
+
+const someHtml1 = html\`<div       > hello \${world} </div     >\`;
+const someHtml2 = /* HTML */ \`<div      > hello \${world} </div     >\`;
+
+html\`\`
+
+html\`<my-element obj=\${obj}></my-element>\`;
+
+html\`  <\${Footer}  >footer      content<//     >  \`
+
+html\`  <div />  \`
+
+html\`
+  <div />
+\`
+
+html\`<span>one</span><span>two</span><span>three</span>\`;
+
+function HelloWorld() {
+  return html\`
+    <h3>Bar List</h3>
+    \${bars.map(bar => html\`
+       <p>\${bar}</p>
+    \`)}
+  \`;
+}
+
+const trickyParens = html\`<script> f((\${expr}) / 2); </script>\`;
+const nestedFun = /* HTML */ \`\${outerExpr( 1 )} <script>const tpl = html\\\`<div>\\\${innerExpr( 1 )} \${outerExpr( 2 )}</div>\\\`</script>\`;
+
+const closingScriptTagShouldBeEscapedProperly = /* HTML */ \`
+  <script>
+    const html = /* HTML */ \\\`<script><\\\\/script>\\\`;
+  </script>
+\`;
+
+const closingScriptTag2 = /* HTML */ \`<script>const  scriptTag='<\\\\/script>'; <\\/script>\`;
+
+=====================================output=====================================
+import { LitElement, html } from "@polymer/lit-element";
+
+class MyElement extends LitElement {
+  static get properties() {
+    return {
+      mood: { type: String },
+    };
+  }
+
+  constructor() {
+    super();
+    this.mood = "happy";
+  }
+
+  render() {
+    return html\`
+      <style>
+        .mood {
+          color: green;
+        }
+      </style>
+
+      Web Components are
+      <span class="mood">\${this.mood}</span>
+      !
+    \`;
+  }
+}
+
+customElements.define("my-element", MyElement);
+
 const someHtml1 = html\`
   <div>hello \${world}</div>
 \`;
@@ -130,6 +464,16 @@ html\`
   <div />
 \`;
 
+html\`
+  <div />
+\`;
+
+html\`
+  <span>one</span>
+  <span>two</span>
+  <span>three</span>
+\`;
+
 function HelloWorld() {
   return html\`
     <h3>Bar List</h3>
diff --git a/tests/multiparser_js_html/html-template-literals.js b/tests/multiparser_js_html/html-template-literals.js
new file mode 100644
index 000000000000..d4a787b5660f
--- /dev/null
+++ b/tests/multiparser_js_html/html-template-literals.js
@@ -0,0 +1,32 @@
+const nestedFun = /* HTML */ `${outerExpr(1)}
+  <script>
+    const tpl = html\`<div>\${innerExpr(1)} ${outerExpr(2)}</div>\`;
+  </script>`;
+
+const nestedFun2 = /* HTML */ `${outerExpr(1)}
+  <script>
+    const tpl = html\`\\n<div>\${innerExpr(1)} ${outerExpr(2)}</div>\\n\`;
+  </script>`;
+
+setFoo(
+  html`<div>one</div>
+    <div>two</div>
+    <div>three</div>`,
+  secondArgument
+);
+
+setFoo(
+  html`<div>
+      <div>nested</div>
+    </div>
+    <div>two</div>
+    <div>three</div>`,
+  secondArgument
+);
+
+setFoo(
+  html`<div>
+    <div>nested</div>
+  </div>`,
+  secondArgument
+);
diff --git a/tests/multiparser_js_html/jsfmt.spec.js b/tests/multiparser_js_html/jsfmt.spec.js
index eb85eda6bd02..ebb684d4174a 100644
--- a/tests/multiparser_js_html/jsfmt.spec.js
+++ b/tests/multiparser_js_html/jsfmt.spec.js
@@ -1,1 +1,4 @@
 run_spec(__dirname, ["babel", "flow", "typescript"]);
+run_spec(__dirname, ["babel", "flow", "typescript"], {
+  htmlWhitespaceSensitivity: "ignore"
+});
diff --git a/tests/multiparser_js_html/lit-html.js b/tests/multiparser_js_html/lit-html.js
index d147907291f8..16ca0fc7d60f 100644
--- a/tests/multiparser_js_html/lit-html.js
+++ b/tests/multiparser_js_html/lit-html.js
@@ -51,6 +51,12 @@ html`  <${Footer}  >footer      content<//     >  `
 
 html`  <div />  `
 
+html`
+  <div />
+`
+
+html`<span>one</span><span>two</span><span>three</span>`;
+
 function HelloWorld() {
   return html`
     <h3>Bar List</h3>