pull/31/head
Rich Harris 8 years ago
parent e620fbbd69
commit 4e7bd1dd2c

@ -1,31 +1,39 @@
import { getLocator } from 'locate-character';
import deindent from './utils/deindent.js'; import deindent from './utils/deindent.js';
import walkHtml from './utils/walkHtml.js'; import walkHtml from './utils/walkHtml.js';
const ROOT = 'options.target'; const ROOT = 'options.target';
export default function generate ( parsed ) { export default function generate ( parsed, template ) {
const counters = { const counters = {
element: 0, element: 0,
text: 0, text: 0,
anchor: 0 anchor: 0,
if: 0
}; };
const renderBlocks = []; const initStatements = [];
const updateBlocks = []; const setStatements = [ deindent`
const teardownBlocks = []; const oldState = state;
state = Object.assign( {}, oldState, newState );
const codeBlocks = []; ` ];
const teardownStatements = [];
// TODO add contents of <script> tag, with `export default` replaced with `var template =` // TODO add contents of <script> tag, with `export default` replaced with `var template =`
// TODO css // TODO css
// create component const locator = getLocator( template );
parsed.html.children.forEach( child => { parsed.html.children.forEach( child => {
const declarations = [];
let current = { let current = {
target: ROOT, target: ROOT,
indentation: 0, conditions: [],
block: [] children: [],
renderBlocks: [],
removeBlocks: [],
anchor: null
}; };
const stack = [ current ]; const stack = [ current ];
@ -35,24 +43,30 @@ export default function generate ( parsed ) {
if ( node.type === 'Element' ) { if ( node.type === 'Element' ) {
current = { current = {
target: `element_${counters.element++}`, target: `element_${counters.element++}`,
indentation: current.indentation, conditions: current.conditions,
block: current.block children: current.children,
renderBlocks: current.renderBlocks,
removeBlocks: current.removeBlocks,
anchor: current.anchor
}; };
stack.push( current ); stack.push( current );
const renderBlock = deindent` declarations.push( `var ${current.target};` );
var ${current.target} = document.createElement( '${node.name}' );
options.target.appendChild( ${current.target} );
`;
renderBlocks.push( renderBlock ); if ( current.anchor ) {
current.renderBlocks.push( deindent`
// updateBlocks.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} );
` );
}
teardownBlocks.push( deindent` current.removeBlocks.push( deindent`
${current.target}.parentNode.removeChild( ${current.target} ); ${current.target}.parentNode.removeChild( ${current.target} );
` ); ` );
} }
@ -61,18 +75,21 @@ export default function generate ( parsed ) {
if ( current.target === ROOT ) { if ( current.target === ROOT ) {
const identifier = `text_${counters.text++}`; const identifier = `text_${counters.text++}`;
renderBlocks.push( deindent` declarations.push( `var ${identifier};` );
var ${identifier} = document.createTextNode( ${JSON.stringify( node.data )} );
current.renderBlocks.push( deindent`
${identifier} = document.createTextNode( ${JSON.stringify( node.data )} );
${current.target}.appendChild( ${identifier} ); ${current.target}.appendChild( ${identifier} );
` ); ` );
teardownBlocks.push( deindent` current.removeBlocks.push( deindent`
${identifier}.parentNode.removeChild( ${identifier} ); ${identifier}.parentNode.removeChild( ${identifier} );
${identifier} = null;
` ); ` );
} }
else { else {
renderBlocks.push( deindent` current.renderBlocks.push( deindent`
${current.target}.appendChild( document.createTextNode( ${JSON.stringify( node.data )} ) ); ${current.target}.appendChild( document.createTextNode( ${JSON.stringify( node.data )} ) );
` ); ` );
} }
@ -82,22 +99,65 @@ export default function generate ( parsed ) {
const identifier = `text_${counters.text++}`; const identifier = `text_${counters.text++}`;
const expression = node.expression.type === 'Identifier' ? node.expression.name : 'TODO'; // TODO handle block-local state const expression = node.expression.type === 'Identifier' ? node.expression.name : 'TODO'; // TODO handle block-local state
renderBlocks.push( deindent` declarations.push( `var ${identifier};` );
var ${identifier} = document.createTextNode( '' );
current.renderBlocks.push( deindent`
${identifier} = document.createTextNode( '' );
${current.target}.appendChild( ${identifier} ); ${current.target}.appendChild( ${identifier} );
` ); ` );
updateBlocks.push( deindent` setStatements.push( deindent`
if ( state.${expression} !== oldState.${expression} ) { if ( state.${expression} !== oldState.${expression} ) { // TODO and conditions
${identifier}.data = state.${expression}; ${identifier}.data = state.${expression};
} }
` ); ` );
if ( current.target === ROOT ) { current.removeBlocks.push( deindent`
teardownBlocks.push( deindent` ${identifier}.parentNode.removeChild( ${identifier} );
${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 );
} }
else { else {
@ -106,14 +166,39 @@ export default function generate ( parsed ) {
}, },
leave ( node ) { leave ( node ) {
if ( node.type === 'Element' ) { 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' ) {
stack.pop(); stack.pop();
current = stack[ stack.length - 1 ]; current = stack[ stack.length - 1 ];
} }
} }
}); });
initStatements.push( ...current.renderBlocks );
teardownStatements.push( ...current.removeBlocks );
}); });
teardownStatements.push( deindent`
state = {};
` );
const code = deindent` const code = deindent`
export default function createComponent ( options ) { export default function createComponent ( options ) {
var component = {}; var component = {};
@ -145,13 +230,6 @@ export default function generate ( parsed ) {
return state[ key ]; return state[ key ];
}; };
component.set = function set ( newState ) {
const oldState = state;
state = Object.assign( {}, oldState, newState );
${updateBlocks.join( '\n\n' )}
};
component.observe = function ( key, callback, options = {} ) { component.observe = function ( key, callback, options = {} ) {
const group = options.defer ? observers.deferred : observers.immediate; const group = options.defer ? observers.deferred : observers.immediate;
@ -166,12 +244,16 @@ export default function generate ( parsed ) {
}; };
}; };
// component-specific methods
component.set = function set ( newState ) {
${setStatements.join( '\n\n' )}
};
component.teardown = function teardown () { component.teardown = function teardown () {
${teardownBlocks.join( '\n\n' )} ${teardownStatements.join( '\n\n' )}
state = {};
}; };
${renderBlocks.join( '\n\n' )} ${initStatements.join( '\n\n' )}
component.set( options.data ); component.set( options.data );

@ -1,38 +1,36 @@
import * as assert from 'assert'; import * as assert from 'assert';
import deindent from './deindent.js'; import deindent from './deindent.js';
describe( 'utils', () => { describe( 'deindent', () => {
describe( 'deindent', () => { it( 'deindents a simple string', () => {
it( 'deindents a simple string', () => { const deindented = deindent`
const deindented = deindent` deindent me please
deindent me please `;
`;
assert.equal( deindented, `deindent me please` ); assert.equal( deindented, `deindent me please` );
}); });
it( 'deindents a multiline string', () => { it( 'deindents a multiline string', () => {
const deindented = deindent` const deindented = deindent`
deindent me please deindent me please
and me as well and me as well
`; `;
assert.equal( deindented, `deindent me please\nand me as well` ); assert.equal( deindented, `deindent me please\nand me as well` );
}); });
it( 'preserves indentation of inserted values', () => { it( 'preserves indentation of inserted values', () => {
const insert = deindent` const insert = deindent`
line one line one
line two line two
`; `;
const deindented = deindent` const deindented = deindent`
before before
${insert} ${insert}
after after
`; `;
assert.equal( deindented, `before\n\tline one\n\tline two\nafter` ); assert.equal( deindented, `before\n\tline one\n\tline two\nafter` );
});
}); });
}); });

@ -4,7 +4,7 @@ import generate from './generate/index.js';
export function compile ( template ) { export function compile ( template ) {
const parsed = parse( template ); const parsed = parse( template );
// TODO validate template // TODO validate template
const generated = generate( parsed ); const generated = generate( parsed, template );
return generated; return generated;
} }

@ -1,7 +1,17 @@
import * as assert from 'assert';
export default { export default {
solo: true,
show: true,
description: '{{#if}}...{{/if}} block', description: '{{#if}}...{{/if}} block',
data: { data: {
visible: true visible: true
}, },
html: '<p>i am visible</p>' html: '<p>i am visible</p><!--#if visible-->',
test ( component, target ) {
component.set({ visible: false });
assert.equal( target.innerHTML, '<!--#if visible-->' );
component.set({ visible: true });
assert.equal( target.innerHTML, '<p>i am visible</p><!--#if visible-->' );
}
}; };

@ -60,12 +60,26 @@ describe( 'svelte', () => {
} }
const { code } = compiled; const { code } = compiled;
const withLineNumbers = code.split( '\n' ).map( ( line, i ) => {
i = String( i + 1 );
while ( i.length < 3 ) i = ` ${i}`;
return `${i}: ${line}`;
}).join( '\n' );
cache[ path.resolve( `test/samples/${dir}/main.svelte` ) ] = code; cache[ path.resolve( `test/samples/${dir}/main.svelte` ) ] = code;
const factory = require( `./samples/${dir}/main.svelte` ).default;
let factory;
try {
factory = require( `./samples/${dir}/main.svelte` ).default;
} catch ( err ) {
console.log( withLineNumbers ); // eslint-disable-line no-console
throw err;
}
if ( config.show ) { if ( config.show ) {
console.log( code ); // eslint-disable-line no-console console.log( withLineNumbers ); // eslint-disable-line no-console
} }
return env() return env()
@ -83,10 +97,13 @@ describe( 'svelte', () => {
if ( config.test ) { if ( config.test ) {
config.test( component, target ); config.test( component, target );
} else {
component.teardown();
assert.equal( target.innerHTML, '' );
} }
}) })
.catch( err => { .catch( err => {
if ( !config.show ) console.log( code ); // eslint-disable-line no-console if ( !config.show ) console.log( withLineNumbers ); // eslint-disable-line no-console
throw err; throw err;
}); });
}); });

Loading…
Cancel
Save