diff --git a/.eslintignore b/.eslintignore index a84efdafe36f..effb19af4547 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1,3 +1,5 @@ src/shared shared.js -test/test.js \ No newline at end of file +test/test.js +**/_actual.js +**/expected.js \ No newline at end of file diff --git a/src/generators/Generator.js b/src/generators/Generator.js index 65b391d20b8c..37d8de90b663 100644 --- a/src/generators/Generator.js +++ b/src/generators/Generator.js @@ -12,11 +12,10 @@ import processCss from './shared/processCss.js'; import annotateWithScopes from './annotateWithScopes.js'; export default class Generator { - constructor ( parsed, source, name, visitors, options ) { + constructor ( parsed, source, name, options ) { this.parsed = parsed; this.source = source; this.name = name; - this.visitors = visitors; this.options = options; this.imports = []; @@ -31,8 +30,6 @@ export default class Generator { // in dev mode this.expectedProperties = new Set(); - this.elementDepth = 0; - this.code = new MagicString( source ); this.css = parsed.css ? processCss( parsed, this.code ) : null; this.cssId = parsed.css ? `svelte-${parsed.hash}` : ''; @@ -43,8 +40,6 @@ export default class Generator { this.importedNames = new Set(); this._aliases = new Map(); this._usedNames = new Set(); - - this._callbacks = new Map(); } addSourcemapLocations ( node ) { @@ -65,14 +60,14 @@ export default class Generator { return alias; } - contextualise ( expression, isEventHandler ) { + contextualise ( fragment, expression, isEventHandler ) { this.addSourcemapLocations( expression ); const usedContexts = []; const dependencies = []; const { code, helpers } = this; - const { contextDependencies, contexts, indexes } = this.current; + const { contextDependencies, contexts, indexes } = fragment; let scope = annotateWithScopes( expression ); @@ -154,15 +149,6 @@ export default class Generator { }; } - fire ( eventName, data ) { - const handlers = this._callbacks.has( eventName ) && this._callbacks.get( eventName ).slice(); - if ( !handlers ) return; - - for ( let i = 0; i < handlers.length; i += 1 ) { - handlers[i].call( this, data ); - } - } - generate ( result, options, { name, format } ) { if ( this.imports.length ) { const statements = []; @@ -430,50 +416,4 @@ export default class Generator { templateProperties }; } - - on ( eventName, handler ) { - if ( this._callbacks.has( eventName ) ) { - this._callbacks.get( eventName ).push( handler ); - } else { - this._callbacks.set( eventName, [ handler ] ); - } - } - - pop () { - const tail = this.current; - this.current = tail.parent; - - return tail; - } - - push ( fragment ) { - const newFragment = Object.assign( {}, this.current, fragment, { - parent: this.current - }); - - this.current = newFragment; - } - - visit ( node ) { - const visitor = this.visitors[ node.type ]; - if ( !visitor ) throw new Error( `Not implemented: ${node.type}` ); - - if ( visitor.enter ) visitor.enter( this, node ); - - if ( visitor.type === 'Element' ) { - this.elementDepth += 1; - } - - if ( node.children ) { - node.children.forEach( child => { - this.visit( child ); - }); - } - - if ( visitor.type === 'Element' ) { - this.elementDepth -= 1; - } - - if ( visitor.leave ) visitor.leave( this, node ); - } } diff --git a/src/generators/dom/Block.js b/src/generators/dom/Block.js new file mode 100644 index 000000000000..3f5b838bbbb4 --- /dev/null +++ b/src/generators/dom/Block.js @@ -0,0 +1,140 @@ +import CodeBuilder from '../../utils/CodeBuilder.js'; +import deindent from '../../utils/deindent.js'; + +export default class Block { + constructor ({ generator, name, key, expression, context, contextDependencies, contexts, indexes, params, indexNames, listNames }) { + this.generator = generator; + this.name = name; + this.key = key; + this.expression = expression; + this.context = context; + + this.contexts = contexts; + this.indexes = indexes; + this.contextDependencies = contextDependencies; + + this.params = params; + this.indexNames = indexNames; + this.listNames = listNames; + + this.builders = { + create: new CodeBuilder(), + mount: new CodeBuilder(), + update: new CodeBuilder(), + detach: new CodeBuilder(), + detachRaw: new CodeBuilder(), + destroy: new CodeBuilder() + }; + + this.getUniqueName = generator.getUniqueNameMaker( params ); + + // unique names + this.component = this.getUniqueName( 'component' ); + } + + addElement ( name, renderStatement, parentNode, needsIdentifier = false ) { + const isToplevel = !parentNode; + if ( needsIdentifier || isToplevel ) { + this.builders.create.addLine( + `var ${name} = ${renderStatement};` + ); + + this.createMountStatement( name, parentNode ); + } else { + this.builders.create.addLine( `${this.generator.helper( 'appendNode' )}( ${renderStatement}, ${parentNode} );` ); + } + + if ( isToplevel ) { + this.builders.detach.addLine( `${this.generator.helper( 'detachNode' )}( ${name} );` ); + } + } + + child ( options ) { + return new Block( Object.assign( {}, this, options, { parent: this } ) ); + } + + createAnchor ( name, parentNode ) { + const renderStatement = `${this.generator.helper( 'createComment' )}()`; + this.addElement( name, renderStatement, parentNode, true ); + } + + createMountStatement ( name, parentNode ) { + if ( parentNode ) { + this.builders.create.addLine( `${this.generator.helper( 'appendNode' )}( ${name}, ${parentNode} );` ); + } else { + this.builders.mount.addLine( `${this.generator.helper( 'insertNode' )}( ${name}, target, anchor );` ); + } + } + + render () { + if ( this.autofocus ) { + this.builders.create.addLine( `${this.autofocus}.focus();` ); + } + + // minor hack – we need to ensure that any {{{triples}}} are detached + // first, so we append normal detach statements to detachRaw + this.builders.detachRaw.addBlock( this.builders.detach ); + + if ( !this.builders.detachRaw.isEmpty() ) { + this.builders.destroy.addBlock( deindent` + if ( detach ) { + ${this.builders.detachRaw} + } + ` ); + } + + const properties = new CodeBuilder(); + + let localKey; + if ( this.key ) { + localKey = this.getUniqueName( 'key' ); + properties.addBlock( `key: ${localKey},` ); + } + + if ( this.builders.mount.isEmpty() ) { + properties.addBlock( `mount: ${this.generator.helper( 'noop' )},` ); + } else { + properties.addBlock( deindent` + mount: function ( target, anchor ) { + ${this.builders.mount} + }, + ` ); + } + + if ( this.builders.update.isEmpty() ) { + properties.addBlock( `update: ${this.generator.helper( 'noop' )},` ); + } else { + if ( this._tmp ) this.builders.update.addBlockAtStart( `var ${this._tmp};` ); + properties.addBlock( deindent` + update: function ( changed, ${this.params.join( ', ' )} ) { + ${this.builders.update} + }, + ` ); + } + + if ( this.builders.destroy.isEmpty() ) { + properties.addBlock( `destroy: ${this.generator.helper( 'noop' )}` ); + } else { + properties.addBlock( deindent` + destroy: function ( detach ) { + ${this.builders.destroy} + } + ` ); + } + + return deindent` + function ${this.name} ( ${this.params.join( ', ' )}, ${this.component}${this.key ? `, ${localKey}` : ''} ) { + ${this.builders.create} + + return { + ${properties} + }; + } + `; + } + + tmp () { + if ( !this._tmp ) this._tmp = this.getUniqueName( 'tmp' ); + return this._tmp; + } +} \ No newline at end of file diff --git a/src/generators/dom/index.js b/src/generators/dom/index.js index ee08ab439411..fcb540d6799c 100644 --- a/src/generators/dom/index.js +++ b/src/generators/dom/index.js @@ -1,14 +1,14 @@ import deindent from '../../utils/deindent.js'; -import getBuilders from './utils/getBuilders.js'; import CodeBuilder from '../../utils/CodeBuilder.js'; -import visitors from './visitors/index.js'; +import visit from './visit.js'; import Generator from '../Generator.js'; +import Block from './Block.js'; import * as shared from '../../shared/index.js'; class DomGenerator extends Generator { - constructor ( parsed, source, name, visitors, options ) { - super( parsed, source, name, visitors, options ); - this.renderers = []; + constructor ( parsed, source, name, options ) { + super( parsed, source, name, options ); + this.blocks = []; this.uses = new Set(); // initial values for e.g. window.innerWidth, if there's a <:Window> meta tag @@ -17,121 +17,8 @@ class DomGenerator extends Generator { }; } - addElement ( name, renderStatement, needsIdentifier = false ) { - const isToplevel = this.current.localElementDepth === 0; - if ( needsIdentifier || isToplevel ) { - this.current.builders.init.addLine( - `var ${name} = ${renderStatement};` - ); - - this.createMountStatement( name ); - } else { - this.current.builders.init.addLine( `${this.helper( 'appendNode' )}( ${renderStatement}, ${this.current.target} );` ); - } - - if ( isToplevel ) { - this.current.builders.detach.addLine( `${this.helper( 'detachNode' )}( ${name} );` ); - } - } - - addRenderer ( fragment ) { - if ( fragment.autofocus ) { - fragment.builders.init.addLine( `${fragment.autofocus}.focus();` ); - } - - // minor hack – we need to ensure that any {{{triples}}} are detached - // first, so we append normal detach statements to detachRaw - fragment.builders.detachRaw.addBlock( fragment.builders.detach ); - - if ( !fragment.builders.detachRaw.isEmpty() ) { - fragment.builders.teardown.addBlock( deindent` - if ( detach ) { - ${fragment.builders.detachRaw} - } - ` ); - } - - const properties = new CodeBuilder(); - - let localKey; - if ( fragment.key ) { - localKey = fragment.getUniqueName( 'key' ); - properties.addBlock( `key: ${localKey},` ); - } - - if ( fragment.builders.mount.isEmpty() ) { - properties.addBlock( `mount: ${this.helper( 'noop' )},` ); - } else { - properties.addBlock( deindent` - mount: function ( target, anchor ) { - ${fragment.builders.mount} - }, - ` ); - } - - if ( fragment.builders.update.isEmpty() ) { - properties.addBlock( `update: ${this.helper( 'noop' )},` ); - } else { - properties.addBlock( deindent` - update: function ( changed, ${fragment.params.join( ', ' )} ) { - ${fragment.tmp ? `var ${fragment.tmp};` : ''} - - ${fragment.builders.update} - }, - ` ); - } - - if ( fragment.builders.teardown.isEmpty() ) { - properties.addBlock( `teardown: ${this.helper( 'noop' )},` ); - } else { - properties.addBlock( deindent` - teardown: function ( detach ) { - ${fragment.builders.teardown} - } - ` ); - } - - this.renderers.push( deindent` - function ${fragment.name} ( ${fragment.params.join( ', ' )}, ${fragment.component}${fragment.key ? `, ${localKey}` : ''} ) { - ${fragment.builders.init} - - return { - ${properties} - }; - } - ` ); - } - - createAnchor ( name ) { - const renderStatement = `${this.helper( 'createComment' )}()`; - this.addElement( name, renderStatement, true ); - } - - createMountStatement ( name ) { - if ( this.current.target === 'target' ) { - this.current.builders.mount.addLine( `${this.helper( 'insertNode' )}( ${name}, target, anchor );` ); - } else { - this.current.builders.init.addLine( `${this.helper( 'appendNode' )}( ${name}, ${this.current.target} );` ); - } - } - - generateBlock ( node, name, type ) { - this.push({ - type, - name, - target: 'target', - localElementDepth: 0, - builders: getBuilders(), - getUniqueName: this.getUniqueNameMaker( this.current.params ) - }); - - // walk the children here - node.children.forEach( node => this.visit( node ) ); - this.addRenderer( this.current ); - this.pop(); - - // unset the children, to avoid them being visited again - node.children = []; + addBlock ( block ) { + this.blocks.push( block ); } helper ( name ) { @@ -149,19 +36,16 @@ export default function dom ( parsed, source, options ) { const format = options.format || 'es'; const name = options.name || 'SvelteComponent'; - const generator = new DomGenerator( parsed, source, name, visitors, options ); + const generator = new DomGenerator( parsed, source, name, options ); const { computations, hasJs, templateProperties, namespace } = generator.parseJs(); const getUniqueName = generator.getUniqueNameMaker( [ 'root' ] ); const component = getUniqueName( 'component' ); - generator.push({ - type: 'block', - name: generator.alias( 'render_main_fragment' ), - namespace, - target: 'target', - localElementDepth: 0, + const mainBlock = new Block({ + generator, + name: generator.alias( 'create_main_fragment' ), key: null, component, @@ -173,13 +57,20 @@ export default function dom ( parsed, source, options ) { indexNames: new Map(), listNames: new Map(), - builders: getBuilders(), - getUniqueName, + getUniqueName }); - parsed.html.children.forEach( node => generator.visit( node ) ); + const state = { + namespace, + parentNode: null, + isTopLevel: true + }; + + parsed.html.children.forEach( node => { + visit( generator, mainBlock, state, node ); + }); - generator.addRenderer( generator.pop() ); + generator.addBlock( mainBlock ); const builders = { main: new CodeBuilder(), @@ -243,8 +134,8 @@ export default function dom ( parsed, source, options ) { ` ); } - let i = generator.renderers.length; - while ( i-- ) builders.main.addBlock( generator.renderers[i] ); + let i = generator.blocks.length; + while ( i-- ) builders.main.addBlock( generator.blocks[i].render() ); builders.init.addLine( `this._torndown = false;` ); @@ -259,7 +150,7 @@ export default function dom ( parsed, source, options ) { if ( generator.hasComplexBindings ) { builders.init.addBlock( deindent` this._bindings = []; - this._fragment = ${generator.alias( 'render_main_fragment' )}( this._state, this ); + this._fragment = ${generator.alias( 'create_main_fragment' )}( this._state, this ); if ( options.target ) this._fragment.mount( options.target, null ); while ( this._bindings.length ) this._bindings.pop()(); ` ); @@ -267,7 +158,7 @@ export default function dom ( parsed, source, options ) { builders._set.addLine( `while ( this._bindings.length ) this._bindings.pop()();` ); } else { builders.init.addBlock( deindent` - this._fragment = ${generator.alias( 'render_main_fragment' )}( this._state, this ); + this._fragment = ${generator.alias( 'create_main_fragment' )}( this._state, this ); if ( options.target ) this._fragment.mount( options.target, null ); ` ); } @@ -367,7 +258,7 @@ export default function dom ( parsed, source, options ) { ${name}.prototype.teardown = ${name}.prototype.destroy = function destroy ( detach ) { this.fire( 'destroy' );${templateProperties.ondestroy ? `\n${generator.alias( 'template' )}.ondestroy.call( this );` : ``} - this._fragment.teardown( detach !== false ); + this._fragment.destroy( detach !== false ); this._fragment = null; this._state = {}; diff --git a/src/generators/dom/utils/findBlock.js b/src/generators/dom/utils/findBlock.js deleted file mode 100644 index 59f71e35527e..000000000000 --- a/src/generators/dom/utils/findBlock.js +++ /dev/null @@ -1,4 +0,0 @@ -export default function findBlock ( fragment ) { - while ( fragment.type !== 'block' ) fragment = fragment.parent; - return fragment; -} \ No newline at end of file diff --git a/src/generators/dom/utils/getBuilders.js b/src/generators/dom/utils/getBuilders.js deleted file mode 100644 index ca66c1bec5ea..000000000000 --- a/src/generators/dom/utils/getBuilders.js +++ /dev/null @@ -1,12 +0,0 @@ -import CodeBuilder from '../../../utils/CodeBuilder.js'; - -export default function getBuilders () { - return { - init: new CodeBuilder(), - mount: new CodeBuilder(), - update: new CodeBuilder(), - detach: new CodeBuilder(), - detachRaw: new CodeBuilder(), - teardown: new CodeBuilder() - }; -} diff --git a/src/generators/dom/visit.js b/src/generators/dom/visit.js new file mode 100644 index 000000000000..22a040af06c7 --- /dev/null +++ b/src/generators/dom/visit.js @@ -0,0 +1,6 @@ +import visitors from './visitors/index.js'; + +export default function visit ( generator, block, state, node ) { + const visitor = visitors[ node.type ]; + visitor( generator, block, state, node ); +} \ No newline at end of file diff --git a/src/generators/dom/visitors/Comment.js b/src/generators/dom/visitors/Comment.js index f32a1e64a8bd..4d3477d7ef0d 100644 --- a/src/generators/dom/visitors/Comment.js +++ b/src/generators/dom/visitors/Comment.js @@ -1,3 +1,3 @@ -export default { +export default function visitComment () { // do nothing -}; +} diff --git a/src/generators/dom/visitors/Component.js b/src/generators/dom/visitors/Component.js index 69e2e3c2acf0..d4c5a86dc6c0 100644 --- a/src/generators/dom/visitors/Component.js +++ b/src/generators/dom/visitors/Component.js @@ -1,5 +1,6 @@ import deindent from '../../../utils/deindent.js'; import CodeBuilder from '../../../utils/CodeBuilder.js'; +import visit from '../visit.js'; import addComponentAttributes from './attributes/addComponentAttributes.js'; function capDown ( name ) { @@ -18,157 +19,152 @@ function stringifyProps ( props ) { return `{ ${joined} }`; } -export default { - enter ( generator, node ) { - const hasChildren = node.children.length > 0; - const { current } = generator; - const name = current.getUniqueName( capDown( node.name === ':Self' ? generator.name : node.name ) ); +export default function visitComponent ( generator, block, state, node ) { + const hasChildren = node.children.length > 0; + const name = block.getUniqueName( capDown( node.name === ':Self' ? generator.name : node.name ) ); - const local = { - name, - namespace: current.namespace, - isComponent: true, + const local = { + name, + namespace: state.namespace, + isComponent: true, - allUsedContexts: [], + allUsedContexts: [], - init: new CodeBuilder(), - update: new CodeBuilder() - }; + create: new CodeBuilder(), + update: new CodeBuilder() + }; - const isToplevel = current.localElementDepth === 0; + const isToplevel = !state.parentNode; - generator.hasComponents = true; + generator.hasComponents = true; - addComponentAttributes( generator, node, local ); + addComponentAttributes( generator, block, node, local ); - if ( local.allUsedContexts.length ) { - const initialProps = local.allUsedContexts.map( contextName => { - if ( contextName === 'root' ) return `root: root`; + if ( local.allUsedContexts.length ) { + const initialProps = local.allUsedContexts.map( contextName => { + if ( contextName === 'root' ) return `root: root`; - const listName = current.listNames.get( contextName ); - const indexName = current.indexNames.get( contextName ); + const listName = block.listNames.get( contextName ); + const indexName = block.indexNames.get( contextName ); - return `${listName}: ${listName},\n${indexName}: ${indexName}`; - }).join( ',\n' ); + return `${listName}: ${listName},\n${indexName}: ${indexName}`; + }).join( ',\n' ); - const updates = local.allUsedContexts.map( contextName => { - if ( contextName === 'root' ) return `${name}._context.root = root;`; + const updates = local.allUsedContexts.map( contextName => { + if ( contextName === 'root' ) return `${name}._context.root = root;`; - const listName = current.listNames.get( contextName ); - const indexName = current.indexNames.get( contextName ); + const listName = block.listNames.get( contextName ); + const indexName = block.indexNames.get( contextName ); - return `${name}._context.${listName} = ${listName};\n${name}._context.${indexName} = ${indexName};`; - }).join( '\n' ); + return `${name}._context.${listName} = ${listName};\n${name}._context.${indexName} = ${indexName};`; + }).join( '\n' ); - local.init.addBlock( deindent` - ${name}._context = { - ${initialProps} - }; - ` ); + local.create.addBlock( deindent` + ${name}._context = { + ${initialProps} + }; + ` ); - local.update.addBlock( updates ); - } + local.update.addBlock( updates ); + } - const componentInitProperties = [ - `target: ${!isToplevel ? current.target: 'null'}`, - `_root: ${current.component}._root || ${current.component}` - ]; + const componentInitProperties = [ + `target: ${!isToplevel ? state.parentNode: 'null'}`, + `_root: ${block.component}._root || ${block.component}` + ]; - // Component has children, put them in a separate {{yield}} block - if ( hasChildren ) { - const yieldName = generator.getUniqueName( `render_${name}_yield_fragment` ); - const params = current.params.join( ', ' ); + // Component has children, put them in a separate {{yield}} block + if ( hasChildren ) { + const params = block.params.join( ', ' ); - generator.generateBlock( node, yieldName, 'block' ); + const childBlock = block.child({ + name: generator.getUniqueName( `create_${name}_yield_fragment` ) // TODO should getUniqueName happen inside Fragment? probably + }); - const yieldFragment = current.getUniqueName( `${name}_yield_fragment` ); + const childState = Object.assign( {}, state, { + parentNode: null + }); - current.builders.init.addLine( - `var ${yieldFragment} = ${yieldName}( ${params}, ${current.component} );` - ); + node.children.forEach( child => { + visit( generator, childBlock, childState, child ); + }); - current.builders.update.addLine( - `${yieldFragment}.update( changed, ${params} );` - ); + const yieldFragment = block.getUniqueName( `${name}_yield_fragment` ); - componentInitProperties.push( `_yield: ${yieldFragment}`); - } + block.builders.create.addLine( + `var ${yieldFragment} = ${childBlock.name}( ${params}, ${block.component} );` + ); - const statements = []; + block.builders.update.addLine( + `${yieldFragment}.update( changed, ${params} );` + ); - if ( local.staticAttributes.length || local.dynamicAttributes.length || local.bindings.length ) { - const initialProps = local.staticAttributes - .concat( local.dynamicAttributes ) - .map( attribute => `${attribute.name}: ${attribute.value}` ); + componentInitProperties.push( `_yield: ${yieldFragment}`); - const initialPropString = stringifyProps( initialProps ); + generator.addBlock( childBlock ); + } - if ( local.bindings.length ) { - const initialData = current.getUniqueName( `${name}_initial_data` ); + const statements = []; - statements.push( `var ${name}_initial_data = ${initialPropString};` ); + if ( local.staticAttributes.length || local.dynamicAttributes.length || local.bindings.length ) { + const initialProps = local.staticAttributes + .concat( local.dynamicAttributes ) + .map( attribute => `${attribute.name}: ${attribute.value}` ); - local.bindings.forEach( binding => { - statements.push( `if ( ${binding.prop} in ${binding.obj} ) ${initialData}.${binding.name} = ${binding.value};` ); - }); + const initialPropString = stringifyProps( initialProps ); - componentInitProperties.push( `data: ${initialData}` ); - } else if ( initialProps.length ) { - componentInitProperties.push( `data: ${initialPropString}` ); - } - } + if ( local.bindings.length ) { + const initialData = block.getUniqueName( `${name}_initial_data` ); - const expression = node.name === ':Self' ? generator.name : generator.importedComponents.get( node.name ) || `${generator.alias( 'template' )}.components.${node.name}`; + statements.push( `var ${name}_initial_data = ${initialPropString};` ); - local.init.addBlockAtStart( deindent` - ${statements.join( '\n' )} - var ${name} = new ${expression}({ - ${componentInitProperties.join(',\n')} + local.bindings.forEach( binding => { + statements.push( `if ( ${binding.prop} in ${binding.obj} ) ${initialData}.${binding.name} = ${binding.value};` ); }); - ` ); - if ( isToplevel ) { - current.builders.mount.addLine( `${name}._fragment.mount( target, anchor );` ); + componentInitProperties.push( `data: ${initialData}` ); + } else if ( initialProps.length ) { + componentInitProperties.push( `data: ${initialPropString}` ); } + } - if ( local.dynamicAttributes.length ) { - const updates = local.dynamicAttributes.map( attribute => { - if ( attribute.dependencies.length ) { - return deindent` - if ( ${attribute.dependencies.map( dependency => `'${dependency}' in changed` ).join( '||' )} ) ${name}_changes.${attribute.name} = ${attribute.value}; - `; - } - - // TODO this is an odd situation to encounter – I *think* it should only happen with - // each block indices, in which case it may be possible to optimise this - return `${name}_changes.${attribute.name} = ${attribute.value};`; - }); + const expression = node.name === ':Self' ? generator.name : generator.importedComponents.get( node.name ) || `${generator.alias( 'template' )}.components.${node.name}`; - local.update.addBlock( deindent` - var ${name}_changes = {}; + local.create.addBlockAtStart( deindent` + ${statements.join( '\n' )} + var ${name} = new ${expression}({ + ${componentInitProperties.join(',\n')} + }); + ` ); - ${updates.join( '\n' )} + if ( isToplevel ) { + block.builders.mount.addLine( `${name}._fragment.mount( target, anchor );` ); + } - if ( Object.keys( ${name}_changes ).length ) ${name}.set( ${name}_changes ); - ` ); - } + if ( local.dynamicAttributes.length ) { + const updates = local.dynamicAttributes.map( attribute => { + if ( attribute.dependencies.length ) { + return deindent` + if ( ${attribute.dependencies.map( dependency => `'${dependency}' in changed` ).join( '||' )} ) ${name}_changes.${attribute.name} = ${attribute.value}; + `; + } - current.builders.teardown.addLine( `${name}.destroy( ${isToplevel ? 'detach' : 'false'} );` ); + // TODO this is an odd situation to encounter – I *think* it should only happen with + // each block indices, in which case it may be possible to optimise this + return `${name}_changes.${attribute.name} = ${attribute.value};`; + }); - current.builders.init.addBlock( local.init ); - if ( !local.update.isEmpty() ) current.builders.update.addBlock( local.update ); + local.update.addBlock( deindent` + var ${name}_changes = {}; - generator.push({ - type: 'component', - namespace: local.namespace, - target: name, - parent: current, - localElementDepth: current.localElementDepth + 1, - key: null - }); - }, + ${updates.join( '\n' )} - leave ( generator ) { - generator.pop(); + if ( Object.keys( ${name}_changes ).length ) ${name}.set( ${name}_changes ); + ` ); } -}; + + block.builders.destroy.addLine( `${name}.destroy( ${isToplevel ? 'detach' : 'false'} );` ); + + block.builders.create.addBlock( local.create ); + if ( !local.update.isEmpty() ) block.builders.update.addBlock( local.update ); +} \ No newline at end of file diff --git a/src/generators/dom/visitors/EachBlock.js b/src/generators/dom/visitors/EachBlock.js index df9936197fc4..cef7478dfc55 100644 --- a/src/generators/dom/visitors/EachBlock.js +++ b/src/generators/dom/visitors/EachBlock.js @@ -1,223 +1,220 @@ import CodeBuilder from '../../../utils/CodeBuilder.js'; import deindent from '../../../utils/deindent.js'; -import getBuilders from '../utils/getBuilders.js'; +import visit from '../visit.js'; -export default { - enter ( generator, node ) { - const name = generator.getUniqueName( `each_block` ); - const renderer = generator.getUniqueName( `render_each_block` ); - const elseName = generator.getUniqueName( `${name}_else` ); - const renderElse = generator.getUniqueName( `${renderer}_else` ); - const i = generator.current.getUniqueName( `i` ); - const params = generator.current.params.join( ', ' ); +export default function visitEachBlock ( generator, block, state, node ) { + const name = generator.getUniqueName( `each_block` ); + const renderer = generator.getUniqueName( `create_each_block` ); + const elseName = generator.getUniqueName( `${name}_else` ); + const renderElse = generator.getUniqueName( `${renderer}_else` ); + const i = block.getUniqueName( `i` ); + const params = block.params.join( ', ' ); - const listName = generator.current.getUniqueName( `${name}_value` ); + const listName = block.getUniqueName( `${name}_value` ); - const isToplevel = generator.current.localElementDepth === 0; + const isToplevel = !state.parentNode; - generator.addSourcemapLocations( node.expression ); + const { dependencies, snippet } = generator.contextualise( block, node.expression ); - const { dependencies, snippet } = generator.contextualise( node.expression ); + const anchor = block.getUniqueName( `${name}_anchor` ); + block.createAnchor( anchor, state.parentNode ); - const anchor = generator.current.getUniqueName( `${name}_anchor` ); - generator.createAnchor( anchor ); + const localVars = {}; - const localVars = {}; + localVars.iteration = block.getUniqueName( `${name}_iteration` ); + localVars.iterations = block.getUniqueName( `${name}_iterations` ); + localVars._iterations = block.getUniqueName( `_${name}_iterations` ); + localVars.lookup = block.getUniqueName( `${name}_lookup` ); + localVars._lookup = block.getUniqueName( `_${name}_lookup` ); - localVars.iteration = generator.current.getUniqueName( `${name}_iteration` ); - localVars.iterations = generator.current.getUniqueName( `${name}_iterations` ); - localVars._iterations = generator.current.getUniqueName( `_${name}_iterations` ); - localVars.lookup = generator.current.getUniqueName( `${name}_lookup` ); - localVars._lookup = generator.current.getUniqueName( `_${name}_lookup` ); + block.builders.create.addLine( `var ${listName} = ${snippet};` ); + block.builders.create.addLine( `var ${localVars.iterations} = [];` ); + if ( node.key ) block.builders.create.addLine( `var ${localVars.lookup} = Object.create( null );` ); + if ( node.else ) block.builders.create.addLine( `var ${elseName} = null;` ); - generator.current.builders.init.addLine( `var ${listName} = ${snippet};` ); - generator.current.builders.init.addLine( `var ${localVars.iterations} = [];` ); - if ( node.key ) generator.current.builders.init.addLine( `var ${localVars.lookup} = Object.create( null );` ); - if ( node.else ) generator.current.builders.init.addLine( `var ${elseName} = null;` ); + const initialRender = new CodeBuilder(); - const initialRender = new CodeBuilder(); + if ( node.key ) { + localVars.fragment = block.getUniqueName( 'fragment' ); + localVars.value = block.getUniqueName( 'value' ); + localVars.key = block.getUniqueName( 'key' ); - if ( node.key ) { - localVars.fragment = generator.current.getUniqueName( 'fragment' ); - localVars.value = generator.current.getUniqueName( 'value' ); - localVars.key = generator.current.getUniqueName( 'key' ); + initialRender.addBlock( deindent` + var ${localVars.key} = ${listName}[${i}].${node.key}; + ${localVars.iterations}[${i}] = ${localVars.lookup}[ ${localVars.key} ] = ${renderer}( ${params}, ${listName}, ${listName}[${i}], ${i}, ${block.component}${node.key ? `, ${localVars.key}` : `` } ); + ` ); + } else { + initialRender.addLine( + `${localVars.iterations}[${i}] = ${renderer}( ${params}, ${listName}, ${listName}[${i}], ${i}, ${block.component} );` + ); + } - initialRender.addBlock( deindent` - var ${localVars.key} = ${listName}[${i}].${node.key}; - ${localVars.iterations}[${i}] = ${localVars.lookup}[ ${localVars.key} ] = ${renderer}( ${params}, ${listName}, ${listName}[${i}], ${i}, ${generator.current.component}${node.key ? `, ${localVars.key}` : `` } ); - ` ); - } else { - initialRender.addLine( - `${localVars.iterations}[${i}] = ${renderer}( ${params}, ${listName}, ${listName}[${i}], ${i}, ${generator.current.component} );` - ); - } + if ( !isToplevel ) { + initialRender.addLine( + `${localVars.iterations}[${i}].mount( ${anchor}.parentNode, ${anchor} );` + ); + } - if ( !isToplevel ) { - initialRender.addLine( - `${localVars.iterations}[${i}].mount( ${anchor}.parentNode, ${anchor} );` - ); + block.builders.create.addBlock( deindent` + for ( var ${i} = 0; ${i} < ${listName}.length; ${i} += 1 ) { + ${initialRender} } + ` ); - generator.current.builders.init.addBlock( deindent` - for ( var ${i} = 0; ${i} < ${listName}.length; ${i} += 1 ) { - ${initialRender} + if ( node.else ) { + block.builders.create.addBlock( deindent` + if ( !${listName}.length ) { + ${elseName} = ${renderElse}( ${params}, ${block.component} ); + ${!isToplevel ? `${elseName}.mount( ${anchor}.parentNode, ${anchor} );` : ''} } ` ); + } + if ( isToplevel ) { + block.builders.mount.addBlock( deindent` + for ( var ${i} = 0; ${i} < ${localVars.iterations}.length; ${i} += 1 ) { + ${localVars.iterations}[${i}].mount( ${anchor}.parentNode, ${anchor} ); + } + ` ); if ( node.else ) { - generator.current.builders.init.addBlock( deindent` - if ( !${listName}.length ) { - ${elseName} = ${renderElse}( ${params}, ${generator.current.component} ); - ${!isToplevel ? `${elseName}.mount( ${anchor}.parentNode, ${anchor} );` : ''} - } - ` ); - } - - if ( isToplevel ) { - generator.current.builders.mount.addBlock( deindent` - for ( var ${i} = 0; ${i} < ${localVars.iterations}.length; ${i} += 1 ) { - ${localVars.iterations}[${i}].mount( ${anchor}.parentNode, ${anchor} ); + block.builders.mount.addBlock( deindent` + if ( ${elseName} ) { + ${elseName}.mount( ${anchor}.parentNode, ${anchor} ); } ` ); - if ( node.else ) { - generator.current.builders.mount.addBlock( deindent` - if ( ${elseName} ) { - ${elseName}.mount( ${anchor}.parentNode, ${anchor} ); - } - ` ); - } } + } - if ( node.key ) { - generator.current.builders.update.addBlock( deindent` - var ${listName} = ${snippet}; - var ${localVars._iterations} = []; - var ${localVars._lookup} = Object.create( null ); + if ( node.key ) { + block.builders.update.addBlock( deindent` + var ${listName} = ${snippet}; + var ${localVars._iterations} = []; + var ${localVars._lookup} = Object.create( null ); - var ${localVars.fragment} = document.createDocumentFragment(); + var ${localVars.fragment} = document.createDocumentFragment(); - // create new iterations as necessary - for ( var ${i} = 0; ${i} < ${listName}.length; ${i} += 1 ) { - var ${localVars.value} = ${listName}[${i}]; - var ${localVars.key} = ${localVars.value}.${node.key}; + // create new iterations as necessary + for ( var ${i} = 0; ${i} < ${listName}.length; ${i} += 1 ) { + var ${localVars.value} = ${listName}[${i}]; + var ${localVars.key} = ${localVars.value}.${node.key}; + + if ( ${localVars.lookup}[ ${localVars.key} ] ) { + ${localVars._iterations}[${i}] = ${localVars._lookup}[ ${localVars.key} ] = ${localVars.lookup}[ ${localVars.key} ]; + ${localVars._lookup}[ ${localVars.key} ].update( changed, ${params}, ${listName}, ${listName}[${i}], ${i} ); + } else { + ${localVars._iterations}[${i}] = ${localVars._lookup}[ ${localVars.key} ] = ${renderer}( ${params}, ${listName}, ${listName}[${i}], ${i}, ${block.component}${node.key ? `, ${localVars.key}` : `` } ); + } - if ( ${localVars.lookup}[ ${localVars.key} ] ) { - ${localVars._iterations}[${i}] = ${localVars._lookup}[ ${localVars.key} ] = ${localVars.lookup}[ ${localVars.key} ]; - ${localVars._lookup}[ ${localVars.key} ].update( changed, ${params}, ${listName}, ${listName}[${i}], ${i} ); - } else { - ${localVars._iterations}[${i}] = ${localVars._lookup}[ ${localVars.key} ] = ${renderer}( ${params}, ${listName}, ${listName}[${i}], ${i}, ${generator.current.component}${node.key ? `, ${localVars.key}` : `` } ); - } + ${localVars._iterations}[${i}].mount( ${localVars.fragment}, null ); + } - ${localVars._iterations}[${i}].mount( ${localVars.fragment}, null ); + // remove old iterations + for ( var ${i} = 0; ${i} < ${localVars.iterations}.length; ${i} += 1 ) { + var ${localVars.iteration} = ${localVars.iterations}[${i}]; + if ( !${localVars._lookup}[ ${localVars.iteration}.key ] ) { + ${localVars.iteration}.destroy( true ); } + } - // remove old iterations - for ( var ${i} = 0; ${i} < ${localVars.iterations}.length; ${i} += 1 ) { - var ${localVars.iteration} = ${localVars.iterations}[${i}]; - if ( !${localVars._lookup}[ ${localVars.iteration}.key ] ) { - ${localVars.iteration}.teardown( true ); - } - } + ${anchor}.parentNode.insertBefore( ${localVars.fragment}, ${anchor} ); - ${anchor}.parentNode.insertBefore( ${localVars.fragment}, ${anchor} ); + ${localVars.iterations} = ${localVars._iterations}; + ${localVars.lookup} = ${localVars._lookup}; + ` ); + } else { + block.builders.update.addBlock( deindent` + var ${listName} = ${snippet}; - ${localVars.iterations} = ${localVars._iterations}; - ${localVars.lookup} = ${localVars._lookup}; - ` ); - } else { - generator.current.builders.update.addBlock( deindent` - var ${listName} = ${snippet}; - - for ( var ${i} = 0; ${i} < ${listName}.length; ${i} += 1 ) { - if ( !${localVars.iterations}[${i}] ) { - ${localVars.iterations}[${i}] = ${renderer}( ${params}, ${listName}, ${listName}[${i}], ${i}, ${generator.current.component} ); - ${localVars.iterations}[${i}].mount( ${anchor}.parentNode, ${anchor} ); - } else { - ${localVars.iterations}[${i}].update( changed, ${params}, ${listName}, ${listName}[${i}], ${i} ); - } + for ( var ${i} = 0; ${i} < ${listName}.length; ${i} += 1 ) { + if ( !${localVars.iterations}[${i}] ) { + ${localVars.iterations}[${i}] = ${renderer}( ${params}, ${listName}, ${listName}[${i}], ${i}, ${block.component} ); + ${localVars.iterations}[${i}].mount( ${anchor}.parentNode, ${anchor} ); + } else { + ${localVars.iterations}[${i}].update( changed, ${params}, ${listName}, ${listName}[${i}], ${i} ); } + } - teardownEach( ${localVars.iterations}, true, ${listName}.length ); + destroyEach( ${localVars.iterations}, true, ${listName}.length ); - ${localVars.iterations}.length = ${listName}.length; - ` ); - } + ${localVars.iterations}.length = ${listName}.length; + ` ); + } - if ( node.else ) { - generator.current.builders.update.addBlock( deindent` - if ( !${listName}.length && ${elseName} ) { - ${elseName}.update( changed, ${params} ); - } else if ( !${listName}.length ) { - ${elseName} = ${renderElse}( ${params}, ${generator.current.component} ); - ${elseName}.mount( ${anchor}.parentNode, ${anchor} ); - } else if ( ${elseName} ) { - ${elseName}.teardown( true ); - } - ` ); - } + if ( node.else ) { + block.builders.update.addBlock( deindent` + if ( !${listName}.length && ${elseName} ) { + ${elseName}.update( changed, ${params} ); + } else if ( !${listName}.length ) { + ${elseName} = ${renderElse}( ${params}, ${block.component} ); + ${elseName}.mount( ${anchor}.parentNode, ${anchor} ); + } else if ( ${elseName} ) { + ${elseName}.destroy( true ); + } + ` ); + } - generator.current.builders.teardown.addBlock( - `${generator.helper( 'teardownEach' )}( ${localVars.iterations}, ${isToplevel ? 'detach' : 'false'} );` ); + block.builders.destroy.addBlock( + `${generator.helper( 'destroyEach' )}( ${localVars.iterations}, ${isToplevel ? 'detach' : 'false'} );` ); - if ( node.else ) { - generator.current.builders.teardown.addBlock( deindent` - if ( ${elseName} ) { - ${elseName}.teardown( ${isToplevel ? 'detach' : 'false'} ); - } - ` ); - } + if ( node.else ) { + block.builders.destroy.addBlock( deindent` + if ( ${elseName} ) { + ${elseName}.destroy( ${isToplevel ? 'detach' : 'false'} ); + } + ` ); + } - if ( node.else ) { - generator.generateBlock( node.else, renderElse, 'block' ); - } + const indexNames = new Map( block.indexNames ); + const indexName = node.index || block.getUniqueName( `${node.context}_index` ); + indexNames.set( node.context, indexName ); - const indexNames = new Map( generator.current.indexNames ); - const indexName = node.index || generator.current.getUniqueName( `${node.context}_index` ); - indexNames.set( node.context, indexName ); + const listNames = new Map( block.listNames ); + listNames.set( node.context, listName ); - const listNames = new Map( generator.current.listNames ); - listNames.set( node.context, listName ); + const context = generator.getUniqueName( node.context ); + const contexts = new Map( block.contexts ); + contexts.set( node.context, context ); - const context = generator.getUniqueName( node.context ); - const contexts = new Map( generator.current.contexts ); - contexts.set( node.context, context ); + const indexes = new Map( block.indexes ); + if ( node.index ) indexes.set( indexName, node.context ); - const indexes = new Map( generator.current.indexes ); - if ( node.index ) indexes.set( indexName, node.context ); + const contextDependencies = new Map( block.contextDependencies ); + contextDependencies.set( node.context, dependencies ); - const contextDependencies = new Map( generator.current.contextDependencies ); - contextDependencies.set( node.context, dependencies ); + const childBlock = block.child({ + name: renderer, + expression: node.expression, + context: node.context, + key: node.key, - const blockParams = generator.current.params.concat( listName, context, indexName ); + contextDependencies, + contexts, + indexes, - const getUniqueName = generator.getUniqueNameMaker( blockParams ); + indexNames, + listNames, + params: block.params.concat( listName, context, indexName ) + }); - generator.push({ - type: 'block', - name: renderer, - target: 'target', - expression: node.expression, - context: node.context, - key: node.key, - localElementDepth: 0, + const childState = Object.assign( {}, state, { + parentNode: null + }); - component: getUniqueName( 'component' ), + node.children.forEach( child => { + visit( generator, childBlock, childState, child ); + }); - contextDependencies, - contexts, - indexes, + generator.addBlock( childBlock ); - indexNames, - listNames, - params: blockParams, + if ( node.else ) { + const childBlock = block.child({ + name: renderElse + }); - builders: getBuilders(), - getUniqueName, + node.else.children.forEach( child => { + visit( generator, childBlock, childState, child ); }); - }, - leave ( generator ) { - generator.addRenderer( generator.current ); - generator.pop(); + generator.addBlock( childBlock ); } -}; +} \ No newline at end of file diff --git a/src/generators/dom/visitors/Element.js b/src/generators/dom/visitors/Element.js index 566a535bab7b..7f3b8d4e65e6 100644 --- a/src/generators/dom/visitors/Element.js +++ b/src/generators/dom/visitors/Element.js @@ -1,131 +1,114 @@ import CodeBuilder from '../../../utils/CodeBuilder.js'; import deindent from '../../../utils/deindent.js'; +import visit from '../visit.js'; import addElementAttributes from './attributes/addElementAttributes.js'; -import Component from './Component.js'; -import Window from './meta/Window.js'; +import visitComponent from './Component.js'; +import visitWindow from './meta/Window.js'; const meta = { - ':Window': Window + ':Window': visitWindow }; -export default { - enter ( generator, node ) { - if ( node.name in meta ) { - return meta[ node.name ].enter( generator, node ); - } - - const isComponent = generator.components.has( node.name ) || node.name === ':Self'; +export default function visitElement ( generator, block, state, node ) { + if ( node.name in meta ) { + return meta[ node.name ]( generator, block, node ); + } - if ( isComponent ) { - return Component.enter( generator, node ); - } + if ( generator.components.has( node.name ) || node.name === ':Self' ) { + return visitComponent( generator, block, state, node ); + } - const name = generator.current.getUniqueName( node.name ); + const name = block.getUniqueName( node.name ); - const local = { - name, - namespace: node.name === 'svg' ? 'http://www.w3.org/2000/svg' : generator.current.namespace, - isComponent: false, + const local = { + name, + namespace: node.name === 'svg' ? 'http://www.w3.org/2000/svg' : state.namespace, + isComponent: false, - allUsedContexts: [], + allUsedContexts: [], - init: new CodeBuilder(), - update: new CodeBuilder(), - teardown: new CodeBuilder() - }; + create: new CodeBuilder(), + update: new CodeBuilder(), + destroy: new CodeBuilder() + }; - const isToplevel = generator.current.localElementDepth === 0; + const isToplevel = !state.parentNode; - addElementAttributes( generator, node, local ); + addElementAttributes( generator, block, node, local ); - if ( local.allUsedContexts.length ) { - const initialProps = local.allUsedContexts.map( contextName => { - if ( contextName === 'root' ) return `root: root`; + if ( local.allUsedContexts.length ) { + const initialProps = local.allUsedContexts.map( contextName => { + if ( contextName === 'root' ) return `root: root`; - const listName = generator.current.listNames.get( contextName ); - const indexName = generator.current.indexNames.get( contextName ); + const listName = block.listNames.get( contextName ); + const indexName = block.indexNames.get( contextName ); - return `${listName}: ${listName},\n${indexName}: ${indexName}`; - }).join( ',\n' ); + return `${listName}: ${listName},\n${indexName}: ${indexName}`; + }).join( ',\n' ); - const updates = local.allUsedContexts.map( contextName => { - if ( contextName === 'root' ) return `${name}.__svelte.root = root;`; + const updates = local.allUsedContexts.map( contextName => { + if ( contextName === 'root' ) return `${name}.__svelte.root = root;`; - const listName = generator.current.listNames.get( contextName ); - const indexName = generator.current.indexNames.get( contextName ); + const listName = block.listNames.get( contextName ); + const indexName = block.indexNames.get( contextName ); - return `${name}.__svelte.${listName} = ${listName};\n${name}.__svelte.${indexName} = ${indexName};`; - }).join( '\n' ); + return `${name}.__svelte.${listName} = ${listName};\n${name}.__svelte.${indexName} = ${indexName};`; + }).join( '\n' ); - local.init.addBlock( deindent` - ${name}.__svelte = { - ${initialProps} - }; - ` ); + local.create.addBlock( deindent` + ${name}.__svelte = { + ${initialProps} + }; + ` ); - local.update.addBlock( updates ); - } + local.update.addBlock( updates ); + } - let render; + let render; - if ( local.namespace ) { - if ( local.namespace === 'http://www.w3.org/2000/svg' ) { - render = `var ${name} = ${generator.helper( 'createSvgElement' )}( '${node.name}' )`; - } else { - render = `var ${name} = document.createElementNS( '${local.namespace}', '${node.name}' );`; - } + if ( local.namespace ) { + if ( local.namespace === 'http://www.w3.org/2000/svg' ) { + render = `var ${name} = ${generator.helper( 'createSvgElement' )}( '${node.name}' )`; } else { - render = `var ${name} = ${generator.helper( 'createElement' )}( '${node.name}' );`; + render = `var ${name} = document.createElementNS( '${local.namespace}', '${node.name}' );`; } + } else { + render = `var ${name} = ${generator.helper( 'createElement' )}( '${node.name}' );`; + } - if ( generator.cssId && !generator.elementDepth ) { - render += `\n${generator.helper( 'setAttribute' )}( ${name}, '${generator.cssId}', '' );`; - } + if ( generator.cssId && state.isTopLevel ) { + render += `\n${generator.helper( 'setAttribute' )}( ${name}, '${generator.cssId}', '' );`; + } - local.init.addLineAtStart( render ); - if ( isToplevel ) { - generator.current.builders.detach.addLine( `${generator.helper( 'detachNode' )}( ${name} );` ); - } + local.create.addLineAtStart( render ); + if ( isToplevel ) { + block.builders.detach.addLine( `${generator.helper( 'detachNode' )}( ${name} );` ); + } - // special case – bound