From c79b38ff6a03306d2408cb1e713e1e339ac53f92 Mon Sep 17 00:00:00 2001 From: Rich-Harris Date: Tue, 22 Nov 2016 12:31:04 -0500 Subject: [PATCH] implement else blocks --- compiler/generate/index.js | 57 ++++++++++------- compiler/generate/visitors/ElseBlock.js | 23 +++++++ compiler/generate/visitors/IfBlock.js | 81 +++++++++++++++---------- compiler/generate/visitors/index.js | 2 + compiler/parse/state/mustache.js | 30 ++++++++- test/compiler/if-block-else/_config.js | 8 +-- 6 files changed, 143 insertions(+), 58 deletions(-) create mode 100644 compiler/generate/visitors/ElseBlock.js diff --git a/compiler/generate/index.js b/compiler/generate/index.js index 75ad1b87fe..3a064915b9 100644 --- a/compiler/generate/index.js +++ b/compiler/generate/index.js @@ -98,9 +98,39 @@ export default function generate ( parsed, source, options = {} ) { helpers: {}, + pop () { + const tail = generator.current; + generator.current = tail.parent; + + return tail; + }, + + push ( fragment ) { + const newFragment = Object.assign( {}, generator.current, fragment, { + parent: generator.current + }); + + generator.current = newFragment; + }, + usesRefs: false, - source + source, + + visit ( node ) { + const visitor = visitors[ node.type ]; + if ( !visitor ) throw new Error( `Not implemented: ${node.type}` ); + + if ( visitor.enter ) visitor.enter( generator, node ); + + if ( node.children ) { + node.children.forEach( child => { + generator.visit( child ); + }); + } + + if ( visitor.leave ) visitor.leave( generator, node ); + } }; const templateProperties = {}; @@ -161,7 +191,7 @@ export default function generate ( parsed, source, options = {} ) { }); } - generator.current = { + generator.push({ useAnchor: false, name: 'renderMainFragment', namespace: null, @@ -179,27 +209,12 @@ export default function generate ( parsed, source, options = {} ) { indexNames: {}, listNames: {}, - counter: counter(), - - parent: null - }; - - parsed.html.children.forEach( function visit ( node ) { - const visitor = visitors[ node.type ]; - if ( !visitor ) throw new Error( `Not implemented: ${node.type}` ); - - if ( visitor.enter ) visitor.enter( generator, node ); - - if ( node.children ) { - node.children.forEach( child => { - visit( child ); - }); - } - - if ( visitor.leave ) visitor.leave( generator, node ); + counter: counter() }); - generator.addRenderer( generator.current ); + parsed.html.children.forEach( generator.visit ); + + generator.addRenderer( generator.pop() ); const setStatements = [ deindent` const oldState = state; diff --git a/compiler/generate/visitors/ElseBlock.js b/compiler/generate/visitors/ElseBlock.js new file mode 100644 index 0000000000..6a9feb816b --- /dev/null +++ b/compiler/generate/visitors/ElseBlock.js @@ -0,0 +1,23 @@ +import deindent from '../utils/deindent.js'; +import counter from '../utils/counter.js'; + +export default { + enter ( generator ) { + const name = generator.current.name.replace( 'If', 'Else' ); + + generator.push({ + name, + + initStatements: [], + updateStatements: [], + teardownStatements: [], + + counter: counter() + }); + }, + + leave ( generator ) { + generator.addRenderer( generator.current ); + generator.pop(); + } +}; diff --git a/compiler/generate/visitors/IfBlock.js b/compiler/generate/visitors/IfBlock.js index d9c516e9b1..8ee257bf58 100644 --- a/compiler/generate/visitors/IfBlock.js +++ b/compiler/generate/visitors/IfBlock.js @@ -1,5 +1,4 @@ import deindent from '../utils/deindent.js'; -import isReference from '../utils/isReference.js'; import counter from '../utils/counter.js'; export default { @@ -8,57 +7,74 @@ export default { const name = `ifBlock_${i}`; const renderer = `renderIfBlock_${i}`; + const elseName = `elseBlock_${i}`; + const elseRenderer = `renderElseBlock_${i}`; + generator.current.initStatements.push( deindent` var ${name}_anchor = document.createComment( ${JSON.stringify( `#if ${generator.source.slice( node.expression.start, node.expression.end )}` )} ); ${generator.current.target}.appendChild( ${name}_anchor ); - var ${name} = null; + var ${name} = null;${node.else ? `\nvar ${elseName} = null;` : ``} ` ); generator.addSourcemapLocations( node.expression ); + generator.contextualise( node.expression ); - const usedContexts = generator.contextualise( node.expression ); const snippet = `[✂${node.expression.start}-${node.expression.end}✂]`; - let expression; - - if ( isReference( node.expression ) ) { - const reference = `${generator.source.slice( node.expression.start, node.expression.end )}`; - expression = usedContexts[0] === 'root' ? `root.${reference}` : reference; + const ifTrue = [ deindent` + if ( !${name } ) { + ${name} = ${renderer}( component, ${generator.current.target}, ${name}_anchor ); + } + ` ]; - generator.current.updateStatements.push( deindent` - if ( ${snippet} && !${name} ) { - ${name} = ${renderer}( component, ${generator.current.target}, ${name}_anchor ); + if ( node.else ) { + ifTrue.push( deindent` + if ( ${elseName } ) { + ${elseName}.teardown(); + ${elseName} = null; } ` ); - } else { - expression = `${name}_value`; + } - generator.current.updateStatements.push( deindent` - var ${expression} = ${snippet}; + const ifFalse = [ deindent` + if ( ${name} ) { + ${name}.teardown(); + ${name} = null; + } + ` ]; - if ( ${expression} && !${name} ) { - ${name} = ${renderer}( component, ${generator.current.target}, ${name}_anchor ); + if ( node.else ) { + ifFalse.push( deindent` + if ( !${elseName } ) { + ${elseName} = ${elseRenderer}( component, ${generator.current.target}, ${name}_anchor ); } ` ); } - generator.current.updateStatements.push( deindent` - else if ( !${expression} && ${name} ) { - ${name}.teardown(); - ${name} = null; + let update = deindent` + if ( ${snippet} ) { + ${ifTrue.join( '\n\n' )} } - if ( ${name} ) { - ${name}.update( ${generator.current.contextChain.join( ', ' )} ); + else { + ${ifFalse.join( '\n\n' )} } - ` ); + + if ( ${name} ) ${name}.update( ${generator.current.contextChain.join( ', ' )} ); + `; + + if ( node.else ) { + update += `\nif ( ${elseName} ) ${elseName}.update( ${generator.current.contextChain.join( ', ' )} );`; + } + + generator.current.updateStatements.push( update ); generator.current.teardownStatements.push( deindent` - if ( ${name} ) ${name}.teardown(); + if ( ${name} ) ${name}.teardown();${node.else ? `\nif ( ${elseName} ) ${elseName}.teardown();` : ``} ${name}_anchor.parentNode.removeChild( ${name}_anchor ); ` ); - generator.current = Object.assign( {}, generator.current, { + generator.push({ useAnchor: true, name: renderer, target: 'target', @@ -67,14 +83,17 @@ export default { updateStatements: [], teardownStatements: [], - counter: counter(), - - parent: generator.current + counter: counter() }); }, - leave ( generator ) { + leave ( generator, node ) { generator.addRenderer( generator.current ); - generator.current = generator.current.parent; + + if ( node.else ) { + generator.visit( node.else ); + } + + generator.pop(); } }; diff --git a/compiler/generate/visitors/index.js b/compiler/generate/visitors/index.js index 064669762f..0d3005366a 100644 --- a/compiler/generate/visitors/index.js +++ b/compiler/generate/visitors/index.js @@ -1,6 +1,7 @@ import Comment from './Comment.js'; import EachBlock from './EachBlock.js'; import Element from './Element.js'; +import ElseBlock from './ElseBlock.js'; import IfBlock from './IfBlock.js'; import MustacheTag from './MustacheTag.js'; import Text from './Text.js'; @@ -9,6 +10,7 @@ export default { Comment, EachBlock, Element, + ElseBlock, IfBlock, MustacheTag, Text diff --git a/compiler/parse/state/mustache.js b/compiler/parse/state/mustache.js index 0305a89296..818a597cd3 100644 --- a/compiler/parse/state/mustache.js +++ b/compiler/parse/state/mustache.js @@ -12,9 +12,16 @@ export default function mustache ( parser ) { // {{/if}} or {{/each}} if ( parser.eat( '/' ) ) { - const block = parser.current(); + let block = parser.current(); let expected; + if ( block.type === 'ElseBlock' ) { + // TODO need to strip whitespace from else blocks + block.end = start; + parser.stack.pop(); + block = parser.current(); + } + if ( block.type === 'IfBlock' ) { expected = 'if'; } else if ( block.type === 'EachBlock' ) { @@ -49,7 +56,26 @@ export default function mustache ( parser ) { parser.stack.pop(); } - // TODO {{else}} and {{elseif expression}} + else if ( parser.eat( 'elseif' ) ) { + throw new Error( 'TODO elseif' ); + } + + else if ( parser.eat( 'else' ) ) { + const block = parser.current(); + if ( block.type !== 'IfBlock' ) parser.error( 'Cannot have an {{else}} block outside an {{#if ...}} block' ); + + parser.allowWhitespace(); + parser.eat( '}}', true ); + + block.else = { + start: parser.index, + end: null, + type: 'ElseBlock', + children: [] + }; + + parser.stack.push( block.else ); + } // {{#if foo}} or {{#each foo}} else if ( parser.eat( '#' ) ) { diff --git a/test/compiler/if-block-else/_config.js b/test/compiler/if-block-else/_config.js index 25c44fd65e..9c0e9061be 100644 --- a/test/compiler/if-block-else/_config.js +++ b/test/compiler/if-block-else/_config.js @@ -5,13 +5,13 @@ export default { foo: true, bar: false }, - html: '

foo

not bar

', + html: '

foo

\n\n

not bar

', test ( component, target ) { component.set({ foo: false }); - assert.equal( target.innerHTML, '

not foo

not bar

' ); + assert.equal( target.innerHTML, '

not foo

\n\n

not bar

' ); component.set({ bar: true }); - assert.equal( target.innerHTML, '

not foo

bar

' ); + assert.equal( target.innerHTML, '

not foo

\n\n

bar

' ); component.set({ foo: true }); - assert.equal( target.innerHTML, '

foo

bar

' ); + assert.equal( target.innerHTML, '

foo

\n\n

bar

' ); } };