preprocess template, so that we can discover dependencies ahead of time

pull/7738/head
Rich-Harris 9 years ago
parent 4ef3e005fb
commit 72f4941417

@ -63,6 +63,8 @@ export default class Generator {
} }
contextualise ( block, expression, context, isEventHandler ) { contextualise ( block, expression, context, isEventHandler ) {
if ( expression._contextualised ) return expression._contextualised;
this.addSourcemapLocations( expression ); this.addSourcemapLocations( expression );
const usedContexts = []; const usedContexts = [];
@ -153,12 +155,13 @@ export default class Generator {
} }
}); });
return { expression._contextualised = {
dependencies, dependencies,
contexts: usedContexts, contexts: usedContexts,
snippet: `[✂${expression.start}-${expression.end}✂]`, snippet: `[✂${expression.start}-${expression.end}✂]`
string: this.code.slice( expression.start, expression.end )
}; };
return expression._contextualised;
} }
generate ( result, options, { name, format } ) { generate ( result, options, { name, format } ) {

@ -2,7 +2,20 @@ import CodeBuilder from '../../utils/CodeBuilder.js';
import deindent from '../../utils/deindent.js'; import deindent from '../../utils/deindent.js';
export default class Block { export default class Block {
constructor ({ generator, name, key, expression, context, contextDependencies, contexts, indexes, params, indexNames, listNames }) { constructor ({
generator,
name,
key,
expression,
context,
contextDependencies,
dependencies,
contexts,
indexes,
params,
indexNames,
listNames
}) {
this.generator = generator; this.generator = generator;
this.name = name; this.name = name;
this.key = key; this.key = key;
@ -12,6 +25,7 @@ export default class Block {
this.contexts = contexts; this.contexts = contexts;
this.indexes = indexes; this.indexes = indexes;
this.contextDependencies = contextDependencies; this.contextDependencies = contextDependencies;
this.dependencies = dependencies;
this.params = params; this.params = params;
this.indexNames = indexNames; this.indexNames = indexNames;

@ -7,7 +7,7 @@ import deindent from '../../utils/deindent.js';
import CodeBuilder from '../../utils/CodeBuilder.js'; import CodeBuilder from '../../utils/CodeBuilder.js';
import visit from './visit.js'; import visit from './visit.js';
import Generator from '../Generator.js'; import Generator from '../Generator.js';
import Block from './Block.js'; import preprocess from './preprocess.js';
import * as shared from '../../shared/index.js'; import * as shared from '../../shared/index.js';
class DomGenerator extends Generator { class DomGenerator extends Generator {
@ -47,37 +47,13 @@ export default function dom ( parsed, source, options ) {
const { computations, hasJs, templateProperties, namespace } = generator.parseJs(); const { computations, hasJs, templateProperties, namespace } = generator.parseJs();
const getUniqueName = generator.getUniqueNameMaker( [ 'root' ] ); const { block, state } = preprocess( generator, parsed.html.children, namespace );
const component = getUniqueName( 'component' );
const mainBlock = new Block({
generator,
name: generator.alias( 'create_main_fragment' ),
key: null,
component,
contexts: new Map(),
indexes: new Map(),
params: [ 'root' ],
indexNames: new Map(),
listNames: new Map(),
getUniqueName
});
const state = {
namespace,
parentNode: null,
isTopLevel: true
};
parsed.html.children.forEach( node => { parsed.html.children.forEach( node => {
visit( generator, mainBlock, state, node ); visit( generator, block, state, node );
}); });
generator.addBlock( mainBlock ); generator.addBlock( block );
const builders = { const builders = {
main: new CodeBuilder(), main: new CodeBuilder(),

@ -0,0 +1,118 @@
import Block from './Block.js';
function isElseIf ( node ) {
return node && node.children.length === 1 && node.children[0].type === 'IfBlock';
}
const preprocessors = {
MustacheTag: ( generator, block, node ) => {
const { dependencies } = block.contextualise( node.expression );
dependencies.forEach( dependency => {
block.dependencies.add( dependency );
});
},
IfBlock: ( generator, block, node ) => {
function attachBlocks ( node ) {
const { dependencies } = block.contextualise( node.expression );
node._block = block.child({
name: generator.getUniqueName( `create_if_block` )
});
preprocessChildren( generator, node._block, node.children );
if ( isElseIf( node.else ) ) {
attachBlocks( node.else );
} else if ( node.else ) {
node.else._block = block.child({
name: generator.getUniqueName( `create_if_block` )
});
preprocessChildren( generator, node.else._block, node.else.children );
}
}
attachBlocks ( node );
},
EachBlock: ( generator, block, node ) => {
const { dependencies } = block.contextualise( node.expression );
const indexNames = new Map( block.indexNames );
const indexName = node.index || block.getUniqueName( `${node.context}_index` );
indexNames.set( node.context, indexName );
const listNames = new Map( block.listNames );
const listName = block.getUniqueName( `each_block_value` );
listNames.set( node.context, listName );
const context = generator.getUniqueName( node.context );
const contexts = new Map( block.contexts );
contexts.set( node.context, context );
const indexes = new Map( block.indexes );
if ( node.index ) indexes.set( indexName, node.context );
const contextDependencies = new Map( block.contextDependencies );
contextDependencies.set( node.context, dependencies );
node._block = block.child({
name: generator.getUniqueName( 'create_each_block' ),
expression: node.expression,
context: node.context,
key: node.key,
contextDependencies,
contexts,
indexes,
indexNames,
listNames,
params: block.params.concat( listName, context, indexName )
});
preprocessChildren( generator, node._block, node.children );
},
Element: ( generator, block, node ) => {
// TODO attributes and bindings (and refs?)...
preprocessChildren( generator, block, node.children );
}
};
preprocessors.RawMustacheTag = preprocessors.MustacheTag;
function preprocessChildren ( generator, block, children ) {
children.forEach( child => {
const preprocess = preprocessors[ child.type ];
if ( preprocess ) preprocess( generator, block, child );
});
}
export default function preprocess ( generator, children, namespace ) {
const block = new Block({
generator,
name: generator.alias( 'create_main_fragment' ),
key: null,
contexts: new Map(),
indexes: new Map(),
params: [ 'root' ],
indexNames: new Map(),
listNames: new Map(),
dependencies: new Set()
});
const state = {
namespace,
parentNode: null,
isTopLevel: true
};
preprocessChildren( generator, block, children );
return { block, state };
}

@ -28,12 +28,12 @@ export default function visitAttribute ( generator, block, state, node, attribut
else { else {
// simple dynamic attributes // simple dynamic attributes
const { dependencies, string } = generator.contextualise( block, value.expression ); const { dependencies, snippet } = block.contextualise( value.expression );
// TODO only update attributes that have changed // TODO only update attributes that have changed
local.dynamicAttributes.push({ local.dynamicAttributes.push({
name: attribute.name, name: attribute.name,
value: string, value: snippet,
dependencies dependencies
}); });
} }
@ -48,12 +48,12 @@ export default function visitAttribute ( generator, block, state, node, attribut
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( block, chunk.expression ); const { dependencies, snippet } = block.contextualise( chunk.expression );
dependencies.forEach( dependency => { dependencies.forEach( dependency => {
if ( !~allDependencies.indexOf( dependency ) ) allDependencies.push( dependency ); if ( !~allDependencies.indexOf( dependency ) ) allDependencies.push( dependency );
}); });
return `( ${string} )`; return `( ${snippet} )`;
} }
}).join( ' + ' ) }).join( ' + ' )
); );

@ -4,7 +4,7 @@ import getSetter from '../shared/binding/getSetter.js';
export default function visitBinding ( generator, block, state, node, attribute, local ) { export default function visitBinding ( generator, block, state, node, attribute, local ) {
const { name, keypath } = flattenReference( attribute.value ); const { name, keypath } = flattenReference( attribute.value );
const { snippet, contexts, dependencies } = generator.contextualise( block, attribute.value ); const { snippet, contexts, dependencies } = block.contextualise( 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!' );

@ -7,7 +7,7 @@ export default function visitEventHandler ( generator, block, state, node, attri
const usedContexts = []; const usedContexts = [];
attribute.expression.arguments.forEach( arg => { attribute.expression.arguments.forEach( arg => {
const { contexts } = generator.contextualise( block, arg, null, true ); const { contexts } = block.contextualise( arg, null, true );
contexts.forEach( context => { contexts.forEach( context => {
if ( !~usedContexts.indexOf( context ) ) usedContexts.push( context ); if ( !~usedContexts.indexOf( context ) ) usedContexts.push( context );

@ -5,7 +5,7 @@ import visit from '../visit.js';
export default function visitEachBlock ( generator, block, state, node ) { export default function visitEachBlock ( generator, block, state, node ) {
const each_block = generator.getUniqueName( `each_block` ); const each_block = generator.getUniqueName( `each_block` );
const each_block_else = generator.getUniqueName( `${each_block}_else` ); const each_block_else = generator.getUniqueName( `${each_block}_else` );
const create_each_block = generator.getUniqueName( `create_each_block` ); const create_each_block = node._block.name;
const create_each_block_else = generator.getUniqueName( `${create_each_block}_else` ); const create_each_block_else = generator.getUniqueName( `${create_each_block}_else` );
const listName = block.getUniqueName( `${each_block}_value` ); const listName = block.getUniqueName( `${each_block}_value` );
const iterations = block.getUniqueName( `${each_block}_iterations` ); const iterations = block.getUniqueName( `${each_block}_iterations` );
@ -15,7 +15,7 @@ export default function visitEachBlock ( generator, block, state, node ) {
const vars = { each_block, create_each_block, listName, iterations, i, params, anchor }; const vars = { each_block, create_each_block, listName, iterations, i, params, anchor };
const { dependencies, snippet } = generator.contextualise( block, node.expression ); const { dependencies, snippet } = block.contextualise( node.expression );
block.createAnchor( anchor, state.parentNode ); block.createAnchor( anchor, state.parentNode );
block.builders.create.addLine( `var ${listName} = ${snippet};` ); block.builders.create.addLine( `var ${listName} = ${snippet};` );
@ -75,37 +75,38 @@ export default function visitEachBlock ( generator, block, state, node ) {
` ); ` );
} }
const indexNames = new Map( block.indexNames ); // const indexNames = new Map( block.indexNames );
const indexName = node.index || block.getUniqueName( `${node.context}_index` ); // const indexName = node.index || block.getUniqueName( `${node.context}_index` );
indexNames.set( node.context, indexName ); // indexNames.set( node.context, indexName );
const listNames = new Map( block.listNames ); // const listNames = new Map( block.listNames );
listNames.set( node.context, listName ); // listNames.set( node.context, listName );
const context = generator.getUniqueName( node.context ); // const context = generator.getUniqueName( node.context );
const contexts = new Map( block.contexts ); // const contexts = new Map( block.contexts );
contexts.set( node.context, context ); // contexts.set( node.context, context );
const indexes = new Map( block.indexes ); // const indexes = new Map( block.indexes );
if ( node.index ) indexes.set( indexName, node.context ); // if ( node.index ) indexes.set( indexName, node.context );
const contextDependencies = new Map( block.contextDependencies ); // const contextDependencies = new Map( block.contextDependencies );
contextDependencies.set( node.context, dependencies ); // contextDependencies.set( node.context, dependencies );
const childBlock = block.child({ // const childBlock = block.child({
name: vars.create_each_block, // name: vars.create_each_block,
expression: node.expression, // expression: node.expression,
context: node.context, // context: node.context,
key: node.key, // key: node.key,
contextDependencies, // contextDependencies,
contexts, // contexts,
indexes, // indexes,
indexNames, // indexNames,
listNames, // listNames,
params: block.params.concat( listName, context, indexName ) // params: block.params.concat( listName, context, indexName )
}); // });
const childBlock = node._block;
const childState = Object.assign( {}, state, { const childState = Object.assign( {}, state, {
parentNode: null, parentNode: null,

@ -27,7 +27,7 @@ export default function visitAttribute ( generator, block, state, node, attribut
if ( attribute.value.length === 1 ) { if ( attribute.value.length === 1 ) {
// single {{tag}} — may be a non-string // single {{tag}} — may be a non-string
const { snippet } = generator.contextualise( block, attribute.value[0].expression ); const { snippet } = block.contextualise( attribute.value[0].expression );
value = snippet; value = snippet;
} else { } else {
// '{{foo}} {{bar}}' — treat as string concatenation // '{{foo}} {{bar}}' — treat as string concatenation
@ -36,7 +36,7 @@ export default function visitAttribute ( generator, block, state, node, attribut
if ( chunk.type === 'Text' ) { if ( chunk.type === 'Text' ) {
return JSON.stringify( chunk.data ); return JSON.stringify( chunk.data );
} else { } else {
const { snippet } = generator.contextualise( block, chunk.expression ); const { snippet } = block.contextualise( chunk.expression );
return `( ${snippet} )`; return `( ${snippet} )`;
} }
}).join( ' + ' ) }).join( ' + ' )

@ -5,7 +5,7 @@ import getStaticAttributeValue from './getStaticAttributeValue.js';
export default function visitBinding ( generator, block, state, node, attribute ) { export default function visitBinding ( generator, block, state, node, attribute ) {
const { name, keypath } = flattenReference( attribute.value ); const { name, keypath } = flattenReference( attribute.value );
const { snippet, contexts, dependencies } = generator.contextualise( block, attribute.value ); const { snippet, contexts, dependencies } = block.contextualise( 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!' );

@ -1,18 +1,21 @@
import deindent from '../../../utils/deindent.js'; import deindent from '../../../utils/deindent.js';
import visit from '../visit.js'; import visit from '../visit.js';
function isElseIf ( node ) {
return node && node.children.length === 1 && node.children[0].type === 'IfBlock';
}
function getConditionsAndBlocks ( generator, block, state, node, _name, i = 0 ) { function getConditionsAndBlocks ( generator, block, state, node, _name, i = 0 ) {
const name = generator.getUniqueName( `${_name}_${i}` ); const name = generator.getUniqueName( `${_name}_${i}` );
const conditionsAndBlocks = [{ const conditionsAndBlocks = [{
condition: generator.contextualise( block, node.expression ).snippet, condition: block.contextualise( node.expression ).snippet,
block: name block: name
}]; }];
generateBlock( generator, block, state, node, name ); generateBlock( generator, block, state, node, name );
if ( node.else && node.else.children.length === 1 && if ( isElseIf( node.else ) ) {
node.else.children[0].type === 'IfBlock' ) {
conditionsAndBlocks.push( conditionsAndBlocks.push(
...getConditionsAndBlocks( generator, block, state, node.else.children[0], _name, i + 1 ) ...getConditionsAndBlocks( generator, block, state, node.else.children[0], _name, i + 1 )
); );
@ -27,6 +30,7 @@ function getConditionsAndBlocks ( generator, block, state, node, _name, i = 0 )
generateBlock( generator, block, state, node.else, name ); generateBlock( generator, block, state, node.else, name );
} }
} }
return conditionsAndBlocks; return conditionsAndBlocks;
} }
@ -53,7 +57,6 @@ export default function visitIfBlock ( generator, block, state, node ) {
const currentBlock = block.getUniqueName( `current_block` ); const currentBlock = block.getUniqueName( `current_block` );
const _currentBlock = block.getUniqueName( `_current_block` ); const _currentBlock = block.getUniqueName( `_current_block` );
const isToplevel = !state.parentNode;
const conditionsAndBlocks = getConditionsAndBlocks( generator, block, state, node, generator.getUniqueName( `create_if_block` ) ); const conditionsAndBlocks = getConditionsAndBlocks( generator, block, state, node, generator.getUniqueName( `create_if_block` ) );
const anchor = `${name}_anchor`; const anchor = `${name}_anchor`;
@ -70,11 +73,12 @@ export default function visitIfBlock ( generator, block, state, node ) {
var ${name} = ${currentBlock} && ${currentBlock}( ${params}, ${block.component} ); var ${name} = ${currentBlock} && ${currentBlock}( ${params}, ${block.component} );
` ); ` );
const mountStatement = `if ( ${name} ) ${name}.mount( ${anchor}.parentNode, ${anchor} );`; const isToplevel = !state.parentNode;
if ( isToplevel ) { if ( isToplevel ) {
block.builders.mount.addLine( mountStatement ); block.builders.mount.addLine( `if ( ${name} ) ${name}.mount( ${block.target}, ${anchor} );` );
} else { } else {
block.builders.create.addLine( mountStatement ); block.builders.create.addLine( `if ( ${name} ) ${name}.mount( ${state.parentNode}, ${anchor} );` );
} }
block.builders.update.addBlock( deindent` block.builders.update.addBlock( deindent`

@ -3,7 +3,7 @@ import deindent from '../../../utils/deindent.js';
export default function visitMustacheTag ( generator, block, state, node ) { export default function visitMustacheTag ( generator, block, state, node ) {
const name = block.getUniqueName( 'text' ); const name = block.getUniqueName( 'text' );
const { snippet } = generator.contextualise( block, node.expression ); const { snippet } = block.contextualise( node.expression );
block.builders.create.addLine( `var last_${name} = ${snippet};` ); block.builders.create.addLine( `var last_${name} = ${snippet};` );
block.addElement( name, `${generator.helper( 'createText' )}( last_${name} )`, state.parentNode, true ); block.addElement( name, `${generator.helper( 'createText' )}( last_${name} )`, state.parentNode, true );

@ -3,7 +3,7 @@ import deindent from '../../../utils/deindent.js';
export default function visitRawMustacheTag ( generator, block, state, node ) { export default function visitRawMustacheTag ( generator, block, state, node ) {
const name = block.getUniqueName( 'raw' ); const name = block.getUniqueName( 'raw' );
const { snippet } = generator.contextualise( block, node.expression ); const { snippet } = block.contextualise( 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.

@ -27,4 +27,8 @@ export default class Block {
child ( options ) { child ( options ) {
return new Block( Object.assign( {}, this, options, { parent: this } ) ); return new Block( Object.assign( {}, this, options, { parent: this } ) );
} }
contextualise ( expression, context, isEventHandler ) {
return this.generator.contextualise( this, expression, context, isEventHandler );
}
} }

@ -5,7 +5,7 @@ export default function visitComponent ( generator, block, 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 } = block.contextualise( chunk.expression );
return '${__escape( ' + snippet + ')}'; return '${__escape( ' + snippet + ')}';
} }
} }
@ -34,7 +34,7 @@ export default function visitComponent ( generator, block, node ) {
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 { } else {
const { snippet } = generator.contextualise( block, chunk.expression ); const { snippet } = block.contextualise( chunk.expression );
value = snippet; value = snippet;
} }
} else { } else {

@ -1,7 +1,7 @@
import visit from '../visit.js'; import visit from '../visit.js';
export default function visitEachBlock ( generator, block, node ) { export default function visitEachBlock ( generator, block, node ) {
const { dependencies, snippet } = generator.contextualise( block, node.expression ); const { dependencies, snippet } = block.contextualise( node.expression );
const open = `\${ ${snippet}.map( ${ node.index ? `( ${node.context}, ${node.index} )` : node.context} => \``; const open = `\${ ${snippet}.map( ${ node.index ? `( ${node.context}, ${node.index} )` : node.context} => \``;
generator.append( open ); generator.append( open );

@ -30,7 +30,7 @@ export default function visitElement ( generator, block, node ) {
return chunk.data; return chunk.data;
} }
const { snippet } = generator.contextualise( block, chunk.expression ); const { snippet } = block.contextualise( chunk.expression );
return '${' + snippet + '}'; return '${' + snippet + '}';
}).join( '' ) + `"`; }).join( '' ) + `"`;
} }

@ -1,7 +1,7 @@
import visit from '../visit.js'; import visit from '../visit.js';
export default function visitIfBlock ( generator, block, node ) { export default function visitIfBlock ( generator, block, node ) {
const { snippet } = generator.contextualise( block, node.expression ); const { snippet } = block.contextualise( node.expression );
generator.append( '${ ' + snippet + ' ? `' ); generator.append( '${ ' + snippet + ' ? `' );

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

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