run prettier on src, update tests

pull/618/head
Rich Harris 8 years ago
parent 399ee4595a
commit 84595fb381

@ -42,7 +42,12 @@ export default class Generator {
aliases: Map<string, string>; aliases: Map<string, string>;
usedNames: Set<string>; usedNames: Set<string>;
constructor ( parsed: Parsed, source: string, name: string, options: CompileOptions ) { constructor(
parsed: Parsed,
source: string,
name: string,
options: CompileOptions
) {
this.parsed = parsed; this.parsed = parsed;
this.source = source; this.source = source;
this.name = name; this.name = name;
@ -61,9 +66,9 @@ export default class Generator {
// in dev mode // in dev mode
this.expectedProperties = new Set(); this.expectedProperties = new Set();
this.code = new MagicString( source ); this.code = new MagicString(source);
this.cascade = options.cascade !== false; // TODO remove this option in v2 this.cascade = options.cascade !== false; // TODO remove this option in v2
this.css = parsed.css ? processCss( parsed, this.code, this.cascade ) : null; this.css = parsed.css ? processCss(parsed, this.code, this.cascade) : null;
this.cssId = parsed.css ? `svelte-${parsed.hash}` : ''; this.cssId = parsed.css ? `svelte-${parsed.hash}` : '';
this.usesRefs = false; this.usesRefs = false;
@ -71,105 +76,112 @@ export default class Generator {
// Svelte's builtin `import { get, ... } from 'svelte/shared.ts'`; // Svelte's builtin `import { get, ... } from 'svelte/shared.ts'`;
this.importedNames = new Set(); this.importedNames = new Set();
this.aliases = new Map(); this.aliases = new Map();
this.usedNames = new Set( [ name ] ); this.usedNames = new Set([name]);
} }
addSourcemapLocations ( node: Node ) { addSourcemapLocations(node: Node) {
walk( node, { walk(node, {
enter: ( node: Node ) => { enter: (node: Node) => {
this.code.addSourcemapLocation( node.start ); this.code.addSourcemapLocation(node.start);
this.code.addSourcemapLocation( node.end ); this.code.addSourcemapLocation(node.end);
} }
}); });
} }
alias ( name: string ) { alias(name: string) {
if ( !this.aliases.has( name ) ) { if (!this.aliases.has(name)) {
this.aliases.set( name, this.getUniqueName( name ) ); this.aliases.set(name, this.getUniqueName(name));
} }
return this.aliases.get( name ); return this.aliases.get(name);
} }
contextualise ( block: DomBlock | SsrBlock, expression: Node, context: string, isEventHandler: boolean ) { contextualise(
this.addSourcemapLocations( expression ); block: DomBlock | SsrBlock,
expression: Node,
context: string,
isEventHandler: boolean
) {
this.addSourcemapLocations(expression);
const usedContexts: string[] = []; const usedContexts: string[] = [];
const { code, helpers } = this; const { code, helpers } = this;
const { contexts, indexes } = block; const { contexts, indexes } = block;
let scope = annotateWithScopes( expression ); // TODO this already happens in findDependencies let scope = annotateWithScopes(expression); // TODO this already happens in findDependencies
let lexicalDepth = 0; let lexicalDepth = 0;
const self = this; const self = this;
walk( expression, { walk(expression, {
enter ( node: Node, parent: Node, key: string ) { enter(node: Node, parent: Node, key: string) {
if ( /^Function/.test( node.type ) ) lexicalDepth += 1; if (/^Function/.test(node.type)) lexicalDepth += 1;
if ( node._scope ) { if (node._scope) {
scope = node._scope; scope = node._scope;
return; return;
} }
if ( node.type === 'ThisExpression' ) { if (node.type === 'ThisExpression') {
if ( lexicalDepth === 0 && context ) code.overwrite( node.start, node.end, context, { storeName: true, contentOnly: false } ); if (lexicalDepth === 0 && context)
} code.overwrite(node.start, node.end, context, {
storeName: true,
else if ( isReference( node, parent ) ) { contentOnly: false
const { name } = flattenReference( node ); });
if ( scope.has( name ) ) return; } else if (isReference(node, parent)) {
const { name } = flattenReference(node);
if ( name === 'event' && isEventHandler ) { if (scope.has(name)) return;
if (name === 'event' && isEventHandler) {
// noop // noop
} } else if (contexts.has(name)) {
const contextName = contexts.get(name);
else if ( contexts.has( name ) ) { if (contextName !== name) {
const contextName = contexts.get( name );
if ( contextName !== name ) {
// this is true for 'reserved' names like `state` and `component` // this is true for 'reserved' names like `state` and `component`
code.overwrite( node.start, node.start + name.length, contextName, { storeName: true, contentOnly: false } ); code.overwrite(
node.start,
node.start + name.length,
contextName,
{ storeName: true, contentOnly: false }
);
} }
if ( !~usedContexts.indexOf( name ) ) usedContexts.push( name ); if (!~usedContexts.indexOf(name)) usedContexts.push(name);
} } else if (helpers.has(name)) {
code.prependRight(node.start, `${self.alias('template')}.helpers.`);
else if ( helpers.has( name ) ) { } else if (indexes.has(name)) {
code.prependRight( node.start, `${self.alias( 'template' )}.helpers.` ); const context = indexes.get(name);
} if (!~usedContexts.indexOf(context)) usedContexts.push(context);
} else {
else if ( indexes.has( name ) ) {
const context = indexes.get( name );
if ( !~usedContexts.indexOf( context ) ) usedContexts.push( context );
}
else {
// handle shorthand properties // handle shorthand properties
if ( parent && parent.type === 'Property' && parent.shorthand ) { if (parent && parent.type === 'Property' && parent.shorthand) {
if ( key === 'key' ) { if (key === 'key') {
code.appendLeft( node.start, `${name}: ` ); code.appendLeft(node.start, `${name}: `);
return; return;
} }
} }
if ( globalWhitelist.has( name ) ) { if (globalWhitelist.has(name)) {
code.prependRight( node.start, `( '${name}' in state ? state.` ); code.prependRight(node.start, `( '${name}' in state ? state.`);
code.appendLeft( node.object ? node.object.end : node.end, ` : ${name} )` ); code.appendLeft(
node.object ? node.object.end : node.end,
` : ${name} )`
);
} else { } else {
code.prependRight( node.start, `state.` ); code.prependRight(node.start, `state.`);
} }
if ( !~usedContexts.indexOf( 'state' ) ) usedContexts.push( 'state' ); if (!~usedContexts.indexOf('state')) usedContexts.push('state');
} }
this.skip(); this.skip();
} }
}, },
leave ( node: Node ) { leave(node: Node) {
if ( /^Function/.test( node.type ) ) lexicalDepth -= 1; if (/^Function/.test(node.type)) lexicalDepth -= 1;
if ( node._scope ) scope = scope.parent; if (node._scope) scope = scope.parent;
} }
}); });
@ -180,112 +192,133 @@ export default class Generator {
}; };
} }
findDependencies ( contextDependencies: Map<string, string[]>, indexes: Map<string, string>, expression: Node ) { findDependencies(
if ( expression._dependencies ) return expression._dependencies; contextDependencies: Map<string, string[]>,
indexes: Map<string, string>,
expression: Node
) {
if (expression._dependencies) return expression._dependencies;
let scope = annotateWithScopes( expression ); let scope = annotateWithScopes(expression);
const dependencies: string[] = []; const dependencies: string[] = [];
const generator = this; // can't use arrow functions, because of this.skip() const generator = this; // can't use arrow functions, because of this.skip()
walk( expression, { walk(expression, {
enter ( node: Node, parent: Node ) { enter(node: Node, parent: Node) {
if ( node._scope ) { if (node._scope) {
scope = node._scope; scope = node._scope;
return; return;
} }
if ( isReference( node, parent ) ) { if (isReference(node, parent)) {
const { name } = flattenReference( node ); const { name } = flattenReference(node);
if ( scope.has( name ) || generator.helpers.has( name ) ) return; if (scope.has(name) || generator.helpers.has(name)) return;
if ( contextDependencies.has( name ) ) { if (contextDependencies.has(name)) {
dependencies.push( ...contextDependencies.get( name ) ); dependencies.push(...contextDependencies.get(name));
} else if ( !indexes.has( name ) ) { } else if (!indexes.has(name)) {
dependencies.push( name ); dependencies.push(name);
} }
this.skip(); this.skip();
} }
}, },
leave ( node: Node ) { leave(node: Node) {
if ( node._scope ) scope = scope.parent; if (node._scope) scope = scope.parent;
} }
}); });
dependencies.forEach( name => { dependencies.forEach(name => {
if ( !globalWhitelist.has( name ) ) { if (!globalWhitelist.has(name)) {
this.expectedProperties.add( name ); this.expectedProperties.add(name);
} }
}); });
return ( expression._dependencies = dependencies ); return (expression._dependencies = dependencies);
} }
generate ( result, options, { name, format } ) { generate(result, options, { name, format }) {
if ( this.imports.length ) { if (this.imports.length) {
const statements: string[] = []; const statements: string[] = [];
this.imports.forEach( ( declaration, i ) => { this.imports.forEach((declaration, i) => {
if ( format === 'es' ) { if (format === 'es') {
statements.push( this.source.slice( declaration.start, declaration.end ) ); statements.push(
this.source.slice(declaration.start, declaration.end)
);
return; return;
} }
const defaultImport = declaration.specifiers.find( ( x: Node ) => x.type === 'ImportDefaultSpecifier' || x.type === 'ImportSpecifier' && x.imported.name === 'default' ); const defaultImport = declaration.specifiers.find(
const namespaceImport = declaration.specifiers.find( ( x: Node ) => x.type === 'ImportNamespaceSpecifier' ); (x: Node) =>
const namedImports = declaration.specifiers.filter( ( x: Node ) => x.type === 'ImportSpecifier' && x.imported.name !== 'default' ); x.type === 'ImportDefaultSpecifier' ||
(x.type === 'ImportSpecifier' && x.imported.name === 'default')
const name = ( defaultImport || namespaceImport ) ? ( defaultImport || namespaceImport ).local.name : `__import${i}`; );
const namespaceImport = declaration.specifiers.find(
(x: Node) => x.type === 'ImportNamespaceSpecifier'
);
const namedImports = declaration.specifiers.filter(
(x: Node) =>
x.type === 'ImportSpecifier' && x.imported.name !== 'default'
);
const name = defaultImport || namespaceImport
? (defaultImport || namespaceImport).local.name
: `__import${i}`;
declaration.name = name; // hacky but makes life a bit easier later declaration.name = name; // hacky but makes life a bit easier later
namedImports.forEach( ( specifier: Node ) => { namedImports.forEach((specifier: Node) => {
statements.push( `var ${specifier.local.name} = ${name}.${specifier.imported.name}` ); statements.push(
`var ${specifier.local.name} = ${name}.${specifier.imported.name}`
);
}); });
if ( defaultImport ) { if (defaultImport) {
statements.push( `${name} = ( ${name} && ${name}.__esModule ) ? ${name}['default'] : ${name};` ); statements.push(
`${name} = ( ${name} && ${name}.__esModule ) ? ${name}['default'] : ${name};`
);
} }
}); });
result = `${statements.join( '\n' )}\n\n${result}`; result = `${statements.join('\n')}\n\n${result}`;
} }
const pattern = /\[✂(\d+)-(\d+)$/; const pattern = /\[✂(\d+)-(\d+)$/;
const parts = result.split( '✂]' ); const parts = result.split('✂]');
const finalChunk = parts.pop(); const finalChunk = parts.pop();
const compiled = new Bundle({ separator: '' }); const compiled = new Bundle({ separator: '' });
function addString ( str: string ) { function addString(str: string) {
compiled.addSource({ compiled.addSource({
content: new MagicString( str ) content: new MagicString(str)
}); });
} }
const intro = getIntro( format, options, this.imports ); const intro = getIntro(format, options, this.imports);
if ( intro ) addString( intro ); if (intro) addString(intro);
const { filename } = options; const { filename } = options;
// special case — the source file doesn't actually get used anywhere. we need // special case — the source file doesn't actually get used anywhere. we need
// to add an empty file to populate map.sources and map.sourcesContent // to add an empty file to populate map.sources and map.sourcesContent
if ( !parts.length ) { if (!parts.length) {
compiled.addSource({ compiled.addSource({
filename, filename,
content: new MagicString( this.source ).remove( 0, this.source.length ) content: new MagicString(this.source).remove(0, this.source.length)
}); });
} }
parts.forEach( ( str: string ) => { parts.forEach((str: string) => {
const chunk = str.replace( pattern, '' ); const chunk = str.replace(pattern, '');
if ( chunk ) addString( chunk ); if (chunk) addString(chunk);
const match = pattern.exec( str ); const match = pattern.exec(str);
const snippet = this.code.snip( +match[1], +match[2] ); const snippet = this.code.snip(+match[1], +match[2]);
compiled.addSource({ compiled.addSource({
filename, filename,
@ -293,36 +326,52 @@ export default class Generator {
}); });
}); });
addString( finalChunk ); addString(finalChunk);
addString( '\n\n' + getOutro( format, name, options, this.imports ) ); addString('\n\n' + getOutro(format, name, options, this.imports));
return { return {
code: compiled.toString(), code: compiled.toString(),
map: compiled.generateMap({ includeContent: true, file: options.outputFilename }), map: compiled.generateMap({
includeContent: true,
file: options.outputFilename
}),
css: this.css css: this.css
}; };
} }
getUniqueName ( name: string ) { getUniqueName(name: string) {
if ( test ) name = `${name}$`; if (test) name = `${name}$`;
let alias = name; let alias = name;
for ( let i = 1; reservedNames.has( alias ) || this.importedNames.has( alias ) || this.usedNames.has( alias ); alias = `${name}_${i++}` ); for (
this.usedNames.add( alias ); let i = 1;
reservedNames.has(alias) ||
this.importedNames.has(alias) ||
this.usedNames.has(alias);
alias = `${name}_${i++}`
);
this.usedNames.add(alias);
return alias; return alias;
} }
getUniqueNameMaker ( params ) { getUniqueNameMaker(params) {
const localUsedNames = new Set( params ); const localUsedNames = new Set(params);
return name => { return name => {
if ( test ) name = `${name}$`; if (test) name = `${name}$`;
let alias = name; let alias = name;
for ( let i = 1; reservedNames.has( alias ) || this.importedNames.has( alias ) || this.usedNames.has( alias ) || localUsedNames.has( alias ); alias = `${name}_${i++}` ); for (
localUsedNames.add( alias ); let i = 1;
reservedNames.has(alias) ||
this.importedNames.has(alias) ||
this.usedNames.has(alias) ||
localUsedNames.has(alias);
alias = `${name}_${i++}`
);
localUsedNames.add(alias);
return alias; return alias;
}; };
} }
parseJs ( ssr: boolean = false ) { parseJs(ssr: boolean = false) {
const { source } = this; const { source } = this;
const { js } = this.parsed; const { js } = this.parsed;
@ -333,154 +382,213 @@ export default class Generator {
let namespace = null; let namespace = null;
let hasJs = !!js; let hasJs = !!js;
if ( js ) { if (js) {
this.addSourcemapLocations( js.content ); this.addSourcemapLocations(js.content);
const body = js.content.body.slice(); // slice, because we're going to be mutating the original const body = js.content.body.slice(); // slice, because we're going to be mutating the original
// imports need to be hoisted out of the IIFE // imports need to be hoisted out of the IIFE
for ( let i = 0; i < body.length; i += 1 ) { for (let i = 0; i < body.length; i += 1) {
const node = body[i]; const node = body[i];
if ( node.type === 'ImportDeclaration' ) { if (node.type === 'ImportDeclaration') {
removeNode( this.code, js.content, node ); removeNode(this.code, js.content, node);
imports.push( node ); imports.push(node);
node.specifiers.forEach( ( specifier: Node ) => { node.specifiers.forEach((specifier: Node) => {
this.importedNames.add( specifier.local.name ); this.importedNames.add(specifier.local.name);
}); });
} }
} }
const defaultExport = body.find( ( node: Node ) => node.type === 'ExportDefaultDeclaration' ); const defaultExport = body.find(
(node: Node) => node.type === 'ExportDefaultDeclaration'
);
if ( defaultExport ) { if (defaultExport) {
defaultExport.declaration.properties.forEach( ( prop: Node ) => { defaultExport.declaration.properties.forEach((prop: Node) => {
templateProperties[ prop.key.name ] = prop; templateProperties[prop.key.name] = prop;
}); });
} }
[ 'helpers', 'events', 'components', 'transitions' ].forEach( key => { ['helpers', 'events', 'components', 'transitions'].forEach(key => {
if ( templateProperties[ key ] ) { if (templateProperties[key]) {
templateProperties[ key ].value.properties.forEach( ( prop: node ) => { templateProperties[key].value.properties.forEach((prop: node) => {
this[ key ].add( prop.key.name ); this[key].add(prop.key.name);
}); });
} }
}); });
if ( templateProperties.computed ) { if (templateProperties.computed) {
const dependencies = new Map(); const dependencies = new Map();
templateProperties.computed.value.properties.forEach( ( prop: Node ) => { templateProperties.computed.value.properties.forEach((prop: Node) => {
const key = prop.key.name; const key = prop.key.name;
const value = prop.value; const value = prop.value;
const deps = value.params.map( ( param: Node ) => param.type === 'AssignmentPattern' ? param.left.name : param.name ); const deps = value.params.map(
dependencies.set( key, deps ); (param: Node) =>
param.type === 'AssignmentPattern' ? param.left.name : param.name
);
dependencies.set(key, deps);
}); });
const visited = new Set(); const visited = new Set();
function visit ( key ) { function visit(key) {
if ( !dependencies.has( key ) ) return; // not a computation if (!dependencies.has(key)) return; // not a computation
if ( visited.has( key ) ) return; if (visited.has(key)) return;
visited.add( key ); visited.add(key);
const deps = dependencies.get( key ); const deps = dependencies.get(key);
deps.forEach( visit ); deps.forEach(visit);
computations.push({ key, deps }); computations.push({ key, deps });
} }
templateProperties.computed.value.properties.forEach( ( prop: Node ) => visit( prop.key.name ) ); templateProperties.computed.value.properties.forEach((prop: Node) =>
visit(prop.key.name)
);
} }
if ( templateProperties.namespace ) { if (templateProperties.namespace) {
const ns = templateProperties.namespace.value.value; const ns = templateProperties.namespace.value.value;
namespace = namespaces[ ns ] || ns; namespace = namespaces[ns] || ns;
removeObjectKey( this.code, defaultExport.declaration, 'namespace' ); removeObjectKey(this.code, defaultExport.declaration, 'namespace');
} }
if ( templateProperties.components ) { if (templateProperties.components) {
let hasNonImportedComponent = false; let hasNonImportedComponent = false;
templateProperties.components.value.properties.forEach( ( property: Node ) => { templateProperties.components.value.properties.forEach(
const key = property.key.name; (property: Node) => {
const value = source.slice( property.value.start, property.value.end ); const key = property.key.name;
if ( this.importedNames.has( value ) ) { const value = source.slice(
this.importedComponents.set( key, value ); property.value.start,
} else { property.value.end
hasNonImportedComponent = true; );
if (this.importedNames.has(value)) {
this.importedComponents.set(key, value);
} else {
hasNonImportedComponent = true;
}
} }
}); );
if ( hasNonImportedComponent ) { if (hasNonImportedComponent) {
// remove the specific components that were imported, as we'll refer to them directly // remove the specific components that were imported, as we'll refer to them directly
Array.from( this.importedComponents.keys() ).forEach( key => { Array.from(this.importedComponents.keys()).forEach(key => {
removeObjectKey( this.code, templateProperties.components.value, key ); removeObjectKey(
this.code,
templateProperties.components.value,
key
);
}); });
} else { } else {
// remove the entire components portion of the export // remove the entire components portion of the export
removeObjectKey( this.code, defaultExport.declaration, 'components' ); removeObjectKey(this.code, defaultExport.declaration, 'components');
} }
} }
// Remove these after version 2 // Remove these after version 2
if ( templateProperties.onrender ) { if (templateProperties.onrender) {
const { key } = templateProperties.onrender; const { key } = templateProperties.onrender;
this.code.overwrite( key.start, key.end, 'oncreate', { storeName: true, contentOnly: false } ); this.code.overwrite(key.start, key.end, 'oncreate', {
storeName: true,
contentOnly: false
});
templateProperties.oncreate = templateProperties.onrender; templateProperties.oncreate = templateProperties.onrender;
} }
if ( templateProperties.onteardown ) { if (templateProperties.onteardown) {
const { key } = templateProperties.onteardown; const { key } = templateProperties.onteardown;
this.code.overwrite( key.start, key.end, 'ondestroy', { storeName: true, contentOnly: false } ); this.code.overwrite(key.start, key.end, 'ondestroy', {
storeName: true,
contentOnly: false
});
templateProperties.ondestroy = templateProperties.onteardown; templateProperties.ondestroy = templateProperties.onteardown;
} }
// in an SSR context, we don't need to include events, methods, oncreate or ondestroy // in an SSR context, we don't need to include events, methods, oncreate or ondestroy
if ( ssr ) { if (ssr) {
if ( templateProperties.oncreate ) removeNode( this.code, defaultExport.declaration, templateProperties.oncreate ); if (templateProperties.oncreate)
if ( templateProperties.ondestroy ) removeNode( this.code, defaultExport.declaration, templateProperties.ondestroy ); removeNode(
if ( templateProperties.methods ) removeNode( this.code, defaultExport.declaration, templateProperties.methods ); this.code,
if ( templateProperties.events ) removeNode( this.code, defaultExport.declaration, templateProperties.events ); defaultExport.declaration,
templateProperties.oncreate
);
if (templateProperties.ondestroy)
removeNode(
this.code,
defaultExport.declaration,
templateProperties.ondestroy
);
if (templateProperties.methods)
removeNode(
this.code,
defaultExport.declaration,
templateProperties.methods
);
if (templateProperties.events)
removeNode(
this.code,
defaultExport.declaration,
templateProperties.events
);
} }
// now that we've analysed the default export, we can determine whether or not we need to keep it // now that we've analysed the default export, we can determine whether or not we need to keep it
let hasDefaultExport = !!defaultExport; let hasDefaultExport = !!defaultExport;
if ( defaultExport && defaultExport.declaration.properties.length === 0 ) { if (defaultExport && defaultExport.declaration.properties.length === 0) {
hasDefaultExport = false; hasDefaultExport = false;
removeNode( this.code, js.content, defaultExport ); removeNode(this.code, js.content, defaultExport);
} }
// if we do need to keep it, then we need to generate a return statement // if we do need to keep it, then we need to generate a return statement
if ( hasDefaultExport ) { if (hasDefaultExport) {
const finalNode = body[ body.length - 1 ]; const finalNode = body[body.length - 1];
if ( defaultExport === finalNode ) { if (defaultExport === finalNode) {
// export is last property, we can just return it // export is last property, we can just return it
this.code.overwrite( defaultExport.start, defaultExport.declaration.start, `return ` ); this.code.overwrite(
defaultExport.start,
defaultExport.declaration.start,
`return `
);
} else { } else {
const { declarations } = annotateWithScopes( js ); const { declarations } = annotateWithScopes(js);
let template = 'template'; let template = 'template';
for ( let i = 1; declarations.has( template ); template = `template_${i++}` ); for (
let i = 1;
this.code.overwrite( defaultExport.start, defaultExport.declaration.start, `var ${template} = ` ); declarations.has(template);
template = `template_${i++}`
);
this.code.overwrite(
defaultExport.start,
defaultExport.declaration.start,
`var ${template} = `
);
let i = defaultExport.start; let i = defaultExport.start;
while ( /\s/.test( source[ i - 1 ] ) ) i--; while (/\s/.test(source[i - 1])) i--;
const indentation = source.slice( i, defaultExport.start ); const indentation = source.slice(i, defaultExport.start);
this.code.appendLeft( finalNode.end, `\n\n${indentation}return ${template};` ); this.code.appendLeft(
finalNode.end,
`\n\n${indentation}return ${template};`
);
} }
} }
// user code gets wrapped in an IIFE // user code gets wrapped in an IIFE
if ( js.content.body.length ) { if (js.content.body.length) {
const prefix = hasDefaultExport ? `var ${this.alias( 'template' )} = (function () {` : `(function () {`; const prefix = hasDefaultExport
this.code.prependRight( js.content.start, prefix ).appendLeft( js.content.end, '}());' ); ? `var ${this.alias('template')} = (function () {`
} : `(function () {`;
this.code
// if there's no need to include user code, remove it altogether .prependRight(js.content.start, prefix)
else { .appendLeft(js.content.end, '}());');
this.code.remove( js.content.start, js.content.end ); } else {
// if there's no need to include user code, remove it altogether
this.code.remove(js.content.start, js.content.end);
hasJs = false; hasJs = false;
} }
} }

@ -48,7 +48,7 @@ export default class Block {
unmount: CodeBuilder; unmount: CodeBuilder;
detachRaw: CodeBuilder; detachRaw: CodeBuilder;
destroy: CodeBuilder; destroy: CodeBuilder;
} };
hasIntroMethod: boolean; hasIntroMethod: boolean;
hasOutroMethod: boolean; hasOutroMethod: boolean;
@ -64,7 +64,7 @@ export default class Block {
hasUpdateMethod: boolean; hasUpdateMethod: boolean;
autofocus: string; autofocus: string;
constructor ( options: BlockOptions ) { constructor(options: BlockOptions) {
this.generator = options.generator; this.generator = options.generator;
this.name = options.name; this.name = options.name;
this.expression = options.expression; this.expression = options.expression;
@ -102,144 +102,169 @@ export default class Block {
this.aliases = new Map(); this.aliases = new Map();
this.variables = new Map(); this.variables = new Map();
this.getUniqueName = this.generator.getUniqueNameMaker( options.params ); this.getUniqueName = this.generator.getUniqueNameMaker(options.params);
// unique names // unique names
this.component = this.getUniqueName( 'component' ); this.component = this.getUniqueName('component');
this.target = this.getUniqueName( 'target' ); this.target = this.getUniqueName('target');
this.hasUpdateMethod = false; // determined later this.hasUpdateMethod = false; // determined later
} }
addDependencies ( dependencies ) { addDependencies(dependencies) {
dependencies.forEach( dependency => { dependencies.forEach(dependency => {
this.dependencies.add( dependency ); this.dependencies.add(dependency);
}); });
} }
addElement ( name: string, renderStatement: string, parentNode: string, needsIdentifier = false ) { addElement(
name: string,
renderStatement: string,
parentNode: string,
needsIdentifier = false
) {
const isToplevel = !parentNode; const isToplevel = !parentNode;
if ( needsIdentifier || isToplevel ) { if (needsIdentifier || isToplevel) {
this.builders.create.addLine( this.builders.create.addLine(`var ${name} = ${renderStatement};`);
`var ${name} = ${renderStatement};`
);
this.mount( name, parentNode ); this.mount(name, parentNode);
} else { } else {
this.builders.create.addLine( `${this.generator.helper( 'appendNode' )}( ${renderStatement}, ${parentNode} );` ); this.builders.create.addLine(
`${this.generator.helper(
'appendNode'
)}( ${renderStatement}, ${parentNode} );`
);
} }
if ( isToplevel ) { if (isToplevel) {
this.builders.unmount.addLine( `${this.generator.helper( 'detachNode' )}( ${name} );` ); this.builders.unmount.addLine(
`${this.generator.helper('detachNode')}( ${name} );`
);
} }
} }
addVariable ( name: string, init?: string ) { addVariable(name: string, init?: string) {
if ( this.variables.has( name ) && this.variables.get( name ) !== init ) { if (this.variables.has(name) && this.variables.get(name) !== init) {
throw new Error( `Variable '${name}' already initialised with a different value` ); throw new Error(
`Variable '${name}' already initialised with a different value`
);
} }
this.variables.set( name, init ); this.variables.set(name, init);
} }
alias ( name: string ) { alias(name: string) {
if ( !this.aliases.has( name ) ) { if (!this.aliases.has(name)) {
this.aliases.set( name, this.getUniqueName( name ) ); this.aliases.set(name, this.getUniqueName(name));
} }
return this.aliases.get( name ); return this.aliases.get(name);
} }
child ( options: BlockOptions ) { child(options: BlockOptions) {
return new Block( Object.assign( {}, this, options, { parent: this } ) ); return new Block(Object.assign({}, this, options, { parent: this }));
} }
contextualise ( expression: Node, context?: string, isEventHandler?: boolean ) { contextualise(expression: Node, context?: string, isEventHandler?: boolean) {
return this.generator.contextualise( this, expression, context, isEventHandler ); return this.generator.contextualise(
this,
expression,
context,
isEventHandler
);
} }
findDependencies ( expression ) { findDependencies(expression) {
return this.generator.findDependencies( this.contextDependencies, this.indexes, expression ); return this.generator.findDependencies(
this.contextDependencies,
this.indexes,
expression
);
} }
mount ( name: string, parentNode: string ) { mount(name: string, parentNode: string) {
if ( parentNode ) { if (parentNode) {
this.builders.create.addLine( `${this.generator.helper( 'appendNode' )}( ${name}, ${parentNode} );` ); this.builders.create.addLine(
`${this.generator.helper('appendNode')}( ${name}, ${parentNode} );`
);
} else { } else {
this.builders.mount.addLine( `${this.generator.helper( 'insertNode' )}( ${name}, ${this.target}, anchor );` ); this.builders.mount.addLine(
`${this.generator.helper('insertNode')}( ${name}, ${this
.target}, anchor );`
);
} }
} }
render () { render() {
let introing; let introing;
const hasIntros = !this.builders.intro.isEmpty(); const hasIntros = !this.builders.intro.isEmpty();
if ( hasIntros ) { if (hasIntros) {
introing = this.getUniqueName( 'introing' ); introing = this.getUniqueName('introing');
this.addVariable( introing ); this.addVariable(introing);
} }
let outroing; let outroing;
const hasOutros = !this.builders.outro.isEmpty(); const hasOutros = !this.builders.outro.isEmpty();
if ( hasOutros ) { if (hasOutros) {
outroing = this.getUniqueName( 'outroing' ); outroing = this.getUniqueName('outroing');
this.addVariable( outroing ); this.addVariable(outroing);
} }
if ( this.variables.size ) { if (this.variables.size) {
const variables = Array.from( this.variables.keys() ) const variables = Array.from(this.variables.keys())
.map( key => { .map(key => {
const init = this.variables.get( key ); const init = this.variables.get(key);
return init !== undefined ? `${key} = ${init}` : key; return init !== undefined ? `${key} = ${init}` : key;
}) })
.join( ', ' ); .join(', ');
this.builders.create.addBlockAtStart( `var ${variables};` ); this.builders.create.addBlockAtStart(`var ${variables};`);
} }
if ( this.autofocus ) { if (this.autofocus) {
this.builders.create.addLine( `${this.autofocus}.focus();` ); this.builders.create.addLine(`${this.autofocus}.focus();`);
} }
// minor hack we need to ensure that any {{{triples}}} are detached first // minor hack we need to ensure that any {{{triples}}} are detached first
this.builders.unmount.addBlockAtStart( this.builders.detachRaw ); this.builders.unmount.addBlockAtStart(this.builders.detachRaw);
const properties = new CodeBuilder(); const properties = new CodeBuilder();
let localKey; let localKey;
if ( this.key ) { if (this.key) {
localKey = this.getUniqueName( 'key' ); localKey = this.getUniqueName('key');
properties.addBlock( `key: ${localKey},` ); properties.addBlock(`key: ${localKey},`);
} }
if ( this.first ) { if (this.first) {
properties.addBlock( `first: ${this.first},` ); properties.addBlock(`first: ${this.first},`);
} }
if ( this.builders.mount.isEmpty() ) { if (this.builders.mount.isEmpty()) {
properties.addBlock( `mount: ${this.generator.helper( 'noop' )},` ); properties.addBlock(`mount: ${this.generator.helper('noop')},`);
} else { } else {
properties.addBlock( deindent` properties.addBlock(deindent`
mount: function ( ${this.target}, anchor ) { mount: function ( ${this.target}, anchor ) {
${this.builders.mount} ${this.builders.mount}
}, },
` ); `);
} }
if ( this.hasUpdateMethod ) { if (this.hasUpdateMethod) {
if ( this.builders.update.isEmpty() ) { if (this.builders.update.isEmpty()) {
properties.addBlock( `update: ${this.generator.helper( 'noop' )},` ); properties.addBlock(`update: ${this.generator.helper('noop')},`);
} else { } else {
properties.addBlock( deindent` properties.addBlock(deindent`
update: function ( changed, ${this.params.join( ', ' )} ) { update: function ( changed, ${this.params.join(', ')} ) {
${this.builders.update} ${this.builders.update}
}, },
` ); `);
} }
} }
if ( this.hasIntroMethod ) { if (this.hasIntroMethod) {
if ( hasIntros ) { if (hasIntros) {
properties.addBlock( deindent` properties.addBlock(deindent`
intro: function ( ${this.target}, anchor ) { intro: function ( ${this.target}, anchor ) {
if ( ${introing} ) return; if ( ${introing} ) return;
${introing} = true; ${introing} = true;
@ -249,60 +274,63 @@ export default class Block {
this.mount( ${this.target}, anchor ); this.mount( ${this.target}, anchor );
}, },
` ); `);
} else { } else {
properties.addBlock( deindent` properties.addBlock(deindent`
intro: function ( ${this.target}, anchor ) { intro: function ( ${this.target}, anchor ) {
this.mount( ${this.target}, anchor ); this.mount( ${this.target}, anchor );
}, },
` ); `);
} }
} }
if ( this.hasOutroMethod ) { if (this.hasOutroMethod) {
if ( hasOutros ) { if (hasOutros) {
properties.addBlock( deindent` properties.addBlock(deindent`
outro: function ( ${this.alias( 'outrocallback' )} ) { outro: function ( ${this.alias('outrocallback')} ) {
if ( ${outroing} ) return; if ( ${outroing} ) return;
${outroing} = true; ${outroing} = true;
${hasIntros && `${introing} = false;`} ${hasIntros && `${introing} = false;`}
var ${this.alias( 'outros' )} = ${this.outros}; var ${this.alias('outros')} = ${this.outros};
${this.builders.outro} ${this.builders.outro}
}, },
` ); `);
} else { } else {
properties.addBlock( deindent` properties.addBlock(deindent`
outro: function ( outrocallback ) { outro: function ( outrocallback ) {
outrocallback(); outrocallback();
}, },
` ); `);
} }
} }
if ( this.builders.unmount.isEmpty() ) { if (this.builders.unmount.isEmpty()) {
properties.addBlock( `unmount: ${this.generator.helper('noop')},`); properties.addBlock(`unmount: ${this.generator.helper('noop')},`);
} else { } else {
properties.addBlock( deindent` properties.addBlock(deindent`
unmount: function () { unmount: function () {
${this.builders.unmount} ${this.builders.unmount}
}, },
` ); `);
} }
if ( this.builders.destroy.isEmpty() ) { if (this.builders.destroy.isEmpty()) {
properties.addBlock( `destroy: ${this.generator.helper( 'noop' )}` ); properties.addBlock(`destroy: ${this.generator.helper('noop')}`);
} else { } else {
properties.addBlock( deindent` properties.addBlock(deindent`
destroy: function () { destroy: function () {
${this.builders.destroy} ${this.builders.destroy}
} }
` ); `);
} }
return deindent` return deindent`
function ${this.name} ( ${this.params.join( ', ' )}, ${this.component}${this.key ? `, ${localKey}` : ''} ) { function ${this.name} ( ${this.params.join(', ')}, ${this.component}${this
.key
? `, ${localKey}`
: ''} ) {
${this.builders.create} ${this.builders.create}
return { return {
@ -311,4 +339,4 @@ export default class Block {
} }
`; `;
} }
} }

@ -13,7 +13,7 @@ import Block from './Block';
import { Parsed, CompileOptions, Node } from '../../interfaces'; import { Parsed, CompileOptions, Node } from '../../interfaces';
export class DomGenerator extends Generator { export class DomGenerator extends Generator {
blocks: Block[] blocks: Block[];
uses: Set<string>; uses: Set<string>;
readonly: Set<string>; readonly: Set<string>;
metaBindings: string[]; metaBindings: string[];
@ -22,8 +22,13 @@ export class DomGenerator extends Generator {
hasOutroTransitions: boolean; hasOutroTransitions: boolean;
hasComplexBindings: boolean; hasComplexBindings: boolean;
constructor ( parsed: Parsed, source: string, name: string, options: CompileOptions ) { constructor(
super( parsed, source, name, options ); parsed: Parsed,
source: string,
name: string,
options: CompileOptions
) {
super(parsed, source, name, options);
this.blocks = []; this.blocks = [];
this.uses = new Set(); this.uses = new Set();
@ -33,29 +38,38 @@ export class DomGenerator extends Generator {
this.metaBindings = []; this.metaBindings = [];
} }
helper ( name: string ) { helper(name: string) {
if ( this.options.dev && `${name}Dev` in shared ) { if (this.options.dev && `${name}Dev` in shared) {
name = `${name}Dev`; name = `${name}Dev`;
} }
this.uses.add( name ); this.uses.add(name);
return this.alias( name ); return this.alias(name);
} }
} }
export default function dom ( parsed: Parsed, source: string, options: CompileOptions ) { export default function dom(
parsed: Parsed,
source: string,
options: CompileOptions
) {
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, options ); const generator = new DomGenerator(parsed, source, name, options);
const { computations, hasJs, templateProperties, namespace } = generator.parseJs(); const {
computations,
hasJs,
templateProperties,
namespace
} = generator.parseJs();
const { block, state } = preprocess( generator, namespace, parsed.html ); const { block, state } = preprocess(generator, namespace, parsed.html);
parsed.html.children.forEach( ( node: Node ) => { parsed.html.children.forEach((node: Node) => {
visit( generator, block, state, node ); visit(generator, block, state, node);
}); });
const builders = { const builders = {
@ -63,94 +77,139 @@ export default function dom ( parsed: Parsed, source: string, options: CompileOp
_set: new CodeBuilder() _set: new CodeBuilder()
}; };
if ( computations.length ) { if (computations.length) {
const builder = new CodeBuilder(); const builder = new CodeBuilder();
const differs = generator.helper( 'differs' ); const differs = generator.helper('differs');
computations.forEach( ({ key, deps }) => { computations.forEach(({ key, deps }) => {
if ( generator.readonly.has( key ) ) { if (generator.readonly.has(key)) {
// <:Window> bindings // <:Window> bindings
throw new Error( `Cannot have a computed value '${key}' that clashes with a read-only property` ); throw new Error(
`Cannot have a computed value '${key}' that clashes with a read-only property`
);
} }
generator.readonly.add( key ); generator.readonly.add(key);
const condition = `isInitial || ${deps.map( dep => `( '${dep}' in newState && ${differs}( state.${dep}, oldState.${dep} ) )` ).join( ' || ' )}`; const condition = `isInitial || ${deps
const statement = `state.${key} = newState.${key} = ${generator.alias( 'template' )}.computed.${key}( ${deps.map( dep => `state.${dep}` ).join( ', ' )} );`; .map(
dep =>
`( '${dep}' in newState && ${differs}( state.${dep}, oldState.${dep} ) )`
)
.join(' || ')}`;
const statement = `state.${key} = newState.${key} = ${generator.alias(
'template'
)}.computed.${key}( ${deps.map(dep => `state.${dep}`).join(', ')} );`;
builder.addConditionalLine( condition, statement ); builder.addConditionalLine(condition, statement);
}); });
builders.main.addBlock( deindent` builders.main.addBlock(deindent`
function ${generator.alias( 'recompute' )} ( state, newState, oldState, isInitial ) { function ${generator.alias(
'recompute'
)} ( state, newState, oldState, isInitial ) {
${builder} ${builder}
} }
` ); `);
} }
builders._set.addBlock( deindent` builders._set.addBlock(deindent`
${options.dev && deindent` ${options.dev &&
deindent`
if ( typeof newState !== 'object' ) { if ( typeof newState !== 'object' ) {
throw new Error( 'Component .set was called without an object of data key-values to update.' ); throw new Error( 'Component .set was called without an object of data key-values to update.' );
} }
${Array.from( generator.readonly ).map( prop => ${Array.from(generator.readonly).map(
`if ( '${prop}' in newState && !this._updatingReadonlyProperty ) throw new Error( "Cannot set read-only property '${prop}'" );` prop =>
`if ( '${prop}' in newState && !this._updatingReadonlyProperty ) throw new Error( "Cannot set read-only property '${prop}'" );`
)} )}
`} `}
var oldState = this._state; var oldState = this._state;
this._state = ${generator.helper( 'assign' )}( {}, oldState, newState ); this._state = ${generator.helper('assign')}( {}, oldState, newState );
${computations.length && `${generator.alias( 'recompute' )}( this._state, newState, oldState, false )`} ${computations.length &&
${generator.helper( 'dispatchObservers' )}( this, this._observers.pre, newState, oldState ); `${generator.alias(
'recompute'
)}( this._state, newState, oldState, false )`}
${generator.helper(
'dispatchObservers'
)}( this, this._observers.pre, newState, oldState );
${block.hasUpdateMethod && `this._fragment.update( newState, this._state );`} ${block.hasUpdateMethod && `this._fragment.update( newState, this._state );`}
${generator.helper( 'dispatchObservers' )}( this, this._observers.post, newState, oldState ); ${generator.helper(
${generator.hasComplexBindings && `while ( this._bindings.length ) this._bindings.pop()();`} 'dispatchObservers'
${( generator.hasComponents || generator.hasIntroTransitions ) && `this._flush();`} )}( this, this._observers.post, newState, oldState );
` ); ${generator.hasComplexBindings &&
`while ( this._bindings.length ) this._bindings.pop()();`}
if ( hasJs ) { ${(generator.hasComponents || generator.hasIntroTransitions) &&
builders.main.addBlock( `[✂${parsed.js.content.start}-${parsed.js.content.end}✂]` ); `this._flush();`}
`);
if (hasJs) {
builders.main.addBlock(
`[✂${parsed.js.content.start}-${parsed.js.content.end}✂]`
);
} }
if ( generator.css && options.css !== false ) { if (generator.css && options.css !== false) {
builders.main.addBlock( deindent` builders.main.addBlock(deindent`
function ${generator.alias( 'add_css' )} () { function ${generator.alias('add_css')} () {
var style = ${generator.helper( 'createElement' )}( 'style' ); var style = ${generator.helper('createElement')}( 'style' );
style.id = ${JSON.stringify( generator.cssId + '-style' )}; style.id = ${JSON.stringify(generator.cssId + '-style')};
style.textContent = ${JSON.stringify( generator.css )}; style.textContent = ${JSON.stringify(generator.css)};
${generator.helper( 'appendNode' )}( style, document.head ); ${generator.helper('appendNode')}( style, document.head );
} }
` ); `);
} }
generator.blocks.forEach( block => { generator.blocks.forEach(block => {
builders.main.addBlock( block.render() ); builders.main.addBlock(block.render());
}); });
const sharedPath = options.shared === true ? 'svelte/shared.js' : options.shared; const sharedPath = options.shared === true
? 'svelte/shared.js'
const prototypeBase = `${name}.prototype` + ( templateProperties.methods ? `, ${generator.alias( 'template' )}.methods` : '' ); : options.shared;
const proto = sharedPath ? `${generator.helper( 'proto' )} ` : deindent`
const prototypeBase =
`${name}.prototype` +
(templateProperties.methods
? `, ${generator.alias('template')}.methods`
: '');
const proto = sharedPath
? `${generator.helper('proto')} `
: deindent`
{ {
${ ${['get', 'fire', 'observe', 'on', 'set', '_flush']
[ 'get', 'fire', 'observe', 'on', 'set', '_flush' ] .map(n => `${n}: ${generator.helper(n)}`)
.map( n => `${n}: ${generator.helper( n )}` ) .join(',\n')}
.join( ',\n' )
}
}`; }`;
// TODO deprecate component.teardown() // TODO deprecate component.teardown()
builders.main.addBlock( deindent` builders.main.addBlock(deindent`
function ${name} ( options ) { function ${name} ( options ) {
options = options || {}; options = options || {};
${options.dev && `if ( !options.target && !options._root ) throw new Error( "'target' is a required option" );`} ${options.dev &&
`if ( !options.target && !options._root ) throw new Error( "'target' is a required option" );`}
${generator.usesRefs && `this.refs = {};`} ${generator.usesRefs && `this.refs = {};`}
this._state = ${templateProperties.data ? `${generator.helper( 'assign' )}( ${generator.alias( 'template' )}.data(), options.data )` : `options.data || {}`}; this._state = ${templateProperties.data
? `${generator.helper('assign')}( ${generator.alias(
'template'
)}.data(), options.data )`
: `options.data || {}`};
${generator.metaBindings} ${generator.metaBindings}
${computations.length && `${generator.alias( 'recompute' )}( this._state, this._state, {}, true );`} ${computations.length &&
${options.dev && Array.from( generator.expectedProperties ).map( prop => `if ( !( '${prop}' in this._state ) ) console.warn( "Component was created without expected data property '${prop}'" );`)} `${generator.alias(
${generator.bindingGroups.length && `this._bindingGroups = [ ${Array( generator.bindingGroups.length ).fill( '[]' ).join( ', ' )} ];`} 'recompute'
)}( this._state, this._state, {}, true );`}
${options.dev &&
Array.from(generator.expectedProperties).map(
prop =>
`if ( !( '${prop}' in this._state ) ) console.warn( "Component was created without expected data property '${prop}'" );`
)}
${generator.bindingGroups.length &&
`this._bindingGroups = [ ${Array(generator.bindingGroups.length)
.fill('[]')
.join(', ')} ];`}
this._observers = { this._observers = {
pre: Object.create( null ), pre: Object.create( null ),
@ -163,25 +222,37 @@ export default function dom ( parsed: Parsed, source: string, options: CompileOp
this._yield = options._yield; this._yield = options._yield;
this._torndown = false; this._torndown = false;
${parsed.css && options.css !== false && `if ( !document.getElementById( ${JSON.stringify( generator.cssId + '-style' )} ) ) ${generator.alias( 'add_css' )}();`} ${parsed.css &&
${( generator.hasComponents || generator.hasIntroTransitions ) && `this._renderHooks = [];`} options.css !== false &&
`if ( !document.getElementById( ${JSON.stringify(
generator.cssId + '-style'
)} ) ) ${generator.alias('add_css')}();`}
${(generator.hasComponents || generator.hasIntroTransitions) &&
`this._renderHooks = [];`}
${generator.hasComplexBindings && `this._bindings = [];`} ${generator.hasComplexBindings && `this._bindings = [];`}
this._fragment = ${generator.alias( 'create_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 );
${generator.hasComplexBindings && `while ( this._bindings.length ) this._bindings.pop()();`} ${generator.hasComplexBindings &&
${( generator.hasComponents || generator.hasIntroTransitions ) && `this._flush();`} `while ( this._bindings.length ) this._bindings.pop()();`}
${(generator.hasComponents || generator.hasIntroTransitions) &&
`this._flush();`}
${templateProperties.oncreate && deindent` ${templateProperties.oncreate &&
deindent`
if ( options._root ) { if ( options._root ) {
options._root._renderHooks.push( ${generator.alias( 'template' )}.oncreate.bind( this ) ); options._root._renderHooks.push( ${generator.alias(
'template'
)}.oncreate.bind( this ) );
} else { } else {
${generator.alias( 'template' )}.oncreate.call( this ); ${generator.alias('template')}.oncreate.call( this );
} }
`} `}
} }
${generator.helper( 'assign' )}( ${prototypeBase}, ${proto}); ${generator.helper('assign')}( ${prototypeBase}, ${proto});
${name}.prototype._set = function _set ( newState ) { ${name}.prototype._set = function _set ( newState ) {
${builders._set} ${builders._set}
@ -189,7 +260,8 @@ export default function dom ( parsed: Parsed, source: string, options: CompileOp
${name}.prototype.teardown = ${name}.prototype.destroy = function destroy ( detach ) { ${name}.prototype.teardown = ${name}.prototype.destroy = function destroy ( detach ) {
this.fire( 'destroy' ); this.fire( 'destroy' );
${templateProperties.ondestroy && `${generator.alias( 'template' )}.ondestroy.call( this );`} ${templateProperties.ondestroy &&
`${generator.alias('template')}.ondestroy.call( this );`}
if ( detach !== false ) this._fragment.unmount(); if ( detach !== false ) this._fragment.unmount();
this._fragment.destroy(); this._fragment.destroy();
@ -198,63 +270,79 @@ export default function dom ( parsed: Parsed, source: string, options: CompileOp
this._state = {}; this._state = {};
this._torndown = true; this._torndown = true;
}; };
` ); `);
if ( sharedPath ) { if (sharedPath) {
if ( format !== 'es' ) { if (format !== 'es') {
throw new Error( `Components with shared helpers must be compiled to ES2015 modules (format: 'es')` ); throw new Error(
`Components with shared helpers must be compiled to ES2015 modules (format: 'es')`
);
} }
const names = Array.from( generator.uses ).sort().map( name => { const names = Array.from(generator.uses).sort().map(name => {
return name !== generator.alias( name ) ? `${name} as ${generator.alias( name )}` : name; return name !== generator.alias(name)
? `${name} as ${generator.alias(name)}`
: name;
}); });
builders.main.addLineAtStart( builders.main.addLineAtStart(
`import { ${names.join( ', ' )} } from ${JSON.stringify( sharedPath )};` `import { ${names.join(', ')} } from ${JSON.stringify(sharedPath)};`
); );
} else { } else {
generator.uses.forEach( key => { generator.uses.forEach(key => {
const str = shared[ key ]; const str = shared[key];
const code = new MagicString( str ); const code = new MagicString(str);
const expression = parseExpressionAt( str, 0 ); const expression = parseExpressionAt(str, 0);
let scope = annotateWithScopes( expression ); let scope = annotateWithScopes(expression);
walk( expression, { walk(expression, {
enter ( node, parent ) { enter(node, parent) {
if ( node._scope ) scope = node._scope; if (node._scope) scope = node._scope;
if ( node.type === 'Identifier' && isReference( node, parent ) && !scope.has( node.name ) ) { if (
if ( node.name in shared ) { node.type === 'Identifier' &&
isReference(node, parent) &&
!scope.has(node.name)
) {
if (node.name in shared) {
// this helper function depends on another one // this helper function depends on another one
const dependency = node.name; const dependency = node.name;
generator.uses.add( dependency ); generator.uses.add(dependency);
const alias = generator.alias( dependency ); const alias = generator.alias(dependency);
if ( alias !== node.name ) code.overwrite( node.start, node.end, alias ); if (alias !== node.name)
code.overwrite(node.start, node.end, alias);
} }
} }
}, },
leave ( node ) { leave(node) {
if ( node._scope ) scope = scope.parent; if (node._scope) scope = scope.parent;
} }
}); });
if ( key === 'transitionManager' ) { // special case if (key === 'transitionManager') {
// special case
const global = `_svelteTransitionManager`; const global = `_svelteTransitionManager`;
builders.main.addBlock( builders.main.addBlock(
`var ${generator.alias( 'transitionManager' )} = window.${global} || ( window.${global} = ${code});` `var ${generator.alias(
'transitionManager'
)} = window.${global} || ( window.${global} = ${code});`
); );
} else { } else {
const alias = generator.alias( expression.id.name ); const alias = generator.alias(expression.id.name);
if ( alias !== expression.id.name ) code.overwrite( expression.id.start, expression.id.end, alias ); if (alias !== expression.id.name)
code.overwrite(expression.id.start, expression.id.end, alias);
builders.main.addBlock( code.toString() ); builders.main.addBlock(code.toString());
} }
}); });
} }
return generator.generate( builders.main.toString(), options, { name, format } ); return generator.generate(builders.main.toString(), options, {
name,
format
});
} }

@ -2,7 +2,7 @@ export interface State {
name: string; name: string;
namespace: string; namespace: string;
parentNode: string; parentNode: string;
isTopLevel: boolean isTopLevel: boolean;
parentNodeName?: string; parentNodeName?: string;
basename?: string; basename?: string;
inEachBlock?: boolean; inEachBlock?: boolean;

@ -5,12 +5,14 @@ import { DomGenerator } from './index';
import { Node } from '../../interfaces'; import { Node } from '../../interfaces';
import { State } from './interfaces'; import { State } from './interfaces';
function isElseIf ( node: Node ) { function isElseIf(node: Node) {
return node && node.children.length === 1 && node.children[0].type === 'IfBlock'; return (
node && node.children.length === 1 && node.children[0].type === 'IfBlock'
);
} }
function getChildState ( parent: State, child = {} ) { function getChildState(parent: State, child = {}) {
return assign( {}, parent, { name: null, parentNode: null }, child || {} ); return assign({}, parent, { name: null, parentNode: null }, child || {});
} }
// Whitespace inside one of these elements will not result in // Whitespace inside one of these elements will not result in
@ -28,118 +30,144 @@ const elementsWithoutText = new Set([
]); ]);
const preprocessors = { const preprocessors = {
MustacheTag: ( generator: DomGenerator, block: Block, state: State, node: Node ) => { MustacheTag: (
const dependencies = block.findDependencies( node.expression ); generator: DomGenerator,
block.addDependencies( dependencies ); block: Block,
state: State,
node._state = getChildState( state, { node: Node
name: block.getUniqueName( 'text' ) ) => {
const dependencies = block.findDependencies(node.expression);
block.addDependencies(dependencies);
node._state = getChildState(state, {
name: block.getUniqueName('text')
}); });
}, },
RawMustacheTag: ( generator: DomGenerator, block: Block, state: State, node: Node ) => { RawMustacheTag: (
const dependencies = block.findDependencies( node.expression ); generator: DomGenerator,
block.addDependencies( dependencies ); block: Block,
state: State,
node: Node
) => {
const dependencies = block.findDependencies(node.expression);
block.addDependencies(dependencies);
const basename = block.getUniqueName( 'raw' ); const basename = block.getUniqueName('raw');
const name = block.getUniqueName( `${basename}_before` ); const name = block.getUniqueName(`${basename}_before`);
node._state = getChildState( state, { basename, name }); node._state = getChildState(state, { basename, name });
}, },
Text: ( generator: DomGenerator, block: Block, state: State, node: Node ) => { Text: (generator: DomGenerator, block: Block, state: State, node: Node) => {
node._state = getChildState( state ); node._state = getChildState(state);
if ( !/\S/.test( node.data ) ) { if (!/\S/.test(node.data)) {
if ( state.namespace ) return; if (state.namespace) return;
if ( elementsWithoutText.has( state.parentNodeName ) ) return; if (elementsWithoutText.has(state.parentNodeName)) return;
} }
node._state.shouldCreate = true; node._state.shouldCreate = true;
node._state.name = block.getUniqueName( `text` ); node._state.name = block.getUniqueName(`text`);
}, },
IfBlock: ( generator: DomGenerator, block: Block, state: State, node: Node ) => { IfBlock: (
generator: DomGenerator,
block: Block,
state: State,
node: Node
) => {
const blocks: Block[] = []; const blocks: Block[] = [];
let dynamic = false; let dynamic = false;
let hasIntros = false; let hasIntros = false;
let hasOutros = false; let hasOutros = false;
function attachBlocks ( node: Node ) { function attachBlocks(node: Node) {
const dependencies = block.findDependencies( node.expression ); const dependencies = block.findDependencies(node.expression);
block.addDependencies( dependencies ); block.addDependencies(dependencies);
node._block = block.child({ node._block = block.child({
name: generator.getUniqueName( `create_if_block` ) name: generator.getUniqueName(`create_if_block`)
}); });
node._state = getChildState( state ); node._state = getChildState(state);
blocks.push( node._block ); blocks.push(node._block);
preprocessChildren( generator, node._block, node._state, node ); preprocessChildren(generator, node._block, node._state, node);
if ( node._block.dependencies.size > 0 ) { if (node._block.dependencies.size > 0) {
dynamic = true; dynamic = true;
block.addDependencies( node._block.dependencies ); block.addDependencies(node._block.dependencies);
} }
if ( node._block.hasIntroMethod ) hasIntros = true; if (node._block.hasIntroMethod) hasIntros = true;
if ( node._block.hasOutroMethod ) hasOutros = true; if (node._block.hasOutroMethod) hasOutros = true;
if ( isElseIf( node.else ) ) { if (isElseIf(node.else)) {
attachBlocks( node.else.children[0] ); attachBlocks(node.else.children[0]);
} else if ( node.else ) { } else if (node.else) {
node.else._block = block.child({ node.else._block = block.child({
name: generator.getUniqueName( `create_if_block` ) name: generator.getUniqueName(`create_if_block`)
}); });
node.else._state = getChildState( state ); node.else._state = getChildState(state);
blocks.push( node.else._block ); blocks.push(node.else._block);
preprocessChildren( generator, node.else._block, node.else._state, node.else ); preprocessChildren(
generator,
node.else._block,
node.else._state,
node.else
);
if ( node.else._block.dependencies.size > 0 ) { if (node.else._block.dependencies.size > 0) {
dynamic = true; dynamic = true;
block.addDependencies( node.else._block.dependencies ); block.addDependencies(node.else._block.dependencies);
} }
} }
} }
attachBlocks( node ); attachBlocks(node);
blocks.forEach( block => { blocks.forEach(block => {
block.hasUpdateMethod = dynamic; block.hasUpdateMethod = dynamic;
block.hasIntroMethod = hasIntros; block.hasIntroMethod = hasIntros;
block.hasOutroMethod = hasOutros; block.hasOutroMethod = hasOutros;
}); });
generator.blocks.push( ...blocks ); generator.blocks.push(...blocks);
}, },
EachBlock: ( generator: DomGenerator, block: Block, state: State, node: Node ) => { EachBlock: (
const dependencies = block.findDependencies( node.expression ); generator: DomGenerator,
block.addDependencies( dependencies ); block: Block,
state: State,
node: Node
) => {
const dependencies = block.findDependencies(node.expression);
block.addDependencies(dependencies);
const indexNames = new Map( block.indexNames ); const indexNames = new Map(block.indexNames);
const indexName = node.index || block.getUniqueName( `${node.context}_index` ); const indexName =
indexNames.set( node.context, indexName ); node.index || block.getUniqueName(`${node.context}_index`);
indexNames.set(node.context, indexName);
const listNames = new Map( block.listNames ); const listNames = new Map(block.listNames);
const listName = block.getUniqueName( `each_block_value` ); const listName = block.getUniqueName(`each_block_value`);
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);
node._block = block.child({ node._block = block.child({
name: generator.getUniqueName( 'create_each_block' ), name: generator.getUniqueName('create_each_block'),
expression: node.expression, expression: node.expression,
context: node.context, context: node.context,
key: node.key, key: node.key,
@ -153,134 +181,152 @@ const preprocessors = {
indexNames, indexNames,
listNames, listNames,
params: block.params.concat( listName, context, indexName ) params: block.params.concat(listName, context, indexName)
}); });
node._state = getChildState( state, { node._state = getChildState(state, {
inEachBlock: true inEachBlock: true
}); });
generator.blocks.push( node._block ); generator.blocks.push(node._block);
preprocessChildren( generator, node._block, node._state, node ); preprocessChildren(generator, node._block, node._state, node);
block.addDependencies( node._block.dependencies ); block.addDependencies(node._block.dependencies);
node._block.hasUpdateMethod = node._block.dependencies.size > 0; node._block.hasUpdateMethod = node._block.dependencies.size > 0;
if ( node.else ) { if (node.else) {
node.else._block = block.child({ node.else._block = block.child({
name: generator.getUniqueName( `${node._block.name}_else` ) name: generator.getUniqueName(`${node._block.name}_else`)
}); });
node.else._state = getChildState( state ); node.else._state = getChildState(state);
generator.blocks.push( node.else._block ); generator.blocks.push(node.else._block);
preprocessChildren( generator, node.else._block, node.else._state, node.else ); preprocessChildren(
generator,
node.else._block,
node.else._state,
node.else
);
node.else._block.hasUpdateMethod = node.else._block.dependencies.size > 0; node.else._block.hasUpdateMethod = node.else._block.dependencies.size > 0;
} }
}, },
Element: ( generator: DomGenerator, block: Block, state: State, node: Node ) => { Element: (
const isComponent = generator.components.has( node.name ) || node.name === ':Self'; generator: DomGenerator,
block: Block,
if ( isComponent ) { state: State,
node._state = getChildState( state ); node: Node
) => {
const isComponent =
generator.components.has(node.name) || node.name === ':Self';
if (isComponent) {
node._state = getChildState(state);
} else { } else {
const name = block.getUniqueName( node.name.replace( /[^a-zA-Z0-9_$]/g, '_' ) ); const name = block.getUniqueName(
node.name.replace(/[^a-zA-Z0-9_$]/g, '_')
);
node._state = getChildState( state, { node._state = getChildState(state, {
isTopLevel: false, isTopLevel: false,
name, name,
parentNode: name, parentNode: name,
parentNodeName: node.name, parentNodeName: node.name,
namespace: node.name === 'svg' ? 'http://www.w3.org/2000/svg' : state.namespace, namespace: node.name === 'svg'
? 'http://www.w3.org/2000/svg'
: state.namespace,
allUsedContexts: [] allUsedContexts: []
}); });
} }
node.attributes.forEach( ( attribute: Node ) => { node.attributes.forEach((attribute: Node) => {
if ( attribute.type === 'Attribute' && attribute.value !== true ) { if (attribute.type === 'Attribute' && attribute.value !== true) {
attribute.value.forEach( ( chunk: Node ) => { attribute.value.forEach((chunk: Node) => {
if ( chunk.type !== 'Text' ) { if (chunk.type !== 'Text') {
const dependencies = block.findDependencies( chunk.expression ); const dependencies = block.findDependencies(chunk.expression);
block.addDependencies( dependencies ); block.addDependencies(dependencies);
} }
}); });
} } else if (attribute.type === 'Binding') {
const dependencies = block.findDependencies(attribute.value);
else if ( attribute.type === 'Binding' ) { block.addDependencies(dependencies);
const dependencies = block.findDependencies( attribute.value ); } else if (attribute.type === 'Transition') {
block.addDependencies( dependencies ); if (attribute.intro)
} generator.hasIntroTransitions = block.hasIntroMethod = true;
if (attribute.outro) {
else if ( attribute.type === 'Transition' ) {
if ( attribute.intro ) generator.hasIntroTransitions = block.hasIntroMethod = true;
if ( attribute.outro ) {
generator.hasOutroTransitions = block.hasOutroMethod = true; generator.hasOutroTransitions = block.hasOutroMethod = true;
block.outros += 1; block.outros += 1;
} }
} }
}); });
if ( node.children.length ) { if (node.children.length) {
if ( isComponent ) { if (isComponent) {
const name = block.getUniqueName( ( node.name === ':Self' ? generator.name : node.name ).toLowerCase() ); const name = block.getUniqueName(
(node.name === ':Self' ? generator.name : node.name).toLowerCase()
);
node._block = block.child({ node._block = block.child({
name: generator.getUniqueName( `create_${name}_yield_fragment` ) name: generator.getUniqueName(`create_${name}_yield_fragment`)
}); });
generator.blocks.push( node._block ); generator.blocks.push(node._block);
preprocessChildren( generator, node._block, node._state, node ); preprocessChildren(generator, node._block, node._state, node);
block.addDependencies( node._block.dependencies ); block.addDependencies(node._block.dependencies);
node._block.hasUpdateMethod = node._block.dependencies.size > 0; node._block.hasUpdateMethod = node._block.dependencies.size > 0;
} } else {
preprocessChildren(generator, block, node._state, node);
else {
preprocessChildren( generator, block, node._state, node );
} }
} }
} }
}; };
function preprocessChildren ( generator: DomGenerator, block: Block, state: State, node: Node, isTopLevel: boolean = false ) { function preprocessChildren(
generator: DomGenerator,
block: Block,
state: State,
node: Node,
isTopLevel: boolean = false
) {
// glue text nodes together // glue text nodes together
const cleaned: Node[] = []; const cleaned: Node[] = [];
let lastChild: Node; let lastChild: Node;
node.children.forEach( ( child: Node ) => { node.children.forEach((child: Node) => {
if ( child.type === 'Comment' ) return; if (child.type === 'Comment') return;
if ( child.type === 'Text' && lastChild && lastChild.type === 'Text' ) { if (child.type === 'Text' && lastChild && lastChild.type === 'Text') {
lastChild.data += child.data; lastChild.data += child.data;
lastChild.end = child.end; lastChild.end = child.end;
} else { } else {
cleaned.push( child ); cleaned.push(child);
} }
lastChild = child; lastChild = child;
}); });
if ( isTopLevel ) { if (isTopLevel) {
// trim leading and trailing whitespace from the top level // trim leading and trailing whitespace from the top level
const firstChild = cleaned[0]; const firstChild = cleaned[0];
if ( firstChild && firstChild.type === 'Text' ) { if (firstChild && firstChild.type === 'Text') {
firstChild.data = trimStart( firstChild.data ); firstChild.data = trimStart(firstChild.data);
if ( !firstChild.data ) cleaned.shift(); if (!firstChild.data) cleaned.shift();
} }
const lastChild = cleaned[ cleaned.length - 1 ]; const lastChild = cleaned[cleaned.length - 1];
if ( lastChild && lastChild.type === 'Text' ) { if (lastChild && lastChild.type === 'Text') {
lastChild.data = trimEnd( lastChild.data ); lastChild.data = trimEnd(lastChild.data);
if ( !lastChild.data ) cleaned.pop(); if (!lastChild.data) cleaned.pop();
} }
} }
lastChild = null; lastChild = null;
cleaned.forEach( ( child: Node ) => { cleaned.forEach((child: Node) => {
const preprocess = preprocessors[ child.type ]; const preprocess = preprocessors[child.type];
if ( preprocess ) preprocess( generator, block, state, child ); if (preprocess) preprocess(generator, block, state, child);
if ( lastChild ) { if (lastChild) {
lastChild.next = child; lastChild.next = child;
lastChild.needsAnchor = !child._state || !child._state.name; lastChild.needsAnchor = !child._state || !child._state.name;
} }
@ -288,24 +334,28 @@ function preprocessChildren ( generator: DomGenerator, block: Block, state: Stat
lastChild = child; lastChild = child;
}); });
if ( lastChild ) { if (lastChild) {
lastChild.needsAnchor = !state.parentNode; lastChild.needsAnchor = !state.parentNode;
} }
node.children = cleaned; node.children = cleaned;
} }
export default function preprocess ( generator: DomGenerator, namespace: string, node: Node ) { export default function preprocess(
generator: DomGenerator,
namespace: string,
node: Node
) {
const block = new Block({ const block = new Block({
generator, generator,
name: generator.alias( 'create_main_fragment' ), name: generator.alias('create_main_fragment'),
key: null, key: null,
contexts: new Map(), contexts: new Map(),
indexes: new Map(), indexes: new Map(),
contextDependencies: new Map(), contextDependencies: new Map(),
params: [ 'state' ], params: ['state'],
indexNames: new Map(), indexNames: new Map(),
listNames: new Map(), listNames: new Map(),
@ -318,9 +368,9 @@ export default function preprocess ( generator: DomGenerator, namespace: string,
isTopLevel: true isTopLevel: true
}; };
generator.blocks.push( block ); generator.blocks.push(block);
preprocessChildren( generator, block, state, node, true ); preprocessChildren(generator, block, state, node, true);
block.hasUpdateMethod = block.dependencies.size > 0; block.hasUpdateMethod = block.dependencies.size > 0;
return { block, state }; return { block, state };
} }

@ -3,7 +3,12 @@ import { DomGenerator } from './index';
import Block from './Block'; import Block from './Block';
import { Node } from '../../interfaces'; import { Node } from '../../interfaces';
export default function visit ( generator: DomGenerator, block: Block, state, node: Node ) { export default function visit(
const visitor = visitors[ node.type ]; generator: DomGenerator,
visitor( generator, block, state, node ); block: Block,
} state,
node: Node
) {
const visitor = visitors[node.type];
visitor(generator, block, state, node);
}

@ -3,37 +3,40 @@ import Block from '../../Block';
import { Node } from '../../../../interfaces'; import { Node } from '../../../../interfaces';
import { State } from '../../interfaces'; import { State } from '../../interfaces';
export default function visitAttribute ( generator: DomGenerator, block: Block, state: State, node: Node, attribute, local ) { export default function visitAttribute(
if ( attribute.value === true ) { generator: DomGenerator,
block: Block,
state: State,
node: Node,
attribute,
local
) {
if (attribute.value === true) {
// attributes without values, e.g. <textarea readonly> // attributes without values, e.g. <textarea readonly>
local.staticAttributes.push({ local.staticAttributes.push({
name: attribute.name, name: attribute.name,
value: true value: true
}); });
} } else if (attribute.value.length === 0) {
else if ( attribute.value.length === 0 ) {
local.staticAttributes.push({ local.staticAttributes.push({
name: attribute.name, name: attribute.name,
value: `''` value: `''`
}); });
} } else if (attribute.value.length === 1) {
else if ( attribute.value.length === 1 ) {
const value = attribute.value[0]; const value = attribute.value[0];
if ( value.type === 'Text' ) { if (value.type === 'Text') {
// static attributes // static attributes
const result = isNaN( value.data ) ? JSON.stringify( value.data ) : value.data; const result = isNaN(value.data)
? JSON.stringify(value.data)
: value.data;
local.staticAttributes.push({ local.staticAttributes.push({
name: attribute.name, name: attribute.name,
value: result value: result
}); });
} } else {
else {
// simple dynamic attributes // simple dynamic attributes
const { dependencies, snippet } = block.contextualise( 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({
@ -42,26 +45,29 @@ export default function visitAttribute ( generator: DomGenerator, block: Block,
dependencies dependencies
}); });
} }
} } else {
else {
// complex dynamic attributes // complex dynamic attributes
const allDependencies = []; const allDependencies = [];
const value = ( attribute.value[0].type === 'Text' ? '' : `"" + ` ) + ( const value =
attribute.value.map( chunk => { (attribute.value[0].type === 'Text' ? '' : `"" + `) +
if ( chunk.type === 'Text' ) { attribute.value
return JSON.stringify( chunk.data ); .map(chunk => {
} else { if (chunk.type === 'Text') {
const { dependencies, snippet } = block.contextualise( chunk.expression ); return JSON.stringify(chunk.data);
dependencies.forEach( dependency => { } else {
if ( !~allDependencies.indexOf( dependency ) ) allDependencies.push( dependency ); const { dependencies, snippet } = block.contextualise(
}); chunk.expression
);
dependencies.forEach(dependency => {
if (!~allDependencies.indexOf(dependency))
allDependencies.push(dependency);
});
return `( ${snippet} )`; return `( ${snippet} )`;
} }
}).join( ' + ' ) })
); .join(' + ');
local.dynamicAttributes.push({ local.dynamicAttributes.push({
name: attribute.name, name: attribute.name,
@ -69,4 +75,4 @@ export default function visitAttribute ( generator: DomGenerator, block: Block,
dependencies: allDependencies dependencies: allDependencies
}); });
} }
} }

@ -6,26 +6,40 @@ import Block from '../../Block';
import { Node } from '../../../../interfaces'; import { Node } from '../../../../interfaces';
import { State } from '../../interfaces'; import { State } from '../../interfaces';
export default function visitBinding ( generator: DomGenerator, block: Block, state: State, node: Node, attribute, local ) { export default function visitBinding(
const { name } = flattenReference( attribute.value ); generator: DomGenerator,
const { snippet, contexts, dependencies } = block.contextualise( attribute.value ); block: Block,
state: State,
node: Node,
attribute,
local
) {
const { name } = flattenReference(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!'
);
contexts.forEach( context => { contexts.forEach(context => {
if ( !~local.allUsedContexts.indexOf( context ) ) local.allUsedContexts.push( context ); if (!~local.allUsedContexts.indexOf(context))
local.allUsedContexts.push(context);
}); });
const contextual = block.contexts.has( name ); const contextual = block.contexts.has(name);
let obj; let obj;
let prop; let prop;
if ( contextual ) { if (contextual) {
obj = block.listNames.get( name ); obj = block.listNames.get(name);
prop = block.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}✂]`;
} else { } else {
obj = 'state'; obj = 'state';
@ -39,14 +53,22 @@ export default function visitBinding ( generator: DomGenerator, block: Block, st
prop prop
}); });
const setter = getSetter({ block, name, snippet, context: '_context', attribute, dependencies, value: 'value' }); const setter = getSetter({
block,
name,
snippet,
context: '_context',
attribute,
dependencies,
value: 'value'
});
generator.hasComplexBindings = true; generator.hasComplexBindings = true;
const updating = block.getUniqueName( `${local.name}_updating` ); const updating = block.getUniqueName(`${local.name}_updating`);
block.addVariable( updating, 'false' ); block.addVariable(updating, 'false');
local.create.addBlock( deindent` local.create.addBlock(deindent`
${block.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 ) {
@ -54,15 +76,19 @@ export default function visitBinding ( generator: DomGenerator, block: Block, st
${updating} = true; ${updating} = true;
${setter} ${setter}
${updating} = false; ${updating} = false;
}, { init: ${generator.helper( 'differs' )}( ${local.name}.get( '${attribute.name}' ), ${snippet} ) }); }, { init: ${generator.helper(
'differs'
)}( ${local.name}.get( '${attribute.name}' ), ${snippet} ) });
}); });
` ); `);
local.update.addBlock( deindent` local.update.addBlock(deindent`
if ( !${updating} && ${dependencies.map( dependency => `'${dependency}' in changed` ).join( '||' )} ) { if ( !${updating} && ${dependencies
.map(dependency => `'${dependency}' in changed`)
.join('||')} ) {
${updating} = true; ${updating} = true;
${local.name}._set({ ${attribute.name}: ${snippet} }); ${local.name}._set({ ${attribute.name}: ${snippet} });
${updating} = false; ${updating} = false;
} }
` ); `);
} }

@ -10,13 +10,13 @@ import Block from '../../Block';
import { Node } from '../../../../interfaces'; import { Node } from '../../../../interfaces';
import { State } from '../../interfaces'; import { State } from '../../interfaces';
function stringifyProps ( props: string[] ) { function stringifyProps(props: string[]) {
if ( !props.length ) return '{}'; if (!props.length) return '{}';
const joined = props.join( ', ' ); const joined = props.join(', ');
if ( joined.length > 40 ) { if (joined.length > 40) {
// make larger data objects readable // make larger data objects readable
return `{\n\t${props.join( ',\n\t' )}\n}`; return `{\n\t${props.join(',\n\t')}\n}`;
} }
return `{ ${joined} }`; return `{ ${joined} }`;
@ -36,9 +36,16 @@ const visitors = {
Ref: visitRef Ref: visitRef
}; };
export default function visitComponent ( generator: DomGenerator, block: Block, state: State, node: Node ) { export default function visitComponent(
generator: DomGenerator,
block: Block,
state: State,
node: Node
) {
const hasChildren = node.children.length > 0; const hasChildren = node.children.length > 0;
const name = block.getUniqueName( ( node.name === ':Self' ? generator.name : node.name ).toLowerCase() ); const name = block.getUniqueName(
(node.name === ':Self' ? generator.name : node.name).toLowerCase()
);
const childState = node._state; const childState = node._state;
@ -61,115 +68,139 @@ export default function visitComponent ( generator: DomGenerator, block: Block,
generator.hasComponents = true; generator.hasComponents = true;
node.attributes node.attributes
.sort( ( a, b ) => order[ a.type ] - order[ b.type ] ) .sort((a, b) => order[a.type] - order[b.type])
.forEach( attribute => { .forEach(attribute => {
visitors[ attribute.type ]( generator, block, childState, node, attribute, local ); visitors[attribute.type](
generator,
block,
childState,
node,
attribute,
local
);
}); });
if ( local.allUsedContexts.length ) { if (local.allUsedContexts.length) {
const initialProps = local.allUsedContexts.map( contextName => { const initialProps = local.allUsedContexts
if ( contextName === 'state' ) return `state: state`; .map(contextName => {
if (contextName === 'state') return `state: state`;
const listName = block.listNames.get( contextName ); const listName = block.listNames.get(contextName);
const indexName = block.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
if ( contextName === 'state' ) return `${name}._context.state = state;`; .map(contextName => {
if (contextName === 'state') return `${name}._context.state = state;`;
const listName = block.listNames.get( contextName ); const listName = block.listNames.get(contextName);
const indexName = block.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.create.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 ? state.parentNode: 'null'}`, `target: ${!isTopLevel ? state.parentNode : 'null'}`,
`_root: ${block.component}._root` `_root: ${block.component}._root`
]; ];
// 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 params = block.params.join( ', ' ); const params = block.params.join(', ');
const childBlock = node._block; const childBlock = node._block;
node.children.forEach( child => { node.children.forEach(child => {
visit( generator, childBlock, childState, child ); visit(generator, childBlock, childState, child);
}); });
const yieldFragment = block.getUniqueName( `${name}_yield_fragment` ); const yieldFragment = block.getUniqueName(`${name}_yield_fragment`);
block.builders.create.addLine( block.builders.create.addLine(
`var ${yieldFragment} = ${childBlock.name}( ${params}, ${block.component} );` `var ${yieldFragment} = ${childBlock.name}( ${params}, ${block.component} );`
); );
if ( childBlock.hasUpdateMethod ) { if (childBlock.hasUpdateMethod) {
block.builders.update.addLine( block.builders.update.addLine(
`${yieldFragment}.update( changed, ${params} );` `${yieldFragment}.update( changed, ${params} );`
); );
} }
block.builders.destroy.addLine( block.builders.destroy.addLine(`${yieldFragment}.destroy();`);
`${yieldFragment}.destroy();`
);
componentInitProperties.push( `_yield: ${yieldFragment}`); componentInitProperties.push(`_yield: ${yieldFragment}`);
} }
const statements: string[] = []; const statements: string[] = [];
if ( local.staticAttributes.length || local.dynamicAttributes.length || local.bindings.length ) { if (
local.staticAttributes.length ||
local.dynamicAttributes.length ||
local.bindings.length
) {
const initialProps = local.staticAttributes const initialProps = local.staticAttributes
.concat( local.dynamicAttributes ) .concat(local.dynamicAttributes)
.map( attribute => `${attribute.name}: ${attribute.value}` ); .map(attribute => `${attribute.name}: ${attribute.value}`);
const initialPropString = stringifyProps( initialProps ); const initialPropString = stringifyProps(initialProps);
if ( local.bindings.length ) { if (local.bindings.length) {
const initialData = block.getUniqueName( `${name}_initial_data` ); const initialData = block.getUniqueName(`${name}_initial_data`);
statements.push( `var ${initialData} = ${initialPropString};` ); statements.push(`var ${initialData} = ${initialPropString};`);
local.bindings.forEach( binding => { local.bindings.forEach(binding => {
statements.push( `if ( ${binding.prop} in ${binding.obj} ) ${initialData}.${binding.name} = ${binding.value};` ); statements.push(
`if ( ${binding.prop} in ${binding.obj} ) ${initialData}.${binding.name} = ${binding.value};`
);
}); });
componentInitProperties.push( `data: ${initialData}` ); componentInitProperties.push(`data: ${initialData}`);
} else if ( initialProps.length ) { } else if (initialProps.length) {
componentInitProperties.push( `data: ${initialPropString}` ); componentInitProperties.push(`data: ${initialPropString}`);
} }
} }
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}`;
local.create.addBlockAtStart( deindent` local.create.addBlockAtStart(deindent`
${statements.join( '\n' )} ${statements.join('\n')}
var ${name} = new ${expression}({ var ${name} = new ${expression}({
${componentInitProperties.join(',\n')} ${componentInitProperties.join(',\n')}
}); });
` ); `);
if ( isTopLevel ) { if (isTopLevel) {
block.builders.mount.addLine( `${name}._fragment.mount( ${block.target}, anchor );` ); block.builders.mount.addLine(
`${name}._fragment.mount( ${block.target}, anchor );`
);
} }
if ( local.dynamicAttributes.length ) { if (local.dynamicAttributes.length) {
const updates = local.dynamicAttributes.map( attribute => { const updates = local.dynamicAttributes.map(attribute => {
if ( attribute.dependencies.length ) { if (attribute.dependencies.length) {
return deindent` return deindent`
if ( ${attribute.dependencies.map( dependency => `'${dependency}' in changed` ).join( '||' )} ) ${name}_changes.${attribute.name} = ${attribute.value}; if ( ${attribute.dependencies
.map(dependency => `'${dependency}' in changed`)
.join(
'||'
)} ) ${name}_changes.${attribute.name} = ${attribute.value};
`; `;
} }
@ -178,18 +209,19 @@ export default function visitComponent ( generator: DomGenerator, block: Block,
return `${name}_changes.${attribute.name} = ${attribute.value};`; return `${name}_changes.${attribute.name} = ${attribute.value};`;
}); });
local.update.addBlock( deindent` local.update.addBlock(deindent`
var ${name}_changes = {}; var ${name}_changes = {};
${updates.join( '\n' )} ${updates.join('\n')}
if ( Object.keys( ${name}_changes ).length ) ${name}.set( ${name}_changes ); if ( Object.keys( ${name}_changes ).length ) ${name}.set( ${name}_changes );
` ); `);
} }
if ( isTopLevel ) block.builders.unmount.addLine( `${name}._fragment.unmount();` ); if (isTopLevel)
block.builders.destroy.addLine( `${name}.destroy( false );` ); block.builders.unmount.addLine(`${name}._fragment.unmount();`);
block.builders.destroy.addLine(`${name}.destroy( false );`);
block.builders.create.addBlock( local.create ); block.builders.create.addBlock(local.create);
if ( !local.update.isEmpty() ) block.builders.update.addBlock( local.update ); if (!local.update.isEmpty()) block.builders.update.addBlock(local.update);
} }

@ -4,36 +4,49 @@ import Block from '../../Block';
import { Node } from '../../../../interfaces'; import { Node } from '../../../../interfaces';
import { State } from '../../interfaces'; import { State } from '../../interfaces';
export default function visitEventHandler ( generator: DomGenerator, block: Block, state: State, node: Node, attribute: Node, local ) { export default function visitEventHandler(
generator: DomGenerator,
block: Block,
state: State,
node: Node,
attribute: Node,
local
) {
// 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, `${block.component}.` ); generator.code.prependRight(
attribute.expression.start,
`${block.component}.`
);
const usedContexts: string[] = []; const usedContexts: string[] = [];
attribute.expression.arguments.forEach( ( arg: Node ) => { attribute.expression.arguments.forEach((arg: Node) => {
const { contexts } = block.contextualise( 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);
if ( !~local.allUsedContexts.indexOf( context ) ) local.allUsedContexts.push( context ); if (!~local.allUsedContexts.indexOf(context))
local.allUsedContexts.push(context);
}); });
}); });
// TODO hoist event handlers? can do `this.__component.method(...)` // TODO hoist event handlers? can do `this.__component.method(...)`
const declarations = usedContexts.map( name => { const declarations = usedContexts.map(name => {
if ( name === 'state' ) return 'var state = this._context.state;'; if (name === 'state') return 'var state = this._context.state;';
const listName = block.listNames.get( name ); const listName = block.listNames.get(name);
const indexName = block.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.create.addBlock( deindent` local.create.addBlock(deindent`
${local.name}.on( '${attribute.name}', function ( event ) { ${local.name}.on( '${attribute.name}', function ( event ) {
${handlerBody} ${handlerBody}
}); });
` ); `);
} }

@ -4,14 +4,21 @@ import Block from '../../Block';
import { Node } from '../../../../interfaces'; import { Node } from '../../../../interfaces';
import { State } from '../../interfaces'; import { State } from '../../interfaces';
export default function visitRef ( generator: DomGenerator, block: Block, state: State, node: Node, attribute: Node, local ) { export default function visitRef(
generator: DomGenerator,
block: Block,
state: State,
node: Node,
attribute: Node,
local
) {
generator.usesRefs = true; generator.usesRefs = true;
local.create.addLine( local.create.addLine(
`${block.component}.refs.${attribute.name} = ${local.name};` `${block.component}.refs.${attribute.name} = ${local.name};`
); );
block.builders.destroy.addLine( deindent` block.builders.destroy.addLine(deindent`
if ( ${block.component}.refs.${attribute.name} === ${local.name} ) ${block.component}.refs.${attribute.name} = null; if ( ${block.component}.refs.${attribute.name} === ${local.name} ) ${block.component}.refs.${attribute.name} = null;
` ); `);
} }

@ -5,72 +5,98 @@ import Block from '../Block';
import { Node } from '../../../interfaces'; import { Node } from '../../../interfaces';
import { State } from '../interfaces'; import { State } from '../interfaces';
export default function visitEachBlock ( generator: DomGenerator, block: Block, state: State, node: Node ) { export default function visitEachBlock(
const each_block = generator.getUniqueName( `each_block` ); generator: DomGenerator,
block: Block,
state: State,
node: Node
) {
const each_block = generator.getUniqueName(`each_block`);
const create_each_block = node._block.name; const create_each_block = node._block.name;
const each_block_value = node._block.listName; const each_block_value = node._block.listName;
const iterations = block.getUniqueName( `${each_block}_iterations` ); const iterations = block.getUniqueName(`${each_block}_iterations`);
const i = block.alias( `i` ); const i = block.alias(`i`);
const params = block.params.join( ', ' ); const params = block.params.join(', ');
const anchor = node.needsAnchor ? block.getUniqueName( `${each_block}_anchor` ) : ( node.next && node.next._state.name ) || 'null'; const anchor = node.needsAnchor
? block.getUniqueName(`${each_block}_anchor`)
: (node.next && node.next._state.name) || 'null';
const mountOrIntro = node._block.hasIntroMethod ? 'intro' : 'mount'; const mountOrIntro = node._block.hasIntroMethod ? 'intro' : 'mount';
const vars = { each_block, create_each_block, each_block_value, iterations, i, params, anchor, mountOrIntro }; const vars = {
each_block,
const { snippet } = block.contextualise( node.expression ); create_each_block,
each_block_value,
block.builders.create.addLine( `var ${each_block_value} = ${snippet};` ); iterations,
i,
if ( node.key ) { params,
keyed( generator, block, state, node, snippet, vars ); anchor,
mountOrIntro
};
const { snippet } = block.contextualise(node.expression);
block.builders.create.addLine(`var ${each_block_value} = ${snippet};`);
if (node.key) {
keyed(generator, block, state, node, snippet, vars);
} else { } else {
unkeyed( generator, block, state, node, snippet, vars ); unkeyed(generator, block, state, node, snippet, vars);
} }
const isToplevel = !state.parentNode; const isToplevel = !state.parentNode;
if ( node.needsAnchor ) { if (node.needsAnchor) {
block.addElement( anchor, `${generator.helper( 'createComment' )}()`, state.parentNode, true ); block.addElement(
} else if ( node.next ) { anchor,
`${generator.helper('createComment')}()`,
state.parentNode,
true
);
} else if (node.next) {
node.next.usedAsAnchor = true; node.next.usedAsAnchor = true;
} }
if ( node.else ) { if (node.else) {
const each_block_else = generator.getUniqueName( `${each_block}_else` ); const each_block_else = generator.getUniqueName(`${each_block}_else`);
block.builders.create.addLine( `var ${each_block_else} = null;` ); block.builders.create.addLine(`var ${each_block_else} = null;`);
// TODO neaten this up... will end up with an empty line in the block // TODO neaten this up... will end up with an empty line in the block
block.builders.create.addBlock( deindent` block.builders.create.addBlock(deindent`
if ( !${each_block_value}.length ) { if ( !${each_block_value}.length ) {
${each_block_else} = ${node.else._block.name}( ${params}, ${block.component} ); ${each_block_else} = ${node.else._block
${!isToplevel ? `${each_block_else}.${mountOrIntro}( ${state.parentNode}, null );` : ''} .name}( ${params}, ${block.component} );
${!isToplevel
? `${each_block_else}.${mountOrIntro}( ${state.parentNode}, null );`
: ''}
} }
` ); `);
block.builders.mount.addBlock( deindent` block.builders.mount.addBlock(deindent`
if ( ${each_block_else} ) { if ( ${each_block_else} ) {
${each_block_else}.${mountOrIntro}( ${state.parentNode || block.target}, null ); ${each_block_else}.${mountOrIntro}( ${state.parentNode ||
block.target}, null );
} }
` ); `);
const parentNode = state.parentNode || `${anchor}.parentNode`; const parentNode = state.parentNode || `${anchor}.parentNode`;
if ( node.else._block.hasUpdateMethod ) { if (node.else._block.hasUpdateMethod) {
block.builders.update.addBlock( deindent` block.builders.update.addBlock(deindent`
if ( !${each_block_value}.length && ${each_block_else} ) { if ( !${each_block_value}.length && ${each_block_else} ) {
${each_block_else}.update( changed, ${params} ); ${each_block_else}.update( changed, ${params} );
} else if ( !${each_block_value}.length ) { } else if ( !${each_block_value}.length ) {
${each_block_else} = ${node.else._block.name}( ${params}, ${block.component} ); ${each_block_else} = ${node.else._block
.name}( ${params}, ${block.component} );
${each_block_else}.${mountOrIntro}( ${parentNode}, ${anchor} ); ${each_block_else}.${mountOrIntro}( ${parentNode}, ${anchor} );
} else if ( ${each_block_else} ) { } else if ( ${each_block_else} ) {
${each_block_else}.unmount(); ${each_block_else}.unmount();
${each_block_else}.destroy(); ${each_block_else}.destroy();
${each_block_else} = null; ${each_block_else} = null;
} }
` ); `);
} else { } else {
block.builders.update.addBlock( deindent` block.builders.update.addBlock(deindent`
if ( ${each_block_value}.length ) { if ( ${each_block_value}.length ) {
if ( ${each_block_else} ) { if ( ${each_block_else} ) {
${each_block_else}.unmount(); ${each_block_else}.unmount();
@ -78,48 +104,70 @@ export default function visitEachBlock ( generator: DomGenerator, block: Block,
${each_block_else} = null; ${each_block_else} = null;
} }
} else if ( !${each_block_else} ) { } else if ( !${each_block_else} ) {
${each_block_else} = ${node.else._block.name}( ${params}, ${block.component} ); ${each_block_else} = ${node.else._block
.name}( ${params}, ${block.component} );
${each_block_else}.${mountOrIntro}( ${parentNode}, ${anchor} ); ${each_block_else}.${mountOrIntro}( ${parentNode}, ${anchor} );
} }
` ); `);
} }
block.builders.unmount.addLine( block.builders.unmount.addLine(
`if ( ${each_block_else} ) ${each_block_else}.unmount()` `if ( ${each_block_else} ) ${each_block_else}.unmount()`
); );
block.builders.destroy.addBlock( deindent` block.builders.destroy.addBlock(deindent`
if ( ${each_block_else} ) ${each_block_else}.destroy( false ); if ( ${each_block_else} ) ${each_block_else}.destroy( false );
` ); `);
} }
node.children.forEach( ( child: Node ) => { node.children.forEach((child: Node) => {
visit( generator, node._block, node._state, child ); visit(generator, node._block, node._state, child);
}); });
if ( node.else ) { if (node.else) {
node.else.children.forEach( ( child: Node ) => { node.else.children.forEach((child: Node) => {
visit( generator, node.else._block, node.else._state, child ); visit(generator, node.else._block, node.else._state, child);
}); });
} }
} }
function keyed ( generator: DomGenerator, block: Block, state: State, node: Node, snippet, { each_block, create_each_block, each_block_value, i, params, anchor, mountOrIntro } ) { function keyed(
const key = block.getUniqueName( 'key' ); generator: DomGenerator,
const lookup = block.getUniqueName( `${each_block}_lookup` ); block: Block,
const iteration = block.getUniqueName( `${each_block}_iteration` ); state: State,
const head = block.getUniqueName( `${each_block}_head` ); node: Node,
const last = block.getUniqueName( `${each_block}_last` ); snippet,
const expected = block.getUniqueName( `${each_block}_expected` ); {
each_block,
if ( node.children[0] && node.children[0].type === 'Element' ) { // TODO or text/tag/raw create_each_block,
each_block_value,
i,
params,
anchor,
mountOrIntro
}
) {
const key = block.getUniqueName('key');
const lookup = block.getUniqueName(`${each_block}_lookup`);
const iteration = block.getUniqueName(`${each_block}_iteration`);
const head = block.getUniqueName(`${each_block}_head`);
const last = block.getUniqueName(`${each_block}_last`);
const expected = block.getUniqueName(`${each_block}_expected`);
if (node.children[0] && node.children[0].type === 'Element') {
// TODO or text/tag/raw
node._block.first = node.children[0]._state.parentNode; // TODO this is highly confusing node._block.first = node.children[0]._state.parentNode; // TODO this is highly confusing
} else { } else {
node._block.first = node._block.getUniqueName( 'first' ); node._block.first = node._block.getUniqueName('first');
node._block.addElement( node._block.first, `${generator.helper( 'createComment' )}()`, null, true ); node._block.addElement(
node._block.first,
`${generator.helper('createComment')}()`,
null,
true
);
} }
block.builders.create.addBlock( deindent` block.builders.create.addBlock(deindent`
var ${lookup} = Object.create( null ); var ${lookup} = Object.create( null );
var ${head}; var ${head};
@ -128,7 +176,8 @@ function keyed ( generator: DomGenerator, block: Block, state: State, node: Node
for ( var ${i} = 0; ${i} < ${each_block_value}.length; ${i} += 1 ) { for ( var ${i} = 0; ${i} < ${each_block_value}.length; ${i} += 1 ) {
var ${key} = ${each_block_value}[${i}].${node.key}; var ${key} = ${each_block_value}[${i}].${node.key};
var ${iteration} = ${lookup}[${key}] = ${create_each_block}( ${params}, ${each_block_value}, ${each_block_value}[${i}], ${i}, ${block.component}, ${key} ); var ${iteration} = ${lookup}[${key}] = ${create_each_block}( ${params}, ${each_block_value}, ${each_block_value}[${i}], ${i}, ${block.component}, ${key} );
${state.parentNode && `${iteration}.${mountOrIntro}( ${state.parentNode}, null );`} ${state.parentNode &&
`${iteration}.${mountOrIntro}( ${state.parentNode}, null );`}
if ( ${last} ) ${last}.next = ${iteration}; if ( ${last} ) ${last}.next = ${iteration};
${iteration}.last = ${last}; ${iteration}.last = ${last};
@ -136,25 +185,25 @@ function keyed ( generator: DomGenerator, block: Block, state: State, node: Node
if ( ${i} === 0 ) ${head} = ${iteration}; if ( ${i} === 0 ) ${head} = ${iteration};
} }
` ); `);
if ( !state.parentNode ) { if (!state.parentNode) {
block.builders.mount.addBlock( deindent` block.builders.mount.addBlock(deindent`
var ${iteration} = ${head}; var ${iteration} = ${head};
while ( ${iteration} ) { while ( ${iteration} ) {
${iteration}.${mountOrIntro}( ${block.target}, anchor ); ${iteration}.${mountOrIntro}( ${block.target}, anchor );
${iteration} = ${iteration}.next; ${iteration} = ${iteration}.next;
} }
` ); `);
} }
const dynamic = node._block.hasUpdateMethod; const dynamic = node._block.hasUpdateMethod;
const parentNode = state.parentNode || `${anchor}.parentNode`; const parentNode = state.parentNode || `${anchor}.parentNode`;
let destroy; let destroy;
if ( node._block.hasOutroMethod ) { if (node._block.hasOutroMethod) {
const fn = block.getUniqueName( `${each_block}_outro` ); const fn = block.getUniqueName(`${each_block}_outro`);
block.builders.create.addBlock( deindent` block.builders.create.addBlock(deindent`
function ${fn} ( iteration ) { function ${fn} ( iteration ) {
iteration.outro( function () { iteration.outro( function () {
iteration.unmount(); iteration.unmount();
@ -162,7 +211,7 @@ function keyed ( generator: DomGenerator, block: Block, state: State, node: Node
${lookup}[iteration.key] = null; ${lookup}[iteration.key] = null;
}); });
} }
` ); `);
destroy = deindent` destroy = deindent`
while ( ${expected} ) { while ( ${expected} ) {
@ -177,14 +226,14 @@ function keyed ( generator: DomGenerator, block: Block, state: State, node: Node
} }
`; `;
} else { } else {
const fn = block.getUniqueName( `${each_block}_destroy` ); const fn = block.getUniqueName(`${each_block}_destroy`);
block.builders.create.addBlock( deindent` block.builders.create.addBlock(deindent`
function ${fn} ( iteration ) { function ${fn} ( iteration ) {
iteration.unmount(); iteration.unmount();
iteration.destroy(); iteration.destroy();
${lookup}[iteration.key] = null; ${lookup}[iteration.key] = null;
} }
` ); `);
destroy = deindent` destroy = deindent`
while ( ${expected} ) { while ( ${expected} ) {
@ -201,7 +250,7 @@ function keyed ( generator: DomGenerator, block: Block, state: State, node: Node
`; `;
} }
block.builders.update.addBlock( deindent` block.builders.update.addBlock(deindent`
var ${each_block_value} = ${snippet}; var ${each_block_value} = ${snippet};
var ${expected} = ${head}; var ${expected} = ${head};
@ -213,7 +262,8 @@ function keyed ( generator: DomGenerator, block: Block, state: State, node: Node
var ${key} = ${each_block_value}[${i}].${node.key}; var ${key} = ${each_block_value}[${i}].${node.key};
var ${iteration} = ${lookup}[${key}]; var ${iteration} = ${lookup}[${key}];
${dynamic && `if ( ${iteration} ) ${iteration}.update( changed, ${params}, ${each_block_value}, ${each_block_value}[${i}], ${i} );`} ${dynamic &&
`if ( ${iteration} ) ${iteration}.update( changed, ${params}, ${each_block_value}, ${each_block_value}[${i}], ${i} );`}
if ( ${expected} ) { if ( ${expected} ) {
if ( ${key} === ${expected}.key ) { if ( ${key} === ${expected}.key ) {
@ -256,7 +306,8 @@ function keyed ( generator: DomGenerator, block: Block, state: State, node: Node
if ( ${last} ) ${last}.next = ${iteration}; if ( ${last} ) ${last}.next = ${iteration};
${iteration}.last = ${last}; ${iteration}.last = ${last};
${node._block.hasIntroMethod && `${iteration}.intro( ${parentNode}, ${anchor} );`} ${node._block.hasIntroMethod &&
`${iteration}.intro( ${parentNode}, ${anchor} );`}
${last} = ${iteration}; ${last} = ${iteration};
} }
@ -265,87 +316,103 @@ function keyed ( generator: DomGenerator, block: Block, state: State, node: Node
${destroy} ${destroy}
${head} = ${lookup}[${each_block_value}[0] && ${each_block_value}[0].${node.key}]; ${head} = ${lookup}[${each_block_value}[0] && ${each_block_value}[0].${node.key}];
` ); `);
if ( !state.parentNode ) { if (!state.parentNode) {
block.builders.unmount.addBlock( deindent` block.builders.unmount.addBlock(deindent`
var ${iteration} = ${head}; var ${iteration} = ${head};
while ( ${iteration} ) { while ( ${iteration} ) {
${iteration}.unmount(); ${iteration}.unmount();
${iteration} = ${iteration}.next; ${iteration} = ${iteration}.next;
} }
` ); `);
} }
block.builders.destroy.addBlock( deindent` block.builders.destroy.addBlock(deindent`
var ${iteration} = ${head}; var ${iteration} = ${head};
while ( ${iteration} ) { while ( ${iteration} ) {
${iteration}.destroy( false ); ${iteration}.destroy( false );
${iteration} = ${iteration}.next; ${iteration} = ${iteration}.next;
} }
` ); `);
} }
function unkeyed ( generator: DomGenerator, block: Block, state: State, node: Node, snippet, { create_each_block, each_block_value, iterations, i, params, anchor, mountOrIntro } ) { function unkeyed(
block.builders.create.addBlock( deindent` generator: DomGenerator,
block: Block,
state: State,
node: Node,
snippet,
{
create_each_block,
each_block_value,
iterations,
i,
params,
anchor,
mountOrIntro
}
) {
block.builders.create.addBlock(deindent`
var ${iterations} = []; var ${iterations} = [];
for ( var ${i} = 0; ${i} < ${each_block_value}.length; ${i} += 1 ) { for ( var ${i} = 0; ${i} < ${each_block_value}.length; ${i} += 1 ) {
${iterations}[${i}] = ${create_each_block}( ${params}, ${each_block_value}, ${each_block_value}[${i}], ${i}, ${block.component} ); ${iterations}[${i}] = ${create_each_block}( ${params}, ${each_block_value}, ${each_block_value}[${i}], ${i}, ${block.component} );
${state.parentNode && `${iterations}[${i}].${mountOrIntro}( ${state.parentNode}, null );`} ${state.parentNode &&
`${iterations}[${i}].${mountOrIntro}( ${state.parentNode}, null );`}
} }
` ); `);
if ( !state.parentNode ) { if (!state.parentNode) {
block.builders.mount.addBlock( deindent` block.builders.mount.addBlock(deindent`
for ( var ${i} = 0; ${i} < ${iterations}.length; ${i} += 1 ) { for ( var ${i} = 0; ${i} < ${iterations}.length; ${i} += 1 ) {
${iterations}[${i}].${mountOrIntro}( ${block.target}, anchor ); ${iterations}[${i}].${mountOrIntro}( ${block.target}, anchor );
} }
` ); `);
} }
const dependencies = block.findDependencies( node.expression ); const dependencies = block.findDependencies(node.expression);
const allDependencies = new Set( node._block.dependencies ); const allDependencies = new Set(node._block.dependencies);
dependencies.forEach( dependency => { dependencies.forEach(dependency => {
allDependencies.add( dependency ); allDependencies.add(dependency);
}); });
// TODO do this for keyed blocks as well // TODO do this for keyed blocks as well
const condition = Array.from( allDependencies ) const condition = Array.from(allDependencies)
.map( dependency => `'${dependency}' in changed` ) .map(dependency => `'${dependency}' in changed`)
.join( ' || ' ); .join(' || ');
const parentNode = state.parentNode || `${anchor}.parentNode`; const parentNode = state.parentNode || `${anchor}.parentNode`;
if ( condition !== '' ) { if (condition !== '') {
const forLoopBody = node._block.hasUpdateMethod ? const forLoopBody = node._block.hasUpdateMethod
node._block.hasIntroMethod ? ? node._block.hasIntroMethod
deindent` ? deindent`
if ( ${iterations}[${i}] ) { if ( ${iterations}[${i}] ) {
${iterations}[${i}].update( changed, ${params}, ${each_block_value}, ${each_block_value}[${i}], ${i} ); ${iterations}[${i}].update( changed, ${params}, ${each_block_value}, ${each_block_value}[${i}], ${i} );
} else { } else {
${iterations}[${i}] = ${create_each_block}( ${params}, ${each_block_value}, ${each_block_value}[${i}], ${i}, ${block.component} ); ${iterations}[${i}] = ${create_each_block}( ${params}, ${each_block_value}, ${each_block_value}[${i}], ${i}, ${block.component} );
} }
${iterations}[${i}].intro( ${parentNode}, ${anchor} ); ${iterations}[${i}].intro( ${parentNode}, ${anchor} );
` : `
deindent` : deindent`
if ( ${iterations}[${i}] ) { if ( ${iterations}[${i}] ) {
${iterations}[${i}].update( changed, ${params}, ${each_block_value}, ${each_block_value}[${i}], ${i} ); ${iterations}[${i}].update( changed, ${params}, ${each_block_value}, ${each_block_value}[${i}], ${i} );
} else { } else {
${iterations}[${i}] = ${create_each_block}( ${params}, ${each_block_value}, ${each_block_value}[${i}], ${i}, ${block.component} ); ${iterations}[${i}] = ${create_each_block}( ${params}, ${each_block_value}, ${each_block_value}[${i}], ${i}, ${block.component} );
${iterations}[${i}].mount( ${parentNode}, ${anchor} ); ${iterations}[${i}].mount( ${parentNode}, ${anchor} );
} }
` : `
deindent` : deindent`
${iterations}[${i}] = ${create_each_block}( ${params}, ${each_block_value}, ${each_block_value}[${i}], ${i}, ${block.component} ); ${iterations}[${i}] = ${create_each_block}( ${params}, ${each_block_value}, ${each_block_value}[${i}], ${i}, ${block.component} );
${iterations}[${i}].${mountOrIntro}( ${parentNode}, ${anchor} ); ${iterations}[${i}].${mountOrIntro}( ${parentNode}, ${anchor} );
`; `;
const start = node._block.hasUpdateMethod ? '0' : `${iterations}.length`; const start = node._block.hasUpdateMethod ? '0' : `${iterations}.length`;
const outro = block.getUniqueName( 'outro' ); const outro = block.getUniqueName('outro');
const destroy = node._block.hasOutroMethod ? const destroy = node._block.hasOutroMethod
deindent` ? deindent`
function ${outro} ( i ) { function ${outro} ( i ) {
if ( ${iterations}[i] ) { if ( ${iterations}[i] ) {
${iterations}[i].outro( function () { ${iterations}[i].outro( function () {
@ -357,8 +424,8 @@ function unkeyed ( generator: DomGenerator, block: Block, state: State, node: No
} }
for ( ; ${i} < ${iterations}.length; ${i} += 1 ) ${outro}( ${i} ); for ( ; ${i} < ${iterations}.length; ${i} += 1 ) ${outro}( ${i} );
` : `
deindent` : deindent`
for ( ; ${i} < ${iterations}.length; ${i} += 1 ) { for ( ; ${i} < ${iterations}.length; ${i} += 1 ) {
${iterations}[${i}].unmount(); ${iterations}[${i}].unmount();
${iterations}[${i}].destroy(); ${iterations}[${i}].destroy();
@ -366,7 +433,7 @@ function unkeyed ( generator: DomGenerator, block: Block, state: State, node: No
${iterations}.length = ${each_block_value}.length; ${iterations}.length = ${each_block_value}.length;
`; `;
block.builders.update.addBlock( deindent` block.builders.update.addBlock(deindent`
var ${each_block_value} = ${snippet}; var ${each_block_value} = ${snippet};
if ( ${condition} ) { if ( ${condition} ) {
@ -376,16 +443,16 @@ function unkeyed ( generator: DomGenerator, block: Block, state: State, node: No
${destroy} ${destroy}
} }
` ); `);
} }
block.builders.unmount.addBlock( deindent` block.builders.unmount.addBlock(deindent`
for ( var ${i} = 0; ${i} < ${iterations}.length; ${i} += 1 ) { for ( var ${i} = 0; ${i} < ${iterations}.length; ${i} += 1 ) {
${iterations}[${i}].unmount(); ${iterations}[${i}].unmount();
} }
` ); `);
block.builders.destroy.addBlock( block.builders.destroy.addBlock(
`${generator.helper( 'destroyEach' )}( ${iterations}, false, 0 );` `${generator.helper('destroyEach')}( ${iterations}, false, 0 );`
); );
} }

@ -6,64 +6,85 @@ import Block from '../../Block';
import { Node } from '../../../../interfaces'; import { Node } from '../../../../interfaces';
import { State } from '../../interfaces'; import { State } from '../../interfaces';
export default function visitAttribute ( generator: DomGenerator, block: Block, state: State, node: Node, attribute: Node ) { export default function visitAttribute(
generator: DomGenerator,
block: Block,
state: State,
node: Node,
attribute: Node
) {
const name = attribute.name; const name = attribute.name;
let metadata = state.namespace ? null : attributeLookup[ name ]; let metadata = state.namespace ? null : attributeLookup[name];
if ( metadata && metadata.appliesTo && !~metadata.appliesTo.indexOf( node.name ) ) metadata = null; if (metadata && metadata.appliesTo && !~metadata.appliesTo.indexOf(node.name))
metadata = null;
const isIndirectlyBoundValue = name === 'value' && ( const isIndirectlyBoundValue =
node.name === 'option' || // TODO check it's actually bound name === 'value' &&
node.name === 'input' && /^(checkbox|radio)$/.test( getStaticAttributeValue( node, 'type' ) ) (node.name === 'option' || // TODO check it's actually bound
); (node.name === 'input' &&
/^(checkbox|radio)$/.test(getStaticAttributeValue(node, 'type'))));
const propertyName = isIndirectlyBoundValue ? '__value' : metadata && metadata.propertyName; const propertyName = isIndirectlyBoundValue
? '__value'
: metadata && metadata.propertyName;
// xlink is a special case... we could maybe extend this to generic // xlink is a special case... we could maybe extend this to generic
// namespaced attributes but I'm not sure that's applicable in // namespaced attributes but I'm not sure that's applicable in
// HTML5? // HTML5?
const method = name.slice( 0, 6 ) === 'xlink:' ? 'setXlinkAttribute' : 'setAttribute'; const method = name.slice(0, 6) === 'xlink:'
? 'setXlinkAttribute'
: 'setAttribute';
const isDynamic = attribute.value !== true && attribute.value.length > 1 || ( attribute.value.length === 1 && attribute.value[0].type !== 'Text' ); const isDynamic =
(attribute.value !== true && attribute.value.length > 1) ||
(attribute.value.length === 1 && attribute.value[0].type !== 'Text');
if ( isDynamic ) { if (isDynamic) {
let value; let value;
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 } = block.contextualise( 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
value = ( attribute.value[0].type === 'Text' ? '' : `"" + ` ) + ( value =
attribute.value.map( ( chunk: Node ) => { (attribute.value[0].type === 'Text' ? '' : `"" + `) +
if ( chunk.type === 'Text' ) { attribute.value
return JSON.stringify( chunk.data ); .map((chunk: Node) => {
} else { if (chunk.type === 'Text') {
const { snippet } = block.contextualise( chunk.expression ); return JSON.stringify(chunk.data);
return `( ${snippet} )`; } else {
} const { snippet } = block.contextualise(chunk.expression);
}).join( ' + ' ) return `( ${snippet} )`;
); }
})
.join(' + ');
} }
const last = block.getUniqueName( `${state.parentNode}_${name.replace( /[^a-zA-Z_$]/g, '_')}_value` ); const last = block.getUniqueName(
block.addVariable( last ); `${state.parentNode}_${name.replace(/[^a-zA-Z_$]/g, '_')}_value`
);
block.addVariable(last);
const isSelectValueAttribute = name === 'value' && state.parentNodeName === 'select'; const isSelectValueAttribute =
name === 'value' && state.parentNodeName === 'select';
let updater; let updater;
if ( isSelectValueAttribute ) { if (isSelectValueAttribute) {
// annoying special case // annoying special case
const isMultipleSelect = node.name === 'select' && node.attributes.find( attr => attr.name.toLowerCase() === 'multiple' ); // TODO use getStaticAttributeValue const isMultipleSelect =
const i = block.getUniqueName( 'i' ); node.name === 'select' &&
const option = block.getUniqueName( 'option' ); node.attributes.find(attr => attr.name.toLowerCase() === 'multiple'); // TODO use getStaticAttributeValue
const i = block.getUniqueName('i');
const ifStatement = isMultipleSelect ? const option = block.getUniqueName('option');
deindent`
${option}.selected = ~${last}.indexOf( ${option}.__value );` : const ifStatement = isMultipleSelect
deindent` ? deindent`
${option}.selected = ~${last}.indexOf( ${option}.__value );`
: deindent`
if ( ${option}.__value === ${last} ) { if ( ${option}.__value === ${last} ) {
${option}.selected = true; ${option}.selected = true;
break; break;
@ -77,53 +98,62 @@ export default function visitAttribute ( generator: DomGenerator, block: Block,
} }
`; `;
block.builders.create.addLine( deindent` block.builders.create.addLine(deindent`
${last} = ${value} ${last} = ${value}
${updater} ${updater}
` ); `);
} else if ( propertyName ) { } else if (propertyName) {
block.builders.create.addLine( `${state.parentNode}.${propertyName} = ${last} = ${value};` ); block.builders.create.addLine(
`${state.parentNode}.${propertyName} = ${last} = ${value};`
);
updater = `${state.parentNode}.${propertyName} = ${last};`; updater = `${state.parentNode}.${propertyName} = ${last};`;
} else { } else {
block.builders.create.addLine( `${generator.helper( method )}( ${state.parentNode}, '${name}', ${last} = ${value} );` ); block.builders.create.addLine(
updater = `${generator.helper( method )}( ${state.parentNode}, '${name}', ${last} );`; `${generator.helper(
method
)}( ${state.parentNode}, '${name}', ${last} = ${value} );`
);
updater = `${generator.helper(
method
)}( ${state.parentNode}, '${name}', ${last} );`;
} }
block.builders.update.addBlock( deindent` block.builders.update.addBlock(deindent`
if ( ${last} !== ( ${last} = ${value} ) ) { if ( ${last} !== ( ${last} = ${value} ) ) {
${updater} ${updater}
} }
` ); `);
} } else {
const value = attribute.value === true
else { ? 'true'
const value = attribute.value === true ? 'true' : : attribute.value.length === 0
attribute.value.length === 0 ? `''` : ? `''`
JSON.stringify( attribute.value[0].data ); : JSON.stringify(attribute.value[0].data);
const statement = propertyName ? const statement = propertyName
`${state.parentNode}.${propertyName} = ${value};` : ? `${state.parentNode}.${propertyName} = ${value};`
`${generator.helper( method )}( ${state.parentNode}, '${name}', ${value} );`; : `${generator.helper(
method
)}( ${state.parentNode}, '${name}', ${value} );`;
block.builders.create.addLine( statement );
block.builders.create.addLine(statement);
// 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 ( attribute.value === true && name === 'autofocus' ) { if (attribute.value === true && name === 'autofocus') {
block.autofocus = state.parentNode; block.autofocus = state.parentNode;
} }
// special case — xmlns // special case — xmlns
if ( name === 'xmlns' ) { if (name === 'xmlns') {
// TODO this attribute must be static enforce at compile time // TODO this attribute must be static enforce at compile time
state.namespace = attribute.value[0].data; state.namespace = attribute.value[0].data;
} }
} }
if ( isIndirectlyBoundValue ) { if (isIndirectlyBoundValue) {
const updateValue = `${state.parentNode}.value = ${state.parentNode}.__value;`; const updateValue = `${state.parentNode}.value = ${state.parentNode}.__value;`;
block.builders.create.addLine( updateValue ); block.builders.create.addLine(updateValue);
if ( isDynamic ) block.builders.update.addLine( updateValue ); if (isDynamic) block.builders.update.addLine(updateValue);
} }
} }

@ -7,51 +7,88 @@ import Block from '../../Block';
import { Node } from '../../../../interfaces'; import { Node } from '../../../../interfaces';
import { State } from '../../interfaces'; import { State } from '../../interfaces';
function getObject ( node ) { function getObject(node) {
// TODO validation should ensure this is an Identifier or a MemberExpression // TODO validation should ensure this is an Identifier or a MemberExpression
while ( node.type === 'MemberExpression' ) node = node.object; while (node.type === 'MemberExpression') node = node.object;
return node; return node;
} }
export default function visitBinding ( generator: DomGenerator, block: Block, state: State, node: Node, attribute: Node ) { export default function visitBinding(
const { name } = getObject( attribute.value ); generator: DomGenerator,
const { snippet, contexts } = block.contextualise( attribute.value ); block: Block,
const dependencies = block.contextDependencies.has( name ) ? block.contextDependencies.get( name ) : [ name ]; state: State,
node: Node,
if ( dependencies.length > 1 ) throw new Error( 'An unexpected situation arose. Please raise an issue at https://github.com/sveltejs/svelte/issues — thanks!' ); attribute: Node
) {
const { name } = getObject(attribute.value);
const { snippet, contexts } = block.contextualise(attribute.value);
const dependencies = block.contextDependencies.has(name)
? block.contextDependencies.get(name)
: [name];
if (dependencies.length > 1)
throw new Error(
'An unexpected situation arose. Please raise an issue at https://github.com/sveltejs/svelte/issues — thanks!'
);
contexts.forEach( context => { contexts.forEach(context => {
if ( !~state.allUsedContexts.indexOf( context ) ) state.allUsedContexts.push( context ); if (!~state.allUsedContexts.indexOf(context))
state.allUsedContexts.push(context);
}); });
const eventName = getBindingEventName( node, attribute ); const eventName = getBindingEventName(node, attribute);
const handler = block.getUniqueName( `${state.parentNode}_${eventName}_handler` ); const handler = block.getUniqueName(
const isMultipleSelect = node.name === 'select' && node.attributes.find( ( attr: Node ) => attr.name.toLowerCase() === 'multiple' ); // TODO use getStaticAttributeValue `${state.parentNode}_${eventName}_handler`
const type = getStaticAttributeValue( node, 'type' ); );
const bindingGroup = attribute.name === 'group' ? getBindingGroup( generator, attribute.value ) : null; const isMultipleSelect =
const value = getBindingValue( generator, block, state, node, attribute, isMultipleSelect, bindingGroup, type ); node.name === 'select' &&
node.attributes.find(
let setter = getSetter({ block, name, snippet, context: '_svelte', attribute, dependencies, value }); (attr: Node) => attr.name.toLowerCase() === 'multiple'
); // TODO use getStaticAttributeValue
const type = getStaticAttributeValue(node, 'type');
const bindingGroup = attribute.name === 'group'
? getBindingGroup(generator, attribute.value)
: null;
const value = getBindingValue(
generator,
block,
state,
node,
attribute,
isMultipleSelect,
bindingGroup,
type
);
let setter = getSetter({
block,
name,
snippet,
context: '_svelte',
attribute,
dependencies,
value
});
let updateElement = `${state.parentNode}.${attribute.name} = ${snippet};`; let updateElement = `${state.parentNode}.${attribute.name} = ${snippet};`;
const lock = block.alias( `${state.parentNode}_updating` ); const lock = block.alias(`${state.parentNode}_updating`);
let updateCondition = `!${lock}`; let updateCondition = `!${lock}`;
block.addVariable( lock, 'false' ); block.addVariable(lock, 'false');
// <select> special case // <select> special case
if ( node.name === 'select' ) { if (node.name === 'select') {
if ( !isMultipleSelect ) { if (!isMultipleSelect) {
setter = `var selectedOption = ${state.parentNode}.querySelector(':checked') || ${state.parentNode}.options[0];\n${setter}`; setter = `var selectedOption = ${state.parentNode}.querySelector(':checked') || ${state.parentNode}.options[0];\n${setter}`;
} }
const value = block.getUniqueName( 'value' ); const value = block.getUniqueName('value');
const i = block.alias( 'i' ); const i = block.alias('i');
const option = block.getUniqueName( 'option' ); const option = block.getUniqueName('option');
const ifStatement = isMultipleSelect ? const ifStatement = isMultipleSelect
deindent` ? deindent`
${option}.selected = ~${value}.indexOf( ${option}.__value );` : ${option}.selected = ~${value}.indexOf( ${option}.__value );`
deindent` : deindent`
if ( ${option}.__value === ${value} ) { if ( ${option}.__value === ${value} ) {
${option}.selected = true; ${option}.selected = true;
break; break;
@ -65,20 +102,18 @@ export default function visitBinding ( generator: DomGenerator, block: Block, st
${ifStatement} ${ifStatement}
} }
`; `;
} } else if (attribute.name === 'group') {
// <input type='checkbox|radio' bind:group='selected'> special case
// <input type='checkbox|radio' bind:group='selected'> special case if (type === 'radio') {
else if ( attribute.name === 'group' ) {
if ( type === 'radio' ) {
setter = deindent` setter = deindent`
if ( !${state.parentNode}.checked ) return; if ( !${state.parentNode}.checked ) return;
${setter} ${setter}
`; `;
} }
const condition = type === 'checkbox' ? const condition = type === 'checkbox'
`~${snippet}.indexOf( ${state.parentNode}.__value )` : ? `~${snippet}.indexOf( ${state.parentNode}.__value )`
`${state.parentNode}.__value === ${snippet}`; : `${state.parentNode}.__value === ${snippet}`;
block.builders.create.addLine( block.builders.create.addLine(
`${block.component}._bindingGroups[${bindingGroup}].push( ${state.parentNode} );` `${block.component}._bindingGroups[${bindingGroup}].push( ${state.parentNode} );`
@ -89,15 +124,15 @@ export default function visitBinding ( generator: DomGenerator, block: Block, st
); );
updateElement = `${state.parentNode}.checked = ${condition};`; updateElement = `${state.parentNode}.checked = ${condition};`;
} } else if (node.name === 'audio' || node.name === 'video') {
else if ( node.name === 'audio' || node.name === 'video' ) {
generator.hasComplexBindings = true; generator.hasComplexBindings = true;
block.builders.create.addBlock( `${block.component}._bindings.push( ${handler} );` ); block.builders.create.addBlock(
`${block.component}._bindings.push( ${handler} );`
);
if ( attribute.name === 'currentTime' ) { if (attribute.name === 'currentTime') {
const frame = block.getUniqueName( `${state.parentNode}_animationframe` ); const frame = block.getUniqueName(`${state.parentNode}_animationframe`);
block.addVariable( frame ); block.addVariable(frame);
setter = deindent` setter = deindent`
cancelAnimationFrame( ${frame} ); cancelAnimationFrame( ${frame} );
if ( !${state.parentNode}.paused ) ${frame} = requestAnimationFrame( ${handler} ); if ( !${state.parentNode}.paused ) ${frame} = requestAnimationFrame( ${handler} );
@ -105,108 +140,132 @@ export default function visitBinding ( generator: DomGenerator, block: Block, st
`; `;
updateCondition += ` && !isNaN( ${snippet} )`; updateCondition += ` && !isNaN( ${snippet} )`;
} } else if (attribute.name === 'duration') {
else if ( attribute.name === 'duration' ) {
updateCondition = null; updateCondition = null;
} } else if (attribute.name === 'paused') {
else if ( attribute.name === 'paused' ) {
// this is necessary to prevent the audio restarting by itself // this is necessary to prevent the audio restarting by itself
const last = block.getUniqueName( `${state.parentNode}_paused_value` ); const last = block.getUniqueName(`${state.parentNode}_paused_value`);
block.addVariable( last, 'true' ); block.addVariable(last, 'true');
updateCondition = `${last} !== ( ${last} = ${snippet} )`; updateCondition = `${last} !== ( ${last} = ${snippet} )`;
updateElement = `${state.parentNode}[ ${last} ? 'pause' : 'play' ]();`; updateElement = `${state.parentNode}[ ${last} ? 'pause' : 'play' ]();`;
} }
} }
block.builders.create.addBlock( deindent` block.builders.create.addBlock(deindent`
function ${handler} () { function ${handler} () {
${lock} = true; ${lock} = true;
${setter} ${setter}
${lock} = false; ${lock} = false;
} }
${generator.helper( 'addEventListener' )}( ${state.parentNode}, '${eventName}', ${handler} ); ${generator.helper(
` ); 'addEventListener'
)}( ${state.parentNode}, '${eventName}', ${handler} );
`);
if ( node.name !== 'audio' && node.name !== 'video' ) node.initialUpdate = updateElement; if (node.name !== 'audio' && node.name !== 'video')
node.initialUpdate = updateElement;
if ( updateCondition !== null ) { if (updateCondition !== null) {
// audio/video duration is read-only, it never updates // audio/video duration is read-only, it never updates
block.builders.update.addBlock( deindent` block.builders.update.addBlock(deindent`
if ( ${updateCondition} ) { if ( ${updateCondition} ) {
${updateElement} ${updateElement}
} }
` ); `);
} }
block.builders.destroy.addLine( deindent` block.builders.destroy.addLine(deindent`
${generator.helper( 'removeEventListener' )}( ${state.parentNode}, '${eventName}', ${handler} ); ${generator.helper(
` ); 'removeEventListener'
)}( ${state.parentNode}, '${eventName}', ${handler} );
`);
if ( attribute.name === 'paused' ) { if (attribute.name === 'paused') {
block.builders.create.addLine( `${generator.helper( 'addEventListener' )}( ${state.parentNode}, 'play', ${handler} );` ); block.builders.create.addLine(
block.builders.destroy.addLine( `${generator.helper( 'removeEventListener' )}( ${state.parentNode}, 'play', ${handler} );` ); `${generator.helper(
'addEventListener'
)}( ${state.parentNode}, 'play', ${handler} );`
);
block.builders.destroy.addLine(
`${generator.helper(
'removeEventListener'
)}( ${state.parentNode}, 'play', ${handler} );`
);
} }
} }
function getBindingEventName ( node: Node, attribute: Node ) { function getBindingEventName(node: Node, attribute: Node) {
if ( node.name === 'input' ) { if (node.name === 'input') {
const typeAttribute = node.attributes.find( ( attr: Node ) => attr.type === 'Attribute' && attr.name === 'type' ); const typeAttribute = node.attributes.find(
(attr: Node) => attr.type === 'Attribute' && attr.name === 'type'
);
const type = typeAttribute ? typeAttribute.value[0].data : 'text'; // TODO in validation, should throw if type attribute is not static const type = typeAttribute ? typeAttribute.value[0].data : 'text'; // TODO in validation, should throw if type attribute is not static
return type === 'checkbox' || type === 'radio' ? 'change' : 'input'; return type === 'checkbox' || type === 'radio' ? 'change' : 'input';
} }
if ( node.name === 'textarea' ) return 'input'; if (node.name === 'textarea') return 'input';
if ( attribute.name === 'currentTime' ) return 'timeupdate'; if (attribute.name === 'currentTime') return 'timeupdate';
if ( attribute.name === 'duration' ) return 'durationchange'; if (attribute.name === 'duration') return 'durationchange';
if ( attribute.name === 'paused' ) return 'pause'; if (attribute.name === 'paused') return 'pause';
return 'change'; return 'change';
} }
function getBindingValue ( generator: DomGenerator, block: Block, state: State, node: Node, attribute: Node, isMultipleSelect: boolean, bindingGroup: number, type: string ) { function getBindingValue(
generator: DomGenerator,
block: Block,
state: State,
node: Node,
attribute: Node,
isMultipleSelect: boolean,
bindingGroup: number,
type: string
) {
// <select multiple bind:value='selected> // <select multiple bind:value='selected>
if ( isMultipleSelect ) { if (isMultipleSelect) {
return `[].map.call( ${state.parentNode}.querySelectorAll(':checked'), function ( option ) { return option.__value; })`; return `[].map.call( ${state.parentNode}.querySelectorAll(':checked'), function ( option ) { return option.__value; })`;
} }
// <select bind:value='selected> // <select bind:value='selected>
if ( node.name === 'select' ) { if (node.name === 'select') {
return 'selectedOption && selectedOption.__value'; return 'selectedOption && selectedOption.__value';
} }
// <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' )}( ${block.component}._bindingGroups[${bindingGroup}] )`; return `${generator.helper(
'getBindingGroupValue'
)}( ${block.component}._bindingGroups[${bindingGroup}] )`;
} }
return `${state.parentNode}.__value`; return `${state.parentNode}.__value`;
} }
// <input type='range|number' bind:value> // <input type='range|number' bind:value>
if ( type === 'range' || type === 'number' ) { if (type === 'range' || type === 'number') {
return `${generator.helper( 'toNumber' )}( ${state.parentNode}.${attribute.name} )`; return `${generator.helper(
'toNumber'
)}( ${state.parentNode}.${attribute.name} )`;
} }
// everything else // everything else
return `${state.parentNode}.${attribute.name}`; return `${state.parentNode}.${attribute.name}`;
} }
function getBindingGroup ( generator: DomGenerator, value: Node ) { function getBindingGroup(generator: DomGenerator, value: Node) {
const { parts } = flattenReference( value ); // TODO handle cases involving computed member expressions const { parts } = flattenReference(value); // TODO handle cases involving computed member expressions
const keypath = parts.join( '.' ); const keypath = parts.join('.');
// 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);
if ( index === -1 ) { if (index === -1) {
index = generator.bindingGroups.length; index = generator.bindingGroups.length;
generator.bindingGroups.push( keypath ); generator.bindingGroups.push(keypath);
} }
return index; return index;

@ -30,87 +30,109 @@ const visitors = {
Ref: visitRef Ref: visitRef
}; };
export default function visitElement ( generator: DomGenerator, block: Block, state: State, node: Node ) { export default function visitElement(
if ( node.name in meta ) { generator: DomGenerator,
return meta[ node.name ]( generator, block, node ); block: Block,
state: State,
node: Node
) {
if (node.name in meta) {
return meta[node.name](generator, block, node);
} }
if ( generator.components.has( node.name ) || node.name === ':Self' ) { if (generator.components.has(node.name) || node.name === ':Self') {
return visitComponent( generator, block, state, node ); return visitComponent(generator, block, state, node);
} }
const childState = node._state; const childState = node._state;
const name = childState.parentNode; const name = childState.parentNode;
block.builders.create.addLine( `var ${name} = ${getRenderStatement( generator, childState.namespace, node.name )};` ); block.builders.create.addLine(
block.mount( name, state.parentNode ); `var ${name} = ${getRenderStatement(
generator,
childState.namespace,
node.name
)};`
);
block.mount(name, state.parentNode);
// add CSS encapsulation attribute // add CSS encapsulation attribute
if ( generator.cssId && ( !generator.cascade || state.isTopLevel ) ) { if (generator.cssId && (!generator.cascade || state.isTopLevel)) {
block.builders.create.addLine( `${generator.helper( 'setAttribute' )}( ${name}, '${generator.cssId}', '' );` ); block.builders.create.addLine(
`${generator.helper(
'setAttribute'
)}( ${name}, '${generator.cssId}', '' );`
);
} }
function visitAttributesAndAddProps () { function visitAttributesAndAddProps() {
let intro; let intro;
let outro; let outro;
node.attributes node.attributes
.sort( ( a: Node, b: Node ) => order[ a.type ] - order[ b.type ] ) .sort((a: Node, b: Node) => order[a.type] - order[b.type])
.forEach( ( attribute: Node ) => { .forEach((attribute: Node) => {
if ( attribute.type === 'Transition' ) { if (attribute.type === 'Transition') {
if ( attribute.intro ) intro = attribute; if (attribute.intro) intro = attribute;
if ( attribute.outro ) outro = attribute; if (attribute.outro) outro = attribute;
return; return;
} }
visitors[ attribute.type ]( generator, block, childState, node, attribute ); visitors[attribute.type](generator, block, childState, node, attribute);
}); });
if ( intro || outro ) addTransitions( generator, block, childState, node, intro, outro ); if (intro || outro)
addTransitions(generator, block, childState, node, intro, outro);
if ( childState.allUsedContexts.length || childState.usesComponent ) { if (childState.allUsedContexts.length || childState.usesComponent) {
const initialProps: string[] = []; const initialProps: string[] = [];
const updates: string[] = []; const updates: string[] = [];
if ( childState.usesComponent ) { if (childState.usesComponent) {
initialProps.push( `component: ${block.component}` ); initialProps.push(`component: ${block.component}`);
} }
childState.allUsedContexts.forEach( ( contextName: string ) => { childState.allUsedContexts.forEach((contextName: string) => {
if ( contextName === 'state' ) return; if (contextName === 'state') return;
const listName = block.listNames.get( contextName ); const listName = block.listNames.get(contextName);
const indexName = block.indexNames.get( contextName ); const indexName = block.indexNames.get(contextName);
initialProps.push( `${listName}: ${listName},\n${indexName}: ${indexName}` ); initialProps.push(
updates.push( `${name}._svelte.${listName} = ${listName};\n${name}._svelte.${indexName} = ${indexName};` ); `${listName}: ${listName},\n${indexName}: ${indexName}`
);
updates.push(
`${name}._svelte.${listName} = ${listName};\n${name}._svelte.${indexName} = ${indexName};`
);
}); });
if ( initialProps.length ) { if (initialProps.length) {
block.builders.create.addBlock( deindent` block.builders.create.addBlock(deindent`
${name}._svelte = { ${name}._svelte = {
${initialProps.join( ',\n' )} ${initialProps.join(',\n')}
}; };
` ); `);
} }
if ( updates.length ) { if (updates.length) {
block.builders.update.addBlock( updates.join( '\n' ) ); block.builders.update.addBlock(updates.join('\n'));
} }
} }
} }
if ( !state.parentNode ) { if (!state.parentNode) {
// TODO we eventually need to consider what happens to elements // TODO we eventually need to consider what happens to elements
// that belong to the same outgroup as an outroing element... // that belong to the same outgroup as an outroing element...
block.builders.unmount.addLine( `${generator.helper( 'detachNode' )}( ${name} );` ); block.builders.unmount.addLine(
`${generator.helper('detachNode')}( ${name} );`
);
} }
if ( node.name !== 'select' ) { if (node.name !== 'select') {
if ( node.name === 'textarea' ) { if (node.name === 'textarea') {
// this is an egregious hack, but it's the easiest way to get <textarea> // this is an egregious hack, but it's the easiest way to get <textarea>
// children treated the same way as a value attribute // children treated the same way as a value attribute
if ( node.children.length > 0 ) { if (node.children.length > 0) {
node.attributes.push({ node.attributes.push({
type: 'Attribute', type: 'Attribute',
name: 'value', name: 'value',
@ -127,36 +149,47 @@ export default function visitElement ( generator: DomGenerator, block: Block, st
} }
// special case bound <option> without a value attribute // special case bound <option> without a value attribute
if ( node.name === 'option' && !node.attributes.find( ( attribute: Node ) => attribute.type === 'Attribute' && attribute.name === 'value' ) ) { // TODO check it's bound if (
node.name === 'option' &&
!node.attributes.find(
(attribute: Node) =>
attribute.type === 'Attribute' && attribute.name === 'value'
)
) {
// TODO check it's bound
const statement = `${name}.__value = ${name}.textContent;`; const statement = `${name}.__value = ${name}.textContent;`;
node.initialUpdate = node.lateUpdate = statement; node.initialUpdate = node.lateUpdate = statement;
} }
node.children.forEach( ( child: Node ) => { node.children.forEach((child: Node) => {
visit( generator, block, childState, child ); visit(generator, block, childState, child);
}); });
if ( node.lateUpdate ) { if (node.lateUpdate) {
block.builders.update.addLine( node.lateUpdate ); block.builders.update.addLine(node.lateUpdate);
} }
if ( node.name === 'select' ) { if (node.name === 'select') {
visitAttributesAndAddProps(); visitAttributesAndAddProps();
} }
if ( node.initialUpdate ) { if (node.initialUpdate) {
block.builders.create.addBlock( node.initialUpdate ); block.builders.create.addBlock(node.initialUpdate);
} }
} }
function getRenderStatement ( generator: DomGenerator, namespace: string, name: string ) { function getRenderStatement(
if ( namespace === 'http://www.w3.org/2000/svg' ) { generator: DomGenerator,
return `${generator.helper( 'createSvgElement' )}( '${name}' )`; namespace: string,
name: string
) {
if (namespace === 'http://www.w3.org/2000/svg') {
return `${generator.helper('createSvgElement')}( '${name}' )`;
} }
if ( namespace ) { if (namespace) {
return `document.createElementNS( '${namespace}', '${name}' )`; return `document.createElementNS( '${namespace}', '${name}' )`;
} }
return `${generator.helper( 'createElement' )}( '${name}' )`; return `${generator.helper('createElement')}( '${name}' )`;
} }

@ -5,49 +5,61 @@ import Block from '../../Block';
import { Node } from '../../../../interfaces'; import { Node } from '../../../../interfaces';
import { State } from '../../interfaces'; import { State } from '../../interfaces';
export default function visitEventHandler ( generator: DomGenerator, block: Block, state: State, node: Node, attribute: Node ) { export default function visitEventHandler(
generator: DomGenerator,
block: Block,
state: State,
node: Node,
attribute: Node
) {
const name = attribute.name; const name = attribute.name;
const isCustomEvent = generator.events.has( name ); const isCustomEvent = generator.events.has(name);
const shouldHoist = !isCustomEvent && state.inEachBlock; const shouldHoist = !isCustomEvent && state.inEachBlock;
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
// 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.code.prependRight( attribute.expression.start, `${block.component}.` ); generator.code.prependRight(
if ( shouldHoist ) state.usesComponent = true; // this feels a bit hacky but it works! attribute.expression.start,
`${block.component}.`
);
if (shouldHoist) state.usesComponent = true; // this feels a bit hacky but it works!
} }
const context = shouldHoist ? null : state.parentNode; const context = shouldHoist ? null : state.parentNode;
const usedContexts: string[] = []; const usedContexts: string[] = [];
attribute.expression.arguments.forEach( ( arg: Node ) => { attribute.expression.arguments.forEach((arg: Node) => {
const { contexts } = block.contextualise( arg, context, true ); const { contexts } = block.contextualise(arg, context, true);
contexts.forEach( context => { contexts.forEach(context => {
if ( !~usedContexts.indexOf( context ) ) usedContexts.push( context ); if (!~usedContexts.indexOf(context)) usedContexts.push(context);
if ( !~state.allUsedContexts.indexOf( context ) ) state.allUsedContexts.push( context ); if (!~state.allUsedContexts.indexOf(context))
state.allUsedContexts.push(context);
}); });
}); });
const _this = context || 'this'; const _this = context || 'this';
const declarations = usedContexts.map( name => { const declarations = usedContexts.map(name => {
if ( name === 'state' ) { if (name === 'state') {
if ( shouldHoist ) state.usesComponent = true; if (shouldHoist) state.usesComponent = true;
return `var state = ${block.component}.get();`; return `var state = ${block.component}.get();`;
} }
const listName = block.listNames.get( name ); const listName = block.listNames.get(name);
const indexName = block.indexNames.get( name ); const indexName = block.indexNames.get(name);
const contextName = block.contexts.get( name ); const contextName = block.contexts.get(name);
return `var ${listName} = ${_this}._svelte.${listName}, ${indexName} = ${_this}._svelte.${indexName}, ${contextName} = ${listName}[${indexName}];`; return `var ${listName} = ${_this}._svelte.${listName}, ${indexName} = ${_this}._svelte.${indexName}, ${contextName} = ${listName}[${indexName}];`;
}); });
// get a name for the event handler that is globally unique // get a name for the event handler that is globally unique
// if hoisted, locally unique otherwise // if hoisted, locally unique otherwise
const handlerName = ( shouldHoist ? generator : block ).getUniqueName( `${name.replace( /[^a-zA-Z0-9_$]/g, '_' )}_handler` ); const handlerName = (shouldHoist ? generator : block).getUniqueName(
`${name.replace(/[^a-zA-Z0-9_$]/g, '_')}_handler`
);
// create the handler body // create the handler body
const handlerBody = deindent` const handlerBody = deindent`
@ -56,37 +68,45 @@ export default function visitEventHandler ( generator: DomGenerator, block: Bloc
[${attribute.expression.start}-${attribute.expression.end}]; [${attribute.expression.start}-${attribute.expression.end}];
`; `;
const handler = isCustomEvent ? const handler = isCustomEvent
deindent` ? deindent`
var ${handlerName} = ${generator.alias( 'template' )}.events.${name}.call( ${block.component}, ${state.parentNode}, function ( event ) { var ${handlerName} = ${generator.alias(
'template'
)}.events.${name}.call( ${block.component}, ${state.parentNode}, function ( event ) {
${handlerBody} ${handlerBody}
}); });
` : `
deindent` : deindent`
function ${handlerName} ( event ) { function ${handlerName} ( event ) {
${handlerBody} ${handlerBody}
} }
`; `;
if ( shouldHoist ) { if (shouldHoist) {
generator.blocks.push(<Block>{ generator.blocks.push(
render: () => handler <Block>{
}); render: () => handler
}
);
} else { } else {
block.builders.create.addBlock( handler ); block.builders.create.addBlock(handler);
} }
if ( isCustomEvent ) { if (isCustomEvent) {
block.builders.destroy.addLine( deindent` block.builders.destroy.addLine(deindent`
${handlerName}.teardown(); ${handlerName}.teardown();
` ); `);
} else { } else {
block.builders.create.addLine( deindent` block.builders.create.addLine(deindent`
${generator.helper( 'addEventListener' )}( ${state.parentNode}, '${name}', ${handlerName} ); ${generator.helper(
` ); 'addEventListener'
)}( ${state.parentNode}, '${name}', ${handlerName} );
`);
block.builders.destroy.addLine( deindent` block.builders.destroy.addLine(deindent`
${generator.helper( 'removeEventListener' )}( ${state.parentNode}, '${name}', ${handlerName} ); ${generator.helper(
` ); 'removeEventListener'
)}( ${state.parentNode}, '${name}', ${handlerName} );
`);
} }
} }

@ -4,16 +4,22 @@ import Block from '../../Block';
import { Node } from '../../../../interfaces'; import { Node } from '../../../../interfaces';
import { State } from '../../interfaces'; import { State } from '../../interfaces';
export default function visitRef ( generator: DomGenerator, block: Block, state: State, node: Node, attribute: Node ) { export default function visitRef(
generator: DomGenerator,
block: Block,
state: State,
node: Node,
attribute: Node
) {
const name = attribute.name; const name = attribute.name;
block.builders.create.addLine( block.builders.create.addLine(
`${block.component}.refs.${name} = ${state.parentNode};` `${block.component}.refs.${name} = ${state.parentNode};`
); );
block.builders.destroy.addLine( deindent` block.builders.destroy.addLine(deindent`
if ( ${block.component}.refs.${name} === ${state.parentNode} ) ${block.component}.refs.${name} = null; if ( ${block.component}.refs.${name} === ${state.parentNode} ) ${block.component}.refs.${name} = null;
` ); `);
generator.usesRefs = true; // so this component.refs object is created generator.usesRefs = true; // so this component.refs object is created
} }

@ -4,77 +4,88 @@ import Block from '../../Block';
import { Node } from '../../../../interfaces'; import { Node } from '../../../../interfaces';
import { State } from '../../interfaces'; import { State } from '../../interfaces';
export default function addTransitions ( generator: DomGenerator, block: Block, state: State, node: Node, intro, outro ) { export default function addTransitions(
const wrapTransition = generator.helper( 'wrapTransition' ); generator: DomGenerator,
block: Block,
state: State,
node: Node,
intro,
outro
) {
const wrapTransition = generator.helper('wrapTransition');
if ( intro === outro ) { if (intro === outro) {
const name = block.getUniqueName( `${state.name}_transition` ); const name = block.getUniqueName(`${state.name}_transition`);
const snippet = intro.expression ? block.contextualise( intro.expression ).snippet : '{}'; const snippet = intro.expression
? block.contextualise(intro.expression).snippet
: '{}';
block.addVariable( name ); block.addVariable(name);
const fn = `${generator.alias( 'template' )}.transitions.${intro.name}`; const fn = `${generator.alias('template')}.transitions.${intro.name}`;
block.builders.intro.addBlock( deindent` block.builders.intro.addBlock(deindent`
${block.component}._renderHooks.push( function () { ${block.component}._renderHooks.push( function () {
if ( !${name} ) ${name} = ${wrapTransition}( ${state.name}, ${fn}, ${snippet}, true, null ); if ( !${name} ) ${name} = ${wrapTransition}( ${state.name}, ${fn}, ${snippet}, true, null );
${name}.run( true, function () { ${name}.run( true, function () {
${block.component}.fire( 'intro.end', { node: ${state.name} }); ${block.component}.fire( 'intro.end', { node: ${state.name} });
}); });
}); });
` ); `);
block.builders.outro.addBlock( deindent` block.builders.outro.addBlock(deindent`
${name}.run( false, function () { ${name}.run( false, function () {
${block.component}.fire( 'outro.end', { node: ${state.name} }); ${block.component}.fire( 'outro.end', { node: ${state.name} });
if ( --${block.alias( 'outros' )} === 0 ) ${block.alias( 'outrocallback' )}(); if ( --${block.alias('outros')} === 0 ) ${block.alias('outrocallback')}();
${name} = null; ${name} = null;
}); });
` ); `);
} } else {
const introName = intro && block.getUniqueName(`${state.name}_intro`);
else { const outroName = outro && block.getUniqueName(`${state.name}_outro`);
const introName = intro && block.getUniqueName( `${state.name}_intro` );
const outroName = outro && block.getUniqueName( `${state.name}_outro` );
if ( intro ) { if (intro) {
block.addVariable( introName ); block.addVariable(introName);
const snippet = intro.expression ? block.contextualise( intro.expression ).snippet : '{}'; const snippet = intro.expression
? block.contextualise(intro.expression).snippet
: '{}';
const fn = `${generator.alias( 'template' )}.transitions.${intro.name}`; // TODO add built-in transitions? const fn = `${generator.alias('template')}.transitions.${intro.name}`; // TODO add built-in transitions?
if ( outro ) { if (outro) {
block.builders.intro.addBlock( deindent` block.builders.intro.addBlock(deindent`
if ( ${introName} ) ${introName}.abort(); if ( ${introName} ) ${introName}.abort();
if ( ${outroName} ) ${outroName}.abort(); if ( ${outroName} ) ${outroName}.abort();
` ); `);
} }
block.builders.intro.addBlock( deindent` block.builders.intro.addBlock(deindent`
${block.component}._renderHooks.push( function () { ${block.component}._renderHooks.push( function () {
${introName} = ${wrapTransition}( ${state.name}, ${fn}, ${snippet}, true, null ); ${introName} = ${wrapTransition}( ${state.name}, ${fn}, ${snippet}, true, null );
${introName}.run( true, function () { ${introName}.run( true, function () {
${block.component}.fire( 'intro.end', { node: ${state.name} }); ${block.component}.fire( 'intro.end', { node: ${state.name} });
}); });
}); });
` ); `);
} }
if ( outro ) { if (outro) {
block.addVariable( outroName ); block.addVariable(outroName);
const snippet = outro.expression ? block.contextualise( outro.expression ).snippet : '{}'; const snippet = outro.expression
? block.contextualise(outro.expression).snippet
: '{}';
const fn = `${generator.alias( 'template' )}.transitions.${outro.name}`; const fn = `${generator.alias('template')}.transitions.${outro.name}`;
// TODO hide elements that have outro'd (unless they belong to a still-outroing // TODO hide elements that have outro'd (unless they belong to a still-outroing
// group) prior to their removal from the DOM // group) prior to their removal from the DOM
block.builders.outro.addBlock( deindent` block.builders.outro.addBlock(deindent`
${outroName} = ${wrapTransition}( ${state.name}, ${fn}, ${snippet}, false, null ); ${outroName} = ${wrapTransition}( ${state.name}, ${fn}, ${snippet}, false, null );
${outroName}.run( false, function () { ${outroName}.run( false, function () {
${block.component}.fire( 'outro.end', { node: ${state.name} }); ${block.component}.fire( 'outro.end', { node: ${state.name} });
if ( --${block.alias( 'outros' )} === 0 ) ${block.alias( 'outrocallback' )}(); if ( --${block.alias('outros')} === 0 ) ${block.alias('outrocallback')}();
}); });
` ); `);
} }
} }
} }

@ -1,13 +1,15 @@
import { Node } from '../../../../interfaces'; import { Node } from '../../../../interfaces';
export default function getStaticAttributeValue ( node: Node, name: string ) { export default function getStaticAttributeValue(node: Node, name: string) {
const attribute = node.attributes.find( ( attr: Node ) => attr.name.toLowerCase() === name ); const attribute = node.attributes.find(
if ( !attribute ) return null; (attr: Node) => attr.name.toLowerCase() === name
);
if (!attribute) return null;
if ( attribute.value.length !== 1 || attribute.value[0].type !== 'Text' ) { if (attribute.value.length !== 1 || attribute.value[0].type !== 'Text') {
// TODO catch this in validation phase, give a more useful error (with location etc) // TODO catch this in validation phase, give a more useful error (with location etc)
throw new Error( `'${name} must be a static attribute` ); throw new Error(`'${name} must be a static attribute`);
} }
return attribute.value[0].data; return attribute.value[0].data;
} }

@ -1,122 +1,235 @@
// source: https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes // source: https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes
const lookup = { const lookup = {
accept: { appliesTo: [ 'form', 'input' ] }, accept: { appliesTo: ['form', 'input'] },
'accept-charset': { propertyName: 'acceptCharset', appliesTo: [ 'form' ] }, 'accept-charset': { propertyName: 'acceptCharset', appliesTo: ['form'] },
accesskey: { propertyName: 'accessKey' }, accesskey: { propertyName: 'accessKey' },
action: { appliesTo: [ 'form' ] }, action: { appliesTo: ['form'] },
align: { appliesTo: [ 'applet', 'caption', 'col', 'colgroup', 'hr', 'iframe', 'img', 'table', 'tbody', 'td', 'tfoot' , 'th', 'thead', 'tr' ] }, align: {
allowfullscreen: { propertyName: 'allowFullscreen', appliesTo: [ 'iframe' ] }, appliesTo: [
alt: { appliesTo: [ 'applet', 'area', 'img', 'input' ] }, 'applet',
async: { appliesTo: [ 'script' ] }, 'caption',
autocomplete: { appliesTo: [ 'form', 'input' ] }, 'col',
autofocus: { appliesTo: [ 'button', 'input', 'keygen', 'select', 'textarea' ] }, 'colgroup',
autoplay: { appliesTo: [ 'audio', 'video' ] }, 'hr',
autosave: { appliesTo: [ 'input' ] }, 'iframe',
bgcolor: { propertyName: 'bgColor', appliesTo: [ 'body', 'col', 'colgroup', 'marquee', 'table', 'tbody', 'tfoot', 'td', 'th', 'tr' ] }, 'img',
border: { appliesTo: [ 'img', 'object', 'table' ] }, 'table',
buffered: { appliesTo: [ 'audio', 'video' ] }, 'tbody',
challenge: { appliesTo: [ 'keygen' ] }, 'td',
charset: { appliesTo: [ 'meta', 'script' ] }, 'tfoot',
checked: { appliesTo: [ 'command', 'input' ] }, 'th',
cite: { appliesTo: [ 'blockquote', 'del', 'ins', 'q' ] }, 'thead',
'tr'
]
},
allowfullscreen: { propertyName: 'allowFullscreen', appliesTo: ['iframe'] },
alt: { appliesTo: ['applet', 'area', 'img', 'input'] },
async: { appliesTo: ['script'] },
autocomplete: { appliesTo: ['form', 'input'] },
autofocus: { appliesTo: ['button', 'input', 'keygen', 'select', 'textarea'] },
autoplay: { appliesTo: ['audio', 'video'] },
autosave: { appliesTo: ['input'] },
bgcolor: {
propertyName: 'bgColor',
appliesTo: [
'body',
'col',
'colgroup',
'marquee',
'table',
'tbody',
'tfoot',
'td',
'th',
'tr'
]
},
border: { appliesTo: ['img', 'object', 'table'] },
buffered: { appliesTo: ['audio', 'video'] },
challenge: { appliesTo: ['keygen'] },
charset: { appliesTo: ['meta', 'script'] },
checked: { appliesTo: ['command', 'input'] },
cite: { appliesTo: ['blockquote', 'del', 'ins', 'q'] },
class: { propertyName: 'className' }, class: { propertyName: 'className' },
code: { appliesTo: [ 'applet' ] }, code: { appliesTo: ['applet'] },
codebase: { propertyName: 'codeBase', appliesTo: [ 'applet' ] }, codebase: { propertyName: 'codeBase', appliesTo: ['applet'] },
color: { appliesTo: [ 'basefont', 'font', 'hr' ] }, color: { appliesTo: ['basefont', 'font', 'hr'] },
cols: { appliesTo: [ 'textarea' ] }, cols: { appliesTo: ['textarea'] },
colspan: { propertyName: 'colSpan', appliesTo: [ 'td', 'th' ] }, colspan: { propertyName: 'colSpan', appliesTo: ['td', 'th'] },
content: { appliesTo: [ 'meta' ] }, content: { appliesTo: ['meta'] },
contenteditable: { propertyName: 'contentEditable' }, contenteditable: { propertyName: 'contentEditable' },
contextmenu: {}, contextmenu: {},
controls: { appliesTo: [ 'audio', 'video' ] }, controls: { appliesTo: ['audio', 'video'] },
coords: { appliesTo: [ 'area' ] }, coords: { appliesTo: ['area'] },
data: { appliesTo: [ 'object' ] }, data: { appliesTo: ['object'] },
datetime: { propertyName: 'dateTime', appliesTo: [ 'del', 'ins', 'time' ] }, datetime: { propertyName: 'dateTime', appliesTo: ['del', 'ins', 'time'] },
default: { appliesTo: [ 'track' ] }, default: { appliesTo: ['track'] },
defer: { appliesTo: [ 'script' ] }, defer: { appliesTo: ['script'] },
dir: {}, dir: {},
dirname: { propertyName: 'dirName', appliesTo: [ 'input', 'textarea' ] }, dirname: { propertyName: 'dirName', appliesTo: ['input', 'textarea'] },
disabled: { appliesTo: [ 'button', 'command', 'fieldset', 'input', 'keygen', 'optgroup', 'option', 'select', 'textarea' ] }, disabled: {
download: { appliesTo: [ 'a', 'area' ] }, appliesTo: [
'button',
'command',
'fieldset',
'input',
'keygen',
'optgroup',
'option',
'select',
'textarea'
]
},
download: { appliesTo: ['a', 'area'] },
draggable: {}, draggable: {},
dropzone: {}, dropzone: {},
enctype: { appliesTo: [ 'form' ] }, enctype: { appliesTo: ['form'] },
for: { propertyName: 'htmlFor', appliesTo: [ 'label', 'output' ] }, for: { propertyName: 'htmlFor', appliesTo: ['label', 'output'] },
form: { appliesTo: [ 'button', 'fieldset', 'input', 'keygen', 'label', 'meter', 'object', 'output', 'progress', 'select', 'textarea' ] }, form: {
formaction: { appliesTo: [ 'input', 'button' ] }, appliesTo: [
headers: { appliesTo: [ 'td', 'th' ] }, 'button',
height: { appliesTo: [ 'canvas', 'embed', 'iframe', 'img', 'input', 'object', 'video' ] }, 'fieldset',
'input',
'keygen',
'label',
'meter',
'object',
'output',
'progress',
'select',
'textarea'
]
},
formaction: { appliesTo: ['input', 'button'] },
headers: { appliesTo: ['td', 'th'] },
height: {
appliesTo: ['canvas', 'embed', 'iframe', 'img', 'input', 'object', 'video']
},
hidden: {}, hidden: {},
high: { appliesTo: [ 'meter' ] }, high: { appliesTo: ['meter'] },
href: { appliesTo: [ 'a', 'area', 'base', 'link' ] }, href: { appliesTo: ['a', 'area', 'base', 'link'] },
hreflang: { appliesTo: [ 'a', 'area', 'link' ] }, hreflang: { appliesTo: ['a', 'area', 'link'] },
'http-equiv': { propertyName: 'httpEquiv', appliesTo: [ 'meta' ] }, 'http-equiv': { propertyName: 'httpEquiv', appliesTo: ['meta'] },
icon: { appliesTo: [ 'command' ] }, icon: { appliesTo: ['command'] },
id: {}, id: {},
ismap: { propertyName: 'isMap', appliesTo: [ 'img' ] }, ismap: { propertyName: 'isMap', appliesTo: ['img'] },
itemprop: {}, itemprop: {},
keytype: { appliesTo: [ 'keygen' ] }, keytype: { appliesTo: ['keygen'] },
kind: { appliesTo: [ 'track' ] }, kind: { appliesTo: ['track'] },
label: { appliesTo: [ 'track' ] }, label: { appliesTo: ['track'] },
lang: {}, lang: {},
language: { appliesTo: [ 'script' ] }, language: { appliesTo: ['script'] },
loop: { appliesTo: [ 'audio', 'bgsound', 'marquee', 'video' ] }, loop: { appliesTo: ['audio', 'bgsound', 'marquee', 'video'] },
low: { appliesTo: [ 'meter' ] }, low: { appliesTo: ['meter'] },
manifest: { appliesTo: [ 'html' ] }, manifest: { appliesTo: ['html'] },
max: { appliesTo: [ 'input', 'meter', 'progress' ] }, max: { appliesTo: ['input', 'meter', 'progress'] },
maxlength: { propertyName: 'maxLength', appliesTo: [ 'input', 'textarea' ] }, maxlength: { propertyName: 'maxLength', appliesTo: ['input', 'textarea'] },
media: { appliesTo: [ 'a', 'area', 'link', 'source', 'style' ] }, media: { appliesTo: ['a', 'area', 'link', 'source', 'style'] },
method: { appliesTo: [ 'form' ] }, method: { appliesTo: ['form'] },
min: { appliesTo: [ 'input', 'meter' ] }, min: { appliesTo: ['input', 'meter'] },
multiple: { appliesTo: [ 'input', 'select' ] }, multiple: { appliesTo: ['input', 'select'] },
muted: { appliesTo: [ 'video' ] }, muted: { appliesTo: ['video'] },
name: { appliesTo: [ 'button', 'form', 'fieldset', 'iframe', 'input', 'keygen', 'object', 'output', 'select', 'textarea', 'map', 'meta', 'param' ] }, name: {
novalidate: { propertyName: 'noValidate', appliesTo: [ 'form' ] }, appliesTo: [
open: { appliesTo: [ 'details' ] }, 'button',
optimum: { appliesTo: [ 'meter' ] }, 'form',
pattern: { appliesTo: [ 'input' ] }, 'fieldset',
ping: { appliesTo: [ 'a', 'area' ] }, 'iframe',
placeholder: { appliesTo: [ 'input', 'textarea' ] }, 'input',
poster: { appliesTo: [ 'video' ] }, 'keygen',
preload: { appliesTo: [ 'audio', 'video' ] }, 'object',
radiogroup: { appliesTo: [ 'command' ] }, 'output',
readonly: { propertyName: 'readOnly', appliesTo: [ 'input', 'textarea' ] }, 'select',
rel: { appliesTo: [ 'a', 'area', 'link' ] }, 'textarea',
required: { appliesTo: [ 'input', 'select', 'textarea' ] }, 'map',
reversed: { appliesTo: [ 'ol' ] }, 'meta',
rows: { appliesTo: [ 'textarea' ] }, 'param'
rowspan: { propertyName: 'rowSpan', appliesTo: [ 'td', 'th' ] }, ]
sandbox: { appliesTo: [ 'iframe' ] }, },
scope: { appliesTo: [ 'th' ] }, novalidate: { propertyName: 'noValidate', appliesTo: ['form'] },
scoped: { appliesTo: [ 'style' ] }, open: { appliesTo: ['details'] },
seamless: { appliesTo: [ 'iframe' ] }, optimum: { appliesTo: ['meter'] },
selected: { appliesTo: [ 'option' ] }, pattern: { appliesTo: ['input'] },
shape: { appliesTo: [ 'a', 'area' ] }, ping: { appliesTo: ['a', 'area'] },
size: { appliesTo: [ 'input', 'select' ] }, placeholder: { appliesTo: ['input', 'textarea'] },
sizes: { appliesTo: [ 'link', 'img', 'source' ] }, poster: { appliesTo: ['video'] },
span: { appliesTo: [ 'col', 'colgroup' ] }, preload: { appliesTo: ['audio', 'video'] },
radiogroup: { appliesTo: ['command'] },
readonly: { propertyName: 'readOnly', appliesTo: ['input', 'textarea'] },
rel: { appliesTo: ['a', 'area', 'link'] },
required: { appliesTo: ['input', 'select', 'textarea'] },
reversed: { appliesTo: ['ol'] },
rows: { appliesTo: ['textarea'] },
rowspan: { propertyName: 'rowSpan', appliesTo: ['td', 'th'] },
sandbox: { appliesTo: ['iframe'] },
scope: { appliesTo: ['th'] },
scoped: { appliesTo: ['style'] },
seamless: { appliesTo: ['iframe'] },
selected: { appliesTo: ['option'] },
shape: { appliesTo: ['a', 'area'] },
size: { appliesTo: ['input', 'select'] },
sizes: { appliesTo: ['link', 'img', 'source'] },
span: { appliesTo: ['col', 'colgroup'] },
spellcheck: {}, spellcheck: {},
src: { appliesTo: [ 'audio', 'embed', 'iframe', 'img', 'input', 'script', 'source', 'track', 'video' ] }, src: {
srcdoc: { appliesTo: [ 'iframe' ] }, appliesTo: [
srclang: { appliesTo: [ 'track' ] }, 'audio',
srcset: { appliesTo: [ 'img' ] }, 'embed',
start: { appliesTo: [ 'ol' ] }, 'iframe',
step: { appliesTo: [ 'input' ] }, 'img',
'input',
'script',
'source',
'track',
'video'
]
},
srcdoc: { appliesTo: ['iframe'] },
srclang: { appliesTo: ['track'] },
srcset: { appliesTo: ['img'] },
start: { appliesTo: ['ol'] },
step: { appliesTo: ['input'] },
style: { propertyName: 'style.cssText' }, style: { propertyName: 'style.cssText' },
summary: { appliesTo: [ 'table' ] }, summary: { appliesTo: ['table'] },
tabindex: { propertyName: 'tabIndex' }, tabindex: { propertyName: 'tabIndex' },
target: { appliesTo: [ 'a', 'area', 'base', 'form' ] }, target: { appliesTo: ['a', 'area', 'base', 'form'] },
title: {}, title: {},
type: { appliesTo: [ 'button', 'input', 'command', 'embed', 'object', 'script', 'source', 'style', 'menu' ] }, type: {
usemap: { propertyName: 'useMap', appliesTo: [ 'img', 'input', 'object' ] }, appliesTo: [
value: { appliesTo: [ 'button', 'option', 'input', 'li', 'meter', 'progress', 'param', 'select', 'textarea' ] }, 'button',
width: { appliesTo: [ 'canvas', 'embed', 'iframe', 'img', 'input', 'object', 'video' ] }, 'input',
wrap: { appliesTo: [ 'textarea' ] } 'command',
'embed',
'object',
'script',
'source',
'style',
'menu'
]
},
usemap: { propertyName: 'useMap', appliesTo: ['img', 'input', 'object'] },
value: {
appliesTo: [
'button',
'option',
'input',
'li',
'meter',
'progress',
'param',
'select',
'textarea'
]
},
width: {
appliesTo: ['canvas', 'embed', 'iframe', 'img', 'input', 'object', 'video']
},
wrap: { appliesTo: ['textarea'] }
}; };
Object.keys( lookup ).forEach( name => { Object.keys(lookup).forEach(name => {
const metadata = lookup[ name ]; const metadata = lookup[name];
if ( !metadata.propertyName ) metadata.propertyName = name; if (!metadata.propertyName) metadata.propertyName = name;
}); });
export default lookup; export default lookup;

@ -22,65 +22,74 @@ const readonly = new Set([
'online' 'online'
]); ]);
export default function visitWindow ( generator: DomGenerator, block: Block, node: Node ) { export default function visitWindow(
generator: DomGenerator,
block: Block,
node: Node
) {
const events = {}; const events = {};
const bindings = {}; const bindings = {};
node.attributes.forEach( ( attribute: Node ) => { node.attributes.forEach((attribute: Node) => {
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);
let usesState = false; let usesState = false;
attribute.expression.arguments.forEach( ( arg: Node ) => { attribute.expression.arguments.forEach((arg: Node) => {
const { contexts } = block.contextualise( arg, null, true ); const { contexts } = block.contextualise(arg, null, true);
if ( contexts.length ) usesState = true; if (contexts.length) usesState = true;
}); });
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, `${block.component}.` ); generator.code.prependRight(
attribute.expression.start,
`${block.component}.`
);
} }
const handlerName = block.getUniqueName( `onwindow${attribute.name}` ); const handlerName = block.getUniqueName(`onwindow${attribute.name}`);
const handlerBody = deindent` const handlerBody = deindent`
${usesState && `var state = ${block.component}.get();`} ${usesState && `var state = ${block.component}.get();`}
[${attribute.expression.start}-${attribute.expression.end}]; [${attribute.expression.start}-${attribute.expression.end}];
`; `;
block.builders.create.addBlock( deindent` block.builders.create.addBlock(deindent`
function ${handlerName} ( event ) { function ${handlerName} ( event ) {
${handlerBody} ${handlerBody}
}; };
window.addEventListener( '${attribute.name}', ${handlerName} ); window.addEventListener( '${attribute.name}', ${handlerName} );
` ); `);
block.builders.destroy.addBlock( deindent` block.builders.destroy.addBlock(deindent`
window.removeEventListener( '${attribute.name}', ${handlerName} ); window.removeEventListener( '${attribute.name}', ${handlerName} );
` ); `);
} }
if ( attribute.type === 'Binding' ) { if (attribute.type === 'Binding') {
// in dev mode, throw if read-only values are written to // in dev mode, throw if read-only values are written to
if ( readonly.has( attribute.name ) ) { if (readonly.has(attribute.name)) {
generator.readonly.add( attribute.value.name ); generator.readonly.add(attribute.value.name);
} }
bindings[ attribute.name ] = attribute.value.name; bindings[attribute.name] = attribute.value.name;
// bind:online is a special case, we need to listen for two separate events // bind:online is a special case, we need to listen for two separate events
if ( attribute.name === 'online' ) return; if (attribute.name === 'online') return;
const associatedEvent = associatedEvents[ attribute.name ]; const associatedEvent = associatedEvents[attribute.name];
if ( !associatedEvent ) { if (!associatedEvent) {
throw new Error( `Cannot bind to ${attribute.name} on <:Window>` ); throw new Error(`Cannot bind to ${attribute.name} on <:Window>`);
} }
if ( !events[ associatedEvent ] ) events[ associatedEvent ] = []; if (!events[associatedEvent]) events[associatedEvent] = [];
events[ associatedEvent ].push( `${attribute.value.name}: this.${attribute.name}` ); events[associatedEvent].push(
`${attribute.value.name}: this.${attribute.name}`
);
// add initial value // add initial value
generator.metaBindings.push( generator.metaBindings.push(
@ -89,14 +98,15 @@ export default function visitWindow ( generator: DomGenerator, block: Block, nod
} }
}); });
const lock = block.getUniqueName( `window_updating` ); const lock = block.getUniqueName(`window_updating`);
Object.keys( events ).forEach( event => { Object.keys(events).forEach(event => {
const handlerName = block.getUniqueName( `onwindow${event}` ); const handlerName = block.getUniqueName(`onwindow${event}`);
const props = events[ event ].join( ',\n' ); const props = events[event].join(',\n');
if ( event === 'scroll' ) { // TODO other bidirectional bindings... if (event === 'scroll') {
block.addVariable( lock, 'false' ); // TODO other bidirectional bindings...
block.addVariable(lock, 'false');
} }
const handlerBody = deindent` const handlerBody = deindent`
@ -111,63 +121,74 @@ export default function visitWindow ( generator: DomGenerator, block: Block, nod
${event === 'scroll' && `${lock} = false;`} ${event === 'scroll' && `${lock} = false;`}
`; `;
block.builders.create.addBlock( deindent` block.builders.create.addBlock(deindent`
function ${handlerName} ( event ) { function ${handlerName} ( event ) {
${handlerBody} ${handlerBody}
}; };
window.addEventListener( '${event}', ${handlerName} ); window.addEventListener( '${event}', ${handlerName} );
` ); `);
block.builders.destroy.addBlock( deindent` block.builders.destroy.addBlock(deindent`
window.removeEventListener( '${event}', ${handlerName} ); window.removeEventListener( '${event}', ${handlerName} );
` ); `);
}); });
// special case... might need to abstract this out if we add more special cases // special case... might need to abstract this out if we add more special cases
if ( bindings.scrollX && bindings.scrollY ) { if (bindings.scrollX && bindings.scrollY) {
const observerCallback = block.getUniqueName( `scrollobserver` ); const observerCallback = block.getUniqueName(`scrollobserver`);
block.builders.create.addBlock( deindent` block.builders.create.addBlock(deindent`
function ${observerCallback} () { function ${observerCallback} () {
if ( ${lock} ) return; if ( ${lock} ) return;
var x = ${bindings.scrollX ? `${block.component}.get( '${bindings.scrollX}' )` : `window.scrollX`}; var x = ${bindings.scrollX
var y = ${bindings.scrollY ? `${block.component}.get( '${bindings.scrollY}' )` : `window.scrollY`}; ? `${block.component}.get( '${bindings.scrollX}' )`
: `window.scrollX`};
var y = ${bindings.scrollY
? `${block.component}.get( '${bindings.scrollY}' )`
: `window.scrollY`};
window.scrollTo( x, y ); window.scrollTo( x, y );
}; };
` ); `);
if ( bindings.scrollX ) block.builders.create.addLine( `${block.component}.observe( '${bindings.scrollX}', ${observerCallback} );` ); if (bindings.scrollX)
if ( bindings.scrollY ) block.builders.create.addLine( `${block.component}.observe( '${bindings.scrollY}', ${observerCallback} );` ); block.builders.create.addLine(
} else if ( bindings.scrollX || bindings.scrollY ) { `${block.component}.observe( '${bindings.scrollX}', ${observerCallback} );`
);
if (bindings.scrollY)
block.builders.create.addLine(
`${block.component}.observe( '${bindings.scrollY}', ${observerCallback} );`
);
} else if (bindings.scrollX || bindings.scrollY) {
const isX = !!bindings.scrollX; const isX = !!bindings.scrollX;
block.builders.create.addBlock( deindent` block.builders.create.addBlock(deindent`
${block.component}.observe( '${bindings.scrollX || bindings.scrollY}', function ( ${isX ? 'x' : 'y'} ) { ${block.component}.observe( '${bindings.scrollX ||
bindings.scrollY}', function ( ${isX ? 'x' : 'y'} ) {
if ( ${lock} ) return; if ( ${lock} ) return;
window.scrollTo( ${isX ? 'x, window.scrollY' : 'window.scrollX, y' } ); window.scrollTo( ${isX ? 'x, window.scrollY' : 'window.scrollX, y'} );
}); });
` ); `);
} }
// another special case. (I'm starting to think these are all special cases.) // another special case. (I'm starting to think these are all special cases.)
if ( bindings.online ) { if (bindings.online) {
const handlerName = block.getUniqueName( `onlinestatuschanged` ); const handlerName = block.getUniqueName(`onlinestatuschanged`);
block.builders.create.addBlock( deindent` block.builders.create.addBlock(deindent`
function ${handlerName} ( event ) { function ${handlerName} ( event ) {
${block.component}.set({ ${bindings.online}: navigator.onLine }); ${block.component}.set({ ${bindings.online}: navigator.onLine });
}; };
window.addEventListener( 'online', ${handlerName} ); window.addEventListener( 'online', ${handlerName} );
window.addEventListener( 'offline', ${handlerName} ); window.addEventListener( 'offline', ${handlerName} );
` ); `);
// add initial value // add initial value
generator.metaBindings.push( generator.metaBindings.push(
`this._state.${bindings.online} = navigator.onLine;` `this._state.${bindings.online} = navigator.onLine;`
); );
block.builders.destroy.addBlock( deindent` block.builders.destroy.addBlock(deindent`
window.removeEventListener( 'online', ${handlerName} ); window.removeEventListener( 'online', ${handlerName} );
window.removeEventListener( 'offline', ${handlerName} ); window.removeEventListener( 'offline', ${handlerName} );
` ); `);
} }
} }

@ -5,28 +5,37 @@ import Block from '../Block';
import { Node } from '../../../interfaces'; import { Node } from '../../../interfaces';
import { State } from '../interfaces'; import { State } from '../interfaces';
function isElseIf ( node: Node ) { function isElseIf(node: Node) {
return node && node.children.length === 1 && node.children[0].type === 'IfBlock'; return (
node && node.children.length === 1 && node.children[0].type === 'IfBlock'
);
} }
function isElseBranch ( branch ) { function isElseBranch(branch) {
return branch.block && !branch.condition; return branch.block && !branch.condition;
} }
function getBranches ( generator: DomGenerator, block: Block, state: State, node: Node ) { function getBranches(
const branches = [{ generator: DomGenerator,
condition: block.contextualise( node.expression ).snippet, block: Block,
block: node._block.name, state: State,
hasUpdateMethod: node._block.hasUpdateMethod, node: Node
hasIntroMethod: node._block.hasIntroMethod, ) {
hasOutroMethod: node._block.hasOutroMethod const branches = [
}]; {
condition: block.contextualise(node.expression).snippet,
block: node._block.name,
hasUpdateMethod: node._block.hasUpdateMethod,
hasIntroMethod: node._block.hasIntroMethod,
hasOutroMethod: node._block.hasOutroMethod
}
];
visitChildren( generator, block, state, node ); visitChildren(generator, block, state, node);
if ( isElseIf( node.else ) ) { if (isElseIf(node.else)) {
branches.push( branches.push(
...getBranches( generator, block, state, node.else.children[0] ) ...getBranches(generator, block, state, node.else.children[0])
); );
} else { } else {
branches.push({ branches.push({
@ -37,28 +46,40 @@ function getBranches ( generator: DomGenerator, block: Block, state: State, node
hasOutroMethod: node.else ? node.else._block.hasOutroMethod : false hasOutroMethod: node.else ? node.else._block.hasOutroMethod : false
}); });
if ( node.else ) { if (node.else) {
visitChildren( generator, block, state, node.else ); visitChildren(generator, block, state, node.else);
} }
} }
return branches; return branches;
} }
function visitChildren ( generator: DomGenerator, block: Block, state: State, node: Node ) { function visitChildren(
node.children.forEach( ( child: Node ) => { generator: DomGenerator,
visit( generator, node._block, node._state, child ); block: Block,
state: State,
node: Node
) {
node.children.forEach((child: Node) => {
visit(generator, node._block, node._state, child);
}); });
} }
export default function visitIfBlock ( generator: DomGenerator, block: Block, state: State, node: Node ) { export default function visitIfBlock(
const name = generator.getUniqueName( `if_block` ); generator: DomGenerator,
const anchor = node.needsAnchor ? block.getUniqueName( `${name}_anchor` ) : ( node.next && node.next._state.name ) || 'null'; block: Block,
const params = block.params.join( ', ' ); state: State,
node: Node
const branches = getBranches( generator, block, state, node ); ) {
const name = generator.getUniqueName(`if_block`);
const hasElse = isElseBranch( branches[ branches.length - 1 ] ); const anchor = node.needsAnchor
? block.getUniqueName(`${name}_anchor`)
: (node.next && node.next._state.name) || 'null';
const params = block.params.join(', ');
const branches = getBranches(generator, block, state, node);
const hasElse = isElseBranch(branches[branches.length - 1]);
const if_name = hasElse ? '' : `if ( ${name} ) `; const if_name = hasElse ? '' : `if ( ${name} ) `;
const dynamic = branches[0].hasUpdateMethod; // can use [0] as proxy for all, since they necessarily have the same value const dynamic = branches[0].hasUpdateMethod; // can use [0] as proxy for all, since they necessarily have the same value
@ -66,42 +87,67 @@ export default function visitIfBlock ( generator: DomGenerator, block: Block, st
const vars = { name, anchor, params, if_name, hasElse }; const vars = { name, anchor, params, if_name, hasElse };
if ( node.else ) { if (node.else) {
if ( hasOutros ) { if (hasOutros) {
compoundWithOutros( generator, block, state, node, branches, dynamic, vars ); compoundWithOutros(
generator,
block,
state,
node,
branches,
dynamic,
vars
);
} else { } else {
compound( generator, block, state, node, branches, dynamic, vars ); compound(generator, block, state, node, branches, dynamic, vars);
} }
} else { } else {
simple( generator, block, state, node, branches[0], dynamic, vars ); simple(generator, block, state, node, branches[0], dynamic, vars);
} }
if ( node.needsAnchor ) { if (node.needsAnchor) {
block.addElement( anchor, `${generator.helper( 'createComment' )}()`, state.parentNode, true ); block.addElement(
} else if ( node.next ) { anchor,
`${generator.helper('createComment')}()`,
state.parentNode,
true
);
} else if (node.next) {
node.next.usedAsAnchor = true; node.next.usedAsAnchor = true;
} }
} }
function simple ( generator: DomGenerator, block: Block, state: State, node: Node, branch, dynamic, { name, anchor, params, if_name } ) { function simple(
block.builders.create.addBlock( deindent` generator: DomGenerator,
block: Block,
state: State,
node: Node,
branch,
dynamic,
{ name, anchor, params, if_name }
) {
block.builders.create.addBlock(deindent`
var ${name} = (${branch.condition}) && ${branch.block}( ${params}, ${block.component} ); var ${name} = (${branch.condition}) && ${branch.block}( ${params}, ${block.component} );
` ); `);
const isTopLevel = !state.parentNode; const isTopLevel = !state.parentNode;
const mountOrIntro = branch.hasIntroMethod ? 'intro' : 'mount'; const mountOrIntro = branch.hasIntroMethod ? 'intro' : 'mount';
if ( isTopLevel ) { if (isTopLevel) {
block.builders.mount.addLine( `if ( ${name} ) ${name}.${mountOrIntro}( ${block.target}, anchor );` ); block.builders.mount.addLine(
`if ( ${name} ) ${name}.${mountOrIntro}( ${block.target}, anchor );`
);
} else { } else {
block.builders.create.addLine( `if ( ${name} ) ${name}.${mountOrIntro}( ${state.parentNode}, null );` ); block.builders.create.addLine(
`if ( ${name} ) ${name}.${mountOrIntro}( ${state.parentNode}, null );`
);
} }
const parentNode = state.parentNode || `${anchor}.parentNode`; const parentNode = state.parentNode || `${anchor}.parentNode`;
const enter = dynamic ? const enter = dynamic
( branch.hasIntroMethod ? ? branch.hasIntroMethod
deindent` ? deindent`
if ( ${name} ) { if ( ${name} ) {
${name}.update( changed, ${params} ); ${name}.update( changed, ${params} );
} else { } else {
@ -109,83 +155,93 @@ function simple ( generator: DomGenerator, block: Block, state: State, node: Nod
} }
${name}.intro( ${parentNode}, ${anchor} ); ${name}.intro( ${parentNode}, ${anchor} );
` : `
deindent` : deindent`
if ( ${name} ) { if ( ${name} ) {
${name}.update( changed, ${params} ); ${name}.update( changed, ${params} );
} else { } else {
${name} = ${branch.block}( ${params}, ${block.component} ); ${name} = ${branch.block}( ${params}, ${block.component} );
${name}.mount( ${parentNode}, ${anchor} ); ${name}.mount( ${parentNode}, ${anchor} );
} }
` ) : `
( branch.hasIntroMethod ? : branch.hasIntroMethod
deindent` ? deindent`
if ( !${name} ) ${name} = ${branch.block}( ${params}, ${block.component} ); if ( !${name} ) ${name} = ${branch.block}( ${params}, ${block.component} );
${name}.intro( ${parentNode}, ${anchor} ); ${name}.intro( ${parentNode}, ${anchor} );
` : `
deindent` : deindent`
if ( !${name} ) { if ( !${name} ) {
${name} = ${branch.block}( ${params}, ${block.component} ); ${name} = ${branch.block}( ${params}, ${block.component} );
${name}.mount( ${parentNode}, ${anchor} ); ${name}.mount( ${parentNode}, ${anchor} );
} }
` ); `;
// no `update()` here — we don't want to update outroing nodes, // no `update()` here — we don't want to update outroing nodes,
// as that will typically result in glitching // as that will typically result in glitching
const exit = branch.hasOutroMethod ? const exit = branch.hasOutroMethod
deindent` ? deindent`
${name}.outro( function () { ${name}.outro( function () {
${name}.unmount(); ${name}.unmount();
${name}.destroy(); ${name}.destroy();
${name} = null; ${name} = null;
}); });
` : `
deindent` : deindent`
${name}.unmount(); ${name}.unmount();
${name}.destroy(); ${name}.destroy();
${name} = null; ${name} = null;
`; `;
block.builders.update.addBlock( deindent` block.builders.update.addBlock(deindent`
if ( ${branch.condition} ) { if ( ${branch.condition} ) {
${enter} ${enter}
} else if ( ${name} ) { } else if ( ${name} ) {
${exit} ${exit}
} }
` ); `);
block.builders.unmount.addLine( block.builders.unmount.addLine(`${if_name}${name}.unmount();`);
`${if_name}${name}.unmount();`
);
block.builders.destroy.addLine( block.builders.destroy.addLine(`${if_name}${name}.destroy();`);
`${if_name}${name}.destroy();`
);
} }
function compound ( generator: DomGenerator, block: Block, state: State, node: Node, branches, dynamic, { name, anchor, params, hasElse, if_name } ) { function compound(
const get_block = block.getUniqueName( `get_block` ); generator: DomGenerator,
const current_block = block.getUniqueName( `current_block` ); block: Block,
state: State,
node: Node,
branches,
dynamic,
{ name, anchor, params, hasElse, if_name }
) {
const get_block = block.getUniqueName(`get_block`);
const current_block = block.getUniqueName(`current_block`);
const current_block_and = hasElse ? '' : `${current_block} && `; const current_block_and = hasElse ? '' : `${current_block} && `;
block.builders.create.addBlock( deindent` block.builders.create.addBlock(deindent`
function ${get_block} ( ${params} ) { function ${get_block} ( ${params} ) {
${branches.map( ({ condition, block }) => { ${branches
return `${condition ? `if ( ${condition} ) ` : ''}return ${block};`; .map(({ condition, block }) => {
} ).join( '\n' )} return `${condition ? `if ( ${condition} ) ` : ''}return ${block};`;
})
.join('\n')}
} }
var ${current_block} = ${get_block}( ${params} ); var ${current_block} = ${get_block}( ${params} );
var ${name} = ${current_block_and}${current_block}( ${params}, ${block.component} ); var ${name} = ${current_block_and}${current_block}( ${params}, ${block.component} );
` ); `);
const isTopLevel = !state.parentNode; const isTopLevel = !state.parentNode;
const mountOrIntro = branches[0].hasIntroMethod ? 'intro' : 'mount'; const mountOrIntro = branches[0].hasIntroMethod ? 'intro' : 'mount';
if ( isTopLevel ) { if (isTopLevel) {
block.builders.mount.addLine( `${if_name}${name}.${mountOrIntro}( ${block.target}, anchor );` ); block.builders.mount.addLine(
`${if_name}${name}.${mountOrIntro}( ${block.target}, anchor );`
);
} else { } else {
block.builders.create.addLine( `${if_name}${name}.${mountOrIntro}( ${state.parentNode}, null );` ); block.builders.create.addLine(
`${if_name}${name}.${mountOrIntro}( ${state.parentNode}, null );`
);
} }
const parentNode = state.parentNode || `${anchor}.parentNode`; const parentNode = state.parentNode || `${anchor}.parentNode`;
@ -199,20 +255,20 @@ function compound ( generator: DomGenerator, block: Block, state: State, node: N
${if_name}${name}.${mountOrIntro}( ${parentNode}, ${anchor} ); ${if_name}${name}.${mountOrIntro}( ${parentNode}, ${anchor} );
`; `;
if ( dynamic ) { if (dynamic) {
block.builders.update.addBlock( deindent` block.builders.update.addBlock(deindent`
if ( ${current_block} === ( ${current_block} = ${get_block}( ${params} ) ) && ${name} ) { if ( ${current_block} === ( ${current_block} = ${get_block}( ${params} ) ) && ${name} ) {
${name}.update( changed, ${params} ); ${name}.update( changed, ${params} );
} else { } else {
${changeBlock} ${changeBlock}
} }
` ); `);
} else { } else {
block.builders.update.addBlock( deindent` block.builders.update.addBlock(deindent`
if ( ${current_block} !== ( ${current_block} = ${get_block}( ${params} ) ) ) { if ( ${current_block} !== ( ${current_block} = ${get_block}( ${params} ) ) ) {
${changeBlock} ${changeBlock}
} }
` ); `);
} }
block.builders.destroy.addLine( block.builders.destroy.addLine(
@ -225,52 +281,70 @@ function compound ( generator: DomGenerator, block: Block, state: State, node: N
// if any of the siblings have outros, we need to keep references to the blocks // if any of the siblings have outros, we need to keep references to the blocks
// (TODO does this only apply to bidi transitions?) // (TODO does this only apply to bidi transitions?)
function compoundWithOutros ( generator: DomGenerator, block: Block, state: State, node: Node, branches, dynamic, { name, anchor, params, hasElse } ) { function compoundWithOutros(
const get_block = block.getUniqueName( `get_block` ); generator: DomGenerator,
const current_block_index = block.getUniqueName( `current_block_index` ); block: Block,
const previous_block_index = block.getUniqueName( `previous_block_index` ); state: State,
const if_block_creators = block.getUniqueName( `if_block_creators` ); node: Node,
const if_blocks = block.getUniqueName( `if_blocks` ); branches,
dynamic,
const if_current_block_index = hasElse ? '' : `if ( ~${current_block_index} ) `; { name, anchor, params, hasElse }
) {
block.addVariable( current_block_index ); const get_block = block.getUniqueName(`get_block`);
block.addVariable( name ); const current_block_index = block.getUniqueName(`current_block_index`);
const previous_block_index = block.getUniqueName(`previous_block_index`);
block.builders.create.addBlock( deindent` const if_block_creators = block.getUniqueName(`if_block_creators`);
const if_blocks = block.getUniqueName(`if_blocks`);
const if_current_block_index = hasElse
? ''
: `if ( ~${current_block_index} ) `;
block.addVariable(current_block_index);
block.addVariable(name);
block.builders.create.addBlock(deindent`
var ${if_block_creators} = [ var ${if_block_creators} = [
${branches.map( branch => branch.block ).join( ',\n' )} ${branches.map(branch => branch.block).join(',\n')}
]; ];
var ${if_blocks} = []; var ${if_blocks} = [];
function ${get_block} ( ${params} ) { function ${get_block} ( ${params} ) {
${branches.map( ({ condition, block }, i ) => { ${branches
return `${condition ? `if ( ${condition} ) ` : ''}return ${block ? i : -1};`; .map(({ condition, block }, i) => {
} ).join( '\n' )} return `${condition ? `if ( ${condition} ) ` : ''}return ${block
? i
: -1};`;
})
.join('\n')}
} }
` ); `);
if ( hasElse ) { if (hasElse) {
block.builders.create.addBlock( deindent` block.builders.create.addBlock(deindent`
${current_block_index} = ${get_block}( ${params} ); ${current_block_index} = ${get_block}( ${params} );
${name} = ${if_blocks}[ ${current_block_index} ] = ${if_block_creators}[ ${current_block_index} ]( ${params}, ${block.component} ); ${name} = ${if_blocks}[ ${current_block_index} ] = ${if_block_creators}[ ${current_block_index} ]( ${params}, ${block.component} );
` ); `);
} else { } else {
block.builders.create.addBlock( deindent` block.builders.create.addBlock(deindent`
if ( ~( ${current_block_index} = ${get_block}( ${params} ) ) ) { if ( ~( ${current_block_index} = ${get_block}( ${params} ) ) ) {
${name} = ${if_blocks}[ ${current_block_index} ] = ${if_block_creators}[ ${current_block_index} ]( ${params}, ${block.component} ); ${name} = ${if_blocks}[ ${current_block_index} ] = ${if_block_creators}[ ${current_block_index} ]( ${params}, ${block.component} );
} }
` ); `);
} }
const isTopLevel = !state.parentNode; const isTopLevel = !state.parentNode;
const mountOrIntro = branches[0].hasIntroMethod ? 'intro' : 'mount'; const mountOrIntro = branches[0].hasIntroMethod ? 'intro' : 'mount';
if ( isTopLevel ) { if (isTopLevel) {
block.builders.mount.addLine( `${if_current_block_index}${if_blocks}[ ${current_block_index} ].${mountOrIntro}( ${block.target}, anchor );` ); block.builders.mount.addLine(
`${if_current_block_index}${if_blocks}[ ${current_block_index} ].${mountOrIntro}( ${block.target}, anchor );`
);
} else { } else {
block.builders.create.addLine( `${if_current_block_index}${if_blocks}[ ${current_block_index} ].${mountOrIntro}( ${state.parentNode}, null );` ); block.builders.create.addLine(
`${if_current_block_index}${if_blocks}[ ${current_block_index} ].${mountOrIntro}( ${state.parentNode}, null );`
);
} }
const parentNode = state.parentNode || `${anchor}.parentNode`; const parentNode = state.parentNode || `${anchor}.parentNode`;
@ -288,13 +362,13 @@ function compoundWithOutros ( generator: DomGenerator, block: Block, state: Stat
${name}.${mountOrIntro}( ${parentNode}, ${anchor} ); ${name}.${mountOrIntro}( ${parentNode}, ${anchor} );
`; `;
const changeBlock = hasElse ? const changeBlock = hasElse
deindent` ? deindent`
${destroyOldBlock} ${destroyOldBlock}
${createNewBlock} ${createNewBlock}
` : `
deindent` : deindent`
if ( ${name} ) { if ( ${name} ) {
${destroyOldBlock} ${destroyOldBlock}
} }
@ -306,8 +380,8 @@ function compoundWithOutros ( generator: DomGenerator, block: Block, state: Stat
} }
`; `;
if ( dynamic ) { if (dynamic) {
block.builders.update.addBlock( deindent` block.builders.update.addBlock(deindent`
var ${previous_block_index} = ${current_block_index}; var ${previous_block_index} = ${current_block_index};
${current_block_index} = ${get_block}( state ); ${current_block_index} = ${get_block}( state );
if ( ${current_block_index} === ${previous_block_index} ) { if ( ${current_block_index} === ${previous_block_index} ) {
@ -315,21 +389,21 @@ function compoundWithOutros ( generator: DomGenerator, block: Block, state: Stat
} else { } else {
${changeBlock} ${changeBlock}
} }
` ); `);
} else { } else {
block.builders.update.addBlock( deindent` block.builders.update.addBlock(deindent`
var ${previous_block_index} = ${current_block_index}; var ${previous_block_index} = ${current_block_index};
${current_block_index} = ${get_block}( state ); ${current_block_index} = ${get_block}( state );
if ( ${current_block_index} !== ${previous_block_index} ) { if ( ${current_block_index} !== ${previous_block_index} ) {
${changeBlock} ${changeBlock}
} }
` ); `);
} }
block.builders.destroy.addLine( deindent` block.builders.destroy.addLine(deindent`
${if_current_block_index}{ ${if_current_block_index}{
${if_blocks}[ ${current_block_index} ].unmount(); ${if_blocks}[ ${current_block_index} ].unmount();
${if_blocks}[ ${current_block_index} ].destroy(); ${if_blocks}[ ${current_block_index} ].destroy();
} }
` ); `);
} }

@ -4,18 +4,28 @@ import Block from '../Block';
import { Node } from '../../../interfaces'; import { Node } from '../../../interfaces';
import { State } from '../interfaces'; import { State } from '../interfaces';
export default function visitMustacheTag ( generator: DomGenerator, block: Block, state: State, node: Node ) { export default function visitMustacheTag(
generator: DomGenerator,
block: Block,
state: State,
node: Node
) {
const name = node._state.name; const name = node._state.name;
const value = block.getUniqueName( `${name}_value` ); const value = block.getUniqueName(`${name}_value`);
const { snippet } = block.contextualise( node.expression ); const { snippet } = block.contextualise(node.expression);
block.addVariable( value ); block.addVariable(value);
block.addElement( name, `${generator.helper( 'createText' )}( ${value} = ${snippet} )`, state.parentNode, true ); block.addElement(
name,
`${generator.helper('createText')}( ${value} = ${snippet} )`,
state.parentNode,
true
);
block.builders.update.addBlock( deindent` block.builders.update.addBlock(deindent`
if ( ${value} !== ( ${value} = ${snippet} ) ) { if ( ${value} !== ( ${value} = ${snippet} ) ) {
${name}.data = ${value}; ${name}.data = ${value};
} }
` ); `);
} }

@ -4,37 +4,54 @@ import Block from '../Block';
import { Node } from '../../../interfaces'; import { Node } from '../../../interfaces';
import { State } from '../interfaces'; import { State } from '../interfaces';
export default function visitRawMustacheTag ( generator: DomGenerator, block: Block, state: State, node: Node ) { export default function visitRawMustacheTag(
generator: DomGenerator,
block: Block,
state: State,
node: Node
) {
const name = node._state.basename; const name = node._state.basename;
const before = node._state.name; const before = node._state.name;
const value = block.getUniqueName( `${name}_value` ); const value = block.getUniqueName(`${name}_value`);
const after = block.getUniqueName( `${name}_after` ); const after = block.getUniqueName(`${name}_after`);
const { snippet } = block.contextualise( 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.
block.addElement( before, `${generator.helper( 'createElement' )}( 'noscript' )`, state.parentNode, true ); block.addElement(
block.addElement( after, `${generator.helper( 'createElement' )}( 'noscript' )`, state.parentNode, true ); before,
`${generator.helper('createElement')}( 'noscript' )`,
state.parentNode,
true
);
block.addElement(
after,
`${generator.helper('createElement')}( 'noscript' )`,
state.parentNode,
true
);
const isToplevel = !state.parentNode; const isToplevel = !state.parentNode;
block.builders.create.addLine( `var ${value} = ${snippet};` ); block.builders.create.addLine(`var ${value} = ${snippet};`);
const mountStatement = `${before}.insertAdjacentHTML( 'afterend', ${value} );`; const mountStatement = `${before}.insertAdjacentHTML( 'afterend', ${value} );`;
const detachStatement = `${generator.helper( 'detachBetween' )}( ${before}, ${after} );`; const detachStatement = `${generator.helper(
'detachBetween'
)}( ${before}, ${after} );`;
if ( isToplevel ) { if (isToplevel) {
block.builders.mount.addLine( mountStatement ); block.builders.mount.addLine(mountStatement);
} else { } else {
block.builders.create.addLine( mountStatement ); block.builders.create.addLine(mountStatement);
} }
block.builders.update.addBlock( deindent` block.builders.update.addBlock(deindent`
if ( ${value} !== ( ${value} = ${snippet} ) ) { if ( ${value} !== ( ${value} = ${snippet} ) ) {
${detachStatement} ${detachStatement}
${mountStatement} ${mountStatement}
} }
` ); `);
block.builders.detachRaw.addBlock( detachStatement ); block.builders.detachRaw.addBlock(detachStatement);
} }

@ -3,7 +3,17 @@ import Block from '../Block';
import { Node } from '../../../interfaces'; import { Node } from '../../../interfaces';
import { State } from '../interfaces'; import { State } from '../interfaces';
export default function visitText ( generator: DomGenerator, block: Block, state: State, node: Node ) { export default function visitText(
if ( !node._state.shouldCreate ) return; generator: DomGenerator,
block.addElement( node._state.name, `${generator.helper( 'createText' )}( ${JSON.stringify( node.data )} )`, state.parentNode, node.usedAsAnchor ); block: Block,
} state: State,
node: Node
) {
if (!node._state.shouldCreate) return;
block.addElement(
node._state.name,
`${generator.helper('createText')}( ${JSON.stringify(node.data)} )`,
state.parentNode,
node.usedAsAnchor
);
}

@ -2,14 +2,18 @@ import { DomGenerator } from '../index';
import Block from '../Block'; import Block from '../Block';
import { State } from '../interfaces'; import { State } from '../interfaces';
export default function visitYieldTag ( generator: DomGenerator, block: Block, state: State ) { export default function visitYieldTag(
generator: DomGenerator,
block: Block,
state: State
) {
const parentNode = state.parentNode || block.target; const parentNode = state.parentNode || block.target;
( state.parentNode ? block.builders.create : block.builders.mount ).addLine( (state.parentNode ? block.builders.create : block.builders.mount).addLine(
`if ( ${block.component}._yield ) ${block.component}._yield.mount( ${parentNode}, null );` `if ( ${block.component}._yield ) ${block.component}._yield.mount( ${parentNode}, null );`
); );
block.builders.destroy.addLine( block.builders.destroy.addLine(
`if ( ${block.component}._yield ) ${block.component}._yield.unmount();` `if ( ${block.component}._yield ) ${block.component}._yield.unmount();`
); );
} }

@ -1,15 +1,25 @@
import deindent from '../../../../../utils/deindent.js'; import deindent from '../../../../../utils/deindent.js';
export default function getSetter ({ block, name, snippet, context, attribute, dependencies, value }) { export default function getSetter({
const tail = attribute.value.type === 'MemberExpression' ? getTailSnippet( attribute.value ) : ''; block,
name,
if ( block.contexts.has( name ) ) { snippet,
context,
attribute,
dependencies,
value
}) {
const tail = attribute.value.type === 'MemberExpression'
? getTailSnippet(attribute.value)
: '';
if (block.contexts.has(name)) {
const prop = dependencies[0]; const prop = dependencies[0];
const computed = isComputed( attribute.value ); const computed = isComputed(attribute.value);
return deindent` return deindent`
var list = this.${context}.${block.listNames.get( name )}; var list = this.${context}.${block.listNames.get(name)};
var index = this.${context}.${block.indexNames.get( name )}; var index = this.${context}.${block.indexNames.get(name)};
${computed && `var state = ${block.component}.get();`} ${computed && `var state = ${block.component}.get();`}
list[index]${tail} = ${value}; list[index]${tail} = ${value};
@ -17,8 +27,8 @@ export default function getSetter ({ block, name, snippet, context, attribute, d
`; `;
} }
if ( attribute.value.type === 'MemberExpression' ) { if (attribute.value.type === 'MemberExpression') {
const alias = block.alias( name ); const alias = block.alias(name);
return deindent` return deindent`
var state = ${block.component}.get(); var state = ${block.component}.get();
@ -30,19 +40,19 @@ export default function getSetter ({ block, name, snippet, context, attribute, d
return `${block.component}._set({ ${name}: ${value} });`; return `${block.component}._set({ ${name}: ${value} });`;
} }
function getTailSnippet ( node ) { function getTailSnippet(node) {
const end = node.end; const end = node.end;
while ( node.type === 'MemberExpression' ) node = node.object; while (node.type === 'MemberExpression') node = node.object;
const start = node.end; const start = node.end;
return `[✂${start}-${end}✂]`; return `[✂${start}-${end}✂]`;
} }
function isComputed ( node ) { function isComputed(node) {
while ( node.type === 'MemberExpression' ) { while (node.type === 'MemberExpression') {
if ( node.computed ) return true; if (node.computed) return true;
node = node.object; node = node.object;
} }
return false; return false;
} }

@ -15,33 +15,39 @@ export default class Block {
indexes: Map<string, string>; indexes: Map<string, string>;
contextDependencies: Map<string, string[]>; contextDependencies: Map<string, string[]>;
constructor ( options: BlockOptions ) { constructor(options: BlockOptions) {
Object.assign( this, options ); Object.assign(this, options);
} }
addBinding ( binding: Node, name: string ) { addBinding(binding: Node, name: string) {
const conditions = [ `!( '${binding.name}' in state )`].concat( // TODO handle contextual bindings... const conditions = [`!( '${binding.name}' in state )`].concat(
this.conditions.map( c => `(${c})` ) // TODO handle contextual bindings...
this.conditions.map(c => `(${c})`)
); );
const { keypath } = flattenReference( binding.value ); const { keypath } = flattenReference(binding.value);
this.generator.bindings.push( deindent` this.generator.bindings.push(deindent`
if ( ${conditions.join( '&&' )} ) { if ( ${conditions.join('&&')} ) {
tmp = ${name}.data(); tmp = ${name}.data();
if ( '${keypath}' in tmp ) { if ( '${keypath}' in tmp ) {
state.${binding.name} = tmp.${keypath}; state.${binding.name} = tmp.${keypath};
settled = false; settled = false;
} }
} }
` ); `);
} }
child ( options: BlockOptions ) { child(options: BlockOptions) {
return new Block( Object.assign( {}, this, options, { parent: this } ) ); return new Block(Object.assign({}, this, options, { parent: this }));
} }
contextualise ( expression: Node, context?: string, isEventHandler?: boolean ) { contextualise(expression: Node, context?: string, isEventHandler?: boolean) {
return this.generator.contextualise( this, expression, context, isEventHandler ); return this.generator.contextualise(
this,
expression,
context,
isEventHandler
);
} }
} }

@ -9,25 +9,34 @@ export class SsrGenerator extends Generator {
renderCode: string; renderCode: string;
elementDepth: number; elementDepth: number;
constructor ( parsed: Parsed, source: string, name: string, options: CompileOptions ) { constructor(
super( parsed, source, name, options ); parsed: Parsed,
source: string,
name: string,
options: CompileOptions
) {
super(parsed, source, name, options);
this.bindings = []; this.bindings = [];
this.renderCode = ''; this.renderCode = '';
this.elementDepth = 0; this.elementDepth = 0;
} }
append ( code: string ) { append(code: string) {
this.renderCode += code; this.renderCode += code;
} }
} }
export default function ssr ( parsed: Parsed, source: string, options: CompileOptions ) { export default function ssr(
parsed: Parsed,
source: string,
options: CompileOptions
) {
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, options ); const generator = new SsrGenerator(parsed, source, name, options);
const { computations, hasJs, templateProperties } = generator.parseJs( true ); const { computations, hasJs, templateProperties } = generator.parseJs(true);
// create main render() function // create main render() function
const mainBlock = new Block({ const mainBlock = new Block({
@ -37,8 +46,8 @@ export default function ssr ( parsed: Parsed, source: string, options: CompileOp
conditions: [] conditions: []
}); });
parsed.html.children.forEach( ( node: Node ) => { parsed.html.children.forEach((node: Node) => {
visit( generator, mainBlock, node ); visit(generator, mainBlock, node);
}); });
const result = deindent` const result = deindent`
@ -46,27 +55,37 @@ export default function ssr ( parsed: Parsed, source: string, options: CompileOp
var ${name} = {}; var ${name} = {};
${name}.filename = ${JSON.stringify( options.filename )}; ${name}.filename = ${JSON.stringify(options.filename)};
${name}.data = function () { ${name}.data = function () {
return ${templateProperties.data ? `${generator.alias( 'template' )}.data()` : `{}`}; return ${templateProperties.data
? `${generator.alias('template')}.data()`
: `{}`};
}; };
${name}.render = function ( state, options ) { ${name}.render = function ( state, options ) {
${templateProperties.data ? `state = Object.assign( ${generator.alias( 'template' )}.data(), state || {} );` : `state = state || {};`} ${templateProperties.data
? `state = Object.assign( ${generator.alias(
${computations.map( ({ key, deps }) => 'template'
`state.${key} = ${generator.alias( 'template' )}.computed.${key}( ${deps.map( dep => `state.${dep}` ).join( ', ' )} );` )}.data(), state || {} );`
: `state = state || {};`}
${computations.map(
({ key, deps }) =>
`state.${key} = ${generator.alias(
'template'
)}.computed.${key}( ${deps.map(dep => `state.${dep}`).join(', ')} );`
)} )}
${generator.bindings.length && deindent` ${generator.bindings.length &&
deindent`
var settled = false; var settled = false;
var tmp; var tmp;
while ( !settled ) { while ( !settled ) {
settled = true; settled = true;
${generator.bindings.join( '\n\n' )} ${generator.bindings.join('\n\n')}
} }
`} `}
@ -76,15 +95,17 @@ export default function ssr ( parsed: Parsed, source: string, options: CompileOp
${name}.renderCss = function () { ${name}.renderCss = function () {
var components = []; var components = [];
${generator.css && deindent` ${generator.css &&
deindent`
components.push({ components.push({
filename: ${name}.filename, filename: ${name}.filename,
css: ${JSON.stringify( generator.css )}, css: ${JSON.stringify(generator.css)},
map: null // TODO map: null // TODO
}); });
`} `}
${templateProperties.components && deindent` ${templateProperties.components &&
deindent`
var seen = {}; var seen = {};
function addComponent ( component ) { function addComponent ( component ) {
@ -96,13 +117,13 @@ export default function ssr ( parsed: Parsed, source: string, options: CompileOp
}); });
} }
${ ${templateProperties.components.value.properties.map(prop => {
templateProperties.components.value.properties.map( prop => { const { name } = prop.key;
const { name } = prop.key; const expression =
const expression = generator.importedComponents.get( name ) || `${generator.alias( 'template' )}.components.${name}`; generator.importedComponents.get(name) ||
return `addComponent( ${expression} );`; `${generator.alias('template')}.components.${name}`;
}) return `addComponent( ${expression} );`;
} })}
`} `}
return { return {
@ -125,5 +146,5 @@ export default function ssr ( parsed: Parsed, source: string, options: CompileOp
} }
`; `;
return generator.generate( result, options, { name, format } ); return generator.generate(result, options, { name, format });
} }

@ -3,7 +3,11 @@ import { SsrGenerator } from './index';
import Block from './Block'; import Block from './Block';
import { Node } from '../../interfaces'; import { Node } from '../../interfaces';
export default function visit ( generator: SsrGenerator, block: Block, node: Node ) { export default function visit(
const visitor = visitors[ node.type ]; generator: SsrGenerator,
visitor( generator, block, node ); block: Block,
} node: Node
) {
const visitor = visitors[node.type];
visitor(generator, block, node);
}

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

@ -4,11 +4,15 @@ import { SsrGenerator } from '../index';
import Block from '../Block'; import Block from '../Block';
import { Node } from '../../../interfaces'; import { Node } from '../../../interfaces';
export default function visitComponent ( generator: SsrGenerator, block: Block, node: Node ) { export default function visitComponent(
function stringify ( chunk: Node ) { generator: SsrGenerator,
if ( chunk.type === 'Text' ) return chunk.data; block: Block,
if ( chunk.type === 'MustacheTag' ) { node: Node
const { snippet } = block.contextualise( chunk.expression ); ) {
function stringify(chunk: Node) {
if (chunk.type === 'Text') return chunk.data;
if (chunk.type === 'MustacheTag') {
const { snippet } = block.contextualise(chunk.expression);
return '${__escape( ' + snippet + ')}'; return '${__escape( ' + snippet + ')}';
} }
} }
@ -16,65 +20,70 @@ export default function visitComponent ( generator: SsrGenerator, block: Block,
const attributes: Node[] = []; const attributes: Node[] = [];
const bindings: Node[] = []; const bindings: Node[] = [];
node.attributes.forEach( ( attribute: Node ) => { node.attributes.forEach((attribute: Node) => {
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 { } else {
const { snippet } = block.contextualise( chunk.expression ); const { snippet } = block.contextualise(chunk.expression);
value = snippet; value = snippet;
} }
} else { } else {
value = '`' + attribute.value.map( stringify ).join( '' ) + '`'; value = '`' + attribute.value.map(stringify).join('') + '`';
} }
return `${attribute.name}: ${value}`; return `${attribute.name}: ${value}`;
}) })
.concat( bindings.map( binding => { .concat(
const { name, keypath } = flattenReference( binding.value ); bindings.map(binding => {
const value = block.contexts.has( name ) ? keypath : `state.${keypath}`; const { name, keypath } = flattenReference(binding.value);
return `${binding.name}: ${value}`; const value = block.contexts.has(name) ? keypath : `state.${keypath}`;
})) 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 => {
block.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);
generator.elementDepth += 1; generator.elementDepth += 1;
node.children.forEach( ( child: Node ) => { node.children.forEach((child: Node) => {
visit( generator, block, child ); visit(generator, block, child);
}); });
generator.elementDepth -= 1; generator.elementDepth -= 1;
const close = node.children.length ? `\` })}` : ')}'; const close = node.children.length ? `\` })}` : ')}';
generator.append( close ); generator.append(close);
} }

@ -3,22 +3,28 @@ import { SsrGenerator } from '../index';
import Block from '../Block'; import Block from '../Block';
import { Node } from '../../../interfaces'; import { Node } from '../../../interfaces';
export default function visitEachBlock ( generator: SsrGenerator, block: Block, node: Node ) { export default function visitEachBlock(
const { dependencies, snippet } = block.contextualise( node.expression ); generator: SsrGenerator,
block: Block,
const open = `\${ ${snippet}.map( ${ node.index ? `( ${node.context}, ${node.index} )` : node.context} => \``; node: Node
generator.append( open ); ) {
const { dependencies, snippet } = block.contextualise(node.expression);
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 // TODO should this be the generator's job? It's duplicated between
// here and the equivalent DOM compiler visitor // here and the equivalent DOM compiler visitor
const contexts = new Map( block.contexts ); const contexts = new Map(block.contexts);
contexts.set( node.context, node.context ); contexts.set(node.context, node.context);
const indexes = new Map( block.indexes ); const indexes = new Map(block.indexes);
if ( node.index ) indexes.set( node.index, node.context ); if (node.index) indexes.set(node.index, 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({
contexts, contexts,
@ -26,10 +32,10 @@ export default function visitEachBlock ( generator: SsrGenerator, block: Block,
contextDependencies contextDependencies
}); });
node.children.forEach( ( child: Node ) => { node.children.forEach((child: Node) => {
visit( generator, childBlock, child ); visit(generator, childBlock, child);
}); });
const close = `\` ).join( '' )}`; const close = `\` ).join( '' )}`;
generator.append( close ); generator.append(close);
} }

@ -10,67 +10,73 @@ const meta = {
':Window': visitWindow ':Window': visitWindow
}; };
function stringifyAttributeValue ( block: Block, chunks: Node[] ) { function stringifyAttributeValue(block: Block, chunks: Node[]) {
return chunks.map( ( chunk: Node ) => { return chunks
if ( chunk.type === 'Text' ) { .map((chunk: Node) => {
return chunk.data; if (chunk.type === 'Text') {
} return chunk.data;
}
const { snippet } = block.contextualise( chunk.expression ); const { snippet } = block.contextualise(chunk.expression);
return '${' + snippet + '}'; return '${' + snippet + '}';
}).join( '' ) })
.join('');
} }
export default function visitElement ( generator: SsrGenerator, block: Block, node: Node ) { export default function visitElement(
if ( node.name in meta ) { generator: SsrGenerator,
return meta[ node.name ]( generator, block, node ); block: Block,
node: Node
) {
if (node.name in meta) {
return meta[node.name](generator, block, node);
} }
if ( generator.components.has( node.name ) || node.name === ':Self' ) { if (generator.components.has(node.name) || node.name === ':Self') {
visitComponent( generator, block, node ); visitComponent(generator, block, node);
return; return;
} }
let openingTag = `<${node.name}`; let openingTag = `<${node.name}`;
let textareaContents; // awkward special case let textareaContents; // awkward special case
node.attributes.forEach( ( attribute: Node ) => { node.attributes.forEach((attribute: Node) => {
if ( attribute.type !== 'Attribute' ) return; if (attribute.type !== 'Attribute') return;
if ( attribute.name === 'value' && node.name === 'textarea' ) { if (attribute.name === 'value' && node.name === 'textarea') {
textareaContents = stringifyAttributeValue( block, attribute.value ); textareaContents = stringifyAttributeValue(block, attribute.value);
} else { } else {
let str = ` ${attribute.name}`; let str = ` ${attribute.name}`;
if ( attribute.value !== true ) { if (attribute.value !== true) {
str += `="${stringifyAttributeValue( block, attribute.value )}"`; str += `="${stringifyAttributeValue(block, attribute.value)}"`;
} }
openingTag += str; openingTag += str;
} }
}); });
if ( generator.cssId && ( !generator.cascade || generator.elementDepth === 0 ) ) { if (generator.cssId && (!generator.cascade || generator.elementDepth === 0)) {
openingTag += ` ${generator.cssId}`; openingTag += ` ${generator.cssId}`;
} }
openingTag += '>'; openingTag += '>';
generator.append( openingTag ); generator.append(openingTag);
if ( node.name === 'textarea' && textareaContents !== undefined ) { if (node.name === 'textarea' && textareaContents !== undefined) {
generator.append( textareaContents ); generator.append(textareaContents);
} else { } else {
generator.elementDepth += 1; generator.elementDepth += 1;
node.children.forEach( ( child: Node ) => { node.children.forEach((child: Node) => {
visit( generator, block, child ); visit(generator, block, child);
}); });
generator.elementDepth -= 1; generator.elementDepth -= 1;
} }
if ( !isVoidElementName( node.name ) ) { if (!isVoidElementName(node.name)) {
generator.append( `</${node.name}>` ); generator.append(`</${node.name}>`);
} }
} }

@ -3,26 +3,30 @@ import { SsrGenerator } from '../index';
import Block from '../Block'; import Block from '../Block';
import { Node } from '../../../interfaces'; import { Node } from '../../../interfaces';
export default function visitIfBlock ( generator: SsrGenerator, block: Block, node: Node ) { export default function visitIfBlock(
const { snippet } = block.contextualise( node.expression ); generator: SsrGenerator,
block: Block,
node: Node
) {
const { snippet } = block.contextualise(node.expression);
generator.append( '${ ' + snippet + ' ? `' ); generator.append('${ ' + snippet + ' ? `');
const childBlock = block.child({ const childBlock = block.child({
conditions: block.conditions.concat( snippet ) conditions: block.conditions.concat(snippet)
}); });
node.children.forEach( ( child: Node ) => { node.children.forEach((child: Node) => {
visit( generator, childBlock, child ); visit(generator, childBlock, child);
}); });
generator.append( '` : `' ); generator.append('` : `');
if ( node.else ) { if (node.else) {
node.else.children.forEach( ( child: Node ) => { node.else.children.forEach((child: Node) => {
visit( generator, childBlock, child ); visit(generator, childBlock, child);
}); });
} }
generator.append( '` }' ); generator.append('` }');
} }

@ -2,7 +2,11 @@ import { SsrGenerator } from '../index';
import Block from '../Block'; import Block from '../Block';
import { Node } from '../../../interfaces'; import { Node } from '../../../interfaces';
export default function visitMustacheTag ( generator: SsrGenerator, block: Block, node: Node ) { export default function visitMustacheTag(
const { snippet } = block.contextualise( node.expression ); generator: SsrGenerator,
generator.append( '${__escape( ' + snippet + ' )}' ); block: Block,
} node: Node
) {
const { snippet } = block.contextualise(node.expression);
generator.append('${__escape( ' + snippet + ' )}');
}

@ -2,7 +2,11 @@ import { SsrGenerator } from '../index';
import Block from '../Block'; import Block from '../Block';
import { Node } from '../../../interfaces'; import { Node } from '../../../interfaces';
export default function visitRawMustacheTag ( generator: SsrGenerator, block: Block, node: Node ) { export default function visitRawMustacheTag(
const { snippet } = block.contextualise( node.expression ); generator: SsrGenerator,
generator.append( '${' + snippet + '}' ); block: Block,
} node: Node
) {
const { snippet } = block.contextualise(node.expression);
generator.append('${' + snippet + '}');
}

@ -2,6 +2,10 @@ import { SsrGenerator } from '../index';
import Block from '../Block'; import Block from '../Block';
import { Node } from '../../../interfaces'; import { Node } from '../../../interfaces';
export default function visitText ( generator: SsrGenerator, block: Block, node: Node ) { export default function visitText(
generator.append( node.data.replace( /\${/g, '\\${' ) ); generator: SsrGenerator,
} block: Block,
node: Node
) {
generator.append(node.data.replace(/\${/g, '\\${'));
}

@ -1,5 +1,5 @@
import { SsrGenerator } from '../index'; import { SsrGenerator } from '../index';
export default function visitYieldTag ( generator: SsrGenerator ) { export default function visitYieldTag(generator: SsrGenerator) {
generator.append( `\${options && options.yield ? options.yield() : ''}` ); generator.append(`\${options && options.yield ? options.yield() : ''}`);
} }

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

@ -3,7 +3,11 @@ import { Parsed, Node } from '../../interfaces';
const commentsPattern = /\/\*[\s\S]*?\*\//g; const commentsPattern = /\/\*[\s\S]*?\*\//g;
export default function processCss ( parsed: Parsed, code: MagicString, cascade: boolean ) { export default function processCss(
parsed: Parsed,
code: MagicString,
cascade: boolean
) {
const css = parsed.css.content.styles; const css = parsed.css.content.styles;
const offset = parsed.css.content.start; const offset = parsed.css.content.start;
@ -11,75 +15,73 @@ export default function processCss ( parsed: Parsed, code: MagicString, cascade:
const keyframes = new Map(); const keyframes = new Map();
function walkKeyframes ( node: Node ) { function walkKeyframes(node: Node) {
if ( node.type === 'Atrule' && node.name.toLowerCase() === 'keyframes' ) { if (node.type === 'Atrule' && node.name.toLowerCase() === 'keyframes') {
node.expression.children.forEach( ( expression: Node ) => { node.expression.children.forEach((expression: Node) => {
if ( expression.type === 'Identifier' ) { if (expression.type === 'Identifier') {
if ( expression.name.startsWith( '-global-' ) ) { if (expression.name.startsWith('-global-')) {
code.remove( expression.start, expression.start + 8 ); code.remove(expression.start, expression.start + 8);
} else { } else {
const newName = `svelte-${parsed.hash}-${expression.name}`; const newName = `svelte-${parsed.hash}-${expression.name}`;
code.overwrite( expression.start, expression.end, newName ); code.overwrite(expression.start, expression.end, newName);
keyframes.set( expression.name, newName ); keyframes.set(expression.name, newName);
} }
} }
}); });
} else if ( node.children ) { } else if (node.children) {
node.children.forEach( walkKeyframes ); node.children.forEach(walkKeyframes);
} else if ( node.block ) { } else if (node.block) {
walkKeyframes( node.block ); walkKeyframes(node.block);
} }
} }
parsed.css.children.forEach( walkKeyframes ); parsed.css.children.forEach(walkKeyframes);
function transform ( rule: Node ) { function transform(rule: Node) {
rule.selector.children.forEach( ( selector: Node ) => { rule.selector.children.forEach((selector: Node) => {
if ( cascade ) { if (cascade) {
// TODO disable cascading (without :global(...)) in v2 // TODO disable cascading (without :global(...)) in v2
const start = selector.start - offset; const start = selector.start - offset;
const end = selector.end - offset; const end = selector.end - offset;
const selectorString = css.slice( start, end ); const selectorString = css.slice(start, end);
const firstToken = selector.children[0]; const firstToken = selector.children[0];
let transformed; let transformed;
if ( firstToken.type === 'TypeSelector' ) { if (firstToken.type === 'TypeSelector') {
const insert = firstToken.end - offset; const insert = firstToken.end - offset;
const head = css.slice( start, insert ); const head = css.slice(start, insert);
const tail = css.slice( insert, end ); const tail = css.slice(insert, end);
transformed = `${head}${attr}${tail}, ${attr} ${selectorString}`; transformed = `${head}${attr}${tail}, ${attr} ${selectorString}`;
} else { } else {
transformed = `${attr}${selectorString}, ${attr} ${selectorString}`; transformed = `${attr}${selectorString}, ${attr} ${selectorString}`;
} }
code.overwrite( selector.start, selector.end, transformed ); code.overwrite(selector.start, selector.end, transformed);
} } else {
else {
let shouldTransform = true; let shouldTransform = true;
let c = selector.start; let c = selector.start;
selector.children.forEach( ( child: Node ) => { selector.children.forEach((child: Node) => {
if ( child.type === 'WhiteSpace' || child.type === 'Combinator' ) { if (child.type === 'WhiteSpace' || child.type === 'Combinator') {
code.appendLeft( c, attr ); code.appendLeft(c, attr);
shouldTransform = true; shouldTransform = true;
return; return;
} }
if ( !shouldTransform ) return; if (!shouldTransform) return;
if ( child.type === 'PseudoClassSelector' ) { if (child.type === 'PseudoClassSelector') {
// `:global(xyz)` > xyz // `:global(xyz)` > xyz
if ( child.name === 'global' ) { if (child.name === 'global') {
const first = child.children[0]; const first = child.children[0];
const last = child.children[child.children.length - 1]; const last = child.children[child.children.length - 1];
code.remove( child.start, first.start ).remove( last.end, child.end ); code.remove(child.start, first.start).remove(last.end, child.end);
} else { } else {
code.prependRight( c, attr ); code.prependRight(c, attr);
} }
shouldTransform = false; shouldTransform = false;
@ -88,21 +90,21 @@ export default function processCss ( parsed: Parsed, code: MagicString, cascade:
c = child.end; c = child.end;
}); });
if ( shouldTransform ) { if (shouldTransform) {
code.appendLeft( c, attr ); code.appendLeft(c, attr);
} }
} }
}); });
rule.block.children.forEach( ( block: Node ) => { rule.block.children.forEach((block: Node) => {
if ( block.type === 'Declaration' ) { if (block.type === 'Declaration') {
const property = block.property.toLowerCase(); const property = block.property.toLowerCase();
if ( property === 'animation' || property === 'animation-name' ) { if (property === 'animation' || property === 'animation-name') {
block.value.children.forEach( ( block: Node ) => { block.value.children.forEach((block: Node) => {
if ( block.type === 'Identifier' ) { if (block.type === 'Identifier') {
const name = block.name; const name = block.name;
if ( keyframes.has( name ) ) { if (keyframes.has(name)) {
code.overwrite( block.start, block.end, keyframes.get( name ) ); code.overwrite(block.start, block.end, keyframes.get(name));
} }
} }
}); });
@ -111,28 +113,31 @@ export default function processCss ( parsed: Parsed, code: MagicString, cascade:
}); });
} }
function walk ( node: Node ) { function walk(node: Node) {
if ( node.type === 'Rule' ) { if (node.type === 'Rule') {
transform( node ); transform(node);
} else if ( node.type === 'Atrule' && node.name.toLowerCase() === 'keyframes' ) { } else if (
node.type === 'Atrule' &&
node.name.toLowerCase() === 'keyframes'
) {
// these have already been processed // these have already been processed
} else if ( node.children ) { } else if (node.children) {
node.children.forEach( walk ); node.children.forEach(walk);
} else if ( node.block ) { } else if (node.block) {
walk( node.block ); walk(node.block);
} }
} }
parsed.css.children.forEach( walk ); parsed.css.children.forEach(walk);
// remove comments. TODO would be nice if this was exposed in css-tree // remove comments. TODO would be nice if this was exposed in css-tree
let match; let match;
while ( match = commentsPattern.exec( css ) ) { while ((match = commentsPattern.exec(css))) {
const start = match.index + offset; const start = match.index + offset;
const end = start + match[0].length; const end = start + match[0].length;
code.remove( start, end ); code.remove(start, end);
} }
return code.slice( parsed.css.content.start, parsed.css.content.end ); return code.slice(parsed.css.content.start, parsed.css.content.end);
} }

@ -1,33 +1,35 @@
import { Declaration, Options } from './getIntro'; import { Declaration, Options } from './getIntro';
export type Globals = (id: string) => any; export type Globals = (id: string) => any;
export default function getGlobals ( imports: Declaration[], options: Options ) { export default function getGlobals(imports: Declaration[], options: Options) {
const { globals, onerror, onwarn } = options; const { globals, onerror, onwarn } = options;
const globalFn = getGlobalFn( globals ); const globalFn = getGlobalFn(globals);
return imports.map( x => { return imports.map(x => {
let name = globalFn( x.source.value ); let name = globalFn(x.source.value);
if ( !name ) { if (!name) {
if ( x.name.startsWith( '__import' ) ) { if (x.name.startsWith('__import')) {
const error = new Error( `Could not determine name for imported module '${x.source.value}' use options.globals` ); const error = new Error(
if ( onerror ) { `Could not determine name for imported module '${x.source
onerror( error ); .value}' use options.globals`
);
if (onerror) {
onerror(error);
} else { } else {
throw error; throw error;
} }
} } else {
else {
const warning = { const warning = {
message: `No name was supplied for imported module '${x.source.value}'. Guessing '${x.name}', but you should use options.globals` message: `No name was supplied for imported module '${x.source
.value}'. Guessing '${x.name}', but you should use options.globals`
}; };
if ( onwarn ) { if (onwarn) {
onwarn( warning ); onwarn(warning);
} else { } else {
console.warn( warning ); // eslint-disable-line no-console console.warn(warning); // eslint-disable-line no-console
} }
} }
@ -38,10 +40,10 @@ export default function getGlobals ( imports: Declaration[], options: Options )
}); });
} }
function getGlobalFn ( globals: any ): Globals { function getGlobalFn(globals: any): Globals {
if ( typeof globals === 'function' ) return globals; if (typeof globals === 'function') return globals;
if ( typeof globals === 'object' ) { if (typeof globals === 'object') {
return id => globals[ id ]; return id => globals[id];
} }
return () => undefined; return () => undefined;

@ -1,7 +1,7 @@
import deindent from '../../../utils/deindent.js'; import deindent from '../../../utils/deindent.js';
import getGlobals, { Globals } from './getGlobals'; import getGlobals, { Globals } from './getGlobals';
export type ModuleFormat = "es" | "amd" | "cjs" | "iife" | "umd" | "eval"; export type ModuleFormat = 'es' | 'amd' | 'cjs' | 'iife' | 'umd' | 'eval';
export interface Options { export interface Options {
name: string; name: string;
@ -20,75 +20,96 @@ export interface Declaration {
}; };
} }
export default function getIntro ( format: ModuleFormat, options: Options, imports: Declaration[] ) { export default function getIntro(
if ( format === 'es' ) return ''; format: ModuleFormat,
if ( format === 'amd' ) return getAmdIntro( options, imports ); options: Options,
if ( format === 'cjs' ) return getCjsIntro( options, imports ); imports: Declaration[]
if ( format === 'iife' ) return getIifeIntro( options, imports ); ) {
if ( format === 'umd' ) return getUmdIntro( options, imports ); if (format === 'es') return '';
if ( format === 'eval' ) return getEvalIntro( options, imports ); if (format === 'amd') return getAmdIntro(options, imports);
if (format === 'cjs') return getCjsIntro(options, imports);
throw new Error( `Not implemented: ${format}` ); if (format === 'iife') return getIifeIntro(options, imports);
if (format === 'umd') return getUmdIntro(options, imports);
if (format === 'eval') return getEvalIntro(options, imports);
throw new Error(`Not implemented: ${format}`);
} }
function getAmdIntro ( options: Options, imports: Declaration[] ) { function getAmdIntro(options: Options, imports: Declaration[]) {
const sourceString = imports.length ? const sourceString = imports.length
`[ ${imports.map( declaration => `'${removeExtension( declaration.source.value )}'` ).join( ', ' )} ], ` : ? `[ ${imports
''; .map(declaration => `'${removeExtension(declaration.source.value)}'`)
.join(', ')} ], `
: '';
const id = options.amd && options.amd.id; const id = options.amd && options.amd.id;
return `define(${id ? ` '${id}', ` : ''}${sourceString}function (${paramString( imports )}) { 'use strict';\n\n`; return `define(${id
? ` '${id}', `
: ''}${sourceString}function (${paramString(imports)}) { 'use strict';\n\n`;
} }
function getCjsIntro ( options: Options, imports: Declaration[] ) { function getCjsIntro(options: Options, imports: Declaration[]) {
const requireBlock = imports const requireBlock = imports
.map( declaration => `var ${declaration.name} = require( '${declaration.source.value}' );` ) .map(
.join( '\n\n' ); declaration =>
`var ${declaration.name} = require( '${declaration.source.value}' );`
)
.join('\n\n');
if ( requireBlock ) { if (requireBlock) {
return `'use strict';\n\n${requireBlock}\n\n`; return `'use strict';\n\n${requireBlock}\n\n`;
} }
return `'use strict';\n\n`; return `'use strict';\n\n`;
} }
function getIifeIntro ( options: Options, imports: Declaration[] ) { function getIifeIntro(options: Options, imports: Declaration[]) {
if ( !options.name ) { if (!options.name) {
throw new Error( `Missing required 'name' option for IIFE export` ); throw new Error(`Missing required 'name' option for IIFE export`);
} }
return `var ${options.name} = (function (${paramString( imports )}) { 'use strict';\n\n`; return `var ${options.name} = (function (${paramString(
imports
)}) { 'use strict';\n\n`;
} }
function getUmdIntro ( options: Options, imports: Declaration[] ) { function getUmdIntro(options: Options, imports: Declaration[]) {
if ( !options.name ) { if (!options.name) {
throw new Error( `Missing required 'name' option for UMD export` ); throw new Error(`Missing required 'name' option for UMD export`);
} }
const amdId = options.amd && options.amd.id ? `'${options.amd.id}', ` : ''; const amdId = options.amd && options.amd.id ? `'${options.amd.id}', ` : '';
const amdDeps = imports.length ? `[${imports.map( declaration => `'${removeExtension( declaration.source.value )}'` ).join( ', ')}], ` : ''; const amdDeps = imports.length
const cjsDeps = imports.map( declaration => `require('${declaration.source.value}')` ).join( ', ' ); ? `[${imports
const globalDeps = getGlobals( imports, options ); .map(declaration => `'${removeExtension(declaration.source.value)}'`)
.join(', ')}], `
return deindent` : '';
const cjsDeps = imports
.map(declaration => `require('${declaration.source.value}')`)
.join(', ');
const globalDeps = getGlobals(imports, options);
return (
deindent`
(function ( global, factory ) { (function ( global, factory ) {
typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory(${cjsDeps}) : typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory(${cjsDeps}) :
typeof define === 'function' && define.amd ? define(${amdId}${amdDeps}factory) : typeof define === 'function' && define.amd ? define(${amdId}${amdDeps}factory) :
(global.${options.name} = factory(${globalDeps})); (global.${options.name} = factory(${globalDeps}));
}(this, (function (${paramString( imports )}) { 'use strict';` + '\n\n'; }(this, (function (${paramString(imports)}) { 'use strict';` + '\n\n'
);
} }
function getEvalIntro ( options: Options, imports: Declaration[] ) { function getEvalIntro(options: Options, imports: Declaration[]) {
return `(function (${paramString( imports )}) { 'use strict';\n\n`; return `(function (${paramString(imports)}) { 'use strict';\n\n`;
} }
function paramString ( imports: Declaration[] ) { function paramString(imports: Declaration[]) {
return imports.length ? ` ${imports.map( dep => dep.name ).join( ', ' )} ` : ''; return imports.length ? ` ${imports.map(dep => dep.name).join(', ')} ` : '';
} }
function removeExtension ( file: string ) { function removeExtension(file: string) {
const index = file.lastIndexOf( '.' ); const index = file.lastIndexOf('.');
return ~index ? file.slice( 0, index ) : file; return ~index ? file.slice(0, index) : file;
} }

@ -1,31 +1,36 @@
import getGlobals from './getGlobals'; import getGlobals from './getGlobals';
export default function getOutro ( format: string, name: string, options, imports ) { export default function getOutro(
if ( format === 'es' ) { format: string,
name: string,
options,
imports
) {
if (format === 'es') {
return `export default ${name};`; return `export default ${name};`;
} }
if ( format === 'amd' ) { if (format === 'amd') {
return `return ${name};\n\n});`; return `return ${name};\n\n});`;
} }
if ( format === 'cjs' ) { if (format === 'cjs') {
return `module.exports = ${name};`; return `module.exports = ${name};`;
} }
if ( format === 'iife' ) { if (format === 'iife') {
const globals = getGlobals( imports, options ); const globals = getGlobals(imports, options);
return `return ${name};\n\n}(${globals.join( ', ' )}));`; return `return ${name};\n\n}(${globals.join(', ')}));`;
} }
if ( format === 'eval' ) { if (format === 'eval') {
const globals = getGlobals( imports, options ); const globals = getGlobals(imports, options);
return `return ${name};\n\n}(${globals.join( ', ' )}));`; return `return ${name};\n\n}(${globals.join(', ')}));`;
} }
if ( format === 'umd' ) { if (format === 'umd') {
return `return ${name};\n\n})));`; return `return ${name};\n\n})));`;
} }
throw new Error( `Not implemented: ${format}` ); throw new Error(`Not implemented: ${format}`);
} }

@ -1,20 +1,20 @@
import { Node } from '../../../interfaces'; import { Node } from '../../../interfaces';
export default function walkHtml ( html: Node, visitors ) { export default function walkHtml(html: Node, visitors) {
function visit ( node: Node ) { function visit(node: Node) {
const visitor = visitors[ node.type ]; const visitor = visitors[node.type];
if ( !visitor ) throw new Error( `Not implemented: ${node.type}` ); if (!visitor) throw new Error(`Not implemented: ${node.type}`);
if ( visitor.enter ) visitor.enter( node ); if (visitor.enter) visitor.enter(node);
if ( node.children ) { if (node.children) {
node.children.forEach( ( child: Node ) => { node.children.forEach((child: Node) => {
visit( child ); visit(child);
}); });
} }
if ( visitor.leave ) visitor.leave( node ); if (visitor.leave) visitor.leave(node);
} }
visit( html ); visit(html);
} }

@ -6,62 +6,65 @@ import { assign } from './shared/index.js';
import { version } from '../package.json'; import { version } from '../package.json';
import { Parsed, CompileOptions, Warning } from './interfaces'; import { Parsed, CompileOptions, Warning } from './interfaces';
function normalizeOptions ( options: CompileOptions ) :CompileOptions { function normalizeOptions(options: CompileOptions): CompileOptions {
return assign({ return assign(
generate: 'dom', {
generate: 'dom',
// a filename is necessary for sourcemap generation // a filename is necessary for sourcemap generation
filename: 'SvelteComponent.html', filename: 'SvelteComponent.html',
onwarn: ( warning: Warning ) => { onwarn: (warning: Warning) => {
if ( warning.loc ) { if (warning.loc) {
console.warn( `(${warning.loc.line}:${warning.loc.column}) ${warning.message}` ); // eslint-disable-line no-console console.warn(
} else { `(${warning.loc.line}:${warning.loc.column}) ${warning.message}`
console.warn( warning.message ); // eslint-disable-line no-console ); // eslint-disable-line no-console
} else {
console.warn(warning.message); // eslint-disable-line no-console
}
},
onerror: (error: Error) => {
throw error;
} }
}, },
options
onerror: ( error: Error ) => { );
throw error;
}
}, options );
} }
export function compile ( source: string, _options: CompileOptions ) { export function compile(source: string, _options: CompileOptions) {
const options = normalizeOptions( _options ); const options = normalizeOptions(_options);
let parsed: Parsed; let parsed: Parsed;
try { try {
parsed = parse( source, options ); parsed = parse(source, options);
} catch ( err ) { } catch (err) {
options.onerror( err ); options.onerror(err);
return; return;
} }
validate( parsed, source, options ); validate(parsed, source, options);
const compiler = options.generate === 'ssr' const compiler = options.generate === 'ssr' ? generateSSR : generate;
? generateSSR
: generate;
return compiler( parsed, source, options ); return compiler(parsed, source, options);
} }
export function create ( source: string, _options: CompileOptions = {} ) { export function create(source: string, _options: CompileOptions = {}) {
_options.format = 'eval'; _options.format = 'eval';
const compiled = compile( source, _options ); const compiled = compile(source, _options);
if ( !compiled || !compiled.code ) { if (!compiled || !compiled.code) {
return; return;
} }
try { try {
return (new Function( 'return ' + compiled.code ))(); return new Function('return ' + compiled.code)();
} catch ( err ) { } catch (err) {
if ( _options.onerror ) { if (_options.onerror) {
_options.onerror( err ); _options.onerror(err);
return; return;
} else { } else {
throw err; throw err;

@ -26,10 +26,10 @@ export interface Parsed {
} }
export interface Warning { export interface Warning {
loc?: {line: number, column: number, pos: number}; loc?: { line: number; column: number; pos: number };
message: string message: string;
filename?: string filename?: string;
toString: () => string toString: () => string;
} }
export interface CompileOptions { export interface CompileOptions {
@ -42,6 +42,6 @@ export interface CompileOptions {
shared?: boolean | string; shared?: boolean | string;
cascade?: boolean; cascade?: boolean;
onerror?: (error: Error) => void onerror?: (error: Error) => void;
onwarn?: (warning: Warning) => void onwarn?: (warning: Warning) => void;
} }

@ -5,17 +5,22 @@ import { trimStart, trimEnd } from '../utils/trim';
import getCodeFrame from '../utils/getCodeFrame'; import getCodeFrame from '../utils/getCodeFrame';
import hash from './utils/hash'; import hash from './utils/hash';
import { Node, Parsed } from '../interfaces'; import { Node, Parsed } from '../interfaces';
import CompileError from '../utils/CompileError' import CompileError from '../utils/CompileError';
class ParseError extends CompileError { class ParseError extends CompileError {
constructor ( message: string, template: string, index: number, filename: string ) { constructor(
super( message, template, index, filename ); message: string,
template: string,
index: number,
filename: string
) {
super(message, template, index, filename);
this.name = 'ParseError'; this.name = 'ParseError';
} }
} }
interface ParserOptions { interface ParserOptions {
filename?: string filename?: string;
} }
export class Parser { export class Parser {
@ -28,14 +33,14 @@ export class Parser {
html: Node; html: Node;
css: Node; css: Node;
js: Node; js: Node;
metaTags: {} metaTags: {};
constructor ( template: string, options: ParserOptions ) { constructor(template: string, options: ParserOptions) {
if ( typeof template !== 'string' ) { if (typeof template !== 'string') {
throw new TypeError( 'Template must be a string' ); throw new TypeError('Template must be a string');
} }
this.template = template.replace( /\s+$/, '' ); this.template = template.replace(/\s+$/, '');
this.filename = options.filename; this.filename = options.filename;
this.index = 0; this.index = 0;
@ -52,36 +57,36 @@ export class Parser {
this.css = null; this.css = null;
this.js = null; this.js = null;
this.stack.push( this.html ); this.stack.push(this.html);
let state = fragment; let state = fragment;
while ( this.index < this.template.length ) { while (this.index < this.template.length) {
state = state( this ) || fragment; state = state(this) || fragment;
} }
if ( this.stack.length > 1 ) { if (this.stack.length > 1) {
const current = this.current(); const current = this.current();
const type = current.type === 'Element' ? `<${current.name}>` : 'Block'; const type = current.type === 'Element' ? `<${current.name}>` : 'Block';
this.error( `${type} was left open`, current.start ); this.error(`${type} was left open`, current.start);
} }
if ( state !== fragment ) { if (state !== fragment) {
this.error( 'Unexpected end of input' ); this.error('Unexpected end of input');
} }
// trim unnecessary whitespace // trim unnecessary whitespace
while ( this.html.children.length ) { while (this.html.children.length) {
const firstChild = this.html.children[0]; const firstChild = this.html.children[0];
this.html.start = firstChild.start; this.html.start = firstChild.start;
if ( firstChild.type !== 'Text' ) break; if (firstChild.type !== 'Text') break;
const length = firstChild.data.length; const length = firstChild.data.length;
firstChild.data = trimStart( firstChild.data ); firstChild.data = trimStart(firstChild.data);
if ( firstChild.data === '' ) { if (firstChild.data === '') {
this.html.children.shift(); this.html.children.shift();
} else { } else {
this.html.start += length - firstChild.data.length; this.html.start += length - firstChild.data.length;
@ -89,16 +94,16 @@ export class Parser {
} }
} }
while ( this.html.children.length ) { while (this.html.children.length) {
const lastChild = this.html.children[ this.html.children.length - 1 ]; const lastChild = this.html.children[this.html.children.length - 1];
this.html.end = lastChild.end; this.html.end = lastChild.end;
if ( lastChild.type !== 'Text' ) break; if (lastChild.type !== 'Text') break;
const length = lastChild.data.length; const length = lastChild.data.length;
lastChild.data = trimEnd( lastChild.data ); lastChild.data = trimEnd(lastChild.data);
if ( lastChild.data === '' ) { if (lastChild.data === '') {
this.html.children.pop(); this.html.children.pop();
} else { } else {
this.html.end -= length - lastChild.data.length; this.html.end -= length - lastChild.data.length;
@ -107,82 +112,89 @@ export class Parser {
} }
} }
current () { current() {
return this.stack[ this.stack.length - 1 ]; return this.stack[this.stack.length - 1];
} }
acornError ( err: Error ) { acornError(err: Error) {
this.error( err.message.replace( / \(\d+:\d+\)$/, '' ), err.pos ); this.error(err.message.replace(/ \(\d+:\d+\)$/, ''), err.pos);
} }
error ( message: string, index = this.index ) { error(message: string, index = this.index) {
throw new ParseError( message, this.template, index, this.filename ); throw new ParseError(message, this.template, index, this.filename);
} }
eat ( str: string, required?: boolean ) { eat(str: string, required?: boolean) {
if ( this.match( str ) ) { if (this.match(str)) {
this.index += str.length; this.index += str.length;
return true; return true;
} }
if ( required ) { if (required) {
this.error( `Expected ${str}` ); this.error(`Expected ${str}`);
} }
} }
match ( str: string ) { match(str: string) {
return this.template.slice( this.index, this.index + str.length ) === str; return this.template.slice(this.index, this.index + str.length) === str;
} }
allowWhitespace () { allowWhitespace() {
while ( this.index < this.template.length && whitespace.test( this.template[ this.index ] ) ) { while (
this.index < this.template.length &&
whitespace.test(this.template[this.index])
) {
this.index++; this.index++;
} }
} }
read ( pattern: RegExp ) { read(pattern: RegExp) {
const match = pattern.exec( this.template.slice( this.index ) ); const match = pattern.exec(this.template.slice(this.index));
if ( !match || match.index !== 0 ) return null; if (!match || match.index !== 0) return null;
this.index += match[0].length; this.index += match[0].length;
return match[0]; return match[0];
} }
readUntil ( pattern: RegExp ) { readUntil(pattern: RegExp) {
if ( this.index >= this.template.length ) this.error( 'Unexpected end of input' ); if (this.index >= this.template.length)
this.error('Unexpected end of input');
const start = this.index; const start = this.index;
const match = pattern.exec( this.template.slice( start ) ); const match = pattern.exec(this.template.slice(start));
if ( match ) { if (match) {
const start = this.index; const start = this.index;
this.index = start + match.index; this.index = start + match.index;
return this.template.slice( start, this.index ); return this.template.slice(start, this.index);
} }
this.index = this.template.length; this.index = this.template.length;
return this.template.slice( start ); return this.template.slice(start);
} }
remaining () { remaining() {
return this.template.slice( this.index ); return this.template.slice(this.index);
} }
requireWhitespace () { requireWhitespace() {
if ( !whitespace.test( this.template[ this.index ] ) ) { if (!whitespace.test(this.template[this.index])) {
this.error( `Expected whitespace` ); this.error(`Expected whitespace`);
} }
this.allowWhitespace(); this.allowWhitespace();
} }
} }
export default function parse ( template: string, options: ParserOptions = {} ) :Parsed { export default function parse(
const parser = new Parser( template, options ); template: string,
options: ParserOptions = {}
): Parsed {
const parser = new Parser(template, options);
return { return {
hash: hash( parser.template ), hash: hash(parser.template),
html: parser.html, html: parser.html,
css: parser.css, css: parser.css,
js: parser.js js: parser.js

@ -2,61 +2,57 @@ import { parseExpressionAt } from 'acorn';
import spaces from '../../utils/spaces.js'; import spaces from '../../utils/spaces.js';
import { Parser } from '../index'; import { Parser } from '../index';
function readExpression ( parser: Parser, start: number, quoteMark ) { function readExpression(parser: Parser, start: number, quoteMark) {
let str = ''; let str = '';
let escaped = false; let escaped = false;
for ( let i = start; i < parser.template.length; i += 1 ) { for (let i = start; i < parser.template.length; i += 1) {
const char = parser.template[i]; const char = parser.template[i];
if ( quoteMark ) { if (quoteMark) {
if ( char === quoteMark ) { if (char === quoteMark) {
if ( escaped ) { if (escaped) {
str += quoteMark; str += quoteMark;
} else { } else {
break; break;
} }
} else if ( escaped ) { } else if (escaped) {
str += '\\' + char; str += '\\' + char;
escaped = false; escaped = false;
} else if ( char === '\\' ) { } else if (char === '\\') {
escaped = true; escaped = true;
} else { } else {
str += char; str += char;
} }
} } else if (/\s/.test(char)) {
else if ( /\s/.test( char ) ) {
break; break;
} } else {
else {
str += char; str += char;
} }
} }
const expression = parseExpressionAt( spaces( start ) + str, start ); const expression = parseExpressionAt(spaces(start) + str, start);
parser.index = expression.end; parser.index = expression.end;
parser.allowWhitespace(); parser.allowWhitespace();
if ( quoteMark ) parser.eat( quoteMark, true ); if (quoteMark) parser.eat(quoteMark, true);
return expression; return expression;
} }
export function readEventHandlerDirective ( parser: Parser, start: number, name: string ) { export function readEventHandlerDirective(
const quoteMark = ( parser: Parser,
parser.eat( `'` ) ? `'` : start: number,
parser.eat( `"` ) ? `"` : name: string
null ) {
); const quoteMark = parser.eat(`'`) ? `'` : parser.eat(`"`) ? `"` : null;
const expressionStart = parser.index; const expressionStart = parser.index;
const expression = readExpression( parser, expressionStart, quoteMark ); const expression = readExpression(parser, expressionStart, quoteMark);
if ( expression.type !== 'CallExpression' ) { if (expression.type !== 'CallExpression') {
parser.error( `Expected call expression`, expressionStart ); parser.error(`Expected call expression`, expressionStart);
} }
return { return {
@ -68,49 +64,49 @@ export function readEventHandlerDirective ( parser: Parser, start: number, name:
}; };
} }
export function readBindingDirective ( parser: Parser, start: number, name: string ) { export function readBindingDirective(
parser: Parser,
start: number,
name: string
) {
let value; let value;
if ( parser.eat( '=' ) ) { if (parser.eat('=')) {
const quoteMark = ( const quoteMark = parser.eat(`'`) ? `'` : parser.eat(`"`) ? `"` : null;
parser.eat( `'` ) ? `'` :
parser.eat( `"` ) ? `"` :
null
);
const a = parser.index; const a = parser.index;
if ( parser.eat( '{{' ) ) { if (parser.eat('{{')) {
let message = 'bound values should not be wrapped'; let message = 'bound values should not be wrapped';
const b = parser.template.indexOf( '}}', a ); const b = parser.template.indexOf('}}', a);
if ( b !== -1 ) { if (b !== -1) {
const value = parser.template.slice( parser.index, b ); const value = parser.template.slice(parser.index, b);
message += ` — use '${value}', not '{{${value}}}'`; message += ` — use '${value}', not '{{${value}}}'`;
} }
parser.error( message, a ); parser.error(message, a);
} }
// this is a bit of a hack so that we can give Acorn something parseable // this is a bit of a hack so that we can give Acorn something parseable
let b; let b;
if ( quoteMark ) { if (quoteMark) {
b = parser.index = parser.template.indexOf( quoteMark, parser.index ); b = parser.index = parser.template.indexOf(quoteMark, parser.index);
} else { } else {
parser.readUntil( /[\s\r\n\/>]/ ); parser.readUntil(/[\s\r\n\/>]/);
b = parser.index; b = parser.index;
} }
const source = spaces( a ) + parser.template.slice( a, b ); const source = spaces(a) + parser.template.slice(a, b);
value = parseExpressionAt( source, a ); value = parseExpressionAt(source, a);
if ( value.type !== 'Identifier' && value.type !== 'MemberExpression' ) { if (value.type !== 'Identifier' && value.type !== 'MemberExpression') {
parser.error( `Cannot bind to rvalue`, value.start ); parser.error(`Cannot bind to rvalue`, value.start);
} }
parser.allowWhitespace(); parser.allowWhitespace();
if ( quoteMark ) { if (quoteMark) {
parser.eat( quoteMark, true ); parser.eat(quoteMark, true);
} }
} else { } else {
// shorthand bind:foo equivalent to bind:foo='foo' // shorthand bind:foo equivalent to bind:foo='foo'
@ -131,22 +127,23 @@ export function readBindingDirective ( parser: Parser, start: number, name: stri
}; };
} }
export function readTransitionDirective ( parser: Parser, start: number, name: string, type: string ) { export function readTransitionDirective(
parser: Parser,
start: number,
name: string,
type: string
) {
let expression = null; let expression = null;
if ( parser.eat( '=' ) ) { if (parser.eat('=')) {
const quoteMark = ( const quoteMark = parser.eat(`'`) ? `'` : parser.eat(`"`) ? `"` : null;
parser.eat( `'` ) ? `'` :
parser.eat( `"` ) ? `"` :
null
);
const expressionStart = parser.index; const expressionStart = parser.index;
expression = readExpression( parser, expressionStart, quoteMark ); expression = readExpression(parser, expressionStart, quoteMark);
if ( expression.type !== 'ObjectExpression' ) { if (expression.type !== 'ObjectExpression') {
parser.error( `Expected object expression`, expressionStart ); parser.error(`Expected object expression`, expressionStart);
} }
} }
@ -159,4 +156,4 @@ export function readTransitionDirective ( parser: Parser, start: number, name: s
outro: type === 'out' || type === 'transition', outro: type === 'out' || type === 'transition',
expression expression
}; };
} }

@ -1,25 +1,21 @@
import { parseExpressionAt } from 'acorn'; import { parseExpressionAt } from 'acorn';
import { Parser } from '../index'; import { Parser } from '../index';
const literals = new Map([ const literals = new Map([['true', true], ['false', false], ['null', null]]);
[ 'true', true ],
[ 'false', false ],
[ 'null', null ]
]);
export default function readExpression ( parser: Parser ) { export default function readExpression(parser: Parser) {
const start = parser.index; const start = parser.index;
const name = parser.readUntil( /\s*}}/ ); const name = parser.readUntil(/\s*}}/);
if ( name && /^[a-z]+$/.test( name ) ) { if (name && /^[a-z]+$/.test(name)) {
const end = start + name.length; const end = start + name.length;
if ( literals.has( name ) ) { if (literals.has(name)) {
return { return {
type: 'Literal', type: 'Literal',
start, start,
end, end,
value: literals.get( name ), value: literals.get(name),
raw: name raw: name
}; };
} }
@ -35,11 +31,13 @@ export default function readExpression ( parser: Parser ) {
parser.index = start; parser.index = start;
try { try {
const node = parseExpressionAt( parser.template, parser.index, { preserveParens: true } ); const node = parseExpressionAt(parser.template, parser.index, {
preserveParens: true
});
parser.index = node.end; parser.index = node.end;
return node; return node;
} catch ( err ) { } catch (err) {
parser.acornError( err ); parser.acornError(err);
} }
} }

@ -2,29 +2,30 @@ import { parse } from 'acorn';
import spaces from '../../utils/spaces.js'; import spaces from '../../utils/spaces.js';
import { Parser } from '../index'; import { Parser } from '../index';
const scriptClosingTag = '<\/script>'; const scriptClosingTag = '</script>';
export default function readScript ( parser: Parser, start: number, attributes ) { export default function readScript(parser: Parser, start: number, attributes) {
const scriptStart = parser.index; const scriptStart = parser.index;
const scriptEnd = parser.template.indexOf( scriptClosingTag, scriptStart ); const scriptEnd = parser.template.indexOf(scriptClosingTag, scriptStart);
if ( scriptEnd === -1 ) parser.error( `<script> must have a closing tag` ); if (scriptEnd === -1) parser.error(`<script> must have a closing tag`);
const source = spaces( scriptStart ) + parser.template.slice( scriptStart, scriptEnd ); const source =
spaces(scriptStart) + parser.template.slice(scriptStart, scriptEnd);
parser.index = scriptEnd + scriptClosingTag.length; parser.index = scriptEnd + scriptClosingTag.length;
let ast; let ast;
try { try {
ast = parse( source, { ast = parse(source, {
ecmaVersion: 8, ecmaVersion: 8,
sourceType: 'module' sourceType: 'module'
}); });
} catch ( err ) { } catch (err) {
parser.acornError( err ); parser.acornError(err);
} }
if ( !ast.body.length ) return null; if (!ast.body.length) return null;
ast.start = scriptStart; ast.start = scriptStart;
return { return {

@ -2,43 +2,43 @@ import parse from 'css-tree/lib/parser/index.js';
import walk from 'css-tree/lib/utils/walk.js'; import walk from 'css-tree/lib/utils/walk.js';
import { Parser } from '../index'; import { Parser } from '../index';
export default function readStyle ( parser: Parser, start: number, attributes ) { export default function readStyle(parser: Parser, start: number, attributes) {
const contentStart = parser.index; const contentStart = parser.index;
const styles = parser.readUntil( /<\/style>/ ); const styles = parser.readUntil(/<\/style>/);
const contentEnd = parser.index; const contentEnd = parser.index;
let ast; let ast;
try { try {
ast = parse( styles, { ast = parse(styles, {
positions: true, positions: true,
offset: contentStart offset: contentStart
}); });
} catch ( err ) { } catch (err) {
if ( err.name === 'CssSyntaxError' ) { if (err.name === 'CssSyntaxError') {
parser.error( err.message, err.offset ); parser.error(err.message, err.offset);
} else { } else {
throw err; throw err;
} }
} }
// tidy up AST // tidy up AST
walk.all( ast, node => { walk.all(ast, node => {
if ( node.loc ) { if (node.loc) {
node.start = node.loc.start.offset; node.start = node.loc.start.offset;
node.end = node.loc.end.offset; node.end = node.loc.end.offset;
delete node.loc; delete node.loc;
} }
}); });
parser.eat( '</style>', true ); parser.eat('</style>', true);
const end = parser.index; const end = parser.index;
return { return {
start, start,
end, end,
attributes, attributes,
children: JSON.parse( JSON.stringify( ast.children ) ), children: JSON.parse(JSON.stringify(ast.children)),
content: { content: {
start: contentStart, start: contentStart,
end: contentEnd, end: contentEnd,

@ -3,12 +3,12 @@ import mustache from './mustache';
import text from './text'; import text from './text';
import { Parser } from '../index'; import { Parser } from '../index';
export default function fragment ( parser: Parser ) { export default function fragment(parser: Parser) {
if ( parser.match( '<' ) ) { if (parser.match('<')) {
return tag; return tag;
} }
if ( parser.match( '{{' ) ) { if (parser.match('{{')) {
return mustache; return mustache;
} }

@ -6,92 +6,93 @@ import { Node } from '../../interfaces';
const validIdentifier = /[a-zA-Z_$][a-zA-Z0-9_$]*/; const validIdentifier = /[a-zA-Z_$][a-zA-Z0-9_$]*/;
function trimWhitespace ( block, trimBefore, trimAfter ) { function trimWhitespace(block, trimBefore, trimAfter) {
const firstChild = block.children[0]; const firstChild = block.children[0];
const lastChild = block.children[ block.children.length - 1 ]; const lastChild = block.children[block.children.length - 1];
if ( firstChild.type === 'Text' && trimBefore ) { if (firstChild.type === 'Text' && trimBefore) {
firstChild.data = trimStart( firstChild.data ); firstChild.data = trimStart(firstChild.data);
if ( !firstChild.data ) block.children.shift(); if (!firstChild.data) block.children.shift();
} }
if ( lastChild.type === 'Text' && trimAfter ) { if (lastChild.type === 'Text' && trimAfter) {
lastChild.data = trimEnd( lastChild.data ); lastChild.data = trimEnd(lastChild.data);
if ( !lastChild.data ) block.children.pop(); if (!lastChild.data) block.children.pop();
} }
if ( block.else ) { if (block.else) {
trimWhitespace( block.else, trimBefore, trimAfter ); trimWhitespace(block.else, trimBefore, trimAfter);
} }
if ( firstChild.elseif ) { if (firstChild.elseif) {
trimWhitespace( firstChild, trimBefore, trimAfter ); trimWhitespace(firstChild, trimBefore, trimAfter);
} }
} }
export default function mustache ( parser: Parser ) { export default function mustache(parser: Parser) {
const start = parser.index; const start = parser.index;
parser.index += 2; parser.index += 2;
parser.allowWhitespace(); parser.allowWhitespace();
// {{/if}} or {{/each}} // {{/if}} or {{/each}}
if ( parser.eat( '/' ) ) { if (parser.eat('/')) {
let block = parser.current(); let block = parser.current();
let expected; let expected;
if ( block.type === 'ElseBlock' ) { if (block.type === 'ElseBlock') {
block.end = start; block.end = start;
parser.stack.pop(); parser.stack.pop();
block = parser.current(); block = parser.current();
} }
if ( block.type === 'IfBlock' ) { if (block.type === 'IfBlock') {
expected = 'if'; expected = 'if';
} else if ( block.type === 'EachBlock' ) { } else if (block.type === 'EachBlock') {
expected = 'each'; expected = 'each';
} else { } else {
parser.error( `Unexpected block closing tag` ); parser.error(`Unexpected block closing tag`);
} }
parser.eat( expected, true ); parser.eat(expected, true);
parser.allowWhitespace(); parser.allowWhitespace();
parser.eat( '}}', true ); parser.eat('}}', true);
while ( block.elseif ) { while (block.elseif) {
block.end = parser.index; block.end = parser.index;
parser.stack.pop(); parser.stack.pop();
block = parser.current(); block = parser.current();
if ( block.else ) { if (block.else) {
block.else.end = start; block.else.end = start;
} }
} }
// strip leading/trailing whitespace as necessary // strip leading/trailing whitespace as necessary
if ( !block.children.length ) parser.error( `Empty block`, block.start ); if (!block.children.length) parser.error(`Empty block`, block.start);
const charBefore = parser.template[ block.start - 1 ]; const charBefore = parser.template[block.start - 1];
const charAfter = parser.template[ parser.index ]; const charAfter = parser.template[parser.index];
const trimBefore = !charBefore || whitespace.test( charBefore ); const trimBefore = !charBefore || whitespace.test(charBefore);
const trimAfter = !charAfter || whitespace.test( charAfter ); const trimAfter = !charAfter || whitespace.test(charAfter);
trimWhitespace( block, trimBefore, trimAfter ); trimWhitespace(block, trimBefore, trimAfter);
block.end = parser.index; block.end = parser.index;
parser.stack.pop(); parser.stack.pop();
} } else if (parser.eat('elseif')) {
else if ( parser.eat( 'elseif' ) ) {
const block = parser.current(); const block = parser.current();
if ( block.type !== 'IfBlock' ) parser.error( 'Cannot have an {{elseif ...}} block outside an {{#if ...}} block' ); if (block.type !== 'IfBlock')
parser.error(
'Cannot have an {{elseif ...}} block outside an {{#if ...}} block'
);
parser.requireWhitespace(); parser.requireWhitespace();
const expression = readExpression( parser ); const expression = readExpression(parser);
parser.allowWhitespace(); parser.allowWhitespace();
parser.eat( '}}', true ); parser.eat('}}', true);
block.else = { block.else = {
start: parser.index, start: parser.index,
@ -109,17 +110,17 @@ export default function mustache ( parser: Parser ) {
] ]
}; };
parser.stack.push( block.else.children[0] ); parser.stack.push(block.else.children[0]);
} } else if (parser.eat('else')) {
else if ( parser.eat( 'else' ) ) {
const block = parser.current(); const block = parser.current();
if ( block.type !== 'IfBlock' && block.type !== 'EachBlock' ) { if (block.type !== 'IfBlock' && block.type !== 'EachBlock') {
parser.error( 'Cannot have an {{else}} block outside an {{#if ...}} or {{#each ...}} block' ); parser.error(
'Cannot have an {{else}} block outside an {{#if ...}} or {{#each ...}} block'
);
} }
parser.allowWhitespace(); parser.allowWhitespace();
parser.eat( '}}', true ); parser.eat('}}', true);
block.else = { block.else = {
start: parser.index, start: parser.index,
@ -128,24 +129,22 @@ export default function mustache ( parser: Parser ) {
children: [] children: []
}; };
parser.stack.push( block.else ); parser.stack.push(block.else);
} } else if (parser.eat('#')) {
// {{#if foo}} or {{#each foo}}
// {{#if foo}} or {{#each foo}}
else if ( parser.eat( '#' ) ) {
let type; let type;
if ( parser.eat( 'if' ) ) { if (parser.eat('if')) {
type = 'IfBlock'; type = 'IfBlock';
} else if ( parser.eat( 'each' ) ) { } else if (parser.eat('each')) {
type = 'EachBlock'; type = 'EachBlock';
} else { } else {
parser.error( `Expected if or each` ); parser.error(`Expected if or each`);
} }
parser.requireWhitespace(); parser.requireWhitespace();
const expression = readExpression( parser ); const expression = readExpression(parser);
const block: Node = { const block: Node = {
start, start,
@ -158,54 +157,49 @@ export default function mustache ( parser: Parser ) {
parser.allowWhitespace(); parser.allowWhitespace();
// {{#each}} blocks must declare a context {{#each list as item}} // {{#each}} blocks must declare a context {{#each list as item}}
if ( type === 'EachBlock' ) { if (type === 'EachBlock') {
parser.eat( 'as', true ); parser.eat('as', true);
parser.requireWhitespace(); parser.requireWhitespace();
block.context = parser.read( validIdentifier ); // TODO check it's not a keyword block.context = parser.read(validIdentifier); // TODO check it's not a keyword
if ( !block.context ) parser.error( `Expected name` ); if (!block.context) parser.error(`Expected name`);
parser.allowWhitespace(); parser.allowWhitespace();
if ( parser.eat( ',' ) ) { if (parser.eat(',')) {
parser.allowWhitespace(); parser.allowWhitespace();
block.index = parser.read( validIdentifier ); block.index = parser.read(validIdentifier);
if ( !block.index ) parser.error( `Expected name` ); if (!block.index) parser.error(`Expected name`);
parser.allowWhitespace(); parser.allowWhitespace();
} }
if ( parser.eat( '@' ) ) { if (parser.eat('@')) {
block.key = parser.read( validIdentifier ); block.key = parser.read(validIdentifier);
if ( !block.key ) parser.error( `Expected name` ); if (!block.key) parser.error(`Expected name`);
parser.allowWhitespace(); parser.allowWhitespace();
} }
} }
parser.eat( '}}', true ); parser.eat('}}', true);
parser.current().children.push( block );
parser.stack.push( block );
}
// {{yield}}
else if ( parser.eat( 'yield' ) ) {
parser.current().children.push(block);
parser.stack.push(block);
} else if (parser.eat('yield')) {
// {{yield}}
parser.allowWhitespace(); parser.allowWhitespace();
parser.eat( '}}', true ); parser.eat('}}', true);
parser.current().children.push({ parser.current().children.push({
start, start,
end: parser.index, end: parser.index,
type: 'YieldTag' type: 'YieldTag'
}); });
} } else if (parser.eat('{')) {
// {{{raw}}} mustache
// {{{raw}}} mustache const expression = readExpression(parser);
else if ( parser.eat( '{' ) ) {
const expression = readExpression( parser );
parser.allowWhitespace(); parser.allowWhitespace();
parser.eat( '}}}', true ); parser.eat('}}}', true);
parser.current().children.push({ parser.current().children.push({
start, start,
@ -213,13 +207,11 @@ export default function mustache ( parser: Parser ) {
type: 'RawMustacheTag', type: 'RawMustacheTag',
expression expression
}); });
} } else {
const expression = readExpression(parser);
else {
const expression = readExpression( parser );
parser.allowWhitespace(); parser.allowWhitespace();
parser.eat( '}}', true ); parser.eat('}}', true);
parser.current().children.push({ parser.current().children.push({
start, start,

@ -1,7 +1,11 @@
import readExpression from '../read/expression'; import readExpression from '../read/expression';
import readScript from '../read/script'; import readScript from '../read/script';
import readStyle from '../read/style'; import readStyle from '../read/style';
import { readEventHandlerDirective, readBindingDirective, readTransitionDirective } from '../read/directives'; import {
readEventHandlerDirective,
readBindingDirective,
readTransitionDirective
} from '../read/directives';
import { trimStart, trimEnd } from '../../utils/trim'; import { trimStart, trimEnd } from '../../utils/trim';
import { decodeCharacterReferences } from '../utils/html'; import { decodeCharacterReferences } from '../utils/html';
import isVoidElementName from '../../utils/isVoidElementName'; import isVoidElementName from '../../utils/isVoidElementName';
@ -16,60 +20,73 @@ const metaTags = {
':Window': true ':Window': true
}; };
const specials = new Map( [ const specials = new Map([
[ 'script', { [
read: readScript, 'script',
property: 'js' {
} ], read: readScript,
[ 'style', { property: 'js'
read: readStyle, }
property: 'css' ],
} ] [
] ); 'style',
{
read: readStyle,
property: 'css'
}
]
]);
// based on http://developers.whatwg.org/syntax.html#syntax-tag-omission // based on http://developers.whatwg.org/syntax.html#syntax-tag-omission
const disallowedContents = new Map( [ const disallowedContents = new Map([
[ 'li', new Set( [ 'li' ] ) ], ['li', new Set(['li'])],
[ 'dt', new Set( [ 'dt', 'dd' ] ) ], ['dt', new Set(['dt', 'dd'])],
[ 'dd', new Set( [ 'dt', 'dd' ] ) ], ['dd', new Set(['dt', 'dd'])],
[ 'p', new Set( 'address article aside blockquote div dl fieldset footer form h1 h2 h3 h4 h5 h6 header hgroup hr main menu nav ol p pre section table ul'.split( ' ' ) ) ], [
[ 'rt', new Set( [ 'rt', 'rp' ] ) ], 'p',
[ 'rp', new Set( [ 'rt', 'rp' ] ) ], new Set(
[ 'optgroup', new Set( [ 'optgroup' ] ) ], 'address article aside blockquote div dl fieldset footer form h1 h2 h3 h4 h5 h6 header hgroup hr main menu nav ol p pre section table ul'.split(
[ 'option', new Set( [ 'option', 'optgroup' ] ) ], ' '
[ 'thead', new Set( [ 'tbody', 'tfoot' ] ) ], )
[ 'tbody', new Set( [ 'tbody', 'tfoot' ] ) ], )
[ 'tfoot', new Set( [ 'tbody' ] ) ], ],
[ 'tr', new Set( [ 'tr', 'tbody' ] ) ], ['rt', new Set(['rt', 'rp'])],
[ 'td', new Set( [ 'td', 'th', 'tr' ] ) ], ['rp', new Set(['rt', 'rp'])],
[ 'th', new Set( [ 'td', 'th', 'tr' ] ) ], ['optgroup', new Set(['optgroup'])],
] ); ['option', new Set(['option', 'optgroup'])],
['thead', new Set(['tbody', 'tfoot'])],
function stripWhitespace ( element ) { ['tbody', new Set(['tbody', 'tfoot'])],
if ( element.children.length ) { ['tfoot', new Set(['tbody'])],
['tr', new Set(['tr', 'tbody'])],
['td', new Set(['td', 'th', 'tr'])],
['th', new Set(['td', 'th', 'tr'])]
]);
function stripWhitespace(element) {
if (element.children.length) {
const firstChild = element.children[0]; const firstChild = element.children[0];
const lastChild = element.children[ element.children.length - 1 ]; const lastChild = element.children[element.children.length - 1];
if ( firstChild.type === 'Text' ) { if (firstChild.type === 'Text') {
firstChild.data = trimStart( firstChild.data ); firstChild.data = trimStart(firstChild.data);
if ( !firstChild.data ) element.children.shift(); if (!firstChild.data) element.children.shift();
} }
if ( lastChild.type === 'Text' ) { if (lastChild.type === 'Text') {
lastChild.data = trimEnd( lastChild.data ); lastChild.data = trimEnd(lastChild.data);
if ( !lastChild.data ) element.children.pop(); if (!lastChild.data) element.children.pop();
} }
} }
} }
export default function tag ( parser: Parser ) { export default function tag(parser: Parser) {
const start = parser.index++; const start = parser.index++;
let parent = parser.current(); let parent = parser.current();
if ( parser.eat( '!--' ) ) { if (parser.eat('!--')) {
const data = parser.readUntil( /-->/ ); const data = parser.readUntil(/-->/);
parser.eat( '-->' ); parser.eat('-->');
parser.current().children.push({ parser.current().children.push({
start, start,
@ -81,38 +98,48 @@ export default function tag ( parser: Parser ) {
return null; return null;
} }
const isClosingTag = parser.eat( '/' ); const isClosingTag = parser.eat('/');
const name = readTagName( parser ); const name = readTagName(parser);
if ( name in metaTags ) { if (name in metaTags) {
if ( name in parser.metaTags ) { if (name in parser.metaTags) {
if ( isClosingTag && parser.current().children.length ) { if (isClosingTag && parser.current().children.length) {
parser.error( `<${name}> cannot have children`, parser.current().children[0].start ); parser.error(
`<${name}> cannot have children`,
parser.current().children[0].start
);
} }
parser.error( `A component can only have one <${name}> tag`, start ); parser.error(`A component can only have one <${name}> tag`, start);
} }
parser.metaTags[ name ] = true; parser.metaTags[name] = true;
if ( parser.stack.length > 1 ) { if (parser.stack.length > 1) {
parser.error( `<${name}> tags cannot be inside elements or blocks`, start ); parser.error(`<${name}> tags cannot be inside elements or blocks`, start);
} }
} }
parser.allowWhitespace(); parser.allowWhitespace();
if ( isClosingTag ) { if (isClosingTag) {
if ( isVoidElementName( name ) ) { if (isVoidElementName(name)) {
parser.error( `<${name}> is a void element and cannot have children, or a closing tag`, start ); parser.error(
`<${name}> is a void element and cannot have children, or a closing tag`,
start
);
} }
if ( !parser.eat( '>' ) ) parser.error( `Expected '>'` ); if (!parser.eat('>')) parser.error(`Expected '>'`);
// close any elements that don't have their own closing tags, e.g. <div><p></div> // close any elements that don't have their own closing tags, e.g. <div><p></div>
while ( parent.name !== name ) { while (parent.name !== name) {
if ( parent.type !== 'Element' ) parser.error( `</${name}> attempted to close an element that was not open`, start ); if (parent.type !== 'Element')
parser.error(
`</${name}> attempted to close an element that was not open`,
start
);
parent.end = start; parent.end = start;
parser.stack.pop(); parser.stack.pop();
@ -121,17 +148,17 @@ export default function tag ( parser: Parser ) {
} }
// strip leading/trailing whitespace as necessary // strip leading/trailing whitespace as necessary
stripWhitespace( parent ); stripWhitespace(parent);
parent.end = parser.index; parent.end = parser.index;
parser.stack.pop(); parser.stack.pop();
return null; return null;
} else if ( disallowedContents.has( parent.name ) ) { } else if (disallowedContents.has(parent.name)) {
// can this be a child of the parent element, or does it implicitly // can this be a child of the parent element, or does it implicitly
// close it, like `<li>one<li>two`? // close it, like `<li>one<li>two`?
if ( disallowedContents.get( parent.name ).has( name ) ) { if (disallowedContents.get(parent.name).has(name)) {
stripWhitespace( parent ); stripWhitespace(parent);
parent.end = start; parent.end = start;
parser.stack.pop(); parser.stack.pop();
@ -142,24 +169,26 @@ export default function tag ( parser: Parser ) {
const uniqueNames = new Set(); const uniqueNames = new Set();
let attribute; let attribute;
while ( attribute = readAttribute( parser, uniqueNames ) ) { while ((attribute = readAttribute(parser, uniqueNames))) {
attributes.push( attribute ); attributes.push(attribute);
parser.allowWhitespace(); parser.allowWhitespace();
} }
parser.allowWhitespace(); parser.allowWhitespace();
// special cases top-level <script> and <style> // special cases top-level <script> and <style>
if ( specials.has( name ) && parser.stack.length === 1 ) { if (specials.has(name) && parser.stack.length === 1) {
const special = specials.get( name ); const special = specials.get(name);
if ( parser[ special.property ] ) { if (parser[special.property]) {
parser.index = start; parser.index = start;
parser.error( `You can only have one top-level <${name}> tag per component` ); parser.error(
`You can only have one top-level <${name}> tag per component`
);
} }
parser.eat( '>', true ); parser.eat('>', true);
parser[ special.property ] = special.read( parser, start, attributes ); parser[special.property] = special.read(parser, start, attributes);
return; return;
} }
@ -172,106 +201,118 @@ export default function tag ( parser: Parser ) {
children: [] children: []
}; };
parser.current().children.push( element ); parser.current().children.push(element);
const selfClosing = parser.eat( '/' ) || isVoidElementName( name ); const selfClosing = parser.eat('/') || isVoidElementName(name);
parser.eat( '>', true ); parser.eat('>', true);
if ( selfClosing ) { if (selfClosing) {
element.end = parser.index; element.end = parser.index;
} else if ( name === 'textarea' ) { } else if (name === 'textarea') {
// special case // special case
element.children = readSequence( parser, () => parser.template.slice( parser.index, parser.index + 11 ) === '</textarea>' ); element.children = readSequence(
parser.read( /<\/textarea>/ ); parser,
() =>
parser.template.slice(parser.index, parser.index + 11) === '</textarea>'
);
parser.read(/<\/textarea>/);
element.end = parser.index; element.end = parser.index;
} else { } else {
// don't push self-closing elements onto the stack // don't push self-closing elements onto the stack
parser.stack.push( element ); parser.stack.push(element);
} }
return null; return null;
} }
function readTagName ( parser: Parser ) { function readTagName(parser: Parser) {
const start = parser.index; const start = parser.index;
if ( parser.eat( SELF ) ) { if (parser.eat(SELF)) {
// check we're inside a block, otherwise this // check we're inside a block, otherwise this
// will cause infinite recursion // will cause infinite recursion
let i = parser.stack.length; let i = parser.stack.length;
let legal = false; let legal = false;
while ( i-- ) { while (i--) {
const fragment = parser.stack[i]; const fragment = parser.stack[i];
if ( fragment.type === 'IfBlock' || fragment.type === 'EachBlock' ) { if (fragment.type === 'IfBlock' || fragment.type === 'EachBlock') {
legal = true; legal = true;
break; break;
} }
} }
if ( !legal ) { if (!legal) {
parser.error( `<${SELF}> components can only exist inside if-blocks or each-blocks`, start ); parser.error(
`<${SELF}> components can only exist inside if-blocks or each-blocks`,
start
);
} }
return SELF; return SELF;
} }
const name = parser.readUntil( /(\s|\/|>)/ ); const name = parser.readUntil(/(\s|\/|>)/);
if ( name in metaTags ) return name; if (name in metaTags) return name;
if ( !validTagName.test( name ) ) { if (!validTagName.test(name)) {
parser.error( `Expected valid tag name`, start ); parser.error(`Expected valid tag name`, start);
} }
return name; return name;
} }
function readAttribute ( parser: Parser, uniqueNames ) { function readAttribute(parser: Parser, uniqueNames) {
const start = parser.index; const start = parser.index;
let name = parser.readUntil( /(\s|=|\/|>)/ ); let name = parser.readUntil(/(\s|=|\/|>)/);
if ( !name ) return null; if (!name) return null;
if ( uniqueNames.has( name ) ) { if (uniqueNames.has(name)) {
parser.error( 'Attributes need to be unique', start ); parser.error('Attributes need to be unique', start);
} }
uniqueNames.add( name ); uniqueNames.add(name);
parser.allowWhitespace(); parser.allowWhitespace();
if ( /^on:/.test( name ) ) { if (/^on:/.test(name)) {
parser.eat( '=', true ); parser.eat('=', true);
return readEventHandlerDirective( parser, start, name.slice( 3 ) ); return readEventHandlerDirective(parser, start, name.slice(3));
} }
if ( /^bind:/.test( name ) ) { if (/^bind:/.test(name)) {
return readBindingDirective( parser, start, name.slice( 5 ) ); return readBindingDirective(parser, start, name.slice(5));
} }
if ( /^ref:/.test( name ) ) { if (/^ref:/.test(name)) {
return { return {
start, start,
end: parser.index, end: parser.index,
type: 'Ref', type: 'Ref',
name: name.slice( 4 ) name: name.slice(4)
}; };
} }
const match = /^(in|out|transition):/.exec( name ); const match = /^(in|out|transition):/.exec(name);
if ( match ) { if (match) {
return readTransitionDirective( parser, start, name.slice( match[0].length ), match[1] ); return readTransitionDirective(
parser,
start,
name.slice(match[0].length),
match[1]
);
} }
let value; let value;
// :foo is shorthand for foo='{{foo}}' // :foo is shorthand for foo='{{foo}}'
if ( /^:\w+$/.test( name ) ) { if (/^:\w+$/.test(name)) {
name = name.slice( 1 ); name = name.slice(1);
value = getShorthandValue( start + 1, name ); value = getShorthandValue(start + 1, name);
} else { } else {
value = parser.eat( '=' ) ? readAttributeValue( parser ) : true; value = parser.eat('=') ? readAttributeValue(parser) : true;
} }
return { return {
@ -283,42 +324,40 @@ function readAttribute ( parser: Parser, uniqueNames ) {
}; };
} }
function readAttributeValue ( parser: Parser ) { function readAttributeValue(parser: Parser) {
const quoteMark = ( const quoteMark = parser.eat(`'`) ? `'` : parser.eat(`"`) ? `"` : null;
parser.eat( `'` ) ? `'` :
parser.eat( `"` ) ? `"` :
null
);
const regex = ( const regex = quoteMark === `'`
quoteMark === `'` ? /'/ : ? /'/
quoteMark === `"` ? /"/ : : quoteMark === `"` ? /"/ : /[\s"'=<>\/`]/;
/[\s"'=<>\/`]/
);
const value = readSequence( parser, () => regex.test( parser.template[ parser.index ] ) ); const value = readSequence(parser, () =>
regex.test(parser.template[parser.index])
);
if ( quoteMark ) parser.index += 1; if (quoteMark) parser.index += 1;
return value; return value;
} }
function getShorthandValue ( start: number, name: string ) { function getShorthandValue(start: number, name: string) {
const end = start + name.length; const end = start + name.length;
return [{ return [
type: 'AttributeShorthand', {
start, type: 'AttributeShorthand',
end,
expression: {
type: 'Identifier',
start, start,
end, end,
name expression: {
type: 'Identifier',
start,
end,
name
}
} }
}]; ];
} }
function readSequence ( parser: Parser, done: () => boolean ) { function readSequence(parser: Parser, done: () => boolean) {
let currentChunk: Node = { let currentChunk: Node = {
start: parser.index, start: parser.index,
end: null, end: null,
@ -328,31 +367,30 @@ function readSequence ( parser: Parser, done: () => boolean ) {
const chunks = []; const chunks = [];
while ( parser.index < parser.template.length ) { while (parser.index < parser.template.length) {
const index = parser.index; const index = parser.index;
if ( done() ) { if (done()) {
currentChunk.end = parser.index; currentChunk.end = parser.index;
if ( currentChunk.data ) chunks.push( currentChunk ); if (currentChunk.data) chunks.push(currentChunk);
chunks.forEach( chunk => { chunks.forEach(chunk => {
if ( chunk.type === 'Text' ) chunk.data = decodeCharacterReferences( chunk.data ); if (chunk.type === 'Text')
chunk.data = decodeCharacterReferences(chunk.data);
}); });
return chunks; return chunks;
} } else if (parser.eat('{{')) {
if (currentChunk.data) {
else if ( parser.eat( '{{' ) ) {
if ( currentChunk.data ) {
currentChunk.end = index; currentChunk.end = index;
chunks.push( currentChunk ); chunks.push(currentChunk);
} }
const expression = readExpression( parser ); const expression = readExpression(parser);
parser.allowWhitespace(); parser.allowWhitespace();
if ( !parser.eat( '}}' ) ) { if (!parser.eat('}}')) {
parser.error( `Expected }}` ); parser.error(`Expected }}`);
} }
chunks.push({ chunks.push({
@ -368,12 +406,10 @@ function readSequence ( parser: Parser, done: () => boolean ) {
type: 'Text', type: 'Text',
data: '' data: ''
}; };
} } else {
currentChunk.data += parser.template[parser.index++];
else {
currentChunk.data += parser.template[ parser.index++ ];
} }
} }
parser.error( `Unexpected end of input` ); parser.error(`Unexpected end of input`);
} }

@ -1,20 +1,24 @@
import { decodeCharacterReferences } from '../utils/html'; import { decodeCharacterReferences } from '../utils/html';
import { Parser } from '../index'; import { Parser } from '../index';
export default function text ( parser: Parser ) { export default function text(parser: Parser) {
const start = parser.index; const start = parser.index;
let data = ''; let data = '';
while ( parser.index < parser.template.length && !parser.match( '<' ) && !parser.match( '{{' ) ) { while (
data += parser.template[ parser.index++ ]; parser.index < parser.template.length &&
!parser.match('<') &&
!parser.match('{{')
) {
data += parser.template[parser.index++];
} }
parser.current().children.push({ parser.current().children.push({
start, start,
end: parser.index, end: parser.index,
type: 'Text', type: 'Text',
data: decodeCharacterReferences( data ) data: decodeCharacterReferences(data)
}); });
return null; return null;

@ -2031,4 +2031,4 @@ export default {
wp: 8472, wp: 8472,
wr: 8768, wr: 8768,
xi: 958 xi: 958
}; };

@ -1,8 +1,8 @@
// https://github.com/darkskyapp/string-hash/blob/master/index.js // https://github.com/darkskyapp/string-hash/blob/master/index.js
export default function hash ( str: string ) :number { export default function hash(str: string): number {
let hash = 5381; let hash = 5381;
let i = str.length; let i = str.length;
while ( i-- ) hash = ( hash * 33 ) ^ str.charCodeAt( i ); while (i--) hash = (hash * 33) ^ str.charCodeAt(i);
return hash >>> 0; return hash >>> 0;
} }

@ -1,26 +1,62 @@
import htmlEntities from './entities'; import htmlEntities from './entities';
const windows1252 = [ 8364, 129, 8218, 402, 8222, 8230, 8224, 8225, 710, 8240, 352, 8249, 338, 141, 381, 143, 144, 8216, 8217, 8220, 8221, 8226, 8211, 8212, 732, 8482, 353, 8250, 339, 157, 382, 376 ]; const windows1252 = [
const entityPattern = new RegExp( `&(#?(?:x[\\w\\d]+|\\d+|${Object.keys( htmlEntities ).join( '|' )}));?`, 'g' ); 8364,
129,
8218,
402,
8222,
8230,
8224,
8225,
710,
8240,
352,
8249,
338,
141,
381,
143,
144,
8216,
8217,
8220,
8221,
8226,
8211,
8212,
732,
8482,
353,
8250,
339,
157,
382,
376
];
const entityPattern = new RegExp(
`&(#?(?:x[\\w\\d]+|\\d+|${Object.keys(htmlEntities).join('|')}));?`,
'g'
);
export function decodeCharacterReferences ( html: string ) { export function decodeCharacterReferences(html: string) {
return html.replace( entityPattern, ( match, entity ) => { return html.replace(entityPattern, (match, entity) => {
let code; let code;
// Handle named entities // Handle named entities
if ( entity[0] !== '#' ) { if (entity[0] !== '#') {
code = htmlEntities[ entity ]; code = htmlEntities[entity];
} else if ( entity[1] === 'x' ) { } else if (entity[1] === 'x') {
code = parseInt( entity.substring( 2 ), 16 ); code = parseInt(entity.substring(2), 16);
} else { } else {
code = parseInt( entity.substring( 1 ), 10 ); code = parseInt(entity.substring(1), 10);
} }
if ( !code ) { if (!code) {
return match; return match;
} }
return String.fromCodePoint( validateCode( code ) ); return String.fromCodePoint(validateCode(code));
}); });
} }
@ -31,45 +67,45 @@ const NUL = 0;
// to replace them ourselves // to replace them ourselves
// //
// Source: http://en.wikipedia.org/wiki/Character_encodings_in_HTML#Illegal_characters // Source: http://en.wikipedia.org/wiki/Character_encodings_in_HTML#Illegal_characters
function validateCode ( code: number ) { function validateCode(code: number) {
// line feed becomes generic whitespace // line feed becomes generic whitespace
if ( code === 10 ) { if (code === 10) {
return 32; return 32;
} }
// ASCII range. (Why someone would use HTML entities for ASCII characters I don't know, but...) // ASCII range. (Why someone would use HTML entities for ASCII characters I don't know, but...)
if ( code < 128 ) { if (code < 128) {
return code; return code;
} }
// code points 128-159 are dealt with leniently by browsers, but they're incorrect. We need // code points 128-159 are dealt with leniently by browsers, but they're incorrect. We need
// to correct the mistake or we'll end up with missing € signs and so on // to correct the mistake or we'll end up with missing € signs and so on
if ( code <= 159 ) { if (code <= 159) {
return windows1252[ code - 128 ]; return windows1252[code - 128];
} }
// basic multilingual plane // basic multilingual plane
if ( code < 55296 ) { if (code < 55296) {
return code; return code;
} }
// UTF-16 surrogate halves // UTF-16 surrogate halves
if ( code <= 57343 ) { if (code <= 57343) {
return NUL; return NUL;
} }
// rest of the basic multilingual plane // rest of the basic multilingual plane
if ( code <= 65535 ) { if (code <= 65535) {
return code; return code;
} }
// supplementary multilingual plane 0x10000 - 0x1ffff // supplementary multilingual plane 0x10000 - 0x1ffff
if ( code >= 65536 && code <= 131071 ) { if (code >= 65536 && code <= 131071) {
return code; return code;
} }
// supplementary ideographic plane 0x20000 - 0x2ffff // supplementary ideographic plane 0x20000 - 0x2ffff
if ( code >= 131072 && code <= 196607 ) { if (code >= 131072 && code <= 196607) {
return code; return code;
} }

@ -2,16 +2,16 @@ import * as fs from 'fs';
import * as path from 'path'; import * as path from 'path';
import { compile } from '../index.ts'; import { compile } from '../index.ts';
function capitalise ( name ) { function capitalise(name) {
return name[0].toUpperCase() + name.slice( 1 ); return name[0].toUpperCase() + name.slice(1);
} }
require.extensions[ '.html' ] = function ( module, filename ) { require.extensions['.html'] = function(module, filename) {
const { code } = compile( fs.readFileSync( filename, 'utf-8' ), { const { code } = compile(fs.readFileSync(filename, 'utf-8'), {
filename, filename,
name: capitalise( path.basename( filename ).replace( /\.html$/, '' ) ), name: capitalise(path.basename(filename).replace(/\.html$/, '')),
generate: 'ssr' generate: 'ssr'
}); });
return module._compile( code, filename ); return module._compile(code, filename);
}; };

@ -1,35 +1,38 @@
const fs = require( 'fs' ); const fs = require('fs');
const path = require( 'path' ); const path = require('path');
const acorn = require( 'acorn' ); const acorn = require('acorn');
const declarations = {}; const declarations = {};
fs.readdirSync( __dirname ).forEach( file => { fs.readdirSync(__dirname).forEach(file => {
if ( !/^[a-z]+\.js$/.test( file ) ) return; if (!/^[a-z]+\.js$/.test(file)) return;
const source = fs.readFileSync( path.join( __dirname, file ), 'utf-8' ); const source = fs.readFileSync(path.join(__dirname, file), 'utf-8');
const ast = acorn.parse( source, { const ast = acorn.parse(source, {
ecmaVersion: 6, ecmaVersion: 6,
sourceType: 'module' sourceType: 'module'
}); });
ast.body.forEach( node => { ast.body.forEach(node => {
if ( node.type !== 'ExportNamedDeclaration' ) return; if (node.type !== 'ExportNamedDeclaration') return;
const declaration = node.declaration; const declaration = node.declaration;
if ( !declaration ) return; if (!declaration) return;
const name = declaration.type === 'VariableDeclaration' ? const name = declaration.type === 'VariableDeclaration'
declaration.declarations[0].id.name : ? declaration.declarations[0].id.name
declaration.id.name; : declaration.id.name;
const value = declaration.type === 'VariableDeclaration' ? const value = declaration.type === 'VariableDeclaration'
declaration.declarations[0].init : ? declaration.declarations[0].init
declaration; : declaration;
declarations[ name ] = source.slice( value.start, value.end ); declarations[name] = source.slice(value.start, value.end);
}); });
}); });
fs.writeFileSync( 'src/generators/dom/shared.ts', `// this file is auto-generated, do not edit it fs.writeFileSync(
export default ${JSON.stringify( declarations, null, '\t' )};` ); 'src/generators/dom/shared.ts',
`// this file is auto-generated, do not edit it
export default ${JSON.stringify(declarations, null, '\t')};`
);

@ -1,68 +1,68 @@
export function appendNode ( node, target ) { export function appendNode(node, target) {
target.appendChild( node ); target.appendChild(node);
} }
export function insertNode ( node, target, anchor ) { export function insertNode(node, target, anchor) {
target.insertBefore( node, anchor ); target.insertBefore(node, anchor);
} }
export function detachNode ( node ) { export function detachNode(node) {
node.parentNode.removeChild( node ); node.parentNode.removeChild(node);
} }
export function detachBetween ( before, after ) { export function detachBetween(before, after) {
while ( before.nextSibling && before.nextSibling !== after ) { while (before.nextSibling && before.nextSibling !== after) {
before.parentNode.removeChild( before.nextSibling ); before.parentNode.removeChild(before.nextSibling);
} }
} }
// TODO this is out of date // TODO this is out of date
export function destroyEach ( iterations, detach, start ) { export function destroyEach(iterations, detach, start) {
for ( var i = start; i < iterations.length; i += 1 ) { for (var i = start; i < iterations.length; i += 1) {
if ( iterations[i] ) iterations[i].destroy( detach ); if (iterations[i]) iterations[i].destroy(detach);
} }
} }
export function createElement ( name ) { export function createElement(name) {
return document.createElement( name ); return document.createElement(name);
} }
export function createSvgElement ( name ) { export function createSvgElement(name) {
return document.createElementNS( 'http://www.w3.org/2000/svg', name ); return document.createElementNS('http://www.w3.org/2000/svg', name);
} }
export function createText ( data ) { export function createText(data) {
return document.createTextNode( data ); return document.createTextNode(data);
} }
export function createComment () { export function createComment() {
return document.createComment( '' ); return document.createComment('');
} }
export function addEventListener ( node, event, handler ) { export function addEventListener(node, event, handler) {
node.addEventListener( event, handler, false ); node.addEventListener(event, handler, false);
} }
export function removeEventListener ( node, event, handler ) { export function removeEventListener(node, event, handler) {
node.removeEventListener( event, handler, false ); node.removeEventListener(event, handler, false);
} }
export function setAttribute ( node, attribute, value ) { export function setAttribute(node, attribute, value) {
node.setAttribute( attribute, value ); node.setAttribute(attribute, value);
} }
export function setXlinkAttribute ( node, attribute, value ) { export function setXlinkAttribute(node, attribute, value) {
node.setAttributeNS( 'http://www.w3.org/1999/xlink', attribute, value ); node.setAttributeNS('http://www.w3.org/1999/xlink', attribute, value);
} }
export function getBindingGroupValue ( group ) { export function getBindingGroupValue(group) {
var value = []; var value = [];
for ( var i = 0; i < group.length; i += 1 ) { for (var i = 0; i < group.length; i += 1) {
if ( group[i].checked ) value.push( group[i].__value ); if (group[i].checked) value.push(group[i].__value);
} }
return value; return value;
} }
export function toNumber ( value ) { export function toNumber(value) {
return value === '' ? undefined : +value; return value === '' ? undefined : +value;
} }

@ -3,109 +3,116 @@ export * from './dom.js';
export * from './transitions.js'; export * from './transitions.js';
export * from './utils.js'; export * from './utils.js';
export function differs ( a, b ) { export function differs(a, b) {
return ( a !== b ) || ( a && ( typeof a === 'object' ) || ( typeof a === 'function' ) ); return a !== b || ((a && typeof a === 'object') || typeof a === 'function');
} }
export function dispatchObservers ( component, group, newState, oldState ) { export function dispatchObservers(component, group, newState, oldState) {
for ( var key in group ) { for (var key in group) {
if ( !( key in newState ) ) continue; if (!(key in newState)) continue;
var newValue = newState[ key ]; var newValue = newState[key];
var oldValue = oldState[ key ]; var oldValue = oldState[key];
if ( differs( newValue, oldValue ) ) { if (differs(newValue, oldValue)) {
var callbacks = group[ key ]; var callbacks = group[key];
if ( !callbacks ) continue; if (!callbacks) continue;
for ( var i = 0; i < callbacks.length; i += 1 ) { for (var i = 0; i < callbacks.length; i += 1) {
var callback = callbacks[i]; var callback = callbacks[i];
if ( callback.__calling ) continue; if (callback.__calling) continue;
callback.__calling = true; callback.__calling = true;
callback.call( component, newValue, oldValue ); callback.call(component, newValue, oldValue);
callback.__calling = false; callback.__calling = false;
} }
} }
} }
} }
export function get ( key ) { export function get(key) {
return key ? this._state[ key ] : this._state; return key ? this._state[key] : this._state;
} }
export function fire ( eventName, data ) { export function fire(eventName, data) {
var handlers = eventName in this._handlers && this._handlers[ eventName ].slice(); var handlers =
if ( !handlers ) return; eventName in this._handlers && this._handlers[eventName].slice();
if (!handlers) return;
for ( var i = 0; i < handlers.length; i += 1 ) { for (var i = 0; i < handlers.length; i += 1) {
handlers[i].call( this, data ); handlers[i].call(this, data);
} }
} }
export function observe ( key, callback, options ) { export function observe(key, callback, options) {
var group = ( options && options.defer ) ? this._observers.post : this._observers.pre; var group = options && options.defer
? this._observers.post
: this._observers.pre;
( group[ key ] || ( group[ key ] = [] ) ).push( callback ); (group[key] || (group[key] = [])).push(callback);
if ( !options || options.init !== false ) { if (!options || options.init !== false) {
callback.__calling = true; callback.__calling = true;
callback.call( this, this._state[ key ] ); callback.call(this, this._state[key]);
callback.__calling = false; callback.__calling = false;
} }
return { return {
cancel: function () { cancel: function() {
var index = group[ key ].indexOf( callback ); var index = group[key].indexOf(callback);
if ( ~index ) group[ key ].splice( index, 1 ); if (~index) group[key].splice(index, 1);
} }
}; };
} }
export function observeDev ( key, callback, options ) { export function observeDev(key, callback, options) {
var c = ( key = '' + key ).search( /[^\w]/ ); var c = (key = '' + key).search(/[^\w]/);
if ( c > -1 ) { if (c > -1) {
var message = "The first argument to component.observe(...) must be the name of a top-level property"; var message =
if ( c > 0 ) message += ", i.e. '" + key.slice( 0, c ) + "' rather than '" + key + "'"; 'The first argument to component.observe(...) must be the name of a top-level property';
if (c > 0)
message += ", i.e. '" + key.slice(0, c) + "' rather than '" + key + "'";
throw new Error( message ); throw new Error(message);
} }
return observe.call( this, key, callback, options ); return observe.call(this, key, callback, options);
} }
export function on ( eventName, handler ) { export function on(eventName, handler) {
if ( eventName === 'teardown' ) return this.on( 'destroy', handler ); if (eventName === 'teardown') return this.on('destroy', handler);
var handlers = this._handlers[ eventName ] || ( this._handlers[ eventName ] = [] ); var handlers = this._handlers[eventName] || (this._handlers[eventName] = []);
handlers.push( handler ); handlers.push(handler);
return { return {
cancel: function () { cancel: function() {
var index = handlers.indexOf( handler ); var index = handlers.indexOf(handler);
if ( ~index ) handlers.splice( index, 1 ); if (~index) handlers.splice(index, 1);
} }
}; };
} }
export function onDev ( eventName, handler ) { export function onDev(eventName, handler) {
if ( eventName === 'teardown' ) { if (eventName === 'teardown') {
console.warn( "Use component.on('destroy', ...) instead of component.on('teardown', ...) which has been deprecated and will be unsupported in Svelte 2" ); console.warn(
return this.on( 'destroy', handler ); "Use component.on('destroy', ...) instead of component.on('teardown', ...) which has been deprecated and will be unsupported in Svelte 2"
);
return this.on('destroy', handler);
} }
return on.call( this, eventName, handler ); return on.call(this, eventName, handler);
} }
export function set ( newState ) { export function set(newState) {
this._set( assign( {}, newState ) ); this._set(assign({}, newState));
this._root._flush(); this._root._flush();
} }
export function _flush () { export function _flush() {
if ( !this._renderHooks ) return; if (!this._renderHooks) return;
while ( this._renderHooks.length ) { while (this._renderHooks.length) {
this._renderHooks.pop()(); this._renderHooks.pop()();
} }
} }

@ -1,50 +1,60 @@
import { assign, noop } from './utils.js'; import { assign, noop } from './utils.js';
export function linear ( t ) { export function linear(t) {
return t; return t;
} }
export function generateKeyframes ( a, b, delta, duration, ease, fn, node, style ) { export function generateKeyframes(
var id = '__svelte' + ~~( Math.random() * 1e9 ); // TODO make this more robust a,
b,
delta,
duration,
ease,
fn,
node,
style
) {
var id = '__svelte' + ~~(Math.random() * 1e9); // TODO make this more robust
var keyframes = '@keyframes ' + id + '{\n'; var keyframes = '@keyframes ' + id + '{\n';
for ( var p = 0; p <= 1; p += 16.666 / duration ) { for (var p = 0; p <= 1; p += 16.666 / duration) {
var t = a + delta * ease( p ); var t = a + delta * ease(p);
keyframes += ( p * 100 ) + '%{' + fn( t ) + '}\n'; keyframes += p * 100 + '%{' + fn(t) + '}\n';
} }
keyframes += '100% {' + fn( b ) + '}\n}'; keyframes += '100% {' + fn(b) + '}\n}';
style.textContent += keyframes; style.textContent += keyframes;
document.head.appendChild( style ); document.head.appendChild(style);
node.style.animation = ( node.style.animation || '' ).split( ',' ) node.style.animation = (node.style.animation || '')
.filter( function ( anim ) { .split(',')
.filter(function(anim) {
// when introing, discard old animations if there are any // when introing, discard old animations if there are any
return anim && ( delta < 0 || !/__svelte/.test( anim ) ); return anim && (delta < 0 || !/__svelte/.test(anim));
}) })
.concat( id + ' ' + duration + 'ms linear 1 forwards' ) .concat(id + ' ' + duration + 'ms linear 1 forwards')
.join( ', ' ); .join(', ');
} }
export function wrapTransition ( node, fn, params, intro, outgroup ) { export function wrapTransition(node, fn, params, intro, outgroup) {
var obj = fn( node, params ); var obj = fn(node, params);
var duration = obj.duration || 300; var duration = obj.duration || 300;
var ease = obj.easing || linear; var ease = obj.easing || linear;
var cssText; var cssText;
// TODO share <style> tag between all transitions? // TODO share <style> tag between all transitions?
if ( obj.css ) { if (obj.css) {
var style = document.createElement( 'style' ); var style = document.createElement('style');
} }
if ( intro ) { if (intro) {
if ( obj.css && obj.delay ) { if (obj.css && obj.delay) {
cssText = node.style.cssText; cssText = node.style.cssText;
node.style.cssText += obj.css( 0 ); node.style.cssText += obj.css(0);
} }
if ( obj.tick ) obj.tick( 0 ); if (obj.tick) obj.tick(0);
} }
return { return {
@ -52,58 +62,67 @@ export function wrapTransition ( node, fn, params, intro, outgroup ) {
running: false, running: false,
program: null, program: null,
pending: null, pending: null,
run: function ( intro, callback ) { run: function(intro, callback) {
var program = { var program = {
start: window.performance.now() + ( obj.delay || 0 ), start: window.performance.now() + (obj.delay || 0),
intro: intro, intro: intro,
callback: callback callback: callback
}; };
if ( obj.delay ) { if (obj.delay) {
this.pending = program; this.pending = program;
} else { } else {
this.start( program ); this.start(program);
} }
if ( !this.running ) { if (!this.running) {
this.running = true; this.running = true;
transitionManager.add( this ); transitionManager.add(this);
} }
}, },
start: function ( program ) { start: function(program) {
program.a = this.t; program.a = this.t;
program.b = program.intro ? 1 : 0; program.b = program.intro ? 1 : 0;
program.delta = program.b - program.a; program.delta = program.b - program.a;
program.duration = duration * Math.abs( program.b - program.a ); program.duration = duration * Math.abs(program.b - program.a);
program.end = program.start + program.duration; program.end = program.start + program.duration;
if ( obj.css ) { if (obj.css) {
if ( obj.delay ) node.style.cssText = cssText; if (obj.delay) node.style.cssText = cssText;
generateKeyframes( program.a, program.b, program.delta, program.duration, ease, obj.css, node, style ); generateKeyframes(
program.a,
program.b,
program.delta,
program.duration,
ease,
obj.css,
node,
style
);
} }
this.program = program; this.program = program;
this.pending = null; this.pending = null;
}, },
update: function ( now ) { update: function(now) {
var program = this.program; var program = this.program;
if ( !program ) return; if (!program) return;
var p = now - program.start; var p = now - program.start;
this.t = program.a + program.delta * ease( p / program.duration ); this.t = program.a + program.delta * ease(p / program.duration);
if ( obj.tick ) obj.tick( this.t ); if (obj.tick) obj.tick(this.t);
}, },
done: function () { done: function() {
this.t = this.program.b; this.t = this.program.b;
if ( obj.tick ) obj.tick( this.t ); if (obj.tick) obj.tick(this.t);
if ( obj.css ) document.head.removeChild( style ); if (obj.css) document.head.removeChild(style);
this.program.callback(); this.program.callback();
this.program = null; this.program = null;
this.running = !!this.pending; this.running = !!this.pending;
}, },
abort: function () { abort: function() {
if ( obj.tick ) obj.tick( 1 ); if (obj.tick) obj.tick(1);
if ( obj.css ) document.head.removeChild( style ); if (obj.css) document.head.removeChild(style);
this.program = this.pending = null; this.program = this.pending = null;
this.running = false; this.running = false;
} }
@ -115,42 +134,42 @@ export var transitionManager = {
transitions: [], transitions: [],
bound: null, bound: null,
add: function ( transition ) { add: function(transition) {
this.transitions.push( transition ); this.transitions.push(transition);
if ( !this.running ) { if (!this.running) {
this.running = true; this.running = true;
this.next(); this.next();
} }
}, },
next: function () { next: function() {
this.running = false; this.running = false;
var now = window.performance.now(); var now = window.performance.now();
var i = this.transitions.length; var i = this.transitions.length;
while ( i-- ) { while (i--) {
var transition = this.transitions[i]; var transition = this.transitions[i];
if ( transition.program && now >= transition.program.end ) { if (transition.program && now >= transition.program.end) {
transition.done(); transition.done();
} }
if ( transition.pending && now >= transition.pending.start ) { if (transition.pending && now >= transition.pending.start) {
transition.start( transition.pending ); transition.start(transition.pending);
} }
if ( transition.running ) { if (transition.running) {
transition.update( now ); transition.update(now);
this.running = true; this.running = true;
} else if ( !transition.pending ) { } else if (!transition.pending) {
this.transitions.splice( i, 1 ); this.transitions.splice(i, 1);
} }
} }
if ( this.running ) { if (this.running) {
requestAnimationFrame( this.bound || ( this.bound = this.next.bind( this ) ) ); requestAnimationFrame(this.bound || (this.bound = this.next.bind(this)));
} }
} }
}; };

@ -1,10 +1,13 @@
export function noop () {} export function noop() {}
export function assign ( target ) { export function assign(target) {
var k, source, i = 1, len = arguments.length; var k,
for ( ; i < len; i++ ) { source,
i = 1,
len = arguments.length;
for (; i < len; i++) {
source = arguments[i]; source = arguments[i];
for ( k in source ) target[k] = source[k]; for (k in source) target[k] = source[k];
} }
return target; return target;

@ -4,26 +4,28 @@ enum ChunkType {
} }
export default class CodeBuilder { export default class CodeBuilder {
result: string result: string;
first: ChunkType first: ChunkType;
last: ChunkType last: ChunkType;
lastCondition: string lastCondition: string;
constructor ( str = '' ) { constructor(str = '') {
this.result = str; this.result = str;
const initial = str ? ( /\n/.test( str ) ? ChunkType.Block : ChunkType.Line ) : null; const initial = str
? /\n/.test(str) ? ChunkType.Block : ChunkType.Line
: null;
this.first = initial; this.first = initial;
this.last = initial; this.last = initial;
this.lastCondition = null; this.lastCondition = null;
} }
addConditionalLine ( condition: string, line: string ) { addConditionalLine(condition: string, line: string) {
if ( condition === this.lastCondition ) { if (condition === this.lastCondition) {
this.result += `\n\t${line}`; this.result += `\n\t${line}`;
} else { } else {
if ( this.lastCondition ) { if (this.lastCondition) {
this.result += `\n}\n\n`; this.result += `\n}\n\n`;
} }
@ -34,69 +36,69 @@ export default class CodeBuilder {
this.last = ChunkType.Block; this.last = ChunkType.Block;
} }
addLine ( line: string ) { addLine(line: string) {
if ( this.lastCondition ) { if (this.lastCondition) {
this.result += `\n}`; this.result += `\n}`;
this.lastCondition = null; this.lastCondition = null;
} }
if ( this.last === ChunkType.Block ) { if (this.last === ChunkType.Block) {
this.result += `\n\n${line}`; this.result += `\n\n${line}`;
} else if ( this.last === ChunkType.Line ) { } else if (this.last === ChunkType.Line) {
this.result += `\n${line}`; this.result += `\n${line}`;
} else { } else {
this.result += line; this.result += line;
} }
this.last = ChunkType.Line; this.last = ChunkType.Line;
if ( !this.first ) this.first = ChunkType.Line; if (!this.first) this.first = ChunkType.Line;
} }
addLineAtStart ( line: string ) { addLineAtStart(line: string) {
if ( this.first === ChunkType.Block ) { if (this.first === ChunkType.Block) {
this.result = `${line}\n\n${this.result}`; this.result = `${line}\n\n${this.result}`;
} else if ( this.first === ChunkType.Line ) { } else if (this.first === ChunkType.Line) {
this.result = `${line}\n${this.result}`; this.result = `${line}\n${this.result}`;
} else { } else {
this.result += line; this.result += line;
} }
this.first = ChunkType.Line; this.first = ChunkType.Line;
if ( !this.last ) this.last = ChunkType.Line; if (!this.last) this.last = ChunkType.Line;
} }
addBlock ( block: string ) { addBlock(block: string) {
if ( this.lastCondition ) { if (this.lastCondition) {
this.result += `\n}`; this.result += `\n}`;
this.lastCondition = null; this.lastCondition = null;
} }
if ( this.result ) { if (this.result) {
this.result += `\n\n${block}`; this.result += `\n\n${block}`;
} else { } else {
this.result += block; this.result += block;
} }
this.last = ChunkType.Block; this.last = ChunkType.Block;
if ( !this.first ) this.first = ChunkType.Block; if (!this.first) this.first = ChunkType.Block;
} }
addBlockAtStart ( block: string ) { addBlockAtStart(block: string) {
if ( this.result ) { if (this.result) {
this.result = `${block}\n\n${this.result}`; this.result = `${block}\n\n${this.result}`;
} else { } else {
this.result += block; this.result += block;
} }
this.first = ChunkType.Block; this.first = ChunkType.Block;
if ( !this.last ) this.last = ChunkType.Block; if (!this.last) this.last = ChunkType.Block;
} }
isEmpty () { isEmpty() {
return this.result === ''; return this.result === '';
} }
toString () { toString() {
return this.result.trim() + ( this.lastCondition ? `\n}` : `` ); return this.result.trim() + (this.lastCondition ? `\n}` : ``);
} }
} }

@ -2,24 +2,30 @@ import { locate } from 'locate-character';
import getCodeFrame from '../utils/getCodeFrame'; import getCodeFrame from '../utils/getCodeFrame';
export default class CompileError extends Error { export default class CompileError extends Error {
frame: string frame: string;
loc: { line: number, column: number } loc: { line: number; column: number };
pos: number pos: number;
filename: string filename: string;
constructor ( message: string, template: string, index: number, filename: string ) { constructor(
super( message ); message: string,
template: string,
index: number,
filename: string
) {
super(message);
const { line, column } = locate( template, index ); const { line, column } = locate(template, index);
this.loc = { line: line + 1, column }; this.loc = { line: line + 1, column };
this.pos = index; this.pos = index;
this.filename = filename; this.filename = filename;
this.frame = getCodeFrame( template, line, column ); this.frame = getCodeFrame(template, line, column);
} }
toString () { toString() {
return `${this.message} (${this.loc.line}:${this.loc.column})\n${this.frame}`; return `${this.message} (${this.loc.line}:${this.loc.column})\n${this
.frame}`;
} }
} }

@ -2,25 +2,25 @@ import * as assert from 'assert';
import deindent from './deindent.js'; import deindent from './deindent.js';
import CodeBuilder from './CodeBuilder'; import CodeBuilder from './CodeBuilder';
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
@ -32,45 +32,47 @@ describe( 'deindent', () => {
after after
`; `;
assert.equal( deindented, `before\n\tline one\n\tline two\nafter` ); assert.equal(deindented, `before\n\tline one\n\tline two\nafter`);
}); });
}); });
describe( 'CodeBuilder', () => { describe('CodeBuilder', () => {
it( 'creates an empty block', () => { it('creates an empty block', () => {
const builder = new CodeBuilder(); const builder = new CodeBuilder();
assert.equal( builder.toString(), '' ); assert.equal(builder.toString(), '');
}); });
it( 'creates a block with a line', () => { it('creates a block with a line', () => {
const builder = new CodeBuilder(); const builder = new CodeBuilder();
builder.addLine( 'var answer = 42;' ); builder.addLine('var answer = 42;');
assert.equal( builder.toString(), 'var answer = 42;' ); assert.equal(builder.toString(), 'var answer = 42;');
}); });
it( 'creates a block with two lines', () => { it('creates a block with two lines', () => {
const builder = new CodeBuilder(); const builder = new CodeBuilder();
builder.addLine( 'var problems = 99;' ); builder.addLine('var problems = 99;');
builder.addLine( 'var answer = 42;' ); builder.addLine('var answer = 42;');
assert.equal( builder.toString(), 'var problems = 99;\nvar answer = 42;' ); assert.equal(builder.toString(), 'var problems = 99;\nvar answer = 42;');
}); });
it( 'adds newlines around blocks', () => { it('adds newlines around blocks', () => {
const builder = new CodeBuilder(); const builder = new CodeBuilder();
builder.addLine( '// line 1' ); builder.addLine('// line 1');
builder.addLine( '// line 2' ); builder.addLine('// line 2');
builder.addBlock( deindent` builder.addBlock(deindent`
if ( foo ) { if ( foo ) {
bar(); bar();
} }
` ); `);
builder.addLine( '// line 3' ); builder.addLine('// line 3');
builder.addLine( '// line 4' ); builder.addLine('// line 4');
assert.equal( builder.toString(), deindent` assert.equal(
builder.toString(),
deindent`
// line 1 // line 1
// line 2 // line 2
@ -80,31 +82,34 @@ describe( 'CodeBuilder', () => {
// line 3 // line 3
// line 4 // line 4
` ); `
);
}); });
it( 'nests codebuilders with correct indentation', () => { it('nests codebuilders with correct indentation', () => {
const child = new CodeBuilder(); const child = new CodeBuilder();
child.addBlock( deindent` child.addBlock(deindent`
var obj = { var obj = {
answer: 42 answer: 42
}; };
` ); `);
const builder = new CodeBuilder(); const builder = new CodeBuilder();
builder.addLine( '// line 1' ); builder.addLine('// line 1');
builder.addLine( '// line 2' ); builder.addLine('// line 2');
builder.addBlock( deindent` builder.addBlock(deindent`
if ( foo ) { if ( foo ) {
${child} ${child}
} }
` ); `);
builder.addLine( '// line 3' ); builder.addLine('// line 3');
builder.addLine( '// line 4' ); builder.addLine('// line 4');
assert.equal( builder.toString(), deindent` assert.equal(
builder.toString(),
deindent`
// line 1 // line 1
// line 2 // line 2
@ -116,57 +121,70 @@ describe( 'CodeBuilder', () => {
// line 3 // line 3
// line 4 // line 4
` ); `
);
}); });
it( 'adds a line at start', () => { it('adds a line at start', () => {
const builder = new CodeBuilder(); const builder = new CodeBuilder();
builder.addLine( '// second' ); builder.addLine('// second');
builder.addLineAtStart( '// first' ); builder.addLineAtStart('// first');
assert.equal( builder.toString(), deindent` assert.equal(
builder.toString(),
deindent`
// first // first
// second // second
` ); `
);
}); });
it( 'adds a line at start before a block', () => { it('adds a line at start before a block', () => {
const builder = new CodeBuilder(); const builder = new CodeBuilder();
builder.addBlock( '// second' ); builder.addBlock('// second');
builder.addLineAtStart( '// first' ); builder.addLineAtStart('// first');
assert.equal( builder.toString(), deindent` assert.equal(
builder.toString(),
deindent`
// first // first
// second // second
` ); `
);
}); });
it( 'adds a block at start', () => { it('adds a block at start', () => {
const builder = new CodeBuilder(); const builder = new CodeBuilder();
builder.addLine( '// second' ); builder.addLine('// second');
builder.addBlockAtStart( '// first' ); builder.addBlockAtStart('// first');
assert.equal( builder.toString(), deindent` assert.equal(
builder.toString(),
deindent`
// first // first
// second // second
` ); `
);
}); });
it( 'adds a block at start before a block', () => { it('adds a block at start before a block', () => {
const builder = new CodeBuilder(); const builder = new CodeBuilder();
builder.addBlock( '// second' ); builder.addBlock('// second');
builder.addBlockAtStart( '// first' ); builder.addBlockAtStart('// first');
assert.equal( builder.toString(), deindent` assert.equal(
builder.toString(),
deindent`
// first // first
// second // second
` ); `
);
}); });
}); });

@ -1,41 +1,35 @@
import { walk } from 'estree-walker'; import { walk } from 'estree-walker';
import { Node } from '../interfaces'; import { Node } from '../interfaces';
export default function annotateWithScopes ( expression: Node ) { export default function annotateWithScopes(expression: Node) {
let scope = new Scope( null, false ); let scope = new Scope(null, false);
walk( expression, { walk(expression, {
enter ( node: Node ) { enter(node: Node) {
if ( /Function/.test( node.type ) ) { if (/Function/.test(node.type)) {
if ( node.type === 'FunctionDeclaration' ) { if (node.type === 'FunctionDeclaration') {
scope.declarations.add( node.id.name ); scope.declarations.add(node.id.name);
} else { } else {
node._scope = scope = new Scope( scope, false ); node._scope = scope = new Scope(scope, false);
if ( node.id ) scope.declarations.add( node.id.name ); if (node.id) scope.declarations.add(node.id.name);
} }
node.params.forEach( ( param: Node ) => { node.params.forEach((param: Node) => {
extractNames( param ).forEach( name => { extractNames(param).forEach(name => {
scope.declarations.add( name ); scope.declarations.add(name);
}); });
}); });
} } else if (/For(?:In|Of)Statement/.test(node.type)) {
node._scope = scope = new Scope(scope, true);
else if ( /For(?:In|Of)Statement/.test( node.type ) ) { } else if (node.type === 'BlockStatement') {
node._scope = scope = new Scope( scope, true ); node._scope = scope = new Scope(scope, true);
} } else if (/(Function|Class|Variable)Declaration/.test(node.type)) {
scope.addDeclaration(node);
else if ( node.type === 'BlockStatement' ) {
node._scope = scope = new Scope( scope, true );
}
else if ( /(Function|Class|Variable)Declaration/.test( node.type ) ) {
scope.addDeclaration( node );
} }
}, },
leave ( node: Node ) { leave(node: Node) {
if ( node._scope ) { if (node._scope) {
scope = scope.parent; scope = scope.parent;
} }
} }
@ -45,63 +39,65 @@ export default function annotateWithScopes ( expression: Node ) {
} }
class Scope { class Scope {
parent: Scope parent: Scope;
block: boolean block: boolean;
declarations: Set<string> declarations: Set<string>;
constructor ( parent: Scope, block: boolean ) { constructor(parent: Scope, block: boolean) {
this.parent = parent; this.parent = parent;
this.block = block; this.block = block;
this.declarations = new Set(); this.declarations = new Set();
} }
addDeclaration ( node: Node ) { addDeclaration(node: Node) {
if ( node.kind === 'var' && !this.block && this.parent ) { if (node.kind === 'var' && !this.block && this.parent) {
this.parent.addDeclaration( node ); this.parent.addDeclaration(node);
} else if ( node.type === 'VariableDeclaration' ) { } else if (node.type === 'VariableDeclaration') {
node.declarations.forEach( ( declarator: Node ) => { node.declarations.forEach((declarator: Node) => {
extractNames( declarator.id ).forEach( name => { extractNames(declarator.id).forEach(name => {
this.declarations.add( name ); this.declarations.add(name);
}); });
}); });
} else { } else {
this.declarations.add( node.id.name ); this.declarations.add(node.id.name);
} }
} }
has ( name: string ) :boolean { has(name: string): boolean {
return this.declarations.has( name ) || this.parent && this.parent.has( name ); return (
this.declarations.has(name) || (this.parent && this.parent.has(name))
);
} }
} }
function extractNames ( param: Node ) { function extractNames(param: Node) {
const names: string[] = []; const names: string[] = [];
extractors[ param.type ]( names, param ); extractors[param.type](names, param);
return names; return names;
} }
const extractors = { const extractors = {
Identifier ( names: string[], param: Node ) { Identifier(names: string[], param: Node) {
names.push( param.name ); names.push(param.name);
}, },
ObjectPattern ( names: string[], param: Node ) { ObjectPattern(names: string[], param: Node) {
param.properties.forEach( ( prop: Node ) => { param.properties.forEach((prop: Node) => {
extractors[ prop.value.type ]( names, prop.value ); extractors[prop.value.type](names, prop.value);
}); });
}, },
ArrayPattern ( names: string[], param: Node ) { ArrayPattern(names: string[], param: Node) {
param.elements.forEach( ( element: Node ) => { param.elements.forEach((element: Node) => {
if ( element ) extractors[ element.type ]( names, element ); if (element) extractors[element.type](names, element);
}); });
}, },
RestElement ( names: string[], param: Node ) { RestElement(names: string[], param: Node) {
extractors[ param.argument.type ]( names, param.argument ); extractors[param.argument.type](names, param.argument);
}, },
AssignmentPattern ( names: string[], param: Node ) { AssignmentPattern(names: string[], param: Node) {
extractors[ param.left.type ]( names, param.left ); extractors[param.left.type](names, param.left);
} }
}; };

@ -1,40 +1,41 @@
const start = /\n(\t+)/; const start = /\n(\t+)/;
export default function deindent ( strings, ...values ) { export default function deindent(strings, ...values) {
const indentation = start.exec( strings[0] )[1]; const indentation = start.exec(strings[0])[1];
const pattern = new RegExp( `^${indentation}`, 'gm' ); const pattern = new RegExp(`^${indentation}`, 'gm');
let result = strings[0].replace( start, '' ).replace( pattern, '' ); let result = strings[0].replace(start, '').replace(pattern, '');
let trailingIndentation = getTrailingIndentation( result ); let trailingIndentation = getTrailingIndentation(result);
for ( let i = 1; i < strings.length; i += 1 ) { for (let i = 1; i < strings.length; i += 1) {
let expression = values[ i - 1 ]; let expression = values[i - 1];
const string = strings[i].replace( pattern, '' ); const string = strings[i].replace(pattern, '');
if ( Array.isArray( expression ) ) { if (Array.isArray(expression)) {
expression = expression.length ? expression.join( '\n' ) : null; expression = expression.length ? expression.join('\n') : null;
} }
if ( expression || expression === '' ) { if (expression || expression === '') {
const value = String( expression ).replace( /\n/g, `\n${trailingIndentation}` ); const value = String(expression).replace(
/\n/g,
`\n${trailingIndentation}`
);
result += value + string; result += value + string;
} } else {
else {
let c = result.length; let c = result.length;
while ( /\s/.test( result[ c - 1 ] ) ) c -= 1; while (/\s/.test(result[c - 1])) c -= 1;
result = result.slice( 0, c ) + string; result = result.slice(0, c) + string;
} }
trailingIndentation = getTrailingIndentation( result ); trailingIndentation = getTrailingIndentation(result);
} }
return result.trim().replace( /\t+$/gm, '' ); return result.trim().replace(/\t+$/gm, '');
} }
function getTrailingIndentation ( str ) { function getTrailingIndentation(str) {
let i = str.length; let i = str.length;
while ( str[ i - 1 ] === ' ' || str[ i - 1 ] === '\t' ) i -= 1; while (str[i - 1] === ' ' || str[i - 1] === '\t') i -= 1;
return str.slice( i, str.length ); return str.slice(i, str.length);
} }

@ -1,21 +1,23 @@
import { Node } from '../interfaces'; import { Node } from '../interfaces';
export default function flatten ( node: Node ) { export default function flatten(node: Node) {
const parts = []; const parts = [];
const propEnd = node.end; const propEnd = node.end;
while ( node.type === 'MemberExpression' ) { while (node.type === 'MemberExpression') {
if ( node.computed ) return null; if (node.computed) return null;
parts.unshift( node.property.name ); parts.unshift(node.property.name);
node = node.object; node = node.object;
} }
const propStart = node.end; const propStart = node.end;
const name = node.type === 'Identifier' ? node.name : node.type === 'ThisExpression' ? 'this' : null; const name = node.type === 'Identifier'
? node.name
: node.type === 'ThisExpression' ? 'this' : null;
if ( !name ) return null; if (!name) return null;
parts.unshift( name ); parts.unshift(name);
return { name, parts, keypath: `${name}[✂${propStart}-${propEnd}✂]` }; return { name, parts, keypath: `${name}[✂${propStart}-${propEnd}✂]` };
} }

@ -1,31 +1,36 @@
import spaces from './spaces.js'; import spaces from './spaces.js';
function tabsToSpaces ( str: string ) { function tabsToSpaces(str: string) {
return str.replace( /^\t+/, match => match.split( '\t' ).join( ' ' ) ); return str.replace(/^\t+/, match => match.split('\t').join(' '));
} }
export default function getCodeFrame ( source: string, line: number, column: number ) { export default function getCodeFrame(
const lines = source.split( '\n' ); source: string,
line: number,
column: number
) {
const lines = source.split('\n');
const frameStart = Math.max( 0, line - 2 ); const frameStart = Math.max(0, line - 2);
const frameEnd = Math.min( line + 3, lines.length ); const frameEnd = Math.min(line + 3, lines.length);
const digits = String( frameEnd + 1 ).length; const digits = String(frameEnd + 1).length;
return lines return lines
.slice( frameStart, frameEnd ) .slice(frameStart, frameEnd)
.map( ( str, i ) => { .map((str, i) => {
const isErrorLine = frameStart + i === line; const isErrorLine = frameStart + i === line;
let lineNum = String( i + frameStart + 1 ); let lineNum = String(i + frameStart + 1);
while ( lineNum.length < digits ) lineNum = ` ${lineNum}`; while (lineNum.length < digits) lineNum = ` ${lineNum}`;
if ( isErrorLine ) { if (isErrorLine) {
const indicator = spaces( digits + 2 + tabsToSpaces( str.slice( 0, column ) ).length ) + '^'; const indicator =
return `${lineNum}: ${tabsToSpaces( str )}\n${indicator}`; spaces(digits + 2 + tabsToSpaces(str.slice(0, column)).length) + '^';
return `${lineNum}: ${tabsToSpaces(str)}\n${indicator}`;
} }
return `${lineNum}: ${tabsToSpaces( str )}`; return `${lineNum}: ${tabsToSpaces(str)}`;
}) })
.join( '\n' ); .join('\n');
} }

@ -1 +1,26 @@
export default new Set( [ 'Array', 'Boolean', 'console', 'Date', 'decodeURI', 'decodeURIComponent', 'encodeURI', 'encodeURIComponent', 'Infinity', 'Intl', 'isFinite', 'isNaN', 'JSON', 'Map', 'Math', 'NaN', 'Number', 'Object', 'parseFloat', 'parseInt', 'RegExp', 'Set', 'String', 'undefined' ] ); export default new Set([
'Array',
'Boolean',
'console',
'Date',
'decodeURI',
'decodeURIComponent',
'encodeURI',
'encodeURIComponent',
'Infinity',
'Intl',
'isFinite',
'isNaN',
'JSON',
'Map',
'Math',
'NaN',
'Number',
'Object',
'parseFloat',
'parseInt',
'RegExp',
'Set',
'String',
'undefined'
]);

@ -1,29 +1,33 @@
import { Node } from '../interfaces'; import { Node } from '../interfaces';
export default function isReference ( node: Node, parent: Node ): boolean { export default function isReference(node: Node, parent: Node): boolean {
if ( node.type === 'MemberExpression' ) { if (node.type === 'MemberExpression') {
return !node.computed && isReference( node.object, node ); return !node.computed && isReference(node.object, node);
} }
if ( node.type === 'Identifier' ) { if (node.type === 'Identifier') {
// the only time we could have an identifier node without a parent is // the only time we could have an identifier node without a parent is
// if it's the entire body of a function without a block statement // if it's the entire body of a function without a block statement
// i.e. an arrow function expression like `a => a` // i.e. an arrow function expression like `a => a`
if ( !parent ) return true; if (!parent) return true;
// TODO is this right? // TODO is this right?
if ( parent.type === 'MemberExpression' || parent.type === 'MethodDefinition' ) { if (
parent.type === 'MemberExpression' ||
parent.type === 'MethodDefinition'
) {
return parent.computed || node === parent.object; return parent.computed || node === parent.object;
} }
// disregard the `bar` in `{ bar: foo }`, but keep it in `{ [bar]: foo }` // disregard the `bar` in `{ bar: foo }`, but keep it in `{ [bar]: foo }`
if ( parent.type === 'Property' ) return parent.computed || node === parent.value; if (parent.type === 'Property')
return parent.computed || node === parent.value;
// disregard the `bar` in `class Foo { bar () {...} }` // disregard the `bar` in `class Foo { bar () {...} }`
if ( parent.type === 'MethodDefinition' ) return false; if (parent.type === 'MethodDefinition') return false;
// disregard the `bar` in `export { foo as bar }` // disregard the `bar` in `export { foo as bar }`
if ( parent.type === 'ExportSpecifier' && node !== parent.local ) return; if (parent.type === 'ExportSpecifier' && node !== parent.local) return;
return true; return true;
} }

@ -1,5 +1,5 @@
const voidElementNames = /^(?:area|base|br|col|command|embed|hr|img|input|keygen|link|meta|param|source|track|wbr)$/; const voidElementNames = /^(?:area|base|br|col|command|embed|hr|img|input|keygen|link|meta|param|source|track|wbr)$/;
export default function isVoidElementName ( name: string ) { export default function isVoidElementName(name: string) {
return voidElementNames.test( name ) || name.toLowerCase() === '!doctype'; return voidElementNames.test(name) || name.toLowerCase() === '!doctype';
} }

@ -1,13 +1,23 @@
export const html = 'http://www.w3.org/1999/xhtml'; export const html = 'http://www.w3.org/1999/xhtml';
export const mathml = 'http://www.w3.org/1998/Math/MathML'; export const mathml = 'http://www.w3.org/1998/Math/MathML';
export const svg = 'http://www.w3.org/2000/svg'; export const svg = 'http://www.w3.org/2000/svg';
export const xlink = 'http://www.w3.org/1999/xlink'; export const xlink = 'http://www.w3.org/1999/xlink';
export const xml = 'http://www.w3.org/XML/1998/namespace'; export const xml = 'http://www.w3.org/XML/1998/namespace';
export const xmlns = 'http://www.w3.org/2000/xmlns'; export const xmlns = 'http://www.w3.org/2000/xmlns';
export const validNamespaces = [ export const validNamespaces = [
'html', 'mathml', 'svg', 'xlink', 'xml', 'xmlns', 'html',
html, mathml, svg, xlink, xml, xmlns 'mathml',
'svg',
'xlink',
'xml',
'xmlns',
html,
mathml,
svg,
xlink,
xml,
xmlns
]; ];
export default { html, mathml, svg, xlink, xml, xmlns }; export default { html, mathml, svg, xlink, xml, xmlns };

@ -6,52 +6,52 @@ const keys = {
}; };
const offsets = { const offsets = {
ObjectExpression: [ 1, -1 ], ObjectExpression: [1, -1],
Program: [ 0, 0 ] Program: [0, 0]
}; };
export function removeNode ( code, parent: Node, node: Node ) { export function removeNode(code, parent: Node, node: Node) {
const key = keys[ parent.type ]; const key = keys[parent.type];
const offset = offsets[ parent.type ]; const offset = offsets[parent.type];
if ( !key || !offset ) throw new Error( `not implemented: ${parent.type}` ); if (!key || !offset) throw new Error(`not implemented: ${parent.type}`);
const list = parent[ key ]; const list = parent[key];
const i = list.indexOf( node ); const i = list.indexOf(node);
if ( i === -1 ) throw new Error( 'node not in list' ); if (i === -1) throw new Error('node not in list');
let a; let a;
let b; let b;
if ( list.length === 1 ) { if (list.length === 1) {
// remove everything, leave {} // remove everything, leave {}
a = parent.start + offset[0]; a = parent.start + offset[0];
b = parent.end + offset[1]; b = parent.end + offset[1];
} else if ( i === 0 ) { } else if (i === 0) {
// remove everything before second node, including comments // remove everything before second node, including comments
a = parent.start + offset[0]; a = parent.start + offset[0];
while ( /\s/.test( code.original[a] ) ) a += 1; while (/\s/.test(code.original[a])) a += 1;
b = list[i].end; b = list[i].end;
while ( /[\s,]/.test( code.original[b] ) ) b += 1; while (/[\s,]/.test(code.original[b])) b += 1;
} else { } else {
// remove the end of the previous node to the end of this one // remove the end of the previous node to the end of this one
a = list[ i - 1 ].end; a = list[i - 1].end;
b = node.end; b = node.end;
} }
code.remove( a, b ); code.remove(a, b);
list.splice( i, 1 ); list.splice(i, 1);
return; return;
} }
export function removeObjectKey ( code, node, key ) { export function removeObjectKey(code, node, key) {
if ( node.type !== 'ObjectExpression' ) return; if (node.type !== 'ObjectExpression') return;
let i = node.properties.length; let i = node.properties.length;
while ( i-- ) { while (i--) {
const property = node.properties[i]; const property = node.properties[i];
if ( property.key.type === 'Identifier' && property.key.name === key ) { if (property.key.type === 'Identifier' && property.key.name === key) {
removeNode( code, node, property ); removeNode(code, node, property);
} }
} }
} }

@ -1,6 +1,55 @@
const reservedNames = new Set( [ 'arguments', 'await', 'break', 'case', 'catch', 'class', 'const', 'continue', 'debugger', 'default', 'delete', 'do', 'else', 'enum', 'eval', 'export', 'extends', 'false', 'finally', 'for', 'function', 'if', 'implements', 'import', 'in', 'instanceof', 'interface', 'let', 'new', 'null', 'package', 'private', 'protected', 'public', 'return', 'static', 'super', 'switch', 'this', 'throw', 'true', 'try', 'typeof', 'var', 'void', 'while', 'with', 'yield' ] ); const reservedNames = new Set([
'arguments',
'await',
'break',
'case',
'catch',
'class',
'const',
'continue',
'debugger',
'default',
'delete',
'do',
'else',
'enum',
'eval',
'export',
'extends',
'false',
'finally',
'for',
'function',
'if',
'implements',
'import',
'in',
'instanceof',
'interface',
'let',
'new',
'null',
'package',
'private',
'protected',
'public',
'return',
'static',
'super',
'switch',
'this',
'throw',
'true',
'try',
'typeof',
'var',
'void',
'while',
'with',
'yield'
]);
// prevent e.g. `{{#each states as state}}` breaking // prevent e.g. `{{#each states as state}}` breaking
reservedNames.add( 'state' ); reservedNames.add('state');
export default reservedNames; export default reservedNames;

@ -1,5 +1,5 @@
export default function spaces ( i ) { export default function spaces(i) {
let result = ''; let result = '';
while ( i-- ) result += ' '; while (i--) result += ' ';
return result; return result;
} }

@ -1,15 +1,15 @@
import { whitespace } from './patterns'; import { whitespace } from './patterns';
export function trimStart ( str: string ) { export function trimStart(str: string) {
let i = 0; let i = 0;
while ( whitespace.test( str[i] ) ) i += 1; while (whitespace.test(str[i])) i += 1;
return str.slice( i ); return str.slice(i);
} }
export function trimEnd ( str: string ) { export function trimEnd(str: string) {
let i = str.length; let i = str.length;
while ( whitespace.test( str[ i - 1 ] ) ) i -= 1; while (whitespace.test(str[i - 1])) i -= 1;
return str.slice( 0, i ); return str.slice(0, i);
} }

@ -6,51 +6,59 @@ import { Node } from '../../interfaces';
const svg = /^(?:altGlyph|altGlyphDef|altGlyphItem|animate|animateColor|animateMotion|animateTransform|circle|clipPath|color-profile|cursor|defs|desc|discard|ellipse|feBlend|feColorMatrix|feComponentTransfer|feComposite|feConvolveMatrix|feDiffuseLighting|feDisplacementMap|feDistantLight|feDropShadow|feFlood|feFuncA|feFuncB|feFuncG|feFuncR|feGaussianBlur|feImage|feMerge|feMergeNode|feMorphology|feOffset|fePointLight|feSpecularLighting|feSpotLight|feTile|feTurbulence|filter|font|font-face|font-face-format|font-face-name|font-face-src|font-face-uri|foreignObject|g|glyph|glyphRef|hatch|hatchpath|hkern|image|line|linearGradient|marker|mask|mesh|meshgradient|meshpatch|meshrow|metadata|missing-glyph|mpath|path|pattern|polygon|polyline|radialGradient|rect|set|solidcolor|stop|switch|symbol|text|textPath|title|tref|tspan|unknown|use|view|vkern)$/; const svg = /^(?:altGlyph|altGlyphDef|altGlyphItem|animate|animateColor|animateMotion|animateTransform|circle|clipPath|color-profile|cursor|defs|desc|discard|ellipse|feBlend|feColorMatrix|feComponentTransfer|feComposite|feConvolveMatrix|feDiffuseLighting|feDisplacementMap|feDistantLight|feDropShadow|feFlood|feFuncA|feFuncB|feFuncG|feFuncR|feGaussianBlur|feImage|feMerge|feMergeNode|feMorphology|feOffset|fePointLight|feSpecularLighting|feSpotLight|feTile|feTurbulence|filter|font|font-face|font-face-format|font-face-name|font-face-src|font-face-uri|foreignObject|g|glyph|glyphRef|hatch|hatchpath|hkern|image|line|linearGradient|marker|mask|mesh|meshgradient|meshpatch|meshrow|metadata|missing-glyph|mpath|path|pattern|polygon|polyline|radialGradient|rect|set|solidcolor|stop|switch|symbol|text|textPath|title|tref|tspan|unknown|use|view|vkern)$/;
const meta = new Map([ const meta = new Map([[':Window', validateWindow]]);
[ ':Window', validateWindow ]
]);
export default function validateHtml ( validator: Validator, html: Node ) { export default function validateHtml(validator: Validator, html: Node) {
let elementDepth = 0; let elementDepth = 0;
function visit ( node: Node ) { function visit(node: Node) {
if ( node.type === 'Element' ) { if (node.type === 'Element') {
if ( elementDepth === 0 && validator.namespace !== namespaces.svg && svg.test( node.name ) ) { if (
validator.warn( `<${node.name}> is an SVG element did you forget to add { namespace: 'svg' } ?`, node.start ); elementDepth === 0 &&
validator.namespace !== namespaces.svg &&
svg.test(node.name)
) {
validator.warn(
`<${node.name}> is an SVG element did you forget to add { namespace: 'svg' } ?`,
node.start
);
} }
if ( meta.has( node.name ) ) { if (meta.has(node.name)) {
return meta.get( node.name )( validator, node ); return meta.get(node.name)(validator, node);
} }
elementDepth += 1; elementDepth += 1;
validateElement( validator, node ); validateElement(validator, node);
} else if ( node.type === 'EachBlock' ) { } else if (node.type === 'EachBlock') {
if ( validator.helpers.has( node.context ) ) { if (validator.helpers.has(node.context)) {
let c = node.expression.end; let c = node.expression.end;
// find start of context // find start of context
while ( /\s/.test( validator.source[c] ) ) c += 1; while (/\s/.test(validator.source[c])) c += 1;
c += 2; c += 2;
while ( /\s/.test( validator.source[c] ) ) c += 1; while (/\s/.test(validator.source[c])) c += 1;
validator.warn( `Context clashes with a helper. Rename one or the other to eliminate any ambiguity`, c ); validator.warn(
`Context clashes with a helper. Rename one or the other to eliminate any ambiguity`,
c
);
} }
} }
if ( node.children ) { if (node.children) {
node.children.forEach( visit ); node.children.forEach(visit);
} }
if ( node.else ) { if (node.else) {
visit( node.else ); visit(node.else);
} }
if ( node.type === 'Element' ) { if (node.type === 'Element') {
elementDepth -= 1; elementDepth -= 1;
} }
} }
html.children.forEach( visit ); html.children.forEach(visit);
} }

@ -2,102 +2,144 @@ import validateEventHandler from './validateEventHandler';
import { Validator } from '../index'; import { Validator } from '../index';
import { Node } from '../../interfaces'; import { Node } from '../../interfaces';
export default function validateElement ( validator: Validator, node: Node ) { export default function validateElement(validator: Validator, node: Node) {
const isComponent = node.name === ':Self' || validator.components.has( node.name ); const isComponent =
node.name === ':Self' || validator.components.has(node.name);
let hasIntro: boolean; let hasIntro: boolean;
let hasOutro: boolean; let hasOutro: boolean;
let hasTransition: boolean; let hasTransition: boolean;
node.attributes.forEach( ( attribute: Node ) => { node.attributes.forEach((attribute: Node) => {
if ( !isComponent && attribute.type === 'Binding' ) { if (!isComponent && attribute.type === 'Binding') {
const { name } = attribute; const { name } = attribute;
if ( name === 'value' ) { if (name === 'value') {
if ( node.name !== 'input' && node.name !== 'textarea' && node.name !== 'select' ) { if (
validator.error( `'value' is not a valid binding on <${node.name}> elements`, attribute.start ); node.name !== 'input' &&
node.name !== 'textarea' &&
node.name !== 'select'
) {
validator.error(
`'value' is not a valid binding on <${node.name}> elements`,
attribute.start
);
} }
} } else if (name === 'checked') {
if (node.name !== 'input') {
else if ( name === 'checked' ) { validator.error(
if ( node.name !== 'input' ) { `'checked' is not a valid binding on <${node.name}> elements`,
validator.error( `'checked' is not a valid binding on <${node.name}> elements`, attribute.start ); attribute.start
);
} }
if ( getType( validator, node ) !== 'checkbox' ) { if (getType(validator, node) !== 'checkbox') {
validator.error( `'checked' binding can only be used with <input type="checkbox">`, attribute.start ); validator.error(
`'checked' binding can only be used with <input type="checkbox">`,
attribute.start
);
} }
} } else if (name === 'group') {
if (node.name !== 'input') {
else if ( name === 'group' ) { validator.error(
if ( node.name !== 'input' ) { `'group' is not a valid binding on <${node.name}> elements`,
validator.error( `'group' is not a valid binding on <${node.name}> elements`, attribute.start ); attribute.start
);
} }
const type = getType( validator, node ); const type = getType(validator, node);
if ( type !== 'checkbox' && type !== 'radio' ) { if (type !== 'checkbox' && type !== 'radio') {
validator.error( `'checked' binding can only be used with <input type="checkbox"> or <input type="radio">`, attribute.start ); validator.error(
`'checked' binding can only be used with <input type="checkbox"> or <input type="radio">`,
attribute.start
);
} }
} } else if (
name === 'currentTime' ||
else if ( name === 'currentTime' || name === 'duration' || name === 'paused' ) { name === 'duration' ||
if ( node.name !== 'audio' && node.name !== 'video' ) { name === 'paused'
validator.error( `'${name}' binding can only be used with <audio> or <video>`, attribute.start ); ) {
if (node.name !== 'audio' && node.name !== 'video') {
validator.error(
`'${name}' binding can only be used with <audio> or <video>`,
attribute.start
);
} }
} else {
validator.error(
`'${attribute.name}' is not a valid binding`,
attribute.start
);
} }
} else if (attribute.type === 'EventHandler') {
else { validateEventHandler(validator, attribute);
validator.error( `'${attribute.name}' is not a valid binding`, attribute.start ); } else if (attribute.type === 'Transition') {
}
}
else if ( attribute.type === 'EventHandler' ) {
validateEventHandler( validator, attribute );
}
else if ( attribute.type === 'Transition' ) {
const bidi = attribute.intro && attribute.outro; const bidi = attribute.intro && attribute.outro;
if ( hasTransition ) { if (hasTransition) {
if ( bidi ) validator.error( `An element can only have one 'transition' directive`, attribute.start ); if (bidi)
validator.error( `An element cannot have both a 'transition' directive and an '${attribute.intro ? 'in' : 'out'}' directive`, attribute.start ); validator.error(
`An element can only have one 'transition' directive`,
attribute.start
);
validator.error(
`An element cannot have both a 'transition' directive and an '${attribute.intro
? 'in'
: 'out'}' directive`,
attribute.start
);
} }
if ( ( hasIntro && attribute.intro ) || ( hasOutro && attribute.outro ) ) { if ((hasIntro && attribute.intro) || (hasOutro && attribute.outro)) {
if ( bidi ) validator.error( `An element cannot have both an '${hasIntro ? 'in' : 'out'}' directive and a 'transition' directive`, attribute.start ); if (bidi)
validator.error( `An element can only have one '${hasIntro ? 'in' : 'out'}' directive`, attribute.start ); validator.error(
`An element cannot have both an '${hasIntro
? 'in'
: 'out'}' directive and a 'transition' directive`,
attribute.start
);
validator.error(
`An element can only have one '${hasIntro ? 'in' : 'out'}' directive`,
attribute.start
);
} }
if ( attribute.intro ) hasIntro = true; if (attribute.intro) hasIntro = true;
if ( attribute.outro ) hasOutro = true; if (attribute.outro) hasOutro = true;
if ( bidi ) hasTransition = true; if (bidi) hasTransition = true;
if ( !validator.transitions.has( attribute.name ) ) { if (!validator.transitions.has(attribute.name)) {
validator.error( `Missing transition '${attribute.name}'`, attribute.start ); validator.error(
`Missing transition '${attribute.name}'`,
attribute.start
);
} }
} } else if (attribute.type === 'Attribute') {
if (attribute.name === 'value' && node.name === 'textarea') {
else if ( attribute.type === 'Attribute' ) { if (node.children.length) {
if ( attribute.name === 'value' && node.name === 'textarea' ) { validator.error(
if ( node.children.length ) { `A <textarea> can have either a value attribute or (equivalently) child content, but not both`,
validator.error( `A <textarea> can have either a value attribute or (equivalently) child content, but not both`, attribute.start ); attribute.start
);
} }
} }
} }
}); });
} }
function getType ( validator: Validator, node: Node ) { function getType(validator: Validator, node: Node) {
const attribute = node.attributes.find( ( attribute: Node ) => attribute.name === 'type' ); const attribute = node.attributes.find(
if ( !attribute ) return null; (attribute: Node) => attribute.name === 'type'
);
if (!attribute) return null;
if ( attribute.value === true ) { if (attribute.value === true) {
validator.error( `'type' attribute must be specified`, attribute.start ); validator.error(`'type' attribute must be specified`, attribute.start);
} }
if ( attribute.value.length > 1 || attribute.value[0].type !== 'Text' ) { if (attribute.value.length > 1 || attribute.value[0].type !== 'Text') {
validator.error( `'type attribute cannot be dynamic`, attribute.start ); validator.error(`'type attribute cannot be dynamic`, attribute.start);
} }
return attribute.value[0].data; return attribute.value[0].data;

@ -3,35 +3,40 @@ import list from '../utils/list';
import { Validator } from '../index'; import { Validator } from '../index';
import { Node } from '../../interfaces'; import { Node } from '../../interfaces';
const validBuiltins = new Set([ const validBuiltins = new Set(['set', 'fire', 'destroy']);
'set',
'fire',
'destroy'
]);
export default function validateEventHandlerCallee ( validator: Validator, attribute: Node ) { export default function validateEventHandlerCallee(
validator: Validator,
attribute: Node
) {
const { callee, start, type } = attribute.expression; const { callee, start, type } = attribute.expression;
if ( type !== 'CallExpression' ) { if (type !== 'CallExpression') {
validator.error( `Expected a call expression`, start ); validator.error(`Expected a call expression`, start);
} }
const { name } = flattenReference( callee ); const { name } = flattenReference(callee);
if ( name === 'this' || name === 'event' ) return; if (name === 'this' || name === 'event') return;
if ( callee.type === 'Identifier' && validBuiltins.has( callee.name ) || validator.methods.has( callee.name ) ) return; if (
(callee.type === 'Identifier' && validBuiltins.has(callee.name)) ||
validator.methods.has(callee.name)
)
return;
const validCallees = [ 'this.*', 'event.*' ] const validCallees = ['this.*', 'event.*'].concat(
.concat( Array.from(validBuiltins),
Array.from( validBuiltins ), Array.from(validator.methods.keys())
Array.from( validator.methods.keys() ) );
);
let message = `'${validator.source.slice( callee.start, callee.end )}' is an invalid callee (should be one of ${list( validCallees )})`; let message = `'${validator.source.slice(
callee.start,
callee.end
)}' is an invalid callee (should be one of ${list(validCallees)})`;
if ( callee.type === 'Identifier' && validator.helpers.has( callee.name ) ) { if (callee.type === 'Identifier' && validator.helpers.has(callee.name)) {
message += `. '${callee.name}' exists on 'helpers', did you put it in the wrong place?`; message += `. '${callee.name}' exists on 'helpers', did you put it in the wrong place?`;
} }
validator.warn( message, start ); validator.warn(message, start);
} }

@ -14,37 +14,43 @@ const validBindings = [
'scrollY' 'scrollY'
]; ];
export default function validateWindow ( validator: Validator, node: Node ) { export default function validateWindow(validator: Validator, node: Node) {
node.attributes.forEach( ( attribute: Node ) => { node.attributes.forEach((attribute: Node) => {
if ( attribute.type === 'Binding' ) { if (attribute.type === 'Binding') {
if ( attribute.value.type !== 'Identifier' ) { if (attribute.value.type !== 'Identifier') {
const { parts } = flattenReference( attribute.value ); const { parts } = flattenReference(attribute.value);
validator.error( validator.error(
`Bindings on <:Window/> must be to top-level properties, e.g. '${parts[ parts.length - 1 ]}' rather than '${parts.join( '.' )}'`, `Bindings on <:Window/> must be to top-level properties, e.g. '${parts[
parts.length - 1
]}' rather than '${parts.join('.')}'`,
attribute.value.start attribute.value.start
); );
} }
if ( !~validBindings.indexOf( attribute.name ) ) { if (!~validBindings.indexOf(attribute.name)) {
const match = ( const match = attribute.name === 'width'
attribute.name === 'width' ? 'innerWidth' : ? 'innerWidth'
attribute.name === 'height' ? 'innerHeight' : : attribute.name === 'height'
fuzzymatch( attribute.name, validBindings ) ? 'innerHeight'
); : fuzzymatch(attribute.name, validBindings);
const message = `'${attribute.name}' is not a valid binding on <:Window>`; const message = `'${attribute.name}' is not a valid binding on <:Window>`;
if ( match ) { if (match) {
validator.error( `${message} (did you mean '${match}'?)`, attribute.start ); validator.error(
`${message} (did you mean '${match}'?)`,
attribute.start
);
} else { } else {
validator.error( `${message} — valid bindings are ${list( validBindings )}`, attribute.start ); validator.error(
`${message} — valid bindings are ${list(validBindings)}`,
attribute.start
);
} }
} }
} } else if (attribute.type === 'EventHandler') {
validateEventHandler(validator, attribute);
else if ( attribute.type === 'EventHandler' ) {
validateEventHandler( validator, attribute );
} }
}); });
} }

@ -2,12 +2,17 @@ import validateJs from './js/index';
import validateHtml from './html/index'; import validateHtml from './html/index';
import { getLocator, Location } from 'locate-character'; import { getLocator, Location } from 'locate-character';
import getCodeFrame from '../utils/getCodeFrame'; import getCodeFrame from '../utils/getCodeFrame';
import CompileError from '../utils/CompileError' import CompileError from '../utils/CompileError';
import { Node, Parsed, CompileOptions, Warning } from '../interfaces'; import { Node, Parsed, CompileOptions, Warning } from '../interfaces';
class ValidationError extends CompileError { class ValidationError extends CompileError {
constructor ( message: string, template: string, index: number, filename: string ) { constructor(
super( message, template, index, filename ); message: string,
template: string,
index: number,
filename: string
) {
super(message, template, index, filename);
this.name = 'ValidationError'; this.name = 'ValidationError';
} }
} }
@ -27,7 +32,7 @@ export class Validator {
helpers: Map<string, Node>; helpers: Map<string, Node>;
transitions: Map<string, Node>; transitions: Map<string, Node>;
constructor ( parsed: Parsed, source: string, options: CompileOptions ) { constructor(parsed: Parsed, source: string, options: CompileOptions) {
this.source = source; this.source = source;
this.filename = options !== undefined ? options.filename : undefined; this.filename = options !== undefined ? options.filename : undefined;
@ -43,15 +48,15 @@ export class Validator {
this.transitions = new Map(); this.transitions = new Map();
} }
error ( message: string, pos: number ) { error(message: string, pos: number) {
throw new ValidationError( message, this.source, pos, this.filename ); throw new ValidationError(message, this.source, pos, this.filename);
} }
warn ( message: string, pos: number ) { warn(message: string, pos: number) {
if ( !this.locator ) this.locator = getLocator( this.source ); if (!this.locator) this.locator = getLocator(this.source);
const { line, column } = this.locator( pos ); const { line, column } = this.locator(pos);
const frame = getCodeFrame( this.source, line, column ); const frame = getCodeFrame(this.source, line, column);
this.onwarn({ this.onwarn({
message, message,
@ -64,16 +69,20 @@ export class Validator {
} }
} }
export default function validate ( parsed: Parsed, source: string, options: CompileOptions ) { export default function validate(
parsed: Parsed,
source: string,
options: CompileOptions
) {
const { onwarn, onerror, name, filename } = options; const { onwarn, onerror, name, filename } = options;
try { try {
if ( name && !/^[a-zA-Z_$][a-zA-Z_$0-9]*$/.test( name ) ) { if (name && !/^[a-zA-Z_$][a-zA-Z_$0-9]*$/.test(name)) {
const error = new Error( `options.name must be a valid identifier` ); const error = new Error(`options.name must be a valid identifier`);
throw error; throw error;
} }
if ( name && !/^[A-Z]/.test( name ) ) { if (name && !/^[A-Z]/.test(name)) {
const message = `options.name should be capitalised`; const message = `options.name should be capitalised`;
onwarn({ onwarn({
message, message,
@ -82,22 +91,22 @@ export default function validate ( parsed: Parsed, source: string, options: Comp
}); });
} }
const validator = new Validator( parsed, source, { const validator = new Validator(parsed, source, {
onwarn, onwarn,
name, name,
filename filename
}); });
if ( parsed.js ) { if (parsed.js) {
validateJs( validator, parsed.js ); validateJs(validator, parsed.js);
} }
if ( parsed.html ) { if (parsed.html) {
validateHtml( validator, parsed.html ); validateHtml(validator, parsed.html);
} }
} catch ( err ) { } catch (err) {
if ( onerror ) { if (onerror) {
onerror( err ); onerror(err);
} else { } else {
throw err; throw err;
} }

@ -6,69 +6,89 @@ import namespaces from '../../utils/namespaces';
import { Validator } from '../'; import { Validator } from '../';
import { Node } from '../../interfaces'; import { Node } from '../../interfaces';
const validPropList = Object.keys( propValidators ); const validPropList = Object.keys(propValidators);
export default function validateJs ( validator: Validator, js: Node ) { export default function validateJs(validator: Validator, js: Node) {
js.content.body.forEach( ( node: Node ) => { js.content.body.forEach((node: Node) => {
// check there are no named exports // check there are no named exports
if ( node.type === 'ExportNamedDeclaration' ) { if (node.type === 'ExportNamedDeclaration') {
validator.error( `A component can only have a default export`, node.start ); validator.error(`A component can only have a default export`, node.start);
} }
if ( node.type === 'ExportDefaultDeclaration' ) { if (node.type === 'ExportDefaultDeclaration') {
if ( node.declaration.type !== 'ObjectExpression' ) { if (node.declaration.type !== 'ObjectExpression') {
return validator.error( `Default export must be an object literal`, node.declaration.start ); return validator.error(
`Default export must be an object literal`,
node.declaration.start
);
} }
checkForComputedKeys( validator, node.declaration.properties ); checkForComputedKeys(validator, node.declaration.properties);
checkForDupes( validator, node.declaration.properties ); checkForDupes(validator, node.declaration.properties);
const props = validator.properties; const props = validator.properties;
node.declaration.properties.forEach( ( prop: Node ) => { node.declaration.properties.forEach((prop: Node) => {
props.set( prop.key.name, prop ); props.set(prop.key.name, prop);
}); });
// Remove these checks in version 2 // Remove these checks in version 2
if ( props.has( 'oncreate' ) && props.has( 'onrender' ) ) { if (props.has('oncreate') && props.has('onrender')) {
validator.error( 'Cannot have both oncreate and onrender', props.get( 'onrender' ).start ); validator.error(
'Cannot have both oncreate and onrender',
props.get('onrender').start
);
} }
if ( props.has( 'ondestroy' ) && props.has( 'onteardown' ) ) { if (props.has('ondestroy') && props.has('onteardown')) {
validator.error( 'Cannot have both ondestroy and onteardown', props.get( 'onteardown' ).start ); validator.error(
'Cannot have both ondestroy and onteardown',
props.get('onteardown').start
);
} }
// ensure all exported props are valid // ensure all exported props are valid
node.declaration.properties.forEach( ( prop: Node ) => { node.declaration.properties.forEach((prop: Node) => {
const propValidator = propValidators[ prop.key.name ]; const propValidator = propValidators[prop.key.name];
if ( propValidator ) { if (propValidator) {
propValidator( validator, prop ); propValidator(validator, prop);
} else { } else {
const match = fuzzymatch( prop.key.name, validPropList ); const match = fuzzymatch(prop.key.name, validPropList);
if ( match ) { if (match) {
validator.error( `Unexpected property '${prop.key.name}' (did you mean '${match}'?)`, prop.start ); validator.error(
} else if ( /FunctionExpression/.test( prop.value.type ) ) { `Unexpected property '${prop.key
validator.error( `Unexpected property '${prop.key.name}' (did you mean to include it in 'methods'?)`, prop.start ); .name}' (did you mean '${match}'?)`,
prop.start
);
} else if (/FunctionExpression/.test(prop.value.type)) {
validator.error(
`Unexpected property '${prop.key
.name}' (did you mean to include it in 'methods'?)`,
prop.start
);
} else { } else {
validator.error( `Unexpected property '${prop.key.name}'`, prop.start ); validator.error(
`Unexpected property '${prop.key.name}'`,
prop.start
);
} }
} }
}); });
if ( props.has( 'namespace' ) ) { if (props.has('namespace')) {
const ns = props.get( 'namespace' ).value.value; const ns = props.get('namespace').value.value;
validator.namespace = namespaces[ ns ] || ns; validator.namespace = namespaces[ns] || ns;
} }
validator.defaultExport = node; validator.defaultExport = node;
} }
}); });
[ 'components', 'methods', 'helpers', 'transitions' ].forEach( key => { ['components', 'methods', 'helpers', 'transitions'].forEach(key => {
if ( validator.properties.has( key ) ) { if (validator.properties.has(key)) {
validator.properties.get( key ).value.properties.forEach( ( prop: Node ) => { validator.properties.get(key).value.properties.forEach((prop: Node) => {
validator[ key ].set( prop.key.name, prop.value ); validator[key].set(prop.key.name, prop.value);
}); });
} }
}); });

@ -3,22 +3,28 @@ import checkForComputedKeys from '../utils/checkForComputedKeys';
import { Validator } from '../../'; import { Validator } from '../../';
import { Node } from '../../../interfaces'; import { Node } from '../../../interfaces';
export default function components ( validator: Validator, prop: Node ) { export default function components(validator: Validator, prop: Node) {
if ( prop.value.type !== 'ObjectExpression' ) { if (prop.value.type !== 'ObjectExpression') {
validator.error( `The 'components' property must be an object literal`, prop.start ); validator.error(
`The 'components' property must be an object literal`,
prop.start
);
return; return;
} }
checkForDupes( validator, prop.value.properties ); checkForDupes(validator, prop.value.properties);
checkForComputedKeys( validator, prop.value.properties ); checkForComputedKeys(validator, prop.value.properties);
prop.value.properties.forEach( ( component: Node ) => { prop.value.properties.forEach((component: Node) => {
if ( component.key.name === 'state' ) { if (component.key.name === 'state') {
validator.error( `Component constructors cannot be called 'state' due to technical limitations`, component.start ); validator.error(
`Component constructors cannot be called 'state' due to technical limitations`,
component.start
);
} }
if ( !/^[A-Z]/.test( component.key.name ) ) { if (!/^[A-Z]/.test(component.key.name)) {
validator.warn( `Component names should be capitalised`, component.start ); validator.warn(`Component names should be capitalised`, component.start);
} }
}); });
} }

@ -3,35 +3,53 @@ import checkForComputedKeys from '../utils/checkForComputedKeys';
import { Validator } from '../../'; import { Validator } from '../../';
import { Node } from '../../../interfaces'; import { Node } from '../../../interfaces';
const isFunctionExpression = new Set( [ 'FunctionExpression', 'ArrowFunctionExpression' ] ); const isFunctionExpression = new Set([
'FunctionExpression',
export default function computed ( validator: Validator, prop: Node ) { 'ArrowFunctionExpression'
if ( prop.value.type !== 'ObjectExpression' ) { ]);
validator.error( `The 'computed' property must be an object literal`, prop.start );
export default function computed(validator: Validator, prop: Node) {
if (prop.value.type !== 'ObjectExpression') {
validator.error(
`The 'computed' property must be an object literal`,
prop.start
);
return; return;
} }
checkForDupes( validator, prop.value.properties ); checkForDupes(validator, prop.value.properties);
checkForComputedKeys( validator, prop.value.properties ); checkForComputedKeys(validator, prop.value.properties);
prop.value.properties.forEach( ( computation: Node ) => { prop.value.properties.forEach((computation: Node) => {
if ( !isFunctionExpression.has( computation.value.type ) ) { if (!isFunctionExpression.has(computation.value.type)) {
validator.error( `Computed properties can be function expressions or arrow function expressions`, computation.value.start ); validator.error(
`Computed properties can be function expressions or arrow function expressions`,
computation.value.start
);
return; return;
} }
const params = computation.value.params; const params = computation.value.params;
if ( params.length === 0 ) { if (params.length === 0) {
validator.error( `A computed value must depend on at least one property`, computation.value.start ); validator.error(
`A computed value must depend on at least one property`,
computation.value.start
);
return; return;
} }
params.forEach( ( param: Node ) => { params.forEach((param: Node) => {
const valid = param.type === 'Identifier' || param.type === 'AssignmentPattern' && param.left.type === 'Identifier'; const valid =
param.type === 'Identifier' ||
if ( !valid ) { (param.type === 'AssignmentPattern' &&
validator.error( `Computed properties cannot use destructuring in function parameters`, param.start ); param.left.type === 'Identifier');
if (!valid) {
validator.error(
`Computed properties cannot use destructuring in function parameters`,
param.start
);
} }
}); });
}); });

@ -1,14 +1,14 @@
import { Validator } from '../../'; import { Validator } from '../../';
import { Node } from '../../../interfaces'; import { Node } from '../../../interfaces';
const disallowed = new Set([ 'Literal', 'ObjectExpression', 'ArrayExpression' ]); const disallowed = new Set(['Literal', 'ObjectExpression', 'ArrayExpression']);
export default function data ( validator: Validator, prop: Node ) { export default function data(validator: Validator, prop: Node) {
while ( prop.type === 'ParenthesizedExpression' ) prop = prop.expression; while (prop.type === 'ParenthesizedExpression') prop = prop.expression;
// TODO should we disallow references and expressions as well? // TODO should we disallow references and expressions as well?
if ( disallowed.has( prop.value.type ) ) { if (disallowed.has(prop.value.type)) {
validator.error( `'data' must be a function`, prop.value.start ); validator.error(`'data' must be a function`, prop.value.start);
} }
} }

@ -3,12 +3,15 @@ import checkForComputedKeys from '../utils/checkForComputedKeys';
import { Validator } from '../../'; import { Validator } from '../../';
import { Node } from '../../../interfaces'; import { Node } from '../../../interfaces';
export default function events ( validator: Validator, prop: Node ) { export default function events(validator: Validator, prop: Node) {
if ( prop.value.type !== 'ObjectExpression' ) { if (prop.value.type !== 'ObjectExpression') {
validator.error( `The 'events' property must be an object literal`, prop.start ); validator.error(
`The 'events' property must be an object literal`,
prop.start
);
return; return;
} }
checkForDupes( validator, prop.value.properties ); checkForDupes(validator, prop.value.properties);
checkForComputedKeys( validator, prop.value.properties ); checkForComputedKeys(validator, prop.value.properties);
} }

@ -4,53 +4,67 @@ import { walk } from 'estree-walker';
import { Validator } from '../../'; import { Validator } from '../../';
import { Node } from '../../../interfaces'; import { Node } from '../../../interfaces';
export default function helpers ( validator: Validator, prop: Node ) { export default function helpers(validator: Validator, prop: Node) {
if ( prop.value.type !== 'ObjectExpression' ) { if (prop.value.type !== 'ObjectExpression') {
validator.error( `The 'helpers' property must be an object literal`, prop.start ); validator.error(
`The 'helpers' property must be an object literal`,
prop.start
);
return; return;
} }
checkForDupes( validator, prop.value.properties ); checkForDupes(validator, prop.value.properties);
checkForComputedKeys( validator, prop.value.properties ); checkForComputedKeys(validator, prop.value.properties);
prop.value.properties.forEach( ( prop: Node ) => { prop.value.properties.forEach((prop: Node) => {
if ( !/FunctionExpression/.test( prop.value.type ) ) return; if (!/FunctionExpression/.test(prop.value.type)) return;
let lexicalDepth = 0; let lexicalDepth = 0;
let usesArguments = false; let usesArguments = false;
walk( prop.value.body, { walk(prop.value.body, {
enter ( node: Node ) { enter(node: Node) {
if ( /^Function/.test( node.type ) ) { if (/^Function/.test(node.type)) {
lexicalDepth += 1; lexicalDepth += 1;
} } else if (lexicalDepth === 0) {
else if ( lexicalDepth === 0 ) {
// handle special case that's caused some people confusion — using `this.get(...)` instead of passing argument // handle special case that's caused some people confusion — using `this.get(...)` instead of passing argument
// TODO do the same thing for computed values? // TODO do the same thing for computed values?
if ( node.type === 'CallExpression' && node.callee.type === 'MemberExpression' && node.callee.object.type === 'ThisExpression' && node.callee.property.name === 'get' && !node.callee.property.computed ) { if (
validator.error( `Cannot use this.get(...) — it must be passed into the helper function as an argument`, node.start ); node.type === 'CallExpression' &&
} node.callee.type === 'MemberExpression' &&
node.callee.object.type === 'ThisExpression' &&
if ( node.type === 'ThisExpression' ) { node.callee.property.name === 'get' &&
validator.error( `Helpers should be pure functions — they do not have access to the component instance and cannot use 'this'. Did you mean to put this in 'methods'?`, node.start ); !node.callee.property.computed
) {
validator.error(
`Cannot use this.get(...) — it must be passed into the helper function as an argument`,
node.start
);
} }
else if ( node.type === 'Identifier' && node.name === 'arguments' ) { if (node.type === 'ThisExpression') {
validator.error(
`Helpers should be pure functions — they do not have access to the component instance and cannot use 'this'. Did you mean to put this in 'methods'?`,
node.start
);
} else if (node.type === 'Identifier' && node.name === 'arguments') {
usesArguments = true; usesArguments = true;
} }
} }
}, },
leave ( node: Node ) { leave(node: Node) {
if ( /^Function/.test( node.type ) ) { if (/^Function/.test(node.type)) {
lexicalDepth -= 1; lexicalDepth -= 1;
} }
} }
}); });
if ( prop.value.params.length === 0 && !usesArguments ) { if (prop.value.params.length === 0 && !usesArguments) {
validator.warn( `Helpers should be pure functions, with at least one argument`, prop.start ); validator.warn(
`Helpers should be pure functions, with at least one argument`,
prop.start
);
} }
}); });
} }

@ -5,26 +5,36 @@ import usesThisOrArguments from '../utils/usesThisOrArguments';
import { Validator } from '../../'; import { Validator } from '../../';
import { Node } from '../../../interfaces'; import { Node } from '../../../interfaces';
const builtin = new Set( [ 'set', 'get', 'on', 'fire', 'observe', 'destroy' ] ); const builtin = new Set(['set', 'get', 'on', 'fire', 'observe', 'destroy']);
export default function methods ( validator: Validator, prop: Node ) { export default function methods(validator: Validator, prop: Node) {
if ( prop.value.type !== 'ObjectExpression' ) { if (prop.value.type !== 'ObjectExpression') {
validator.error( `The 'methods' property must be an object literal`, prop.start ); validator.error(
`The 'methods' property must be an object literal`,
prop.start
);
return; return;
} }
checkForAccessors( validator, prop.value.properties, 'Methods' ); checkForAccessors(validator, prop.value.properties, 'Methods');
checkForDupes( validator, prop.value.properties ); checkForDupes(validator, prop.value.properties);
checkForComputedKeys( validator, prop.value.properties ); checkForComputedKeys(validator, prop.value.properties);
prop.value.properties.forEach( ( prop: Node ) => { prop.value.properties.forEach((prop: Node) => {
if ( builtin.has( prop.key.name ) ) { if (builtin.has(prop.key.name)) {
validator.error( `Cannot overwrite built-in method '${prop.key.name}'`, prop.start ); validator.error(
`Cannot overwrite built-in method '${prop.key.name}'`,
prop.start
);
} }
if ( prop.value.type === 'ArrowFunctionExpression' ) { if (prop.value.type === 'ArrowFunctionExpression') {
if ( usesThisOrArguments( prop.value.body ) ) { if (usesThisOrArguments(prop.value.body)) {
validator.error( `Method '${prop.key.name}' should be a function expression, not an arrow function expression`, prop.start ); validator.error(
`Method '${prop.key
.name}' should be a function expression, not an arrow function expression`,
prop.start
);
} }
} }
}); });

@ -3,21 +3,27 @@ import fuzzymatch from '../../utils/fuzzymatch';
import { Validator } from '../../'; import { Validator } from '../../';
import { Node } from '../../../interfaces'; import { Node } from '../../../interfaces';
const valid = new Set( namespaces.validNamespaces ); const valid = new Set(namespaces.validNamespaces);
export default function namespace ( validator: Validator, prop: Node ) { export default function namespace(validator: Validator, prop: Node) {
const ns = prop.value.value; const ns = prop.value.value;
if ( prop.value.type !== 'Literal' || typeof ns !== 'string' ) { if (prop.value.type !== 'Literal' || typeof ns !== 'string') {
validator.error( `The 'namespace' property must be a string literal representing a valid namespace`, prop.start ); validator.error(
`The 'namespace' property must be a string literal representing a valid namespace`,
prop.start
);
} }
if ( !valid.has( ns ) ) { if (!valid.has(ns)) {
const match = fuzzymatch( ns, namespaces.validNamespaces ); const match = fuzzymatch(ns, namespaces.validNamespaces);
if ( match ) { if (match) {
validator.error( `Invalid namespace '${ns}' (did you mean '${match}'?)`, prop.start ); validator.error(
`Invalid namespace '${ns}' (did you mean '${match}'?)`,
prop.start
);
} else { } else {
validator.error( `Invalid namespace '${ns}'`, prop.start ); validator.error(`Invalid namespace '${ns}'`, prop.start);
} }
} }
} }

@ -2,10 +2,13 @@ import usesThisOrArguments from '../utils/usesThisOrArguments';
import { Validator } from '../../'; import { Validator } from '../../';
import { Node } from '../../../interfaces'; import { Node } from '../../../interfaces';
export default function oncreate ( validator: Validator, prop: Node ) { export default function oncreate(validator: Validator, prop: Node) {
if ( prop.value.type === 'ArrowFunctionExpression' ) { if (prop.value.type === 'ArrowFunctionExpression') {
if ( usesThisOrArguments( prop.value.body ) ) { if (usesThisOrArguments(prop.value.body)) {
validator.error( `'oncreate' should be a function expression, not an arrow function expression`, prop.start ); validator.error(
`'oncreate' should be a function expression, not an arrow function expression`,
prop.start
);
} }
} }
} }

@ -2,10 +2,13 @@ import usesThisOrArguments from '../utils/usesThisOrArguments';
import { Validator } from '../../'; import { Validator } from '../../';
import { Node } from '../../../interfaces'; import { Node } from '../../../interfaces';
export default function ondestroy ( validator: Validator, prop: Node ) { export default function ondestroy(validator: Validator, prop: Node) {
if ( prop.value.type === 'ArrowFunctionExpression' ) { if (prop.value.type === 'ArrowFunctionExpression') {
if ( usesThisOrArguments( prop.value.body ) ) { if (usesThisOrArguments(prop.value.body)) {
validator.error( `'ondestroy' should be a function expression, not an arrow function expression`, prop.start ); validator.error(
`'ondestroy' should be a function expression, not an arrow function expression`,
prop.start
);
} }
} }
} }

@ -2,7 +2,10 @@ import oncreate from './oncreate';
import { Validator } from '../../'; import { Validator } from '../../';
import { Node } from '../../../interfaces'; import { Node } from '../../../interfaces';
export default function onrender ( validator: Validator, prop: Node ) { export default function onrender(validator: Validator, prop: Node) {
validator.warn( `'onrender' has been deprecated in favour of 'oncreate', and will cause an error in Svelte 2.x`, prop.start ); validator.warn(
oncreate( validator, prop ); `'onrender' has been deprecated in favour of 'oncreate', and will cause an error in Svelte 2.x`,
prop.start
);
oncreate(validator, prop);
} }

@ -2,7 +2,10 @@ import ondestroy from './ondestroy';
import { Validator } from '../../'; import { Validator } from '../../';
import { Node } from '../../../interfaces'; import { Node } from '../../../interfaces';
export default function onteardown ( validator: Validator, prop: Node ) { export default function onteardown(validator: Validator, prop: Node) {
validator.warn( `'onteardown' has been deprecated in favour of 'ondestroy', and will cause an error in Svelte 2.x`, prop.start ); validator.warn(
ondestroy( validator, prop ); `'onteardown' has been deprecated in favour of 'ondestroy', and will cause an error in Svelte 2.x`,
prop.start
);
ondestroy(validator, prop);
} }

@ -3,16 +3,19 @@ import checkForComputedKeys from '../utils/checkForComputedKeys';
import { Validator } from '../../'; import { Validator } from '../../';
import { Node } from '../../../interfaces'; import { Node } from '../../../interfaces';
export default function transitions ( validator: Validator, prop: Node ) { export default function transitions(validator: Validator, prop: Node) {
if ( prop.value.type !== 'ObjectExpression' ) { if (prop.value.type !== 'ObjectExpression') {
validator.error( `The 'transitions' property must be an object literal`, prop.start ); validator.error(
`The 'transitions' property must be an object literal`,
prop.start
);
return; return;
} }
checkForDupes( validator, prop.value.properties ); checkForDupes(validator, prop.value.properties);
checkForComputedKeys( validator, prop.value.properties ); checkForComputedKeys(validator, prop.value.properties);
prop.value.properties.forEach( () => { prop.value.properties.forEach(() => {
// TODO probably some validation that can happen here... // TODO probably some validation that can happen here...
// checking for use of `this` etc? // checking for use of `this` etc?
}); });

@ -1,10 +1,14 @@
import { Validator } from '../../'; import { Validator } from '../../';
import { Node } from '../../../interfaces'; import { Node } from '../../../interfaces';
export default function checkForAccessors ( validator: Validator, properties: Node[], label: string ) { export default function checkForAccessors(
properties.forEach( prop => { validator: Validator,
if ( prop.kind !== 'init' ) { properties: Node[],
validator.error( `${label} cannot use getters and setters`, prop.start ); label: string
) {
properties.forEach(prop => {
if (prop.kind !== 'init') {
validator.error(`${label} cannot use getters and setters`, prop.start);
} }
}); });
} }

@ -1,10 +1,13 @@
import { Validator } from '../../'; import { Validator } from '../../';
import { Node } from '../../../interfaces'; import { Node } from '../../../interfaces';
export default function checkForComputedKeys ( validator: Validator, properties: Node[] ) { export default function checkForComputedKeys(
properties.forEach( prop => { validator: Validator,
if ( prop.key.computed ) { properties: Node[]
validator.error( `Cannot use computed keys`, prop.start ); ) {
properties.forEach(prop => {
if (prop.key.computed) {
validator.error(`Cannot use computed keys`, prop.start);
} }
}); });
} }

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

Loading…
Cancel
Save