Merge pull request from sveltejs/prettier

Prettier
pull/628/head
Rich Harris 8 years ago committed by GitHub
commit fe75570b91

@ -5,7 +5,6 @@
"semi": [ 2, "always" ],
"keyword-spacing": [ 2, { "before": true, "after": true } ],
"space-before-blocks": [ 2, "always" ],
"space-before-function-paren": [ 2, "always" ],
"no-mixed-spaces-and-tabs": [ 2, "smart-tabs" ],
"no-cond-assign": 0,
"no-unused-vars": 2,

@ -23,7 +23,8 @@
"dev": "node src/shared/_build.js && rollup -c rollup/rollup.config.main.js -w",
"dev:shared": "rollup -c rollup/rollup.config.shared.js -w",
"pretest": "npm run build",
"prepublish": "npm run build && npm run lint"
"prepublish": "npm run build && npm run lint",
"prettier": "prettier --use-tabs --single-quote --trailing-comma es5 --write \"src/**/*.ts\""
},
"repository": {
"type": "git",
@ -70,6 +71,7 @@
"mocha": "^3.2.0",
"node-resolve": "^1.3.3",
"nyc": "^10.0.0",
"prettier": "^1.4.1",
"reify": "^0.4.4",
"rollup": "^0.39.0",
"rollup-plugin-buble": "^0.15.0",

@ -1,9 +0,0 @@
const glob = require( 'glob' );
const fs = require( 'fs' );
glob.sync( 'src/**/*.js' ).forEach( file => {
console.log( file );
const js = fs.readFileSync( file, 'utf-8' );
fs.writeFileSync( file.replace( /\.js$/, '.ts' ), js );
fs.unlinkSync( file );
});

@ -4,22 +4,25 @@ import commonjs from 'rollup-plugin-commonjs';
import json from 'rollup-plugin-json';
import typescript from 'rollup-plugin-typescript';
const src = path.resolve( 'src' );
const src = path.resolve('src');
export default {
entry: 'src/index.ts',
moduleName: 'svelte',
targets: [
{ dest: 'compiler/svelte.js', format: 'umd' }
],
targets: [{ dest: 'compiler/svelte.js', format: 'umd' }],
plugins: [
{
resolveId ( importee, importer ) {
resolveId(importee, importer) {
// bit of a hack — TypeScript only really works if it can resolve imports,
// but they misguidedly chose to reject imports with file extensions. This
// means we need to resolve them here
if ( importer && importer.startsWith( src ) && importee[0] === '.' && path.extname( importee ) === '' ) {
return path.resolve( path.dirname( importer ), `${importee}.ts` );
if (
importer &&
importer.startsWith(src) &&
importee[0] === '.' &&
path.extname(importee) === ''
) {
return path.resolve(path.dirname(importer), `${importee}.ts`);
}
}
},
@ -29,7 +32,7 @@ export default {
typescript({
include: 'src/**',
exclude: 'src/shared/**',
typescript: require( 'typescript' )
typescript: require('typescript')
})
],
sourceMap: true

@ -6,9 +6,7 @@ import buble from 'rollup-plugin-buble';
export default {
entry: 'src/server-side-rendering/register.js',
moduleName: 'svelte',
targets: [
{ dest: 'ssr/register.js', format: 'cjs' }
],
targets: [{ dest: 'ssr/register.js', format: 'cjs' }],
plugins: [
nodeResolve({ jsnext: true, module: true }),
commonjs(),
@ -20,9 +18,9 @@ export default {
}
})
],
external: [ path.resolve( 'src/index.ts' ), 'fs', 'path' ],
external: [path.resolve('src/index.ts'), 'fs', 'path'],
paths: {
[ path.resolve( 'src/index.ts' ) ]: '../compiler/svelte.js'
[path.resolve('src/index.ts')]: '../compiler/svelte.js'
},
sourceMap: true
};

@ -42,7 +42,12 @@ export default class Generator {
aliases: Map<string, 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.source = source;
this.name = name;
@ -61,9 +66,9 @@ export default class Generator {
// in dev mode
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.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.usesRefs = false;
@ -71,258 +76,302 @@ export default class Generator {
// Svelte's builtin `import { get, ... } from 'svelte/shared.ts'`;
this.importedNames = new Set();
this.aliases = new Map();
this.usedNames = new Set( [ name ] );
this.usedNames = new Set([name]);
}
addSourcemapLocations ( node: Node ) {
walk( node, {
enter: ( node: Node ) => {
this.code.addSourcemapLocation( node.start );
this.code.addSourcemapLocation( node.end );
}
addSourcemapLocations(node: Node) {
walk(node, {
enter: (node: Node) => {
this.code.addSourcemapLocation(node.start);
this.code.addSourcemapLocation(node.end);
},
});
}
alias ( name: string ) {
if ( !this.aliases.has( name ) ) {
this.aliases.set( name, this.getUniqueName( name ) );
alias(name: string) {
if (!this.aliases.has(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 ) {
this.addSourcemapLocations( expression );
contextualise(
block: DomBlock | SsrBlock,
expression: Node,
context: string,
isEventHandler: boolean
) {
this.addSourcemapLocations(expression);
const usedContexts: string[] = [];
const { code, helpers } = this;
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;
const self = this;
walk( expression, {
enter ( node: Node, parent: Node, key: string ) {
if ( /^Function/.test( node.type ) ) lexicalDepth += 1;
walk(expression, {
enter(node: Node, parent: Node, key: string) {
if (/^Function/.test(node.type)) lexicalDepth += 1;
if ( node._scope ) {
if (node._scope) {
scope = node._scope;
return;
}
if ( node.type === 'ThisExpression' ) {
if ( lexicalDepth === 0 && context ) code.overwrite( node.start, node.end, context, { storeName: true, contentOnly: false } );
}
else if ( isReference( node, parent ) ) {
const { name } = flattenReference( node );
if ( scope.has( name ) ) return;
if ( name === 'event' && isEventHandler ) {
if (node.type === 'ThisExpression') {
if (lexicalDepth === 0 && context)
code.overwrite(node.start, node.end, context, {
storeName: true,
contentOnly: false,
});
} else if (isReference(node, parent)) {
const { name } = flattenReference(node);
if (scope.has(name)) return;
if (name === 'event' && isEventHandler) {
// noop
}
else if ( contexts.has( name ) ) {
const contextName = contexts.get( name );
if ( contextName !== name ) {
} else if (contexts.has(name)) {
const contextName = contexts.get(name);
if (contextName !== name) {
// 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 );
}
else if ( helpers.has( name ) ) {
code.prependRight( node.start, `${self.alias( 'template' )}.helpers.` );
}
else if ( indexes.has( name ) ) {
const context = indexes.get( name );
if ( !~usedContexts.indexOf( context ) ) usedContexts.push( context );
}
else {
if (!~usedContexts.indexOf(name)) usedContexts.push(name);
} else if (helpers.has(name)) {
code.prependRight(node.start, `${self.alias('template')}.helpers.`);
} else if (indexes.has(name)) {
const context = indexes.get(name);
if (!~usedContexts.indexOf(context)) usedContexts.push(context);
} else {
// handle shorthand properties
if ( parent && parent.type === 'Property' && parent.shorthand ) {
if ( key === 'key' ) {
code.appendLeft( node.start, `${name}: ` );
if (parent && parent.type === 'Property' && parent.shorthand) {
if (key === 'key') {
code.appendLeft(node.start, `${name}: `);
return;
}
}
if ( globalWhitelist.has( name ) ) {
code.prependRight( node.start, `( '${name}' in state ? state.` );
code.appendLeft( node.object ? node.object.end : node.end, ` : ${name} )` );
if (globalWhitelist.has(name)) {
code.prependRight(node.start, `( '${name}' in state ? state.`);
code.appendLeft(
node.object ? node.object.end : node.end,
` : ${name} )`
);
} 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();
}
},
leave ( node: Node ) {
if ( /^Function/.test( node.type ) ) lexicalDepth -= 1;
if ( node._scope ) scope = scope.parent;
}
leave(node: Node) {
if (/^Function/.test(node.type)) lexicalDepth -= 1;
if (node._scope) scope = scope.parent;
},
});
return {
dependencies: expression._dependencies, // TODO probably a better way to do this
contexts: usedContexts,
snippet: `[✂${expression.start}-${expression.end}✂]`
snippet: `[✂${expression.start}-${expression.end}✂]`,
};
}
findDependencies ( contextDependencies: Map<string, string[]>, indexes: Map<string, string>, expression: Node ) {
if ( expression._dependencies ) return expression._dependencies;
findDependencies(
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 generator = this; // can't use arrow functions, because of this.skip()
walk( expression, {
enter ( node: Node, parent: Node ) {
if ( node._scope ) {
walk(expression, {
enter(node: Node, parent: Node) {
if (node._scope) {
scope = node._scope;
return;
}
if ( isReference( node, parent ) ) {
const { name } = flattenReference( node );
if ( scope.has( name ) || generator.helpers.has( name ) ) return;
if (isReference(node, parent)) {
const { name } = flattenReference(node);
if (scope.has(name) || generator.helpers.has(name)) return;
if ( contextDependencies.has( name ) ) {
dependencies.push( ...contextDependencies.get( name ) );
} else if ( !indexes.has( name ) ) {
dependencies.push( name );
if (contextDependencies.has(name)) {
dependencies.push(...contextDependencies.get(name));
} else if (!indexes.has(name)) {
dependencies.push(name);
}
this.skip();
}
},
leave ( node: Node ) {
if ( node._scope ) scope = scope.parent;
}
leave(node: Node) {
if (node._scope) scope = scope.parent;
},
});
dependencies.forEach( name => {
if ( !globalWhitelist.has( name ) ) {
this.expectedProperties.add( name );
dependencies.forEach(name => {
if (!globalWhitelist.has(name)) {
this.expectedProperties.add(name);
}
});
return ( expression._dependencies = dependencies );
return (expression._dependencies = dependencies);
}
generate ( result, options, { name, format } ) {
if ( this.imports.length ) {
generate(result, options, { name, format }) {
if (this.imports.length) {
const statements: string[] = [];
this.imports.forEach( ( declaration, i ) => {
if ( format === 'es' ) {
statements.push( this.source.slice( declaration.start, declaration.end ) );
this.imports.forEach((declaration, i) => {
if (format === 'es') {
statements.push(
this.source.slice(declaration.start, declaration.end)
);
return;
}
const defaultImport = declaration.specifiers.find( ( x: Node ) => x.type === 'ImportDefaultSpecifier' || x.type === 'ImportSpecifier' && x.imported.name === 'default' );
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}`;
const defaultImport = declaration.specifiers.find(
(x: Node) =>
x.type === 'ImportDefaultSpecifier' ||
(x.type === 'ImportSpecifier' && x.imported.name === 'default')
);
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
namedImports.forEach( ( specifier: Node ) => {
statements.push( `var ${specifier.local.name} = ${name}.${specifier.imported.name}` );
namedImports.forEach((specifier: Node) => {
statements.push(
`var ${specifier.local.name} = ${name}.${specifier.imported.name}`
);
});
if ( defaultImport ) {
statements.push( `${name} = ( ${name} && ${name}.__esModule ) ? ${name}['default'] : ${name};` );
if (defaultImport) {
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 parts = result.split( '✂]' );
const parts = result.split('✂]');
const finalChunk = parts.pop();
const compiled = new Bundle({ separator: '' });
function addString ( str: string ) {
function addString(str: string) {
compiled.addSource({
content: new MagicString( str )
content: new MagicString(str),
});
}
const intro = getIntro( format, options, this.imports );
if ( intro ) addString( intro );
const intro = getIntro(format, options, this.imports);
if (intro) addString(intro);
const { filename } = options;
// 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
if ( !parts.length ) {
if (!parts.length) {
compiled.addSource({
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 ) => {
const chunk = str.replace( pattern, '' );
if ( chunk ) addString( chunk );
parts.forEach((str: string) => {
const chunk = str.replace(pattern, '');
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({
filename,
content: snippet
content: snippet,
});
});
addString( finalChunk );
addString( '\n\n' + getOutro( format, name, options, this.imports ) );
addString(finalChunk);
addString('\n\n' + getOutro(format, name, options, this.imports));
return {
code: compiled.toString(),
map: compiled.generateMap({ includeContent: true, file: options.outputFilename }),
css: this.css
map: compiled.generateMap({
includeContent: true,
file: options.outputFilename,
}),
css: this.css,
};
}
getUniqueName ( name: string ) {
if ( test ) name = `${name}$`;
getUniqueName(name: string) {
if (test) name = `${name}$`;
let alias = name;
for ( let i = 1; reservedNames.has( alias ) || this.importedNames.has( alias ) || this.usedNames.has( alias ); alias = `${name}_${i++}` );
this.usedNames.add( alias );
for (
let i = 1;
reservedNames.has(alias) ||
this.importedNames.has(alias) ||
this.usedNames.has(alias);
alias = `${name}_${i++}`
);
this.usedNames.add(alias);
return alias;
}
getUniqueNameMaker ( params ) {
const localUsedNames = new Set( params );
getUniqueNameMaker(params) {
const localUsedNames = new Set(params);
return name => {
if ( test ) name = `${name}$`;
if (test) name = `${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++}` );
localUsedNames.add( alias );
for (
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;
};
}
parseJs ( ssr: boolean = false ) {
parseJs(ssr: boolean = false) {
const { source } = this;
const { js } = this.parsed;
@ -333,154 +382,213 @@ export default class Generator {
let namespace = null;
let hasJs = !!js;
if ( js ) {
this.addSourcemapLocations( js.content );
if (js) {
this.addSourcemapLocations(js.content);
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
for ( let i = 0; i < body.length; i += 1 ) {
for (let i = 0; i < body.length; i += 1) {
const node = body[i];
if ( node.type === 'ImportDeclaration' ) {
removeNode( this.code, js.content, node );
imports.push( node );
if (node.type === 'ImportDeclaration') {
removeNode(this.code, js.content, node);
imports.push(node);
node.specifiers.forEach( ( specifier: Node ) => {
this.importedNames.add( specifier.local.name );
node.specifiers.forEach((specifier: Node) => {
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 ) {
defaultExport.declaration.properties.forEach( ( prop: Node ) => {
templateProperties[ prop.key.name ] = prop;
if (defaultExport) {
defaultExport.declaration.properties.forEach((prop: Node) => {
templateProperties[prop.key.name] = prop;
});
}
[ 'helpers', 'events', 'components', 'transitions' ].forEach( key => {
if ( templateProperties[ key ] ) {
templateProperties[ key ].value.properties.forEach( ( prop: node ) => {
this[ key ].add( prop.key.name );
['helpers', 'events', 'components', 'transitions'].forEach(key => {
if (templateProperties[key]) {
templateProperties[key].value.properties.forEach((prop: node) => {
this[key].add(prop.key.name);
});
}
});
if ( templateProperties.computed ) {
if (templateProperties.computed) {
const dependencies = new Map();
templateProperties.computed.value.properties.forEach( ( prop: Node ) => {
templateProperties.computed.value.properties.forEach((prop: Node) => {
const key = prop.key.name;
const value = prop.value;
const deps = value.params.map( ( param: Node ) => param.type === 'AssignmentPattern' ? param.left.name : param.name );
dependencies.set( key, deps );
const deps = value.params.map(
(param: Node) =>
param.type === 'AssignmentPattern' ? param.left.name : param.name
);
dependencies.set(key, deps);
});
const visited = new Set();
function visit ( key ) {
if ( !dependencies.has( key ) ) return; // not a computation
function visit(key) {
if (!dependencies.has(key)) return; // not a computation
if ( visited.has( key ) ) return;
visited.add( key );
if (visited.has(key)) return;
visited.add(key);
const deps = dependencies.get( key );
deps.forEach( visit );
const deps = dependencies.get(key);
deps.forEach(visit);
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;
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;
templateProperties.components.value.properties.forEach( ( property: Node ) => {
const key = property.key.name;
const value = source.slice( property.value.start, property.value.end );
if ( this.importedNames.has( value ) ) {
this.importedComponents.set( key, value );
} else {
hasNonImportedComponent = true;
templateProperties.components.value.properties.forEach(
(property: Node) => {
const key = property.key.name;
const value = source.slice(
property.value.start,
property.value.end
);
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
Array.from( this.importedComponents.keys() ).forEach( key => {
removeObjectKey( this.code, templateProperties.components.value, key );
Array.from(this.importedComponents.keys()).forEach(key => {
removeObjectKey(
this.code,
templateProperties.components.value,
key
);
});
} else {
// 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
if ( templateProperties.onrender ) {
if (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;
}
if ( templateProperties.onteardown ) {
if (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;
}
// in an SSR context, we don't need to include events, methods, oncreate or ondestroy
if ( ssr ) {
if ( templateProperties.oncreate ) removeNode( this.code, 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 );
if (ssr) {
if (templateProperties.oncreate)
removeNode(
this.code,
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
let hasDefaultExport = !!defaultExport;
if ( defaultExport && defaultExport.declaration.properties.length === 0 ) {
if (defaultExport && defaultExport.declaration.properties.length === 0) {
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 ( hasDefaultExport ) {
const finalNode = body[ body.length - 1 ];
if ( defaultExport === finalNode ) {
if (hasDefaultExport) {
const finalNode = body[body.length - 1];
if (defaultExport === finalNode) {
// 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 {
const { declarations } = annotateWithScopes( js );
const { declarations } = annotateWithScopes(js);
let template = 'template';
for ( let i = 1; declarations.has( template ); template = `template_${i++}` );
this.code.overwrite( defaultExport.start, defaultExport.declaration.start, `var ${template} = ` );
for (
let i = 1;
declarations.has(template);
template = `template_${i++}`
);
this.code.overwrite(
defaultExport.start,
defaultExport.declaration.start,
`var ${template} = `
);
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 );
this.code.appendLeft( finalNode.end, `\n\n${indentation}return ${template};` );
const indentation = source.slice(i, defaultExport.start);
this.code.appendLeft(
finalNode.end,
`\n\n${indentation}return ${template};`
);
}
}
// user code gets wrapped in an IIFE
if ( js.content.body.length ) {
const prefix = hasDefaultExport ? `var ${this.alias( 'template' )} = (function () {` : `(function () {`;
this.code.prependRight( js.content.start, prefix ).appendLeft( js.content.end, '}());' );
}
// if there's no need to include user code, remove it altogether
else {
this.code.remove( js.content.start, js.content.end );
if (js.content.body.length) {
const prefix = hasDefaultExport
? `var ${this.alias('template')} = (function () {`
: `(function () {`;
this.code
.prependRight(js.content.start, prefix)
.appendLeft(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;
}
}
@ -489,7 +597,7 @@ export default class Generator {
computations,
hasJs,
namespace,
templateProperties
templateProperties,
};
}
}

@ -1,5 +1,5 @@
import CodeBuilder from '../../utils/CodeBuilder';
import deindent from '../../utils/deindent.js';
import deindent from '../../utils/deindent';
import { DomGenerator } from './index';
import { Node } from '../../interfaces';
@ -48,7 +48,7 @@ export default class Block {
unmount: CodeBuilder;
detachRaw: CodeBuilder;
destroy: CodeBuilder;
}
};
hasIntroMethod: boolean;
hasOutroMethod: boolean;
@ -64,7 +64,7 @@ export default class Block {
hasUpdateMethod: boolean;
autofocus: string;
constructor ( options: BlockOptions ) {
constructor(options: BlockOptions) {
this.generator = options.generator;
this.name = options.name;
this.expression = options.expression;
@ -93,7 +93,7 @@ export default class Block {
outro: new CodeBuilder(),
unmount: new CodeBuilder(),
detachRaw: new CodeBuilder(),
destroy: new CodeBuilder()
destroy: new CodeBuilder(),
};
this.hasIntroMethod = false; // a block could have an intro method but not intro transitions, e.g. if a sibling block has intros
@ -102,144 +102,169 @@ export default class Block {
this.aliases = new Map();
this.variables = new Map();
this.getUniqueName = this.generator.getUniqueNameMaker( options.params );
this.getUniqueName = this.generator.getUniqueNameMaker(options.params);
// unique names
this.component = this.getUniqueName( 'component' );
this.target = this.getUniqueName( 'target' );
this.component = this.getUniqueName('component');
this.target = this.getUniqueName('target');
this.hasUpdateMethod = false; // determined later
}
addDependencies ( dependencies ) {
dependencies.forEach( dependency => {
this.dependencies.add( dependency );
addDependencies(dependencies) {
dependencies.forEach(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;
if ( needsIdentifier || isToplevel ) {
this.builders.create.addLine(
`var ${name} = ${renderStatement};`
);
if (needsIdentifier || isToplevel) {
this.builders.create.addLine(`var ${name} = ${renderStatement};`);
this.mount( name, parentNode );
this.mount(name, parentNode);
} else {
this.builders.create.addLine( `${this.generator.helper( 'appendNode' )}( ${renderStatement}, ${parentNode} );` );
this.builders.create.addLine(
`${this.generator.helper(
'appendNode'
)}( ${renderStatement}, ${parentNode} );`
);
}
if ( isToplevel ) {
this.builders.unmount.addLine( `${this.generator.helper( 'detachNode' )}( ${name} );` );
if (isToplevel) {
this.builders.unmount.addLine(
`${this.generator.helper('detachNode')}( ${name} );`
);
}
}
addVariable ( name: string, init?: string ) {
if ( this.variables.has( name ) && this.variables.get( name ) !== init ) {
throw new Error( `Variable '${name}' already initialised with a different value` );
addVariable(name: string, init?: string) {
if (this.variables.has(name) && this.variables.get(name) !== init) {
throw new Error(
`Variable '${name}' already initialised with a different value`
);
}
this.variables.set( name, init );
this.variables.set(name, init);
}
alias ( name: string ) {
if ( !this.aliases.has( name ) ) {
this.aliases.set( name, this.getUniqueName( name ) );
alias(name: string) {
if (!this.aliases.has(name)) {
this.aliases.set(name, this.getUniqueName(name));
}
return this.aliases.get( name );
return this.aliases.get(name);
}
child ( options: BlockOptions ) {
return new Block( Object.assign( {}, this, options, { parent: this } ) );
child(options: BlockOptions) {
return new Block(Object.assign({}, this, options, { parent: this }));
}
contextualise ( expression: Node, context?: string, isEventHandler?: boolean ) {
return this.generator.contextualise( this, expression, context, isEventHandler );
contextualise(expression: Node, context?: string, isEventHandler?: boolean) {
return this.generator.contextualise(
this,
expression,
context,
isEventHandler
);
}
findDependencies ( expression ) {
return this.generator.findDependencies( this.contextDependencies, this.indexes, expression );
findDependencies(expression) {
return this.generator.findDependencies(
this.contextDependencies,
this.indexes,
expression
);
}
mount ( name: string, parentNode: string ) {
if ( parentNode ) {
this.builders.create.addLine( `${this.generator.helper( 'appendNode' )}( ${name}, ${parentNode} );` );
mount(name: string, parentNode: string) {
if (parentNode) {
this.builders.create.addLine(
`${this.generator.helper('appendNode')}( ${name}, ${parentNode} );`
);
} 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;
const hasIntros = !this.builders.intro.isEmpty();
if ( hasIntros ) {
introing = this.getUniqueName( 'introing' );
this.addVariable( introing );
if (hasIntros) {
introing = this.getUniqueName('introing');
this.addVariable(introing);
}
let outroing;
const hasOutros = !this.builders.outro.isEmpty();
if ( hasOutros ) {
outroing = this.getUniqueName( 'outroing' );
this.addVariable( outroing );
if (hasOutros) {
outroing = this.getUniqueName('outroing');
this.addVariable(outroing);
}
if ( this.variables.size ) {
const variables = Array.from( this.variables.keys() )
.map( key => {
const init = this.variables.get( key );
if (this.variables.size) {
const variables = Array.from(this.variables.keys())
.map(key => {
const init = this.variables.get(key);
return init !== undefined ? `${key} = ${init}` : key;
})
.join( ', ' );
.join(', ');
this.builders.create.addBlockAtStart( `var ${variables};` );
this.builders.create.addBlockAtStart(`var ${variables};`);
}
if ( this.autofocus ) {
this.builders.create.addLine( `${this.autofocus}.focus();` );
if (this.autofocus) {
this.builders.create.addLine(`${this.autofocus}.focus();`);
}
// 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();
let localKey;
if ( this.key ) {
localKey = this.getUniqueName( 'key' );
properties.addBlock( `key: ${localKey},` );
if (this.key) {
localKey = this.getUniqueName('key');
properties.addBlock(`key: ${localKey},`);
}
if ( this.first ) {
properties.addBlock( `first: ${this.first},` );
if (this.first) {
properties.addBlock(`first: ${this.first},`);
}
if ( this.builders.mount.isEmpty() ) {
properties.addBlock( `mount: ${this.generator.helper( 'noop' )},` );
if (this.builders.mount.isEmpty()) {
properties.addBlock(`mount: ${this.generator.helper('noop')},`);
} else {
properties.addBlock( deindent`
properties.addBlock(deindent`
mount: function ( ${this.target}, anchor ) {
${this.builders.mount}
},
` );
`);
}
if ( this.hasUpdateMethod ) {
if ( this.builders.update.isEmpty() ) {
properties.addBlock( `update: ${this.generator.helper( 'noop' )},` );
if (this.hasUpdateMethod) {
if (this.builders.update.isEmpty()) {
properties.addBlock(`update: ${this.generator.helper('noop')},`);
} else {
properties.addBlock( deindent`
update: function ( changed, ${this.params.join( ', ' )} ) {
properties.addBlock(deindent`
update: function ( changed, ${this.params.join(', ')} ) {
${this.builders.update}
},
` );
`);
}
}
if ( this.hasIntroMethod ) {
if ( hasIntros ) {
properties.addBlock( deindent`
if (this.hasIntroMethod) {
if (hasIntros) {
properties.addBlock(deindent`
intro: function ( ${this.target}, anchor ) {
if ( ${introing} ) return;
${introing} = true;
@ -249,60 +274,63 @@ export default class Block {
this.mount( ${this.target}, anchor );
},
` );
`);
} else {
properties.addBlock( deindent`
properties.addBlock(deindent`
intro: function ( ${this.target}, anchor ) {
this.mount( ${this.target}, anchor );
},
` );
`);
}
}
if ( this.hasOutroMethod ) {
if ( hasOutros ) {
properties.addBlock( deindent`
outro: function ( ${this.alias( 'outrocallback' )} ) {
if (this.hasOutroMethod) {
if (hasOutros) {
properties.addBlock(deindent`
outro: function ( ${this.alias('outrocallback')} ) {
if ( ${outroing} ) return;
${outroing} = true;
${hasIntros && `${introing} = false;`}
var ${this.alias( 'outros' )} = ${this.outros};
var ${this.alias('outros')} = ${this.outros};
${this.builders.outro}
},
` );
`);
} else {
properties.addBlock( deindent`
properties.addBlock(deindent`
outro: function ( outrocallback ) {
outrocallback();
},
` );
`);
}
}
if ( this.builders.unmount.isEmpty() ) {
properties.addBlock( `unmount: ${this.generator.helper('noop')},`);
if (this.builders.unmount.isEmpty()) {
properties.addBlock(`unmount: ${this.generator.helper('noop')},`);
} else {
properties.addBlock( deindent`
properties.addBlock(deindent`
unmount: function () {
${this.builders.unmount}
},
` );
`);
}
if ( this.builders.destroy.isEmpty() ) {
properties.addBlock( `destroy: ${this.generator.helper( 'noop' )}` );
if (this.builders.destroy.isEmpty()) {
properties.addBlock(`destroy: ${this.generator.helper('noop')}`);
} else {
properties.addBlock( deindent`
properties.addBlock(deindent`
destroy: function () {
${this.builders.destroy}
}
` );
`);
}
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}
return {
@ -311,4 +339,4 @@ export default class Block {
}
`;
}
}
}

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

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

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

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

@ -1,4 +1,4 @@
import deindent from '../../../../utils/deindent.js';
import deindent from '../../../../utils/deindent';
import flattenReference from '../../../../utils/flattenReference';
import getSetter from '../shared/binding/getSetter';
import { DomGenerator } from '../../index';
@ -6,26 +6,40 @@ import Block from '../../Block';
import { Node } from '../../../../interfaces';
import { State } from '../../interfaces';
export default function visitBinding ( generator: DomGenerator, block: Block, state: State, node: Node, attribute, local ) {
const { name } = flattenReference( attribute.value );
const { snippet, contexts, dependencies } = block.contextualise( attribute.value );
export default function visitBinding(
generator: DomGenerator,
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 => {
if ( !~local.allUsedContexts.indexOf( context ) ) local.allUsedContexts.push( context );
contexts.forEach(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 prop;
if ( contextual ) {
obj = block.listNames.get( name );
prop = block.indexNames.get( name );
} else if ( attribute.value.type === 'MemberExpression' ) {
prop = `'[✂${attribute.value.property.start}-${attribute.value.property.end}✂]'`;
if (contextual) {
obj = block.listNames.get(name);
prop = block.indexNames.get(name);
} else if (attribute.value.type === 'MemberExpression') {
prop = `'[✂${attribute.value.property.start}-${attribute.value.property
.end}]'`;
obj = `[✂${attribute.value.object.start}-${attribute.value.object.end}✂]`;
} else {
obj = 'state';
@ -36,17 +50,25 @@ export default function visitBinding ( generator: DomGenerator, block: Block, st
name: attribute.name,
value: snippet,
obj,
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;
const updating = block.getUniqueName( `${local.name}_updating` );
block.addVariable( updating, 'false' );
const updating = block.getUniqueName(`${local.name}_updating`);
block.addVariable(updating, 'false');
local.create.addBlock( deindent`
local.create.addBlock(deindent`
${block.component}._bindings.push( function () {
if ( ${local.name}._torndown ) return;
${local.name}.observe( '${attribute.name}', function ( value ) {
@ -54,15 +76,19 @@ export default function visitBinding ( generator: DomGenerator, block: Block, st
${updating} = true;
${setter}
${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`
if ( !${updating} && ${dependencies.map( dependency => `'${dependency}' in changed` ).join( '||' )} ) {
local.update.addBlock(deindent`
if ( !${updating} && ${dependencies
.map(dependency => `'${dependency}' in changed`)
.join('||')} ) {
${updating} = true;
${local.name}._set({ ${attribute.name}: ${snippet} });
${updating} = false;
}
` );
}
`);
}

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

@ -1,39 +1,52 @@
import deindent from '../../../../utils/deindent.js';
import deindent from '../../../../utils/deindent';
import { DomGenerator } from '../../index';
import Block from '../../Block';
import { Node } 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)
generator.addSourcemapLocations( attribute.expression );
generator.code.prependRight( attribute.expression.start, `${block.component}.` );
generator.addSourcemapLocations(attribute.expression);
generator.code.prependRight(
attribute.expression.start,
`${block.component}.`
);
const usedContexts: string[] = [];
attribute.expression.arguments.forEach( ( arg: Node ) => {
const { contexts } = block.contextualise( arg, null, true );
attribute.expression.arguments.forEach((arg: Node) => {
const { contexts } = block.contextualise(arg, null, true);
contexts.forEach( context => {
if ( !~usedContexts.indexOf( context ) ) usedContexts.push( context );
if ( !~local.allUsedContexts.indexOf( context ) ) local.allUsedContexts.push( context );
contexts.forEach(context => {
if (!~usedContexts.indexOf(context)) usedContexts.push(context);
if (!~local.allUsedContexts.indexOf(context))
local.allUsedContexts.push(context);
});
});
// TODO hoist event handlers? can do `this.__component.method(...)`
const declarations = usedContexts.map( name => {
if ( name === 'state' ) return 'var state = this._context.state;';
const declarations = usedContexts.map(name => {
if (name === 'state') return 'var state = this._context.state;';
const listName = block.listNames.get( name );
const indexName = block.indexNames.get( name );
const listName = block.listNames.get(name);
const indexName = block.indexNames.get(name);
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 ) {
${handlerBody}
});
` );
}
`);
}

@ -1,17 +1,24 @@
import deindent from '../../../../utils/deindent.js';
import deindent from '../../../../utils/deindent';
import { DomGenerator } from '../../index';
import Block from '../../Block';
import { Node } 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;
local.create.addLine(
`${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;
` );
}
`);
}

@ -1,76 +1,102 @@
import deindent from '../../../utils/deindent.js';
import deindent from '../../../utils/deindent';
import visit from '../visit';
import { DomGenerator } from '../index';
import Block from '../Block';
import { Node } from '../../../interfaces';
import { State } from '../interfaces';
export default function visitEachBlock ( generator: DomGenerator, block: Block, state: State, node: Node ) {
const each_block = generator.getUniqueName( `each_block` );
export default function visitEachBlock(
generator: DomGenerator,
block: Block,
state: State,
node: Node
) {
const each_block = generator.getUniqueName(`each_block`);
const create_each_block = node._block.name;
const each_block_value = node._block.listName;
const iterations = block.getUniqueName( `${each_block}_iterations` );
const i = block.alias( `i` );
const params = block.params.join( ', ' );
const anchor = node.needsAnchor ? block.getUniqueName( `${each_block}_anchor` ) : ( node.next && node.next._state.name ) || 'null';
const iterations = block.getUniqueName(`${each_block}_iterations`);
const i = block.alias(`i`);
const params = block.params.join(', ');
const anchor = node.needsAnchor
? block.getUniqueName(`${each_block}_anchor`)
: (node.next && node.next._state.name) || 'null';
const mountOrIntro = node._block.hasIntroMethod ? 'intro' : 'mount';
const vars = { each_block, create_each_block, each_block_value, iterations, i, params, 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 );
const vars = {
each_block,
create_each_block,
each_block_value,
iterations,
i,
params,
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 {
unkeyed( generator, block, state, node, snippet, vars );
unkeyed(generator, block, state, node, snippet, vars);
}
const isToplevel = !state.parentNode;
if ( node.needsAnchor ) {
block.addElement( anchor, `${generator.helper( 'createComment' )}()`, state.parentNode, true );
} else if ( node.next ) {
if (node.needsAnchor) {
block.addElement(
anchor,
`${generator.helper('createComment')}()`,
state.parentNode,
true
);
} else if (node.next) {
node.next.usedAsAnchor = true;
}
if ( node.else ) {
const each_block_else = generator.getUniqueName( `${each_block}_else` );
if (node.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
block.builders.create.addBlock( deindent`
block.builders.create.addBlock(deindent`
if ( !${each_block_value}.length ) {
${each_block_else} = ${node.else._block.name}( ${params}, ${block.component} );
${!isToplevel ? `${each_block_else}.${mountOrIntro}( ${state.parentNode}, null );` : ''}
${each_block_else} = ${node.else._block
.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} ) {
${each_block_else}.${mountOrIntro}( ${state.parentNode || block.target}, null );
${each_block_else}.${mountOrIntro}( ${state.parentNode ||
block.target}, null );
}
` );
`);
const parentNode = state.parentNode || `${anchor}.parentNode`;
if ( node.else._block.hasUpdateMethod ) {
block.builders.update.addBlock( deindent`
if (node.else._block.hasUpdateMethod) {
block.builders.update.addBlock(deindent`
if ( !${each_block_value}.length && ${each_block_else} ) {
${each_block_else}.update( changed, ${params} );
} 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} );
} else if ( ${each_block_else} ) {
${each_block_else}.unmount();
${each_block_else}.destroy();
${each_block_else} = null;
}
` );
`);
} else {
block.builders.update.addBlock( deindent`
block.builders.update.addBlock(deindent`
if ( ${each_block_value}.length ) {
if ( ${each_block_else} ) {
${each_block_else}.unmount();
@ -78,48 +104,70 @@ export default function visitEachBlock ( generator: DomGenerator, block: Block,
${each_block_else} = null;
}
} 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} );
}
` );
`);
}
block.builders.unmount.addLine(
`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 );
` );
`);
}
node.children.forEach( ( child: Node ) => {
visit( generator, node._block, node._state, child );
node.children.forEach((child: Node) => {
visit(generator, node._block, node._state, child);
});
if ( node.else ) {
node.else.children.forEach( ( child: Node ) => {
visit( generator, node.else._block, node.else._state, child );
if (node.else) {
node.else.children.forEach((child: Node) => {
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 } ) {
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
function keyed(
generator: DomGenerator,
block: Block,
state: State,
node: Node,
snippet,
{
each_block,
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
} else {
node._block.first = node._block.getUniqueName( 'first' );
node._block.addElement( node._block.first, `${generator.helper( 'createComment' )}()`, null, true );
node._block.first = node._block.getUniqueName('first');
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 ${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 ) {
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} );
${state.parentNode && `${iteration}.${mountOrIntro}( ${state.parentNode}, null );`}
${state.parentNode &&
`${iteration}.${mountOrIntro}( ${state.parentNode}, null );`}
if ( ${last} ) ${last}.next = ${iteration};
${iteration}.last = ${last};
@ -136,25 +185,25 @@ function keyed ( generator: DomGenerator, block: Block, state: State, node: Node
if ( ${i} === 0 ) ${head} = ${iteration};
}
` );
`);
if ( !state.parentNode ) {
block.builders.mount.addBlock( deindent`
if (!state.parentNode) {
block.builders.mount.addBlock(deindent`
var ${iteration} = ${head};
while ( ${iteration} ) {
${iteration}.${mountOrIntro}( ${block.target}, anchor );
${iteration} = ${iteration}.next;
}
` );
`);
}
const dynamic = node._block.hasUpdateMethod;
const parentNode = state.parentNode || `${anchor}.parentNode`;
let destroy;
if ( node._block.hasOutroMethod ) {
const fn = block.getUniqueName( `${each_block}_outro` );
block.builders.create.addBlock( deindent`
if (node._block.hasOutroMethod) {
const fn = block.getUniqueName(`${each_block}_outro`);
block.builders.create.addBlock(deindent`
function ${fn} ( iteration ) {
iteration.outro( function () {
iteration.unmount();
@ -162,7 +211,7 @@ function keyed ( generator: DomGenerator, block: Block, state: State, node: Node
${lookup}[iteration.key] = null;
});
}
` );
`);
destroy = deindent`
while ( ${expected} ) {
@ -177,14 +226,14 @@ function keyed ( generator: DomGenerator, block: Block, state: State, node: Node
}
`;
} else {
const fn = block.getUniqueName( `${each_block}_destroy` );
block.builders.create.addBlock( deindent`
const fn = block.getUniqueName(`${each_block}_destroy`);
block.builders.create.addBlock(deindent`
function ${fn} ( iteration ) {
iteration.unmount();
iteration.destroy();
${lookup}[iteration.key] = null;
}
` );
`);
destroy = deindent`
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 ${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 ${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 ( ${key} === ${expected}.key ) {
@ -256,7 +306,8 @@ function keyed ( generator: DomGenerator, block: Block, state: State, node: Node
if ( ${last} ) ${last}.next = ${iteration};
${iteration}.last = ${last};
${node._block.hasIntroMethod && `${iteration}.intro( ${parentNode}, ${anchor} );`}
${node._block.hasIntroMethod &&
`${iteration}.intro( ${parentNode}, ${anchor} );`}
${last} = ${iteration};
}
@ -265,87 +316,103 @@ function keyed ( generator: DomGenerator, block: Block, state: State, node: Node
${destroy}
${head} = ${lookup}[${each_block_value}[0] && ${each_block_value}[0].${node.key}];
` );
`);
if ( !state.parentNode ) {
block.builders.unmount.addBlock( deindent`
if (!state.parentNode) {
block.builders.unmount.addBlock(deindent`
var ${iteration} = ${head};
while ( ${iteration} ) {
${iteration}.unmount();
${iteration} = ${iteration}.next;
}
` );
`);
}
block.builders.destroy.addBlock( deindent`
block.builders.destroy.addBlock(deindent`
var ${iteration} = ${head};
while ( ${iteration} ) {
${iteration}.destroy( false );
${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 } ) {
block.builders.create.addBlock( deindent`
function unkeyed(
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} = [];
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} );
${state.parentNode && `${iterations}[${i}].${mountOrIntro}( ${state.parentNode}, null );`}
${state.parentNode &&
`${iterations}[${i}].${mountOrIntro}( ${state.parentNode}, null );`}
}
` );
`);
if ( !state.parentNode ) {
block.builders.mount.addBlock( deindent`
if (!state.parentNode) {
block.builders.mount.addBlock(deindent`
for ( var ${i} = 0; ${i} < ${iterations}.length; ${i} += 1 ) {
${iterations}[${i}].${mountOrIntro}( ${block.target}, anchor );
}
` );
`);
}
const dependencies = block.findDependencies( node.expression );
const allDependencies = new Set( node._block.dependencies );
dependencies.forEach( dependency => {
allDependencies.add( dependency );
const dependencies = block.findDependencies(node.expression);
const allDependencies = new Set(node._block.dependencies);
dependencies.forEach(dependency => {
allDependencies.add(dependency);
});
// TODO do this for keyed blocks as well
const condition = Array.from( allDependencies )
.map( dependency => `'${dependency}' in changed` )
.join( ' || ' );
const condition = Array.from(allDependencies)
.map(dependency => `'${dependency}' in changed`)
.join(' || ');
const parentNode = state.parentNode || `${anchor}.parentNode`;
if ( condition !== '' ) {
const forLoopBody = node._block.hasUpdateMethod ?
node._block.hasIntroMethod ?
deindent`
if (condition !== '') {
const forLoopBody = node._block.hasUpdateMethod
? node._block.hasIntroMethod
? deindent`
if ( ${iterations}[${i}] ) {
${iterations}[${i}].update( changed, ${params}, ${each_block_value}, ${each_block_value}[${i}], ${i} );
} else {
${iterations}[${i}] = ${create_each_block}( ${params}, ${each_block_value}, ${each_block_value}[${i}], ${i}, ${block.component} );
}
${iterations}[${i}].intro( ${parentNode}, ${anchor} );
` :
deindent`
`
: deindent`
if ( ${iterations}[${i}] ) {
${iterations}[${i}].update( changed, ${params}, ${each_block_value}, ${each_block_value}[${i}], ${i} );
} else {
${iterations}[${i}] = ${create_each_block}( ${params}, ${each_block_value}, ${each_block_value}[${i}], ${i}, ${block.component} );
${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}].${mountOrIntro}( ${parentNode}, ${anchor} );
`;
const start = node._block.hasUpdateMethod ? '0' : `${iterations}.length`;
const outro = block.getUniqueName( 'outro' );
const destroy = node._block.hasOutroMethod ?
deindent`
const outro = block.getUniqueName('outro');
const destroy = node._block.hasOutroMethod
? deindent`
function ${outro} ( i ) {
if ( ${iterations}[i] ) {
${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} );
` :
deindent`
`
: deindent`
for ( ; ${i} < ${iterations}.length; ${i} += 1 ) {
${iterations}[${i}].unmount();
${iterations}[${i}].destroy();
@ -366,7 +433,7 @@ function unkeyed ( generator: DomGenerator, block: Block, state: State, node: No
${iterations}.length = ${each_block_value}.length;
`;
block.builders.update.addBlock( deindent`
block.builders.update.addBlock(deindent`
var ${each_block_value} = ${snippet};
if ( ${condition} ) {
@ -376,16 +443,16 @@ function unkeyed ( generator: DomGenerator, block: Block, state: State, node: No
${destroy}
}
` );
`);
}
block.builders.unmount.addBlock( deindent`
block.builders.unmount.addBlock(deindent`
for ( var ${i} = 0; ${i} < ${iterations}.length; ${i} += 1 ) {
${iterations}[${i}].unmount();
}
` );
`);
block.builders.destroy.addBlock(
`${generator.helper( 'destroyEach' )}( ${iterations}, false, 0 );`
`${generator.helper('destroyEach')}( ${iterations}, false, 0 );`
);
}

@ -1,69 +1,90 @@
import attributeLookup from './lookup';
import deindent from '../../../../utils/deindent.js';
import deindent from '../../../../utils/deindent';
import getStaticAttributeValue from './getStaticAttributeValue';
import { DomGenerator } from '../../index';
import Block from '../../Block';
import { Node } 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;
let metadata = state.namespace ? null : attributeLookup[ name ];
if ( metadata && metadata.appliesTo && !~metadata.appliesTo.indexOf( node.name ) ) metadata = null;
let metadata = state.namespace ? null : attributeLookup[name];
if (metadata && metadata.appliesTo && !~metadata.appliesTo.indexOf(node.name))
metadata = null;
const isIndirectlyBoundValue = name === 'value' && (
node.name === 'option' || // TODO check it's actually bound
node.name === 'input' && /^(checkbox|radio)$/.test( getStaticAttributeValue( node, 'type' ) )
);
const isIndirectlyBoundValue =
name === 'value' &&
(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
// namespaced attributes but I'm not sure that's applicable in
// 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;
if ( attribute.value.length === 1 ) {
if (attribute.value.length === 1) {
// 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;
} else {
// '{{foo}} {{bar}}' — treat as string concatenation
value = ( attribute.value[0].type === 'Text' ? '' : `"" + ` ) + (
attribute.value.map( ( chunk: Node ) => {
if ( chunk.type === 'Text' ) {
return JSON.stringify( chunk.data );
} else {
const { snippet } = block.contextualise( chunk.expression );
return `( ${snippet} )`;
}
}).join( ' + ' )
);
value =
(attribute.value[0].type === 'Text' ? '' : `"" + `) +
attribute.value
.map((chunk: Node) => {
if (chunk.type === 'Text') {
return JSON.stringify(chunk.data);
} else {
const { snippet } = block.contextualise(chunk.expression);
return `( ${snippet} )`;
}
})
.join(' + ');
}
const last = block.getUniqueName( `${state.parentNode}_${name.replace( /[^a-zA-Z_$]/g, '_')}_value` );
block.addVariable( last );
const last = block.getUniqueName(
`${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;
if ( isSelectValueAttribute ) {
if (isSelectValueAttribute) {
// annoying special case
const isMultipleSelect = node.name === 'select' && node.attributes.find( attr => attr.name.toLowerCase() === 'multiple' ); // TODO use getStaticAttributeValue
const i = block.getUniqueName( 'i' );
const option = block.getUniqueName( 'option' );
const ifStatement = isMultipleSelect ?
deindent`
${option}.selected = ~${last}.indexOf( ${option}.__value );` :
deindent`
const isMultipleSelect =
node.name === 'select' &&
node.attributes.find(attr => attr.name.toLowerCase() === 'multiple'); // TODO use getStaticAttributeValue
const i = block.getUniqueName('i');
const option = block.getUniqueName('option');
const ifStatement = isMultipleSelect
? deindent`
${option}.selected = ~${last}.indexOf( ${option}.__value );`
: deindent`
if ( ${option}.__value === ${last} ) {
${option}.selected = true;
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}
${updater}
` );
} else if ( propertyName ) {
block.builders.create.addLine( `${state.parentNode}.${propertyName} = ${last} = ${value};` );
`);
} else if (propertyName) {
block.builders.create.addLine(
`${state.parentNode}.${propertyName} = ${last} = ${value};`
);
updater = `${state.parentNode}.${propertyName} = ${last};`;
} else {
block.builders.create.addLine( `${generator.helper( method )}( ${state.parentNode}, '${name}', ${last} = ${value} );` );
updater = `${generator.helper( method )}( ${state.parentNode}, '${name}', ${last} );`;
block.builders.create.addLine(
`${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} ) ) {
${updater}
}
` );
}
else {
const value = attribute.value === true ? 'true' :
attribute.value.length === 0 ? `''` :
JSON.stringify( attribute.value[0].data );
const statement = propertyName ?
`${state.parentNode}.${propertyName} = ${value};` :
`${generator.helper( method )}( ${state.parentNode}, '${name}', ${value} );`;
block.builders.create.addLine( statement );
`);
} else {
const value = attribute.value === true
? 'true'
: attribute.value.length === 0
? `''`
: JSON.stringify(attribute.value[0].data);
const statement = propertyName
? `${state.parentNode}.${propertyName} = ${value};`
: `${generator.helper(
method
)}( ${state.parentNode}, '${name}', ${value} );`;
block.builders.create.addLine(statement);
// 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;
}
// special case — xmlns
if ( name === 'xmlns' ) {
if (name === 'xmlns') {
// TODO this attribute must be static enforce at compile time
state.namespace = attribute.value[0].data;
}
}
if ( isIndirectlyBoundValue ) {
if (isIndirectlyBoundValue) {
const updateValue = `${state.parentNode}.value = ${state.parentNode}.__value;`;
block.builders.create.addLine( updateValue );
if ( isDynamic ) block.builders.update.addLine( updateValue );
block.builders.create.addLine(updateValue);
if (isDynamic) block.builders.update.addLine(updateValue);
}
}
}

@ -1,4 +1,4 @@
import deindent from '../../../../utils/deindent.js';
import deindent from '../../../../utils/deindent';
import flattenReference from '../../../../utils/flattenReference';
import getSetter from '../shared/binding/getSetter';
import getStaticAttributeValue from './getStaticAttributeValue';
@ -7,51 +7,88 @@ import Block from '../../Block';
import { Node } from '../../../../interfaces';
import { State } from '../../interfaces';
function getObject ( node ) {
function getObject(node) {
// 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;
}
export default function visitBinding ( generator: DomGenerator, block: Block, state: State, node: Node, 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!' );
export default function visitBinding(
generator: DomGenerator,
block: Block,
state: State,
node: Node,
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 => {
if ( !~state.allUsedContexts.indexOf( context ) ) state.allUsedContexts.push( context );
contexts.forEach(context => {
if (!~state.allUsedContexts.indexOf(context))
state.allUsedContexts.push(context);
});
const eventName = getBindingEventName( node, attribute );
const handler = block.getUniqueName( `${state.parentNode}_${eventName}_handler` );
const isMultipleSelect = node.name === 'select' && node.attributes.find( ( 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 });
const eventName = getBindingEventName(node, attribute);
const handler = block.getUniqueName(
`${state.parentNode}_${eventName}_handler`
);
const isMultipleSelect =
node.name === 'select' &&
node.attributes.find(
(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};`;
const lock = block.alias( `${state.parentNode}_updating` );
const lock = block.alias(`${state.parentNode}_updating`);
let updateCondition = `!${lock}`;
block.addVariable( lock, 'false' );
block.addVariable(lock, 'false');
// <select> special case
if ( node.name === 'select' ) {
if ( !isMultipleSelect ) {
if (node.name === 'select') {
if (!isMultipleSelect) {
setter = `var selectedOption = ${state.parentNode}.querySelector(':checked') || ${state.parentNode}.options[0];\n${setter}`;
}
const value = block.getUniqueName( 'value' );
const i = block.alias( 'i' );
const option = block.getUniqueName( 'option' );
const value = block.getUniqueName('value');
const i = block.alias('i');
const option = block.getUniqueName('option');
const ifStatement = isMultipleSelect ?
deindent`
${option}.selected = ~${value}.indexOf( ${option}.__value );` :
deindent`
const ifStatement = isMultipleSelect
? deindent`
${option}.selected = ~${value}.indexOf( ${option}.__value );`
: deindent`
if ( ${option}.__value === ${value} ) {
${option}.selected = true;
break;
@ -65,20 +102,18 @@ export default function visitBinding ( generator: DomGenerator, block: Block, st
${ifStatement}
}
`;
}
// <input type='checkbox|radio' bind:group='selected'> special case
else if ( attribute.name === 'group' ) {
if ( type === 'radio' ) {
} else if (attribute.name === 'group') {
// <input type='checkbox|radio' bind:group='selected'> special case
if (type === 'radio') {
setter = deindent`
if ( !${state.parentNode}.checked ) return;
${setter}
`;
}
const condition = type === 'checkbox' ?
`~${snippet}.indexOf( ${state.parentNode}.__value )` :
`${state.parentNode}.__value === ${snippet}`;
const condition = type === 'checkbox'
? `~${snippet}.indexOf( ${state.parentNode}.__value )`
: `${state.parentNode}.__value === ${snippet}`;
block.builders.create.addLine(
`${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};`;
}
else if ( node.name === 'audio' || node.name === 'video' ) {
} else if (node.name === 'audio' || node.name === 'video') {
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' ) {
const frame = block.getUniqueName( `${state.parentNode}_animationframe` );
block.addVariable( frame );
if (attribute.name === 'currentTime') {
const frame = block.getUniqueName(`${state.parentNode}_animationframe`);
block.addVariable(frame);
setter = deindent`
cancelAnimationFrame( ${frame} );
if ( !${state.parentNode}.paused ) ${frame} = requestAnimationFrame( ${handler} );
@ -105,108 +140,132 @@ export default function visitBinding ( generator: DomGenerator, block: Block, st
`;
updateCondition += ` && !isNaN( ${snippet} )`;
}
else if ( attribute.name === 'duration' ) {
} else if (attribute.name === 'duration') {
updateCondition = null;
}
else if ( attribute.name === 'paused' ) {
} else if (attribute.name === 'paused') {
// this is necessary to prevent the audio restarting by itself
const last = block.getUniqueName( `${state.parentNode}_paused_value` );
block.addVariable( last, 'true' );
const last = block.getUniqueName(`${state.parentNode}_paused_value`);
block.addVariable(last, 'true');
updateCondition = `${last} !== ( ${last} = ${snippet} )`;
updateElement = `${state.parentNode}[ ${last} ? 'pause' : 'play' ]();`;
}
}
block.builders.create.addBlock( deindent`
block.builders.create.addBlock(deindent`
function ${handler} () {
${lock} = true;
${setter}
${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
block.builders.update.addBlock( deindent`
block.builders.update.addBlock(deindent`
if ( ${updateCondition} ) {
${updateElement}
}
` );
`);
}
block.builders.destroy.addLine( deindent`
${generator.helper( 'removeEventListener' )}( ${state.parentNode}, '${eventName}', ${handler} );
` );
block.builders.destroy.addLine(deindent`
${generator.helper(
'removeEventListener'
)}( ${state.parentNode}, '${eventName}', ${handler} );
`);
if ( attribute.name === 'paused' ) {
block.builders.create.addLine( `${generator.helper( 'addEventListener' )}( ${state.parentNode}, 'play', ${handler} );` );
block.builders.destroy.addLine( `${generator.helper( 'removeEventListener' )}( ${state.parentNode}, 'play', ${handler} );` );
if (attribute.name === 'paused') {
block.builders.create.addLine(
`${generator.helper(
'addEventListener'
)}( ${state.parentNode}, 'play', ${handler} );`
);
block.builders.destroy.addLine(
`${generator.helper(
'removeEventListener'
)}( ${state.parentNode}, 'play', ${handler} );`
);
}
}
function getBindingEventName ( node: Node, attribute: Node ) {
if ( node.name === 'input' ) {
const typeAttribute = node.attributes.find( ( attr: Node ) => attr.type === 'Attribute' && attr.name === 'type' );
function getBindingEventName(node: Node, attribute: Node) {
if (node.name === 'input') {
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
return type === 'checkbox' || type === 'radio' ? 'change' : 'input';
}
if ( node.name === 'textarea' ) return 'input';
if ( attribute.name === 'currentTime' ) return 'timeupdate';
if ( attribute.name === 'duration' ) return 'durationchange';
if ( attribute.name === 'paused' ) return 'pause';
if (node.name === 'textarea') return 'input';
if (attribute.name === 'currentTime') return 'timeupdate';
if (attribute.name === 'duration') return 'durationchange';
if (attribute.name === 'paused') return 'pause';
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>
if ( isMultipleSelect ) {
if (isMultipleSelect) {
return `[].map.call( ${state.parentNode}.querySelectorAll(':checked'), function ( option ) { return option.__value; })`;
}
// <select bind:value='selected>
if ( node.name === 'select' ) {
if (node.name === 'select') {
return 'selectedOption && selectedOption.__value';
}
// <input type='checkbox' bind:group='foo'>
if ( attribute.name === 'group' ) {
if ( type === 'checkbox' ) {
return `${generator.helper( 'getBindingGroupValue' )}( ${block.component}._bindingGroups[${bindingGroup}] )`;
if (attribute.name === 'group') {
if (type === 'checkbox') {
return `${generator.helper(
'getBindingGroupValue'
)}( ${block.component}._bindingGroups[${bindingGroup}] )`;
}
return `${state.parentNode}.__value`;
}
// <input type='range|number' bind:value>
if ( type === 'range' || type === 'number' ) {
return `${generator.helper( 'toNumber' )}( ${state.parentNode}.${attribute.name} )`;
if (type === 'range' || type === 'number') {
return `${generator.helper(
'toNumber'
)}( ${state.parentNode}.${attribute.name} )`;
}
// everything else
return `${state.parentNode}.${attribute.name}`;
}
function getBindingGroup ( generator: DomGenerator, value: Node ) {
const { parts } = flattenReference( value ); // TODO handle cases involving computed member expressions
const keypath = parts.join( '.' );
function getBindingGroup(generator: DomGenerator, value: Node) {
const { parts } = flattenReference(value); // TODO handle cases involving computed member expressions
const keypath = parts.join('.');
// TODO handle contextual bindings — `keypath` should include unique ID of
// each block that provides context
let index = generator.bindingGroups.indexOf( keypath );
if ( index === -1 ) {
let index = generator.bindingGroups.indexOf(keypath);
if (index === -1) {
index = generator.bindingGroups.length;
generator.bindingGroups.push( keypath );
generator.bindingGroups.push(keypath);
}
return index;

@ -1,4 +1,4 @@
import deindent from '../../../../utils/deindent.js';
import deindent from '../../../../utils/deindent';
import visit from '../../visit';
import visitComponent from '../Component/Component';
import visitWindow from './meta/Window';
@ -13,108 +13,130 @@ import { Node } from '../../../../interfaces';
import { State } from '../../interfaces';
const meta = {
':Window': visitWindow
':Window': visitWindow,
};
const order = {
Attribute: 1,
Binding: 2,
EventHandler: 3,
Ref: 4
Ref: 4,
};
const visitors = {
Attribute: visitAttribute,
EventHandler: visitEventHandler,
Binding: visitBinding,
Ref: visitRef
Ref: visitRef,
};
export default function visitElement ( generator: DomGenerator, block: Block, state: State, node: Node ) {
if ( node.name in meta ) {
return meta[ node.name ]( generator, block, node );
export default function visitElement(
generator: DomGenerator,
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' ) {
return visitComponent( generator, block, state, node );
if (generator.components.has(node.name) || node.name === ':Self') {
return visitComponent(generator, block, state, node);
}
const childState = node._state;
const name = childState.parentNode;
block.builders.create.addLine( `var ${name} = ${getRenderStatement( generator, childState.namespace, node.name )};` );
block.mount( name, state.parentNode );
block.builders.create.addLine(
`var ${name} = ${getRenderStatement(
generator,
childState.namespace,
node.name
)};`
);
block.mount(name, state.parentNode);
// add CSS encapsulation attribute
if ( generator.cssId && ( !generator.cascade || state.isTopLevel ) ) {
block.builders.create.addLine( `${generator.helper( 'setAttribute' )}( ${name}, '${generator.cssId}', '' );` );
if (generator.cssId && (!generator.cascade || state.isTopLevel)) {
block.builders.create.addLine(
`${generator.helper(
'setAttribute'
)}( ${name}, '${generator.cssId}', '' );`
);
}
function visitAttributesAndAddProps () {
function visitAttributesAndAddProps() {
let intro;
let outro;
node.attributes
.sort( ( a: Node, b: Node ) => order[ a.type ] - order[ b.type ] )
.forEach( ( attribute: Node ) => {
if ( attribute.type === 'Transition' ) {
if ( attribute.intro ) intro = attribute;
if ( attribute.outro ) outro = attribute;
.sort((a: Node, b: Node) => order[a.type] - order[b.type])
.forEach((attribute: Node) => {
if (attribute.type === 'Transition') {
if (attribute.intro) intro = attribute;
if (attribute.outro) outro = attribute;
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 updates: string[] = [];
if ( childState.usesComponent ) {
initialProps.push( `component: ${block.component}` );
if (childState.usesComponent) {
initialProps.push(`component: ${block.component}`);
}
childState.allUsedContexts.forEach( ( contextName: string ) => {
if ( contextName === 'state' ) return;
childState.allUsedContexts.forEach((contextName: string) => {
if (contextName === 'state') return;
const listName = block.listNames.get( contextName );
const indexName = block.indexNames.get( contextName );
const listName = block.listNames.get(contextName);
const indexName = block.indexNames.get(contextName);
initialProps.push( `${listName}: ${listName},\n${indexName}: ${indexName}` );
updates.push( `${name}._svelte.${listName} = ${listName};\n${name}._svelte.${indexName} = ${indexName};` );
initialProps.push(
`${listName}: ${listName},\n${indexName}: ${indexName}`
);
updates.push(
`${name}._svelte.${listName} = ${listName};\n${name}._svelte.${indexName} = ${indexName};`
);
});
if ( initialProps.length ) {
block.builders.create.addBlock( deindent`
if (initialProps.length) {
block.builders.create.addBlock(deindent`
${name}._svelte = {
${initialProps.join( ',\n' )}
${initialProps.join(',\n')}
};
` );
`);
}
if ( updates.length ) {
block.builders.update.addBlock( updates.join( '\n' ) );
if (updates.length) {
block.builders.update.addBlock(updates.join('\n'));
}
}
}
if ( !state.parentNode ) {
if (!state.parentNode) {
// TODO we eventually need to consider what happens to elements
// 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 === 'textarea' ) {
if (node.name !== 'select') {
if (node.name === 'textarea') {
// this is an egregious hack, but it's the easiest way to get <textarea>
// children treated the same way as a value attribute
if ( node.children.length > 0 ) {
if (node.children.length > 0) {
node.attributes.push({
type: 'Attribute',
name: 'value',
value: node.children
value: node.children,
});
node.children = [];
@ -127,36 +149,47 @@ export default function visitElement ( generator: DomGenerator, block: Block, st
}
// 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;`;
node.initialUpdate = node.lateUpdate = statement;
}
node.children.forEach( ( child: Node ) => {
visit( generator, block, childState, child );
node.children.forEach((child: Node) => {
visit(generator, block, childState, child);
});
if ( node.lateUpdate ) {
block.builders.update.addLine( node.lateUpdate );
if (node.lateUpdate) {
block.builders.update.addLine(node.lateUpdate);
}
if ( node.name === 'select' ) {
if (node.name === 'select') {
visitAttributesAndAddProps();
}
if ( node.initialUpdate ) {
block.builders.create.addBlock( node.initialUpdate );
if (node.initialUpdate) {
block.builders.create.addBlock(node.initialUpdate);
}
}
function getRenderStatement ( generator: DomGenerator, namespace: string, name: string ) {
if ( namespace === 'http://www.w3.org/2000/svg' ) {
return `${generator.helper( 'createSvgElement' )}( '${name}' )`;
function getRenderStatement(
generator: DomGenerator,
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 `${generator.helper( 'createElement' )}( '${name}' )`;
return `${generator.helper('createElement')}( '${name}' )`;
}

@ -1,53 +1,65 @@
import deindent from '../../../../utils/deindent.js';
import deindent from '../../../../utils/deindent';
import flattenReference from '../../../../utils/flattenReference';
import { DomGenerator } from '../../index';
import Block from '../../Block';
import { Node } 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 isCustomEvent = generator.events.has( name );
const isCustomEvent = generator.events.has(name);
const shouldHoist = !isCustomEvent && state.inEachBlock;
generator.addSourcemapLocations( attribute.expression );
generator.addSourcemapLocations(attribute.expression);
const flattened = flattenReference( attribute.expression.callee );
if ( flattened.name !== 'event' && flattened.name !== 'this' ) {
const flattened = flattenReference(attribute.expression.callee);
if (flattened.name !== 'event' && flattened.name !== 'this') {
// allow event.stopPropagation(), this.select() etc
// TODO verify that it's a valid callee (i.e. built-in or declared method)
generator.code.prependRight( attribute.expression.start, `${block.component}.` );
if ( shouldHoist ) state.usesComponent = true; // this feels a bit hacky but it works!
generator.code.prependRight(
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 usedContexts: string[] = [];
attribute.expression.arguments.forEach( ( arg: Node ) => {
const { contexts } = block.contextualise( arg, context, true );
attribute.expression.arguments.forEach((arg: Node) => {
const { contexts } = block.contextualise(arg, context, true);
contexts.forEach( context => {
if ( !~usedContexts.indexOf( context ) ) usedContexts.push( context );
if ( !~state.allUsedContexts.indexOf( context ) ) state.allUsedContexts.push( context );
contexts.forEach(context => {
if (!~usedContexts.indexOf(context)) usedContexts.push(context);
if (!~state.allUsedContexts.indexOf(context))
state.allUsedContexts.push(context);
});
});
const _this = context || 'this';
const declarations = usedContexts.map( name => {
if ( name === 'state' ) {
if ( shouldHoist ) state.usesComponent = true;
const declarations = usedContexts.map(name => {
if (name === 'state') {
if (shouldHoist) state.usesComponent = true;
return `var state = ${block.component}.get();`;
}
const listName = block.listNames.get( name );
const indexName = block.indexNames.get( name );
const contextName = block.contexts.get( name );
const listName = block.listNames.get(name);
const indexName = block.indexNames.get(name);
const contextName = block.contexts.get(name);
return `var ${listName} = ${_this}._svelte.${listName}, ${indexName} = ${_this}._svelte.${indexName}, ${contextName} = ${listName}[${indexName}];`;
});
// get a name for the event handler that is globally unique
// 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
const handlerBody = deindent`
@ -56,37 +68,45 @@ export default function visitEventHandler ( generator: DomGenerator, block: Bloc
[${attribute.expression.start}-${attribute.expression.end}];
`;
const handler = isCustomEvent ?
deindent`
var ${handlerName} = ${generator.alias( 'template' )}.events.${name}.call( ${block.component}, ${state.parentNode}, function ( event ) {
const handler = isCustomEvent
? deindent`
var ${handlerName} = ${generator.alias(
'template'
)}.events.${name}.call( ${block.component}, ${state.parentNode}, function ( event ) {
${handlerBody}
});
` :
deindent`
`
: deindent`
function ${handlerName} ( event ) {
${handlerBody}
}
`;
if ( shouldHoist ) {
generator.blocks.push(<Block>{
render: () => handler
});
if (shouldHoist) {
generator.blocks.push(
<Block>{
render: () => handler,
}
);
} else {
block.builders.create.addBlock( handler );
block.builders.create.addBlock(handler);
}
if ( isCustomEvent ) {
block.builders.destroy.addLine( deindent`
if (isCustomEvent) {
block.builders.destroy.addLine(deindent`
${handlerName}.teardown();
` );
`);
} else {
block.builders.create.addLine( deindent`
${generator.helper( 'addEventListener' )}( ${state.parentNode}, '${name}', ${handlerName} );
` );
block.builders.create.addLine(deindent`
${generator.helper(
'addEventListener'
)}( ${state.parentNode}, '${name}', ${handlerName} );
`);
block.builders.destroy.addLine( deindent`
${generator.helper( 'removeEventListener' )}( ${state.parentNode}, '${name}', ${handlerName} );
` );
block.builders.destroy.addLine(deindent`
${generator.helper(
'removeEventListener'
)}( ${state.parentNode}, '${name}', ${handlerName} );
`);
}
}

@ -1,19 +1,25 @@
import deindent from '../../../../utils/deindent.js';
import deindent from '../../../../utils/deindent';
import { DomGenerator } from '../../index';
import Block from '../../Block';
import { Node } 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;
block.builders.create.addLine(
`${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;
` );
`);
generator.usesRefs = true; // so this component.refs object is created
}
}

@ -1,80 +1,91 @@
import deindent from '../../../../utils/deindent.js';
import deindent from '../../../../utils/deindent';
import { DomGenerator } from '../../index';
import Block from '../../Block';
import { Node } from '../../../../interfaces';
import { State } from '../../interfaces';
export default function addTransitions ( generator: DomGenerator, block: Block, state: State, node: Node, intro, outro ) {
const wrapTransition = generator.helper( 'wrapTransition' );
export default function addTransitions(
generator: DomGenerator,
block: Block,
state: State,
node: Node,
intro,
outro
) {
const wrapTransition = generator.helper('wrapTransition');
if ( intro === outro ) {
const name = block.getUniqueName( `${state.name}_transition` );
const snippet = intro.expression ? block.contextualise( intro.expression ).snippet : '{}';
if (intro === outro) {
const name = block.getUniqueName(`${state.name}_transition`);
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 () {
if ( !${name} ) ${name} = ${wrapTransition}( ${state.name}, ${fn}, ${snippet}, true, null );
${name}.run( true, function () {
${block.component}.fire( 'intro.end', { node: ${state.name} });
});
});
` );
`);
block.builders.outro.addBlock( deindent`
block.builders.outro.addBlock(deindent`
${name}.run( false, function () {
${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;
});
` );
}
else {
const introName = intro && block.getUniqueName( `${state.name}_intro` );
const outroName = outro && block.getUniqueName( `${state.name}_outro` );
`);
} else {
const introName = intro && block.getUniqueName(`${state.name}_intro`);
const outroName = outro && block.getUniqueName(`${state.name}_outro`);
if ( intro ) {
block.addVariable( introName );
const snippet = intro.expression ? block.contextualise( intro.expression ).snippet : '{}';
if (intro) {
block.addVariable(introName);
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 ) {
block.builders.intro.addBlock( deindent`
if (outro) {
block.builders.intro.addBlock(deindent`
if ( ${introName} ) ${introName}.abort();
if ( ${outroName} ) ${outroName}.abort();
` );
`);
}
block.builders.intro.addBlock( deindent`
block.builders.intro.addBlock(deindent`
${block.component}._renderHooks.push( function () {
${introName} = ${wrapTransition}( ${state.name}, ${fn}, ${snippet}, true, null );
${introName}.run( true, function () {
${block.component}.fire( 'intro.end', { node: ${state.name} });
});
});
` );
`);
}
if ( outro ) {
block.addVariable( outroName );
const snippet = outro.expression ? block.contextualise( outro.expression ).snippet : '{}';
if (outro) {
block.addVariable(outroName);
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
// 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}.run( false, function () {
${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';
export default function getStaticAttributeValue ( node: Node, name: string ) {
const attribute = node.attributes.find( ( attr: Node ) => attr.name.toLowerCase() === name );
if ( !attribute ) return null;
export default function getStaticAttributeValue(node: Node, name: string) {
const attribute = node.attributes.find(
(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)
throw new Error( `'${name} must be a static attribute` );
throw new Error(`'${name} must be a static attribute`);
}
return attribute.value[0].data;
}
}

@ -1,122 +1,235 @@
// source: https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes
const lookup = {
accept: { appliesTo: [ 'form', 'input' ] },
'accept-charset': { propertyName: 'acceptCharset', appliesTo: [ 'form' ] },
accept: { appliesTo: ['form', 'input'] },
'accept-charset': { propertyName: 'acceptCharset', appliesTo: ['form'] },
accesskey: { propertyName: 'accessKey' },
action: { appliesTo: [ 'form' ] },
align: { appliesTo: [ 'applet', 'caption', 'col', 'colgroup', 'hr', 'iframe', 'img', 'table', 'tbody', 'td', 'tfoot' , 'th', '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' ] },
action: { appliesTo: ['form'] },
align: {
appliesTo: [
'applet',
'caption',
'col',
'colgroup',
'hr',
'iframe',
'img',
'table',
'tbody',
'td',
'tfoot',
'th',
'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' },
code: { appliesTo: [ 'applet' ] },
codebase: { propertyName: 'codeBase', appliesTo: [ 'applet' ] },
color: { appliesTo: [ 'basefont', 'font', 'hr' ] },
cols: { appliesTo: [ 'textarea' ] },
colspan: { propertyName: 'colSpan', appliesTo: [ 'td', 'th' ] },
content: { appliesTo: [ 'meta' ] },
code: { appliesTo: ['applet'] },
codebase: { propertyName: 'codeBase', appliesTo: ['applet'] },
color: { appliesTo: ['basefont', 'font', 'hr'] },
cols: { appliesTo: ['textarea'] },
colspan: { propertyName: 'colSpan', appliesTo: ['td', 'th'] },
content: { appliesTo: ['meta'] },
contenteditable: { propertyName: 'contentEditable' },
contextmenu: {},
controls: { appliesTo: [ 'audio', 'video' ] },
coords: { appliesTo: [ 'area' ] },
data: { appliesTo: [ 'object' ] },
datetime: { propertyName: 'dateTime', appliesTo: [ 'del', 'ins', 'time' ] },
default: { appliesTo: [ 'track' ] },
defer: { appliesTo: [ 'script' ] },
controls: { appliesTo: ['audio', 'video'] },
coords: { appliesTo: ['area'] },
data: { appliesTo: ['object'] },
datetime: { propertyName: 'dateTime', appliesTo: ['del', 'ins', 'time'] },
default: { appliesTo: ['track'] },
defer: { appliesTo: ['script'] },
dir: {},
dirname: { propertyName: 'dirName', appliesTo: [ 'input', 'textarea' ] },
disabled: { appliesTo: [ 'button', 'command', 'fieldset', 'input', 'keygen', 'optgroup', 'option', 'select', 'textarea' ] },
download: { appliesTo: [ 'a', 'area' ] },
dirname: { propertyName: 'dirName', appliesTo: ['input', 'textarea'] },
disabled: {
appliesTo: [
'button',
'command',
'fieldset',
'input',
'keygen',
'optgroup',
'option',
'select',
'textarea',
],
},
download: { appliesTo: ['a', 'area'] },
draggable: {},
dropzone: {},
enctype: { appliesTo: [ 'form' ] },
for: { propertyName: 'htmlFor', appliesTo: [ 'label', 'output' ] },
form: { appliesTo: [ 'button', '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' ] },
enctype: { appliesTo: ['form'] },
for: { propertyName: 'htmlFor', appliesTo: ['label', 'output'] },
form: {
appliesTo: [
'button',
'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: {},
high: { appliesTo: [ 'meter' ] },
href: { appliesTo: [ 'a', 'area', 'base', 'link' ] },
hreflang: { appliesTo: [ 'a', 'area', 'link' ] },
'http-equiv': { propertyName: 'httpEquiv', appliesTo: [ 'meta' ] },
icon: { appliesTo: [ 'command' ] },
high: { appliesTo: ['meter'] },
href: { appliesTo: ['a', 'area', 'base', 'link'] },
hreflang: { appliesTo: ['a', 'area', 'link'] },
'http-equiv': { propertyName: 'httpEquiv', appliesTo: ['meta'] },
icon: { appliesTo: ['command'] },
id: {},
ismap: { propertyName: 'isMap', appliesTo: [ 'img' ] },
ismap: { propertyName: 'isMap', appliesTo: ['img'] },
itemprop: {},
keytype: { appliesTo: [ 'keygen' ] },
kind: { appliesTo: [ 'track' ] },
label: { appliesTo: [ 'track' ] },
keytype: { appliesTo: ['keygen'] },
kind: { appliesTo: ['track'] },
label: { appliesTo: ['track'] },
lang: {},
language: { appliesTo: [ 'script' ] },
loop: { appliesTo: [ 'audio', 'bgsound', 'marquee', 'video' ] },
low: { appliesTo: [ 'meter' ] },
manifest: { appliesTo: [ 'html' ] },
max: { appliesTo: [ 'input', 'meter', 'progress' ] },
maxlength: { propertyName: 'maxLength', appliesTo: [ 'input', 'textarea' ] },
media: { appliesTo: [ 'a', 'area', 'link', 'source', 'style' ] },
method: { appliesTo: [ 'form' ] },
min: { appliesTo: [ 'input', 'meter' ] },
multiple: { appliesTo: [ 'input', 'select' ] },
muted: { appliesTo: [ 'video' ] },
name: { appliesTo: [ 'button', 'form', 'fieldset', 'iframe', 'input', 'keygen', 'object', 'output', 'select', 'textarea', 'map', 'meta', 'param' ] },
novalidate: { propertyName: 'noValidate', appliesTo: [ 'form' ] },
open: { appliesTo: [ 'details' ] },
optimum: { appliesTo: [ 'meter' ] },
pattern: { appliesTo: [ 'input' ] },
ping: { appliesTo: [ 'a', 'area' ] },
placeholder: { appliesTo: [ 'input', 'textarea' ] },
poster: { appliesTo: [ 'video' ] },
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' ] },
language: { appliesTo: ['script'] },
loop: { appliesTo: ['audio', 'bgsound', 'marquee', 'video'] },
low: { appliesTo: ['meter'] },
manifest: { appliesTo: ['html'] },
max: { appliesTo: ['input', 'meter', 'progress'] },
maxlength: { propertyName: 'maxLength', appliesTo: ['input', 'textarea'] },
media: { appliesTo: ['a', 'area', 'link', 'source', 'style'] },
method: { appliesTo: ['form'] },
min: { appliesTo: ['input', 'meter'] },
multiple: { appliesTo: ['input', 'select'] },
muted: { appliesTo: ['video'] },
name: {
appliesTo: [
'button',
'form',
'fieldset',
'iframe',
'input',
'keygen',
'object',
'output',
'select',
'textarea',
'map',
'meta',
'param',
],
},
novalidate: { propertyName: 'noValidate', appliesTo: ['form'] },
open: { appliesTo: ['details'] },
optimum: { appliesTo: ['meter'] },
pattern: { appliesTo: ['input'] },
ping: { appliesTo: ['a', 'area'] },
placeholder: { appliesTo: ['input', 'textarea'] },
poster: { appliesTo: ['video'] },
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: {},
src: { appliesTo: [ 'audio', 'embed', 'iframe', 'img', 'input', 'script', 'source', 'track', 'video' ] },
srcdoc: { appliesTo: [ 'iframe' ] },
srclang: { appliesTo: [ 'track' ] },
srcset: { appliesTo: [ 'img' ] },
start: { appliesTo: [ 'ol' ] },
step: { appliesTo: [ 'input' ] },
src: {
appliesTo: [
'audio',
'embed',
'iframe',
'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' },
summary: { appliesTo: [ 'table' ] },
summary: { appliesTo: ['table'] },
tabindex: { propertyName: 'tabIndex' },
target: { appliesTo: [ 'a', 'area', 'base', 'form' ] },
target: { appliesTo: ['a', 'area', 'base', 'form'] },
title: {},
type: { appliesTo: [ 'button', 'input', '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' ] }
type: {
appliesTo: [
'button',
'input',
'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 => {
const metadata = lookup[ name ];
if ( !metadata.propertyName ) metadata.propertyName = name;
Object.keys(lookup).forEach(name => {
const metadata = lookup[name];
if (!metadata.propertyName) metadata.propertyName = name;
});
export default lookup;

@ -1,5 +1,5 @@
import flattenReference from '../../../../../utils/flattenReference';
import deindent from '../../../../../utils/deindent.js';
import deindent from '../../../../../utils/deindent';
import { DomGenerator } from '../../../index';
import Block from '../../../Block';
import { Node } from '../../../../../interfaces';
@ -11,7 +11,7 @@ const associatedEvents = {
outerHeight: 'resize',
scrollX: 'scroll',
scrollY: 'scroll'
scrollY: 'scroll',
};
const readonly = new Set([
@ -19,68 +19,77 @@ const readonly = new Set([
'innerHeight',
'outerWidth',
'outerHeight',
'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 bindings = {};
node.attributes.forEach( ( attribute: Node ) => {
if ( attribute.type === 'EventHandler' ) {
node.attributes.forEach((attribute: Node) => {
if (attribute.type === 'EventHandler') {
// 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;
attribute.expression.arguments.forEach( ( arg: Node ) => {
const { contexts } = block.contextualise( arg, null, true );
if ( contexts.length ) usesState = true;
attribute.expression.arguments.forEach((arg: Node) => {
const { contexts } = block.contextualise(arg, null, true);
if (contexts.length) usesState = true;
});
const flattened = flattenReference( attribute.expression.callee );
if ( flattened.name !== 'event' && flattened.name !== 'this' ) {
const flattened = flattenReference(attribute.expression.callee);
if (flattened.name !== 'event' && flattened.name !== 'this') {
// 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`
${usesState && `var state = ${block.component}.get();`}
[${attribute.expression.start}-${attribute.expression.end}];
`;
block.builders.create.addBlock( deindent`
block.builders.create.addBlock(deindent`
function ${handlerName} ( event ) {
${handlerBody}
};
window.addEventListener( '${attribute.name}', ${handlerName} );
` );
`);
block.builders.destroy.addBlock( deindent`
block.builders.destroy.addBlock(deindent`
window.removeEventListener( '${attribute.name}', ${handlerName} );
` );
`);
}
if ( attribute.type === 'Binding' ) {
if (attribute.type === 'Binding') {
// in dev mode, throw if read-only values are written to
if ( readonly.has( attribute.name ) ) {
generator.readonly.add( attribute.value.name );
if (readonly.has(attribute.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
if ( attribute.name === 'online' ) return;
if (attribute.name === 'online') return;
const associatedEvent = associatedEvents[ attribute.name ];
const associatedEvent = associatedEvents[attribute.name];
if ( !associatedEvent ) {
throw new Error( `Cannot bind to ${attribute.name} on <:Window>` );
if (!associatedEvent) {
throw new Error(`Cannot bind to ${attribute.name} on <:Window>`);
}
if ( !events[ associatedEvent ] ) events[ associatedEvent ] = [];
events[ associatedEvent ].push( `${attribute.value.name}: this.${attribute.name}` );
if (!events[associatedEvent]) events[associatedEvent] = [];
events[associatedEvent].push(
`${attribute.value.name}: this.${attribute.name}`
);
// add initial value
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 => {
const handlerName = block.getUniqueName( `onwindow${event}` );
const props = events[ event ].join( ',\n' );
Object.keys(events).forEach(event => {
const handlerName = block.getUniqueName(`onwindow${event}`);
const props = events[event].join(',\n');
if ( event === 'scroll' ) { // TODO other bidirectional bindings...
block.addVariable( lock, 'false' );
if (event === 'scroll') {
// TODO other bidirectional bindings...
block.addVariable(lock, 'false');
}
const handlerBody = deindent`
@ -111,63 +121,74 @@ export default function visitWindow ( generator: DomGenerator, block: Block, nod
${event === 'scroll' && `${lock} = false;`}
`;
block.builders.create.addBlock( deindent`
block.builders.create.addBlock(deindent`
function ${handlerName} ( event ) {
${handlerBody}
};
window.addEventListener( '${event}', ${handlerName} );
` );
`);
block.builders.destroy.addBlock( deindent`
block.builders.destroy.addBlock(deindent`
window.removeEventListener( '${event}', ${handlerName} );
` );
`);
});
// special case... might need to abstract this out if we add more special cases
if ( bindings.scrollX && bindings.scrollY ) {
const observerCallback = block.getUniqueName( `scrollobserver` );
if (bindings.scrollX && bindings.scrollY) {
const observerCallback = block.getUniqueName(`scrollobserver`);
block.builders.create.addBlock( deindent`
block.builders.create.addBlock(deindent`
function ${observerCallback} () {
if ( ${lock} ) return;
var x = ${bindings.scrollX ? `${block.component}.get( '${bindings.scrollX}' )` : `window.scrollX`};
var y = ${bindings.scrollY ? `${block.component}.get( '${bindings.scrollY}' )` : `window.scrollY`};
var x = ${bindings.scrollX
? `${block.component}.get( '${bindings.scrollX}' )`
: `window.scrollX`};
var y = ${bindings.scrollY
? `${block.component}.get( '${bindings.scrollY}' )`
: `window.scrollY`};
window.scrollTo( x, y );
};
` );
`);
if ( bindings.scrollX ) block.builders.create.addLine( `${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 ) {
if (bindings.scrollX)
block.builders.create.addLine(
`${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;
block.builders.create.addBlock( deindent`
${block.component}.observe( '${bindings.scrollX || bindings.scrollY}', function ( ${isX ? 'x' : 'y'} ) {
block.builders.create.addBlock(deindent`
${block.component}.observe( '${bindings.scrollX ||
bindings.scrollY}', function ( ${isX ? 'x' : 'y'} ) {
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.)
if ( bindings.online ) {
const handlerName = block.getUniqueName( `onlinestatuschanged` );
block.builders.create.addBlock( deindent`
if (bindings.online) {
const handlerName = block.getUniqueName(`onlinestatuschanged`);
block.builders.create.addBlock(deindent`
function ${handlerName} ( event ) {
${block.component}.set({ ${bindings.online}: navigator.onLine });
};
window.addEventListener( 'online', ${handlerName} );
window.addEventListener( 'offline', ${handlerName} );
` );
`);
// add initial value
generator.metaBindings.push(
`this._state.${bindings.online} = navigator.onLine;`
);
block.builders.destroy.addBlock( deindent`
block.builders.destroy.addBlock(deindent`
window.removeEventListener( 'online', ${handlerName} );
window.removeEventListener( 'offline', ${handlerName} );
` );
`);
}
}
}

@ -1,32 +1,41 @@
import deindent from '../../../utils/deindent.js';
import deindent from '../../../utils/deindent';
import visit from '../visit';
import { DomGenerator } from '../index';
import Block from '../Block';
import { Node } from '../../../interfaces';
import { State } from '../interfaces';
function isElseIf ( node: Node ) {
return node && node.children.length === 1 && node.children[0].type === 'IfBlock';
function isElseIf(node: Node) {
return (
node && node.children.length === 1 && node.children[0].type === 'IfBlock'
);
}
function isElseBranch ( branch ) {
function isElseBranch(branch) {
return branch.block && !branch.condition;
}
function getBranches ( generator: DomGenerator, block: Block, state: State, node: Node ) {
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 );
if ( isElseIf( node.else ) ) {
function getBranches(
generator: DomGenerator,
block: Block,
state: State,
node: Node
) {
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);
if (isElseIf(node.else)) {
branches.push(
...getBranches( generator, block, state, node.else.children[0] )
...getBranches(generator, block, state, node.else.children[0])
);
} else {
branches.push({
@ -34,31 +43,43 @@ function getBranches ( generator: DomGenerator, block: Block, state: State, node
block: node.else ? node.else._block.name : null,
hasUpdateMethod: node.else ? node.else._block.hasUpdateMethod : false,
hasIntroMethod: node.else ? node.else._block.hasIntroMethod : false,
hasOutroMethod: node.else ? node.else._block.hasOutroMethod : false
hasOutroMethod: node.else ? node.else._block.hasOutroMethod : false,
});
if ( node.else ) {
visitChildren( generator, block, state, node.else );
if (node.else) {
visitChildren(generator, block, state, node.else);
}
}
return branches;
}
function visitChildren ( generator: DomGenerator, block: Block, state: State, node: Node ) {
node.children.forEach( ( child: Node ) => {
visit( generator, node._block, node._state, child );
function visitChildren(
generator: DomGenerator,
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 ) {
const name = generator.getUniqueName( `if_block` );
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 ] );
export default function visitIfBlock(
generator: DomGenerator,
block: Block,
state: State,
node: Node
) {
const name = generator.getUniqueName(`if_block`);
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 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 };
if ( node.else ) {
if ( hasOutros ) {
compoundWithOutros( generator, block, state, node, branches, dynamic, vars );
if (node.else) {
if (hasOutros) {
compoundWithOutros(
generator,
block,
state,
node,
branches,
dynamic,
vars
);
} else {
compound( generator, block, state, node, branches, dynamic, vars );
compound(generator, block, state, node, branches, dynamic, vars);
}
} else {
simple( generator, block, state, node, branches[0], dynamic, vars );
simple(generator, block, state, node, branches[0], dynamic, vars);
}
if ( node.needsAnchor ) {
block.addElement( anchor, `${generator.helper( 'createComment' )}()`, state.parentNode, true );
} else if ( node.next ) {
if (node.needsAnchor) {
block.addElement(
anchor,
`${generator.helper('createComment')}()`,
state.parentNode,
true
);
} else if (node.next) {
node.next.usedAsAnchor = true;
}
}
function simple ( generator: DomGenerator, block: Block, state: State, node: Node, branch, dynamic, { name, anchor, params, if_name } ) {
block.builders.create.addBlock( deindent`
function simple(
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} );
` );
`);
const isTopLevel = !state.parentNode;
const mountOrIntro = branch.hasIntroMethod ? 'intro' : 'mount';
if ( isTopLevel ) {
block.builders.mount.addLine( `if ( ${name} ) ${name}.${mountOrIntro}( ${block.target}, anchor );` );
if (isTopLevel) {
block.builders.mount.addLine(
`if ( ${name} ) ${name}.${mountOrIntro}( ${block.target}, anchor );`
);
} 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 enter = dynamic ?
( branch.hasIntroMethod ?
deindent`
const enter = dynamic
? branch.hasIntroMethod
? deindent`
if ( ${name} ) {
${name}.update( changed, ${params} );
} else {
@ -109,83 +155,93 @@ function simple ( generator: DomGenerator, block: Block, state: State, node: Nod
}
${name}.intro( ${parentNode}, ${anchor} );
` :
deindent`
`
: deindent`
if ( ${name} ) {
${name}.update( changed, ${params} );
} else {
${name} = ${branch.block}( ${params}, ${block.component} );
${name}.mount( ${parentNode}, ${anchor} );
}
` ) :
( branch.hasIntroMethod ?
deindent`
`
: branch.hasIntroMethod
? deindent`
if ( !${name} ) ${name} = ${branch.block}( ${params}, ${block.component} );
${name}.intro( ${parentNode}, ${anchor} );
` :
deindent`
`
: deindent`
if ( !${name} ) {
${name} = ${branch.block}( ${params}, ${block.component} );
${name}.mount( ${parentNode}, ${anchor} );
}
` );
`;
// no `update()` here — we don't want to update outroing nodes,
// as that will typically result in glitching
const exit = branch.hasOutroMethod ?
deindent`
const exit = branch.hasOutroMethod
? deindent`
${name}.outro( function () {
${name}.unmount();
${name}.destroy();
${name} = null;
});
` :
deindent`
`
: deindent`
${name}.unmount();
${name}.destroy();
${name} = null;
`;
block.builders.update.addBlock( deindent`
block.builders.update.addBlock(deindent`
if ( ${branch.condition} ) {
${enter}
} else if ( ${name} ) {
${exit}
}
` );
`);
block.builders.unmount.addLine(
`${if_name}${name}.unmount();`
);
block.builders.unmount.addLine(`${if_name}${name}.unmount();`);
block.builders.destroy.addLine(
`${if_name}${name}.destroy();`
);
block.builders.destroy.addLine(`${if_name}${name}.destroy();`);
}
function compound ( generator: DomGenerator, 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` );
function compound(
generator: DomGenerator,
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} && `;
block.builders.create.addBlock( deindent`
block.builders.create.addBlock(deindent`
function ${get_block} ( ${params} ) {
${branches.map( ({ condition, block }) => {
return `${condition ? `if ( ${condition} ) ` : ''}return ${block};`;
} ).join( '\n' )}
${branches
.map(({ condition, block }) => {
return `${condition ? `if ( ${condition} ) ` : ''}return ${block};`;
})
.join('\n')}
}
var ${current_block} = ${get_block}( ${params} );
var ${name} = ${current_block_and}${current_block}( ${params}, ${block.component} );
` );
`);
const isTopLevel = !state.parentNode;
const mountOrIntro = branches[0].hasIntroMethod ? 'intro' : 'mount';
if ( isTopLevel ) {
block.builders.mount.addLine( `${if_name}${name}.${mountOrIntro}( ${block.target}, anchor );` );
if (isTopLevel) {
block.builders.mount.addLine(
`${if_name}${name}.${mountOrIntro}( ${block.target}, anchor );`
);
} 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`;
@ -199,20 +255,20 @@ function compound ( generator: DomGenerator, block: Block, state: State, node: N
${if_name}${name}.${mountOrIntro}( ${parentNode}, ${anchor} );
`;
if ( dynamic ) {
block.builders.update.addBlock( deindent`
if (dynamic) {
block.builders.update.addBlock(deindent`
if ( ${current_block} === ( ${current_block} = ${get_block}( ${params} ) ) && ${name} ) {
${name}.update( changed, ${params} );
} else {
${changeBlock}
}
` );
`);
} else {
block.builders.update.addBlock( deindent`
block.builders.update.addBlock(deindent`
if ( ${current_block} !== ( ${current_block} = ${get_block}( ${params} ) ) ) {
${changeBlock}
}
` );
`);
}
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
// (TODO does this only apply to bidi transitions?)
function compoundWithOutros ( generator: DomGenerator, block: Block, state: State, node: Node, branches, dynamic, { name, anchor, params, hasElse } ) {
const get_block = block.getUniqueName( `get_block` );
const current_block_index = block.getUniqueName( `current_block_index` );
const previous_block_index = block.getUniqueName( `previous_block_index` );
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`
function compoundWithOutros(
generator: DomGenerator,
block: Block,
state: State,
node: Node,
branches,
dynamic,
{ name, anchor, params, hasElse }
) {
const get_block = block.getUniqueName(`get_block`);
const current_block_index = block.getUniqueName(`current_block_index`);
const previous_block_index = block.getUniqueName(`previous_block_index`);
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} = [
${branches.map( branch => branch.block ).join( ',\n' )}
${branches.map(branch => branch.block).join(',\n')}
];
var ${if_blocks} = [];
function ${get_block} ( ${params} ) {
${branches.map( ({ condition, block }, i ) => {
return `${condition ? `if ( ${condition} ) ` : ''}return ${block ? i : -1};`;
} ).join( '\n' )}
${branches
.map(({ condition, block }, i) => {
return `${condition ? `if ( ${condition} ) ` : ''}return ${block
? i
: -1};`;
})
.join('\n')}
}
` );
`);
if ( hasElse ) {
block.builders.create.addBlock( deindent`
if (hasElse) {
block.builders.create.addBlock(deindent`
${current_block_index} = ${get_block}( ${params} );
${name} = ${if_blocks}[ ${current_block_index} ] = ${if_block_creators}[ ${current_block_index} ]( ${params}, ${block.component} );
` );
`);
} else {
block.builders.create.addBlock( deindent`
block.builders.create.addBlock(deindent`
if ( ~( ${current_block_index} = ${get_block}( ${params} ) ) ) {
${name} = ${if_blocks}[ ${current_block_index} ] = ${if_block_creators}[ ${current_block_index} ]( ${params}, ${block.component} );
}
` );
`);
}
const isTopLevel = !state.parentNode;
const mountOrIntro = branches[0].hasIntroMethod ? 'intro' : 'mount';
if ( isTopLevel ) {
block.builders.mount.addLine( `${if_current_block_index}${if_blocks}[ ${current_block_index} ].${mountOrIntro}( ${block.target}, anchor );` );
if (isTopLevel) {
block.builders.mount.addLine(
`${if_current_block_index}${if_blocks}[ ${current_block_index} ].${mountOrIntro}( ${block.target}, anchor );`
);
} 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`;
@ -288,13 +362,13 @@ function compoundWithOutros ( generator: DomGenerator, block: Block, state: Stat
${name}.${mountOrIntro}( ${parentNode}, ${anchor} );
`;
const changeBlock = hasElse ?
deindent`
const changeBlock = hasElse
? deindent`
${destroyOldBlock}
${createNewBlock}
` :
deindent`
`
: deindent`
if ( ${name} ) {
${destroyOldBlock}
}
@ -306,8 +380,8 @@ function compoundWithOutros ( generator: DomGenerator, block: Block, state: Stat
}
`;
if ( dynamic ) {
block.builders.update.addBlock( deindent`
if (dynamic) {
block.builders.update.addBlock(deindent`
var ${previous_block_index} = ${current_block_index};
${current_block_index} = ${get_block}( state );
if ( ${current_block_index} === ${previous_block_index} ) {
@ -315,21 +389,21 @@ function compoundWithOutros ( generator: DomGenerator, block: Block, state: Stat
} else {
${changeBlock}
}
` );
`);
} else {
block.builders.update.addBlock( deindent`
block.builders.update.addBlock(deindent`
var ${previous_block_index} = ${current_block_index};
${current_block_index} = ${get_block}( state );
if ( ${current_block_index} !== ${previous_block_index} ) {
${changeBlock}
}
` );
`);
}
block.builders.destroy.addLine( deindent`
block.builders.destroy.addLine(deindent`
${if_current_block_index}{
${if_blocks}[ ${current_block_index} ].unmount();
${if_blocks}[ ${current_block_index} ].destroy();
}
` );
`);
}

@ -1,21 +1,31 @@
import deindent from '../../../utils/deindent.js';
import deindent from '../../../utils/deindent';
import { DomGenerator } from '../index';
import Block from '../Block';
import { Node } 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 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.addElement( name, `${generator.helper( 'createText' )}( ${value} = ${snippet} )`, state.parentNode, true );
block.addVariable(value);
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} ) ) {
${name}.data = ${value};
}
` );
}
`);
}

@ -1,40 +1,57 @@
import deindent from '../../../utils/deindent.js';
import deindent from '../../../utils/deindent';
import { DomGenerator } from '../index';
import Block from '../Block';
import { Node } 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 before = node._state.name;
const value = block.getUniqueName( `${name}_value` );
const after = block.getUniqueName( `${name}_after` );
const value = block.getUniqueName(`${name}_value`);
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
// exists for `Element`s.
block.addElement( before, `${generator.helper( 'createElement' )}( 'noscript' )`, state.parentNode, true );
block.addElement( after, `${generator.helper( 'createElement' )}( 'noscript' )`, state.parentNode, true );
block.addElement(
before,
`${generator.helper('createElement')}( 'noscript' )`,
state.parentNode,
true
);
block.addElement(
after,
`${generator.helper('createElement')}( 'noscript' )`,
state.parentNode,
true
);
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 detachStatement = `${generator.helper( 'detachBetween' )}( ${before}, ${after} );`;
const detachStatement = `${generator.helper(
'detachBetween'
)}( ${before}, ${after} );`;
if ( isToplevel ) {
block.builders.mount.addLine( mountStatement );
if (isToplevel) {
block.builders.mount.addLine(mountStatement);
} 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} ) ) {
${detachStatement}
${mountStatement}
}
` );
`);
block.builders.detachRaw.addBlock( detachStatement );
}
block.builders.detachRaw.addBlock(detachStatement);
}

@ -3,7 +3,17 @@ import Block from '../Block';
import { Node } from '../../../interfaces';
import { State } from '../interfaces';
export default function visitText ( generator: DomGenerator, 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 );
}
export default function visitText(
generator: DomGenerator,
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 { 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;
( 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 );`
);
block.builders.destroy.addLine(
`if ( ${block.component}._yield ) ${block.component}._yield.unmount();`
);
}
}

@ -13,5 +13,5 @@ export default {
MustacheTag,
RawMustacheTag,
Text,
YieldTag
YieldTag,
};

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

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

@ -1,4 +1,4 @@
import deindent from '../../utils/deindent.js';
import deindent from '../../utils/deindent';
import Generator from '../Generator';
import Block from './Block';
import visit from './visit';
@ -9,36 +9,45 @@ export class SsrGenerator extends Generator {
renderCode: string;
elementDepth: number;
constructor ( parsed: Parsed, source: string, name: string, options: CompileOptions ) {
super( parsed, source, name, options );
constructor(
parsed: Parsed,
source: string,
name: string,
options: CompileOptions
) {
super(parsed, source, name, options);
this.bindings = [];
this.renderCode = '';
this.elementDepth = 0;
}
append ( code: string ) {
append(code: string) {
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 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
const mainBlock = new Block({
generator,
contexts: new Map(),
indexes: new Map(),
conditions: []
conditions: [],
});
parsed.html.children.forEach( ( node: Node ) => {
visit( generator, mainBlock, node );
parsed.html.children.forEach((node: Node) => {
visit(generator, mainBlock, node);
});
const result = deindent`
@ -46,27 +55,37 @@ export default function ssr ( parsed: Parsed, source: string, options: CompileOp
var ${name} = {};
${name}.filename = ${JSON.stringify( options.filename )};
${name}.filename = ${JSON.stringify(options.filename)};
${name}.data = function () {
return ${templateProperties.data ? `${generator.alias( 'template' )}.data()` : `{}`};
return ${templateProperties.data
? `${generator.alias('template')}.data()`
: `{}`};
};
${name}.render = function ( state, options ) {
${templateProperties.data ? `state = Object.assign( ${generator.alias( 'template' )}.data(), state || {} );` : `state = state || {};`}
${computations.map( ({ key, deps }) =>
`state.${key} = ${generator.alias( 'template' )}.computed.${key}( ${deps.map( dep => `state.${dep}` ).join( ', ' )} );`
${templateProperties.data
? `state = Object.assign( ${generator.alias(
'template'
)}.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 tmp;
while ( !settled ) {
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 () {
var components = [];
${generator.css && deindent`
${generator.css &&
deindent`
components.push({
filename: ${name}.filename,
css: ${JSON.stringify( generator.css )},
css: ${JSON.stringify(generator.css)},
map: null // TODO
});
`}
${templateProperties.components && deindent`
${templateProperties.components &&
deindent`
var seen = {};
function addComponent ( component ) {
@ -96,13 +117,13 @@ export default function ssr ( parsed: Parsed, source: string, options: CompileOp
});
}
${
templateProperties.components.value.properties.map( prop => {
const { name } = prop.key;
const expression = generator.importedComponents.get( name ) || `${generator.alias( 'template' )}.components.${name}`;
return `addComponent( ${expression} );`;
})
}
${templateProperties.components.value.properties.map(prop => {
const { name } = prop.key;
const expression =
generator.importedComponents.get(name) ||
`${generator.alias('template')}.components.${name}`;
return `addComponent( ${expression} );`;
})}
`}
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 { Node } from '../../interfaces';
export default function visit ( generator: SsrGenerator, block: Block, node: Node ) {
const visitor = visitors[ node.type ];
visitor( generator, block, node );
}
export default function visit(
generator: SsrGenerator,
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
}

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

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

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

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

@ -2,7 +2,11 @@ import { SsrGenerator } from '../index';
import Block from '../Block';
import { Node } from '../../../interfaces';
export default function visitMustacheTag ( generator: SsrGenerator, block: Block, node: Node ) {
const { snippet } = block.contextualise( node.expression );
generator.append( '${__escape( ' + snippet + ' )}' );
}
export default function visitMustacheTag(
generator: SsrGenerator,
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 { Node } from '../../../interfaces';
export default function visitRawMustacheTag ( generator: SsrGenerator, block: Block, node: Node ) {
const { snippet } = block.contextualise( node.expression );
generator.append( '${' + snippet + '}' );
}
export default function visitRawMustacheTag(
generator: SsrGenerator,
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 { Node } from '../../../interfaces';
export default function visitText ( generator: SsrGenerator, block: Block, node: Node ) {
generator.append( node.data.replace( /\${/g, '\\${' ) );
}
export default function visitText(
generator: SsrGenerator,
block: Block,
node: Node
) {
generator.append(node.data.replace(/\${/g, '\\${'));
}

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

@ -15,5 +15,5 @@ export default {
MustacheTag,
RawMustacheTag,
Text,
YieldTag
YieldTag,
};

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

@ -3,7 +3,11 @@ import { Parsed, Node } from '../../interfaces';
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 offset = parsed.css.content.start;
@ -11,75 +15,73 @@ export default function processCss ( parsed: Parsed, code: MagicString, cascade:
const keyframes = new Map();
function walkKeyframes ( node: Node ) {
if ( node.type === 'Atrule' && node.name.toLowerCase() === 'keyframes' ) {
node.expression.children.forEach( ( expression: Node ) => {
if ( expression.type === 'Identifier' ) {
if ( expression.name.startsWith( '-global-' ) ) {
code.remove( expression.start, expression.start + 8 );
function walkKeyframes(node: Node) {
if (node.type === 'Atrule' && node.name.toLowerCase() === 'keyframes') {
node.expression.children.forEach((expression: Node) => {
if (expression.type === 'Identifier') {
if (expression.name.startsWith('-global-')) {
code.remove(expression.start, expression.start + 8);
} else {
const newName = `svelte-${parsed.hash}-${expression.name}`;
code.overwrite( expression.start, expression.end, newName );
keyframes.set( expression.name, newName );
code.overwrite(expression.start, expression.end, newName);
keyframes.set(expression.name, newName);
}
}
});
} else if ( node.children ) {
node.children.forEach( walkKeyframes );
} else if ( node.block ) {
walkKeyframes( node.block );
} else if (node.children) {
node.children.forEach(walkKeyframes);
} else if (node.block) {
walkKeyframes(node.block);
}
}
parsed.css.children.forEach( walkKeyframes );
parsed.css.children.forEach(walkKeyframes);
function transform ( rule: Node ) {
rule.selector.children.forEach( ( selector: Node ) => {
if ( cascade ) {
function transform(rule: Node) {
rule.selector.children.forEach((selector: Node) => {
if (cascade) {
// TODO disable cascading (without :global(...)) in v2
const start = selector.start - offset;
const end = selector.end - offset;
const selectorString = css.slice( start, end );
const selectorString = css.slice(start, end);
const firstToken = selector.children[0];
let transformed;
if ( firstToken.type === 'TypeSelector' ) {
if (firstToken.type === 'TypeSelector') {
const insert = firstToken.end - offset;
const head = css.slice( start, insert );
const tail = css.slice( insert, end );
const head = css.slice(start, insert);
const tail = css.slice(insert, end);
transformed = `${head}${attr}${tail}, ${attr} ${selectorString}`;
} else {
transformed = `${attr}${selectorString}, ${attr} ${selectorString}`;
}
code.overwrite( selector.start, selector.end, transformed );
}
else {
code.overwrite(selector.start, selector.end, transformed);
} else {
let shouldTransform = true;
let c = selector.start;
selector.children.forEach( ( child: Node ) => {
if ( child.type === 'WhiteSpace' || child.type === 'Combinator' ) {
code.appendLeft( c, attr );
selector.children.forEach((child: Node) => {
if (child.type === 'WhiteSpace' || child.type === 'Combinator') {
code.appendLeft(c, attr);
shouldTransform = true;
return;
}
if ( !shouldTransform ) return;
if (!shouldTransform) return;
if ( child.type === 'PseudoClassSelector' ) {
if (child.type === 'PseudoClassSelector') {
// `:global(xyz)` > xyz
if ( child.name === 'global' ) {
if (child.name === 'global') {
const first = child.children[0];
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 {
code.prependRight( c, attr );
code.prependRight(c, attr);
}
shouldTransform = false;
@ -88,21 +90,21 @@ export default function processCss ( parsed: Parsed, code: MagicString, cascade:
c = child.end;
});
if ( shouldTransform ) {
code.appendLeft( c, attr );
if (shouldTransform) {
code.appendLeft(c, attr);
}
}
});
rule.block.children.forEach( ( block: Node ) => {
if ( block.type === 'Declaration' ) {
rule.block.children.forEach((block: Node) => {
if (block.type === 'Declaration') {
const property = block.property.toLowerCase();
if ( property === 'animation' || property === 'animation-name' ) {
block.value.children.forEach( ( block: Node ) => {
if ( block.type === 'Identifier' ) {
if (property === 'animation' || property === 'animation-name') {
block.value.children.forEach((block: Node) => {
if (block.type === 'Identifier') {
const name = block.name;
if ( keyframes.has( name ) ) {
code.overwrite( block.start, block.end, keyframes.get( name ) );
if (keyframes.has(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 ) {
if ( node.type === 'Rule' ) {
transform( node );
} else if ( node.type === 'Atrule' && node.name.toLowerCase() === 'keyframes' ) {
function walk(node: Node) {
if (node.type === 'Rule') {
transform(node);
} else if (
node.type === 'Atrule' &&
node.name.toLowerCase() === 'keyframes'
) {
// these have already been processed
} else if ( node.children ) {
node.children.forEach( walk );
} else if ( node.block ) {
walk( node.block );
} else if (node.children) {
node.children.forEach(walk);
} else if (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
let match;
while ( match = commentsPattern.exec( css ) ) {
while ((match = commentsPattern.exec(css))) {
const start = match.index + offset;
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 default function getGlobals ( imports: Declaration[], options: Options ) {
export default function getGlobals(imports: Declaration[], options: Options) {
const { globals, onerror, onwarn } = options;
const globalFn = getGlobalFn( globals );
return imports.map( x => {
let name = globalFn( x.source.value );
if ( !name ) {
if ( x.name.startsWith( '__import' ) ) {
const error = new Error( `Could not determine name for imported module '${x.source.value}' use options.globals` );
if ( onerror ) {
onerror( error );
const globalFn = getGlobalFn(globals);
return imports.map(x => {
let name = globalFn(x.source.value);
if (!name) {
if (x.name.startsWith('__import')) {
const error = new Error(
`Could not determine name for imported module '${x.source
.value}' use options.globals`
);
if (onerror) {
onerror(error);
} else {
throw error;
}
}
else {
} else {
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 ) {
onwarn( warning );
if (onwarn) {
onwarn(warning);
} 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 {
if ( typeof globals === 'function' ) return globals;
if ( typeof globals === 'object' ) {
return id => globals[ id ];
function getGlobalFn(globals: any): Globals {
if (typeof globals === 'function') return globals;
if (typeof globals === 'object') {
return id => globals[id];
}
return () => undefined;

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

@ -1,31 +1,36 @@
import getGlobals from './getGlobals';
export default function getOutro ( format: string, name: string, options, imports ) {
if ( format === 'es' ) {
export default function getOutro(
format: string,
name: string,
options,
imports
) {
if (format === 'es') {
return `export default ${name};`;
}
if ( format === 'amd' ) {
if (format === 'amd') {
return `return ${name};\n\n});`;
}
if ( format === 'cjs' ) {
if (format === 'cjs') {
return `module.exports = ${name};`;
}
if ( format === 'iife' ) {
const globals = getGlobals( imports, options );
return `return ${name};\n\n}(${globals.join( ', ' )}));`;
if (format === 'iife') {
const globals = getGlobals(imports, options);
return `return ${name};\n\n}(${globals.join(', ')}));`;
}
if ( format === 'eval' ) {
const globals = getGlobals( imports, options );
return `return ${name};\n\n}(${globals.join( ', ' )}));`;
if (format === 'eval') {
const globals = getGlobals(imports, options);
return `return ${name};\n\n}(${globals.join(', ')}));`;
}
if ( format === 'umd' ) {
if (format === 'umd') {
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';
export default function walkHtml ( html: Node, visitors ) {
function visit ( node: Node ) {
const visitor = visitors[ node.type ];
if ( !visitor ) throw new Error( `Not implemented: ${node.type}` );
export default function walkHtml(html: Node, visitors) {
function visit(node: Node) {
const visitor = visitors[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 ) {
node.children.forEach( ( child: Node ) => {
visit( child );
if (node.children) {
node.children.forEach((child: Node) => {
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 { Parsed, CompileOptions, Warning } from './interfaces';
function normalizeOptions ( options: CompileOptions ) :CompileOptions {
return assign({
generate: 'dom',
function normalizeOptions(options: CompileOptions): CompileOptions {
return assign(
{
generate: 'dom',
// a filename is necessary for sourcemap generation
filename: 'SvelteComponent.html',
// a filename is necessary for sourcemap generation
filename: 'SvelteComponent.html',
onwarn: ( warning: Warning ) => {
if ( warning.loc ) {
console.warn( `(${warning.loc.line}:${warning.loc.column}) ${warning.message}` ); // eslint-disable-line no-console
} else {
console.warn( warning.message ); // eslint-disable-line no-console
}
},
onwarn: (warning: Warning) => {
if (warning.loc) {
console.warn(
`(${warning.loc.line}:${warning.loc.column}) ${warning.message}`
); // 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 ) {
const options = normalizeOptions( _options );
export function compile(source: string, _options: CompileOptions) {
const options = normalizeOptions(_options);
let parsed: Parsed;
try {
parsed = parse( source, options );
} catch ( err ) {
options.onerror( err );
parsed = parse(source, options);
} catch (err) {
options.onerror(err);
return;
}
validate( parsed, source, options );
validate(parsed, source, options);
const compiler = options.generate === 'ssr'
? generateSSR
: generate;
const compiler = options.generate === 'ssr' ? 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';
const compiled = compile( source, _options );
const compiled = compile(source, _options);
if ( !compiled || !compiled.code ) {
if (!compiled || !compiled.code) {
return;
}
try {
return (new Function( 'return ' + compiled.code ))();
} catch ( err ) {
if ( _options.onerror ) {
_options.onerror( err );
return new Function('return ' + compiled.code)();
} catch (err) {
if (_options.onerror) {
_options.onerror(err);
return;
} else {
throw err;

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

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

@ -1,62 +1,58 @@
import { parseExpressionAt } from 'acorn';
import spaces from '../../utils/spaces.js';
import spaces from '../../utils/spaces';
import { Parser } from '../index';
function readExpression ( parser: Parser, start: number, quoteMark ) {
function readExpression(parser: Parser, start: number, quoteMark) {
let str = '';
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];
if ( quoteMark ) {
if ( char === quoteMark ) {
if ( escaped ) {
if (quoteMark) {
if (char === quoteMark) {
if (escaped) {
str += quoteMark;
} else {
break;
}
} else if ( escaped ) {
} else if (escaped) {
str += '\\' + char;
escaped = false;
} else if ( char === '\\' ) {
} else if (char === '\\') {
escaped = true;
} else {
str += char;
}
}
else if ( /\s/.test( char ) ) {
} else if (/\s/.test(char)) {
break;
}
else {
} else {
str += char;
}
}
const expression = parseExpressionAt( spaces( start ) + str, start );
const expression = parseExpressionAt(spaces(start) + str, start);
parser.index = expression.end;
parser.allowWhitespace();
if ( quoteMark ) parser.eat( quoteMark, true );
if (quoteMark) parser.eat(quoteMark, true);
return expression;
}
export function readEventHandlerDirective ( parser: Parser, start: number, name: string ) {
const quoteMark = (
parser.eat( `'` ) ? `'` :
parser.eat( `"` ) ? `"` :
null
);
export function readEventHandlerDirective(
parser: Parser,
start: number,
name: string
) {
const quoteMark = parser.eat(`'`) ? `'` : parser.eat(`"`) ? `"` : null;
const expressionStart = parser.index;
const expression = readExpression( parser, expressionStart, quoteMark );
const expression = readExpression(parser, expressionStart, quoteMark);
if ( expression.type !== 'CallExpression' ) {
parser.error( `Expected call expression`, expressionStart );
if (expression.type !== 'CallExpression') {
parser.error(`Expected call expression`, expressionStart);
}
return {
@ -64,53 +60,53 @@ export function readEventHandlerDirective ( parser: Parser, start: number, name:
end: parser.index,
type: 'EventHandler',
name,
expression
expression,
};
}
export function readBindingDirective ( parser: Parser, start: number, name: string ) {
export function readBindingDirective(
parser: Parser,
start: number,
name: string
) {
let value;
if ( parser.eat( '=' ) ) {
const quoteMark = (
parser.eat( `'` ) ? `'` :
parser.eat( `"` ) ? `"` :
null
);
if (parser.eat('=')) {
const quoteMark = parser.eat(`'`) ? `'` : parser.eat(`"`) ? `"` : null;
const a = parser.index;
if ( parser.eat( '{{' ) ) {
if (parser.eat('{{')) {
let message = 'bound values should not be wrapped';
const b = parser.template.indexOf( '}}', a );
if ( b !== -1 ) {
const value = parser.template.slice( parser.index, b );
const b = parser.template.indexOf('}}', a);
if (b !== -1) {
const value = parser.template.slice(parser.index, b);
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
let b;
if ( quoteMark ) {
b = parser.index = parser.template.indexOf( quoteMark, parser.index );
if (quoteMark) {
b = parser.index = parser.template.indexOf(quoteMark, parser.index);
} else {
parser.readUntil( /[\s\r\n\/>]/ );
parser.readUntil(/[\s\r\n\/>]/);
b = parser.index;
}
const source = spaces( a ) + parser.template.slice( a, b );
value = parseExpressionAt( source, a );
const source = spaces(a) + parser.template.slice(a, b);
value = parseExpressionAt(source, a);
if ( value.type !== 'Identifier' && value.type !== 'MemberExpression' ) {
parser.error( `Cannot bind to rvalue`, value.start );
if (value.type !== 'Identifier' && value.type !== 'MemberExpression') {
parser.error(`Cannot bind to rvalue`, value.start);
}
parser.allowWhitespace();
if ( quoteMark ) {
parser.eat( quoteMark, true );
if (quoteMark) {
parser.eat(quoteMark, true);
}
} else {
// shorthand bind:foo equivalent to bind:foo='foo'
@ -118,7 +114,7 @@ export function readBindingDirective ( parser: Parser, start: number, name: stri
type: 'Identifier',
start: start + 5,
end: parser.index,
name
name,
};
}
@ -127,26 +123,27 @@ export function readBindingDirective ( parser: Parser, start: number, name: stri
end: parser.index,
type: 'Binding',
name,
value
value,
};
}
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;
if ( parser.eat( '=' ) ) {
const quoteMark = (
parser.eat( `'` ) ? `'` :
parser.eat( `"` ) ? `"` :
null
);
if (parser.eat('=')) {
const quoteMark = parser.eat(`'`) ? `'` : parser.eat(`"`) ? `"` : null;
const expressionStart = parser.index;
expression = readExpression( parser, expressionStart, quoteMark );
expression = readExpression(parser, expressionStart, quoteMark);
if ( expression.type !== 'ObjectExpression' ) {
parser.error( `Expected object expression`, expressionStart );
if (expression.type !== 'ObjectExpression') {
parser.error(`Expected object expression`, expressionStart);
}
}
@ -157,6 +154,6 @@ export function readTransitionDirective ( parser: Parser, start: number, name: s
name,
intro: type === 'in' || type === 'transition',
outro: type === 'out' || type === 'transition',
expression
expression,
};
}
}

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

@ -1,36 +1,37 @@
import { parse } from 'acorn';
import spaces from '../../utils/spaces.js';
import spaces from '../../utils/spaces';
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 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;
let ast;
try {
ast = parse( source, {
ast = parse(source, {
ecmaVersion: 8,
sourceType: 'module'
sourceType: 'module',
});
} catch ( err ) {
parser.acornError( err );
} catch (err) {
parser.acornError(err);
}
if ( !ast.body.length ) return null;
if (!ast.body.length) return null;
ast.start = scriptStart;
return {
start,
end: parser.index,
attributes,
content: ast
content: ast,
};
}

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

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

@ -6,92 +6,93 @@ import { Node } from '../../interfaces';
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 lastChild = block.children[ block.children.length - 1 ];
const lastChild = block.children[block.children.length - 1];
if ( firstChild.type === 'Text' && trimBefore ) {
firstChild.data = trimStart( firstChild.data );
if ( !firstChild.data ) block.children.shift();
if (firstChild.type === 'Text' && trimBefore) {
firstChild.data = trimStart(firstChild.data);
if (!firstChild.data) block.children.shift();
}
if ( lastChild.type === 'Text' && trimAfter ) {
lastChild.data = trimEnd( lastChild.data );
if ( !lastChild.data ) block.children.pop();
if (lastChild.type === 'Text' && trimAfter) {
lastChild.data = trimEnd(lastChild.data);
if (!lastChild.data) block.children.pop();
}
if ( block.else ) {
trimWhitespace( block.else, trimBefore, trimAfter );
if (block.else) {
trimWhitespace(block.else, trimBefore, trimAfter);
}
if ( firstChild.elseif ) {
trimWhitespace( firstChild, trimBefore, trimAfter );
if (firstChild.elseif) {
trimWhitespace(firstChild, trimBefore, trimAfter);
}
}
export default function mustache ( parser: Parser ) {
export default function mustache(parser: Parser) {
const start = parser.index;
parser.index += 2;
parser.allowWhitespace();
// {{/if}} or {{/each}}
if ( parser.eat( '/' ) ) {
if (parser.eat('/')) {
let block = parser.current();
let expected;
if ( block.type === 'ElseBlock' ) {
if (block.type === 'ElseBlock') {
block.end = start;
parser.stack.pop();
block = parser.current();
}
if ( block.type === 'IfBlock' ) {
if (block.type === 'IfBlock') {
expected = 'if';
} else if ( block.type === 'EachBlock' ) {
} else if (block.type === 'EachBlock') {
expected = 'each';
} else {
parser.error( `Unexpected block closing tag` );
parser.error(`Unexpected block closing tag`);
}
parser.eat( expected, true );
parser.eat(expected, true);
parser.allowWhitespace();
parser.eat( '}}', true );
parser.eat('}}', true);
while ( block.elseif ) {
while (block.elseif) {
block.end = parser.index;
parser.stack.pop();
block = parser.current();
if ( block.else ) {
if (block.else) {
block.else.end = start;
}
}
// 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 charAfter = parser.template[ parser.index ];
const trimBefore = !charBefore || whitespace.test( charBefore );
const trimAfter = !charAfter || whitespace.test( charAfter );
const charBefore = parser.template[block.start - 1];
const charAfter = parser.template[parser.index];
const trimBefore = !charBefore || whitespace.test(charBefore);
const trimAfter = !charAfter || whitespace.test(charAfter);
trimWhitespace( block, trimBefore, trimAfter );
trimWhitespace(block, trimBefore, trimAfter);
block.end = parser.index;
parser.stack.pop();
}
else if ( parser.eat( 'elseif' ) ) {
} else if (parser.eat('elseif')) {
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();
const expression = readExpression( parser );
const expression = readExpression(parser);
parser.allowWhitespace();
parser.eat( '}}', true );
parser.eat('}}', true);
block.else = {
start: parser.index,
@ -104,128 +105,119 @@ export default function mustache ( parser: Parser ) {
type: 'IfBlock',
elseif: true,
expression,
children: []
}
]
children: [],
},
],
};
parser.stack.push( block.else.children[0] );
}
else if ( parser.eat( 'else' ) ) {
parser.stack.push(block.else.children[0]);
} else if (parser.eat('else')) {
const block = parser.current();
if ( block.type !== 'IfBlock' && block.type !== 'EachBlock' ) {
parser.error( 'Cannot have an {{else}} block outside an {{#if ...}} or {{#each ...}} block' );
if (block.type !== 'IfBlock' && block.type !== 'EachBlock') {
parser.error(
'Cannot have an {{else}} block outside an {{#if ...}} or {{#each ...}} block'
);
}
parser.allowWhitespace();
parser.eat( '}}', true );
parser.eat('}}', true);
block.else = {
start: parser.index,
end: null,
type: 'ElseBlock',
children: []
children: [],
};
parser.stack.push( block.else );
}
// {{#if foo}} or {{#each foo}}
else if ( parser.eat( '#' ) ) {
parser.stack.push(block.else);
} else if (parser.eat('#')) {
// {{#if foo}} or {{#each foo}}
let type;
if ( parser.eat( 'if' ) ) {
if (parser.eat('if')) {
type = 'IfBlock';
} else if ( parser.eat( 'each' ) ) {
} else if (parser.eat('each')) {
type = 'EachBlock';
} else {
parser.error( `Expected if or each` );
parser.error(`Expected if or each`);
}
parser.requireWhitespace();
const expression = readExpression( parser );
const expression = readExpression(parser);
const block: Node = {
start,
end: null,
type,
expression,
children: []
children: [],
};
parser.allowWhitespace();
// {{#each}} blocks must declare a context {{#each list as item}}
if ( type === 'EachBlock' ) {
parser.eat( 'as', true );
if (type === 'EachBlock') {
parser.eat('as', true);
parser.requireWhitespace();
block.context = parser.read( validIdentifier ); // TODO check it's not a keyword
if ( !block.context ) parser.error( `Expected name` );
block.context = parser.read(validIdentifier); // TODO check it's not a keyword
if (!block.context) parser.error(`Expected name`);
parser.allowWhitespace();
if ( parser.eat( ',' ) ) {
if (parser.eat(',')) {
parser.allowWhitespace();
block.index = parser.read( validIdentifier );
if ( !block.index ) parser.error( `Expected name` );
block.index = parser.read(validIdentifier);
if (!block.index) parser.error(`Expected name`);
parser.allowWhitespace();
}
if ( parser.eat( '@' ) ) {
block.key = parser.read( validIdentifier );
if ( !block.key ) parser.error( `Expected name` );
if (parser.eat('@')) {
block.key = parser.read(validIdentifier);
if (!block.key) parser.error(`Expected name`);
parser.allowWhitespace();
}
}
parser.eat( '}}', true );
parser.current().children.push( block );
parser.stack.push( block );
}
// {{yield}}
else if ( parser.eat( 'yield' ) ) {
parser.eat('}}', true);
parser.current().children.push(block);
parser.stack.push(block);
} else if (parser.eat('yield')) {
// {{yield}}
parser.allowWhitespace();
parser.eat( '}}', true );
parser.eat('}}', true);
parser.current().children.push({
start,
end: parser.index,
type: 'YieldTag'
type: 'YieldTag',
});
}
// {{{raw}}} mustache
else if ( parser.eat( '{' ) ) {
const expression = readExpression( parser );
} else if (parser.eat('{')) {
// {{{raw}}} mustache
const expression = readExpression(parser);
parser.allowWhitespace();
parser.eat( '}}}', true );
parser.eat('}}}', true);
parser.current().children.push({
start,
end: parser.index,
type: 'RawMustacheTag',
expression
expression,
});
}
else {
const expression = readExpression( parser );
} else {
const expression = readExpression(parser);
parser.allowWhitespace();
parser.eat( '}}', true );
parser.eat('}}', true);
parser.current().children.push({
start,
end: parser.index,
type: 'MustacheTag',
expression
expression,
});
}

@ -1,7 +1,11 @@
import readExpression from '../read/expression';
import readScript from '../read/script';
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 { decodeCharacterReferences } from '../utils/html';
import isVoidElementName from '../../utils/isVoidElementName';
@ -13,106 +17,129 @@ const validTagName = /^\!?[a-zA-Z]{1,}:?[a-zA-Z0-9\-]*/;
const SELF = ':Self';
const metaTags = {
':Window': true
':Window': true,
};
const specials = new Map( [
[ 'script', {
read: readScript,
property: 'js'
} ],
[ 'style', {
read: readStyle,
property: 'css'
} ]
] );
const specials = new Map([
[
'script',
{
read: readScript,
property: 'js',
},
],
[
'style',
{
read: readStyle,
property: 'css',
},
],
]);
// based on http://developers.whatwg.org/syntax.html#syntax-tag-omission
const disallowedContents = new Map( [
[ 'li', new Set( [ 'li' ] ) ],
[ 'dt', 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' ] ) ],
[ 'rp', new Set( [ 'rt', 'rp' ] ) ],
[ 'optgroup', new Set( [ 'optgroup' ] ) ],
[ 'option', new Set( [ 'option', 'optgroup' ] ) ],
[ 'thead', new Set( [ 'tbody', 'tfoot' ] ) ],
[ 'tbody', new Set( [ 'tbody', 'tfoot' ] ) ],
[ '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 disallowedContents = new Map([
['li', new Set(['li'])],
['dt', 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'])],
['rp', new Set(['rt', 'rp'])],
['optgroup', new Set(['optgroup'])],
['option', new Set(['option', 'optgroup'])],
['thead', new Set(['tbody', 'tfoot'])],
['tbody', new Set(['tbody', 'tfoot'])],
['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 lastChild = element.children[ element.children.length - 1 ];
const lastChild = element.children[element.children.length - 1];
if ( firstChild.type === 'Text' ) {
firstChild.data = trimStart( firstChild.data );
if ( !firstChild.data ) element.children.shift();
if (firstChild.type === 'Text') {
firstChild.data = trimStart(firstChild.data);
if (!firstChild.data) element.children.shift();
}
if ( lastChild.type === 'Text' ) {
lastChild.data = trimEnd( lastChild.data );
if ( !lastChild.data ) element.children.pop();
if (lastChild.type === 'Text') {
lastChild.data = trimEnd(lastChild.data);
if (!lastChild.data) element.children.pop();
}
}
}
export default function tag ( parser: Parser ) {
export default function tag(parser: Parser) {
const start = parser.index++;
let parent = parser.current();
if ( parser.eat( '!--' ) ) {
const data = parser.readUntil( /-->/ );
parser.eat( '-->' );
if (parser.eat('!--')) {
const data = parser.readUntil(/-->/);
parser.eat('-->');
parser.current().children.push({
start,
end: parser.index,
type: 'Comment',
data
data,
});
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 parser.metaTags ) {
if ( isClosingTag && parser.current().children.length ) {
parser.error( `<${name}> cannot have children`, parser.current().children[0].start );
if (name in metaTags) {
if (name in parser.metaTags) {
if (isClosingTag && parser.current().children.length) {
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 ) {
parser.error( `<${name}> tags cannot be inside elements or blocks`, start );
if (parser.stack.length > 1) {
parser.error(`<${name}> tags cannot be inside elements or blocks`, start);
}
}
parser.allowWhitespace();
if ( isClosingTag ) {
if ( isVoidElementName( name ) ) {
parser.error( `<${name}> is a void element and cannot have children, or a closing tag`, start );
if (isClosingTag) {
if (isVoidElementName(name)) {
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>
while ( parent.name !== name ) {
if ( parent.type !== 'Element' ) parser.error( `</${name}> attempted to close an element that was not open`, start );
while (parent.name !== name) {
if (parent.type !== 'Element')
parser.error(
`</${name}> attempted to close an element that was not open`,
start
);
parent.end = start;
parser.stack.pop();
@ -121,17 +148,17 @@ export default function tag ( parser: Parser ) {
}
// strip leading/trailing whitespace as necessary
stripWhitespace( parent );
stripWhitespace(parent);
parent.end = parser.index;
parser.stack.pop();
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
// close it, like `<li>one<li>two`?
if ( disallowedContents.get( parent.name ).has( name ) ) {
stripWhitespace( parent );
if (disallowedContents.get(parent.name).has(name)) {
stripWhitespace(parent);
parent.end = start;
parser.stack.pop();
@ -142,24 +169,26 @@ export default function tag ( parser: Parser ) {
const uniqueNames = new Set();
let attribute;
while ( attribute = readAttribute( parser, uniqueNames ) ) {
attributes.push( attribute );
while ((attribute = readAttribute(parser, uniqueNames))) {
attributes.push(attribute);
parser.allowWhitespace();
}
parser.allowWhitespace();
// special cases top-level <script> and <style>
if ( specials.has( name ) && parser.stack.length === 1 ) {
const special = specials.get( name );
if (specials.has(name) && parser.stack.length === 1) {
const special = specials.get(name);
if ( parser[ special.property ] ) {
if (parser[special.property]) {
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[ special.property ] = special.read( parser, start, attributes );
parser.eat('>', true);
parser[special.property] = special.read(parser, start, attributes);
return;
}
@ -169,109 +198,121 @@ export default function tag ( parser: Parser ) {
type: 'Element',
name,
attributes,
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;
} else if ( name === 'textarea' ) {
} else if (name === 'textarea') {
// special case
element.children = readSequence( parser, () => parser.template.slice( parser.index, parser.index + 11 ) === '</textarea>' );
parser.read( /<\/textarea>/ );
element.children = readSequence(
parser,
() =>
parser.template.slice(parser.index, parser.index + 11) === '</textarea>'
);
parser.read(/<\/textarea>/);
element.end = parser.index;
} else {
// don't push self-closing elements onto the stack
parser.stack.push( element );
parser.stack.push(element);
}
return null;
}
function readTagName ( parser: Parser ) {
function readTagName(parser: Parser) {
const start = parser.index;
if ( parser.eat( SELF ) ) {
if (parser.eat(SELF)) {
// check we're inside a block, otherwise this
// will cause infinite recursion
let i = parser.stack.length;
let legal = false;
while ( i-- ) {
while (i--) {
const fragment = parser.stack[i];
if ( fragment.type === 'IfBlock' || fragment.type === 'EachBlock' ) {
if (fragment.type === 'IfBlock' || fragment.type === 'EachBlock') {
legal = true;
break;
}
}
if ( !legal ) {
parser.error( `<${SELF}> components can only exist inside if-blocks or each-blocks`, start );
if (!legal) {
parser.error(
`<${SELF}> components can only exist inside if-blocks or each-blocks`,
start
);
}
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 ) ) {
parser.error( `Expected valid tag name`, start );
if (!validTagName.test(name)) {
parser.error(`Expected valid tag name`, start);
}
return name;
}
function readAttribute ( parser: Parser, uniqueNames ) {
function readAttribute(parser: Parser, uniqueNames) {
const start = parser.index;
let name = parser.readUntil( /(\s|=|\/|>)/ );
if ( !name ) return null;
if ( uniqueNames.has( name ) ) {
parser.error( 'Attributes need to be unique', start );
let name = parser.readUntil(/(\s|=|\/|>)/);
if (!name) return null;
if (uniqueNames.has(name)) {
parser.error('Attributes need to be unique', start);
}
uniqueNames.add( name );
uniqueNames.add(name);
parser.allowWhitespace();
if ( /^on:/.test( name ) ) {
parser.eat( '=', true );
return readEventHandlerDirective( parser, start, name.slice( 3 ) );
if (/^on:/.test(name)) {
parser.eat('=', true);
return readEventHandlerDirective(parser, start, name.slice(3));
}
if ( /^bind:/.test( name ) ) {
return readBindingDirective( parser, start, name.slice( 5 ) );
if (/^bind:/.test(name)) {
return readBindingDirective(parser, start, name.slice(5));
}
if ( /^ref:/.test( name ) ) {
if (/^ref:/.test(name)) {
return {
start,
end: parser.index,
type: 'Ref',
name: name.slice( 4 )
name: name.slice(4),
};
}
const match = /^(in|out|transition):/.exec( name );
if ( match ) {
return readTransitionDirective( parser, start, name.slice( match[0].length ), match[1] );
const match = /^(in|out|transition):/.exec(name);
if (match) {
return readTransitionDirective(
parser,
start,
name.slice(match[0].length),
match[1]
);
}
let value;
// :foo is shorthand for foo='{{foo}}'
if ( /^:\w+$/.test( name ) ) {
name = name.slice( 1 );
value = getShorthandValue( start + 1, name );
if (/^:\w+$/.test(name)) {
name = name.slice(1);
value = getShorthandValue(start + 1, name);
} else {
value = parser.eat( '=' ) ? readAttributeValue( parser ) : true;
value = parser.eat('=') ? readAttributeValue(parser) : true;
}
return {
@ -279,101 +320,96 @@ function readAttribute ( parser: Parser, uniqueNames ) {
end: parser.index,
type: 'Attribute',
name,
value
value,
};
}
function readAttributeValue ( parser: Parser ) {
const quoteMark = (
parser.eat( `'` ) ? `'` :
parser.eat( `"` ) ? `"` :
null
);
function readAttributeValue(parser: Parser) {
const quoteMark = parser.eat(`'`) ? `'` : parser.eat(`"`) ? `"` : null;
const regex = (
quoteMark === `'` ? /'/ :
quoteMark === `"` ? /"/ :
/[\s"'=<>\/`]/
);
const regex = quoteMark === `'`
? /'/
: quoteMark === `"` ? /"/ : /[\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;
}
function getShorthandValue ( start: number, name: string ) {
function getShorthandValue(start: number, name: string) {
const end = start + name.length;
return [{
type: 'AttributeShorthand',
start,
end,
expression: {
type: 'Identifier',
return [
{
type: 'AttributeShorthand',
start,
end,
name
}
}];
expression: {
type: 'Identifier',
start,
end,
name,
},
},
];
}
function readSequence ( parser: Parser, done: () => boolean ) {
function readSequence(parser: Parser, done: () => boolean) {
let currentChunk: Node = {
start: parser.index,
end: null,
type: 'Text',
data: ''
data: '',
};
const chunks = [];
while ( parser.index < parser.template.length ) {
while (parser.index < parser.template.length) {
const index = parser.index;
if ( done() ) {
if (done()) {
currentChunk.end = parser.index;
if ( currentChunk.data ) chunks.push( currentChunk );
if (currentChunk.data) chunks.push(currentChunk);
chunks.forEach( chunk => {
if ( chunk.type === 'Text' ) chunk.data = decodeCharacterReferences( chunk.data );
chunks.forEach(chunk => {
if (chunk.type === 'Text')
chunk.data = decodeCharacterReferences(chunk.data);
});
return chunks;
}
else if ( parser.eat( '{{' ) ) {
if ( currentChunk.data ) {
} else if (parser.eat('{{')) {
if (currentChunk.data) {
currentChunk.end = index;
chunks.push( currentChunk );
chunks.push(currentChunk);
}
const expression = readExpression( parser );
const expression = readExpression(parser);
parser.allowWhitespace();
if ( !parser.eat( '}}' ) ) {
parser.error( `Expected }}` );
if (!parser.eat('}}')) {
parser.error(`Expected }}`);
}
chunks.push({
start: index,
end: parser.index,
type: 'MustacheTag',
expression
expression,
});
currentChunk = {
start: parser.index,
end: null,
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 { Parser } from '../index';
export default function text ( parser: Parser ) {
export default function text(parser: Parser) {
const start = parser.index;
let data = '';
while ( parser.index < parser.template.length && !parser.match( '<' ) && !parser.match( '{{' ) ) {
data += parser.template[ parser.index++ ];
while (
parser.index < parser.template.length &&
!parser.match('<') &&
!parser.match('{{')
) {
data += parser.template[parser.index++];
}
parser.current().children.push({
start,
end: parser.index,
type: 'Text',
data: decodeCharacterReferences( data )
data: decodeCharacterReferences(data),
});
return null;

@ -2030,5 +2030,5 @@ export default {
sc: 8827,
wp: 8472,
wr: 8768,
xi: 958
};
xi: 958,
};

@ -1,8 +1,8 @@
// 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 i = str.length;
while ( i-- ) hash = ( hash * 33 ) ^ str.charCodeAt( i );
while (i--) hash = (hash * 33) ^ str.charCodeAt(i);
return hash >>> 0;
}

@ -1,26 +1,62 @@
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 entityPattern = new RegExp( `&(#?(?:x[\\w\\d]+|\\d+|${Object.keys( htmlEntities ).join( '|' )}));?`, 'g' );
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 entityPattern = new RegExp(
`&(#?(?:x[\\w\\d]+|\\d+|${Object.keys(htmlEntities).join('|')}));?`,
'g'
);
export function decodeCharacterReferences ( html: string ) {
return html.replace( entityPattern, ( match, entity ) => {
export function decodeCharacterReferences(html: string) {
return html.replace(entityPattern, (match, entity) => {
let code;
// Handle named entities
if ( entity[0] !== '#' ) {
code = htmlEntities[ entity ];
} else if ( entity[1] === 'x' ) {
code = parseInt( entity.substring( 2 ), 16 );
if (entity[0] !== '#') {
code = htmlEntities[entity];
} else if (entity[1] === 'x') {
code = parseInt(entity.substring(2), 16);
} else {
code = parseInt( entity.substring( 1 ), 10 );
code = parseInt(entity.substring(1), 10);
}
if ( !code ) {
if (!code) {
return match;
}
return String.fromCodePoint( validateCode( code ) );
return String.fromCodePoint(validateCode(code));
});
}
@ -31,45 +67,45 @@ const NUL = 0;
// to replace them ourselves
//
// 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
if ( code === 10 ) {
if (code === 10) {
return 32;
}
// ASCII range. (Why someone would use HTML entities for ASCII characters I don't know, but...)
if ( code < 128 ) {
if (code < 128) {
return code;
}
// 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
if ( code <= 159 ) {
return windows1252[ code - 128 ];
if (code <= 159) {
return windows1252[code - 128];
}
// basic multilingual plane
if ( code < 55296 ) {
if (code < 55296) {
return code;
}
// UTF-16 surrogate halves
if ( code <= 57343 ) {
if (code <= 57343) {
return NUL;
}
// rest of the basic multilingual plane
if ( code <= 65535 ) {
if (code <= 65535) {
return code;
}
// supplementary multilingual plane 0x10000 - 0x1ffff
if ( code >= 65536 && code <= 131071 ) {
if (code >= 65536 && code <= 131071) {
return code;
}
// supplementary ideographic plane 0x20000 - 0x2ffff
if ( code >= 131072 && code <= 196607 ) {
if (code >= 131072 && code <= 196607) {
return code;
}

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

@ -1,35 +1,38 @@
const fs = require( 'fs' );
const path = require( 'path' );
const acorn = require( 'acorn' );
const fs = require('fs');
const path = require('path');
const acorn = require('acorn');
const declarations = {};
fs.readdirSync( __dirname ).forEach( file => {
if ( !/^[a-z]+\.js$/.test( file ) ) return;
fs.readdirSync(__dirname).forEach(file => {
if (!/^[a-z]+\.js$/.test(file)) return;
const source = fs.readFileSync( path.join( __dirname, file ), 'utf-8' );
const ast = acorn.parse( source, {
const source = fs.readFileSync(path.join(__dirname, file), 'utf-8');
const ast = acorn.parse(source, {
ecmaVersion: 6,
sourceType: 'module'
});
ast.body.forEach( node => {
if ( node.type !== 'ExportNamedDeclaration' ) return;
ast.body.forEach(node => {
if (node.type !== 'ExportNamedDeclaration') return;
const declaration = node.declaration;
if ( !declaration ) return;
if (!declaration) return;
const name = declaration.type === 'VariableDeclaration' ?
declaration.declarations[0].id.name :
declaration.id.name;
const name = declaration.type === 'VariableDeclaration'
? declaration.declarations[0].id.name
: declaration.id.name;
const value = declaration.type === 'VariableDeclaration' ?
declaration.declarations[0].init :
declaration;
const value = declaration.type === 'VariableDeclaration'
? declaration.declarations[0].init
: 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
export default ${JSON.stringify( declarations, null, '\t' )};` );
fs.writeFileSync(
'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 ) {
target.appendChild( node );
export function appendNode(node, target) {
target.appendChild(node);
}
export function insertNode ( node, target, anchor ) {
target.insertBefore( node, anchor );
export function insertNode(node, target, anchor) {
target.insertBefore(node, anchor);
}
export function detachNode ( node ) {
node.parentNode.removeChild( node );
export function detachNode(node) {
node.parentNode.removeChild(node);
}
export function detachBetween ( before, after ) {
while ( before.nextSibling && before.nextSibling !== after ) {
before.parentNode.removeChild( before.nextSibling );
export function detachBetween(before, after) {
while (before.nextSibling && before.nextSibling !== after) {
before.parentNode.removeChild(before.nextSibling);
}
}
// TODO this is out of date
export function destroyEach ( iterations, detach, start ) {
for ( var i = start; i < iterations.length; i += 1 ) {
if ( iterations[i] ) iterations[i].destroy( detach );
export function destroyEach(iterations, detach, start) {
for (var i = start; i < iterations.length; i += 1) {
if (iterations[i]) iterations[i].destroy(detach);
}
}
export function createElement ( name ) {
return document.createElement( name );
export function createElement(name) {
return document.createElement(name);
}
export function createSvgElement ( name ) {
return document.createElementNS( 'http://www.w3.org/2000/svg', name );
export function createSvgElement(name) {
return document.createElementNS('http://www.w3.org/2000/svg', name);
}
export function createText ( data ) {
return document.createTextNode( data );
export function createText(data) {
return document.createTextNode(data);
}
export function createComment () {
return document.createComment( '' );
export function createComment() {
return document.createComment('');
}
export function addEventListener ( node, event, handler ) {
node.addEventListener( event, handler, false );
export function addEventListener(node, event, handler) {
node.addEventListener(event, handler, false);
}
export function removeEventListener ( node, event, handler ) {
node.removeEventListener( event, handler, false );
export function removeEventListener(node, event, handler) {
node.removeEventListener(event, handler, false);
}
export function setAttribute ( node, attribute, value ) {
node.setAttribute( attribute, value );
export function setAttribute(node, attribute, value) {
node.setAttribute(attribute, value);
}
export function setXlinkAttribute ( node, attribute, value ) {
node.setAttributeNS( 'http://www.w3.org/1999/xlink', attribute, value );
export function setXlinkAttribute(node, attribute, value) {
node.setAttributeNS('http://www.w3.org/1999/xlink', attribute, value);
}
export function getBindingGroupValue ( group ) {
export function getBindingGroupValue(group) {
var value = [];
for ( var i = 0; i < group.length; i += 1 ) {
if ( group[i].checked ) value.push( group[i].__value );
for (var i = 0; i < group.length; i += 1) {
if (group[i].checked) value.push(group[i].__value);
}
return value;
}
export function toNumber ( value ) {
export function toNumber(value) {
return value === '' ? undefined : +value;
}
}

@ -3,109 +3,116 @@ export * from './dom.js';
export * from './transitions.js';
export * from './utils.js';
export function differs ( a, b ) {
return ( a !== b ) || ( a && ( typeof a === 'object' ) || ( typeof a === 'function' ) );
export function differs(a, b) {
return a !== b || ((a && typeof a === 'object') || typeof a === 'function');
}
export function dispatchObservers ( component, group, newState, oldState ) {
for ( var key in group ) {
if ( !( key in newState ) ) continue;
export function dispatchObservers(component, group, newState, oldState) {
for (var key in group) {
if (!(key in newState)) continue;
var newValue = newState[ key ];
var oldValue = oldState[ key ];
var newValue = newState[key];
var oldValue = oldState[key];
if ( differs( newValue, oldValue ) ) {
var callbacks = group[ key ];
if ( !callbacks ) continue;
if (differs(newValue, oldValue)) {
var callbacks = group[key];
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];
if ( callback.__calling ) continue;
if (callback.__calling) continue;
callback.__calling = true;
callback.call( component, newValue, oldValue );
callback.call(component, newValue, oldValue);
callback.__calling = false;
}
}
}
}
export function get ( key ) {
return key ? this._state[ key ] : this._state;
export function get(key) {
return key ? this._state[key] : this._state;
}
export function fire ( eventName, data ) {
var handlers = eventName in this._handlers && this._handlers[ eventName ].slice();
if ( !handlers ) return;
export function fire(eventName, data) {
var handlers =
eventName in this._handlers && this._handlers[eventName].slice();
if (!handlers) return;
for ( var i = 0; i < handlers.length; i += 1 ) {
handlers[i].call( this, data );
for (var i = 0; i < handlers.length; i += 1) {
handlers[i].call(this, data);
}
}
export function observe ( key, callback, options ) {
var group = ( options && options.defer ) ? this._observers.post : this._observers.pre;
export function observe(key, callback, options) {
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.call( this, this._state[ key ] );
callback.call(this, this._state[key]);
callback.__calling = false;
}
return {
cancel: function () {
var index = group[ key ].indexOf( callback );
if ( ~index ) group[ key ].splice( index, 1 );
cancel: function() {
var index = group[key].indexOf(callback);
if (~index) group[key].splice(index, 1);
}
};
}
export function observeDev ( key, callback, options ) {
var c = ( key = '' + key ).search( /[^\w]/ );
if ( c > -1 ) {
var message = "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 + "'";
export function observeDev(key, callback, options) {
var c = (key = '' + key).search(/[^\w]/);
if (c > -1) {
var message =
'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 ) {
if ( eventName === 'teardown' ) return this.on( 'destroy', handler );
export function on(eventName, handler) {
if (eventName === 'teardown') return this.on('destroy', handler);
var handlers = this._handlers[ eventName ] || ( this._handlers[ eventName ] = [] );
handlers.push( handler );
var handlers = this._handlers[eventName] || (this._handlers[eventName] = []);
handlers.push(handler);
return {
cancel: function () {
var index = handlers.indexOf( handler );
if ( ~index ) handlers.splice( index, 1 );
cancel: function() {
var index = handlers.indexOf(handler);
if (~index) handlers.splice(index, 1);
}
};
}
export function onDev ( eventName, handler ) {
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" );
return this.on( 'destroy', handler );
export function onDev(eventName, handler) {
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"
);
return this.on('destroy', handler);
}
return on.call( this, eventName, handler );
return on.call(this, eventName, handler);
}
export function set ( newState ) {
this._set( assign( {}, newState ) );
export function set(newState) {
this._set(assign({}, newState));
this._root._flush();
}
export function _flush () {
if ( !this._renderHooks ) return;
export function _flush() {
if (!this._renderHooks) return;
while ( this._renderHooks.length ) {
while (this._renderHooks.length) {
this._renderHooks.pop()();
}
}

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

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

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

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

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

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

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

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

@ -1,21 +1,23 @@
import { Node } from '../interfaces';
export default function flatten ( node: Node ) {
export default function flatten(node: Node) {
const parts = [];
const propEnd = node.end;
while ( node.type === 'MemberExpression' ) {
if ( node.computed ) return null;
parts.unshift( node.property.name );
while (node.type === 'MemberExpression') {
if (node.computed) return null;
parts.unshift(node.property.name);
node = node.object;
}
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}✂]` };
}

@ -1,31 +1,36 @@
import spaces from './spaces.js';
import spaces from './spaces';
function tabsToSpaces ( str: string ) {
return str.replace( /^\t+/, match => match.split( '\t' ).join( ' ' ) );
function tabsToSpaces(str: string) {
return str.replace(/^\t+/, match => match.split('\t').join(' '));
}
export default function getCodeFrame ( source: string, line: number, column: number ) {
const lines = source.split( '\n' );
export default function getCodeFrame(
source: string,
line: number,
column: number
) {
const lines = source.split('\n');
const frameStart = Math.max( 0, line - 2 );
const frameEnd = Math.min( line + 3, lines.length );
const frameStart = Math.max(0, line - 2);
const frameEnd = Math.min(line + 3, lines.length);
const digits = String( frameEnd + 1 ).length;
const digits = String(frameEnd + 1).length;
return lines
.slice( frameStart, frameEnd )
.map( ( str, i ) => {
.slice(frameStart, frameEnd)
.map((str, i) => {
const isErrorLine = frameStart + i === line;
let lineNum = String( i + frameStart + 1 );
while ( lineNum.length < digits ) lineNum = ` ${lineNum}`;
let lineNum = String(i + frameStart + 1);
while (lineNum.length < digits) lineNum = ` ${lineNum}`;
if ( isErrorLine ) {
const indicator = spaces( digits + 2 + tabsToSpaces( str.slice( 0, column ) ).length ) + '^';
return `${lineNum}: ${tabsToSpaces( str )}\n${indicator}`;
if (isErrorLine) {
const 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';
export default function isReference ( node: Node, parent: Node ): boolean {
if ( node.type === 'MemberExpression' ) {
return !node.computed && isReference( node.object, node );
export default function isReference(node: Node, parent: Node): boolean {
if (node.type === 'MemberExpression') {
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
// if it's the entire body of a function without a block statement
// i.e. an arrow function expression like `a => a`
if ( !parent ) return true;
if (!parent) return true;
// TODO is this right?
if ( parent.type === 'MemberExpression' || parent.type === 'MethodDefinition' ) {
if (
parent.type === 'MemberExpression' ||
parent.type === 'MethodDefinition'
) {
return parent.computed || node === parent.object;
}
// 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 () {...} }`
if ( parent.type === 'MethodDefinition' ) return false;
if (parent.type === 'MethodDefinition') return false;
// 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;
}

@ -1,5 +1,5 @@
const voidElementNames = /^(?:area|base|br|col|command|embed|hr|img|input|keygen|link|meta|param|source|track|wbr)$/;
export default function isVoidElementName ( name: string ) {
return voidElementNames.test( name ) || name.toLowerCase() === '!doctype';
export default function isVoidElementName(name: string) {
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 svg = 'http://www.w3.org/2000/svg';
export const xlink = 'http://www.w3.org/1999/xlink';
export const xml = 'http://www.w3.org/XML/1998/namespace';
export const xmlns = 'http://www.w3.org/2000/xmlns';
export const svg = 'http://www.w3.org/2000/svg';
export const xlink = 'http://www.w3.org/1999/xlink';
export const xml = 'http://www.w3.org/XML/1998/namespace';
export const xmlns = 'http://www.w3.org/2000/xmlns';
export const validNamespaces = [
'html', 'mathml', 'svg', 'xlink', 'xml', 'xmlns',
html, mathml, svg, xlink, xml, xmlns
'html',
'mathml',
'svg',
'xlink',
'xml',
'xmlns',
html,
mathml,
svg,
xlink,
xml,
xmlns,
];
export default { html, mathml, svg, xlink, xml, xmlns };

@ -2,56 +2,56 @@ import { Node } from '../interfaces';
const keys = {
ObjectExpression: 'properties',
Program: 'body'
Program: 'body',
};
const offsets = {
ObjectExpression: [ 1, -1 ],
Program: [ 0, 0 ]
ObjectExpression: [1, -1],
Program: [0, 0],
};
export function removeNode ( code, parent: Node, node: Node ) {
const key = keys[ parent.type ];
const offset = offsets[ parent.type ];
if ( !key || !offset ) throw new Error( `not implemented: ${parent.type}` );
export function removeNode(code, parent: Node, node: Node) {
const key = keys[parent.type];
const offset = offsets[parent.type];
if (!key || !offset) throw new Error(`not implemented: ${parent.type}`);
const list = parent[ key ];
const i = list.indexOf( node );
if ( i === -1 ) throw new Error( 'node not in list' );
const list = parent[key];
const i = list.indexOf(node);
if (i === -1) throw new Error('node not in list');
let a;
let b;
if ( list.length === 1 ) {
if (list.length === 1) {
// remove everything, leave {}
a = parent.start + offset[0];
b = parent.end + offset[1];
} else if ( i === 0 ) {
} else if (i === 0) {
// remove everything before second node, including comments
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;
while ( /[\s,]/.test( code.original[b] ) ) b += 1;
while (/[\s,]/.test(code.original[b])) b += 1;
} else {
// 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;
}
code.remove( a, b );
list.splice( i, 1 );
code.remove(a, b);
list.splice(i, 1);
return;
}
export function removeObjectKey ( code, node, key ) {
if ( node.type !== 'ObjectExpression' ) return;
export function removeObjectKey(code, node, key) {
if (node.type !== 'ObjectExpression') return;
let i = node.properties.length;
while ( i-- ) {
while (i--) {
const property = node.properties[i];
if ( property.key.type === 'Identifier' && property.key.name === key ) {
removeNode( code, node, property );
if (property.key.type === 'Identifier' && property.key.name === key) {
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
reservedNames.add( 'state' );
reservedNames.add('state');
export default reservedNames;
export default reservedNames;

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

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

@ -1,15 +1,15 @@
import { whitespace } from './patterns';
export function trimStart ( str: string ) {
export function trimStart(str: string) {
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;
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 meta = new Map([
[ ':Window', validateWindow ]
]);
const meta = new Map([[':Window', validateWindow]]);
export default function validateHtml ( validator: Validator, html: Node ) {
export default function validateHtml(validator: Validator, html: Node) {
let elementDepth = 0;
function visit ( node: Node ) {
if ( node.type === 'Element' ) {
if ( 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 );
function visit(node: Node) {
if (node.type === 'Element') {
if (
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 ) ) {
return meta.get( node.name )( validator, node );
if (meta.has(node.name)) {
return meta.get(node.name)(validator, node);
}
elementDepth += 1;
validateElement( validator, node );
} else if ( node.type === 'EachBlock' ) {
if ( validator.helpers.has( node.context ) ) {
validateElement(validator, node);
} else if (node.type === 'EachBlock') {
if (validator.helpers.has(node.context)) {
let c = node.expression.end;
// find start of context
while ( /\s/.test( validator.source[c] ) ) c += 1;
while (/\s/.test(validator.source[c])) c += 1;
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 ) {
node.children.forEach( visit );
if (node.children) {
node.children.forEach(visit);
}
if ( node.else ) {
visit( node.else );
if (node.else) {
visit(node.else);
}
if ( node.type === 'Element' ) {
if (node.type === 'Element') {
elementDepth -= 1;
}
}
html.children.forEach( visit );
html.children.forEach(visit);
}

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

@ -3,35 +3,40 @@ import list from '../utils/list';
import { Validator } from '../index';
import { Node } from '../../interfaces';
const validBuiltins = new Set([
'set',
'fire',
'destroy'
]);
const validBuiltins = new Set(['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;
if ( type !== 'CallExpression' ) {
validator.error( `Expected a call expression`, start );
if (type !== 'CallExpression') {
validator.error(`Expected a call expression`, start);
}
const { name } = flattenReference( callee );
const { name } = flattenReference(callee);
if ( name === 'this' || name === 'event' ) return;
if ( callee.type === 'Identifier' && validBuiltins.has( callee.name ) || validator.methods.has( callee.name ) ) return;
if (name === 'this' || name === 'event') return;
if (
(callee.type === 'Identifier' && validBuiltins.has(callee.name)) ||
validator.methods.has(callee.name)
)
return;
const validCallees = [ 'this.*', 'event.*' ]
.concat(
Array.from( validBuiltins ),
Array.from( validator.methods.keys() )
);
const validCallees = ['this.*', 'event.*'].concat(
Array.from(validBuiltins),
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?`;
}
validator.warn( message, start );
validator.warn(message, start);
}

@ -11,40 +11,46 @@ const validBindings = [
'outerWidth',
'outerHeight',
'scrollX',
'scrollY'
'scrollY',
];
export default function validateWindow ( validator: Validator, node: Node ) {
node.attributes.forEach( ( attribute: Node ) => {
if ( attribute.type === 'Binding' ) {
if ( attribute.value.type !== 'Identifier' ) {
const { parts } = flattenReference( attribute.value );
export default function validateWindow(validator: Validator, node: Node) {
node.attributes.forEach((attribute: Node) => {
if (attribute.type === 'Binding') {
if (attribute.value.type !== 'Identifier') {
const { parts } = flattenReference(attribute.value);
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
);
}
if ( !~validBindings.indexOf( attribute.name ) ) {
const match = (
attribute.name === 'width' ? 'innerWidth' :
attribute.name === 'height' ? 'innerHeight' :
fuzzymatch( attribute.name, validBindings )
);
if (!~validBindings.indexOf(attribute.name)) {
const match = attribute.name === 'width'
? 'innerWidth'
: attribute.name === 'height'
? 'innerHeight'
: fuzzymatch(attribute.name, validBindings);
const message = `'${attribute.name}' is not a valid binding on <:Window>`;
if ( match ) {
validator.error( `${message} (did you mean '${match}'?)`, attribute.start );
if (match) {
validator.error(
`${message} (did you mean '${match}'?)`,
attribute.start
);
} 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 { getLocator, Location } from 'locate-character';
import getCodeFrame from '../utils/getCodeFrame';
import CompileError from '../utils/CompileError'
import CompileError from '../utils/CompileError';
import { Node, Parsed, CompileOptions, Warning } from '../interfaces';
class ValidationError extends CompileError {
constructor ( message: string, template: string, index: number, filename: string ) {
super( message, template, index, filename );
constructor(
message: string,
template: string,
index: number,
filename: string
) {
super(message, template, index, filename);
this.name = 'ValidationError';
}
}
@ -27,7 +32,7 @@ export class Validator {
helpers: 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.filename = options !== undefined ? options.filename : undefined;
@ -43,15 +48,15 @@ export class Validator {
this.transitions = new Map();
}
error ( message: string, pos: number ) {
throw new ValidationError( message, this.source, pos, this.filename );
error(message: string, pos: number) {
throw new ValidationError(message, this.source, pos, this.filename);
}
warn ( message: string, pos: number ) {
if ( !this.locator ) this.locator = getLocator( this.source );
const { line, column } = this.locator( pos );
warn(message: string, pos: number) {
if (!this.locator) this.locator = getLocator(this.source);
const { line, column } = this.locator(pos);
const frame = getCodeFrame( this.source, line, column );
const frame = getCodeFrame(this.source, line, column);
this.onwarn({
message,
@ -59,45 +64,49 @@ export class Validator {
loc: { line: line + 1, column },
pos,
filename: this.filename,
toString: () => `${message} (${line + 1}:${column})\n${frame}`
toString: () => `${message} (${line + 1}:${column})\n${frame}`,
});
}
}
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;
try {
if ( name && !/^[a-zA-Z_$][a-zA-Z_$0-9]*$/.test( name ) ) {
const error = new Error( `options.name must be a valid identifier` );
if (name && !/^[a-zA-Z_$][a-zA-Z_$0-9]*$/.test(name)) {
const error = new Error(`options.name must be a valid identifier`);
throw error;
}
if ( name && !/^[A-Z]/.test( name ) ) {
if (name && !/^[A-Z]/.test(name)) {
const message = `options.name should be capitalised`;
onwarn({
message,
filename,
toString: () => message
toString: () => message,
});
}
const validator = new Validator( parsed, source, {
const validator = new Validator(parsed, source, {
onwarn,
name,
filename
filename,
});
if ( parsed.js ) {
validateJs( validator, parsed.js );
if (parsed.js) {
validateJs(validator, parsed.js);
}
if ( parsed.html ) {
validateHtml( validator, parsed.html );
if (parsed.html) {
validateHtml(validator, parsed.html);
}
} catch ( err ) {
if ( onerror ) {
onerror( err );
} catch (err) {
if (onerror) {
onerror(err);
} else {
throw err;
}

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

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

@ -3,35 +3,53 @@ import checkForComputedKeys from '../utils/checkForComputedKeys';
import { Validator } from '../../';
import { Node } from '../../../interfaces';
const isFunctionExpression = new Set( [ 'FunctionExpression', 'ArrowFunctionExpression' ] );
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 );
const isFunctionExpression = new Set([
'FunctionExpression',
'ArrowFunctionExpression',
]);
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;
}
checkForDupes( validator, prop.value.properties );
checkForComputedKeys( validator, prop.value.properties );
checkForDupes(validator, prop.value.properties);
checkForComputedKeys(validator, prop.value.properties);
prop.value.properties.forEach( ( computation: Node ) => {
if ( !isFunctionExpression.has( computation.value.type ) ) {
validator.error( `Computed properties can be function expressions or arrow function expressions`, computation.value.start );
prop.value.properties.forEach((computation: Node) => {
if (!isFunctionExpression.has(computation.value.type)) {
validator.error(
`Computed properties can be function expressions or arrow function expressions`,
computation.value.start
);
return;
}
const params = computation.value.params;
if ( params.length === 0 ) {
validator.error( `A computed value must depend on at least one property`, computation.value.start );
if (params.length === 0) {
validator.error(
`A computed value must depend on at least one property`,
computation.value.start
);
return;
}
params.forEach( ( param: Node ) => {
const valid = param.type === 'Identifier' || param.type === 'AssignmentPattern' && param.left.type === 'Identifier';
if ( !valid ) {
validator.error( `Computed properties cannot use destructuring in function parameters`, param.start );
params.forEach((param: Node) => {
const valid =
param.type === 'Identifier' ||
(param.type === 'AssignmentPattern' &&
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 { 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 ) {
while ( prop.type === 'ParenthesizedExpression' ) prop = prop.expression;
export default function data(validator: Validator, prop: Node) {
while (prop.type === 'ParenthesizedExpression') prop = prop.expression;
// TODO should we disallow references and expressions as well?
if ( disallowed.has( prop.value.type ) ) {
validator.error( `'data' must be a function`, prop.value.start );
if (disallowed.has(prop.value.type)) {
validator.error(`'data' must be a function`, prop.value.start);
}
}

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

@ -4,53 +4,67 @@ import { walk } from 'estree-walker';
import { Validator } from '../../';
import { Node } from '../../../interfaces';
export default function helpers ( validator: Validator, prop: Node ) {
if ( prop.value.type !== 'ObjectExpression' ) {
validator.error( `The 'helpers' property must be an object literal`, prop.start );
export default function helpers(validator: Validator, prop: Node) {
if (prop.value.type !== 'ObjectExpression') {
validator.error(
`The 'helpers' property must be an object literal`,
prop.start
);
return;
}
checkForDupes( validator, prop.value.properties );
checkForComputedKeys( validator, prop.value.properties );
checkForDupes(validator, prop.value.properties);
checkForComputedKeys(validator, prop.value.properties);
prop.value.properties.forEach( ( prop: Node ) => {
if ( !/FunctionExpression/.test( prop.value.type ) ) return;
prop.value.properties.forEach((prop: Node) => {
if (!/FunctionExpression/.test(prop.value.type)) return;
let lexicalDepth = 0;
let usesArguments = false;
walk( prop.value.body, {
enter ( node: Node ) {
if ( /^Function/.test( node.type ) ) {
walk(prop.value.body, {
enter(node: Node) {
if (/^Function/.test(node.type)) {
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
// 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 ) {
validator.error( `Cannot use this.get(...) — it must be passed into the helper function as an argument`, node.start );
if (
node.type === 'CallExpression' &&
node.callee.type === 'MemberExpression' &&
node.callee.object.type === 'ThisExpression' &&
node.callee.property.name === 'get' &&
!node.callee.property.computed
) {
validator.error(
`Cannot use this.get(...) — it must be passed into the helper function as an argument`,
node.start
);
}
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' ) {
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;
}
}
},
leave ( node: Node ) {
if ( /^Function/.test( node.type ) ) {
leave(node: Node) {
if (/^Function/.test(node.type)) {
lexicalDepth -= 1;
}
}
},
});
if ( prop.value.params.length === 0 && !usesArguments ) {
validator.warn( `Helpers should be pure functions, with at least one argument`, prop.start );
if (prop.value.params.length === 0 && !usesArguments) {
validator.warn(
`Helpers should be pure functions, with at least one argument`,
prop.start
);
}
});
}

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

Loading…
Cancel
Save