From 2f62a5aacad654894a14f2998c76fbe301bac489 Mon Sep 17 00:00:00 2001
From: Rich-Harris <richard.a.harris@gmail.com>
Date: Sat, 19 Nov 2016 10:59:11 -0500
Subject: [PATCH] more whitespace stuff

---
 compiler/parse/index.js                       | 44 ++++++++++--
 compiler/parse/patterns.js                    |  1 +
 compiler/parse/state/fragment.js              |  2 -
 compiler/parse/state/mustache.js              | 28 ++++++--
 compiler/parse/state/tag.js                   | 21 +++++-
 compiler/parse/state/text.js                  |  2 +-
 compiler/parse/utils/trim.js                  | 15 ++++
 test/compiler/computed-values/_config.js      | 11 +++
 test/compiler/computed-values/main.svelte     | 16 +++++
 test/compiler/custom-method/_config.js        |  6 +-
 test/compiler/event-handler/_config.js        |  6 +-
 test/parser/event-handler/output.json         |  6 ++
 .../space-between-mustaches/input.svelte      |  1 +
 .../space-between-mustaches/output.json       | 71 +++++++++++++++++++
 14 files changed, 210 insertions(+), 20 deletions(-)
 create mode 100644 compiler/parse/patterns.js
 create mode 100644 compiler/parse/utils/trim.js
 create mode 100644 test/compiler/computed-values/_config.js
 create mode 100644 test/compiler/computed-values/main.svelte
 create mode 100644 test/parser/space-between-mustaches/input.svelte
 create mode 100644 test/parser/space-between-mustaches/output.json

diff --git a/compiler/parse/index.js b/compiler/parse/index.js
index 264211ae10..4e1e2ca1bc 100644
--- a/compiler/parse/index.js
+++ b/compiler/parse/index.js
@@ -1,7 +1,7 @@
 import { locate } from 'locate-character';
 import fragment from './state/fragment.js';
-
-const whitespace = /\s/;
+import { whitespace } from './patterns.js';
+import { trimStart, trimEnd } from './utils/trim.js';
 
 export default function parse ( template ) {
 	const parser = {
@@ -14,7 +14,7 @@ export default function parse ( template ) {
 		},
 
 		error ( message, index = this.index ) {
-			const { line, column } = locate( this.template, this.index );
+			const { line, column } = locate( this.template, index );
 			throw new Error( `${message} (${line}:${column})` );
 		},
 
@@ -66,7 +66,7 @@ export default function parse ( template ) {
 		},
 
 		html: {
-			start: 0,
+			start: null,
 			end: null,
 			type: 'Fragment',
 			children: []
@@ -85,8 +85,40 @@ export default function parse ( template ) {
 		state = state( parser ) || fragment;
 	}
 
-	const lastTemplateItem = parser.html.children[ parser.html.children.length - 1 ];
-	parser.html.end = lastTemplateItem ? lastTemplateItem.end : 0;
+	// trim unnecessary whitespace
+	while ( parser.html.children.length ) {
+		const firstChild = parser.html.children[0];
+		parser.html.start = firstChild.start;
+
+		if ( firstChild.type !== 'Text' ) break;
+
+		const length = firstChild.data.length;
+		firstChild.data = trimStart( firstChild.data );
+
+		if ( firstChild.data === '' ) {
+			parser.html.children.shift();
+		} else {
+			parser.html.start += length - firstChild.data.length;
+			break;
+		}
+	}
+
+	while ( parser.html.children.length ) {
+		const lastChild = parser.html.children[ parser.html.children.length - 1 ];
+		parser.html.end = lastChild.end;
+
+		if ( lastChild.type !== 'Text' ) break;
+
+		const length = lastChild.data.length;
+		lastChild.data = trimEnd( lastChild.data );
+
+		if ( lastChild.data === '' ) {
+			parser.html.children.pop();
+		} else {
+			parser.html.end -= length - lastChild.data.length;
+			break;
+		}
+	}
 
 	return {
 		html: parser.html,
diff --git a/compiler/parse/patterns.js b/compiler/parse/patterns.js
new file mode 100644
index 0000000000..313f40a464
--- /dev/null
+++ b/compiler/parse/patterns.js
@@ -0,0 +1 @@
+export const whitespace = /\s/;
diff --git a/compiler/parse/state/fragment.js b/compiler/parse/state/fragment.js
index 59868a3270..100e28687d 100644
--- a/compiler/parse/state/fragment.js
+++ b/compiler/parse/state/fragment.js
@@ -3,8 +3,6 @@ import mustache from './mustache.js';
 import text from './text.js';
 
 export default function fragment ( parser ) {
-	parser.allowWhitespace();
-
 	if ( parser.match( '<' ) ) {
 		return tag;
 	}
diff --git a/compiler/parse/state/mustache.js b/compiler/parse/state/mustache.js
index abe7dac84f..beba91da12 100644
--- a/compiler/parse/state/mustache.js
+++ b/compiler/parse/state/mustache.js
@@ -1,4 +1,6 @@
 import readExpression from '../read/expression.js';
+import { whitespace } from '../patterns.js';
+import { trimStart, trimEnd } from '../utils/trim.js';
 
 const validIdentifier = /[a-zA-Z_$][a-zA-Z0-9_$]*/;
 
@@ -10,12 +12,12 @@ export default function mustache ( parser ) {
 
 	// {{/if}} or {{/each}}
 	if ( parser.eat( '/' ) ) {
-		const current = parser.current();
+		const block = parser.current();
 		let expected;
 
-		if ( current.type === 'IfBlock' ) {
+		if ( block.type === 'IfBlock' ) {
 			expected = 'if';
-		} else if ( current.type === 'EachBlock' ) {
+		} else if ( block.type === 'EachBlock' ) {
 			expected = 'each';
 		} else {
 			parser.error( `Unexpected block closing tag` );
@@ -25,7 +27,25 @@ export default function mustache ( parser ) {
 		parser.allowWhitespace();
 		parser.eat( '}}', true );
 
-		current.end = parser.index;
+		// strip leading/trailing whitespace as necessary
+		if ( !block.children.length ) parser.error( `Empty block`, block.start );
+		const firstChild = block.children[0];
+		const lastChild = block.children[ block.children.length - 1 ];
+
+		const charBefore = parser.template[ block.start - 1 ];
+		const charAfter = parser.template[ parser.index ];
+
+		if ( firstChild.type === 'Text' && !charBefore || whitespace.test( charBefore ) ) {
+			firstChild.data = trimStart( firstChild.data );
+			if ( !firstChild.data ) block.children.shift();
+		}
+
+		if ( lastChild.type === 'Text' && !charAfter || whitespace.test( charAfter ) ) {
+			lastChild.data = trimEnd( lastChild.data );
+			if ( !lastChild.data ) block.children.pop();
+		}
+
+		block.end = parser.index;
 		parser.stack.pop();
 	}
 
diff --git a/compiler/parse/state/tag.js b/compiler/parse/state/tag.js
index 4aec2e79f4..bb88197696 100644
--- a/compiler/parse/state/tag.js
+++ b/compiler/parse/state/tag.js
@@ -2,6 +2,7 @@ import readExpression from '../read/expression.js';
 import readScript from '../read/script.js';
 import readStyle from '../read/style.js';
 import { readEventHandlerDirective } from '../read/directives.js';
+import { trimStart, trimEnd } from '../utils/trim.js';
 
 const validTagName = /^[a-zA-Z]{1,}:?[a-zA-Z0-9\-]*/;
 const voidElementNames = /^(?:area|base|br|col|command|doctype|embed|hr|img|input|keygen|link|meta|param|source|track|wbr)$/i;
@@ -32,7 +33,25 @@ export default function tag ( parser ) {
 	if ( isClosingTag ) {
 		if ( !parser.eat( '>' ) ) parser.error( `Expected '>'` );
 
-		parser.current().end = parser.index;
+		const element = parser.current();
+
+		// strip leading/trailing whitespace as necessary
+		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();
+			}
+		}
+
+		element.end = parser.index;
 		parser.stack.pop();
 
 		return null;
diff --git a/compiler/parse/state/text.js b/compiler/parse/state/text.js
index 51ccffa74d..8a0c9046a7 100644
--- a/compiler/parse/state/text.js
+++ b/compiler/parse/state/text.js
@@ -3,7 +3,7 @@ export default function text ( parser ) {
 
 	let data = '';
 
-	while ( !parser.match( '<' ) && !parser.match( '{{' ) ) {
+	while ( parser.index < parser.template.length && !parser.match( '<' ) && !parser.match( '{{' ) ) {
 		data += parser.template[ parser.index++ ];
 	}
 
diff --git a/compiler/parse/utils/trim.js b/compiler/parse/utils/trim.js
new file mode 100644
index 0000000000..012203cd22
--- /dev/null
+++ b/compiler/parse/utils/trim.js
@@ -0,0 +1,15 @@
+import { whitespace } from '../patterns.js';
+
+export function trimStart ( str ) {
+	let i = 0;
+	while ( whitespace.test( str[i] ) ) i += 1;
+
+	return str.slice( i );
+}
+
+export function trimEnd ( str ) {
+	let i = str.length;
+	while ( whitespace.test( str[ i - 1 ] ) ) i -= 1;
+
+	return str.slice( 0, i );
+}
diff --git a/test/compiler/computed-values/_config.js b/test/compiler/computed-values/_config.js
new file mode 100644
index 0000000000..3d55fa3154
--- /dev/null
+++ b/test/compiler/computed-values/_config.js
@@ -0,0 +1,11 @@
+import * as assert from 'assert';
+
+export default {
+	html: '<p>1 + 2 = 3</p><p>3 * 3 = 9</p>',
+	test ( component, target ) {
+		component.set({ a: 3 });
+		assert.equal( component.get( 'c' ), 5 );
+		assert.equal( component.get( 'cSquared' ), 25 );
+		assert.equal( target.innerHTML, '<p>3 + 2 = 5</p><p>5 * 5 = 25</p>' );
+	}
+};
diff --git a/test/compiler/computed-values/main.svelte b/test/compiler/computed-values/main.svelte
new file mode 100644
index 0000000000..0cee54f867
--- /dev/null
+++ b/test/compiler/computed-values/main.svelte
@@ -0,0 +1,16 @@
+<p>{{a}} + {{b}} = {{c}}</p>
+<p>{{c}} * {{c}} = {{cSquared}}</p>
+
+<script>
+	export default {
+		data: () => ({
+			a: 1,
+			b: 2
+		}),
+
+		computed: {
+			c: ( a, b ) => a + b,
+			cSquared: c => c * c
+		}
+	};
+</script>
diff --git a/test/compiler/custom-method/_config.js b/test/compiler/custom-method/_config.js
index 2931baf74c..a2a4882737 100644
--- a/test/compiler/custom-method/_config.js
+++ b/test/compiler/custom-method/_config.js
@@ -1,18 +1,18 @@
 import * as assert from 'assert';
 
 export default {
-	html: '<button>+1</button><p>0</p>',
+	html: '<button>+1</button>\n\n<p>0</p>',
 	test ( component, target, window ) {
 		const button = target.querySelector( 'button' );
 		const event = new window.MouseEvent( 'click' );
 
 		button.dispatchEvent( event );
 		assert.equal( component.get( 'counter' ), 1 );
-		assert.equal( target.innerHTML, '<button>+1</button><p>1</p>' );
+		assert.equal( target.innerHTML, '<button>+1</button>\n\n<p>1</p>' );
 
 		button.dispatchEvent( event );
 		assert.equal( component.get( 'counter' ), 2 );
-		assert.equal( target.innerHTML, '<button>+1</button><p>2</p>' );
+		assert.equal( target.innerHTML, '<button>+1</button>\n\n<p>2</p>' );
 
 		assert.equal( component.foo(), 42 );
 	}
diff --git a/test/compiler/event-handler/_config.js b/test/compiler/event-handler/_config.js
index b3d6b5e1b0..13cec519da 100644
--- a/test/compiler/event-handler/_config.js
+++ b/test/compiler/event-handler/_config.js
@@ -1,15 +1,15 @@
 import * as assert from 'assert';
 
 export default {
-	html: '<button>toggle</button><!--#if visible-->',
+	html: '<button>toggle</button>\n\n<!--#if visible-->',
 	test ( component, target, window ) {
 		const button = target.querySelector( 'button' );
 		const event = new window.MouseEvent( 'click' );
 
 		button.dispatchEvent( event );
-		assert.equal( target.innerHTML, '<button>toggle</button><p>hello!</p><!--#if visible-->' );
+		assert.equal( target.innerHTML, '<button>toggle</button>\n\n<p>hello!</p><!--#if visible-->' );
 
 		button.dispatchEvent( event );
-		assert.equal( target.innerHTML, '<button>toggle</button><!--#if visible-->' );
+		assert.equal( target.innerHTML, '<button>toggle</button>\n\n<!--#if visible-->' );
 	}
 };
diff --git a/test/parser/event-handler/output.json b/test/parser/event-handler/output.json
index 185abdea11..3712a5d130 100644
--- a/test/parser/event-handler/output.json
+++ b/test/parser/event-handler/output.json
@@ -72,6 +72,12 @@
 					}
 				]
 			},
+			{
+				"start": 61,
+				"end": 63,
+				"type": "Text",
+				"data": "\n\n"
+			},
 			{
 				"start": 63,
 				"end": 101,
diff --git a/test/parser/space-between-mustaches/input.svelte b/test/parser/space-between-mustaches/input.svelte
new file mode 100644
index 0000000000..465a43e5f1
--- /dev/null
+++ b/test/parser/space-between-mustaches/input.svelte
@@ -0,0 +1 @@
+<p> {{a}} {{b}} : {{c}} : </p>
diff --git a/test/parser/space-between-mustaches/output.json b/test/parser/space-between-mustaches/output.json
new file mode 100644
index 0000000000..03a01a64c4
--- /dev/null
+++ b/test/parser/space-between-mustaches/output.json
@@ -0,0 +1,71 @@
+{
+	"html": {
+		"start": 0,
+		"end": 30,
+		"type": "Fragment",
+		"children": [
+			{
+				"start": 0,
+				"end": 30,
+				"type": "Element",
+				"name": "p",
+				"attributes": [],
+				"children": [
+					{
+						"start": 4,
+						"end": 9,
+						"type": "MustacheTag",
+						"expression": {
+							"start": 6,
+							"end": 7,
+							"type": "Identifier",
+							"name": "a"
+						}
+					},
+					{
+						"start": 9,
+						"end": 10,
+						"type": "Text",
+						"data": " "
+					},
+					{
+						"start": 10,
+						"end": 15,
+						"type": "MustacheTag",
+						"expression": {
+							"start": 12,
+							"end": 13,
+							"type": "Identifier",
+							"name": "b"
+						}
+					},
+					{
+						"start": 15,
+						"end": 18,
+						"type": "Text",
+						"data": " : "
+					},
+					{
+						"start": 18,
+						"end": 23,
+						"type": "MustacheTag",
+						"expression": {
+							"start": 20,
+							"end": 21,
+							"type": "Identifier",
+							"name": "c"
+						}
+					},
+					{
+						"start": 23,
+						"end": 26,
+						"type": "Text",
+						"data": " :"
+					}
+				]
+			}
+		]
+	},
+	"css": null,
+	"js": null
+}