Repository: sveltejs/prettier-plugin-svelte
Pull request #127 (2020-08-26 09:23:16)

Preformatted text
Maintain whitespace and newlines as-in inside `<pre>` tags, i.e. formatting

```
<pre>
  _______
 /  12   \
|    |    |
|9   |   3|
|     \   |
|         |
 \___6___/
</pre>
```

does not turn into 

```
<pre> _______ / 12 \\ | | | |9 | 3| | \\ | | | \\___6___/</pre>
```

Also consider attribute values (with one exception: the `class` attribute) to be pre-formatted. Do not add or remove line breaks there.

This builds on PR #120. I thought it sensible to nevertheless keep it as a separate PR.

I got some code from #41, e.g. the `isPreTagContent` function.

Relates to #28 . I chose not to pursue the idea of configuring which tags are white-space sensitive. I'm happy to add that if there is a feeling it should be added; in any case it can easily be added on top of this PR.

dummdidumm: I'm a little confused now on what to review first 😄 does this PR contain everything of #120 + the additional `<pre>` behavior?
ehrencrona: @dummdidumm Yes it does. It probably makes sense to first review #120. If that is ok, this one (#127) can be reviewed and #116 can be closed. 

If #120 is rejected, #127 can be closed (because it builds on it) but it's still worth taking a look at #116.

Patch:
diff --git a/src/lib/elements.ts b/src/lib/elements.ts
index 2338fa9..a45bec0 100644
--- a/src/lib/elements.ts
+++ b/src/lib/elements.ts
@@ -72,3 +72,10 @@ export const inlineElements: TagName[] = [
   'video',
   'wbr'
 ];
+
+/**
+ * HTML attributes that we may safely reformat (trim whitespace, add or remove newlines)
+ */
+export const formattableAttributes = [
+  'class'
+]
diff --git a/src/print/helpers.ts b/src/print/helpers.ts
index 3007f92..6bbf057 100644
--- a/src/print/helpers.ts
+++ b/src/print/helpers.ts
@@ -1,4 +1,6 @@
-import { ASTNode } from './nodes';
+import { ASTNode, Node } from './nodes';
+import { FastPath } from 'prettier';
+import { formattableAttributes } from '../lib/elements';
 
 /**
  * Determines whether or not given node
@@ -7,3 +9,13 @@ import { ASTNode } from './nodes';
 export function isASTNode(n: any): n is ASTNode {
     return n && n.__isRoot;
 }
+
+export function isPreTagContent(path: FastPath): boolean {
+    const stack = path.stack as Node[];
+
+    return stack.some(
+        (node) =>
+            (node.type === 'Element' && node.name.toLowerCase() === 'pre') ||
+            (node.type === 'Attribute' && !formattableAttributes.includes(node.name)),
+    );
+}
diff --git a/src/print/index.ts b/src/print/index.ts
index 03cd24c..f3eaf27 100644
--- a/src/print/index.ts
+++ b/src/print/index.ts
@@ -1,11 +1,11 @@
 import { FastPath, Doc, doc, ParserOptions } from 'prettier';
 import { Node, MustacheTagNode, IfBlockNode } from './nodes';
-import { isASTNode } from './helpers';
+import { isASTNode, isPreTagContent } from './helpers';
 import { extractAttributes } from '../lib/extractAttributes';
 import { getText } from '../lib/getText';
 import { parseSortOrder, SortOrderPart } from '../options';
 import { hasSnippedContent, unsnipContent } from '../lib/snipTagContent';
-import { selfClosingTags } from '../lib/elements';
+import { selfClosingTags, formattableAttributes } from '../lib/elements';
 import {
     canBreakBefore,
     canBreakAfter,
@@ -112,37 +112,45 @@ export function print(path: FastPath, options: ParserOptions, print: PrintFn): D
                 return '';
             }
 
-            return concat([...trim(printChildren(path, print), isLine), hardline]);
-        case 'Text':
-            if (isEmptyNode(node)) {
-                return {
-                    /**
-                     * Empty (whitespace-only) text nodes are collapsed into a single `line`,
-                     * which will be rendered as a single space if this node's group fits on a
-                     * single line. This follows how vanilla HTML is handled both by browsers and
-                     * by Prettier core.
-                     */
-                    ...line,
-
-                    /**
-                     * A text node is considered lonely if it is in a group without other inline
-                     * elements, such as the line breaks between otherwise consecutive HTML tags.
-                     * Text nodes that are both empty and lonely are discarded unless they have at
-                     * least one empty line (i.e. at least two linebreak sequences). This is to
-                     * allow for flexible grouping of HTML tags in a particular indentation level,
-                     * and is similar to how vanilla HTML is handled in Prettier core.
-                     */
-                    keepIfLonely: /\n\r?\s*\n\r?/.test(node.raw || node.data),
-                };
+            if (!isPreTagContent(path)) {
+                return concat([...trim(printChildren(path, print), isLine), hardline]);
+            } else {
+                return concat(printChildren(path, print));
             }
+        case 'Text':
+            if (!isPreTagContent(path)) {
+                if (isEmptyNode(node)) {
+                    return {
+                        /**
+                         * Empty (whitespace-only) text nodes are collapsed into a single `line`,
+                         * which will be rendered as a single space if this node's group fits on a
+                         * single line. This follows how vanilla HTML is handled both by browsers and
+                         * by Prettier core.
+                         */
+                        ...line,
+
+                        /**
+                         * A text node is considered lonely if it is in a group without other inline
+                         * elements, such as the line breaks between otherwise consecutive HTML tags.
+                         * Text nodes that are both empty and lonely are discarded unless they have at
+                         * least one empty line (i.e. at least two linebreak sequences). This is to
+                         * allow for flexible grouping of HTML tags in a particular indentation level,
+                         * and is similar to how vanilla HTML is handled in Prettier core.
+                         */
+                        keepIfLonely: /\n\r?\s*\n\r?/.test(node.raw || node.data),
+                    };
+                }
 
-            /**
-             * For non-empty text nodes each sequence of non-whitespace characters (effectively,
-             * each "word") is joined by a single `line`, which will be rendered as a single space
-             * until this node's current line is out of room, at which `fill` will break at the
-             * most convenient instance of `line`.
-             */
-            return fill(splitTextToDocs(node.raw || node.data));
+                /**
+                 * For non-empty text nodes each sequence of non-whitespace characters (effectively,
+                 * each "word") is joined by a single `line`, which will be rendered as a single space
+                 * until this node's current line is out of room, at which `fill` will break at the
+                 * most convenient instance of `line`.
+                 */
+                return fill(splitTextToDocs(node.raw || node.data));
+            } else {
+                return node.data;
+            }
         case 'Element':
         case 'InlineComponent':
         case 'Slot':
@@ -188,7 +196,7 @@ export function print(path: FastPath, options: ParserOptions, print: PrintFn): D
                               '>',
                               isEmpty
                                   ? ''
-                                  : isInlineElement(node)
+                                  : isInlineElement(node) || isPreTagContent(path)
                                   ? printIndentedPreservingWhitespace(path, print)
                                   : printIndentedWithNewlines(path, print),
                               `</${node.name}>`,
@@ -241,7 +249,15 @@ export function print(path: FastPath, options: ParserOptions, print: PrintFn): D
                     const quotes = !hasLoneMustacheTag || options.svelteStrictMode;
 
                     quotes && def.push('"');
-                    def.push(...path.map((childPath) => childPath.call(print), 'value'));
+
+                    const valueDocs = path.map((childPath) => childPath.call(print), 'value');
+
+                    if (!quotes || !formattableAttributes.includes(node.name)) {
+                        def.push(concat(valueDocs));
+                    } else {
+                        def.push(indent(group(concat(trim(valueDocs, isLine)))));
+                    }
+
                     quotes && def.push('"');
                 }
                 return concat(def);
@@ -478,6 +494,8 @@ function printChildren(path: FastPath, print: PrintFn): Doc[] {
     // the index of the last child doc we could add a linebreak after
     let lastBreakIndex = -1;
 
+    const isPreformat = isPreTagContent(path);
+
     /**
      * Call when reaching a point where a linebreak is possible. Will
      * put all `childDocs` since the last possible linebreak position
@@ -498,28 +516,30 @@ function printChildren(path: FastPath, print: PrintFn): Doc[] {
      * @param childDoc null means do not add anything but allow for the possibility of a linebreak here.
      */
     function outputChildDoc(childDoc: Doc | null, fromNodes: Node[]) {
-        const firstNode = fromNodes[0];
-        const lastNode = fromNodes[fromNodes.length - 1];
-
-        if (!childDoc || canBreakBefore(firstNode)) {
-            linebreakPossible();
-
-            const lastChild = childDocs[childDocs.length - 1];
-
-            // separate children by softlines, but not if the children are already lines.
-            // one exception: allow for a line break before "keepIfLonely" lines because they represent an empty line
-            if (
-                childDoc != null &&
-                !isLineDiscardedIfLonely(childDoc) &&
-                lastChild != null &&
-                !isLine(lastChild)
-            ) {
-                childDocs.push(softline);
+        if (!isPreformat) {
+            const firstNode = fromNodes[0];
+            const lastNode = fromNodes[fromNodes.length - 1];
+
+            if (!childDoc || canBreakBefore(firstNode)) {
+                linebreakPossible();
+
+                const lastChild = childDocs[childDocs.length - 1];
+
+                // separate children by softlines, but not if the children are already lines.
+                // one exception: allow for a line break before "keepIfLonely" lines because they represent an empty line
+                if (
+                    childDoc != null &&
+                    !isLineDiscardedIfLonely(childDoc) &&
+                    lastChild != null &&
+                    !isLine(lastChild)
+                ) {
+                    childDocs.push(softline);
+                }
             }
-        }
 
-        if (lastBreakIndex < 0 && childDoc && !canBreakAfter(lastNode)) {
-            lastBreakIndex = childDocs.length;
+            if (lastBreakIndex < 0 && childDoc && !canBreakAfter(lastNode)) {
+                lastBreakIndex = childDocs.length;
+            }
         }
 
         if (childDoc) {


Test patch:
diff --git a/test/formatting/samples/attribute-with-linebreaks/input.html b/test/formatting/samples/attribute-with-linebreaks/input.html
new file mode 100644
index 0000000..5f98333
--- /dev/null
+++ b/test/formatting/samples/attribute-with-linebreaks/input.html
@@ -0,0 +1,8 @@
+<div
+    class=" {longExpressionThatForcesLinebreaks} longExpressionThatForcesLinebreaks longExpressionThatForcesLinebreaks  "
+    alt="When reformatting class attributes it is fine to trim and add new line breaks. 
+That's not the case with other attributes. "
+    data-value={array.map((item) => {
+        return item + 1
+    })}
+/>
diff --git a/test/formatting/samples/attribute-with-linebreaks/output.html b/test/formatting/samples/attribute-with-linebreaks/output.html
new file mode 100644
index 0000000..d28db2c
--- /dev/null
+++ b/test/formatting/samples/attribute-with-linebreaks/output.html
@@ -0,0 +1,8 @@
+<div
+    class="{longExpressionThatForcesLinebreaks} longExpressionThatForcesLinebreaks
+        longExpressionThatForcesLinebreaks"
+    alt="When reformatting class attributes it is fine to trim and add new line breaks. 
+That's not the case with other attributes. "
+    data-value={array.map((item) => {
+        return item + 1;
+    })} />
diff --git a/test/printer/samples/pre-element-inside-other-element.html b/test/printer/samples/pre-element-inside-other-element.html
new file mode 100644
index 0000000..091fe8d
--- /dev/null
+++ b/test/printer/samples/pre-element-inside-other-element.html
@@ -0,0 +1,12 @@
+<div>
+    <pre>foo
+bar
+
+    <b>bold</b>     <div>d</div>
+
+
+
+    <b>bold</b>     <div>d</div>
+
+baz</pre>
+</div>
diff --git a/test/printer/samples/pre-element-with-code-element.html b/test/printer/samples/pre-element-with-code-element.html
new file mode 100644
index 0000000..fea29ab
--- /dev/null
+++ b/test/printer/samples/pre-element-with-code-element.html
@@ -0,0 +1,7 @@
+<pre>
+    <code>
+        const greeting = "Hello",
+            name = "World",
+            text = greeting + ", " + name + "!";
+    </code>
+</pre>
diff --git a/test/printer/samples/pre-element.html b/test/printer/samples/pre-element.html
new file mode 100644
index 0000000..da03cd7
--- /dev/null
+++ b/test/printer/samples/pre-element.html
@@ -0,0 +1,2 @@
+<pre>foo
+bar baz</pre>