Repository: sveltejs/svelte (JavaScript) — Bug Fix

Issue:
Spaces on the edges of elements are not preserved
When I render this html in the browser:

```html
<h1>Hello <strong>World! </strong><span>How are you?</span></h1>
```

The space at the end of the `strong` element is preserved, and there is a space between the two sentences.

However, right now (1.21.0) Svelte strips that space.  [REPL reproduction](https://svelte.technology/repl?version=1.21.0&gist=c03d6fe9db4fb65fc800e3b498656560)


Discussion:
This is a bit less straightforward to handle than I was initially thinking. This gets us right into that optimizing/collapsing whitespace stuff that's come up before - when do we want to trim whitespace? when do we want to collapse to a single space? when do we want to leave all whitespace as-is? We can't know this for certain without examining CSS, which we can't reliably do at compile time.

My suggestion (and part of this might have to wait until version 2 as it would be a breaking change) is to more aggressively remove whitespace, but to encourage the use of mustaches with static string literals if specific whitespace is needed. I've seen this idiom in JSX I believe. It's a bit less user friendly than I'd like, but I don't see what else would be practical to implement.

The compiled component code generated from static string mustaches actually isn't bad at the moment, but if this were to be officially endorsed as a method of addressing this, it may be good to tidy up the generated code a bit.

Resolving patch:
diff --git a/src/generators/dom/preprocess.ts b/src/generators/dom/preprocess.ts
--- a/src/generators/dom/preprocess.ts
+++ b/src/generators/dom/preprocess.ts
@@ -34,7 +34,8 @@ const preprocessors = {
 		generator: DomGenerator,
 		block: Block,
 		state: State,
-		node: Node
+		node: Node,
+		stripWhitespace: boolean
 	) => {
 		const dependencies = block.findDependencies(node.expression);
 		block.addDependencies(dependencies);
@@ -48,7 +49,8 @@ const preprocessors = {
 		generator: DomGenerator,
 		block: Block,
 		state: State,
-		node: Node
+		node: Node,
+		stripWhitespace: boolean
 	) => {
 		const dependencies = block.findDependencies(node.expression);
 		block.addDependencies(dependencies);
@@ -59,7 +61,7 @@ const preprocessors = {
 		node._state = getChildState(state, { basename, name });
 	},
 
-	Text: (generator: DomGenerator, block: Block, state: State, node: Node) => {
+	Text: (generator: DomGenerator, block: Block, state: State, node: Node, stripWhitespace: boolean) => {
 		node._state = getChildState(state);
 
 		if (!/\S/.test(node.data)) {
@@ -75,7 +77,9 @@ const preprocessors = {
 		generator: DomGenerator,
 		block: Block,
 		state: State,
-		node: Node
+		node: Node,
+		stripWhitespace: boolean,
+		nextSibling: Node
 	) => {
 		const blocks: Block[] = [];
 		let dynamic = false;
@@ -93,7 +97,7 @@ const preprocessors = {
 			node._state = getChildState(state);
 
 			blocks.push(node._block);
-			preprocessChildren(generator, node._block, node._state, node);
+			preprocessChildren(generator, node._block, node._state, node, stripWhitespace, node);
 
 			if (node._block.dependencies.size > 0) {
 				dynamic = true;
@@ -117,7 +121,9 @@ const preprocessors = {
 					generator,
 					node.else._block,
 					node.else._state,
-					node.else
+					node.else,
+					stripWhitespace,
+					nextSibling
 				);
 
 				if (node.else._block.dependencies.size > 0) {
@@ -142,7 +148,9 @@ const preprocessors = {
 		generator: DomGenerator,
 		block: Block,
 		state: State,
-		node: Node
+		node: Node,
+		stripWhitespace: boolean,
+		nextSibling: Node
 	) => {
 		const dependencies = block.findDependencies(node.expression);
 		block.addDependencies(dependencies);
@@ -189,7 +197,7 @@ const preprocessors = {
 		});
 
 		generator.blocks.push(node._block);
-		preprocessChildren(generator, node._block, node._state, node);
+		preprocessChildren(generator, node._block, node._state, node, stripWhitespace, nextSibling);
 		block.addDependencies(node._block.dependencies);
 		node._block.hasUpdateMethod = node._block.dependencies.size > 0;
 
@@ -205,7 +213,9 @@ const preprocessors = {
 				generator,
 				node.else._block,
 				node.else._state,
-				node.else
+				node.else,
+				stripWhitespace,
+				nextSibling
 			);
 			node.else._block.hasUpdateMethod = node.else._block.dependencies.size > 0;
 		}
@@ -215,7 +225,9 @@ const preprocessors = {
 		generator: DomGenerator,
 		block: Block,
 		state: State,
-		node: Node
+		node: Node,
+		stripWhitespace: boolean,
+		nextSibling: Node
 	) => {
 		node.attributes.forEach((attribute: Node) => {
 			if (attribute.type === 'Attribute' && attribute.value !== true) {
@@ -305,11 +317,12 @@ const preprocessors = {
 				});
 
 				generator.blocks.push(node._block);
-				preprocessChildren(generator, node._block, node._state, node);
+				preprocessChildren(generator, node._block, node._state, node, stripWhitespace, nextSibling);
 				block.addDependencies(node._block.dependencies);
 				node._block.hasUpdateMethod = node._block.dependencies.size > 0;
 			} else {
-				preprocessChildren(generator, block, node._state, node);
+				if (node.name === 'pre' || node.name === 'textarea') stripWhitespace = false;
+				preprocessChildren(generator, block, node._state, node, stripWhitespace, nextSibling);
 			}
 		}
 	},
@@ -320,7 +333,8 @@ function preprocessChildren(
 	block: Block,
 	state: State,
 	node: Node,
-	isTopLevel: boolean = false
+	stripWhitespace: boolean,
+	nextSibling: Node
 ) {
 	// glue text nodes together
 	const cleaned: Node[] = [];
@@ -333,32 +347,22 @@ function preprocessChildren(
 			lastChild.data += child.data;
 			lastChild.end = child.end;
 		} else {
-			cleaned.push(child);
+			if (child.type === 'Text' && stripWhitespace && cleaned.length === 0) {
+				child.data = trimStart(child.data);
+				if (child.data) cleaned.push(child);
+			} else {
+				cleaned.push(child);
+			}
 		}
 
 		lastChild = child;
 	});
 
-	if (isTopLevel) {
-		// trim leading and trailing whitespace from the top level
-		const firstChild = cleaned[0];
-		if (firstChild && firstChild.type === 'Text') {
-			firstChild.data = trimStart(firstChild.data);
-			if (!firstChild.data) cleaned.shift();
-		}
-
-		const lastChild = cleaned[cleaned.length - 1];
-		if (lastChild && lastChild.type === 'Text') {
-			lastChild.data = trimEnd(lastChild.data);
-			if (!lastChild.data) cleaned.pop();
-		}
-	}
-
 	lastChild = null;
 
-	cleaned.forEach((child: Node) => {
-		const preprocess = preprocessors[child.type];
-		if (preprocess) preprocess(generator, block, state, child);
+	cleaned.forEach((child: Node, i: number) => {
+		const preprocessor = preprocessors[child.type];
+		if (preprocessor) preprocessor(generator, block, state, child, stripWhitespace, cleaned[i + 1] || nextSibling);
 
 		if (lastChild) {
 			lastChild.next = child;
@@ -368,6 +372,19 @@ function preprocessChildren(
 		lastChild = child;
 	});
 
+	// We want to remove trailing whitespace inside an element/component/block,
+	// *unless* there is no whitespace between this node and its next sibling
+	if (lastChild && lastChild.type === 'Text') {
+		if (stripWhitespace && (!nextSibling || (nextSibling.type === 'Text' && /^\s/.test(nextSibling.data)))) {
+			lastChild.data = trimEnd(lastChild.data);
+			if (!lastChild.data) {
+				cleaned.pop();
+				lastChild = cleaned[cleaned.length - 1];
+				lastChild.next = null;
+			}
+		}
+	}
+
 	if (lastChild) {
 		lastChild.needsAnchor = !state.parentNode;
 	}
@@ -404,7 +421,7 @@ export default function preprocess(
 	};
 
 	generator.blocks.push(block);
-	preprocessChildren(generator, block, state, node, true);
+	preprocessChildren(generator, block, state, node, true, null);
 	block.hasUpdateMethod = block.dependencies.size > 0;
 
 	return { block, state };
diff --git a/src/generators/server-side-rendering/index.ts b/src/generators/server-side-rendering/index.ts
--- a/src/generators/server-side-rendering/index.ts
+++ b/src/generators/server-side-rendering/index.ts
@@ -117,7 +117,7 @@ export default function ssr(
 				}
 			`}
 
-			return \`${generator.renderCode}\`;
+			return \`${generator.renderCode}\`.trim();
 		};
 
 		${name}.renderCss = function () {
diff --git a/src/parse/index.ts b/src/parse/index.ts
--- a/src/parse/index.ts
+++ b/src/parse/index.ts
@@ -76,39 +76,17 @@ export class Parser {
 			this.error('Unexpected end of input');
 		}
 
-		// trim unnecessary whitespace
-		while (this.html.children.length) {
-			const firstChild = this.html.children[0];
-			this.html.start = firstChild.start;
-
-			if (firstChild.type !== 'Text') break;
-
-			const length = firstChild.data.length;
-			firstChild.data = trimStart(firstChild.data);
-
-			if (firstChild.data === '') {
-				this.html.children.shift();
-			} else {
-				this.html.start += length - firstChild.data.length;
-				break;
-			}
-		}
-
-		while (this.html.children.length) {
-			const lastChild = this.html.children[this.html.children.length - 1];
-			this.html.end = lastChild.end;
-
-			if (lastChild.type !== 'Text') break;
+		if (this.html.children.length) {
+			let start = this.html.children[0] && this.html.children[0].start;
+			while (/\s/.test(template[start])) start += 1;
 
-			const length = lastChild.data.length;
-			lastChild.data = trimEnd(lastChild.data);
+			let end = this.html.children[this.html.children.length - 1] && this.html.children[this.html.children.length - 1].end;
+			while (/\s/.test(template[end - 1])) end -= 1;
 
-			if (lastChild.data === '') {
-				this.html.children.pop();
-			} else {
-				this.html.end -= length - lastChild.data.length;
-				break;
-			}
+			this.html.start = start;
+			this.html.end = end;
+		} else {
+			this.html.start = this.html.end = null;
 		}
 	}
 
diff --git a/src/parse/state/tag.ts b/src/parse/state/tag.ts
--- a/src/parse/state/tag.ts
+++ b/src/parse/state/tag.ts
@@ -62,23 +62,6 @@ const disallowedContents = new Map([
 	['th', new Set(['td', 'th', 'tr'])],
 ]);
 
-function stripWhitespace(element) {
-	if (element.children.length) {
-		const firstChild = element.children[0];
-		const lastChild = element.children[element.children.length - 1];
-
-		if (firstChild.type === 'Text') {
-			firstChild.data = trimStart(firstChild.data);
-			if (!firstChild.data) element.children.shift();
-		}
-
-		if (lastChild.type === 'Text') {
-			lastChild.data = trimEnd(lastChild.data);
-			if (!lastChild.data) element.children.pop();
-		}
-	}
-}
-
 export default function tag(parser: Parser) {
 	const start = parser.index++;
 
@@ -147,9 +130,6 @@ export default function tag(parser: Parser) {
 			parent = parser.current();
 		}
 
-		// strip leading/trailing whitespace as necessary
-		stripWhitespace(parent);
-
 		parent.end = parser.index;
 		parser.stack.pop();
 
@@ -158,8 +138,6 @@ export default function tag(parser: Parser) {
 		// can this be a child of the parent element, or does it implicitly
 		// close it, like `<li>one<li>two`?
 		if (disallowedContents.get(parent.name).has(name)) {
-			stripWhitespace(parent);
-
 			parent.end = start;
 			parser.stack.pop();
 		}


Test patch:
diff --git a/test/js/samples/use-elements-as-anchors/expected-bundle.js b/test/js/samples/use-elements-as-anchors/expected-bundle.js
--- a/test/js/samples/use-elements-as-anchors/expected-bundle.js
+++ b/test/js/samples/use-elements-as-anchors/expected-bundle.js
@@ -136,7 +136,7 @@ var proto = {
 };
 
 function create_main_fragment ( state, component ) {
-	var div, text, p, text_1, text_2, text_3, text_4, p_1, text_5, text_6, text_7, text_8, if_block_4_anchor;
+	var div, text, p, text_1, text_2, text_3, text_4, p_1, text_5, text_6, text_8, if_block_4_anchor;
 
 	var if_block = (state.a) && create_if_block( state, component );
 
@@ -164,7 +164,6 @@ function create_main_fragment ( state, component ) {
 			text_5 = createText( "so can this" );
 			text_6 = createText( "\n\n\t" );
 			if ( if_block_3 ) if_block_3.create();
-			text_7 = createText( "\n\n\t" );
 			text_8 = createText( "\n\n" );
 			if ( if_block_4 ) if_block_4.create();
 			if_block_4_anchor = createComment();
@@ -185,7 +184,6 @@ function create_main_fragment ( state, component ) {
 			appendNode( text_5, p_1 );
 			appendNode( text_6, div );
 			if ( if_block_3 ) if_block_3.mount( div, null );
-			appendNode( text_7, div );
 			insertNode( text_8, target, anchor );
 			if ( if_block_4 ) if_block_4.mount( target, anchor );
 			insertNode( if_block_4_anchor, target, anchor );
@@ -232,7 +230,7 @@ function create_main_fragment ( state, component ) {
 				if ( !if_block_3 ) {
 					if_block_3 = create_if_block_3( state, component );
 					if_block_3.create();
-					if_block_3.mount( div, text_7 );
+					if_block_3.mount( div, null );
 				}
 			} else if ( if_block_3 ) {
 				if_block_3.unmount();
diff --git a/test/js/samples/use-elements-as-anchors/expected.js b/test/js/samples/use-elements-as-anchors/expected.js
--- a/test/js/samples/use-elements-as-anchors/expected.js
+++ b/test/js/samples/use-elements-as-anchors/expected.js
@@ -1,7 +1,7 @@
 import { appendNode, assign, createComment, createElement, createText, detachNode, dispatchObservers, insertNode, noop, proto } from "svelte/shared.js";
 
 function create_main_fragment ( state, component ) {
-	var div, text, p, text_1, text_2, text_3, text_4, p_1, text_5, text_6, text_7, text_8, if_block_4_anchor;
+	var div, text, p, text_1, text_2, text_3, text_4, p_1, text_5, text_6, text_8, if_block_4_anchor;
 
 	var if_block = (state.a) && create_if_block( state, component );
 
@@ -29,7 +29,6 @@ function create_main_fragment ( state, component ) {
 			text_5 = createText( "so can this" );
 			text_6 = createText( "\n\n\t" );
 			if ( if_block_3 ) if_block_3.create();
-			text_7 = createText( "\n\n\t" );
 			text_8 = createText( "\n\n" );
 			if ( if_block_4 ) if_block_4.create();
 			if_block_4_anchor = createComment();
@@ -50,7 +49,6 @@ function create_main_fragment ( state, component ) {
 			appendNode( text_5, p_1 );
 			appendNode( text_6, div );
 			if ( if_block_3 ) if_block_3.mount( div, null );
-			appendNode( text_7, div );
 			insertNode( text_8, target, anchor );
 			if ( if_block_4 ) if_block_4.mount( target, anchor );
 			insertNode( if_block_4_anchor, target, anchor );
@@ -97,7 +95,7 @@ function create_main_fragment ( state, component ) {
 				if ( !if_block_3 ) {
 					if_block_3 = create_if_block_3( state, component );
 					if_block_3.create();
-					if_block_3.mount( div, text_7 );
+					if_block_3.mount( div, null );
 				}
 			} else if ( if_block_3 ) {
 				if_block_3.unmount();
diff --git a/test/parser/samples/attribute-dynamic-boolean/output.json b/test/parser/samples/attribute-dynamic-boolean/output.json
--- a/test/parser/samples/attribute-dynamic-boolean/output.json
+++ b/test/parser/samples/attribute-dynamic-boolean/output.json
@@ -1,4 +1,5 @@
 {
+	"hash": 3179574701,
 	"html": {
 		"start": 0,
 		"end": 45,
@@ -21,9 +22,9 @@
 								"end": 32,
 								"type": "MustacheTag",
 								"expression": {
+									"type": "Identifier",
 									"start": 22,
 									"end": 30,
-									"type": "Identifier",
 									"name": "readonly"
 								}
 							}
@@ -36,4 +37,4 @@
 	},
 	"css": null,
 	"js": null
-}
+}
\ No newline at end of file
diff --git a/test/parser/samples/attribute-dynamic-reserved/output.json b/test/parser/samples/attribute-dynamic-reserved/output.json
--- a/test/parser/samples/attribute-dynamic-reserved/output.json
+++ b/test/parser/samples/attribute-dynamic-reserved/output.json
@@ -1,5 +1,5 @@
 {
-	"hash": 3305933215,
+	"hash": 2788845841,
 	"html": {
 		"start": 0,
 		"end": 29,
diff --git a/test/parser/samples/attribute-dynamic/output.json b/test/parser/samples/attribute-dynamic/output.json
--- a/test/parser/samples/attribute-dynamic/output.json
+++ b/test/parser/samples/attribute-dynamic/output.json
@@ -1,4 +1,5 @@
 {
+	"hash": 804348386,
 	"html": {
 		"start": 0,
 		"end": 46,
@@ -27,9 +28,9 @@
 								"end": 28,
 								"type": "MustacheTag",
 								"expression": {
+									"type": "Identifier",
 									"start": 21,
 									"end": 26,
-									"type": "Identifier",
 									"name": "color"
 								}
 							},
@@ -48,9 +49,9 @@
 						"end": 40,
 						"type": "MustacheTag",
 						"expression": {
+							"type": "Identifier",
 							"start": 33,
 							"end": 38,
-							"type": "Identifier",
 							"name": "color"
 						}
 					}
@@ -60,4 +61,4 @@
 	},
 	"css": null,
 	"js": null
-}
+}
\ No newline at end of file
diff --git a/test/parser/samples/attribute-escaped/output.json b/test/parser/samples/attribute-escaped/output.json
--- a/test/parser/samples/attribute-escaped/output.json
+++ b/test/parser/samples/attribute-escaped/output.json
@@ -1,4 +1,5 @@
 {
+	"hash": 1563956934,
 	"html": {
 		"start": 0,
 		"end": 41,
@@ -31,4 +32,4 @@
 	},
 	"css": null,
 	"js": null
-}
+}
\ No newline at end of file
diff --git a/test/parser/samples/attribute-multiple/output.json b/test/parser/samples/attribute-multiple/output.json
--- a/test/parser/samples/attribute-multiple/output.json
+++ b/test/parser/samples/attribute-multiple/output.json
@@ -1,4 +1,5 @@
 {
+	"hash": 507039402,
 	"html": {
 		"start": 0,
 		"end": 28,
@@ -45,4 +46,4 @@
 	},
 	"css": null,
 	"js": null
-}
+}
\ No newline at end of file
diff --git a/test/parser/samples/attribute-shorthand/output.json b/test/parser/samples/attribute-shorthand/output.json
--- a/test/parser/samples/attribute-shorthand/output.json
+++ b/test/parser/samples/attribute-shorthand/output.json
@@ -1,5 +1,5 @@
 {
-	"hash": 4120363214,
+	"hash": 1705925892,
 	"html": {
 		"start": 0,
 		"end": 10,
@@ -18,9 +18,9 @@
 						"name": "id",
 						"value": [
 							{
+								"type": "AttributeShorthand",
 								"start": 6,
 								"end": 8,
-								"type": "AttributeShorthand",
 								"expression": {
 									"type": "Identifier",
 									"start": 6,
diff --git a/test/parser/samples/attribute-static-boolean/output.json b/test/parser/samples/attribute-static-boolean/output.json
--- a/test/parser/samples/attribute-static-boolean/output.json
+++ b/test/parser/samples/attribute-static-boolean/output.json
@@ -1,4 +1,5 @@
 {
+	"hash": 606864228,
 	"html": {
 		"start": 0,
 		"end": 30,
@@ -24,4 +25,4 @@
 	},
 	"css": null,
 	"js": null
-}
+}
\ No newline at end of file
diff --git a/test/parser/samples/attribute-static/output.json b/test/parser/samples/attribute-static/output.json
--- a/test/parser/samples/attribute-static/output.json
+++ b/test/parser/samples/attribute-static/output.json
@@ -1,4 +1,5 @@
 {
+	"hash": 1493227373,
 	"html": {
 		"start": 0,
 		"end": 23,
@@ -31,4 +32,4 @@
 	},
 	"css": null,
 	"js": null
-}
+}
\ No newline at end of file
diff --git a/test/parser/samples/attribute-unquoted/output.json b/test/parser/samples/attribute-unquoted/output.json
--- a/test/parser/samples/attribute-unquoted/output.json
+++ b/test/parser/samples/attribute-unquoted/output.json
@@ -1,4 +1,5 @@
 {
+	"hash": 3488539025,
 	"html": {
 		"start": 0,
 		"end": 21,
@@ -31,4 +32,4 @@
 	},
 	"css": null,
 	"js": null
-}
+}
\ No newline at end of file
diff --git a/test/parser/samples/binding-shorthand/output.json b/test/parser/samples/binding-shorthand/output.json
--- a/test/parser/samples/binding-shorthand/output.json
+++ b/test/parser/samples/binding-shorthand/output.json
@@ -1,4 +1,5 @@
 {
+	"hash": 3088875001,
 	"html": {
 		"start": 0,
 		"end": 18,
@@ -29,4 +30,4 @@
 	},
 	"css": null,
 	"js": null
-}
+}
\ No newline at end of file
diff --git a/test/parser/samples/binding/output.json b/test/parser/samples/binding/output.json
--- a/test/parser/samples/binding/output.json
+++ b/test/parser/samples/binding/output.json
@@ -1,4 +1,5 @@
 {
+	"hash": 1937205193,
 	"html": {
 		"start": 0,
 		"end": 25,
@@ -16,9 +17,9 @@
 						"type": "Binding",
 						"name": "value",
 						"value": {
+							"type": "Identifier",
 							"start": 19,
 							"end": 23,
-							"type": "Identifier",
 							"name": "name"
 						}
 					}
@@ -29,4 +30,4 @@
 	},
 	"css": null,
 	"js": null
-}
+}
\ No newline at end of file
diff --git a/test/parser/samples/comment/output.json b/test/parser/samples/comment/output.json
--- a/test/parser/samples/comment/output.json
+++ b/test/parser/samples/comment/output.json
@@ -1,4 +1,5 @@
 {
+	"hash": 3294612990,
 	"html": {
 		"start": 0,
 		"end": 18,
@@ -14,4 +15,4 @@
 	},
 	"css": null,
 	"js": null
-}
+}
\ No newline at end of file
diff --git a/test/parser/samples/convert-entities-in-element/output.json b/test/parser/samples/convert-entities-in-element/output.json
--- a/test/parser/samples/convert-entities-in-element/output.json
+++ b/test/parser/samples/convert-entities-in-element/output.json
@@ -1,4 +1,5 @@
 {
+	"hash": 2365862121,
 	"html": {
 		"start": 0,
 		"end": 24,
@@ -23,4 +24,4 @@
 	},
 	"css": null,
 	"js": null
-}
+}
\ No newline at end of file
diff --git a/test/parser/samples/convert-entities/output.json b/test/parser/samples/convert-entities/output.json
--- a/test/parser/samples/convert-entities/output.json
+++ b/test/parser/samples/convert-entities/output.json
@@ -1,4 +1,5 @@
 {
+	"hash": 156753432,
 	"html": {
 		"start": 0,
 		"end": 17,
@@ -14,4 +15,4 @@
 	},
 	"css": null,
 	"js": null
-}
+}
\ No newline at end of file
diff --git a/test/parser/samples/css/output.json b/test/parser/samples/css/output.json
--- a/test/parser/samples/css/output.json
+++ b/test/parser/samples/css/output.json
@@ -1,4 +1,5 @@
 {
+	"hash": 1147407419,
 	"html": {
 		"start": 0,
 		"end": 14,
@@ -18,6 +19,12 @@
 						"data": "foo"
 					}
 				]
+			},
+			{
+				"start": 14,
+				"end": 16,
+				"type": "Text",
+				"data": "\n\n"
 			}
 		]
 	},
@@ -25,65 +32,65 @@
 		"start": 16,
 		"end": 56,
 		"attributes": [],
-		"content": {
-			"start": 23,
-			"end": 48,
-			"styles": "\n\tdiv {\n\t\tcolor: red;\n\t}\n"
-		},
 		"children": [
 			{
 				"type": "Rule",
-				"start": 25,
-				"end": 47,
 				"selector": {
 					"type": "SelectorList",
-					"start": 25,
-					"end": 28,
 					"children": [
 						{
 							"type": "Selector",
-							"start": 25,
-							"end": 28,
 							"children": [
 								{
 									"type": "TypeSelector",
+									"name": "div",
 									"start": 25,
-									"end": 28,
-									"name": "div"
+									"end": 28
 								}
-							]
+							],
+							"start": 25,
+							"end": 28
 						}
-					]
+					],
+					"start": 25,
+					"end": 28
 				},
 				"block": {
 					"type": "Block",
-					"start": 29,
-					"end": 47,
 					"children": [
 						{
 							"type": "Declaration",
-							"start": 33,
-							"end": 43,
 							"important": false,
 							"property": "color",
 							"value": {
 								"type": "Value",
-								"start": 39,
-								"end": 43,
 								"children": [
 									{
 										"type": "Identifier",
+										"name": "red",
 										"start": 40,
-										"end": 43,
-										"name": "red"
+										"end": 43
 									}
-								]
-							}
+								],
+								"start": 39,
+								"end": 43
+							},
+							"start": 33,
+							"end": 43
 						}
-					]
-				}
+					],
+					"start": 29,
+					"end": 47
+				},
+				"start": 25,
+				"end": 47
 			}
-		]
+		],
+		"content": {
+			"start": 23,
+			"end": 48,
+			"styles": "\n\tdiv {\n\t\tcolor: red;\n\t}\n"
+		}
 	},
 	"js": null
-}
+}
\ No newline at end of file
diff --git a/test/parser/samples/each-block-else/output.json b/test/parser/samples/each-block-else/output.json
--- a/test/parser/samples/each-block-else/output.json
+++ b/test/parser/samples/each-block-else/output.json
@@ -1,4 +1,5 @@
 {
+	"hash": 3238289871,
 	"html": {
 		"start": 0,
 		"end": 84,
@@ -9,12 +10,11 @@
 				"end": 84,
 				"type": "EachBlock",
 				"expression": {
+					"type": "Identifier",
 					"start": 8,
 					"end": 15,
-					"type": "Identifier",
 					"name": "animals"
 				},
-				"context": "animal",
 				"children": [
 					{
 						"start": 29,
@@ -28,40 +28,41 @@
 								"end": 42,
 								"type": "MustacheTag",
 								"expression": {
+									"type": "Identifier",
 									"start": 34,
 									"end": 40,
-									"type": "Identifier",
 									"name": "animal"
 								}
 							}
 						]
 					}
 				],
+				"context": "animal",
 				"else": {
+					"start": 55,
+					"end": 75,
+					"type": "ElseBlock",
 					"children": [
 						{
+							"start": 57,
+							"end": 74,
+							"type": "Element",
+							"name": "p",
 							"attributes": [],
 							"children": [
 								{
-									"data": "no animals",
-									"end": 70,
 									"start": 60,
-									"type": "Text"
+									"end": 70,
+									"type": "Text",
+									"data": "no animals"
 								}
-							],
-							"end": 74,
-							"name": "p",
-							"start": 57,
-							"type": "Element"
+							]
 						}
-					],
-					"end": 75,
-					"start": 55,
-					"type": "ElseBlock"
+					]
 				}
 			}
 		]
 	},
 	"css": null,
 	"js": null
-}
+}
\ No newline at end of file
diff --git a/test/parser/samples/each-block-indexed/output.json b/test/parser/samples/each-block-indexed/output.json
--- a/test/parser/samples/each-block-indexed/output.json
+++ b/test/parser/samples/each-block-indexed/output.json
@@ -1,4 +1,5 @@
 {
+	"hash": 2841674990,
 	"html": {
 		"start": 0,
 		"end": 66,
@@ -9,13 +10,11 @@
 				"end": 66,
 				"type": "EachBlock",
 				"expression": {
+					"type": "Identifier",
 					"start": 8,
 					"end": 15,
-					"type": "Identifier",
 					"name": "animals"
 				},
-				"context": "animal",
-				"index": "i",
 				"children": [
 					{
 						"start": 32,
@@ -29,9 +28,9 @@
 								"end": 40,
 								"type": "MustacheTag",
 								"expression": {
+									"type": "Identifier",
 									"start": 37,
 									"end": 38,
-									"type": "Identifier",
 									"name": "i"
 								}
 							},
@@ -46,18 +45,20 @@
 								"end": 52,
 								"type": "MustacheTag",
 								"expression": {
+									"type": "Identifier",
 									"start": 44,
 									"end": 50,
-									"type": "Identifier",
 									"name": "animal"
 								}
 							}
 						]
 					}
-				]
+				],
+				"context": "animal",
+				"index": "i"
 			}
 		]
 	},
 	"css": null,
 	"js": null
-}
+}
\ No newline at end of file
diff --git a/test/parser/samples/each-block-keyed/output.json b/test/parser/samples/each-block-keyed/output.json
--- a/test/parser/samples/each-block-keyed/output.json
+++ b/test/parser/samples/each-block-keyed/output.json
@@ -1,4 +1,5 @@
 {
+	"hash": 2025411181,
 	"html": {
 		"start": 0,
 		"end": 54,
@@ -9,13 +10,11 @@
 				"end": 54,
 				"type": "EachBlock",
 				"expression": {
+					"type": "Identifier",
 					"start": 8,
 					"end": 13,
-					"type": "Identifier",
 					"name": "todos"
 				},
-				"context": "todo",
-				"key": "id",
 				"children": [
 					{
 						"start": 29,
@@ -29,18 +28,20 @@
 								"end": 40,
 								"type": "MustacheTag",
 								"expression": {
+									"type": "Identifier",
 									"start": 34,
 									"end": 38,
-									"type": "Identifier",
 									"name": "todo"
 								}
 							}
 						]
 					}
-				]
+				],
+				"context": "todo",
+				"key": "id"
 			}
 		]
 	},
 	"css": null,
 	"js": null
-}
+}
\ No newline at end of file
diff --git a/test/parser/samples/each-block/output.json b/test/parser/samples/each-block/output.json
--- a/test/parser/samples/each-block/output.json
+++ b/test/parser/samples/each-block/output.json
@@ -1,4 +1,5 @@
 {
+	"hash": 220340986,
 	"html": {
 		"start": 0,
 		"end": 56,
@@ -9,12 +10,11 @@
 				"end": 56,
 				"type": "EachBlock",
 				"expression": {
+					"type": "Identifier",
 					"start": 8,
 					"end": 15,
-					"type": "Identifier",
 					"name": "animals"
 				},
-				"context": "animal",
 				"children": [
 					{
 						"start": 29,
@@ -28,18 +28,19 @@
 								"end": 42,
 								"type": "MustacheTag",
 								"expression": {
+									"type": "Identifier",
 									"start": 34,
 									"end": 40,
-									"type": "Identifier",
 									"name": "animal"
 								}
 							}
 						]
 					}
-				]
+				],
+				"context": "animal"
 			}
 		]
 	},
 	"css": null,
 	"js": null
-}
+}
\ No newline at end of file
diff --git a/test/parser/samples/element-with-mustache/output.json b/test/parser/samples/element-with-mustache/output.json
--- a/test/parser/samples/element-with-mustache/output.json
+++ b/test/parser/samples/element-with-mustache/output.json
@@ -1,4 +1,5 @@
 {
+	"hash": 1265376132,
 	"html": {
 		"start": 0,
 		"end": 24,
@@ -22,9 +23,9 @@
 						"end": 18,
 						"type": "MustacheTag",
 						"expression": {
+							"type": "Identifier",
 							"start": 12,
 							"end": 16,
-							"type": "Identifier",
 							"name": "name"
 						}
 					},
@@ -40,4 +41,4 @@
 	},
 	"css": null,
 	"js": null
-}
+}
\ No newline at end of file
diff --git a/test/parser/samples/element-with-text/output.json b/test/parser/samples/element-with-text/output.json
--- a/test/parser/samples/element-with-text/output.json
+++ b/test/parser/samples/element-with-text/output.json
@@ -1,4 +1,5 @@
 {
+	"hash": 611274658,
 	"html": {
 		"start": 0,
 		"end": 17,
@@ -23,4 +24,4 @@
 	},
 	"css": null,
 	"js": null
-}
+}
\ No newline at end of file
diff --git a/test/parser/samples/elements/output.json b/test/parser/samples/elements/output.json
--- a/test/parser/samples/elements/output.json
+++ b/test/parser/samples/elements/output.json
@@ -1,23 +1,28 @@
 {
-    "html": {
-        "start": 0,
-        "end": 15,
-        "type": "Fragment",
-        "children": [{
-            "attributes": [{
-                "end": 14,
-                "name": "html",
-                "start": 10,
-                "type": "Attribute",
-                "value": true
-            }],
-            "children": [],
-            "end": 15,
-            "name": "!doctype",
-            "start": 0,
-            "type": "Element"
-        }]
-    },
-    "css": null,
-    "js": null
-}
+	"hash": 825536165,
+	"html": {
+		"start": 0,
+		"end": 15,
+		"type": "Fragment",
+		"children": [
+			{
+				"start": 0,
+				"end": 15,
+				"type": "Element",
+				"name": "!doctype",
+				"attributes": [
+					{
+						"start": 10,
+						"end": 14,
+						"type": "Attribute",
+						"name": "html",
+						"value": true
+					}
+				],
+				"children": []
+			}
+		]
+	},
+	"css": null,
+	"js": null
+}
\ No newline at end of file
diff --git a/test/parser/samples/event-handler/output.json b/test/parser/samples/event-handler/output.json
--- a/test/parser/samples/event-handler/output.json
+++ b/test/parser/samples/event-handler/output.json
@@ -1,4 +1,5 @@
 {
+	"hash": 4260626221,
 	"html": {
 		"start": 0,
 		"end": 101,
@@ -16,48 +17,48 @@
 						"type": "EventHandler",
 						"name": "click",
 						"expression": {
+							"type": "CallExpression",
 							"start": 18,
 							"end": 44,
-							"type": "CallExpression",
 							"callee": {
+								"type": "Identifier",
 								"start": 18,
 								"end": 21,
-								"type": "Identifier",
 								"name": "set"
 							},
 							"arguments": [
 								{
+									"type": "ObjectExpression",
 									"start": 22,
 									"end": 43,
-									"type": "ObjectExpression",
 									"properties": [
 										{
+											"type": "Property",
 											"start": 24,
 											"end": 41,
-											"type": "Property",
-											"kind": "init",
-											"computed": false,
 											"method": false,
 											"shorthand": false,
+											"computed": false,
 											"key": {
+												"type": "Identifier",
 												"start": 24,
 												"end": 31,
-												"type": "Identifier",
 												"name": "visible"
 											},
 											"value": {
+												"type": "UnaryExpression",
 												"start": 33,
 												"end": 41,
-												"type": "UnaryExpression",
 												"operator": "!",
 												"prefix": true,
 												"argument": {
+													"type": "Identifier",
 													"start": 34,
 													"end": 41,
-													"type": "Identifier",
 													"name": "visible"
 												}
-											}
+											},
+											"kind": "init"
 										}
 									]
 								}
@@ -67,10 +68,10 @@
 				],
 				"children": [
 					{
-						"data": "toggle",
 						"start": 46,
 						"end": 52,
-						"type": "Text"
+						"type": "Text",
+						"data": "toggle"
 					}
 				]
 			},
@@ -85,10 +86,10 @@
 				"end": 101,
 				"type": "IfBlock",
 				"expression": {
-					"end": 76,
-					"name": "visible",
+					"type": "Identifier",
 					"start": 69,
-					"type": "Identifier"
+					"end": 76,
+					"name": "visible"
 				},
 				"children": [
 					{
@@ -99,10 +100,10 @@
 						"attributes": [],
 						"children": [
 							{
-								"data": "hello!",
-								"end": 89,
 								"start": 83,
-								"type": "Text"
+								"end": 89,
+								"type": "Text",
+								"data": "hello!"
 							}
 						]
 					}
@@ -112,4 +113,4 @@
 	},
 	"css": null,
 	"js": null
-}
+}
\ No newline at end of file
diff --git a/test/parser/samples/if-block-else/output.json b/test/parser/samples/if-block-else/output.json
--- a/test/parser/samples/if-block-else/output.json
+++ b/test/parser/samples/if-block-else/output.json
@@ -1,4 +1,5 @@
 {
+	"hash": 3134964533,
 	"html": {
 		"start": 0,
 		"end": 56,
@@ -9,9 +10,9 @@
 				"end": 56,
 				"type": "IfBlock",
 				"expression": {
+					"type": "Identifier",
 					"start": 6,
 					"end": 9,
-					"type": "Identifier",
 					"name": "foo"
 				},
 				"children": [
@@ -58,4 +59,4 @@
 	},
 	"css": null,
 	"js": null
-}
+}
\ No newline at end of file
diff --git a/test/parser/samples/if-block-elseif/output.json b/test/parser/samples/if-block-elseif/output.json
--- a/test/parser/samples/if-block-elseif/output.json
+++ b/test/parser/samples/if-block-elseif/output.json
@@ -1,4 +1,5 @@
 {
+	"hash": 985817334,
 	"html": {
 		"start": 0,
 		"end": 93,
@@ -9,20 +10,20 @@
 				"end": 93,
 				"type": "IfBlock",
 				"expression": {
+					"type": "BinaryExpression",
 					"start": 6,
 					"end": 12,
-					"type": "BinaryExpression",
-					"operator": ">",
 					"left": {
+						"type": "Identifier",
 						"start": 6,
 						"end": 7,
-						"type": "Identifier",
 						"name": "x"
 					},
+					"operator": ">",
 					"right": {
+						"type": "Literal",
 						"start": 10,
 						"end": 12,
-						"type": "Literal",
 						"value": 10,
 						"raw": "10"
 					}
@@ -55,20 +56,20 @@
 							"type": "IfBlock",
 							"elseif": true,
 							"expression": {
+								"type": "BinaryExpression",
 								"start": 53,
 								"end": 58,
-								"type": "BinaryExpression",
-								"operator": "<",
 								"left": {
+									"type": "Identifier",
 									"start": 53,
 									"end": 54,
-									"type": "Identifier",
 									"name": "x"
 								},
+								"operator": "<",
 								"right": {
+									"type": "Literal",
 									"start": 57,
 									"end": 58,
-									"type": "Literal",
 									"value": 5,
 									"raw": "5"
 								}
@@ -98,4 +99,4 @@
 	},
 	"css": null,
 	"js": null
-}
+}
\ No newline at end of file
diff --git a/test/parser/samples/if-block/output.json b/test/parser/samples/if-block/output.json
--- a/test/parser/samples/if-block/output.json
+++ b/test/parser/samples/if-block/output.json
@@ -1,4 +1,5 @@
 {
+	"hash": 2374871934,
 	"html": {
 		"start": 0,
 		"end": 21,
@@ -9,9 +10,9 @@
 				"end": 21,
 				"type": "IfBlock",
 				"expression": {
+					"type": "Identifier",
 					"start": 6,
 					"end": 9,
-					"type": "Identifier",
 					"name": "foo"
 				},
 				"children": [
@@ -27,4 +28,4 @@
 	},
 	"css": null,
 	"js": null
-}
+}
\ No newline at end of file
diff --git a/test/parser/samples/implicitly-closed-li/output.json b/test/parser/samples/implicitly-closed-li/output.json
--- a/test/parser/samples/implicitly-closed-li/output.json
+++ b/test/parser/samples/implicitly-closed-li/output.json
@@ -1,5 +1,5 @@
 {
-	"hash": 3806276940,
+	"hash": 126082492,
 	"html": {
 		"start": 0,
 		"end": 31,
@@ -12,6 +12,12 @@
 				"name": "ul",
 				"attributes": [],
 				"children": [
+					{
+						"start": 4,
+						"end": 6,
+						"type": "Text",
+						"data": "\n\t"
+					},
 					{
 						"start": 6,
 						"end": 13,
@@ -23,7 +29,7 @@
 								"start": 10,
 								"end": 13,
 								"type": "Text",
-								"data": "a"
+								"data": "a\n\t"
 							}
 						]
 					},
@@ -38,7 +44,7 @@
 								"start": 17,
 								"end": 20,
 								"type": "Text",
-								"data": "b"
+								"data": "b\n\t"
 							}
 						]
 					},
diff --git a/test/parser/samples/nbsp/output.json b/test/parser/samples/nbsp/output.json
--- a/test/parser/samples/nbsp/output.json
+++ b/test/parser/samples/nbsp/output.json
@@ -1,5 +1,5 @@
 {
-	"hash": 2678229240,
+	"hash": 4049070444,
 	"html": {
 		"start": 0,
 		"end": 19,
diff --git a/test/parser/samples/raw-mustaches/output.json b/test/parser/samples/raw-mustaches/output.json
--- a/test/parser/samples/raw-mustaches/output.json
+++ b/test/parser/samples/raw-mustaches/output.json
@@ -1,4 +1,5 @@
 {
+	"hash": 183399343,
 	"html": {
 		"start": 0,
 		"end": 30,
@@ -11,14 +12,20 @@
 				"name": "p",
 				"attributes": [],
 				"children": [
+					{
+						"start": 3,
+						"end": 4,
+						"type": "Text",
+						"data": " "
+					},
 					{
 						"start": 4,
 						"end": 14,
 						"type": "RawMustacheTag",
 						"expression": {
+							"type": "Identifier",
 							"start": 7,
 							"end": 11,
-							"type": "Identifier",
 							"name": "raw1"
 						}
 					},
@@ -33,11 +40,17 @@
 						"end": 25,
 						"type": "RawMustacheTag",
 						"expression": {
+							"type": "Identifier",
 							"start": 18,
 							"end": 22,
-							"type": "Identifier",
 							"name": "raw2"
 						}
+					},
+					{
+						"start": 25,
+						"end": 26,
+						"type": "Text",
+						"data": " "
 					}
 				]
 			}
@@ -45,4 +58,4 @@
 	},
 	"css": null,
 	"js": null
-}
+}
\ No newline at end of file
diff --git a/test/parser/samples/refs/output.json b/test/parser/samples/refs/output.json
--- a/test/parser/samples/refs/output.json
+++ b/test/parser/samples/refs/output.json
@@ -1,4 +1,5 @@
 {
+	"hash": 850398275,
 	"html": {
 		"start": 0,
 		"end": 25,
@@ -23,4 +24,4 @@
 	},
 	"css": null,
 	"js": null
-}
+}
\ No newline at end of file
diff --git a/test/parser/samples/script-comment-only/output.json b/test/parser/samples/script-comment-only/output.json
--- a/test/parser/samples/script-comment-only/output.json
+++ b/test/parser/samples/script-comment-only/output.json
@@ -1,4 +1,5 @@
 {
+	"hash": 3451341610,
 	"html": {
 		"start": 0,
 		"end": 11,
@@ -11,9 +12,15 @@
 				"name": "div",
 				"attributes": [],
 				"children": []
+			},
+			{
+				"start": 11,
+				"end": 13,
+				"type": "Text",
+				"data": "\n\n"
 			}
 		]
 	},
 	"css": null,
 	"js": null
-}
+}
\ No newline at end of file
diff --git a/test/parser/samples/script-comment-trailing-multiline/output.json b/test/parser/samples/script-comment-trailing-multiline/output.json
--- a/test/parser/samples/script-comment-trailing-multiline/output.json
+++ b/test/parser/samples/script-comment-trailing-multiline/output.json
@@ -1,4 +1,5 @@
 {
+	"hash": 1378757574,
 	"html": {
 		"start": 0,
 		"end": 24,
@@ -22,9 +23,9 @@
 						"end": 18,
 						"type": "MustacheTag",
 						"expression": {
+							"type": "Identifier",
 							"start": 12,
 							"end": 16,
-							"type": "Identifier",
 							"name": "name"
 						}
 					},
@@ -35,6 +36,12 @@
 						"data": "!"
 					}
 				]
+			},
+			{
+				"start": 24,
+				"end": 26,
+				"type": "Text",
+				"data": "\n\n"
 			}
 		]
 	},
@@ -49,73 +56,73 @@
 			"end": 134,
 			"body": [
 				{
+					"type": "ExportDefaultDeclaration",
+					"start": 36,
+					"end": 95,
 					"declaration": {
+						"type": "ObjectExpression",
 						"start": 51,
 						"end": 94,
-						"type": "ObjectExpression",
 						"properties": [
 							{
+								"type": "Property",
 								"start": 55,
 								"end": 91,
-								"type": "Property",
+								"method": false,
+								"shorthand": false,
 								"computed": false,
 								"key": {
+									"type": "Identifier",
 									"start": 55,
 									"end": 59,
-									"type": "Identifier",
 									"name": "data"
 								},
-								"kind": "init",
-								"method": false,
-								"shorthand": false,
 								"value": {
+									"type": "ArrowFunctionExpression",
 									"start": 61,
 									"end": 91,
-									"type": "ArrowFunctionExpression",
+									"id": null,
+									"generator": false,
+									"expression": true,
 									"async": false,
+									"params": [],
 									"body": {
+										"type": "ObjectExpression",
 										"start": 68,
 										"end": 90,
-										"type": "ObjectExpression",
 										"properties": [
 											{
+												"type": "Property",
 												"start": 73,
 												"end": 86,
-												"type": "Property",
+												"method": false,
+												"shorthand": false,
 												"computed": false,
 												"key": {
+													"type": "Identifier",
 													"start": 73,
 													"end": 77,
-													"type": "Identifier",
 													"name": "name"
 												},
-												"kind": "init",
-												"method": false,
-												"shorthand": false,
 												"value": {
+													"type": "Literal",
 													"start": 79,
 													"end": 86,
-													"type": "Literal",
-													"raw": "'world'",
-													"value": "world"
-												}
+													"value": "world",
+													"raw": "'world'"
+												},
+												"kind": "init"
 											}
 										]
-									},
-									"expression": true,
-									"generator": false,
-									"id": null,
-									"params": []
-								}
+									}
+								},
+								"kind": "init"
 							}
 						]
-					},
-					"end": 95,
-					"start": 36,
-					"type": "ExportDefaultDeclaration"
+					}
 				}
 			],
 			"sourceType": "module"
 		}
 	}
-}
+}
\ No newline at end of file
diff --git a/test/parser/samples/script-comment-trailing/output.json b/test/parser/samples/script-comment-trailing/output.json
--- a/test/parser/samples/script-comment-trailing/output.json
+++ b/test/parser/samples/script-comment-trailing/output.json
@@ -1,4 +1,5 @@
 {
+	"hash": 619854804,
 	"html": {
 		"start": 0,
 		"end": 24,
@@ -22,9 +23,9 @@
 						"end": 18,
 						"type": "MustacheTag",
 						"expression": {
+							"type": "Identifier",
 							"start": 12,
 							"end": 16,
-							"type": "Identifier",
 							"name": "name"
 						}
 					},
@@ -35,6 +36,12 @@
 						"data": "!"
 					}
 				]
+			},
+			{
+				"start": 24,
+				"end": 26,
+				"type": "Text",
+				"data": "\n\n"
 			}
 		]
 	},
@@ -49,73 +56,73 @@
 			"end": 123,
 			"body": [
 				{
+					"type": "ExportDefaultDeclaration",
+					"start": 36,
+					"end": 95,
 					"declaration": {
+						"type": "ObjectExpression",
 						"start": 51,
 						"end": 94,
-						"type": "ObjectExpression",
 						"properties": [
 							{
+								"type": "Property",
 								"start": 55,
 								"end": 91,
-								"type": "Property",
+								"method": false,
+								"shorthand": false,
 								"computed": false,
 								"key": {
+									"type": "Identifier",
 									"start": 55,
 									"end": 59,
-									"type": "Identifier",
 									"name": "data"
 								},
-								"kind": "init",
-								"method": false,
-								"shorthand": false,
 								"value": {
+									"type": "ArrowFunctionExpression",
 									"start": 61,
 									"end": 91,
-									"type": "ArrowFunctionExpression",
+									"id": null,
+									"generator": false,
+									"expression": true,
 									"async": false,
+									"params": [],
 									"body": {
+										"type": "ObjectExpression",
 										"start": 68,
 										"end": 90,
-										"type": "ObjectExpression",
 										"properties": [
 											{
+												"type": "Property",
 												"start": 73,
 												"end": 86,
-												"type": "Property",
+												"method": false,
+												"shorthand": false,
 												"computed": false,
 												"key": {
+													"type": "Identifier",
 													"start": 73,
 													"end": 77,
-													"type": "Identifier",
 													"name": "name"
 												},
-												"kind": "init",
-												"method": false,
-												"shorthand": false,
 												"value": {
+													"type": "Literal",
 													"start": 79,
 													"end": 86,
-													"type": "Literal",
-													"raw": "'world'",
-													"value": "world"
-												}
+													"value": "world",
+													"raw": "'world'"
+												},
+												"kind": "init"
 											}
 										]
-									},
-									"expression": true,
-									"generator": false,
-									"id": null,
-									"params": []
-								}
+									}
+								},
+								"kind": "init"
 							}
 						]
-					},
-					"end": 95,
-					"start": 36,
-					"type": "ExportDefaultDeclaration"
+					}
 				}
 			],
 			"sourceType": "module"
 		}
 	}
-}
+}
\ No newline at end of file
diff --git a/test/parser/samples/script/output.json b/test/parser/samples/script/output.json
--- a/test/parser/samples/script/output.json
+++ b/test/parser/samples/script/output.json
@@ -1,4 +1,5 @@
 {
+	"hash": 388108696,
 	"html": {
 		"start": 0,
 		"end": 24,
@@ -22,9 +23,9 @@
 						"end": 18,
 						"type": "MustacheTag",
 						"expression": {
+							"type": "Identifier",
 							"start": 12,
 							"end": 16,
-							"type": "Identifier",
 							"name": "name"
 						}
 					},
@@ -35,6 +36,12 @@
 						"data": "!"
 					}
 				]
+			},
+			{
+				"start": 24,
+				"end": 26,
+				"type": "Text",
+				"data": "\n\n"
 			}
 		]
 	},
@@ -49,73 +56,73 @@
 			"end": 96,
 			"body": [
 				{
+					"type": "ExportDefaultDeclaration",
+					"start": 36,
+					"end": 95,
 					"declaration": {
+						"type": "ObjectExpression",
 						"start": 51,
 						"end": 94,
-						"type": "ObjectExpression",
 						"properties": [
 							{
+								"type": "Property",
 								"start": 55,
 								"end": 91,
-								"type": "Property",
+								"method": false,
+								"shorthand": false,
 								"computed": false,
 								"key": {
+									"type": "Identifier",
 									"start": 55,
 									"end": 59,
-									"type": "Identifier",
 									"name": "data"
 								},
-								"kind": "init",
-								"method": false,
-								"shorthand": false,
 								"value": {
+									"type": "ArrowFunctionExpression",
 									"start": 61,
 									"end": 91,
-									"type": "ArrowFunctionExpression",
+									"id": null,
+									"generator": false,
+									"expression": true,
 									"async": false,
+									"params": [],
 									"body": {
+										"type": "ObjectExpression",
 										"start": 68,
 										"end": 90,
-										"type": "ObjectExpression",
 										"properties": [
 											{
+												"type": "Property",
 												"start": 73,
 												"end": 86,
-												"type": "Property",
+												"method": false,
+												"shorthand": false,
 												"computed": false,
 												"key": {
+													"type": "Identifier",
 													"start": 73,
 													"end": 77,
-													"type": "Identifier",
 													"name": "name"
 												},
-												"kind": "init",
-												"method": false,
-												"shorthand": false,
 												"value": {
+													"type": "Literal",
 													"start": 79,
 													"end": 86,
-													"type": "Literal",
-													"raw": "'world'",
-													"value": "world"
-												}
+													"value": "world",
+													"raw": "'world'"
+												},
+												"kind": "init"
 											}
 										]
-									},
-									"expression": true,
-									"generator": false,
-									"id": null,
-									"params": []
-								}
+									}
+								},
+								"kind": "init"
 							}
 						]
-					},
-					"end": 95,
-					"start": 36,
-					"type": "ExportDefaultDeclaration"
+					}
 				}
 			],
 			"sourceType": "module"
 		}
 	}
-}
+}
\ No newline at end of file
diff --git a/test/parser/samples/self-closing-element/output.json b/test/parser/samples/self-closing-element/output.json
--- a/test/parser/samples/self-closing-element/output.json
+++ b/test/parser/samples/self-closing-element/output.json
@@ -1,4 +1,5 @@
 {
+	"hash": 4200201687,
 	"html": {
 		"start": 0,
 		"end": 6,
@@ -16,4 +17,4 @@
 	},
 	"css": null,
 	"js": null
-}
+}
\ No newline at end of file
diff --git a/test/parser/samples/self-reference/output.json b/test/parser/samples/self-reference/output.json
--- a/test/parser/samples/self-reference/output.json
+++ b/test/parser/samples/self-reference/output.json
@@ -1,5 +1,5 @@
 {
-	"hash": 1792372370,
+	"hash": 216762188,
 	"html": {
 		"start": 0,
 		"end": 57,
diff --git a/test/parser/samples/space-between-mustaches/output.json b/test/parser/samples/space-between-mustaches/output.json
--- a/test/parser/samples/space-between-mustaches/output.json
+++ b/test/parser/samples/space-between-mustaches/output.json
@@ -1,4 +1,5 @@
 {
+	"hash": 1185019088,
 	"html": {
 		"start": 0,
 		"end": 30,
@@ -11,14 +12,20 @@
 				"name": "p",
 				"attributes": [],
 				"children": [
+					{
+						"start": 3,
+						"end": 4,
+						"type": "Text",
+						"data": " "
+					},
 					{
 						"start": 4,
 						"end": 9,
 						"type": "MustacheTag",
 						"expression": {
+							"type": "Identifier",
 							"start": 6,
 							"end": 7,
-							"type": "Identifier",
 							"name": "a"
 						}
 					},
@@ -33,9 +40,9 @@
 						"end": 15,
 						"type": "MustacheTag",
 						"expression": {
+							"type": "Identifier",
 							"start": 12,
 							"end": 13,
-							"type": "Identifier",
 							"name": "b"
 						}
 					},
@@ -50,9 +57,9 @@
 						"end": 23,
 						"type": "MustacheTag",
 						"expression": {
+							"type": "Identifier",
 							"start": 20,
 							"end": 21,
-							"type": "Identifier",
 							"name": "c"
 						}
 					},
@@ -60,7 +67,7 @@
 						"start": 23,
 						"end": 26,
 						"type": "Text",
-						"data": " :"
+						"data": " : "
 					}
 				]
 			}
@@ -68,4 +75,4 @@
 	},
 	"css": null,
 	"js": null
-}
+}
\ No newline at end of file
diff --git a/test/parser/samples/textarea-children/output.json b/test/parser/samples/textarea-children/output.json
--- a/test/parser/samples/textarea-children/output.json
+++ b/test/parser/samples/textarea-children/output.json
@@ -1,5 +1,5 @@
 {
-	"hash": 3618147195,
+	"hash": 2992234421,
 	"html": {
 		"start": 0,
 		"end": 63,
diff --git a/test/parser/samples/transition-intro-no-params/output.json b/test/parser/samples/transition-intro-no-params/output.json
--- a/test/parser/samples/transition-intro-no-params/output.json
+++ b/test/parser/samples/transition-intro-no-params/output.json
@@ -1,5 +1,5 @@
 {
-	"hash": 1535528483,
+	"hash": 503236647,
 	"html": {
 		"start": 0,
 		"end": 27,
diff --git a/test/parser/samples/transition-intro/output.json b/test/parser/samples/transition-intro/output.json
--- a/test/parser/samples/transition-intro/output.json
+++ b/test/parser/samples/transition-intro/output.json
@@ -1,5 +1,5 @@
 {
-	"hash": 3160753914,
+	"hash": 3731674194,
 	"html": {
 		"start": 0,
 		"end": 43,
@@ -19,18 +19,17 @@
 						"intro": true,
 						"outro": false,
 						"expression": {
-								"start": 15,
-							"end": 27,
 							"type": "ObjectExpression",
+							"start": 15,
+							"end": 27,
 							"properties": [
 								{
+									"type": "Property",
 									"start": 16,
 									"end": 26,
-									"type": "Property",
 									"method": false,
-									"computed": false,
 									"shorthand": false,
-									"kind": "init",
+									"computed": false,
 									"key": {
 										"type": "Identifier",
 										"start": 16,
@@ -38,12 +37,13 @@
 										"name": "opacity"
 									},
 									"value": {
+										"type": "Literal",
 										"start": 25,
 										"end": 26,
-										"type": "Literal",
 										"value": 0,
 										"raw": "0"
-									}
+									},
+									"kind": "init"
 								}
 							]
 						}
diff --git a/test/parser/samples/whitespace-leading-trailing/output.json b/test/parser/samples/whitespace-leading-trailing/output.json
--- a/test/parser/samples/whitespace-leading-trailing/output.json
+++ b/test/parser/samples/whitespace-leading-trailing/output.json
@@ -1,9 +1,16 @@
 {
+	"hash": 424837432,
 	"html": {
 		"start": 6,
 		"end": 36,
 		"type": "Fragment",
 		"children": [
+			{
+				"start": 0,
+				"end": 6,
+				"type": "Text",
+				"data": "\n\n\t\t\t\t"
+			},
 			{
 				"start": 6,
 				"end": 36,
@@ -23,4 +30,4 @@
 	},
 	"css": null,
 	"js": null
-}
+}
\ No newline at end of file
diff --git a/test/parser/samples/whitespace-normal/input.html b/test/parser/samples/whitespace-normal/input.html
new file mode 100644
--- /dev/null
+++ b/test/parser/samples/whitespace-normal/input.html
@@ -0,0 +1 @@
+<h1>Hello <strong>{{name}}! </strong><span>How are you?</span></h1>
diff --git a/test/parser/samples/whitespace-normal/output.json b/test/parser/samples/whitespace-normal/output.json
new file mode 100644
--- /dev/null
+++ b/test/parser/samples/whitespace-normal/output.json
@@ -0,0 +1,68 @@
+{
+	"hash": 2961389466,
+	"html": {
+		"start": 0,
+		"end": 67,
+		"type": "Fragment",
+		"children": [
+			{
+				"start": 0,
+				"end": 67,
+				"type": "Element",
+				"name": "h1",
+				"attributes": [],
+				"children": [
+					{
+						"start": 4,
+						"end": 10,
+						"type": "Text",
+						"data": "Hello "
+					},
+					{
+						"start": 10,
+						"end": 37,
+						"type": "Element",
+						"name": "strong",
+						"attributes": [],
+						"children": [
+							{
+								"start": 18,
+								"end": 26,
+								"type": "MustacheTag",
+								"expression": {
+									"type": "Identifier",
+									"start": 20,
+									"end": 24,
+									"name": "name"
+								}
+							},
+							{
+								"start": 26,
+								"end": 28,
+								"type": "Text",
+								"data": "! "
+							}
+						]
+					},
+					{
+						"start": 37,
+						"end": 62,
+						"type": "Element",
+						"name": "span",
+						"attributes": [],
+						"children": [
+							{
+								"start": 43,
+								"end": 55,
+								"type": "Text",
+								"data": "How are you?"
+							}
+						]
+					}
+				]
+			}
+		]
+	},
+	"css": null,
+	"js": null
+}
\ No newline at end of file
diff --git a/test/parser/samples/yield/output.json b/test/parser/samples/yield/output.json
--- a/test/parser/samples/yield/output.json
+++ b/test/parser/samples/yield/output.json
@@ -1,14 +1,17 @@
 {
-  "html": {
-    "start": 0,
-    "end": 9,
-    "type": "Fragment",
-    "children": [
-      {
-        "start": 0,
-        "end": 9,
-        "type": "YieldTag"
-      }
-    ]
-  }
-}
+	"hash": 3659433152,
+	"html": {
+		"start": 0,
+		"end": 9,
+		"type": "Fragment",
+		"children": [
+			{
+				"start": 0,
+				"end": 9,
+				"type": "YieldTag"
+			}
+		]
+	},
+	"css": null,
+	"js": null
+}
\ No newline at end of file
diff --git a/test/parser/update.js b/test/parser/update.js
new file mode 100644
--- /dev/null
+++ b/test/parser/update.js
@@ -0,0 +1,13 @@
+// this file will replace all the expected.js and expected-bundle.js files with
+// their _actual equivalents. Only use it when you're sure that you haven't
+// broken anything!
+const fs = require("fs");
+const glob = require("glob");
+
+glob.sync("samples/*/_actual.json", { cwd: __dirname }).forEach(file => {
+	const actual = fs.readFileSync(`${__dirname}/${file}`, "utf-8");
+	fs.writeFileSync(
+		`${__dirname}/${file.replace("_actual", "output")}`,
+		actual
+	);
+});
diff --git a/test/runtime/samples/whitespace-normal/_config.js b/test/runtime/samples/whitespace-normal/_config.js
new file mode 100644
--- /dev/null
+++ b/test/runtime/samples/whitespace-normal/_config.js
@@ -0,0 +1,12 @@
+export default {
+	data: {
+		name: 'world'
+	},
+
+	test ( assert, component, target ) {
+		assert.equal(
+			target.textContent,
+			`Hello world! How are you?`
+		);
+	}
+};
\ No newline at end of file
diff --git a/test/runtime/samples/whitespace-normal/main.html b/test/runtime/samples/whitespace-normal/main.html
new file mode 100644
--- /dev/null
+++ b/test/runtime/samples/whitespace-normal/main.html
@@ -0,0 +1 @@
+<h1>Hello <strong>{{name}}! </strong><span>How are you?</span></h1>
diff --git a/test/server-side-rendering/samples/component-data-dynamic/_actual.html b/test/server-side-rendering/samples/component-data-dynamic/_actual.html
--- a/test/server-side-rendering/samples/component-data-dynamic/_actual.html
+++ b/test/server-side-rendering/samples/component-data-dynamic/_actual.html
@@ -1,4 +1,6 @@
-<div><p>foo: lol</p>
+<div>
+	<p>foo: lol</p>
 <p>baz: 42 (number)</p>
 <p>qux: this is a piece of string</p>
-<p>quux: core</p></div>
\ No newline at end of file
+<p>quux: core</p>
+</div>
\ No newline at end of file
diff --git a/test/server-side-rendering/samples/component-data-empty/_actual.html b/test/server-side-rendering/samples/component-data-empty/_actual.html
--- a/test/server-side-rendering/samples/component-data-empty/_actual.html
+++ b/test/server-side-rendering/samples/component-data-empty/_actual.html
@@ -1 +1,3 @@
-<div><p>foo: ''</p></div>
\ No newline at end of file
+<div>
+	<p>foo: ''</p>
+</div>
\ No newline at end of file
diff --git a/test/server-side-rendering/samples/component-data-static/_actual.html b/test/server-side-rendering/samples/component-data-static/_actual.html
--- a/test/server-side-rendering/samples/component-data-static/_actual.html
+++ b/test/server-side-rendering/samples/component-data-static/_actual.html
@@ -1,2 +1,4 @@
-<div><p>foo: bar</p>
-<p>baz: 42 (number)</p></div>
\ No newline at end of file
+<div>
+	<p>foo: bar</p>
+<p>baz: 42 (number)</p>
+</div>
\ No newline at end of file
diff --git a/test/server-side-rendering/samples/component-yield/_actual.html b/test/server-side-rendering/samples/component-yield/_actual.html
--- a/test/server-side-rendering/samples/component-yield/_actual.html
+++ b/test/server-side-rendering/samples/component-yield/_actual.html
@@ -1 +1,3 @@
-<div><p>Hello</p></div>
\ No newline at end of file
+<div>
+	<p>Hello</p>
+</div>
\ No newline at end of file
diff --git a/test/server-side-rendering/samples/component/_actual.html b/test/server-side-rendering/samples/component/_actual.html
--- a/test/server-side-rendering/samples/component/_actual.html
+++ b/test/server-side-rendering/samples/component/_actual.html
@@ -1 +1,3 @@
-<div><p>i am a widget</p></div>
\ No newline at end of file
+<div>
+	<p>i am a widget</p>
+</div>
\ No newline at end of file