You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
svelte/compiler/generate/index.js

266 lines
7.0 KiB

8 years ago
import { getLocator } from 'locate-character';
8 years ago
import deindent from './utils/deindent.js';
import walkHtml from './utils/walkHtml.js';
const ROOT = 'options.target';
8 years ago
8 years ago
export default function generate ( parsed, template ) {
8 years ago
const counters = {
element: 0,
text: 0,
8 years ago
anchor: 0,
if: 0
8 years ago
};
8 years ago
const initStatements = [];
const setStatements = [ deindent`
const oldState = state;
state = Object.assign( {}, oldState, newState );
` ];
const teardownStatements = [];
8 years ago
// TODO add contents of <script> tag, with `export default` replaced with `var template =`
// TODO css
8 years ago
const locator = getLocator( template );
8 years ago
parsed.html.children.forEach( child => {
8 years ago
const declarations = [];
8 years ago
let current = {
target: ROOT,
8 years ago
conditions: [],
children: [],
renderBlocks: [],
removeBlocks: [],
anchor: null
8 years ago
};
const stack = [ current ];
8 years ago
walkHtml( child, {
enter ( node ) {
if ( node.type === 'Element' ) {
8 years ago
current = {
target: `element_${counters.element++}`,
8 years ago
conditions: current.conditions,
children: current.children,
renderBlocks: current.renderBlocks,
removeBlocks: current.removeBlocks,
anchor: current.anchor
8 years ago
};
stack.push( current );
8 years ago
8 years ago
declarations.push( `var ${current.target};` );
8 years ago
8 years ago
if ( current.anchor ) {
current.renderBlocks.push( deindent`
${current.target} = document.createElement( '${node.name}' );
${current.anchor}.parentNode.insertBefore( ${current.target}, ${current.anchor} );
` );
} else {
current.renderBlocks.push( deindent`
${current.target} = document.createElement( '${node.name}' );
options.target.appendChild( ${current.target} );
` );
}
8 years ago
8 years ago
current.removeBlocks.push( deindent`
8 years ago
${current.target}.parentNode.removeChild( ${current.target} );
8 years ago
` );
}
else if ( node.type === 'Text' ) {
8 years ago
if ( current.target === ROOT ) {
const identifier = `text_${counters.text++}`;
8 years ago
declarations.push( `var ${identifier};` );
current.renderBlocks.push( deindent`
${identifier} = document.createTextNode( ${JSON.stringify( node.data )} );
8 years ago
${current.target}.appendChild( ${identifier} );
` );
8 years ago
current.removeBlocks.push( deindent`
8 years ago
${identifier}.parentNode.removeChild( ${identifier} );
8 years ago
${identifier} = null;
8 years ago
` );
}
else {
8 years ago
current.renderBlocks.push( deindent`
8 years ago
${current.target}.appendChild( document.createTextNode( ${JSON.stringify( node.data )} ) );
` );
}
}
else if ( node.type === 'MustacheTag' ) {
const identifier = `text_${counters.text++}`;
const expression = node.expression.type === 'Identifier' ? node.expression.name : 'TODO'; // TODO handle block-local state
8 years ago
declarations.push( `var ${identifier};` );
current.renderBlocks.push( deindent`
${identifier} = document.createTextNode( '' );
8 years ago
${current.target}.appendChild( ${identifier} );
8 years ago
` );
8 years ago
8 years ago
setStatements.push( deindent`
if ( state.${expression} !== oldState.${expression} ) { // TODO and conditions
8 years ago
${identifier}.data = state.${expression};
}
` );
8 years ago
current.removeBlocks.push( deindent`
${identifier}.parentNode.removeChild( ${identifier} );
${identifier} = null;
` );
}
else if ( node.type === 'IfBlock' ) {
const anchor = `anchor_${counters.anchor++}`;
const suffix = `if_${counters.if++}`;
declarations.push( `var ${anchor};` );
const expression = node.expression.type === 'Identifier' ? node.expression.name : 'TODO'; // TODO handle block-local state
current.renderBlocks.push( deindent`
${anchor} = document.createComment( '#if ${template.slice( node.expression.start, node.expression.end)}' );
${current.target}.appendChild( ${anchor} );
` );
current.removeBlocks.push( deindent`
${anchor}.parentNode.removeChild( ${anchor} );
${anchor} = null;
` );
current = {
renderName: `render_${suffix}`,
removeName: `remove_${suffix}`,
target: current.target,
conditions: current.conditions.concat( expression ),
renderBlocks: [],
removeBlocks: [],
anchor
};
setStatements.push( deindent`
// TODO account for conditions (nested ifs)
if ( state.${expression} && !oldState.${expression} ) ${current.renderName}();
else if ( !state.${expression} && oldState.${expression} ) ${current.removeName}();
` );
teardownStatements.push( deindent`
// TODO account for conditions (nested ifs)
if ( state.${expression} ) ${current.removeName}();
` );
stack.push( current );
8 years ago
}
else {
throw new Error( `Not implemented: ${node.type}` );
}
},
leave ( node ) {
8 years ago
if ( node.type === 'IfBlock' ) {
const { line, column } = locator( node.start );
initStatements.push( deindent`
// (${line}:${column}) {{#if ${template.slice( node.expression.start, node.expression.end )}}}...{{/if}}
function ${current.renderName} () {
${current.renderBlocks.join( '\n\n' )}
}
function ${current.removeName} () {
${current.removeBlocks.join( '\n\n' )}
}
` );
stack.pop();
current = stack[ stack.length - 1 ];
}
else if ( node.type === 'Element' ) {
8 years ago
stack.pop();
current = stack[ stack.length - 1 ];
8 years ago
}
}
});
8 years ago
initStatements.push( ...current.renderBlocks );
teardownStatements.push( ...current.removeBlocks );
8 years ago
});
8 years ago
teardownStatements.push( deindent`
state = {};
` );
8 years ago
const code = deindent`
8 years ago
export default function createComponent ( options ) {
8 years ago
var component = {};
var state = {};
var observers = {
immediate: Object.create( null ),
deferred: Object.create( null )
};
// universal methods
function dispatchObservers ( group, state, oldState ) {
for ( const key in group ) {
const newValue = state[ key ];
const oldValue = oldState[ key ];
if ( newValue === oldValue && typeof newValue !== 'object' ) continue;
const callbacks = group[ key ];
if ( !callbacks ) continue;
for ( let i = 0; i < callbacks.length; i += 1 ) {
callbacks[i].call( component, newValue, oldValue );
}
}
}
component.get = function get ( key ) {
return state[ key ];
};
component.observe = function ( key, callback, options = {} ) {
const group = options.defer ? observers.deferred : observers.immediate;
( group[ key ] || ( group[ key ] = [] ) ).push( callback );
if ( options.init !== false ) callback( state[ key ] );
return {
cancel () {
const index = group[ key ].indexOf( callback );
if ( ~index ) group[ key ].splice( index, 1 );
}
};
};
8 years ago
// component-specific methods
component.set = function set ( newState ) {
${setStatements.join( '\n\n' )}
};
8 years ago
component.teardown = function teardown () {
8 years ago
${teardownStatements.join( '\n\n' )}
8 years ago
};
8 years ago
${initStatements.join( '\n\n' )}
8 years ago
component.set( options.data );
return component;
8 years ago
}
8 years ago
`;
8 years ago
return { code };
8 years ago
}