Merge pull request #453 from sveltejs/refactor

Refactor
pull/457/head
Rich Harris 9 years ago committed by GitHub
commit ec709cb490

@ -1,3 +1,5 @@
src/shared src/shared
shared.js shared.js
test/test.js test/test.js
**/_actual.js
**/expected.js

@ -12,11 +12,10 @@ import processCss from './shared/processCss.js';
import annotateWithScopes from './annotateWithScopes.js'; import annotateWithScopes from './annotateWithScopes.js';
export default class Generator { export default class Generator {
constructor ( parsed, source, name, visitors, options ) { constructor ( parsed, source, name, options ) {
this.parsed = parsed; this.parsed = parsed;
this.source = source; this.source = source;
this.name = name; this.name = name;
this.visitors = visitors;
this.options = options; this.options = options;
this.imports = []; this.imports = [];
@ -31,8 +30,6 @@ export default class Generator {
// in dev mode // in dev mode
this.expectedProperties = new Set(); this.expectedProperties = new Set();
this.elementDepth = 0;
this.code = new MagicString( source ); this.code = new MagicString( source );
this.css = parsed.css ? processCss( parsed, this.code ) : null; this.css = parsed.css ? processCss( parsed, this.code ) : null;
this.cssId = parsed.css ? `svelte-${parsed.hash}` : ''; this.cssId = parsed.css ? `svelte-${parsed.hash}` : '';
@ -43,8 +40,6 @@ export default class Generator {
this.importedNames = new Set(); this.importedNames = new Set();
this._aliases = new Map(); this._aliases = new Map();
this._usedNames = new Set(); this._usedNames = new Set();
this._callbacks = new Map();
} }
addSourcemapLocations ( node ) { addSourcemapLocations ( node ) {
@ -65,14 +60,14 @@ export default class Generator {
return alias; return alias;
} }
contextualise ( expression, isEventHandler ) { contextualise ( fragment, expression, isEventHandler ) {
this.addSourcemapLocations( expression ); this.addSourcemapLocations( expression );
const usedContexts = []; const usedContexts = [];
const dependencies = []; const dependencies = [];
const { code, helpers } = this; const { code, helpers } = this;
const { contextDependencies, contexts, indexes } = this.current; const { contextDependencies, contexts, indexes } = fragment;
let scope = annotateWithScopes( expression ); 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 } ) { generate ( result, options, { name, format } ) {
if ( this.imports.length ) { if ( this.imports.length ) {
const statements = []; const statements = [];
@ -430,50 +416,4 @@ export default class Generator {
templateProperties 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 );
}
} }

@ -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;
}
}

@ -1,14 +1,14 @@
import deindent from '../../utils/deindent.js'; import deindent from '../../utils/deindent.js';
import getBuilders from './utils/getBuilders.js';
import CodeBuilder from '../../utils/CodeBuilder.js'; import CodeBuilder from '../../utils/CodeBuilder.js';
import visitors from './visitors/index.js'; import visit from './visit.js';
import Generator from '../Generator.js'; import Generator from '../Generator.js';
import Block from './Block.js';
import * as shared from '../../shared/index.js'; import * as shared from '../../shared/index.js';
class DomGenerator extends Generator { class DomGenerator extends Generator {
constructor ( parsed, source, name, visitors, options ) { constructor ( parsed, source, name, options ) {
super( parsed, source, name, visitors, options ); super( parsed, source, name, options );
this.renderers = []; this.blocks = [];
this.uses = new Set(); this.uses = new Set();
// initial values for e.g. window.innerWidth, if there's a <:Window> meta tag // 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 ) { addBlock ( block ) {
const isToplevel = this.current.localElementDepth === 0; this.blocks.push( block );
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 = [];
} }
helper ( name ) { helper ( name ) {
@ -149,19 +36,16 @@ export default function dom ( parsed, source, options ) {
const format = options.format || 'es'; const format = options.format || 'es';
const name = options.name || 'SvelteComponent'; 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 { computations, hasJs, templateProperties, namespace } = generator.parseJs();
const getUniqueName = generator.getUniqueNameMaker( [ 'root' ] ); const getUniqueName = generator.getUniqueNameMaker( [ 'root' ] );
const component = getUniqueName( 'component' ); const component = getUniqueName( 'component' );
generator.push({ const mainBlock = new Block({
type: 'block', generator,
name: generator.alias( 'render_main_fragment' ), name: generator.alias( 'create_main_fragment' ),
namespace,
target: 'target',
localElementDepth: 0,
key: null, key: null,
component, component,
@ -173,13 +57,20 @@ export default function dom ( parsed, source, options ) {
indexNames: new Map(), indexNames: new Map(),
listNames: 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 = { const builders = {
main: new CodeBuilder(), main: new CodeBuilder(),
@ -243,8 +134,8 @@ export default function dom ( parsed, source, options ) {
` ); ` );
} }
let i = generator.renderers.length; let i = generator.blocks.length;
while ( i-- ) builders.main.addBlock( generator.renderers[i] ); while ( i-- ) builders.main.addBlock( generator.blocks[i].render() );
builders.init.addLine( `this._torndown = false;` ); builders.init.addLine( `this._torndown = false;` );
@ -259,7 +150,7 @@ export default function dom ( parsed, source, options ) {
if ( generator.hasComplexBindings ) { if ( generator.hasComplexBindings ) {
builders.init.addBlock( deindent` builders.init.addBlock( deindent`
this._bindings = []; 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 ); if ( options.target ) this._fragment.mount( options.target, null );
while ( this._bindings.length ) this._bindings.pop()(); 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()();` ); builders._set.addLine( `while ( this._bindings.length ) this._bindings.pop()();` );
} else { } else {
builders.init.addBlock( deindent` 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 ); 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 ) { ${name}.prototype.teardown = ${name}.prototype.destroy = function destroy ( detach ) {
this.fire( 'destroy' );${templateProperties.ondestroy ? `\n${generator.alias( 'template' )}.ondestroy.call( this );` : ``} 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._fragment = null;
this._state = {}; this._state = {};

@ -1,4 +0,0 @@
export default function findBlock ( fragment ) {
while ( fragment.type !== 'block' ) fragment = fragment.parent;
return fragment;
}

@ -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()
};
}

@ -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 );
}

@ -1,3 +1,3 @@
export default { export default function visitComment () {
// do nothing // do nothing
}; }

@ -1,5 +1,6 @@
import deindent from '../../../utils/deindent.js'; import deindent from '../../../utils/deindent.js';
import CodeBuilder from '../../../utils/CodeBuilder.js'; import CodeBuilder from '../../../utils/CodeBuilder.js';
import visit from '../visit.js';
import addComponentAttributes from './attributes/addComponentAttributes.js'; import addComponentAttributes from './attributes/addComponentAttributes.js';
function capDown ( name ) { function capDown ( name ) {
@ -18,157 +19,152 @@ function stringifyProps ( props ) {
return `{ ${joined} }`; return `{ ${joined} }`;
} }
export default { export default function visitComponent ( generator, block, state, node ) {
enter ( generator, node ) { const hasChildren = node.children.length > 0;
const hasChildren = node.children.length > 0; const name = block.getUniqueName( capDown( node.name === ':Self' ? generator.name : node.name ) );
const { current } = generator;
const name = current.getUniqueName( capDown( node.name === ':Self' ? generator.name : node.name ) );
const local = { const local = {
name, name,
namespace: current.namespace, namespace: state.namespace,
isComponent: true, isComponent: true,
allUsedContexts: [], allUsedContexts: [],
init: new CodeBuilder(), create: new CodeBuilder(),
update: 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 ) { if ( local.allUsedContexts.length ) {
const initialProps = local.allUsedContexts.map( contextName => { const initialProps = local.allUsedContexts.map( contextName => {
if ( contextName === 'root' ) return `root: root`; if ( contextName === 'root' ) return `root: root`;
const listName = current.listNames.get( contextName ); const listName = block.listNames.get( contextName );
const indexName = current.indexNames.get( contextName ); const indexName = block.indexNames.get( contextName );
return `${listName}: ${listName},\n${indexName}: ${indexName}`; return `${listName}: ${listName},\n${indexName}: ${indexName}`;
}).join( ',\n' ); }).join( ',\n' );
const updates = local.allUsedContexts.map( contextName => { const updates = local.allUsedContexts.map( contextName => {
if ( contextName === 'root' ) return `${name}._context.root = root;`; if ( contextName === 'root' ) return `${name}._context.root = root;`;
const listName = current.listNames.get( contextName ); const listName = block.listNames.get( contextName );
const indexName = current.indexNames.get( contextName ); const indexName = block.indexNames.get( contextName );
return `${name}._context.${listName} = ${listName};\n${name}._context.${indexName} = ${indexName};`; return `${name}._context.${listName} = ${listName};\n${name}._context.${indexName} = ${indexName};`;
}).join( '\n' ); }).join( '\n' );
local.init.addBlock( deindent` local.create.addBlock( deindent`
${name}._context = { ${name}._context = {
${initialProps} ${initialProps}
}; };
` ); ` );
local.update.addBlock( updates ); local.update.addBlock( updates );
} }
const componentInitProperties = [ const componentInitProperties = [
`target: ${!isToplevel ? current.target: 'null'}`, `target: ${!isToplevel ? state.parentNode: 'null'}`,
`_root: ${current.component}._root || ${current.component}` `_root: ${block.component}._root || ${block.component}`
]; ];
// Component has children, put them in a separate {{yield}} block // Component has children, put them in a separate {{yield}} block
if ( hasChildren ) { if ( hasChildren ) {
const yieldName = generator.getUniqueName( `render_${name}_yield_fragment` ); const params = block.params.join( ', ' );
const params = current.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( node.children.forEach( child => {
`var ${yieldFragment} = ${yieldName}( ${params}, ${current.component} );` visit( generator, childBlock, childState, child );
); });
current.builders.update.addLine( const yieldFragment = block.getUniqueName( `${name}_yield_fragment` );
`${yieldFragment}.update( changed, ${params} );`
);
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 ) { componentInitProperties.push( `_yield: ${yieldFragment}`);
const initialProps = local.staticAttributes
.concat( local.dynamicAttributes )
.map( attribute => `${attribute.name}: ${attribute.value}` );
const initialPropString = stringifyProps( initialProps ); generator.addBlock( childBlock );
}
if ( local.bindings.length ) { const statements = [];
const initialData = current.getUniqueName( `${name}_initial_data` );
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 => { const initialPropString = stringifyProps( initialProps );
statements.push( `if ( ${binding.prop} in ${binding.obj} ) ${initialData}.${binding.name} = ${binding.value};` );
});
componentInitProperties.push( `data: ${initialData}` ); if ( local.bindings.length ) {
} else if ( initialProps.length ) { const initialData = block.getUniqueName( `${name}_initial_data` );
componentInitProperties.push( `data: ${initialPropString}` );
}
}
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` local.bindings.forEach( binding => {
${statements.join( '\n' )} statements.push( `if ( ${binding.prop} in ${binding.obj} ) ${initialData}.${binding.name} = ${binding.value};` );
var ${name} = new ${expression}({
${componentInitProperties.join(',\n')}
}); });
` );
if ( isToplevel ) { componentInitProperties.push( `data: ${initialData}` );
current.builders.mount.addLine( `${name}._fragment.mount( target, anchor );` ); } else if ( initialProps.length ) {
componentInitProperties.push( `data: ${initialPropString}` );
} }
}
if ( local.dynamicAttributes.length ) { const expression = node.name === ':Self' ? generator.name : generator.importedComponents.get( node.name ) || `${generator.alias( 'template' )}.components.${node.name}`;
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};`;
});
local.update.addBlock( deindent` local.create.addBlockAtStart( deindent`
var ${name}_changes = {}; ${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 ); local.update.addBlock( deindent`
if ( !local.update.isEmpty() ) current.builders.update.addBlock( local.update ); var ${name}_changes = {};
generator.push({ ${updates.join( '\n' )}
type: 'component',
namespace: local.namespace,
target: name,
parent: current,
localElementDepth: current.localElementDepth + 1,
key: null
});
},
leave ( generator ) { if ( Object.keys( ${name}_changes ).length ) ${name}.set( ${name}_changes );
generator.pop(); ` );
} }
};
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 );
}

@ -1,223 +1,220 @@
import CodeBuilder from '../../../utils/CodeBuilder.js'; import CodeBuilder from '../../../utils/CodeBuilder.js';
import deindent from '../../../utils/deindent.js'; import deindent from '../../../utils/deindent.js';
import getBuilders from '../utils/getBuilders.js'; import visit from '../visit.js';
export default { export default function visitEachBlock ( generator, block, state, node ) {
enter ( generator, node ) { const name = generator.getUniqueName( `each_block` );
const name = generator.getUniqueName( `each_block` ); const renderer = generator.getUniqueName( `create_each_block` );
const renderer = generator.getUniqueName( `render_each_block` ); const elseName = generator.getUniqueName( `${name}_else` );
const elseName = generator.getUniqueName( `${name}_else` ); const renderElse = generator.getUniqueName( `${renderer}_else` );
const renderElse = generator.getUniqueName( `${renderer}_else` ); const i = block.getUniqueName( `i` );
const i = generator.current.getUniqueName( `i` ); const params = block.params.join( ', ' );
const params = generator.current.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` ); const localVars = {};
generator.createAnchor( anchor );
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` ); block.builders.create.addLine( `var ${listName} = ${snippet};` );
localVars.iterations = generator.current.getUniqueName( `${name}_iterations` ); block.builders.create.addLine( `var ${localVars.iterations} = [];` );
localVars._iterations = generator.current.getUniqueName( `_${name}_iterations` ); if ( node.key ) block.builders.create.addLine( `var ${localVars.lookup} = Object.create( null );` );
localVars.lookup = generator.current.getUniqueName( `${name}_lookup` ); if ( node.else ) block.builders.create.addLine( `var ${elseName} = null;` );
localVars._lookup = generator.current.getUniqueName( `_${name}_lookup` );
generator.current.builders.init.addLine( `var ${listName} = ${snippet};` ); const initialRender = new CodeBuilder();
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(); if ( node.key ) {
localVars.fragment = block.getUniqueName( 'fragment' );
localVars.value = block.getUniqueName( 'value' );
localVars.key = block.getUniqueName( 'key' );
if ( node.key ) { initialRender.addBlock( deindent`
localVars.fragment = generator.current.getUniqueName( 'fragment' ); var ${localVars.key} = ${listName}[${i}].${node.key};
localVars.value = generator.current.getUniqueName( 'value' ); ${localVars.iterations}[${i}] = ${localVars.lookup}[ ${localVars.key} ] = ${renderer}( ${params}, ${listName}, ${listName}[${i}], ${i}, ${block.component}${node.key ? `, ${localVars.key}` : `` } );
localVars.key = generator.current.getUniqueName( 'key' ); ` );
} else {
initialRender.addLine(
`${localVars.iterations}[${i}] = ${renderer}( ${params}, ${listName}, ${listName}[${i}], ${i}, ${block.component} );`
);
}
initialRender.addBlock( deindent` if ( !isToplevel ) {
var ${localVars.key} = ${listName}[${i}].${node.key}; initialRender.addLine(
${localVars.iterations}[${i}] = ${localVars.lookup}[ ${localVars.key} ] = ${renderer}( ${params}, ${listName}, ${listName}[${i}], ${i}, ${generator.current.component}${node.key ? `, ${localVars.key}` : `` } ); `${localVars.iterations}[${i}].mount( ${anchor}.parentNode, ${anchor} );`
` ); );
} else { }
initialRender.addLine(
`${localVars.iterations}[${i}] = ${renderer}( ${params}, ${listName}, ${listName}[${i}], ${i}, ${generator.current.component} );`
);
}
if ( !isToplevel ) { block.builders.create.addBlock( deindent`
initialRender.addLine( for ( var ${i} = 0; ${i} < ${listName}.length; ${i} += 1 ) {
`${localVars.iterations}[${i}].mount( ${anchor}.parentNode, ${anchor} );` ${initialRender}
);
} }
` );
generator.current.builders.init.addBlock( deindent` if ( node.else ) {
for ( var ${i} = 0; ${i} < ${listName}.length; ${i} += 1 ) { block.builders.create.addBlock( deindent`
${initialRender} 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 ) { if ( node.else ) {
generator.current.builders.init.addBlock( deindent` block.builders.mount.addBlock( deindent`
if ( !${listName}.length ) { if ( ${elseName} ) {
${elseName} = ${renderElse}( ${params}, ${generator.current.component} ); ${elseName}.mount( ${anchor}.parentNode, ${anchor} );
${!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} );
} }
` ); ` );
if ( node.else ) {
generator.current.builders.mount.addBlock( deindent`
if ( ${elseName} ) {
${elseName}.mount( ${anchor}.parentNode, ${anchor} );
}
` );
}
} }
}
if ( node.key ) { if ( node.key ) {
generator.current.builders.update.addBlock( deindent` block.builders.update.addBlock( deindent`
var ${listName} = ${snippet}; var ${listName} = ${snippet};
var ${localVars._iterations} = []; var ${localVars._iterations} = [];
var ${localVars._lookup} = Object.create( null ); var ${localVars._lookup} = Object.create( null );
var ${localVars.fragment} = document.createDocumentFragment(); var ${localVars.fragment} = document.createDocumentFragment();
// create new iterations as necessary // create new iterations as necessary
for ( var ${i} = 0; ${i} < ${listName}.length; ${i} += 1 ) { for ( var ${i} = 0; ${i} < ${listName}.length; ${i} += 1 ) {
var ${localVars.value} = ${listName}[${i}]; var ${localVars.value} = ${listName}[${i}];
var ${localVars.key} = ${localVars.value}.${node.key}; 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}].mount( ${localVars.fragment}, null );
${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 ); // 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 ${anchor}.parentNode.insertBefore( ${localVars.fragment}, ${anchor} );
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} ); ${localVars.iterations} = ${localVars._iterations};
${localVars.lookup} = ${localVars._lookup};
` );
} else {
block.builders.update.addBlock( deindent`
var ${listName} = ${snippet};
${localVars.iterations} = ${localVars._iterations}; for ( var ${i} = 0; ${i} < ${listName}.length; ${i} += 1 ) {
${localVars.lookup} = ${localVars._lookup}; if ( !${localVars.iterations}[${i}] ) {
` ); ${localVars.iterations}[${i}] = ${renderer}( ${params}, ${listName}, ${listName}[${i}], ${i}, ${block.component} );
} else { ${localVars.iterations}[${i}].mount( ${anchor}.parentNode, ${anchor} );
generator.current.builders.update.addBlock( deindent` } else {
var ${listName} = ${snippet}; ${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}, ${generator.current.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 ) { if ( node.else ) {
generator.current.builders.update.addBlock( deindent` block.builders.update.addBlock( deindent`
if ( !${listName}.length && ${elseName} ) { if ( !${listName}.length && ${elseName} ) {
${elseName}.update( changed, ${params} ); ${elseName}.update( changed, ${params} );
} else if ( !${listName}.length ) { } else if ( !${listName}.length ) {
${elseName} = ${renderElse}( ${params}, ${generator.current.component} ); ${elseName} = ${renderElse}( ${params}, ${block.component} );
${elseName}.mount( ${anchor}.parentNode, ${anchor} ); ${elseName}.mount( ${anchor}.parentNode, ${anchor} );
} else if ( ${elseName} ) { } else if ( ${elseName} ) {
${elseName}.teardown( true ); ${elseName}.destroy( true );
} }
` ); ` );
} }
generator.current.builders.teardown.addBlock( block.builders.destroy.addBlock(
`${generator.helper( 'teardownEach' )}( ${localVars.iterations}, ${isToplevel ? 'detach' : 'false'} );` ); `${generator.helper( 'destroyEach' )}( ${localVars.iterations}, ${isToplevel ? 'detach' : 'false'} );` );
if ( node.else ) { if ( node.else ) {
generator.current.builders.teardown.addBlock( deindent` block.builders.destroy.addBlock( deindent`
if ( ${elseName} ) { if ( ${elseName} ) {
${elseName}.teardown( ${isToplevel ? 'detach' : 'false'} ); ${elseName}.destroy( ${isToplevel ? 'detach' : 'false'} );
} }
` ); ` );
} }
if ( node.else ) { const indexNames = new Map( block.indexNames );
generator.generateBlock( node.else, renderElse, 'block' ); const indexName = node.index || block.getUniqueName( `${node.context}_index` );
} indexNames.set( node.context, indexName );
const indexNames = new Map( generator.current.indexNames ); const listNames = new Map( block.listNames );
const indexName = node.index || generator.current.getUniqueName( `${node.context}_index` ); listNames.set( node.context, listName );
indexNames.set( node.context, indexName );
const listNames = new Map( generator.current.listNames ); const context = generator.getUniqueName( node.context );
listNames.set( node.context, listName ); const contexts = new Map( block.contexts );
contexts.set( node.context, context );
const context = generator.getUniqueName( node.context ); const indexes = new Map( block.indexes );
const contexts = new Map( generator.current.contexts ); if ( node.index ) indexes.set( indexName, node.context );
contexts.set( node.context, context );
const indexes = new Map( generator.current.indexes ); const contextDependencies = new Map( block.contextDependencies );
if ( node.index ) indexes.set( indexName, node.context ); contextDependencies.set( node.context, dependencies );
const contextDependencies = new Map( generator.current.contextDependencies ); const childBlock = block.child({
contextDependencies.set( node.context, dependencies ); 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({ const childState = Object.assign( {}, state, {
type: 'block', parentNode: null
name: renderer, });
target: 'target',
expression: node.expression,
context: node.context,
key: node.key,
localElementDepth: 0,
component: getUniqueName( 'component' ), node.children.forEach( child => {
visit( generator, childBlock, childState, child );
});
contextDependencies, generator.addBlock( childBlock );
contexts,
indexes,
indexNames, if ( node.else ) {
listNames, const childBlock = block.child({
params: blockParams, name: renderElse
});
builders: getBuilders(), node.else.children.forEach( child => {
getUniqueName, visit( generator, childBlock, childState, child );
}); });
},
leave ( generator ) { generator.addBlock( childBlock );
generator.addRenderer( generator.current );
generator.pop();
} }
}; }

@ -1,131 +1,114 @@
import CodeBuilder from '../../../utils/CodeBuilder.js'; import CodeBuilder from '../../../utils/CodeBuilder.js';
import deindent from '../../../utils/deindent.js'; import deindent from '../../../utils/deindent.js';
import visit from '../visit.js';
import addElementAttributes from './attributes/addElementAttributes.js'; import addElementAttributes from './attributes/addElementAttributes.js';
import Component from './Component.js'; import visitComponent from './Component.js';
import Window from './meta/Window.js'; import visitWindow from './meta/Window.js';
const meta = { const meta = {
':Window': Window ':Window': visitWindow
}; };
export default { export default function visitElement ( generator, block, state, node ) {
enter ( generator, node ) { if ( node.name in meta ) {
if ( node.name in meta ) { return meta[ node.name ]( generator, block, node );
return meta[ node.name ].enter( generator, node ); }
}
const isComponent = generator.components.has( node.name ) || node.name === ':Self';
if ( isComponent ) { if ( generator.components.has( node.name ) || node.name === ':Self' ) {
return Component.enter( generator, node ); return visitComponent( generator, block, state, node );
} }
const name = generator.current.getUniqueName( node.name ); const name = block.getUniqueName( node.name );
const local = { const local = {
name, name,
namespace: node.name === 'svg' ? 'http://www.w3.org/2000/svg' : generator.current.namespace, namespace: node.name === 'svg' ? 'http://www.w3.org/2000/svg' : state.namespace,
isComponent: false, isComponent: false,
allUsedContexts: [], allUsedContexts: [],
init: new CodeBuilder(), create: new CodeBuilder(),
update: new CodeBuilder(), update: new CodeBuilder(),
teardown: 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 ) { if ( local.allUsedContexts.length ) {
const initialProps = local.allUsedContexts.map( contextName => { const initialProps = local.allUsedContexts.map( contextName => {
if ( contextName === 'root' ) return `root: root`; if ( contextName === 'root' ) return `root: root`;
const listName = generator.current.listNames.get( contextName ); const listName = block.listNames.get( contextName );
const indexName = generator.current.indexNames.get( contextName ); const indexName = block.indexNames.get( contextName );
return `${listName}: ${listName},\n${indexName}: ${indexName}`; return `${listName}: ${listName},\n${indexName}: ${indexName}`;
}).join( ',\n' ); }).join( ',\n' );
const updates = local.allUsedContexts.map( contextName => { const updates = local.allUsedContexts.map( contextName => {
if ( contextName === 'root' ) return `${name}.__svelte.root = root;`; if ( contextName === 'root' ) return `${name}.__svelte.root = root;`;
const listName = generator.current.listNames.get( contextName ); const listName = block.listNames.get( contextName );
const indexName = generator.current.indexNames.get( contextName ); const indexName = block.indexNames.get( contextName );
return `${name}.__svelte.${listName} = ${listName};\n${name}.__svelte.${indexName} = ${indexName};`; return `${name}.__svelte.${listName} = ${listName};\n${name}.__svelte.${indexName} = ${indexName};`;
}).join( '\n' ); }).join( '\n' );
local.init.addBlock( deindent` local.create.addBlock( deindent`
${name}.__svelte = { ${name}.__svelte = {
${initialProps} ${initialProps}
}; };
` ); ` );
local.update.addBlock( updates ); local.update.addBlock( updates );
} }
let render; let render;
if ( local.namespace ) { if ( local.namespace ) {
if ( local.namespace === 'http://www.w3.org/2000/svg' ) { if ( local.namespace === 'http://www.w3.org/2000/svg' ) {
render = `var ${name} = ${generator.helper( 'createSvgElement' )}( '${node.name}' )`; render = `var ${name} = ${generator.helper( 'createSvgElement' )}( '${node.name}' )`;
} else {
render = `var ${name} = document.createElementNS( '${local.namespace}', '${node.name}' );`;
}
} else { } 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 ) { if ( generator.cssId && state.isTopLevel ) {
render += `\n${generator.helper( 'setAttribute' )}( ${name}, '${generator.cssId}', '' );`; render += `\n${generator.helper( 'setAttribute' )}( ${name}, '${generator.cssId}', '' );`;
} }
local.init.addLineAtStart( render ); local.create.addLineAtStart( render );
if ( isToplevel ) { if ( isToplevel ) {
generator.current.builders.detach.addLine( `${generator.helper( 'detachNode' )}( ${name} );` ); block.builders.detach.addLine( `${generator.helper( 'detachNode' )}( ${name} );` );
} }
// special case bound <option> without a value attribute // special case bound <option> without a value attribute
if ( node.name === 'option' && !node.attributes.find( attribute => attribute.type === 'Attribute' && attribute.name === 'value' ) ) { // TODO check it's bound if ( node.name === 'option' && !node.attributes.find( attribute => attribute.type === 'Attribute' && attribute.name === 'value' ) ) { // TODO check it's bound
const statement = `${name}.__value = ${name}.textContent;`; const statement = `${name}.__value = ${name}.textContent;`;
local.update.addLine( statement ); local.update.addLine( statement );
node.initialUpdate = statement; node.initialUpdate = statement;
} }
generator.current.builders.init.addBlock( local.init ); block.builders.create.addBlock( local.create );
if ( !local.update.isEmpty() ) generator.current.builders.update.addBlock( local.update ); if ( !local.update.isEmpty() ) block.builders.update.addBlock( local.update );
if ( !local.teardown.isEmpty() ) generator.current.builders.teardown.addBlock( local.teardown ); if ( !local.destroy.isEmpty() ) block.builders.destroy.addBlock( local.destroy );
generator.createMountStatement( name );
generator.push({
type: 'element',
namespace: local.namespace,
target: name,
parent: generator.current,
localElementDepth: generator.current.localElementDepth + 1,
key: null
});
},
leave ( generator, node ) {
if ( node.name in meta ) {
if ( meta[ node.name ].leave ) meta[ node.name ].leave( generator, node );
return;
}
const isComponent = generator.components.has( node.name ); block.createMountStatement( name, state.parentNode );
if ( isComponent ) { const childState = Object.assign( {}, state, {
return Component.leave( generator, node ); isTopLevel: false,
} parentNode: name,
namespace: local.namespace
});
if ( node.initialUpdate ) { node.children.forEach( child => {
generator.current.builders.init.addBlock( node.initialUpdate ); visit( generator, block, childState, child );
} });
generator.pop(); if ( node.initialUpdate ) {
block.builders.create.addBlock( node.initialUpdate );
} }
}; }

@ -1,20 +1,20 @@
import deindent from '../../../utils/deindent.js'; import deindent from '../../../utils/deindent.js';
import visit from '../visit.js';
function getConditionsAndBlocks ( generator, node, _name, i = 0 ) { function getConditionsAndBlocks ( generator, block, state, node, _name, i = 0 ) {
generator.addSourcemapLocations( node.expression );
const name = generator.getUniqueName( `${_name}_${i}` ); const name = generator.getUniqueName( `${_name}_${i}` );
const conditionsAndBlocks = [{ const conditionsAndBlocks = [{
condition: generator.contextualise( node.expression ).snippet, condition: generator.contextualise( block, node.expression ).snippet,
block: name block: name
}]; }];
generator.generateBlock( node, name, 'block' ); generateBlock( generator, block, state, node, name );
if ( node.else && node.else.children.length === 1 && if ( node.else && node.else.children.length === 1 &&
node.else.children[0].type === 'IfBlock' ) { node.else.children[0].type === 'IfBlock' ) {
conditionsAndBlocks.push( conditionsAndBlocks.push(
...getConditionsAndBlocks( generator, node.else.children[0], _name, i + 1 ) ...getConditionsAndBlocks( generator, block, state, node.else.children[0], _name, i + 1 )
); );
} else { } else {
const name = generator.getUniqueName( `${_name}_${i + 1}` ); const name = generator.getUniqueName( `${_name}_${i + 1}` );
@ -24,58 +24,72 @@ function getConditionsAndBlocks ( generator, node, _name, i = 0 ) {
}); });
if ( node.else ) { if ( node.else ) {
generator.generateBlock( node.else, name, 'block' ); generateBlock( generator, block, state, node.else, name );
} }
} }
return conditionsAndBlocks; return conditionsAndBlocks;
} }
export default { function generateBlock ( generator, block, state, node, name ) {
enter ( generator, node ) { const childBlock = block.child({
const params = generator.current.params.join( ', ' ); name
const name = generator.getUniqueName( `if_block` ); });
const getBlock = generator.current.getUniqueName( `get_block` );
const currentBlock = generator.current.getUniqueName( `current_block` ); const childState = Object.assign( {}, state, {
const _currentBlock = generator.current.getUniqueName( `_current_block` ); parentNode: null
});
const isToplevel = generator.current.localElementDepth === 0;
const conditionsAndBlocks = getConditionsAndBlocks( generator, node, generator.getUniqueName( `render_if_block` ) ); node.children.forEach( node => {
visit( generator, childBlock, childState, node );
const anchor = `${name}_anchor`; });
generator.createAnchor( anchor );
generator.addBlock( childBlock );
generator.current.builders.init.addBlock( deindent` }
function ${getBlock} ( ${params} ) {
${conditionsAndBlocks.map( ({ condition, block }) => { export default function visitIfBlock ( generator, block, state, node ) {
return `${condition ? `if ( ${condition} ) ` : ''}return ${block};`; const params = block.params.join( ', ' );
} ).join( '\n' )} const name = generator.getUniqueName( `if_block` );
} const getBlock = block.getUniqueName( `get_block` );
const currentBlock = block.getUniqueName( `current_block` );
var ${currentBlock} = ${getBlock}( ${params} ); const _currentBlock = block.getUniqueName( `_current_block` );
var ${name} = ${currentBlock} && ${currentBlock}( ${params}, ${generator.current.component} );
` ); const isToplevel = !state.parentNode;
const conditionsAndBlocks = getConditionsAndBlocks( generator, block, state, node, generator.getUniqueName( `create_if_block` ) );
const mountStatement = `if ( ${name} ) ${name}.mount( ${anchor}.parentNode, ${anchor} );`;
if ( isToplevel ) { const anchor = `${name}_anchor`;
generator.current.builders.mount.addLine( mountStatement ); block.createAnchor( anchor, state.parentNode );
} else {
generator.current.builders.init.addLine( mountStatement ); block.builders.create.addBlock( deindent`
function ${getBlock} ( ${params} ) {
${conditionsAndBlocks.map( ({ condition, block }) => {
return `${condition ? `if ( ${condition} ) ` : ''}return ${block};`;
} ).join( '\n' )}
} }
generator.current.builders.update.addBlock( deindent` var ${currentBlock} = ${getBlock}( ${params} );
var ${_currentBlock} = ${currentBlock}; var ${name} = ${currentBlock} && ${currentBlock}( ${params}, ${block.component} );
${currentBlock} = ${getBlock}( ${params} ); ` );
if ( ${_currentBlock} === ${currentBlock} && ${name}) {
${name}.update( changed, ${params} ); const mountStatement = `if ( ${name} ) ${name}.mount( ${anchor}.parentNode, ${anchor} );`;
} else { if ( isToplevel ) {
if ( ${name} ) ${name}.teardown( true ); block.builders.mount.addLine( mountStatement );
${name} = ${currentBlock} && ${currentBlock}( ${params}, ${generator.current.component} ); } else {
if ( ${name} ) ${name}.mount( ${anchor}.parentNode, ${anchor} ); block.builders.create.addLine( mountStatement );
}
` );
generator.current.builders.teardown.addLine(
`if ( ${name} ) ${name}.teardown( ${isToplevel ? 'detach' : 'false'} );`
);
} }
};
block.builders.update.addBlock( deindent`
var ${_currentBlock} = ${currentBlock};
${currentBlock} = ${getBlock}( ${params} );
if ( ${_currentBlock} === ${currentBlock} && ${name}) {
${name}.update( changed, ${params} );
} else {
if ( ${name} ) ${name}.destroy( true );
${name} = ${currentBlock} && ${currentBlock}( ${params}, ${block.component} );
if ( ${name} ) ${name}.mount( ${anchor}.parentNode, ${anchor} );
}
` );
block.builders.destroy.addLine(
`if ( ${name} ) ${name}.destroy( ${isToplevel ? 'detach' : 'false'} );`
);
}

@ -1,22 +1,16 @@
import deindent from '../../../utils/deindent.js'; import deindent from '../../../utils/deindent.js';
import findBlock from '../utils/findBlock.js';
export default { export default function visitMustacheTag ( generator, block, state, node ) {
enter ( generator, node ) { const name = block.getUniqueName( 'text' );
const name = generator.current.getUniqueName( 'text' );
const { snippet } = generator.contextualise( node.expression ); const { snippet } = generator.contextualise( block, node.expression );
generator.current.builders.init.addLine( `var last_${name} = ${snippet};` ); block.builders.create.addLine( `var last_${name} = ${snippet};` );
generator.addElement( name, `${generator.helper( 'createText' )}( last_${name} )`, true ); block.addElement( name, `${generator.helper( 'createText' )}( last_${name} )`, state.parentNode, true );
const fragment = findBlock( generator.current ); block.builders.update.addBlock( deindent`
if ( !fragment.tmp ) fragment.tmp = fragment.getUniqueName( 'tmp' ); if ( ( ${block.tmp()} = ${snippet} ) !== last_${name} ) {
${name}.data = last_${name} = ${block.tmp()};
generator.current.builders.update.addBlock( deindent` }
if ( ( ${fragment.tmp} = ${snippet} ) !== last_${name} ) { ` );
${name}.data = last_${name} = ${fragment.tmp}; }
}
` );
}
};

@ -1,43 +1,37 @@
import deindent from '../../../utils/deindent.js'; import deindent from '../../../utils/deindent.js';
import findBlock from '../utils/findBlock.js';
export default { export default function visitRawMustacheTag ( generator, block, state, node ) {
enter ( generator, node ) { const name = block.getUniqueName( 'raw' );
const name = generator.current.getUniqueName( 'raw' );
const { snippet } = generator.contextualise( node.expression ); const { snippet } = generator.contextualise( block, node.expression );
// we would have used comments here, but the `insertAdjacentHTML` api only // we would have used comments here, but the `insertAdjacentHTML` api only
// exists for `Element`s. // exists for `Element`s.
const before = `${name}_before`; const before = `${name}_before`;
generator.addElement( before, `${generator.helper( 'createElement' )}( 'noscript' )`, true ); block.addElement( before, `${generator.helper( 'createElement' )}( 'noscript' )`, state.parentNode, true );
const after = `${name}_after`; const after = `${name}_after`;
generator.addElement( after, `${generator.helper( 'createElement' )}( 'noscript' )`, true ); block.addElement( after, `${generator.helper( 'createElement' )}( 'noscript' )`, state.parentNode, true );
const isToplevel = generator.current.localElementDepth === 0; const isToplevel = !state.parentNode;
generator.current.builders.init.addLine( `var last_${name} = ${snippet};` ); block.builders.create.addLine( `var last_${name} = ${snippet};` );
const mountStatement = `${before}.insertAdjacentHTML( 'afterend', last_${name} );`; const mountStatement = `${before}.insertAdjacentHTML( 'afterend', last_${name} );`;
const detachStatement = `${generator.helper( 'detachBetween' )}( ${before}, ${after} );`; const detachStatement = `${generator.helper( 'detachBetween' )}( ${before}, ${after} );`;
if ( isToplevel ) { if ( isToplevel ) {
generator.current.builders.mount.addLine( mountStatement ); block.builders.mount.addLine( mountStatement );
} else { } else {
generator.current.builders.init.addLine( mountStatement ); block.builders.create.addLine( mountStatement );
} }
const fragment = findBlock( generator.current );
if ( !fragment.tmp ) fragment.tmp = fragment.getUniqueName( 'tmp' );
generator.current.builders.update.addBlock( deindent` block.builders.update.addBlock( deindent`
if ( ( ${fragment.tmp} = ${snippet} ) !== last_${name} ) { if ( ( ${block.tmp()} = ${snippet} ) !== last_${name} ) {
last_${name} = ${fragment.tmp}; last_${name} = ${block.tmp()};
${detachStatement} ${detachStatement}
${mountStatement} ${mountStatement}
} }
` ); ` );
generator.current.builders.detachRaw.addBlock( detachStatement ); block.builders.detachRaw.addBlock( detachStatement );
} }
};

@ -1,10 +1,8 @@
export default { export default function visitText ( generator, block, state, node ) {
enter ( generator, node ) { if ( state.namespace && !/\S/.test( node.data ) ) {
if ( generator.current.namespace && !/\S/.test( node.data ) ) { return;
return;
}
const name = generator.current.getUniqueName( `text` );
generator.addElement( name, `${generator.helper( 'createText' )}( ${JSON.stringify( node.data )} )`, false );
} }
};
const name = block.getUniqueName( `text` );
block.addElement( name, `${generator.helper( 'createText' )}( ${JSON.stringify( node.data )} )`, state.parentNode, false );
}

@ -1,14 +1,12 @@
export default { export default function visitYieldTag ( generator, block, state ) {
enter ( generator ) { const anchor = `yield_anchor`;
const anchor = `yield_anchor`; block.createAnchor( anchor, state.parentNode );
generator.createAnchor( anchor );
generator.current.builders.mount.addLine( block.builders.mount.addLine(
`${generator.current.component}._yield && ${generator.current.component}._yield.mount( ${generator.current.target}, ${anchor} );` `${block.component}._yield && ${block.component}._yield.mount( ${state.parentNode || 'target'}, ${anchor} );`
); );
generator.current.builders.teardown.addLine( block.builders.destroy.addLine(
`${generator.current.component}._yield && ${generator.current.component}._yield.teardown( detach );` `${block.component}._yield && ${block.component}._yield.destroy( detach );`
); );
} }
};

@ -1,7 +1,7 @@
import addComponentBinding from './addComponentBinding.js'; import addComponentBinding from './addComponentBinding.js';
import deindent from '../../../../utils/deindent.js'; import deindent from '../../../../utils/deindent.js';
export default function addComponentAttributes ( generator, node, local ) { export default function addComponentAttributes ( generator, block, node, local ) {
local.staticAttributes = []; local.staticAttributes = [];
local.dynamicAttributes = []; local.dynamicAttributes = [];
local.bindings = []; local.bindings = [];
@ -37,7 +37,7 @@ export default function addComponentAttributes ( generator, node, local ) {
else { else {
// simple dynamic attributes // simple dynamic attributes
const { dependencies, string } = generator.contextualise( value.expression ); const { dependencies, string } = generator.contextualise( block, value.expression );
// TODO only update attributes that have changed // TODO only update attributes that have changed
local.dynamicAttributes.push({ local.dynamicAttributes.push({
@ -57,7 +57,7 @@ export default function addComponentAttributes ( generator, node, local ) {
if ( chunk.type === 'Text' ) { if ( chunk.type === 'Text' ) {
return JSON.stringify( chunk.data ); return JSON.stringify( chunk.data );
} else { } else {
const { dependencies, string } = generator.contextualise( chunk.expression ); const { dependencies, string } = generator.contextualise( block, chunk.expression );
dependencies.forEach( dependency => { dependencies.forEach( dependency => {
if ( !~allDependencies.indexOf( dependency ) ) allDependencies.push( dependency ); if ( !~allDependencies.indexOf( dependency ) ) allDependencies.push( dependency );
}); });
@ -78,11 +78,11 @@ export default function addComponentAttributes ( generator, node, local ) {
else if ( attribute.type === 'EventHandler' ) { else if ( attribute.type === 'EventHandler' ) {
// TODO verify that it's a valid callee (i.e. built-in or declared method) // TODO verify that it's a valid callee (i.e. built-in or declared method)
generator.addSourcemapLocations( attribute.expression ); generator.addSourcemapLocations( attribute.expression );
generator.code.prependRight( attribute.expression.start, `${generator.current.component}.` ); generator.code.prependRight( attribute.expression.start, `${block.component}.` );
const usedContexts = []; const usedContexts = [];
attribute.expression.arguments.forEach( arg => { attribute.expression.arguments.forEach( arg => {
const { contexts } = generator.contextualise( arg, true ); const { contexts } = generator.contextualise( block, arg, true );
contexts.forEach( context => { contexts.forEach( context => {
if ( !~usedContexts.indexOf( context ) ) usedContexts.push( context ); if ( !~usedContexts.indexOf( context ) ) usedContexts.push( context );
@ -94,15 +94,15 @@ export default function addComponentAttributes ( generator, node, local ) {
const declarations = usedContexts.map( name => { const declarations = usedContexts.map( name => {
if ( name === 'root' ) return 'var root = this._context.root;'; if ( name === 'root' ) return 'var root = this._context.root;';
const listName = generator.current.listNames.get( name ); const listName = block.listNames.get( name );
const indexName = generator.current.indexNames.get( name ); const indexName = block.indexNames.get( name );
return `var ${listName} = this._context.${listName}, ${indexName} = this._context.${indexName}, ${name} = ${listName}[${indexName}]`; return `var ${listName} = this._context.${listName}, ${indexName} = this._context.${indexName}, ${name} = ${listName}[${indexName}]`;
}); });
const handlerBody = ( declarations.length ? declarations.join( '\n' ) + '\n\n' : '' ) + `[✂${attribute.expression.start}-${attribute.expression.end}✂];`; const handlerBody = ( declarations.length ? declarations.join( '\n' ) + '\n\n' : '' ) + `[✂${attribute.expression.start}-${attribute.expression.end}✂];`;
local.init.addBlock( deindent` local.create.addBlock( deindent`
${local.name}.on( '${attribute.name}', function ( event ) { ${local.name}.on( '${attribute.name}', function ( event ) {
${handlerBody} ${handlerBody}
}); });
@ -110,18 +110,18 @@ export default function addComponentAttributes ( generator, node, local ) {
} }
else if ( attribute.type === 'Binding' ) { else if ( attribute.type === 'Binding' ) {
addComponentBinding( generator, node, attribute, generator.current, local ); addComponentBinding( generator, node, attribute, block, local );
} }
else if ( attribute.type === 'Ref' ) { else if ( attribute.type === 'Ref' ) {
generator.usesRefs = true; generator.usesRefs = true;
local.init.addLine( local.create.addLine(
`${generator.current.component}.refs.${attribute.name} = ${local.name};` `${block.component}.refs.${attribute.name} = ${local.name};`
); );
generator.current.builders.teardown.addLine( deindent` block.builders.destroy.addLine( deindent`
if ( ${generator.current.component}.refs.${attribute.name} === ${local.name} ) ${generator.current.component}.refs.${attribute.name} = null; if ( ${block.component}.refs.${attribute.name} === ${local.name} ) ${block.component}.refs.${attribute.name} = null;
` ); ` );
} }

@ -2,9 +2,9 @@ import deindent from '../../../../utils/deindent.js';
import flattenReference from '../../../../utils/flattenReference.js'; import flattenReference from '../../../../utils/flattenReference.js';
import getSetter from './binding/getSetter.js'; import getSetter from './binding/getSetter.js';
export default function createBinding ( generator, node, attribute, current, local ) { export default function addComponentBinding ( generator, node, attribute, block, local ) {
const { name, keypath } = flattenReference( attribute.value ); const { name, keypath } = flattenReference( attribute.value );
const { snippet, contexts, dependencies } = generator.contextualise( attribute.value ); const { snippet, contexts, dependencies } = generator.contextualise( block, attribute.value );
if ( dependencies.length > 1 ) throw new Error( 'An unexpected situation arose. Please raise an issue at https://github.com/sveltejs/svelte/issues — thanks!' ); if ( dependencies.length > 1 ) throw new Error( 'An unexpected situation arose. Please raise an issue at https://github.com/sveltejs/svelte/issues — thanks!' );
@ -12,14 +12,14 @@ export default function createBinding ( generator, node, attribute, current, loc
if ( !~local.allUsedContexts.indexOf( context ) ) local.allUsedContexts.push( context ); if ( !~local.allUsedContexts.indexOf( context ) ) local.allUsedContexts.push( context );
}); });
const contextual = current.contexts.has( name ); const contextual = block.contexts.has( name );
let obj; let obj;
let prop; let prop;
if ( contextual ) { if ( contextual ) {
obj = current.listNames.get( name ); obj = block.listNames.get( name );
prop = current.indexNames.get( name ); prop = block.indexNames.get( name );
} else if ( attribute.value.type === 'MemberExpression' ) { } else if ( attribute.value.type === 'MemberExpression' ) {
prop = `'[✂${attribute.value.property.start}-${attribute.value.property.end}✂]'`; prop = `'[✂${attribute.value.property.start}-${attribute.value.property.end}✂]'`;
obj = `[✂${attribute.value.object.start}-${attribute.value.object.end}✂]`; obj = `[✂${attribute.value.object.start}-${attribute.value.object.end}✂]`;
@ -35,16 +35,16 @@ export default function createBinding ( generator, node, attribute, current, loc
prop prop
}); });
const setter = getSetter({ current, name, keypath, context: '_context', attribute, dependencies, value: 'value' }); const setter = getSetter({ block, name, keypath, context: '_context', attribute, dependencies, value: 'value' });
generator.hasComplexBindings = true; generator.hasComplexBindings = true;
const updating = generator.current.getUniqueName( `${local.name}_updating` ); const updating = block.getUniqueName( `${local.name}_updating` );
local.init.addBlock( deindent` local.create.addBlock( deindent`
var ${updating} = false; var ${updating} = false;
${generator.current.component}._bindings.push( function () { ${block.component}._bindings.push( function () {
if ( ${local.name}._torndown ) return; if ( ${local.name}._torndown ) return;
${local.name}.observe( '${attribute.name}', function ( value ) { ${local.name}.observe( '${attribute.name}', function ( value ) {
if ( ${updating} ) return; if ( ${updating} ) return;

@ -3,9 +3,8 @@ import addElementBinding from './addElementBinding';
import deindent from '../../../../utils/deindent.js'; import deindent from '../../../../utils/deindent.js';
import flattenReference from '../../../../utils/flattenReference.js'; import flattenReference from '../../../../utils/flattenReference.js';
import getStaticAttributeValue from './binding/getStaticAttributeValue.js'; import getStaticAttributeValue from './binding/getStaticAttributeValue.js';
import findBlock from '../../utils/findBlock.js';
export default function addElementAttributes ( generator, node, local ) { export default function addElementAttributes ( generator, block, node, local ) {
node.attributes.forEach( attribute => { node.attributes.forEach( attribute => {
const name = attribute.name; const name = attribute.name;
@ -32,28 +31,28 @@ export default function addElementAttributes ( generator, node, local ) {
if ( attribute.value === true ) { if ( attribute.value === true ) {
// attributes without values, e.g. <textarea readonly> // attributes without values, e.g. <textarea readonly>
if ( propertyName ) { if ( propertyName ) {
local.init.addLine( local.create.addLine(
`${local.name}.${propertyName} = true;` `${local.name}.${propertyName} = true;`
); );
} else { } else {
local.init.addLine( local.create.addLine(
`${generator.helper( method )}( ${local.name}, '${name}', true );` `${generator.helper( method )}( ${local.name}, '${name}', true );`
); );
} }
// special case autofocus. has to be handled in a bit of a weird way // special case autofocus. has to be handled in a bit of a weird way
if ( name === 'autofocus' ) { if ( name === 'autofocus' ) {
generator.current.autofocus = local.name; block.autofocus = local.name;
} }
} }
else if ( attribute.value.length === 0 ) { else if ( attribute.value.length === 0 ) {
if ( propertyName ) { if ( propertyName ) {
local.init.addLine( local.create.addLine(
`${local.name}.${propertyName} = '';` `${local.name}.${propertyName} = '';`
); );
} else { } else {
local.init.addLine( local.create.addLine(
`${generator.helper( method )}( ${local.name}, '${name}', '' );` `${generator.helper( method )}( ${local.name}, '${name}', '' );`
); );
} }
@ -75,7 +74,7 @@ export default function addElementAttributes ( generator, node, local ) {
local.namespace = value.data; local.namespace = value.data;
addAttribute = true; addAttribute = true;
} else if ( propertyName ) { } else if ( propertyName ) {
local.init.addLine( local.create.addLine(
`${local.name}.${propertyName} = ${result};` `${local.name}.${propertyName} = ${result};`
); );
} else { } else {
@ -83,7 +82,7 @@ export default function addElementAttributes ( generator, node, local ) {
} }
if ( addAttribute ) { if ( addAttribute ) {
local.init.addLine( local.create.addLine(
`${generator.helper( method )}( ${local.name}, '${name}', ${result} );` `${generator.helper( method )}( ${local.name}, '${name}', ${result} );`
); );
} }
@ -93,10 +92,10 @@ export default function addElementAttributes ( generator, node, local ) {
dynamic = true; dynamic = true;
// dynamic but potentially non-string attributes // dynamic but potentially non-string attributes
const { snippet } = generator.contextualise( value.expression ); const { snippet } = generator.contextualise( block, value.expression );
const last = `last_${local.name}_${name.replace( /-/g, '_')}`; const last = `last_${local.name}_${name.replace( /-/g, '_')}`;
local.init.addLine( `var ${last} = ${snippet};` ); local.create.addLine( `var ${last} = ${snippet};` );
let updater; let updater;
if ( propertyName ) { if ( propertyName ) {
@ -105,13 +104,11 @@ export default function addElementAttributes ( generator, node, local ) {
updater = `${generator.helper( method )}( ${local.name}, '${name}', ${last} );`; updater = `${generator.helper( method )}( ${local.name}, '${name}', ${last} );`;
} }
local.init.addLine( updater ); local.create.addLine( updater );
const fragment = findBlock( generator.current );
if ( !fragment.tmp ) fragment.tmp = fragment.getUniqueName( 'tmp' );
local.update.addBlock( deindent` local.update.addBlock( deindent`
if ( ( ${fragment.tmp} = ${snippet} ) !== ${last} ) { if ( ( ${block.tmp()} = ${snippet} ) !== ${last} ) {
${last} = ${fragment.tmp}; ${last} = ${block.tmp()};
${updater} ${updater}
} }
` ); ` );
@ -126,7 +123,7 @@ export default function addElementAttributes ( generator, node, local ) {
if ( chunk.type === 'Text' ) { if ( chunk.type === 'Text' ) {
return JSON.stringify( chunk.data ); return JSON.stringify( chunk.data );
} else { } else {
const { snippet } = generator.contextualise( chunk.expression ); const { snippet } = generator.contextualise( block, chunk.expression );
return `( ${snippet} )`; return `( ${snippet} )`;
} }
}).join( ' + ' ) }).join( ' + ' )
@ -139,14 +136,14 @@ export default function addElementAttributes ( generator, node, local ) {
updater = `${generator.helper( method )}( ${local.name}, '${name}', ${value} );`; updater = `${generator.helper( method )}( ${local.name}, '${name}', ${value} );`;
} }
local.init.addLine( updater ); local.create.addLine( updater );
local.update.addLine( updater ); local.update.addLine( updater );
} }
if ( isIndirectlyBoundValue ) { if ( isIndirectlyBoundValue ) {
const updateValue = `${local.name}.value = ${local.name}.__value;`; const updateValue = `${local.name}.value = ${local.name}.__value;`;
local.init.addLine( updateValue ); local.create.addLine( updateValue );
if ( dynamic ) local.update.addLine( updateValue ); if ( dynamic ) local.update.addLine( updateValue );
} }
} }
@ -158,12 +155,12 @@ export default function addElementAttributes ( generator, node, local ) {
const flattened = flattenReference( attribute.expression.callee ); const flattened = flattenReference( attribute.expression.callee );
if ( flattened.name !== 'event' && flattened.name !== 'this' ) { if ( flattened.name !== 'event' && flattened.name !== 'this' ) {
// allow event.stopPropagation(), this.select() etc // allow event.stopPropagation(), this.select() etc
generator.code.prependRight( attribute.expression.start, `${generator.current.component}.` ); generator.code.prependRight( attribute.expression.start, `${block.component}.` );
} }
const usedContexts = []; const usedContexts = [];
attribute.expression.arguments.forEach( arg => { attribute.expression.arguments.forEach( arg => {
const { contexts } = generator.contextualise( arg, true ); const { contexts } = generator.contextualise( block, arg, true );
contexts.forEach( context => { contexts.forEach( context => {
if ( !~usedContexts.indexOf( context ) ) usedContexts.push( context ); if ( !~usedContexts.indexOf( context ) ) usedContexts.push( context );
@ -175,27 +172,27 @@ export default function addElementAttributes ( generator, node, local ) {
const declarations = usedContexts.map( name => { const declarations = usedContexts.map( name => {
if ( name === 'root' ) return 'var root = this.__svelte.root;'; if ( name === 'root' ) return 'var root = this.__svelte.root;';
const listName = generator.current.listNames.get( name ); const listName = block.listNames.get( name );
const indexName = generator.current.indexNames.get( name ); const indexName = block.indexNames.get( name );
return `var ${listName} = this.__svelte.${listName}, ${indexName} = this.__svelte.${indexName}, ${name} = ${listName}[${indexName}]`; return `var ${listName} = this.__svelte.${listName}, ${indexName} = this.__svelte.${indexName}, ${name} = ${listName}[${indexName}]`;
}); });
const handlerName = generator.current.getUniqueName( `${name}_handler` ); const handlerName = block.getUniqueName( `${name}_handler` );
const handlerBody = ( declarations.length ? declarations.join( '\n' ) + '\n\n' : '' ) + `[✂${attribute.expression.start}-${attribute.expression.end}✂];`; const handlerBody = ( declarations.length ? declarations.join( '\n' ) + '\n\n' : '' ) + `[✂${attribute.expression.start}-${attribute.expression.end}✂];`;
if ( generator.events.has( name ) ) { if ( generator.events.has( name ) ) {
local.init.addBlock( deindent` local.create.addBlock( deindent`
var ${handlerName} = ${generator.alias( 'template' )}.events.${name}.call( ${generator.current.component}, ${local.name}, function ( event ) { var ${handlerName} = ${generator.alias( 'template' )}.events.${name}.call( ${block.component}, ${local.name}, function ( event ) {
${handlerBody} ${handlerBody}
}.bind( ${local.name} ) ); }.bind( ${local.name} ) );
` ); ` );
generator.current.builders.teardown.addLine( deindent` block.builders.destroy.addLine( deindent`
${handlerName}.teardown(); ${handlerName}.teardown();
` ); ` );
} else { } else {
local.init.addBlock( deindent` local.create.addBlock( deindent`
function ${handlerName} ( event ) { function ${handlerName} ( event ) {
${handlerBody} ${handlerBody}
} }
@ -203,25 +200,25 @@ export default function addElementAttributes ( generator, node, local ) {
${generator.helper( 'addEventListener' )}( ${local.name}, '${name}', ${handlerName} ); ${generator.helper( 'addEventListener' )}( ${local.name}, '${name}', ${handlerName} );
` ); ` );
generator.current.builders.teardown.addLine( deindent` block.builders.destroy.addLine( deindent`
${generator.helper( 'removeEventListener' )}( ${local.name}, '${name}', ${handlerName} ); ${generator.helper( 'removeEventListener' )}( ${local.name}, '${name}', ${handlerName} );
` ); ` );
} }
} }
else if ( attribute.type === 'Binding' ) { else if ( attribute.type === 'Binding' ) {
addElementBinding( generator, node, attribute, generator.current, local ); addElementBinding( generator, node, attribute, block, local );
} }
else if ( attribute.type === 'Ref' ) { else if ( attribute.type === 'Ref' ) {
generator.usesRefs = true; generator.usesRefs = true;
local.init.addLine( local.create.addLine(
`${generator.current.component}.refs.${name} = ${local.name};` `${block.component}.refs.${name} = ${local.name};`
); );
generator.current.builders.teardown.addLine( deindent` block.builders.destroy.addLine( deindent`
if ( ${generator.current.component}.refs.${name} === ${local.name} ) ${generator.current.component}.refs.${name} = null; if ( ${block.component}.refs.${name} === ${local.name} ) ${block.component}.refs.${name} = null;
` ); ` );
} }

@ -3,9 +3,9 @@ import flattenReference from '../../../../utils/flattenReference.js';
import getSetter from './binding/getSetter.js'; import getSetter from './binding/getSetter.js';
import getStaticAttributeValue from './binding/getStaticAttributeValue.js'; import getStaticAttributeValue from './binding/getStaticAttributeValue.js';
export default function createBinding ( generator, node, attribute, current, local ) { export default function addElementBinding ( generator, node, attribute, block, local ) {
const { name, keypath } = flattenReference( attribute.value ); const { name, keypath } = flattenReference( attribute.value );
const { snippet, contexts, dependencies } = generator.contextualise( attribute.value ); const { snippet, contexts, dependencies } = generator.contextualise( block, attribute.value );
if ( dependencies.length > 1 ) throw new Error( 'An unexpected situation arose. Please raise an issue at https://github.com/sveltejs/svelte/issues — thanks!' ); if ( dependencies.length > 1 ) throw new Error( 'An unexpected situation arose. Please raise an issue at https://github.com/sveltejs/svelte/issues — thanks!' );
@ -13,15 +13,15 @@ export default function createBinding ( generator, node, attribute, current, loc
if ( !~local.allUsedContexts.indexOf( context ) ) local.allUsedContexts.push( context ); if ( !~local.allUsedContexts.indexOf( context ) ) local.allUsedContexts.push( context );
}); });
const handler = current.getUniqueName( `${local.name}_change_handler` ); const handler = block.getUniqueName( `${local.name}_change_handler` );
const isMultipleSelect = node.name === 'select' && node.attributes.find( attr => attr.name.toLowerCase() === 'multiple' ); // TODO use getStaticAttributeValue const isMultipleSelect = node.name === 'select' && node.attributes.find( attr => attr.name.toLowerCase() === 'multiple' ); // TODO use getStaticAttributeValue
const type = getStaticAttributeValue( node, 'type' ); const type = getStaticAttributeValue( node, 'type' );
const bindingGroup = attribute.name === 'group' ? getBindingGroup( generator, current, attribute, keypath ) : null; const bindingGroup = attribute.name === 'group' ? getBindingGroup( generator, keypath ) : null;
const value = getBindingValue( generator, local, node, attribute, isMultipleSelect, bindingGroup, type ); const value = getBindingValue( generator, block, local, node, attribute, isMultipleSelect, bindingGroup, type );
const eventName = getBindingEventName( node ); const eventName = getBindingEventName( node );
let setter = getSetter({ current, name, keypath, context: '__svelte', attribute, dependencies, value }); let setter = getSetter({ block, name, keypath, context: '__svelte', attribute, dependencies, value });
let updateElement; let updateElement;
// <select> special case // <select> special case
@ -30,9 +30,9 @@ export default function createBinding ( generator, node, attribute, current, loc
setter = `var selectedOption = ${local.name}.selectedOptions[0] || ${local.name}.options[0];\n${setter}`; setter = `var selectedOption = ${local.name}.selectedOptions[0] || ${local.name}.options[0];\n${setter}`;
} }
const value = current.getUniqueName( 'value' ); const value = block.getUniqueName( 'value' );
const i = current.getUniqueName( 'i' ); const i = block.getUniqueName( 'i' );
const option = current.getUniqueName( 'option' ); const option = block.getUniqueName( 'option' );
const ifStatement = isMultipleSelect ? const ifStatement = isMultipleSelect ?
deindent` deindent`
@ -66,12 +66,12 @@ export default function createBinding ( generator, node, attribute, current, loc
`~${snippet}.indexOf( ${local.name}.__value )` : `~${snippet}.indexOf( ${local.name}.__value )` :
`${local.name}.__value === ${snippet}`; `${local.name}.__value === ${snippet}`;
local.init.addLine( local.create.addLine(
`${current.component}._bindingGroups[${bindingGroup}].push( ${local.name} );` `${block.component}._bindingGroups[${bindingGroup}].push( ${local.name} );`
); );
local.teardown.addBlock( local.destroy.addBlock(
`${current.component}._bindingGroups[${bindingGroup}].splice( ${current.component}._bindingGroups[${bindingGroup}].indexOf( ${local.name} ), 1 );` `${block.component}._bindingGroups[${bindingGroup}].splice( ${block.component}._bindingGroups[${bindingGroup}].indexOf( ${local.name} ), 1 );`
); );
updateElement = `${local.name}.checked = ${condition};`; updateElement = `${local.name}.checked = ${condition};`;
@ -82,9 +82,9 @@ export default function createBinding ( generator, node, attribute, current, loc
updateElement = `${local.name}.${attribute.name} = ${snippet};`; updateElement = `${local.name}.${attribute.name} = ${snippet};`;
} }
const updating = generator.current.getUniqueName( `${local.name}_updating` ); const updating = block.getUniqueName( `${local.name}_updating` );
local.init.addBlock( deindent` local.create.addBlock( deindent`
var ${updating} = false; var ${updating} = false;
function ${handler} () { function ${handler} () {
@ -104,7 +104,7 @@ export default function createBinding ( generator, node, attribute, current, loc
} }
` ); ` );
current.builders.teardown.addLine( deindent` block.builders.destroy.addLine( deindent`
${generator.helper( 'removeEventListener' )}( ${local.name}, '${eventName}', ${handler} ); ${generator.helper( 'removeEventListener' )}( ${local.name}, '${eventName}', ${handler} );
` ); ` );
} }
@ -124,7 +124,7 @@ function getBindingEventName ( node ) {
return 'change'; return 'change';
} }
function getBindingValue ( generator, local, node, attribute, isMultipleSelect, bindingGroup, type ) { function getBindingValue ( generator, block, local, node, attribute, isMultipleSelect, bindingGroup, type ) {
// <select multiple bind:value='selected> // <select multiple bind:value='selected>
if ( isMultipleSelect ) { if ( isMultipleSelect ) {
return `[].map.call( ${local.name}.selectedOptions, function ( option ) { return option.__value; })`; return `[].map.call( ${local.name}.selectedOptions, function ( option ) { return option.__value; })`;
@ -138,7 +138,7 @@ function getBindingValue ( generator, local, node, attribute, isMultipleSelect,
// <input type='checkbox' bind:group='foo'> // <input type='checkbox' bind:group='foo'>
if ( attribute.name === 'group' ) { if ( attribute.name === 'group' ) {
if ( type === 'checkbox' ) { if ( type === 'checkbox' ) {
return `${generator.helper( 'getBindingGroupValue' )}( ${generator.current.component}._bindingGroups[${bindingGroup}] )`; return `${generator.helper( 'getBindingGroupValue' )}( ${block.component}._bindingGroups[${bindingGroup}] )`;
} }
return `${local.name}.__value`; return `${local.name}.__value`;
@ -153,7 +153,7 @@ function getBindingValue ( generator, local, node, attribute, isMultipleSelect,
return `${local.name}.${attribute.name}`; return `${local.name}.${attribute.name}`;
} }
function getBindingGroup ( generator, current, attribute, keypath ) { function getBindingGroup ( generator, keypath ) {
// TODO handle contextual bindings — `keypath` should include unique ID of // TODO handle contextual bindings — `keypath` should include unique ID of
// each block that provides context // each block that provides context
let index = generator.bindingGroups.indexOf( keypath ); let index = generator.bindingGroups.indexOf( keypath );

@ -1,28 +1,28 @@
import deindent from '../../../../../utils/deindent.js'; import deindent from '../../../../../utils/deindent.js';
export default function getSetter ({ current, name, keypath, context, attribute, dependencies, value }) { export default function getSetter ({ block, name, keypath, context, attribute, dependencies, value }) {
if ( current.contexts.has( name ) ) { if ( block.contexts.has( name ) ) {
const prop = dependencies[0]; const prop = dependencies[0];
const tail = attribute.value.type === 'MemberExpression' ? getTailSnippet( attribute.value ) : ''; const tail = attribute.value.type === 'MemberExpression' ? getTailSnippet( attribute.value ) : '';
return deindent` return deindent`
var list = this.${context}.${current.listNames.get( name )}; var list = this.${context}.${block.listNames.get( name )};
var index = this.${context}.${current.indexNames.get( name )}; var index = this.${context}.${block.indexNames.get( name )};
list[index]${tail} = ${value}; list[index]${tail} = ${value};
${current.component}._set({ ${prop}: ${current.component}.get( '${prop}' ) }); ${block.component}._set({ ${prop}: ${block.component}.get( '${prop}' ) });
`; `;
} }
if ( attribute.value.type === 'MemberExpression' ) { if ( attribute.value.type === 'MemberExpression' ) {
return deindent` return deindent`
var ${name} = ${current.component}.get( '${name}' ); var ${name} = ${block.component}.get( '${name}' );
${keypath} = ${value}; ${keypath} = ${value};
${current.component}._set({ ${name}: ${name} }); ${block.component}._set({ ${name}: ${name} });
`; `;
} }
return `${current.component}._set({ ${name}: ${value} });`; return `${block.component}._set({ ${name}: ${value} });`;
} }
function getTailSnippet ( node ) { function getTailSnippet ( node ) {

@ -11,74 +11,72 @@ const associatedEvents = {
scrollY: 'scroll' scrollY: 'scroll'
}; };
export default { export default function visitWindow ( generator, block, node ) {
enter ( generator, node ) { const events = {};
const events = {};
node.attributes.forEach( attribute => {
node.attributes.forEach( attribute => { if ( attribute.type === 'EventHandler' ) {
if ( attribute.type === 'EventHandler' ) { // TODO verify that it's a valid callee (i.e. built-in or declared method)
// TODO verify that it's a valid callee (i.e. built-in or declared method) generator.addSourcemapLocations( attribute.expression );
generator.addSourcemapLocations( attribute.expression );
const flattened = flattenReference( attribute.expression.callee );
const flattened = flattenReference( attribute.expression.callee ); if ( flattened.name !== 'event' && flattened.name !== 'this' ) {
if ( flattened.name !== 'event' && flattened.name !== 'this' ) { // allow event.stopPropagation(), this.select() etc
// allow event.stopPropagation(), this.select() etc generator.code.prependRight( attribute.expression.start, 'component.' );
generator.code.prependRight( attribute.expression.start, 'component.' );
}
const handlerName = generator.current.getUniqueName( `onwindow${attribute.name}` );
generator.current.builders.init.addBlock( deindent`
var ${handlerName} = function ( event ) {
[${attribute.expression.start}-${attribute.expression.end}];
};
window.addEventListener( '${attribute.name}', ${handlerName} );
` );
generator.current.builders.teardown.addBlock( deindent`
window.removeEventListener( '${attribute.name}', ${handlerName} );
` );
} }
if ( attribute.type === 'Binding' ) { const handlerName = block.getUniqueName( `onwindow${attribute.name}` );
const associatedEvent = associatedEvents[ attribute.name ];
if ( !associatedEvent ) { block.builders.create.addBlock( deindent`
throw new Error( `Cannot bind to ${attribute.name} on <:Window>` ); var ${handlerName} = function ( event ) {
} [${attribute.expression.start}-${attribute.expression.end}];
};
window.addEventListener( '${attribute.name}', ${handlerName} );
` );
if ( attribute.value.type !== 'Identifier' ) { block.builders.destroy.addBlock( deindent`
const { parts, keypath } = flattenReference( attribute.value ); window.removeEventListener( '${attribute.name}', ${handlerName} );
throw new Error( `Bindings on <:Window/> must be to top-level properties, e.g. '${parts.pop()}' rather than '${keypath}'` ); ` );
} }
if ( !events[ associatedEvent ] ) events[ associatedEvent ] = []; if ( attribute.type === 'Binding' ) {
events[ associatedEvent ].push( `${attribute.value.name}: this.${attribute.name}` ); const associatedEvent = associatedEvents[ attribute.name ];
// add initial value if ( !associatedEvent ) {
generator.builders.metaBindings.addLine( throw new Error( `Cannot bind to ${attribute.name} on <:Window>` );
`this._state.${attribute.value.name} = window.${attribute.name};`
);
} }
});
Object.keys( events ).forEach( event => {
const handlerName = generator.current.getUniqueName( `onwindow${event}` );
const props = events[ event ].join( ',\n' );
generator.current.builders.init.addBlock( deindent` if ( attribute.value.type !== 'Identifier' ) {
var ${handlerName} = function ( event ) { const { parts, keypath } = flattenReference( attribute.value );
component.set({ throw new Error( `Bindings on <:Window/> must be to top-level properties, e.g. '${parts.pop()}' rather than '${keypath}'` );
${props} }
});
};
window.addEventListener( '${event}', ${handlerName} );
` );
generator.current.builders.teardown.addBlock( deindent` if ( !events[ associatedEvent ] ) events[ associatedEvent ] = [];
window.removeEventListener( '${event}', ${handlerName} ); events[ associatedEvent ].push( `${attribute.value.name}: this.${attribute.name}` );
` );
}); // add initial value
} generator.builders.metaBindings.addLine(
}; `this._state.${attribute.value.name} = window.${attribute.name};`
);
}
});
Object.keys( events ).forEach( event => {
const handlerName = block.getUniqueName( `onwindow${event}` );
const props = events[ event ].join( ',\n' );
block.builders.create.addBlock( deindent`
var ${handlerName} = function ( event ) {
component.set({
${props}
});
};
window.addEventListener( '${event}', ${handlerName} );
` );
block.builders.destroy.addBlock( deindent`
window.removeEventListener( '${event}', ${handlerName} );
` );
});
}

@ -0,0 +1,30 @@
import deindent from '../../utils/deindent.js';
import flattenReference from '../../utils/flattenReference.js';
export default class Block {
constructor ( options ) {
Object.assign( this, options );
}
addBinding ( binding, name ) {
const conditions = [ `!( '${binding.name}' in root )`].concat( // TODO handle contextual bindings...
this.conditions.map( c => `(${c})` )
);
const { keypath } = flattenReference( binding.value );
this.generator.bindings.push( deindent`
if ( ${conditions.join( '&&' )} ) {
tmp = ${name}.data();
if ( '${keypath}' in tmp ) {
root.${binding.name} = tmp.${keypath};
settled = false;
}
}
` );
}
child ( options ) {
return new Block( Object.assign( {}, this, options, { parent: this } ) );
}
}

@ -1,34 +1,16 @@
import deindent from '../../utils/deindent.js'; import deindent from '../../utils/deindent.js';
import CodeBuilder from '../../utils/CodeBuilder.js'; import CodeBuilder from '../../utils/CodeBuilder.js';
import flattenReference from '../../utils/flattenReference.js';
import visitors from './visitors/index.js';
import Generator from '../Generator.js'; import Generator from '../Generator.js';
import Block from './Block.js';
import visit from './visit.js';
class SsrGenerator extends Generator { class SsrGenerator extends Generator {
constructor ( parsed, source, name, visitors, options ) { constructor ( parsed, source, name, options ) {
super( parsed, source, name, visitors, options ); super( parsed, source, name, options );
this.bindings = []; this.bindings = [];
this.renderCode = ''; this.renderCode = '';
} }
addBinding ( binding, name ) {
const conditions = [ `!( '${binding.name}' in root )`].concat( // TODO handle contextual bindings...
this.current.conditions.map( c => `(${c})` )
);
const { keypath } = flattenReference( binding.value );
this.bindings.push( deindent`
if ( ${conditions.join( '&&' )} ) {
tmp = ${name}.data();
if ( '${keypath}' in tmp ) {
root.${binding.name} = tmp.${keypath};
settled = false;
}
}
` );
}
append ( code ) { append ( code ) {
this.renderCode += code; this.renderCode += code;
} }
@ -38,7 +20,7 @@ export default function ssr ( parsed, source, options ) {
const format = options.format || 'cjs'; const format = options.format || 'cjs';
const name = options.name || 'SvelteComponent'; const name = options.name || 'SvelteComponent';
const generator = new SsrGenerator( parsed, source, name, visitors, options ); const generator = new SsrGenerator( parsed, source, name, options );
const { computations, hasJs, templateProperties } = generator.parseJs( true ); const { computations, hasJs, templateProperties } = generator.parseJs( true );
@ -50,13 +32,16 @@ export default function ssr ( parsed, source, options ) {
}; };
// create main render() function // create main render() function
generator.push({ const mainBlock = new Block({
generator,
contexts: new Map(), contexts: new Map(),
indexes: new Map(), indexes: new Map(),
conditions: [] conditions: []
}); });
parsed.html.children.forEach( node => generator.visit( node ) ); parsed.html.children.forEach( node => {
visit( generator, mainBlock, node );
});
builders.render.addLine( builders.render.addLine(
templateProperties.data ? `root = Object.assign( ${generator.alias( 'template' )}.data(), root || {} );` : `root = root || {};` templateProperties.data ? `root = Object.assign( ${generator.alias( 'template' )}.data(), root || {} );` : `root = root || {};`

@ -0,0 +1,6 @@
import visitors from './visitors/index.js';
export default function visit ( generator, fragment, node ) {
const visitor = visitors[ node.type ];
visitor( generator, fragment, node );
}

@ -1,3 +1,3 @@
export default { export default function visitComment () {
// do nothing // do nothing
}; }

@ -1,72 +1,77 @@
import flattenReference from '../../../utils/flattenReference.js'; import flattenReference from '../../../utils/flattenReference.js';
import visit from '../visit.js';
export default { export default function visitComponent ( generator, block, node ) {
enter ( generator, node ) { function stringify ( chunk ) {
function stringify ( chunk ) { if ( chunk.type === 'Text' ) return chunk.data;
if ( chunk.type === 'Text' ) return chunk.data; if ( chunk.type === 'MustacheTag' ) {
if ( chunk.type === 'MustacheTag' ) { const { snippet } = generator.contextualise( block, chunk.expression );
const { snippet } = generator.contextualise( chunk.expression ); return '${__escape( ' + snippet + ')}';
return '${__escape( ' + snippet + ')}';
}
} }
}
const attributes = []; const attributes = [];
const bindings = []; const bindings = [];
node.attributes.forEach( attribute => { node.attributes.forEach( attribute => {
if ( attribute.type === 'Attribute' ) { if ( attribute.type === 'Attribute' ) {
attributes.push( attribute ); attributes.push( attribute );
} else if ( attribute.type === 'Binding' ) { } else if ( attribute.type === 'Binding' ) {
bindings.push( attribute ); bindings.push( attribute );
} }
}); });
const props = attributes const props = attributes
.map( attribute => { .map( attribute => {
let value; let value;
if ( attribute.value === true ) { if ( attribute.value === true ) {
value = `true`; value = `true`;
} else if ( attribute.value.length === 0 ) { } else if ( attribute.value.length === 0 ) {
value = `''`; value = `''`;
} else if ( attribute.value.length === 1 ) { } else if ( attribute.value.length === 1 ) {
const chunk = attribute.value[0]; const chunk = attribute.value[0];
if ( chunk.type === 'Text' ) { if ( chunk.type === 'Text' ) {
value = isNaN( chunk.data ) ? JSON.stringify( chunk.data ) : chunk.data; value = isNaN( chunk.data ) ? JSON.stringify( chunk.data ) : chunk.data;
} else {
const { snippet } = generator.contextualise( chunk.expression );
value = snippet;
}
} else { } else {
value = '`' + attribute.value.map( stringify ).join( '' ) + '`'; const { snippet } = generator.contextualise( block, chunk.expression );
value = snippet;
} }
} else {
value = '`' + attribute.value.map( stringify ).join( '' ) + '`';
}
return `${attribute.name}: ${value}`; return `${attribute.name}: ${value}`;
}) })
.concat( bindings.map( binding => { .concat( bindings.map( binding => {
const { name, keypath } = flattenReference( binding.value ); const { name, keypath } = flattenReference( binding.value );
const value = generator.current.contexts.has( name ) ? keypath : `root.${keypath}`; const value = block.contexts.has( name ) ? keypath : `root.${keypath}`;
return `${binding.name}: ${value}`; return `${binding.name}: ${value}`;
})) }))
.join( ', ' ); .join( ', ' );
const expression = node.name === ':Self' ? generator.name : generator.importedComponents.get( node.name ) || `${generator.alias( 'template' )}.components.${node.name}`; const expression = node.name === ':Self' ? generator.name : generator.importedComponents.get( node.name ) || `${generator.alias( 'template' )}.components.${node.name}`;
bindings.forEach( binding => { bindings.forEach( binding => {
generator.addBinding( binding, expression ); block.addBinding( binding, expression );
}); });
let open = `\${${expression}.render({${props}}`; let open = `\${${expression}.render({${props}}`;
if ( node.children.length ) { if ( node.children.length ) {
open += `, { yield: () => \``; open += `, { yield: () => \``;
} }
generator.append( open ); generator.append( open );
},
leave ( generator, node ) { generator.elementDepth += 1;
const close = node.children.length ? `\` })}` : ')}';
generator.append( close ); node.children.forEach( child => {
} visit( generator, block, child );
}; });
generator.elementDepth -= 1;
const close = node.children.length ? `\` })}` : ')}';
generator.append( close );
}

@ -1,32 +1,32 @@
export default { import visit from '../visit.js';
enter ( generator, node ) {
const { dependencies, snippet } = generator.contextualise( node.expression ); export default function visitEachBlock ( generator, block, node ) {
const { dependencies, snippet } = generator.contextualise( block, node.expression );
const open = `\${ ${snippet}.map( ${ node.index ? `( ${node.context}, ${node.index} )` : node.context} => \``;
generator.append( open ); const open = `\${ ${snippet}.map( ${ node.index ? `( ${node.context}, ${node.index} )` : node.context} => \``;
generator.append( open );
// TODO should this be the generator's job? It's duplicated between
// here and the equivalent DOM compiler visitor // TODO should this be the generator's job? It's duplicated between
const contexts = new Map( generator.current.contexts ); // here and the equivalent DOM compiler visitor
contexts.set( node.context, node.context ); const contexts = new Map( block.contexts );
contexts.set( node.context, node.context );
const indexes = new Map( generator.current.indexes );
if ( node.index ) indexes.set( node.index, node.context ); const indexes = new Map( block.indexes );
if ( node.index ) indexes.set( node.index, node.context );
const contextDependencies = new Map( generator.current.contextDependencies );
contextDependencies.set( node.context, dependencies ); const contextDependencies = new Map( block.contextDependencies );
contextDependencies.set( node.context, dependencies );
generator.push({
contexts, const childBlock = block.child({
indexes, contexts,
contextDependencies indexes,
}); contextDependencies
}, });
leave ( generator ) { node.children.forEach( child => {
const close = `\` ).join( '' )}`; visit( generator, childBlock, child );
generator.append( close ); });
generator.pop(); const close = `\` ).join( '' )}`;
} generator.append( close );
}; }

@ -1,65 +1,60 @@
import Component from './Component.js'; import visitComponent from './Component.js';
import isVoidElementName from '../../../utils/isVoidElementName.js'; import isVoidElementName from '../../../utils/isVoidElementName.js';
import Window from './meta/Window.js'; import visit from '../visit.js';
import visitWindow from './meta/Window.js';
const meta = { const meta = {
':Window': Window ':Window': visitWindow
}; };
export default { export default function visitElement ( generator, block, node ) {
enter ( generator, node ) { if ( node.name in meta ) {
if ( node.name in meta ) { return meta[ node.name ]( generator, block, node );
return meta[ node.name ].enter( generator, node ); }
}
if ( generator.components.has( node.name ) || node.name === ':Self' ) { if ( generator.components.has( node.name ) || node.name === ':Self' ) {
Component.enter( generator, node ); visitComponent( generator, block, node );
return; return;
} }
let openingTag = `<${node.name}`; let openingTag = `<${node.name}`;
node.attributes.forEach( attribute => { node.attributes.forEach( attribute => {
if ( attribute.type !== 'Attribute' ) return; if ( attribute.type !== 'Attribute' ) return;
let str = ` ${attribute.name}`; let str = ` ${attribute.name}`;
if ( attribute.value !== true ) { if ( attribute.value !== true ) {
str += `="` + attribute.value.map( chunk => { str += `="` + attribute.value.map( chunk => {
if ( chunk.type === 'Text' ) { if ( chunk.type === 'Text' ) {
return chunk.data; return chunk.data;
} }
const { snippet } = generator.contextualise( chunk.expression ); const { snippet } = generator.contextualise( block, chunk.expression );
return '${' + snippet + '}'; return '${' + snippet + '}';
}).join( '' ) + `"`; }).join( '' ) + `"`;
} }
openingTag += str; openingTag += str;
}); });
if ( generator.cssId && !generator.elementDepth ) { if ( generator.cssId && !generator.elementDepth ) {
openingTag += ` ${generator.cssId}`; openingTag += ` ${generator.cssId}`;
} }
openingTag += '>'; openingTag += '>';
generator.append( openingTag ); generator.append( openingTag );
},
leave ( generator, node ) { generator.elementDepth += 1;
if ( node.name in meta ) {
if ( meta[ node.name ].leave ) meta[ node.name ].leave( generator, node );
return;
}
if ( generator.components.has( node.name ) || node.name === ':Self' ) { node.children.forEach( child => {
Component.leave( generator, node ); visit( generator, block, child );
return; });
}
if ( !isVoidElementName( node.name ) ) { generator.elementDepth -= 1;
generator.append( `</${node.name}>` );
} if ( !isVoidElementName( node.name ) ) {
generator.append( `</${node.name}>` );
} }
}; }

@ -1,19 +1,25 @@
export default { import visit from '../visit.js';
enter ( generator, node ) {
const { snippet } = generator.contextualise( node.expression );
generator.append( '${ ' + snippet + ' ? `' ); export default function visitIfBlock ( generator, block, node ) {
const { snippet } = generator.contextualise( block, node.expression );
generator.push({ generator.append( '${ ' + snippet + ' ? `' );
conditions: generator.current.conditions.concat( snippet )
}); const childBlock = block.child({
}, conditions: block.conditions.concat( snippet )
});
node.children.forEach( child => {
visit( generator, childBlock, child );
});
leave ( generator, node ) { generator.append( '` : `' );
generator.append( '` : `' );
if ( node.else ) node.else.children.forEach( child => generator.visit( child ) );
generator.append( '` }' );
generator.pop(); if ( node.else ) {
node.else.children.forEach( child => {
visit( generator, childBlock, child );
});
} }
};
generator.append( '` }' );
}

@ -1,6 +1,4 @@
export default { export default function visitMustacheTag ( generator, block, node ) {
enter ( generator, node ) { const { snippet } = generator.contextualise( block, node.expression );
const { snippet } = generator.contextualise( node.expression ); generator.append( '${__escape( ' + snippet + ' )}' );
generator.append( '${__escape( ' + snippet + ' )}' ); }
}
};

@ -1,6 +1,4 @@
export default { export default function visitRawMustacheTag ( generator, block, node ) {
enter ( generator, node ) { const { snippet } = generator.contextualise( block, node.expression );
const { snippet } = generator.contextualise( node.expression ); generator.append( '${' + snippet + '}' );
generator.append( '${' + snippet + '}' ); }
}
};

@ -1,5 +1,3 @@
export default { export default function visitText ( generator, block, node ) {
enter ( generator, node ) { generator.append( node.data.replace( /\${/g, '\\${' ) );
generator.append( node.data.replace( /\${/g, '\\${' ) ); }
}
};

@ -1,5 +1,3 @@
export default { export default function visitYieldTag ( generator ) {
enter ( generator ) { generator.append( `\${options && options.yield ? options.yield() : ''}` );
generator.append( `\${options && options.yield ? options.yield() : ''}` ); }
}
};

@ -1,9 +1,3 @@
export default { export default function visitWindow () {
enter () { // noop
// noop }
},
leave () {
// noop
}
};

@ -16,9 +16,9 @@ export function detachBetween ( before, after ) {
} }
} }
export function teardownEach ( iterations, detach, start ) { export function destroyEach ( iterations, detach, start ) {
for ( var i = ( start || 0 ); i < iterations.length; i += 1 ) { for ( var i = ( start || 0 ); i < iterations.length; i += 1 ) {
iterations[i].teardown( detach ); iterations[i].destroy( detach );
} }
} }

@ -2,7 +2,7 @@ import checkForDupes from '../utils/checkForDupes.js';
import checkForComputedKeys from '../utils/checkForComputedKeys.js'; import checkForComputedKeys from '../utils/checkForComputedKeys.js';
import usesThisOrArguments from '../utils/usesThisOrArguments.js'; import usesThisOrArguments from '../utils/usesThisOrArguments.js';
const builtin = new Set( [ 'set', 'get', 'on', 'fire', 'observe', 'teardown' ] ); const builtin = new Set( [ 'set', 'get', 'on', 'fire', 'observe', 'destroy' ] );
export default function methods ( validator, prop ) { export default function methods ( validator, prop ) {
if ( prop.value.type !== 'ObjectExpression' ) { if ( prop.value.type !== 'ObjectExpression' ) {

@ -1,17 +0,0 @@
export default {
data: {
animals: [ 'alpaca', 'baboon', 'capybara' ],
foo: 'something else'
},
html: 'before\n<p>alpaca</p><p>baboon</p><p>capybara</p>\nafter',
test ( assert, component, target ) {
component.set({ animals: [] });
assert.htmlEqual( target.innerHTML, 'before\n<p>no animals, but rather something else</p>\nafter' );
component.set({ foo: 'something other' });
assert.htmlEqual( target.innerHTML, 'before\n<p>no animals, but rather something other</p>\nafter' );
component.set({ animals: ['wombat'] });
assert.htmlEqual( target.innerHTML, 'before\n<p>wombat</p>\nafter' );
}
};

@ -0,0 +1 @@
Use these tests sparingly, as they will need to be updated frequently as the code generation changes.

@ -0,0 +1,28 @@
import assert from 'assert';
import * as fs from 'fs';
import * as path from 'path';
import { svelte } from '../helpers.js';
describe( 'js', () => {
fs.readdirSync( 'test/js/samples' ).forEach( dir => {
if ( dir[0] === '.' ) return;
// add .solo to a sample directory name to only run that test
const solo = /\.solo/.test( dir );
if ( solo && process.env.CI ) {
throw new Error( 'Forgot to remove `solo: true` from test' );
}
( solo ? it.only : it )( dir, () => {
dir = path.resolve( 'test/js/samples', dir );
const input = fs.readFileSync( `${dir}/input.html`, 'utf-8' ).replace( /\s+$/, '' );
const actual = svelte.compile( input ).code;
fs.writeFileSync( `${dir}/_actual.js`, actual );
const expected = fs.readFileSync( `${dir}/expected.js`, 'utf-8' );
assert.equal( actual.trim(), expected.trim() );
});
});
});

@ -32,13 +32,13 @@ require.extensions[ '.html' ] = function ( module, filename ) {
const Object_assign = Object.assign; const Object_assign = Object.assign;
describe( 'generate', () => { describe( 'runtime', () => {
before( setupHtmlEqual ); before( setupHtmlEqual );
function runTest ( dir, shared ) { function runTest ( dir, shared ) {
if ( dir[0] === '.' ) return; if ( dir[0] === '.' ) return;
const config = loadConfig( `./generator/samples/${dir}/_config.js` ); const config = loadConfig( `./runtime/samples/${dir}/_config.js` );
if ( config.solo && process.env.CI ) { if ( config.solo && process.env.CI ) {
throw new Error( 'Forgot to remove `solo: true` from test' ); throw new Error( 'Forgot to remove `solo: true` from test' );
@ -53,7 +53,7 @@ describe( 'generate', () => {
compileOptions.dev = config.dev; compileOptions.dev = config.dev;
try { try {
const source = fs.readFileSync( `test/generator/samples/${dir}/main.html`, 'utf-8' ); const source = fs.readFileSync( `test/runtime/samples/${dir}/main.html`, 'utf-8' );
compiled = svelte.compile( source, compileOptions ); compiled = svelte.compile( source, compileOptions );
} catch ( err ) { } catch ( err ) {
if ( config.compileError ) { if ( config.compileError ) {
@ -69,7 +69,8 @@ describe( 'generate', () => {
// check that no ES2015+ syntax slipped in // check that no ES2015+ syntax slipped in
if ( !config.allowES2015 ) { if ( !config.allowES2015 ) {
try { try {
const startIndex = code.indexOf( 'function render_main_fragment' ); // may change! const startIndex = code.indexOf( 'function create_main_fragment' ); // may change!
if ( startIndex === -1 ) throw new Error( 'missing create_main_fragment' );
const es5 = spaces( startIndex ) + code.slice( startIndex ).replace( /export default .+/, '' ); const es5 = spaces( startIndex ) + code.slice( startIndex ).replace( /export default .+/, '' );
acorn.parse( es5, { ecmaVersion: 5 }); acorn.parse( es5, { ecmaVersion: 5 });
} catch ( err ) { } catch ( err ) {
@ -117,6 +118,8 @@ describe( 'generate', () => {
data: config.data data: config.data
}); });
Object.assign = Object_assign;
console.warn = warn; console.warn = warn;
if ( config.error ) { if ( config.error ) {
@ -160,13 +163,13 @@ describe( 'generate', () => {
} }
describe( 'inline helpers', () => { describe( 'inline helpers', () => {
fs.readdirSync( 'test/generator/samples' ).forEach( dir => { fs.readdirSync( 'test/runtime/samples' ).forEach( dir => {
runTest( dir, null ); runTest( dir, null );
}); });
}); });
describe( 'shared helpers', () => { describe( 'shared helpers', () => {
fs.readdirSync( 'test/generator/samples' ).forEach( dir => { fs.readdirSync( 'test/runtime/samples' ).forEach( dir => {
runTest( dir, path.resolve( 'shared.js' ) ); runTest( dir, path.resolve( 'shared.js' ) );
}); });
}); });

@ -7,5 +7,9 @@ export default {
] ]
}, },
html: `<div class="foo ">1</div><div class=" bar">2</div><div class="foo bar">3</div><!---->` html: `
<div class="foo ">1</div>
<div class=" bar">2</div>
<div class="foo bar">3</div>
`
}; };

@ -1,5 +1,6 @@
export default { export default {
html: `<div style="color: red;">red</div>`, html: `<div style="color: red;">red</div>`,
test ( assert, component, target ) { test ( assert, component, target ) {
const div = target.querySelector( 'div' ); const div = target.querySelector( 'div' );

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save