diff --git a/src/generators/nodes/Component.ts b/src/generators/nodes/Component.ts index 661bfd7d15..1fc249e960 100644 --- a/src/generators/nodes/Component.ts +++ b/src/generators/nodes/Component.ts @@ -137,27 +137,21 @@ export default class Component extends Node { block.addVariable(name_updating, '{}'); statements.push(`var ${name_initial_data} = ${initialPropString};`); - const setParentFromChildOnChange = new CodeBuilder(); - const setParentFromChildOnInit = new CodeBuilder(); + let hasLocalBindings = false; + let hasStoreBindings = false; - const setStoreFromChildOnChange = new CodeBuilder(); - const setStoreFromChildOnInit = new CodeBuilder(); + const builder = new CodeBuilder(); bindings.forEach((binding: Binding) => { let { name: key } = getObject(binding.value); - const isStoreProp = generator.options.store && key[0] === '$'; - if (isStoreProp) key = key.slice(1); - const newState = isStoreProp ? 'newStoreState' : 'newState'; - binding.contexts.forEach(context => { allContexts.add(context); }); let setFromChild; - if (!isStoreProp && block.contexts.has(key)) { - const prop = binding.dependencies[0]; + if (block.contexts.has(key)) { const computed = isComputed(binding.value); const tail = binding.value.type === 'MemberExpression' ? getTailSnippet(binding.value) : ''; @@ -167,20 +161,38 @@ export default class Component extends Node { list[index]${tail} = childState.${binding.name}; ${binding.dependencies - .map((prop: string) => `${newState}.${prop} = state.${prop};`) - .join('\n')} - `; - } + .map((name: string) => { + const isStoreProp = generator.options.store && name[0] === '$'; + const prop = isStoreProp ? name.slice(1) : name; + const newState = isStoreProp ? 'newStoreState' : 'newState'; - else if (binding.value.type === 'MemberExpression') { - setFromChild = deindent` - ${binding.snippet} = childState.${binding.name}; - ${binding.dependencies.map((prop: string) => `${newState}.${prop} = state.${prop};`).join('\n')} + if (isStoreProp) hasStoreBindings = true; + else hasLocalBindings = true; + + return `${newState}.${prop} = state.${name};`; + }) + .join('\n')} `; } else { - setFromChild = `${newState}.${key} = childState.${binding.name};`; + const isStoreProp = generator.options.store && key[0] === '$'; + const prop = isStoreProp ? key.slice(1) : key; + const newState = isStoreProp ? 'newStoreState' : 'newState'; + + if (isStoreProp) hasStoreBindings = true; + else hasLocalBindings = true; + + if (binding.value.type === 'MemberExpression') { + setFromChild = deindent` + ${binding.snippet} = childState.${binding.name}; + ${newState}.${prop} = state.${key}; + `; + } + + else { + setFromChild = `${newState}.${prop} = childState.${binding.name};`; + } } statements.push(deindent` @@ -190,16 +202,11 @@ export default class Component extends Node { }` ); - (isStoreProp ? setStoreFromChildOnChange : setParentFromChildOnChange).addConditional( + builder.addConditional( `!${name_updating}.${binding.name} && changed.${binding.name}`, setFromChild ); - (isStoreProp ? setStoreFromChildOnInit : setParentFromChildOnInit).addConditional( - `!${name_updating}.${binding.name}`, - setFromChild - ); - // TODO could binding.dependencies.length ever be 0? if (binding.dependencies.length) { updates.push(deindent` @@ -215,44 +222,23 @@ export default class Component extends Node { const initialisers = [ 'state = #component.get()', - !setParentFromChildOnChange.isEmpty() && 'newState = {}', - !setStoreFromChildOnChange.isEmpty() && 'newStoreState = {}', + hasLocalBindings && 'newState = {}', + hasStoreBindings && 'newStoreState = {}', ].filter(Boolean).join(', '); componentInitProperties.push(deindent` _bind: function(changed, childState) { var ${initialisers}; - ${!setStoreFromChildOnChange.isEmpty() && deindent` - ${setStoreFromChildOnChange} - ${name_updating} = @assign({}, changed); - #component.store.set(newStoreState); - `} - ${!setParentFromChildOnChange.isEmpty() && deindent` - ${setParentFromChildOnChange} - ${name_updating} = @assign({}, changed); - #component._set(newState); - `} + ${builder} + ${hasStoreBindings && `#component.store.set(newStoreState);`} + ${hasLocalBindings && `#component._set(newState);`} ${name_updating} = {}; } `); - // TODO can `!childState` ever be true? beforecreate = deindent` #component.root._beforecreate.push(function() { - var childState = ${name}.get(), ${initialisers}; - if (!childState) return; - ${setParentFromChildOnInit} - ${!setStoreFromChildOnInit.isEmpty() && deindent` - ${setStoreFromChildOnInit} - ${name_updating} = { ${bindings.map((binding: Binding) => `${binding.name}: true`).join(', ')} }; - #component.store.set(newStoreState); - `} - ${!setParentFromChildOnInit.isEmpty() && deindent` - ${setParentFromChildOnInit} - ${name_updating} = { ${bindings.map((binding: Binding) => `${binding.name}: true`).join(', ')} }; - #component._set(newState); - `} - ${name_updating} = {}; + ${name}._bind({ ${bindings.map(b => `${b.name}: 1`).join(', ')} }, ${name}.get()); }); `; } else if (initialProps.length) { diff --git a/src/parse/index.ts b/src/parse/index.ts index c81d2f61b0..91d7c3253d 100644 --- a/src/parse/index.ts +++ b/src/parse/index.ts @@ -112,14 +112,14 @@ export class Parser { throw new ParseError(message, this.template, index, this.filename); } - eat(str: string, required?: boolean) { + eat(str: string, required?: boolean, message?: string) { if (this.match(str)) { this.index += str.length; return true; } if (required) { - this.error(`Expected ${str}`); + this.error(message || `Expected ${str}`); } return false; diff --git a/src/parse/state/mustache.ts b/src/parse/state/mustache.ts index bd1db07460..0a255b1256 100644 --- a/src/parse/state/mustache.ts +++ b/src/parse/state/mustache.ts @@ -6,7 +6,7 @@ import { Parser } from '../index'; import { Node } from '../../interfaces'; function trimWhitespace(block: Node, trimBefore: boolean, trimAfter: boolean) { - if (!block.children) return; // AwaitBlock + if (!block.children || block.children.length === 0) return; // AwaitBlock const firstChild = block.children[0]; const lastChild = block.children[block.children.length - 1]; @@ -74,8 +74,6 @@ export default function mustache(parser: Parser) { } // strip leading/trailing whitespace as necessary - if (block.children && !block.children.length) parser.error(`Empty block`, block.start); - const charBefore = parser.template[block.start - 1]; const charAfter = parser.template[parser.index]; const trimBefore = !charBefore || whitespace.test(charBefore); diff --git a/src/parse/state/tag.ts b/src/parse/state/tag.ts index 8b701cae1d..57e2b1bbb9 100644 --- a/src/parse/state/tag.ts +++ b/src/parse/state/tag.ts @@ -71,7 +71,7 @@ export default function tag(parser: Parser) { if (parser.eat('!--')) { const data = parser.readUntil(/-->/); - parser.eat('-->'); + parser.eat('-->', true, 'comment was left open, expected -->'); parser.current().children.push({ start, diff --git a/src/validate/html/index.ts b/src/validate/html/index.ts index b2a190696f..9ae50baca1 100644 --- a/src/validate/html/index.ts +++ b/src/validate/html/index.ts @@ -12,6 +12,13 @@ const meta = new Map([ [':Head', validateHead] ]); +function isEmptyBlock(node: Node) { + if (!/Block$/.test(node.type) || !node.children) return false; + if (node.children.length > 1) return false; + const child = node.children[0]; + return !child || (child.type === 'Text' && !/\S/.test(child.data)); +} + export default function validateHtml(validator: Validator, html: Node) { const refs = new Map(); const refCallees: Node[] = []; @@ -58,6 +65,10 @@ export default function validateHtml(validator: Validator, html: Node) { } } + if (validator.options.dev && isEmptyBlock(node)) { + validator.warn('Empty block', node.start); + } + if (node.children) { if (node.type === 'Element') elementStack.push(node); stack.push(node); diff --git a/src/validate/index.ts b/src/validate/index.ts index b57f2b0c2a..da603a437b 100644 --- a/src/validate/index.ts +++ b/src/validate/index.ts @@ -93,7 +93,7 @@ export default function validate( stylesheet: Stylesheet, options: CompileOptions ) { - const { onwarn, onerror, name, filename, store } = options; + const { onwarn, onerror, name, filename, store, dev } = options; try { if (name && !/^[a-zA-Z_$][a-zA-Z_$0-9]*$/.test(name)) { @@ -114,7 +114,8 @@ export default function validate( onwarn, name, filename, - store + store, + dev }); if (parsed.js) { diff --git a/test/parser/samples/error-comment-unclosed/error.json b/test/parser/samples/error-comment-unclosed/error.json new file mode 100644 index 0000000000..f392e12fa1 --- /dev/null +++ b/test/parser/samples/error-comment-unclosed/error.json @@ -0,0 +1,8 @@ +{ + "message": "comment was left open, expected -->", + "loc": { + "line": 1, + "column": 24 + }, + "pos": 24 +} diff --git a/test/parser/samples/error-comment-unclosed/input.html b/test/parser/samples/error-comment-unclosed/input.html new file mode 100644 index 0000000000..725cf9be60 --- /dev/null +++ b/test/parser/samples/error-comment-unclosed/input.html @@ -0,0 +1 @@ +