Repository: sveltejs/svelte (JavaScript)
Date: 2021-08-03 09:25:22+00:00
Task type: Feature

Issue:
Normalize whitespaces in attribute values during compilation
**Is your feature request related to a problem? Please describe.**
It's not unusual to want to format your code (in this example particularly your CSS classes) in the following style:

![image](https://user-images.githubusercontent.com/26527405/108560880-a2a10800-7312-11eb-8ac8-cea0bd4f7b9f.png)

In fact, I think Prettier and some other formatting tools encourage a similar thing, namely breaking a long line of code into several shorter ones, and Prettier specifically does it with CSS classes.

But this apparently causes the Svelte compiler to generate code like this:
```
class: button_class_value = "button\r\n           button--size--" + /*size*/ ctx[0] + "\r\n           button--theme--" + /*theme*/ ctx[1] + "\r\n           " + (/*$$restProps*/ ctx[6].class || "")
```
![image](https://user-images.githubusercontent.com/26527405/108561560-b7ca6680-7313-11eb-9700-be59bfdb5298.png)

Notice all the unnecessary "\r"s, "\n"s, and multiple whitespace characters.
These:
- Make the bundle size bigger completely unnecessarily.
- Make the HTML output rather ugly:

![image](https://user-images.githubusercontent.com/26527405/108561511-9f5a4c00-7313-11eb-81ad-77199d571424.png)
Screenshot from Chrome DevTools


**Describe the solution you'd like**
Since multiple whitespaces, tabs, and line breaks inside HTML attribute values are always ignored by browsers and cause no functional difference, the Svelte compiler could easily ignore these characters, and "normalize" the whitespaces in attribute values, without causing any harm.
So that the result would look like this:
```
class: button_class_value = "button button--size--" + /*size*/ ctx[0] + " button--theme--" + /*theme*/ ctx[1] + (/*$$restProps*/ ctx[6].class || "")
```


Issue discussion:
As of version 1.3.0, via https://github.com/sveltejs/prettier-plugin-svelte/pull/145, the Prettier plugin no longer formats `class` attributes, to keep in line with Prettier's HTML formatting.

I have mixed feelings about whether the compiler should collapse whitespace in `class` attributes. It should definitely not collapse whitespace in all attributes, though.
@Conduitry  Okay, but even if Prettier doesn't do that anymore, it still makes perfect sense to format your classes that way if you have a lot of them; otherwise if they're all on a single line it would become ridiculously long and pretty hard to look at! Wouldn't you agree?

> I have mixed feelings about whether the compiler should collapse whitespace in class attributes.

I can't think of any cases where that could be undesirable or problematic, since it would make no functional difference whatsoever. (in the case of the `class` attribute at least, I'm not quite sure about others.)
Can you provide some examples? Why exactly do you have mixed feelings about this?
It also happens to HTML only components even if you don't split the opening tag. You get a lot of \n and whitespaces that are not needed and increase the bundle size. Just commenting because I think a whitespace stripping option should also include these so we can create even smaller production bundles.
![image](https://user-images.githubusercontent.com/15054534/108607613-b22b5a00-73c1-11eb-8922-ec62a825db38.png)


I agree. I think not only the classes but all the whitespaces in the entire HTML should be taken car eof, any redundant whitespaces should be removed. There is no reason not to do this.

As mentioned, they just increase the bundle size completely unnecessarily.

> ...It should definitely not collapse whitespace in all attributes, though.

Why shouldn't it?! In what attribute does that make any difference?
Removing whitespace inside HTML content (not attributes) can be dangerous because people might style the element such that it should preserve whitespace. 
@dummdidumm Sure, I don't think Svelte should collapse whitespaces inside tags, although it'd be nice if we could have an option for that too, such that the developer could make the decision themself, because after all, most of the multiwhitespaces are just there for formatting, and so they ultimately add to the bundle size totally unnecessarily).

But in the case of attribute values, again, I can't think of any cases where multiple whitespaces would make any difference at all, and therefore there is no reason to preserve them there.
This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.

Hi there again.
Are there any reasons that make this idea non-feasible?

Solution patch:
diff --git a/src/compiler/compile/nodes/Element.ts b/src/compiler/compile/nodes/Element.ts
--- a/src/compiler/compile/nodes/Element.ts
+++ b/src/compiler/compile/nodes/Element.ts
@@ -96,6 +96,8 @@ const react_attributes = new Map([
 	['htmlFor', 'for']
 ]);
 
+const attributes_to_compact_whitespace = ['class', 'style'];
+
 function get_namespace(parent: Element, element: Element, explicit_namespace: string) {
 	const parent_element = parent.find_nearest(/^Element/);
 
@@ -241,6 +243,8 @@ export default class Element extends Node {
 
 		this.validate();
 
+		this.optimise();
+
 		component.apply_stylesheet(this);
 	}
 
@@ -750,6 +754,25 @@ export default class Element extends Node {
 	get slot_template_name() {
 		return this.attributes.find(attribute => attribute.name === 'slot').get_static_value() as string;
 	}
+
+	optimise() {
+		attributes_to_compact_whitespace.forEach(attribute_name => {
+			const attribute = this.attributes.find(a => a.name === attribute_name);
+			if (attribute && !attribute.is_true) {
+				attribute.chunks.forEach((chunk, index) => {
+					if (chunk.type === 'Text') {
+						let data = chunk.data.replace(/[\s\n\t]+/g, ' ');
+						if (index === 0) {
+							data = data.trimLeft();
+						} else if (index === attribute.chunks.length - 1) {
+							data = data.trimRight();
+						}
+						chunk.data = data;
+					}
+				});
+			}
+		});
+	}
 }
 
 function should_have_attribute(


Test patch:
diff --git a/test/js/samples/collapse-element-class-name/expected.js b/test/js/samples/collapse-element-class-name/expected.js
new file mode 100644
--- /dev/null
+++ b/test/js/samples/collapse-element-class-name/expected.js
@@ -0,0 +1,114 @@
+/* generated by Svelte vX.Y.Z */
+import {
+	SvelteComponent,
+	assign,
+	attr,
+	compute_rest_props,
+	create_component,
+	destroy_component,
+	detach,
+	element,
+	exclude_internal_props,
+	init,
+	insert,
+	mount_component,
+	safe_not_equal,
+	space,
+	transition_in,
+	transition_out
+} from "svelte/internal";
+
+import Component from "./Component.svelte";
+
+function create_fragment(ctx) {
+	let div;
+	let div_class_value;
+	let div_style_value;
+	let div_other_value;
+	let t;
+	let component;
+	let current;
+
+	component = new Component({
+			props: {
+				class: "\n\t\tbutton\n\t\tbutton--size--" + /*size*/ ctx[0] + "\n\t\tbutton--theme--" + /*theme*/ ctx[1] + "\n  \t" + (/*$$restProps*/ ctx[2].class || ''),
+				style: "\n\t\tcolor: green;\n\t\tbackground: white;\n\t\tfont-size: " + /*size*/ ctx[0] + ";\n  \ttransform: " + /*$$restProps*/ ctx[2].scale + "  " + /*$$restProps*/ ctx[2].rotate + ";\n\t\t" + /*$$restProps*/ ctx[2].styles,
+				other: "\n\t\tbutton\n\t\tbutton--size--" + /*size*/ ctx[0] + "\n\t\tbutton--theme--" + /*theme*/ ctx[1] + "\n  \t" + (/*$$restProps*/ ctx[2].class || '')
+			}
+		});
+
+	return {
+		c() {
+			div = element("div");
+			t = space();
+			create_component(component.$$.fragment);
+			attr(div, "class", div_class_value = "button button--size--" + /*size*/ ctx[0] + " button--theme--" + /*theme*/ ctx[1] + " " + (/*$$restProps*/ ctx[2].class || ''));
+			attr(div, "style", div_style_value = "color: green; background: white; font-size: " + /*size*/ ctx[0] + "; transform: " + /*$$restProps*/ ctx[2].scale + " " + /*$$restProps*/ ctx[2].rotate + "; " + /*$$restProps*/ ctx[2].styles);
+			attr(div, "other", div_other_value = "\n\t\tbutton\n\t\tbutton--size--" + /*size*/ ctx[0] + "\n\t\tbutton--theme--" + /*theme*/ ctx[1] + "\n  \t" + (/*$$restProps*/ ctx[2].class || ''));
+		},
+		m(target, anchor) {
+			insert(target, div, anchor);
+			insert(target, t, anchor);
+			mount_component(component, target, anchor);
+			current = true;
+		},
+		p(ctx, [dirty]) {
+			if (!current || dirty & /*size, theme, $$restProps*/ 7 && div_class_value !== (div_class_value = "button button--size--" + /*size*/ ctx[0] + " button--theme--" + /*theme*/ ctx[1] + " " + (/*$$restProps*/ ctx[2].class || ''))) {
+				attr(div, "class", div_class_value);
+			}
+
+			if (!current || dirty & /*size, $$restProps*/ 5 && div_style_value !== (div_style_value = "color: green; background: white; font-size: " + /*size*/ ctx[0] + "; transform: " + /*$$restProps*/ ctx[2].scale + " " + /*$$restProps*/ ctx[2].rotate + "; " + /*$$restProps*/ ctx[2].styles)) {
+				attr(div, "style", div_style_value);
+			}
+
+			if (!current || dirty & /*size, theme, $$restProps*/ 7 && div_other_value !== (div_other_value = "\n\t\tbutton\n\t\tbutton--size--" + /*size*/ ctx[0] + "\n\t\tbutton--theme--" + /*theme*/ ctx[1] + "\n  \t" + (/*$$restProps*/ ctx[2].class || ''))) {
+				attr(div, "other", div_other_value);
+			}
+
+			const component_changes = {};
+			if (dirty & /*size, theme, $$restProps*/ 7) component_changes.class = "\n\t\tbutton\n\t\tbutton--size--" + /*size*/ ctx[0] + "\n\t\tbutton--theme--" + /*theme*/ ctx[1] + "\n  \t" + (/*$$restProps*/ ctx[2].class || '');
+			if (dirty & /*size, $$restProps*/ 5) component_changes.style = "\n\t\tcolor: green;\n\t\tbackground: white;\n\t\tfont-size: " + /*size*/ ctx[0] + ";\n  \ttransform: " + /*$$restProps*/ ctx[2].scale + "  " + /*$$restProps*/ ctx[2].rotate + ";\n\t\t" + /*$$restProps*/ ctx[2].styles;
+			if (dirty & /*size, theme, $$restProps*/ 7) component_changes.other = "\n\t\tbutton\n\t\tbutton--size--" + /*size*/ ctx[0] + "\n\t\tbutton--theme--" + /*theme*/ ctx[1] + "\n  \t" + (/*$$restProps*/ ctx[2].class || '');
+			component.$set(component_changes);
+		},
+		i(local) {
+			if (current) return;
+			transition_in(component.$$.fragment, local);
+			current = true;
+		},
+		o(local) {
+			transition_out(component.$$.fragment, local);
+			current = false;
+		},
+		d(detaching) {
+			if (detaching) detach(div);
+			if (detaching) detach(t);
+			destroy_component(component, detaching);
+		}
+	};
+}
+
+function instance($$self, $$props, $$invalidate) {
+	const omit_props_names = ["size","theme"];
+	let $$restProps = compute_rest_props($$props, omit_props_names);
+	let { size } = $$props;
+	let { theme } = $$props;
+
+	$$self.$$set = $$new_props => {
+		$$props = assign(assign({}, $$props), exclude_internal_props($$new_props));
+		$$invalidate(2, $$restProps = compute_rest_props($$props, omit_props_names));
+		if ('size' in $$new_props) $$invalidate(0, size = $$new_props.size);
+		if ('theme' in $$new_props) $$invalidate(1, theme = $$new_props.theme);
+	};
+
+	return [size, theme, $$restProps];
+}
+
+class Component_1 extends SvelteComponent {
+	constructor(options) {
+		super();
+		init(this, options, instance, create_fragment, safe_not_equal, { size: 0, theme: 1 });
+	}
+}
+
+export default Component_1;
\ No newline at end of file
diff --git a/test/js/samples/collapse-element-class-name/input.svelte b/test/js/samples/collapse-element-class-name/input.svelte
new file mode 100644
--- /dev/null
+++ b/test/js/samples/collapse-element-class-name/input.svelte
@@ -0,0 +1,41 @@
+<script>
+  import Component from "./Component.svelte";
+  export let size;
+  export let theme;
+</script>
+
+<div
+  class="
+		button
+		button--size--{size}
+		button--theme--{theme}
+  	{$$restProps.class || ''}"
+	style="
+		color: green;
+		background: white;
+		font-size: {size};
+  	transform: {$$restProps.scale}  {$$restProps.rotate};
+		{$$restProps.styles}"
+  other="
+		button
+		button--size--{size}
+		button--theme--{theme}
+  	{$$restProps.class || ''}" />
+
+<Component
+	class="
+		button
+		button--size--{size}
+		button--theme--{theme}
+  	{$$restProps.class || ''}"
+	style="
+		color: green;
+		background: white;
+		font-size: {size};
+  	transform: {$$restProps.scale}  {$$restProps.rotate};
+		{$$restProps.styles}"
+  other="
+		button
+		button--size--{size}
+		button--theme--{theme}
+  	{$$restProps.class || ''}" />
\ No newline at end of file
diff --git a/test/server-side-rendering/samples/spread-attributes-white-space/_expected.html b/test/server-side-rendering/samples/spread-attributes-white-space/_expected.html
--- a/test/server-side-rendering/samples/spread-attributes-white-space/_expected.html
+++ b/test/server-side-rendering/samples/spread-attributes-white-space/_expected.html
@@ -2,7 +2,4 @@
 	bar
 ">
 
-<input class="
-	white
-	space
-">
\ No newline at end of file
+<input class="white space ">
\ No newline at end of file