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

Issue:
Extra whitespace in class attributes should be removed.
Consider the following piece of HTML:

```html
<div class="something        else"></div>
```
If this code is inside an `.svelte` file, prettier does not remove the extra whitespace in the class attribute, however if it's inside a `.html` file, prettier will indeed remove them (in accordance to prettier's docs).

To reproduce, follow this minimal example:

- Create Svelte Skeleton project:

```
pnpm create svelte@latest demo
cd demo
pnpm i
```

- edit `src/app.html` to contain the following line somewhere inside the `<body>` tag:
```html
<div class="something        else"></div>
```

- edit `src/routes/+page.svelte` to contain the same line somewhere.
- run: `pnpm run format`
- check `app.html` and find the class attribute value formatted, no extra whitespaces.
- check `src/routes/+page.svelte` and find the class attribute value NOT formatted. Extra whitespaces everywhere.


I think the formatting should be consistent, no matter if the class attribute is inside a `.svelte` file or inside a `.html` file.



Pull request description:
(feat) trim whitespace in `class` attributes
fixes #339

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 7009ae8..223f64d 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,7 @@
 
 -   (feat) support `requirePragma` and `insertPragma` options ([#350](https://github.com/sveltejs/prettier-plugin-svelte/issues/350))
 -   (feat) support `<svelte:document>`
+-   (feat) trim whitespace in `class` attributes ([#339](https://github.com/sveltejs/prettier-plugin-svelte/issues/339))
 
 ## 2.9.0
 
diff --git a/src/print/index.ts b/src/print/index.ts
index 603a286..98f8c3a 100644
--- a/src/print/index.ts
+++ b/src/print/index.ts
@@ -180,10 +180,38 @@ export function print(path: FastPath, options: ParserOptions, print: PrintFn): D
                  */
                 return fill(splitTextToDocs(node));
             } else {
-                const rawText = getUnencodedText(node);
-                if (path.getParentNode().type === 'Attribute') {
+                let rawText = getUnencodedText(node);
+                const parent = path.getParentNode();
+                if (parent.type === 'Attribute') {
                     // Direct child of attribute value -> add literallines at end of lines
                     // so that other things don't break in unexpected places
+                    if (parent.name === 'class' && path.getParentNode(1).type === 'Element') {
+                        // Special treatment for class attribute on html elements. Prettier
+                        // will force everything into one line, we deviate from that and preserve lines.
+                        rawText = rawText.replace(
+                            /([^ \t\n])(([ \t]+$)|([ \t]+(\r?\n))|[ \t]+)/g,
+                            // Remove trailing whitespace in lines with non-whitespace characters
+                            // except at the end of the string
+                            (
+                                match,
+                                characterBeforeWhitespace,
+                                _,
+                                isEndOfString,
+                                isEndOfLine,
+                                endOfLine,
+                            ) =>
+                                isEndOfString
+                                    ? match
+                                    : characterBeforeWhitespace + (isEndOfLine ? endOfLine : ' '),
+                        );
+                        // Shrink trailing whitespace in case it's followed by a mustache tag
+                        // and remove it completely if it's at the end of the string, but not
+                        // if it's on its own line
+                        rawText = rawText.replace(
+                            /([^ \t\n])[ \t]+$/,
+                            parent.value.indexOf(node) === parent.value.length - 1 ? '$1' : '$1 ',
+                        );
+                    }
                     return concat(replaceEndOfLineWith(rawText, literalline));
                 }
                 return rawText;


Test patch:
diff --git a/test/formatting/samples/attribute-with-linebreaks/output.html b/test/formatting/samples/attribute-with-linebreaks/output.html
index 630db44..2993c8b 100644
--- a/test/formatting/samples/attribute-with-linebreaks/output.html
+++ b/test/formatting/samples/attribute-with-linebreaks/output.html
@@ -1,5 +1,5 @@
 <div
-    class=" {longExpressionThatForcesLinebreaks} longExpressionThatForcesLinebreaks longExpressionThatForcesLinebreaks  "
+    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) => {
diff --git a/test/formatting/samples/attributes-class/input.html b/test/formatting/samples/attributes-class/input.html
new file mode 100644
index 0000000..0f9963d
--- /dev/null
+++ b/test/formatting/samples/attributes-class/input.html
@@ -0,0 +1,26 @@
+<button
+    class="bg-green-500 text-gray-100 hover:bg-green-400 flex items-center px-4 py-2 border border-l-0        rounded-r "
+>
+    Copy
+</button>
+<div
+    class="bg-green-500 text-gray-100 hover:bg-green-400       flex items-center px-4 py-2 border border-l-0 rounded-r"
+>
+    Copy
+</div>
+<span
+    class="bg-green-500 text-gray-100 hover:bg-green-400 flex 
+    
+    items-center px-4 py-2 border border-l-0 rounded-r "
+>
+    Copy
+</span>
+<p
+    class="bg-green-500 
+    text-gray-100 hover:bg-green-400 flex items-center px-4 py-2 border border-l-0 rounded-r 
+    "
+>
+    Copy
+</p>
+
+<P class="leave me   alone   because I'm a prop  " />
diff --git a/test/formatting/samples/attributes-class/output.html b/test/formatting/samples/attributes-class/output.html
new file mode 100644
index 0000000..4569851
--- /dev/null
+++ b/test/formatting/samples/attributes-class/output.html
@@ -0,0 +1,26 @@
+<button
+    class="bg-green-500 text-gray-100 hover:bg-green-400 flex items-center px-4 py-2 border border-l-0 rounded-r"
+>
+    Copy
+</button>
+<div
+    class="bg-green-500 text-gray-100 hover:bg-green-400 flex items-center px-4 py-2 border border-l-0 rounded-r"
+>
+    Copy
+</div>
+<span
+    class="bg-green-500 text-gray-100 hover:bg-green-400 flex
+    
+    items-center px-4 py-2 border border-l-0 rounded-r"
+>
+    Copy
+</span>
+<p
+    class="bg-green-500
+    text-gray-100 hover:bg-green-400 flex items-center px-4 py-2 border border-l-0 rounded-r
+    "
+>
+    Copy
+</p>
+
+<P class="leave me   alone   because I'm a prop  " />