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 +}