Merge branch 'master' into gh-592

pull/603/head
Rich Harris 8 years ago
commit 77fb38a123

@ -42,6 +42,8 @@
}, },
"homepage": "https://github.com/sveltejs/svelte#README", "homepage": "https://github.com/sveltejs/svelte#README",
"devDependencies": { "devDependencies": {
"@types/mocha": "^2.2.41",
"@types/node": "^7.0.22",
"acorn": "^4.0.4", "acorn": "^4.0.4",
"babel": "^6.23.0", "babel": "^6.23.0",
"babel-core": "^6.23.1", "babel-core": "^6.23.1",
@ -74,9 +76,11 @@
"rollup-plugin-commonjs": "^7.0.0", "rollup-plugin-commonjs": "^7.0.0",
"rollup-plugin-json": "^2.1.0", "rollup-plugin-json": "^2.1.0",
"rollup-plugin-node-resolve": "^2.0.0", "rollup-plugin-node-resolve": "^2.0.0",
"rollup-plugin-typescript": "^0.8.1",
"rollup-watch": "^3.2.2", "rollup-watch": "^3.2.2",
"source-map": "^0.5.6", "source-map": "^0.5.6",
"source-map-support": "^0.4.8" "source-map-support": "^0.4.8",
"typescript": "^2.3.2"
}, },
"nyc": { "nyc": {
"include": [ "include": [

@ -0,0 +1,9 @@
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 );
});

@ -1,24 +1,35 @@
import path from 'path';
import nodeResolve from 'rollup-plugin-node-resolve'; import nodeResolve from 'rollup-plugin-node-resolve';
import commonjs from 'rollup-plugin-commonjs'; import commonjs from 'rollup-plugin-commonjs';
import json from 'rollup-plugin-json'; import json from 'rollup-plugin-json';
import buble from 'rollup-plugin-buble'; import typescript from 'rollup-plugin-typescript';
const src = path.resolve( 'src' );
export default { export default {
entry: 'src/index.js', entry: 'src/index.ts',
moduleName: 'svelte', moduleName: 'svelte',
targets: [ targets: [
{ dest: 'compiler/svelte.js', format: 'umd' } { dest: 'compiler/svelte.js', format: 'umd' }
], ],
plugins: [ plugins: [
{
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` );
}
}
},
nodeResolve({ jsnext: true, module: true }), nodeResolve({ jsnext: true, module: true }),
commonjs(), commonjs(),
json(), json(),
buble({ typescript({
include: 'src/**', include: 'src/**',
exclude: 'src/shared/**', exclude: 'src/shared/**',
target: { typescript: require( 'typescript' )
node: 4
}
}) })
], ],
sourceMap: true sourceMap: true

@ -20,9 +20,9 @@ export default {
} }
}) })
], ],
external: [ path.resolve( 'src/index.js' ), 'fs', 'path' ], external: [ path.resolve( 'src/index.ts' ), 'fs', 'path' ],
paths: { paths: {
[ path.resolve( 'src/index.js' ) ]: '../compiler/svelte.js' [ path.resolve( 'src/index.ts' ) ]: '../compiler/svelte.js'
}, },
sourceMap: true sourceMap: true
}; };

@ -1,20 +1,45 @@
import MagicString, { Bundle } from 'magic-string'; import MagicString, { Bundle } from 'magic-string';
import { walk } from 'estree-walker'; import { walk } from 'estree-walker';
import isReference from '../utils/isReference.js'; import isReference from '../utils/isReference';
import flattenReference from '../utils/flattenReference.js'; import flattenReference from '../utils/flattenReference';
import globalWhitelist from '../utils/globalWhitelist.js'; import globalWhitelist from '../utils/globalWhitelist';
import reservedNames from '../utils/reservedNames.js'; import reservedNames from '../utils/reservedNames';
import namespaces from '../utils/namespaces.js'; import namespaces from '../utils/namespaces';
import { removeNode, removeObjectKey } from '../utils/removeNode.js'; import { removeNode, removeObjectKey } from '../utils/removeNode';
import getIntro from './shared/utils/getIntro.js'; import getIntro from './shared/utils/getIntro';
import getOutro from './shared/utils/getOutro.js'; import getOutro from './shared/utils/getOutro';
import processCss from './shared/processCss.js'; import processCss from './shared/processCss';
import annotateWithScopes from '../utils/annotateWithScopes.js'; import annotateWithScopes from '../utils/annotateWithScopes';
import DomBlock from './dom/Block';
import SsrBlock from './server-side-rendering/Block';
import { Node, Parsed, CompileOptions } from '../interfaces';
const test = typeof global !== 'undefined' && global.__svelte_test; const test = typeof global !== 'undefined' && global.__svelte_test;
export default class Generator { export default class Generator {
constructor ( parsed, source, name, options ) { parsed: Parsed;
source: string;
name: string;
options: CompileOptions;
imports: Node[];
helpers: Set<string>;
components: Set<string>;
events: Set<string>;
transitions: Set<string>;
importedComponents: Map<string, string>;
bindingGroups: string[];
expectedProperties: Set<string>;
css: string;
cssId: string;
usesRefs: boolean;
importedNames: Set<string>;
aliases: Map<string, string>;
usedNames: Set<string>;
constructor ( parsed: Parsed, source: string, name: string, options: CompileOptions ) {
this.parsed = parsed; this.parsed = parsed;
this.source = source; this.source = source;
this.name = name; this.name = name;
@ -39,22 +64,22 @@ export default class Generator {
this.usesRefs = false; this.usesRefs = false;
// allow compiler to deconflict user's `import { get } from 'whatever'` and // allow compiler to deconflict user's `import { get } from 'whatever'` and
// Svelte's builtin `import { get, ... } from 'svelte/shared.js'`; // Svelte's builtin `import { get, ... } from 'svelte/shared.ts'`;
this.importedNames = new Set(); this.importedNames = new Set();
this.aliases = new Map(); this.aliases = new Map();
this._usedNames = new Set( [ name ] ); this.usedNames = new Set( [ name ] );
} }
addSourcemapLocations ( node ) { addSourcemapLocations ( node: Node ) {
walk( node, { walk( node, {
enter: node => { enter: ( node: Node ) => {
this.code.addSourcemapLocation( node.start ); this.code.addSourcemapLocation( node.start );
this.code.addSourcemapLocation( node.end ); this.code.addSourcemapLocation( node.end );
} }
}); });
} }
alias ( name ) { alias ( name: string ) {
if ( !this.aliases.has( name ) ) { if ( !this.aliases.has( name ) ) {
this.aliases.set( name, this.getUniqueName( name ) ); this.aliases.set( name, this.getUniqueName( name ) );
} }
@ -62,10 +87,10 @@ export default class Generator {
return this.aliases.get( name ); return this.aliases.get( name );
} }
contextualise ( block, expression, context, isEventHandler ) { contextualise ( block: DomBlock | SsrBlock, expression: Node, context: string, isEventHandler: boolean ) {
this.addSourcemapLocations( expression ); this.addSourcemapLocations( expression );
const usedContexts = []; const usedContexts: string[] = [];
const { code, helpers } = this; const { code, helpers } = this;
const { contexts, indexes } = block; const { contexts, indexes } = block;
@ -76,7 +101,7 @@ export default class Generator {
const self = this; const self = this;
walk( expression, { walk( expression, {
enter ( node, parent, key ) { enter ( node: Node, parent: Node, key: string ) {
if ( /^Function/.test( node.type ) ) lexicalDepth += 1; if ( /^Function/.test( node.type ) ) lexicalDepth += 1;
if ( node._scope ) { if ( node._scope ) {
@ -138,7 +163,7 @@ export default class Generator {
} }
}, },
leave ( node ) { leave ( node: Node ) {
if ( /^Function/.test( node.type ) ) lexicalDepth -= 1; if ( /^Function/.test( node.type ) ) lexicalDepth -= 1;
if ( node._scope ) scope = scope.parent; if ( node._scope ) scope = scope.parent;
} }
@ -151,16 +176,16 @@ export default class Generator {
}; };
} }
findDependencies ( contextDependencies, indexes, expression ) { findDependencies ( contextDependencies: Map<string, string[]>, indexes: Map<string, string>, expression: Node ) {
if ( expression._dependencies ) return expression._dependencies; if ( expression._dependencies ) return expression._dependencies;
let scope = annotateWithScopes( expression ); let scope = annotateWithScopes( expression );
const dependencies = []; const dependencies: string[] = [];
const generator = this; // can't use arrow functions, because of this.skip() const generator = this; // can't use arrow functions, because of this.skip()
walk( expression, { walk( expression, {
enter ( node, parent ) { enter ( node: Node, parent: Node ) {
if ( node._scope ) { if ( node._scope ) {
scope = node._scope; scope = node._scope;
return; return;
@ -180,7 +205,7 @@ export default class Generator {
} }
}, },
leave ( node ) { leave ( node: Node ) {
if ( node._scope ) scope = scope.parent; if ( node._scope ) scope = scope.parent;
} }
}); });
@ -196,7 +221,7 @@ export default class Generator {
generate ( result, options, { name, format } ) { generate ( result, options, { name, format } ) {
if ( this.imports.length ) { if ( this.imports.length ) {
const statements = []; const statements: string[] = [];
this.imports.forEach( ( declaration, i ) => { this.imports.forEach( ( declaration, i ) => {
if ( format === 'es' ) { if ( format === 'es' ) {
@ -204,14 +229,14 @@ export default class Generator {
return; return;
} }
const defaultImport = declaration.specifiers.find( x => x.type === 'ImportDefaultSpecifier' || x.type === 'ImportSpecifier' && x.imported.name === 'default' ); const defaultImport = declaration.specifiers.find( ( x: Node ) => x.type === 'ImportDefaultSpecifier' || x.type === 'ImportSpecifier' && x.imported.name === 'default' );
const namespaceImport = declaration.specifiers.find( x => x.type === 'ImportNamespaceSpecifier' ); const namespaceImport = declaration.specifiers.find( ( x: Node ) => x.type === 'ImportNamespaceSpecifier' );
const namedImports = declaration.specifiers.filter( x => x.type === 'ImportSpecifier' && x.imported.name !== 'default' ); 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 name = ( defaultImport || namespaceImport ) ? ( defaultImport || namespaceImport ).local.name : `__import${i}`;
declaration.name = name; // hacky but makes life a bit easier later declaration.name = name; // hacky but makes life a bit easier later
namedImports.forEach( specifier => { namedImports.forEach( ( specifier: Node ) => {
statements.push( `var ${specifier.local.name} = ${name}.${specifier.imported.name}` ); statements.push( `var ${specifier.local.name} = ${name}.${specifier.imported.name}` );
}); });
@ -230,7 +255,7 @@ export default class Generator {
const compiled = new Bundle({ separator: '' }); const compiled = new Bundle({ separator: '' });
function addString ( str ) { function addString ( str: string ) {
compiled.addSource({ compiled.addSource({
content: new MagicString( str ) content: new MagicString( str )
}); });
@ -250,7 +275,7 @@ export default class Generator {
}); });
} }
parts.forEach( str => { parts.forEach( ( str: string ) => {
const chunk = str.replace( pattern, '' ); const chunk = str.replace( pattern, '' );
if ( chunk ) addString( chunk ); if ( chunk ) addString( chunk );
@ -274,11 +299,11 @@ export default class Generator {
}; };
} }
getUniqueName ( name ) { getUniqueName ( name: string ) {
if ( test ) name = `${name}$`; if ( test ) name = `${name}$`;
let alias = name; let alias = name;
for ( let i = 1; reservedNames.has( alias ) || this.importedNames.has( alias ) || this._usedNames.has( alias ); alias = `${name}_${i++}` ); for ( let i = 1; reservedNames.has( alias ) || this.importedNames.has( alias ) || this.usedNames.has( alias ); alias = `${name}_${i++}` );
this._usedNames.add( alias ); this.usedNames.add( alias );
return alias; return alias;
} }
@ -287,13 +312,13 @@ export default class Generator {
return name => { return name => {
if ( test ) name = `${name}$`; if ( test ) name = `${name}$`;
let alias = name; let alias = name;
for ( let i = 1; reservedNames.has( alias ) || this.importedNames.has( alias ) || this._usedNames.has( alias ) || localUsedNames.has( alias ); alias = `${name}_${i++}` ); for ( let i = 1; reservedNames.has( alias ) || this.importedNames.has( alias ) || this.usedNames.has( alias ) || localUsedNames.has( alias ); alias = `${name}_${i++}` );
localUsedNames.add( alias ); localUsedNames.add( alias );
return alias; return alias;
}; };
} }
parseJs ( ssr ) { parseJs ( ssr: boolean = false ) {
const { source } = this; const { source } = this;
const { js } = this.parsed; const { js } = this.parsed;
@ -315,23 +340,23 @@ export default class Generator {
removeNode( this.code, js.content, node ); removeNode( this.code, js.content, node );
imports.push( node ); imports.push( node );
node.specifiers.forEach( specifier => { node.specifiers.forEach( ( specifier: Node ) => {
this.importedNames.add( specifier.local.name ); this.importedNames.add( specifier.local.name );
}); });
} }
} }
const defaultExport = body.find( node => node.type === 'ExportDefaultDeclaration' ); const defaultExport = body.find( ( node: Node ) => node.type === 'ExportDefaultDeclaration' );
if ( defaultExport ) { if ( defaultExport ) {
defaultExport.declaration.properties.forEach( prop => { defaultExport.declaration.properties.forEach( ( prop: Node ) => {
templateProperties[ prop.key.name ] = prop; templateProperties[ prop.key.name ] = prop;
}); });
} }
[ 'helpers', 'events', 'components', 'transitions' ].forEach( key => { [ 'helpers', 'events', 'components', 'transitions' ].forEach( key => {
if ( templateProperties[ key ] ) { if ( templateProperties[ key ] ) {
templateProperties[ key ].value.properties.forEach( prop => { templateProperties[ key ].value.properties.forEach( ( prop: node ) => {
this[ key ].add( prop.key.name ); this[ key ].add( prop.key.name );
}); });
} }
@ -340,11 +365,11 @@ export default class Generator {
if ( templateProperties.computed ) { if ( templateProperties.computed ) {
const dependencies = new Map(); const dependencies = new Map();
templateProperties.computed.value.properties.forEach( prop => { templateProperties.computed.value.properties.forEach( ( prop: Node ) => {
const key = prop.key.name; const key = prop.key.name;
const value = prop.value; const value = prop.value;
const deps = value.params.map( param => param.type === 'AssignmentPattern' ? param.left.name : param.name ); const deps = value.params.map( ( param: Node ) => param.type === 'AssignmentPattern' ? param.left.name : param.name );
dependencies.set( key, deps ); dependencies.set( key, deps );
}); });
@ -362,7 +387,7 @@ export default class Generator {
computations.push({ key, deps }); computations.push({ key, deps });
} }
templateProperties.computed.value.properties.forEach( prop => visit( prop.key.name ) ); templateProperties.computed.value.properties.forEach( ( prop: Node ) => visit( prop.key.name ) );
} }
if ( templateProperties.namespace ) { if ( templateProperties.namespace ) {
@ -374,7 +399,7 @@ export default class Generator {
if ( templateProperties.components ) { if ( templateProperties.components ) {
let hasNonImportedComponent = false; let hasNonImportedComponent = false;
templateProperties.components.value.properties.forEach( property => { templateProperties.components.value.properties.forEach( ( property: Node ) => {
const key = property.key.name; const key = property.key.name;
const value = source.slice( property.value.start, property.value.end ); const value = source.slice( property.value.start, property.value.end );
if ( this.importedNames.has( value ) ) { if ( this.importedNames.has( value ) ) {

@ -1,8 +1,70 @@
import CodeBuilder from '../../utils/CodeBuilder.js'; import CodeBuilder from '../../utils/CodeBuilder';
import deindent from '../../utils/deindent.js'; import deindent from '../../utils/deindent.js';
import { DomGenerator } from './index';
import { Node } from '../../interfaces';
export interface BlockOptions {
name: string;
generator?: DomGenerator;
expression?: Node;
context?: string;
key?: string;
contexts?: Map<string, string>;
indexes?: Map<string, string>;
contextDependencies?: Map<string, string[]>;
params?: string[];
indexNames?: Map<string, string>;
listNames?: Map<string, string>;
indexName?: string;
listName?: string;
dependencies?: Set<string>;
}
export default class Block { export default class Block {
constructor ( options ) { generator: DomGenerator;
name: string;
expression: Node;
context: string;
key: string;
first: string;
contexts: Map<string, string>;
indexes: Map<string, string>;
contextDependencies: Map<string, string[]>;
dependencies: Set<string>;
params: string[];
indexNames: Map<string, string>;
listNames: Map<string, string>;
indexName: string;
listName: string;
builders: {
create: CodeBuilder;
mount: CodeBuilder;
update: CodeBuilder;
intro: CodeBuilder;
outro: CodeBuilder;
detach: CodeBuilder;
detachRaw: CodeBuilder;
destroy: CodeBuilder;
}
hasIntroMethod: boolean;
hasOutroMethod: boolean;
outros: number;
aliases: Map<string, string>;
variables: Map<string, string>;
getUniqueName: (name: string) => string;
component: string;
target: string;
hasUpdateMethod: boolean;
autofocus: string;
constructor ( options: BlockOptions ) {
this.generator = options.generator; this.generator = options.generator;
this.name = options.name; this.name = options.name;
this.expression = options.expression; this.expression = options.expression;
@ -55,7 +117,7 @@ export default class Block {
}); });
} }
addElement ( name, renderStatement, parentNode, needsIdentifier = false ) { addElement ( name: string, renderStatement: string, parentNode: string, needsIdentifier = false ) {
const isToplevel = !parentNode; const isToplevel = !parentNode;
if ( needsIdentifier || isToplevel ) { if ( needsIdentifier || isToplevel ) {
this.builders.create.addLine( this.builders.create.addLine(
@ -72,7 +134,7 @@ export default class Block {
} }
} }
addVariable ( name, init ) { addVariable ( name: string, init?: string ) {
if ( this.variables.has( name ) && this.variables.get( name ) !== init ) { if ( this.variables.has( name ) && this.variables.get( name ) !== init ) {
throw new Error( `Variable '${name}' already initialised with a different value` ); throw new Error( `Variable '${name}' already initialised with a different value` );
} }
@ -80,7 +142,7 @@ export default class Block {
this.variables.set( name, init ); this.variables.set( name, init );
} }
alias ( name ) { alias ( name: string ) {
if ( !this.aliases.has( name ) ) { if ( !this.aliases.has( name ) ) {
this.aliases.set( name, this.getUniqueName( name ) ); this.aliases.set( name, this.getUniqueName( name ) );
} }
@ -88,11 +150,11 @@ export default class Block {
return this.aliases.get( name ); return this.aliases.get( name );
} }
child ( options ) { child ( options: BlockOptions ) {
return new Block( Object.assign( {}, this, options, { parent: this } ) ); return new Block( Object.assign( {}, this, options, { parent: this } ) );
} }
contextualise ( expression, context, isEventHandler ) { contextualise ( expression: Node, context?: string, isEventHandler?: boolean ) {
return this.generator.contextualise( this, expression, context, isEventHandler ); return this.generator.contextualise( this, expression, context, isEventHandler );
} }
@ -100,7 +162,7 @@ export default class Block {
return this.generator.findDependencies( this.contextDependencies, this.indexes, expression ); return this.generator.findDependencies( this.contextDependencies, this.indexes, expression );
} }
mount ( name, parentNode ) { mount ( name: string, parentNode: string ) {
if ( parentNode ) { if ( parentNode ) {
this.builders.create.addLine( `${this.generator.helper( 'appendNode' )}( ${name}, ${parentNode} );` ); this.builders.create.addLine( `${this.generator.helper( 'appendNode' )}( ${name}, ${parentNode} );` );
} else { } else {

@ -1,17 +1,28 @@
import MagicString from 'magic-string'; import MagicString from 'magic-string';
import { parseExpressionAt } from 'acorn'; import { parseExpressionAt } from 'acorn';
import annotateWithScopes from '../../utils/annotateWithScopes.js'; import annotateWithScopes from '../../utils/annotateWithScopes';
import isReference from '../../utils/isReference.js'; import isReference from '../../utils/isReference';
import { walk } from 'estree-walker'; import { walk } from 'estree-walker';
import deindent from '../../utils/deindent.js'; import deindent from '../../utils/deindent.js';
import CodeBuilder from '../../utils/CodeBuilder.js'; import CodeBuilder from '../../utils/CodeBuilder';
import visit from './visit.js'; import visit from './visit';
import shared from './shared.js'; import shared from './shared';
import Generator from '../Generator.js'; import Generator from '../Generator';
import preprocess from './preprocess.js'; import preprocess from './preprocess';
import Block from './Block';
class DomGenerator extends Generator { import { Parsed, CompileOptions, Node } from '../../interfaces';
constructor ( parsed, source, name, options ) {
export class DomGenerator extends Generator {
blocks: Block[]
uses: Set<string>;
readonly: Set<string>;
metaBindings: string[];
hasIntroTransitions: boolean;
hasOutroTransitions: boolean;
hasComplexBindings: boolean;
constructor ( parsed: Parsed, source: string, name: string, options: CompileOptions ) {
super( parsed, source, name, options ); super( parsed, source, name, options );
this.blocks = []; this.blocks = [];
this.uses = new Set(); this.uses = new Set();
@ -22,7 +33,7 @@ class DomGenerator extends Generator {
this.metaBindings = []; this.metaBindings = [];
} }
helper ( name ) { helper ( name: string ) {
if ( this.options.dev && `${name}Dev` in shared ) { if ( this.options.dev && `${name}Dev` in shared ) {
name = `${name}Dev`; name = `${name}Dev`;
} }
@ -33,7 +44,7 @@ class DomGenerator extends Generator {
} }
} }
export default function dom ( parsed, source, options ) { export default function dom ( parsed: Parsed, source: string, options: CompileOptions ) {
const format = options.format || 'es'; const format = options.format || 'es';
const name = options.name || 'SvelteComponent'; const name = options.name || 'SvelteComponent';
@ -41,15 +52,9 @@ export default function dom ( parsed, source, options ) {
const { computations, hasJs, templateProperties, namespace } = generator.parseJs(); const { computations, hasJs, templateProperties, namespace } = generator.parseJs();
const state = { const { block, state } = preprocess( generator, namespace, parsed.html );
namespace,
parentNode: null,
isTopLevel: true
};
const block = preprocess( generator, state, parsed.html );
parsed.html.children.forEach( node => { parsed.html.children.forEach( ( node: Node ) => {
visit( generator, block, state, node ); visit( generator, block, state, node );
}); });

@ -0,0 +1,11 @@
export interface State {
name: string;
namespace: string;
parentNode: string;
isTopLevel: boolean
parentNodeName?: string;
basename?: string;
inEachBlock?: boolean;
allUsedContexts?: string[];
usesComponent?: boolean;
}

@ -1,12 +1,15 @@
import Block from './Block.js'; import Block from './Block';
import { trimStart, trimEnd } from '../../utils/trim.js'; import { trimStart, trimEnd } from '../../utils/trim';
import { assign } from '../../shared/index.js'; import { assign } from '../../shared/index.js';
import { DomGenerator } from './index';
import { Node } from '../../interfaces';
import { State } from './interfaces';
function isElseIf ( node ) { function isElseIf ( node: Node ) {
return node && node.children.length === 1 && node.children[0].type === 'IfBlock'; return node && node.children.length === 1 && node.children[0].type === 'IfBlock';
} }
function getChildState ( parent, child ) { function getChildState ( parent: State, child = {} ) {
return assign( {}, parent, { name: null, parentNode: null }, child || {} ); return assign( {}, parent, { name: null, parentNode: null }, child || {} );
} }
@ -25,7 +28,7 @@ const elementsWithoutText = new Set([
]); ]);
const preprocessors = { const preprocessors = {
MustacheTag: ( generator, block, state, node ) => { MustacheTag: ( generator: DomGenerator, block: Block, state: State, node: Node ) => {
const dependencies = block.findDependencies( node.expression ); const dependencies = block.findDependencies( node.expression );
block.addDependencies( dependencies ); block.addDependencies( dependencies );
@ -34,7 +37,7 @@ const preprocessors = {
}); });
}, },
RawMustacheTag: ( generator, block, state, node ) => { RawMustacheTag: ( generator: DomGenerator, block: Block, state: State, node: Node ) => {
const dependencies = block.findDependencies( node.expression ); const dependencies = block.findDependencies( node.expression );
block.addDependencies( dependencies ); block.addDependencies( dependencies );
@ -44,7 +47,7 @@ const preprocessors = {
node._state = getChildState( state, { basename, name }); node._state = getChildState( state, { basename, name });
}, },
Text: ( generator, block, state, node ) => { Text: ( generator: DomGenerator, block: Block, state: State, node: Node ) => {
node._state = getChildState( state ); node._state = getChildState( state );
if ( !/\S/.test( node.data ) ) { if ( !/\S/.test( node.data ) ) {
@ -56,13 +59,13 @@ const preprocessors = {
node._state.name = block.getUniqueName( `text` ); node._state.name = block.getUniqueName( `text` );
}, },
IfBlock: ( generator, block, state, node ) => { IfBlock: ( generator: DomGenerator, block: Block, state: State, node: Node ) => {
const blocks = []; const blocks: Block[] = [];
let dynamic = false; let dynamic = false;
let hasIntros = false; let hasIntros = false;
let hasOutros = false; let hasOutros = false;
function attachBlocks ( node ) { function attachBlocks ( node: Node ) {
const dependencies = block.findDependencies( node.expression ); const dependencies = block.findDependencies( node.expression );
block.addDependencies( dependencies ); block.addDependencies( dependencies );
@ -113,7 +116,7 @@ const preprocessors = {
generator.blocks.push( ...blocks ); generator.blocks.push( ...blocks );
}, },
EachBlock: ( generator, block, state, node ) => { EachBlock: ( generator: DomGenerator, block: Block, state: State, node: Node ) => {
const dependencies = block.findDependencies( node.expression ); const dependencies = block.findDependencies( node.expression );
block.addDependencies( dependencies ); block.addDependencies( dependencies );
@ -175,7 +178,7 @@ const preprocessors = {
} }
}, },
Element: ( generator, block, state, node ) => { Element: ( generator: DomGenerator, block: Block, state: State, node: Node ) => {
const isComponent = generator.components.has( node.name ) || node.name === ':Self'; const isComponent = generator.components.has( node.name ) || node.name === ':Self';
if ( isComponent ) { if ( isComponent ) {
@ -193,9 +196,9 @@ const preprocessors = {
}); });
} }
node.attributes.forEach( attribute => { node.attributes.forEach( ( attribute: Node ) => {
if ( attribute.type === 'Attribute' && attribute.value !== true ) { if ( attribute.type === 'Attribute' && attribute.value !== true ) {
attribute.value.forEach( chunk => { attribute.value.forEach( ( chunk: Node ) => {
if ( chunk.type !== 'Text' ) { if ( chunk.type !== 'Text' ) {
const dependencies = block.findDependencies( chunk.expression ); const dependencies = block.findDependencies( chunk.expression );
block.addDependencies( dependencies ); block.addDependencies( dependencies );
@ -238,12 +241,12 @@ const preprocessors = {
} }
}; };
function preprocessChildren ( generator, block, state, node, isTopLevel ) { function preprocessChildren ( generator: DomGenerator, block: Block, state: State, node: Node, isTopLevel: boolean = false ) {
// glue text nodes together // glue text nodes together
const cleaned = []; const cleaned: Node[] = [];
let lastChild; let lastChild: Node;
node.children.forEach( child => { node.children.forEach( ( child: Node ) => {
if ( child.type === 'Comment' ) return; if ( child.type === 'Comment' ) return;
if ( child.type === 'Text' && lastChild && lastChild.type === 'Text' ) { if ( child.type === 'Text' && lastChild && lastChild.type === 'Text' ) {
@ -273,7 +276,7 @@ function preprocessChildren ( generator, block, state, node, isTopLevel ) {
lastChild = null; lastChild = null;
cleaned.forEach( child => { cleaned.forEach( ( child: Node ) => {
const preprocess = preprocessors[ child.type ]; const preprocess = preprocessors[ child.type ];
if ( preprocess ) preprocess( generator, block, state, child ); if ( preprocess ) preprocess( generator, block, state, child );
@ -292,7 +295,7 @@ function preprocessChildren ( generator, block, state, node, isTopLevel ) {
node.children = cleaned; node.children = cleaned;
} }
export default function preprocess ( generator, state, node ) { export default function preprocess ( generator: DomGenerator, namespace: string, node: Node ) {
const block = new Block({ const block = new Block({
generator, generator,
name: generator.alias( 'create_main_fragment' ), name: generator.alias( 'create_main_fragment' ),
@ -309,9 +312,15 @@ export default function preprocess ( generator, state, node ) {
dependencies: new Set() dependencies: new Set()
}); });
const state: State = {
namespace,
parentNode: null,
isTopLevel: true
};
generator.blocks.push( block ); generator.blocks.push( block );
preprocessChildren( generator, block, state, node, true ); preprocessChildren( generator, block, state, node, true );
block.hasUpdateMethod = block.dependencies.size > 0; block.hasUpdateMethod = block.dependencies.size > 0;
return block; return { block, state };
} }

@ -0,0 +1,35 @@
// this file is auto-generated, do not edit it
export default {
"appendNode": "function appendNode ( node, target ) {\n\ttarget.appendChild( node );\n}",
"insertNode": "function insertNode ( node, target, anchor ) {\n\ttarget.insertBefore( node, anchor );\n}",
"detachNode": "function detachNode ( node ) {\n\tnode.parentNode.removeChild( node );\n}",
"detachBetween": "function detachBetween ( before, after ) {\n\twhile ( before.nextSibling && before.nextSibling !== after ) {\n\t\tbefore.parentNode.removeChild( before.nextSibling );\n\t}\n}",
"destroyEach": "function destroyEach ( iterations, detach, start ) {\n\tfor ( var i = start; i < iterations.length; i += 1 ) {\n\t\tif ( iterations[i] ) iterations[i].destroy( detach );\n\t}\n}",
"createElement": "function createElement ( name ) {\n\treturn document.createElement( name );\n}",
"createSvgElement": "function createSvgElement ( name ) {\n\treturn document.createElementNS( 'http://www.w3.org/2000/svg', name );\n}",
"createText": "function createText ( data ) {\n\treturn document.createTextNode( data );\n}",
"createComment": "function createComment () {\n\treturn document.createComment( '' );\n}",
"addEventListener": "function addEventListener ( node, event, handler ) {\n\tnode.addEventListener( event, handler, false );\n}",
"removeEventListener": "function removeEventListener ( node, event, handler ) {\n\tnode.removeEventListener( event, handler, false );\n}",
"setAttribute": "function setAttribute ( node, attribute, value ) {\n\tnode.setAttribute( attribute, value );\n}",
"setXlinkAttribute": "function setXlinkAttribute ( node, attribute, value ) {\n\tnode.setAttributeNS( 'http://www.w3.org/1999/xlink', attribute, value );\n}",
"getBindingGroupValue": "function getBindingGroupValue ( group ) {\n\tvar value = [];\n\tfor ( var i = 0; i < group.length; i += 1 ) {\n\t\tif ( group[i].checked ) value.push( group[i].__value );\n\t}\n\treturn value;\n}",
"differs": "function differs ( a, b ) {\n\treturn ( a !== b ) || ( a && ( typeof a === 'object' ) || ( typeof a === 'function' ) );\n}",
"dispatchObservers": "function dispatchObservers ( component, group, newState, oldState ) {\n\tfor ( var key in group ) {\n\t\tif ( !( key in newState ) ) continue;\n\n\t\tvar newValue = newState[ key ];\n\t\tvar oldValue = oldState[ key ];\n\n\t\tif ( differs( newValue, oldValue ) ) {\n\t\t\tvar callbacks = group[ key ];\n\t\t\tif ( !callbacks ) continue;\n\n\t\t\tfor ( var i = 0; i < callbacks.length; i += 1 ) {\n\t\t\t\tvar callback = callbacks[i];\n\t\t\t\tif ( callback.__calling ) continue;\n\n\t\t\t\tcallback.__calling = true;\n\t\t\t\tcallback.call( component, newValue, oldValue );\n\t\t\t\tcallback.__calling = false;\n\t\t\t}\n\t\t}\n\t}\n}",
"get": "function get ( key ) {\n\treturn key ? this._state[ key ] : this._state;\n}",
"fire": "function fire ( eventName, data ) {\n\tvar handlers = eventName in this._handlers && this._handlers[ eventName ].slice();\n\tif ( !handlers ) return;\n\n\tfor ( var i = 0; i < handlers.length; i += 1 ) {\n\t\thandlers[i].call( this, data );\n\t}\n}",
"observe": "function observe ( key, callback, options ) {\n\tvar group = ( options && options.defer ) ? this._observers.post : this._observers.pre;\n\n\t( group[ key ] || ( group[ key ] = [] ) ).push( callback );\n\n\tif ( !options || options.init !== false ) {\n\t\tcallback.__calling = true;\n\t\tcallback.call( this, this._state[ key ] );\n\t\tcallback.__calling = false;\n\t}\n\n\treturn {\n\t\tcancel: function () {\n\t\t\tvar index = group[ key ].indexOf( callback );\n\t\t\tif ( ~index ) group[ key ].splice( index, 1 );\n\t\t}\n\t};\n}",
"observeDev": "function observeDev ( key, callback, options ) {\n\tvar c = ( key = '' + key ).search( /[^\\w]/ );\n\tif ( c > -1 ) {\n\t\tvar message = \"The first argument to component.observe(...) must be the name of a top-level property\";\n\t\tif ( c > 0 ) message += \", i.e. '\" + key.slice( 0, c ) + \"' rather than '\" + key + \"'\";\n\n\t\tthrow new Error( message );\n\t}\n\n\treturn observe.call( this, key, callback, options );\n}",
"on": "function on ( eventName, handler ) {\n\tif ( eventName === 'teardown' ) return this.on( 'destroy', handler );\n\n\tvar handlers = this._handlers[ eventName ] || ( this._handlers[ eventName ] = [] );\n\thandlers.push( handler );\n\n\treturn {\n\t\tcancel: function () {\n\t\t\tvar index = handlers.indexOf( handler );\n\t\t\tif ( ~index ) handlers.splice( index, 1 );\n\t\t}\n\t};\n}",
"onDev": "function onDev ( eventName, handler ) {\n\tif ( eventName === 'teardown' ) {\n\t\tconsole.warn( \"Use component.on('destroy', ...) instead of component.on('teardown', ...) which has been deprecated and will be unsupported in Svelte 2\" );\n\t\treturn this.on( 'destroy', handler );\n\t}\n\n\treturn on.call( this, eventName, handler );\n}",
"set": "function set ( newState ) {\n\tthis._set( assign( {}, newState ) );\n\tthis._root._flush();\n}",
"_flush": "function _flush () {\n\tif ( !this._renderHooks ) return;\n\n\twhile ( this._renderHooks.length ) {\n\t\tthis._renderHooks.pop()();\n\t}\n}",
"proto": "{\n\tget: get,\n\tfire: fire,\n\tobserve: observe,\n\ton: on,\n\tset: set,\n\t_flush: _flush\n}",
"protoDev": "{\n\tget: get,\n\tfire: fire,\n\tobserve: observeDev,\n\ton: onDev,\n\tset: set,\n\t_flush: _flush\n}",
"linear": "function linear ( t ) {\n\treturn t;\n}",
"generateKeyframes": "function generateKeyframes ( a, b, delta, duration, ease, fn, node, style ) {\n\tvar id = '__svelte' + ~~( Math.random() * 1e9 ); // TODO make this more robust\n\tvar keyframes = '@keyframes ' + id + '{\\n';\n\n\tfor ( var p = 0; p <= 1; p += 16.666 / duration ) {\n\t\tvar t = a + delta * ease( p );\n\t\tkeyframes += ( p * 100 ) + '%{' + fn( t ) + '}\\n';\n\t}\n\n\tkeyframes += '100% {' + fn( b ) + '}\\n}';\n\tstyle.textContent += keyframes;\n\n\tdocument.head.appendChild( style );\n\n\tnode.style.animation = ( node.style.animation || '' ).split( ',' )\n\t\t.filter( function ( anim ) {\n\t\t\t// when introing, discard old animations if there are any\n\t\t\treturn anim && ( delta < 0 || !/__svelte/.test( anim ) );\n\t\t})\n\t\t.concat( id + ' ' + duration + 'ms linear 1 forwards' )\n\t\t.join( ', ' );\n}",
"wrapTransition": "function wrapTransition ( node, fn, params, intro, outgroup ) {\n\tvar obj = fn( node, params );\n\tvar duration = obj.duration || 300;\n\tvar ease = obj.easing || linear;\n\n\t// TODO share <style> tag between all transitions?\n\tif ( obj.css ) {\n\t\tvar style = document.createElement( 'style' );\n\t}\n\n\tif ( intro && obj.tick ) obj.tick( 0 );\n\n\treturn {\n\t\tt: intro ? 0 : 1,\n\t\trunning: false,\n\t\tprogram: null,\n\t\tpending: null,\n\t\trun: function ( intro, callback ) {\n\t\t\tvar program = {\n\t\t\t\tstart: window.performance.now() + ( obj.delay || 0 ),\n\t\t\t\tintro: intro,\n\t\t\t\tcallback: callback\n\t\t\t};\n\n\t\t\tif ( obj.delay ) {\n\t\t\t\tthis.pending = program;\n\t\t\t} else {\n\t\t\t\tthis.start( program );\n\t\t\t}\n\n\t\t\tif ( !this.running ) {\n\t\t\t\tthis.running = true;\n\t\t\t\ttransitionManager.add( this );\n\t\t\t}\n\t\t},\n\t\tstart: function ( program ) {\n\t\t\tprogram.a = this.t;\n\t\t\tprogram.b = program.intro ? 1 : 0;\n\t\t\tprogram.delta = program.b - program.a;\n\t\t\tprogram.duration = duration * Math.abs( program.b - program.a );\n\t\t\tprogram.end = program.start + program.duration;\n\n\t\t\tif ( obj.css ) {\n\t\t\t\tgenerateKeyframes( program.a, program.b, program.delta, program.duration, ease, obj.css, node, style );\n\t\t\t}\n\n\t\t\tthis.program = program;\n\t\t\tthis.pending = null;\n\t\t},\n\t\tupdate: function ( now ) {\n\t\t\tvar program = this.program;\n\t\t\tif ( !program ) return;\n\n\t\t\tvar p = now - program.start;\n\t\t\tthis.t = program.a + program.delta * ease( p / program.duration );\n\t\t\tif ( obj.tick ) obj.tick( this.t );\n\t\t},\n\t\tdone: function () {\n\t\t\tthis.t = this.program.b;\n\t\t\tif ( obj.tick ) obj.tick( this.t );\n\t\t\tif ( obj.css ) document.head.removeChild( style );\n\t\t\tthis.program.callback();\n\t\t\tthis.program = null;\n\t\t\tthis.running = !!this.pending;\n\t\t},\n\t\tabort: function () {\n\t\t\tif ( obj.tick ) obj.tick( 1 );\n\t\t\tif ( obj.css ) document.head.removeChild( style );\n\t\t\tthis.program = this.pending = null;\n\t\t\tthis.running = false;\n\t\t}\n\t};\n}",
"transitionManager": "{\n\trunning: false,\n\ttransitions: [],\n\tbound: null,\n\n\tadd: function ( transition ) {\n\t\tthis.transitions.push( transition );\n\n\t\tif ( !this.running ) {\n\t\t\tthis.running = true;\n\t\t\tthis.next();\n\t\t}\n\t},\n\n\tnext: function () {\n\t\tthis.running = false;\n\n\t\tvar now = window.performance.now();\n\t\tvar i = this.transitions.length;\n\n\t\twhile ( i-- ) {\n\t\t\tvar transition = this.transitions[i];\n\n\t\t\tif ( transition.program && now >= transition.program.end ) {\n\t\t\t\ttransition.done();\n\t\t\t}\n\n\t\t\tif ( transition.pending && now >= transition.pending.start ) {\n\t\t\t\ttransition.start( transition.pending );\n\t\t\t}\n\n\t\t\tif ( transition.running ) {\n\t\t\t\ttransition.update( now );\n\t\t\t\tthis.running = true;\n\t\t\t} else if ( !transition.pending ) {\n\t\t\t\tthis.transitions.splice( i, 1 );\n\t\t\t}\n\t\t}\n\n\t\tif ( this.running ) {\n\t\t\trequestAnimationFrame( this.bound || ( this.bound = this.next.bind( this ) ) );\n\t\t}\n\t}\n}",
"noop": "function noop () {}",
"assign": "function assign ( target ) {\n\tfor ( var i = 1; i < arguments.length; i += 1 ) {\n\t\tvar source = arguments[i];\n\t\tfor ( var k in source ) target[k] = source[k];\n\t}\n\n\treturn target;\n}"
};

@ -1,6 +0,0 @@
import visitors from './visitors/index.js';
export default function visit ( generator, block, state, node ) {
const visitor = visitors[ node.type ];
visitor( generator, block, state, node );
}

@ -0,0 +1,9 @@
import visitors from './visitors/index';
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 );
}

@ -1,4 +1,9 @@
export default function visitAttribute ( generator, block, state, node, attribute, local ) { 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, local ) {
if ( attribute.value === true ) { if ( attribute.value === true ) {
// attributes without values, e.g. <textarea readonly> // attributes without values, e.g. <textarea readonly>
local.staticAttributes.push({ local.staticAttributes.push({

@ -1,8 +1,12 @@
import deindent from '../../../../utils/deindent.js'; import deindent from '../../../../utils/deindent.js';
import flattenReference from '../../../../utils/flattenReference.js'; import flattenReference from '../../../../utils/flattenReference';
import getSetter from '../shared/binding/getSetter.js'; import getSetter from '../shared/binding/getSetter';
import { DomGenerator } from '../../index';
import Block from '../../Block';
import { Node } from '../../../../interfaces';
import { State } from '../../interfaces';
export default function visitBinding ( generator, block, state, node, attribute, local ) { export default function visitBinding ( generator: DomGenerator, block: Block, state: State, node: Node, attribute, local ) {
const { name } = flattenReference( attribute.value ); const { name } = flattenReference( attribute.value );
const { snippet, contexts, dependencies } = block.contextualise( attribute.value ); const { snippet, contexts, dependencies } = block.contextualise( attribute.value );

@ -1,12 +1,16 @@
import deindent from '../../../../utils/deindent.js'; import deindent from '../../../../utils/deindent.js';
import CodeBuilder from '../../../../utils/CodeBuilder.js'; import CodeBuilder from '../../../../utils/CodeBuilder';
import visit from '../../visit.js'; import visit from '../../visit';
import visitAttribute from './Attribute.js'; import visitAttribute from './Attribute';
import visitEventHandler from './EventHandler.js'; import visitEventHandler from './EventHandler';
import visitBinding from './Binding.js'; import visitBinding from './Binding';
import visitRef from './Ref.js'; import visitRef from './Ref';
import { DomGenerator } from '../../index';
function stringifyProps ( props ) { import Block from '../../Block';
import { Node } from '../../../../interfaces';
import { State } from '../../interfaces';
function stringifyProps ( props: string[] ) {
if ( !props.length ) return '{}'; if ( !props.length ) return '{}';
const joined = props.join( ', ' ); const joined = props.join( ', ' );
@ -32,7 +36,7 @@ const visitors = {
Ref: visitRef Ref: visitRef
}; };
export default function visitComponent ( generator, block, state, node ) { export default function visitComponent ( generator: DomGenerator, block: Block, state: State, node: Node ) {
const hasChildren = node.children.length > 0; const hasChildren = node.children.length > 0;
const name = block.getUniqueName( ( node.name === ':Self' ? generator.name : node.name ).toLowerCase() ); const name = block.getUniqueName( ( node.name === ':Self' ? generator.name : node.name ).toLowerCase() );
@ -120,7 +124,7 @@ export default function visitComponent ( generator, block, state, node ) {
componentInitProperties.push( `_yield: ${yieldFragment}`); componentInitProperties.push( `_yield: ${yieldFragment}`);
} }
const statements = []; const statements: string[] = [];
if ( local.staticAttributes.length || local.dynamicAttributes.length || local.bindings.length ) { if ( local.staticAttributes.length || local.dynamicAttributes.length || local.bindings.length ) {
const initialProps = local.staticAttributes const initialProps = local.staticAttributes

@ -1,12 +1,16 @@
import deindent from '../../../../utils/deindent.js'; import deindent from '../../../../utils/deindent.js';
import { DomGenerator } from '../../index';
import Block from '../../Block';
import { Node } from '../../../../interfaces';
import { State } from '../../interfaces';
export default function visitEventHandler ( generator, block, state, node, attribute, local ) { export default function visitEventHandler ( generator: DomGenerator, block: Block, state: State, node: Node, attribute: Node, local ) {
// TODO verify that it's a valid callee (i.e. built-in or declared method) // TODO verify that it's a valid callee (i.e. built-in or declared method)
generator.addSourcemapLocations( attribute.expression ); generator.addSourcemapLocations( attribute.expression );
generator.code.prependRight( attribute.expression.start, `${block.component}.` ); generator.code.prependRight( attribute.expression.start, `${block.component}.` );
const usedContexts = []; const usedContexts: string[] = [];
attribute.expression.arguments.forEach( arg => { attribute.expression.arguments.forEach( ( arg: Node ) => {
const { contexts } = block.contextualise( arg, null, true ); const { contexts } = block.contextualise( arg, null, true );
contexts.forEach( context => { contexts.forEach( context => {

@ -1,6 +1,10 @@
import deindent from '../../../../utils/deindent.js'; import deindent from '../../../../utils/deindent.js';
import { DomGenerator } from '../../index';
import Block from '../../Block';
import { Node } from '../../../../interfaces';
import { State } from '../../interfaces';
export default function visitRef ( generator, block, state, node, attribute, local ) { export default function visitRef ( generator: DomGenerator, block: Block, state: State, node: Node, attribute: Node, local ) {
generator.usesRefs = true; generator.usesRefs = true;
local.create.addLine( local.create.addLine(

@ -1,7 +1,11 @@
import deindent from '../../../utils/deindent.js'; import deindent from '../../../utils/deindent.js';
import visit from '../visit.js'; 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, block, state, node ) { export default function visitEachBlock ( generator: DomGenerator, block: Block, state: State, node: Node ) {
const each_block = generator.getUniqueName( `each_block` ); const each_block = generator.getUniqueName( `each_block` );
const create_each_block = node._block.name; const create_each_block = node._block.name;
const each_block_value = node._block.listName; const each_block_value = node._block.listName;
@ -86,18 +90,18 @@ export default function visitEachBlock ( generator, block, state, node ) {
` ); ` );
} }
node.children.forEach( child => { node.children.forEach( ( child: Node ) => {
visit( generator, node._block, node._state, child ); visit( generator, node._block, node._state, child );
}); });
if ( node.else ) { if ( node.else ) {
node.else.children.forEach( child => { node.else.children.forEach( ( child: Node ) => {
visit( generator, node.else._block, node.else._state, child ); visit( generator, node.else._block, node.else._state, child );
}); });
} }
} }
function keyed ( generator, block, state, node, snippet, { each_block, create_each_block, each_block_value, i, params, anchor, mountOrIntro } ) { 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 key = block.getUniqueName( 'key' );
const lookup = block.getUniqueName( `${each_block}_lookup` ); const lookup = block.getUniqueName( `${each_block}_lookup` );
const iteration = block.getUniqueName( `${each_block}_iteration` ); const iteration = block.getUniqueName( `${each_block}_iteration` );
@ -151,8 +155,6 @@ function keyed ( generator, block, state, node, snippet, { each_block, create_ea
function ${fn} ( iteration ) { function ${fn} ( iteration ) {
iteration.outro( function () { iteration.outro( function () {
iteration.destroy( true ); iteration.destroy( true );
if ( iteration.next ) iteration.next.last = iteration.last;
if ( iteration.last ) iteration.last.next = iteration.next;
${lookup}[iteration.key] = null; ${lookup}[iteration.key] = null;
}); });
} }
@ -175,8 +177,6 @@ function keyed ( generator, block, state, node, snippet, { each_block, create_ea
block.builders.create.addBlock( deindent` block.builders.create.addBlock( deindent`
function ${fn} ( iteration ) { function ${fn} ( iteration ) {
iteration.destroy( true ); iteration.destroy( true );
if ( iteration.next && iteration.next.last === iteration ) iteration.next.last = iteration.last;
if ( iteration.last && iteration.last.next === iteration ) iteration.last.next = iteration.next;
${lookup}[iteration.key] = null; ${lookup}[iteration.key] = null;
} }
` ); ` );
@ -200,7 +200,7 @@ function keyed ( generator, block, state, node, snippet, { each_block, create_ea
var ${each_block_value} = ${snippet}; var ${each_block_value} = ${snippet};
var ${expected} = ${head}; var ${expected} = ${head};
var ${last}; var ${last} = null;
var discard_pile = []; var discard_pile = [];
@ -216,24 +216,24 @@ function keyed ( generator, block, state, node, snippet, { each_block, create_ea
} else { } else {
if ( ${iteration} ) { if ( ${iteration} ) {
// probably a deletion // probably a deletion
do {
while ( ${expected} && ${expected}.key !== ${key} ) {
${expected}.discard = true; ${expected}.discard = true;
discard_pile.push( ${expected} ); discard_pile.push( ${expected} );
${expected} = ${expected}.next; ${expected} = ${expected}.next;
} while ( ${expected} && ${expected}.key !== ${key} ); };
${expected} = ${expected} && ${expected}.next; ${expected} = ${expected} && ${expected}.next;
${iteration}.discard = false; ${iteration}.discard = false;
${iteration}.last = ${last}; ${iteration}.last = ${last};
${iteration}.next = ${expected};
${iteration}.mount( ${parentNode}, ${expected} ? ${expected}.first : ${anchor} ); if (!${expected}) ${iteration}.mount( ${parentNode}, ${anchor} );
} else { } else {
// key is being inserted // key is being inserted
${iteration} = ${lookup}[${key}] = ${create_each_block}( ${params}, ${each_block_value}, ${each_block_value}[${i}], ${i}, ${block.component}, ${key} ); ${iteration} = ${lookup}[${key}] = ${create_each_block}( ${params}, ${each_block_value}, ${each_block_value}[${i}], ${i}, ${block.component}, ${key} );
${iteration}.${mountOrIntro}( ${parentNode}, ${expected}.first ); ${iteration}.${mountOrIntro}( ${parentNode}, ${expected}.first );
if ( ${expected} ) ${expected}.last = ${iteration}; ${expected}.last = ${iteration};
${iteration}.next = ${expected}; ${iteration}.next = ${expected};
} }
} }
@ -271,7 +271,7 @@ function keyed ( generator, block, state, node, snippet, { each_block, create_ea
` ); ` );
} }
function unkeyed ( generator, block, state, node, snippet, { create_each_block, each_block_value, iterations, i, params, anchor, mountOrIntro } ) { 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` block.builders.create.addBlock( deindent`
var ${iterations} = []; var ${iterations} = [];
@ -363,4 +363,4 @@ function unkeyed ( generator, block, state, node, snippet, { create_each_block,
block.builders.destroy.addBlock( block.builders.destroy.addBlock(
`${generator.helper( 'destroyEach' )}( ${iterations}, ${state.parentNode ? 'false' : 'detach'}, 0 );` `${generator.helper( 'destroyEach' )}( ${iterations}, ${state.parentNode ? 'false' : 'detach'}, 0 );`
); );
} }

@ -1,8 +1,12 @@
import attributeLookup from './lookup.js'; import attributeLookup from './lookup';
import deindent from '../../../../utils/deindent.js'; import deindent from '../../../../utils/deindent.js';
import getStaticAttributeValue from './getStaticAttributeValue.js'; 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, block, state, node, attribute ) { export default function visitAttribute ( generator: DomGenerator, block: Block, state: State, node: Node, attribute: Node ) {
const name = attribute.name; const name = attribute.name;
let metadata = state.namespace ? null : attributeLookup[ name ]; let metadata = state.namespace ? null : attributeLookup[ name ];
@ -32,7 +36,7 @@ export default function visitAttribute ( generator, block, state, node, attribut
} else { } else {
// '{{foo}} {{bar}}' — treat as string concatenation // '{{foo}} {{bar}}' — treat as string concatenation
value = ( attribute.value[0].type === 'Text' ? '' : `"" + ` ) + ( value = ( attribute.value[0].type === 'Text' ? '' : `"" + ` ) + (
attribute.value.map( chunk => { attribute.value.map( ( chunk: Node ) => {
if ( chunk.type === 'Text' ) { if ( chunk.type === 'Text' ) {
return JSON.stringify( chunk.data ); return JSON.stringify( chunk.data );
} else { } else {

@ -1,9 +1,13 @@
import deindent from '../../../../utils/deindent.js'; import deindent from '../../../../utils/deindent.js';
import flattenReference from '../../../../utils/flattenReference.js'; import flattenReference from '../../../../utils/flattenReference';
import getSetter from '../shared/binding/getSetter.js'; import getSetter from '../shared/binding/getSetter';
import getStaticAttributeValue from './getStaticAttributeValue.js'; import getStaticAttributeValue from './getStaticAttributeValue';
import { DomGenerator } from '../../index';
export default function visitBinding ( generator, block, state, node, attribute ) { 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: Node ) {
const { name, parts } = flattenReference( attribute.value ); const { name, parts } = flattenReference( attribute.value );
const { snippet, contexts, dependencies } = block.contextualise( attribute.value ); const { snippet, contexts, dependencies } = block.contextualise( attribute.value );
@ -15,7 +19,7 @@ export default function visitBinding ( generator, block, state, node, attribute
const eventName = getBindingEventName( node, attribute ); const eventName = getBindingEventName( node, attribute );
const handler = block.getUniqueName( `${state.parentNode}_${eventName}_handler` ); const handler = block.getUniqueName( `${state.parentNode}_${eventName}_handler` );
const isMultipleSelect = node.name === 'select' && node.attributes.find( attr => attr.name.toLowerCase() === 'multiple' ); // TODO use getStaticAttributeValue const isMultipleSelect = node.name === 'select' && node.attributes.find( ( attr: Node ) => attr.name.toLowerCase() === 'multiple' ); // TODO use getStaticAttributeValue
const type = getStaticAttributeValue( node, 'type' ); const type = getStaticAttributeValue( node, 'type' );
const bindingGroup = attribute.name === 'group' ? getBindingGroup( generator, parts.join( '.' ) ) : null; const bindingGroup = attribute.name === 'group' ? getBindingGroup( generator, parts.join( '.' ) ) : null;
const value = getBindingValue( generator, block, state, node, attribute, isMultipleSelect, bindingGroup, type ); const value = getBindingValue( generator, block, state, node, attribute, isMultipleSelect, bindingGroup, type );
@ -141,9 +145,9 @@ export default function visitBinding ( generator, block, state, node, attribute
} }
} }
function getBindingEventName ( node, attribute ) { function getBindingEventName ( node: Node, attribute: Node ) {
if ( node.name === 'input' ) { if ( node.name === 'input' ) {
const typeAttribute = node.attributes.find( attr => attr.type === 'Attribute' && attr.name === 'type' ); const typeAttribute = node.attributes.find( ( attr: Node ) => attr.type === 'Attribute' && attr.name === 'type' );
const type = typeAttribute ? typeAttribute.value[0].data : 'text'; // TODO in validation, should throw if type attribute is not static const type = typeAttribute ? typeAttribute.value[0].data : 'text'; // TODO in validation, should throw if type attribute is not static
return type === 'checkbox' || type === 'radio' ? 'change' : 'input'; return type === 'checkbox' || type === 'radio' ? 'change' : 'input';
@ -157,7 +161,7 @@ function getBindingEventName ( node, attribute ) {
return 'change'; return 'change';
} }
function getBindingValue ( generator, block, state, node, attribute, isMultipleSelect, bindingGroup, type ) { function getBindingValue ( generator: DomGenerator, block: Block, state: State, node: Node, attribute: Node, isMultipleSelect: boolean, bindingGroup: number, type: string ) {
// <select multiple bind:value='selected> // <select multiple bind:value='selected>
if ( isMultipleSelect ) { if ( isMultipleSelect ) {
return `[].map.call( ${state.parentNode}.querySelectorAll(':checked'), function ( option ) { return option.__value; })`; return `[].map.call( ${state.parentNode}.querySelectorAll(':checked'), function ( option ) { return option.__value; })`;
@ -186,7 +190,7 @@ function getBindingValue ( generator, block, state, node, attribute, isMultipleS
return `${state.parentNode}.${attribute.name}`; return `${state.parentNode}.${attribute.name}`;
} }
function getBindingGroup ( generator, keypath ) { function getBindingGroup ( generator: DomGenerator, keypath: string ) {
// TODO handle contextual bindings — `keypath` should include unique ID of // TODO handle contextual bindings — `keypath` should include unique ID of
// each block that provides context // each block that provides context
let index = generator.bindingGroups.indexOf( keypath ); let index = generator.bindingGroups.indexOf( keypath );

@ -1,12 +1,16 @@
import deindent from '../../../../utils/deindent.js'; import deindent from '../../../../utils/deindent.js';
import visit from '../../visit.js'; import visit from '../../visit';
import visitComponent from '../Component/Component.js'; import visitComponent from '../Component/Component';
import visitWindow from './meta/Window.js'; import visitWindow from './meta/Window';
import visitAttribute from './Attribute.js'; import visitAttribute from './Attribute';
import visitEventHandler from './EventHandler.js'; import visitEventHandler from './EventHandler';
import visitBinding from './Binding.js'; import visitBinding from './Binding';
import visitRef from './Ref.js'; import visitRef from './Ref';
import addTransitions from './addTransitions.js'; import addTransitions from './addTransitions';
import { DomGenerator } from '../../index';
import Block from '../../Block';
import { Node } from '../../../../interfaces';
import { State } from '../../interfaces';
const meta = { const meta = {
':Window': visitWindow ':Window': visitWindow
@ -26,7 +30,7 @@ const visitors = {
Ref: visitRef Ref: visitRef
}; };
export default function visitElement ( generator, block, state, node ) { export default function visitElement ( generator: DomGenerator, block: Block, state: State, node: Node ) {
if ( node.name in meta ) { if ( node.name in meta ) {
return meta[ node.name ]( generator, block, node ); return meta[ node.name ]( generator, block, node );
} }
@ -46,13 +50,13 @@ export default function visitElement ( generator, block, state, node ) {
block.builders.create.addLine( `${generator.helper( 'setAttribute' )}( ${name}, '${generator.cssId}', '' );` ); block.builders.create.addLine( `${generator.helper( 'setAttribute' )}( ${name}, '${generator.cssId}', '' );` );
} }
function visitAttributes () { function visitAttributesAndAddProps () {
let intro; let intro;
let outro; let outro;
node.attributes node.attributes
.sort( ( a, b ) => order[ a.type ] - order[ b.type ] ) .sort( ( a: Node, b: Node ) => order[ a.type ] - order[ b.type ] )
.forEach( attribute => { .forEach( ( attribute: Node ) => {
if ( attribute.type === 'Transition' ) { if ( attribute.type === 'Transition' ) {
if ( attribute.intro ) intro = attribute; if ( attribute.intro ) intro = attribute;
if ( attribute.outro ) outro = attribute; if ( attribute.outro ) outro = attribute;
@ -63,6 +67,37 @@ export default function visitElement ( generator, block, state, node ) {
}); });
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 ) {
const initialProps: string[] = [];
const updates: string[] = [];
if ( childState.usesComponent ) {
initialProps.push( `component: ${block.component}` );
}
childState.allUsedContexts.forEach( ( contextName: string ) => {
if ( contextName === 'state' ) return;
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};` );
});
if ( initialProps.length ) {
block.builders.create.addBlock( deindent`
${name}._svelte = {
${initialProps.join( ',\n' )}
};
` );
}
if ( updates.length ) {
block.builders.update.addBlock( updates.join( '\n' ) );
}
}
} }
if ( !state.parentNode ) { if ( !state.parentNode ) {
@ -74,47 +109,16 @@ export default function visitElement ( generator, block, state, node ) {
if ( node.name !== 'select' ) { if ( node.name !== 'select' ) {
// <select> value attributes are an annoying special case — it must be handled // <select> value attributes are an annoying special case — it must be handled
// *after* its children have been updated // *after* its children have been updated
visitAttributes(); visitAttributesAndAddProps();
} }
// special case bound <option> without a value attribute // special case bound <option> without a value attribute
if ( node.name === 'option' && !node.attributes.find( attribute => attribute.type === 'Attribute' && attribute.name === 'value' ) ) { // TODO check it's bound if ( node.name === 'option' && !node.attributes.find( ( attribute: Node ) => attribute.type === 'Attribute' && attribute.name === 'value' ) ) { // TODO check it's bound
const statement = `${name}.__value = ${name}.textContent;`; const statement = `${name}.__value = ${name}.textContent;`;
node.initialUpdate = node.lateUpdate = statement; node.initialUpdate = node.lateUpdate = statement;
} }
if ( childState.allUsedContexts.length || childState.usesComponent ) { node.children.forEach( ( child: Node ) => {
const initialProps = [];
const updates = [];
if ( childState.usesComponent ) {
initialProps.push( `component: ${block.component}` );
}
childState.allUsedContexts.forEach( contextName => {
if ( contextName === 'state' ) return;
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};` );
});
if ( initialProps.length ) {
block.builders.create.addBlock( deindent`
${name}._svelte = {
${initialProps.join( ',\n' )}
};
` );
}
if ( updates.length ) {
block.builders.update.addBlock( updates.join( '\n' ) );
}
}
node.children.forEach( child => {
visit( generator, block, childState, child ); visit( generator, block, childState, child );
}); });
@ -123,7 +127,7 @@ export default function visitElement ( generator, block, state, node ) {
} }
if ( node.name === 'select' ) { if ( node.name === 'select' ) {
visitAttributes(); visitAttributesAndAddProps();
} }
if ( node.initialUpdate ) { if ( node.initialUpdate ) {
@ -131,7 +135,7 @@ export default function visitElement ( generator, block, state, node ) {
} }
} }
function getRenderStatement ( generator, namespace, name ) { function getRenderStatement ( generator: DomGenerator, namespace: string, name: string ) {
if ( namespace === 'http://www.w3.org/2000/svg' ) { if ( namespace === 'http://www.w3.org/2000/svg' ) {
return `${generator.helper( 'createSvgElement' )}( '${name}' )`; return `${generator.helper( 'createSvgElement' )}( '${name}' )`;
} }

@ -1,7 +1,11 @@
import deindent from '../../../../utils/deindent.js'; import deindent from '../../../../utils/deindent.js';
import flattenReference from '../../../../utils/flattenReference.js'; 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, block, state, node, attribute ) { export default function visitEventHandler ( generator: DomGenerator, block: Block, state: State, node: Node, attribute: Node ) {
const name = attribute.name; const name = attribute.name;
const isCustomEvent = generator.events.has( name ); const isCustomEvent = generator.events.has( name );
const shouldHoist = !isCustomEvent && state.inEachBlock; const shouldHoist = !isCustomEvent && state.inEachBlock;
@ -17,8 +21,8 @@ export default function visitEventHandler ( generator, block, state, node, attri
} }
const context = shouldHoist ? null : state.parentNode; const context = shouldHoist ? null : state.parentNode;
const usedContexts = []; const usedContexts: string[] = [];
attribute.expression.arguments.forEach( arg => { attribute.expression.arguments.forEach( ( arg: Node ) => {
const { contexts } = block.contextualise( arg, context, true ); const { contexts } = block.contextualise( arg, context, true );
contexts.forEach( context => { contexts.forEach( context => {

@ -1,6 +1,10 @@
import deindent from '../../../../utils/deindent.js'; import deindent from '../../../../utils/deindent.js';
import { DomGenerator } from '../../index';
import Block from '../../Block';
import { Node } from '../../../../interfaces';
import { State } from '../../interfaces';
export default function visitRef ( generator, block, state, node, attribute ) { export default function visitRef ( generator: DomGenerator, block: Block, state: State, node: Node, attribute: Node ) {
const name = attribute.name; const name = attribute.name;
block.builders.create.addLine( block.builders.create.addLine(

@ -1,6 +1,10 @@
import deindent from '../../../../utils/deindent.js'; import deindent from '../../../../utils/deindent.js';
import { DomGenerator } from '../../index';
import Block from '../../Block';
import { Node } from '../../../../interfaces';
import { State } from '../../interfaces';
export default function addTransitions ( generator, block, state, node, intro, outro ) { export default function addTransitions ( generator: DomGenerator, block: Block, state: State, node: Node, intro, outro ) {
const wrapTransition = generator.helper( 'wrapTransition' ); const wrapTransition = generator.helper( 'wrapTransition' );
if ( intro === outro ) { if ( intro === outro ) {

@ -1,5 +1,7 @@
export default function getStaticAttributeValue ( node, name ) { import { Node } from '../../../../interfaces';
const attribute = node.attributes.find( attr => attr.name.toLowerCase() === name );
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 ) return null;
if ( attribute.value.length !== 1 || attribute.value[0].type !== 'Text' ) { if ( attribute.value.length !== 1 || attribute.value[0].type !== 'Text' ) {

@ -1,5 +1,8 @@
import flattenReference from '../../../../../utils/flattenReference.js'; import flattenReference from '../../../../../utils/flattenReference';
import deindent from '../../../../../utils/deindent.js'; import deindent from '../../../../../utils/deindent.js';
import { DomGenerator } from '../../../index';
import Block from '../../../Block';
import { Node } from '../../../../../interfaces';
const associatedEvents = { const associatedEvents = {
innerWidth: 'resize', innerWidth: 'resize',
@ -19,18 +22,18 @@ const readonly = new Set([
'online' 'online'
]); ]);
export default function visitWindow ( generator, block, node ) { export default function visitWindow ( generator: DomGenerator, block: Block, node: Node ) {
const events = {}; const events = {};
const bindings = {}; const bindings = {};
node.attributes.forEach( attribute => { node.attributes.forEach( ( attribute: Node ) => {
if ( attribute.type === 'EventHandler' ) { if ( attribute.type === 'EventHandler' ) {
// TODO verify that it's a valid callee (i.e. built-in or declared method) // TODO verify that it's a valid callee (i.e. built-in or declared method)
generator.addSourcemapLocations( attribute.expression ); generator.addSourcemapLocations( attribute.expression );
let usesState = false; let usesState = false;
attribute.expression.arguments.forEach( arg => { attribute.expression.arguments.forEach( ( arg: Node ) => {
const { contexts } = block.contextualise( arg, null, true ); const { contexts } = block.contextualise( arg, null, true );
if ( contexts.length ) usesState = true; if ( contexts.length ) usesState = true;
}); });

@ -1,7 +1,11 @@
import deindent from '../../../utils/deindent.js'; import deindent from '../../../utils/deindent.js';
import visit from '../visit.js'; import visit from '../visit';
import { DomGenerator } from '../index';
import Block from '../Block';
import { Node } from '../../../interfaces';
import { State } from '../interfaces';
function isElseIf ( node ) { function isElseIf ( node: Node ) {
return node && node.children.length === 1 && node.children[0].type === 'IfBlock'; return node && node.children.length === 1 && node.children[0].type === 'IfBlock';
} }
@ -9,7 +13,7 @@ function isElseBranch ( branch ) {
return branch.block && !branch.condition; return branch.block && !branch.condition;
} }
function getBranches ( generator, block, state, node ) { function getBranches ( generator: DomGenerator, block: Block, state: State, node: Node ) {
const branches = [{ const branches = [{
condition: block.contextualise( node.expression ).snippet, condition: block.contextualise( node.expression ).snippet,
block: node._block.name, block: node._block.name,
@ -41,13 +45,13 @@ function getBranches ( generator, block, state, node ) {
return branches; return branches;
} }
function visitChildren ( generator, block, state, node ) { function visitChildren ( generator: DomGenerator, block: Block, state: State, node: Node ) {
node.children.forEach( child => { node.children.forEach( ( child: Node ) => {
visit( generator, node._block, node._state, child ); visit( generator, node._block, node._state, child );
}); });
} }
export default function visitIfBlock ( generator, block, state, node ) { export default function visitIfBlock ( generator: DomGenerator, block: Block, state: State, node: Node ) {
const name = generator.getUniqueName( `if_block` ); const name = generator.getUniqueName( `if_block` );
const anchor = node.needsAnchor ? block.getUniqueName( `${name}_anchor` ) : ( node.next && node.next._state.name ) || 'null'; const anchor = node.needsAnchor ? block.getUniqueName( `${name}_anchor` ) : ( node.next && node.next._state.name ) || 'null';
const params = block.params.join( ', ' ); const params = block.params.join( ', ' );
@ -79,7 +83,7 @@ export default function visitIfBlock ( generator, block, state, node ) {
} }
} }
function simple ( generator, block, state, node, branch, dynamic, { name, anchor, params, if_name } ) { function simple ( generator: DomGenerator, block: Block, state: State, node: Node, branch, dynamic, { name, anchor, params, if_name } ) {
block.builders.create.addBlock( deindent` block.builders.create.addBlock( deindent`
var ${name} = (${branch.condition}) && ${branch.block}( ${params}, ${block.component} ); var ${name} = (${branch.condition}) && ${branch.block}( ${params}, ${block.component} );
` ); ` );
@ -153,7 +157,7 @@ function simple ( generator, block, state, node, branch, dynamic, { name, anchor
); );
} }
function compound ( generator, block, state, node, branches, dynamic, { name, anchor, params, hasElse, if_name } ) { 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 get_block = block.getUniqueName( `get_block` );
const current_block = block.getUniqueName( `current_block` ); const current_block = block.getUniqueName( `current_block` );
const current_block_and = hasElse ? '' : `${current_block} && `; const current_block_and = hasElse ? '' : `${current_block} && `;
@ -209,7 +213,7 @@ function compound ( generator, block, state, node, branches, dynamic, { name, an
// if any of the siblings have outros, we need to keep references to the blocks // if any of the siblings have outros, we need to keep references to the blocks
// (TODO does this only apply to bidi transitions?) // (TODO does this only apply to bidi transitions?)
function compoundWithOutros ( generator, block, state, node, branches, dynamic, { name, anchor, params, hasElse } ) { function compoundWithOutros ( generator: DomGenerator, block: Block, state: State, node: Node, branches, dynamic, { name, anchor, params, hasElse } ) {
const get_block = block.getUniqueName( `get_block` ); const get_block = block.getUniqueName( `get_block` );
const current_block_index = block.getUniqueName( `current_block_index` ); const current_block_index = block.getUniqueName( `current_block_index` );
const previous_block_index = block.getUniqueName( `previous_block_index` ); const previous_block_index = block.getUniqueName( `previous_block_index` );

@ -1,6 +1,10 @@
import deindent from '../../../utils/deindent.js'; import deindent from '../../../utils/deindent.js';
import { DomGenerator } from '../index';
import Block from '../Block';
import { Node } from '../../../interfaces';
import { State } from '../interfaces';
export default function visitMustacheTag ( generator, block, state, node ) { export default function visitMustacheTag ( generator: DomGenerator, block: Block, state: State, node: Node ) {
const name = node._state.name; const name = node._state.name;
const value = block.getUniqueName( `${name}_value` ); const value = block.getUniqueName( `${name}_value` );

@ -1,6 +1,10 @@
import deindent from '../../../utils/deindent.js'; import deindent from '../../../utils/deindent.js';
import { DomGenerator } from '../index';
import Block from '../Block';
import { Node } from '../../../interfaces';
import { State } from '../interfaces';
export default function visitRawMustacheTag ( generator, block, state, node ) { export default function visitRawMustacheTag ( generator: DomGenerator, block: Block, state: State, node: Node ) {
const name = node._state.basename; const name = node._state.basename;
const before = node._state.name; const before = node._state.name;
const value = block.getUniqueName( `${name}_value` ); const value = block.getUniqueName( `${name}_value` );

@ -1,6 +0,0 @@
export default function visitText ( generator, block, state, node ) {
if ( !node._state.shouldCreate ) return;
block.addElement( node._state.name, `${generator.helper( 'createText' )}( ${JSON.stringify( node.data )} )`, state.parentNode, node.usedAsAnchor );
}

@ -0,0 +1,9 @@
import { DomGenerator } from '../index';
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 );
}

@ -1,4 +1,8 @@
export default function visitYieldTag ( generator, block, state ) { import { DomGenerator } from '../index';
import Block from '../Block';
import { State } from '../interfaces';
export default function visitYieldTag ( generator: DomGenerator, block: Block, state: State ) {
const parentNode = state.parentNode || block.target; const parentNode = state.parentNode || block.target;
( state.parentNode ? block.builders.create : block.builders.mount ).addLine( ( state.parentNode ? block.builders.create : block.builders.mount ).addLine(

@ -1,17 +0,0 @@
import EachBlock from './EachBlock.js';
import Element from './Element/Element.js';
import IfBlock from './IfBlock.js';
import MustacheTag from './MustacheTag.js';
import RawMustacheTag from './RawMustacheTag.js';
import Text from './Text.js';
import YieldTag from './YieldTag.js';
export default {
EachBlock,
Element,
IfBlock,
MustacheTag,
RawMustacheTag,
Text,
YieldTag
};

@ -0,0 +1,17 @@
import EachBlock from './EachBlock';
import Element from './Element/Element';
import IfBlock from './IfBlock';
import MustacheTag from './MustacheTag';
import RawMustacheTag from './RawMustacheTag';
import Text from './Text';
import YieldTag from './YieldTag';
export default {
EachBlock,
Element,
IfBlock,
MustacheTag,
RawMustacheTag,
Text,
YieldTag
};

@ -1,12 +1,25 @@
import deindent from '../../utils/deindent.js'; import deindent from '../../utils/deindent.js';
import flattenReference from '../../utils/flattenReference.js'; import flattenReference from '../../utils/flattenReference';
import { SsrGenerator } from './index';
import { Node } from '../../interfaces';
interface BlockOptions {
// TODO
}
export default class Block { export default class Block {
constructor ( options ) { generator: SsrGenerator;
conditions: string[];
contexts: Map<string, string>;
indexes: Map<string, string>;
contextDependencies: Map<string, string[]>;
constructor ( options: BlockOptions ) {
Object.assign( this, options ); Object.assign( this, options );
} }
addBinding ( binding, name ) { addBinding ( binding: Node, name: string ) {
const conditions = [ `!( '${binding.name}' in state )`].concat( // TODO handle contextual bindings... const conditions = [ `!( '${binding.name}' in state )`].concat( // TODO handle contextual bindings...
this.conditions.map( c => `(${c})` ) this.conditions.map( c => `(${c})` )
); );
@ -24,11 +37,11 @@ export default class Block {
` ); ` );
} }
child ( options ) { child ( options: BlockOptions ) {
return new Block( Object.assign( {}, this, options, { parent: this } ) ); return new Block( Object.assign( {}, this, options, { parent: this } ) );
} }
contextualise ( expression, context, isEventHandler ) { contextualise ( expression: Node, context?: string, isEventHandler?: boolean ) {
return this.generator.contextualise( this, expression, context, isEventHandler ); return this.generator.contextualise( this, expression, context, isEventHandler );
} }
} }

@ -1,21 +1,26 @@
import deindent from '../../utils/deindent.js'; import deindent from '../../utils/deindent.js';
import Generator from '../Generator.js'; import Generator from '../Generator';
import Block from './Block.js'; import Block from './Block';
import visit from './visit.js'; import visit from './visit';
import { Parsed, Node, CompileOptions } from '../../interfaces';
class SsrGenerator extends Generator { export class SsrGenerator extends Generator {
constructor ( parsed, source, name, options ) { bindings: string[];
renderCode: string;
elementDepth: number;
constructor ( parsed: Parsed, source: string, name: string, options: CompileOptions ) {
super( parsed, source, name, options ); super( parsed, source, name, options );
this.bindings = []; this.bindings = [];
this.renderCode = ''; this.renderCode = '';
} }
append ( code ) { append ( code: string ) {
this.renderCode += code; this.renderCode += code;
} }
} }
export default function ssr ( parsed, source, options ) { export default function ssr ( parsed: Parsed, source: string, options: CompileOptions ) {
const format = options.format || 'cjs'; const format = options.format || 'cjs';
const name = options.name || 'SvelteComponent'; const name = options.name || 'SvelteComponent';
@ -31,7 +36,7 @@ export default function ssr ( parsed, source, options ) {
conditions: [] conditions: []
}); });
parsed.html.children.forEach( node => { parsed.html.children.forEach( ( node: Node ) => {
visit( generator, mainBlock, node ); visit( generator, mainBlock, node );
}); });

@ -1,6 +0,0 @@
import visitors from './visitors/index.js';
export default function visit ( generator, fragment, node ) {
const visitor = visitors[ node.type ];
visitor( generator, fragment, node );
}

@ -0,0 +1,9 @@
import visitors from './visitors/index';
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 );
}

@ -1,8 +1,11 @@
import flattenReference from '../../../utils/flattenReference.js'; import flattenReference from '../../../utils/flattenReference';
import visit from '../visit.js'; import visit from '../visit';
import { SsrGenerator } from '../index';
import Block from '../Block';
import { Node } from '../../../interfaces';
export default function visitComponent ( generator, block, node ) { export default function visitComponent ( generator: SsrGenerator, block: Block, node: Node ) {
function stringify ( chunk ) { function stringify ( chunk: Node ) {
if ( chunk.type === 'Text' ) return chunk.data; if ( chunk.type === 'Text' ) return chunk.data;
if ( chunk.type === 'MustacheTag' ) { if ( chunk.type === 'MustacheTag' ) {
const { snippet } = block.contextualise( chunk.expression ); const { snippet } = block.contextualise( chunk.expression );
@ -10,10 +13,10 @@ export default function visitComponent ( generator, block, node ) {
} }
} }
const attributes = []; const attributes: Node[] = [];
const bindings = []; const bindings: Node[] = [];
node.attributes.forEach( attribute => { node.attributes.forEach( ( attribute: Node ) => {
if ( attribute.type === 'Attribute' ) { if ( attribute.type === 'Attribute' ) {
attributes.push( attribute ); attributes.push( attribute );
} else if ( attribute.type === 'Binding' ) { } else if ( attribute.type === 'Binding' ) {
@ -66,7 +69,7 @@ export default function visitComponent ( generator, block, node ) {
generator.elementDepth += 1; generator.elementDepth += 1;
node.children.forEach( child => { node.children.forEach( ( child: Node ) => {
visit( generator, block, child ); visit( generator, block, child );
}); });

@ -1,6 +1,9 @@
import visit from '../visit.js'; import visit from '../visit';
import { SsrGenerator } from '../index';
import Block from '../Block';
import { Node } from '../../../interfaces';
export default function visitEachBlock ( generator, block, node ) { export default function visitEachBlock ( generator: SsrGenerator, block: Block, node: Node ) {
const { dependencies, snippet } = block.contextualise( node.expression ); const { dependencies, snippet } = block.contextualise( node.expression );
const open = `\${ ${snippet}.map( ${ node.index ? `( ${node.context}, ${node.index} )` : node.context} => \``; const open = `\${ ${snippet}.map( ${ node.index ? `( ${node.context}, ${node.index} )` : node.context} => \``;
@ -23,7 +26,7 @@ export default function visitEachBlock ( generator, block, node ) {
contextDependencies contextDependencies
}); });
node.children.forEach( child => { node.children.forEach( ( child: Node ) => {
visit( generator, childBlock, child ); visit( generator, childBlock, child );
}); });

@ -1,13 +1,16 @@
import visitComponent from './Component.js'; import visitComponent from './Component';
import isVoidElementName from '../../../utils/isVoidElementName.js'; import isVoidElementName from '../../../utils/isVoidElementName';
import visit from '../visit.js'; import visit from '../visit';
import visitWindow from './meta/Window.js'; import visitWindow from './meta/Window';
import { SsrGenerator } from '../index';
import Block from '../Block';
import { Node } from '../../../interfaces';
const meta = { const meta = {
':Window': visitWindow ':Window': visitWindow
}; };
export default function visitElement ( generator, block, node ) { export default function visitElement ( generator: SsrGenerator, block: Block, node: Node ) {
if ( node.name in meta ) { if ( node.name in meta ) {
return meta[ node.name ]( generator, block, node ); return meta[ node.name ]( generator, block, node );
} }
@ -19,13 +22,13 @@ export default function visitElement ( generator, block, node ) {
let openingTag = `<${node.name}`; let openingTag = `<${node.name}`;
node.attributes.forEach( attribute => { node.attributes.forEach( ( attribute: Node ) => {
if ( attribute.type !== 'Attribute' ) return; if ( attribute.type !== 'Attribute' ) return;
let str = ` ${attribute.name}`; let str = ` ${attribute.name}`;
if ( attribute.value !== true ) { if ( attribute.value !== true ) {
str += `="` + attribute.value.map( chunk => { str += `="` + attribute.value.map( ( chunk: Node ) => {
if ( chunk.type === 'Text' ) { if ( chunk.type === 'Text' ) {
return chunk.data; return chunk.data;
} }
@ -48,7 +51,7 @@ export default function visitElement ( generator, block, node ) {
generator.elementDepth += 1; generator.elementDepth += 1;
node.children.forEach( child => { node.children.forEach( ( child: Node ) => {
visit( generator, block, child ); visit( generator, block, child );
}); });

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

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

@ -0,0 +1,8 @@
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 + ' )}' );
}

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

@ -0,0 +1,8 @@
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 + '}' );
}

@ -1,3 +0,0 @@
export default function visitText ( generator, block, node ) {
generator.append( node.data.replace( /\${/g, '\\${' ) );
}

@ -0,0 +1,7 @@
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, '\\${' ) );
}

@ -1,3 +0,0 @@
export default function visitYieldTag ( generator ) {
generator.append( `\${options && options.yield ? options.yield() : ''}` );
}

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

@ -1,19 +0,0 @@
import Comment from './Comment.js';
import EachBlock from './EachBlock.js';
import Element from './Element.js';
import IfBlock from './IfBlock.js';
import MustacheTag from './MustacheTag.js';
import RawMustacheTag from './RawMustacheTag.js';
import Text from './Text.js';
import YieldTag from './YieldTag.js';
export default {
Comment,
EachBlock,
Element,
IfBlock,
MustacheTag,
RawMustacheTag,
Text,
YieldTag
};

@ -0,0 +1,19 @@
import Comment from './Comment';
import EachBlock from './EachBlock';
import Element from './Element';
import IfBlock from './IfBlock';
import MustacheTag from './MustacheTag';
import RawMustacheTag from './RawMustacheTag';
import Text from './Text';
import YieldTag from './YieldTag';
export default {
Comment,
EachBlock,
Element,
IfBlock,
MustacheTag,
RawMustacheTag,
Text,
YieldTag
};

@ -1,6 +1,8 @@
import { Parsed, Node } from '../../interfaces';
const commentsPattern = /\/\*[\s\S]*?\*\//g; const commentsPattern = /\/\*[\s\S]*?\*\//g;
export default function processCss ( parsed, code ) { export default function processCss ( parsed: Parsed, code ) {
const css = parsed.css.content.styles; const css = parsed.css.content.styles;
const offset = parsed.css.content.start; const offset = parsed.css.content.start;
@ -8,9 +10,9 @@ export default function processCss ( parsed, code ) {
const keyframes = new Map(); const keyframes = new Map();
function walkKeyframes ( node ) { function walkKeyframes ( node: Node ) {
if ( node.type === 'Atrule' && node.name.toLowerCase() === 'keyframes' ) { if ( node.type === 'Atrule' && node.name.toLowerCase() === 'keyframes' ) {
node.expression.children.forEach( expression => { node.expression.children.forEach( ( expression: Node ) => {
if ( expression.type === 'Identifier' ) { if ( expression.type === 'Identifier' ) {
const newName = `svelte-${parsed.hash}-${expression.name}`; const newName = `svelte-${parsed.hash}-${expression.name}`;
code.overwrite( expression.start, expression.end, newName ); code.overwrite( expression.start, expression.end, newName );
@ -26,8 +28,8 @@ export default function processCss ( parsed, code ) {
parsed.css.children.forEach( walkKeyframes ); parsed.css.children.forEach( walkKeyframes );
function transform ( rule ) { function transform ( rule: Node ) {
rule.selector.children.forEach( selector => { rule.selector.children.forEach( ( selector: Node ) => {
const start = selector.start - offset; const start = selector.start - offset;
const end = selector.end - offset; const end = selector.end - offset;
@ -50,11 +52,11 @@ export default function processCss ( parsed, code ) {
code.overwrite( start + offset, end + offset, transformed ); code.overwrite( start + offset, end + offset, transformed );
}); });
rule.block.children.forEach( block => { rule.block.children.forEach( ( block: Node ) => {
if ( block.type === 'Declaration' ) { if ( block.type === 'Declaration' ) {
const property = block.property.toLowerCase(); const property = block.property.toLowerCase();
if ( property === 'animation' || property === 'animation-name' ) { if ( property === 'animation' || property === 'animation-name' ) {
block.value.children.forEach( block => { block.value.children.forEach( ( block: Node ) => {
if ( block.type === 'Identifier' ) { if ( block.type === 'Identifier' ) {
const name = block.name; const name = block.name;
if ( keyframes.has( name ) ) { if ( keyframes.has( name ) ) {
@ -67,7 +69,7 @@ export default function processCss ( parsed, code ) {
}); });
} }
function walk ( node ) { function walk ( node: Node ) {
if ( node.type === 'Rule' ) { if ( node.type === 'Rule' ) {
transform( node ); transform( node );
} else if ( node.type === 'Atrule' && node.name.toLowerCase() === 'keyframes' ) { } else if ( node.type === 'Atrule' && node.name.toLowerCase() === 'keyframes' ) {

@ -1,4 +1,9 @@
export default function getGlobals ( imports, { globals, onerror, onwarn } ) { import { Declaration, Options } from './getIntro';
export type Globals = (id: string) => any;
export default function getGlobals ( imports: Declaration[], options: Options ) {
const { globals, onerror, onwarn } = options;
const globalFn = getGlobalFn( globals ); const globalFn = getGlobalFn( globals );
return imports.map( x => { return imports.map( x => {
@ -33,7 +38,7 @@ export default function getGlobals ( imports, { globals, onerror, onwarn } ) {
}); });
} }
function getGlobalFn ( globals ) { function getGlobalFn ( globals: any ): Globals {
if ( typeof globals === 'function' ) return globals; if ( typeof globals === 'function' ) return globals;
if ( typeof globals === 'object' ) { if ( typeof globals === 'object' ) {
return id => globals[ id ]; return id => globals[ id ];

@ -1,7 +1,26 @@
import deindent from '../../../utils/deindent.js'; import deindent from '../../../utils/deindent.js';
import getGlobals from './getGlobals.js'; import getGlobals, { Globals } from './getGlobals';
export type ModuleFormat = "es" | "amd" | "cjs" | "iife" | "umd" | "eval";
export interface Options {
name: string;
amd?: {
id?: string;
};
globals: Globals | object;
onerror: (err: Error) => void;
onwarn: (obj: Error | { message: string }) => void;
}
export interface Declaration {
name: string;
source: {
value: string;
};
}
export default function getIntro ( format, options, imports ) { export default function getIntro ( format: ModuleFormat, options: Options, imports: Declaration[] ) {
if ( format === 'es' ) return ''; if ( format === 'es' ) return '';
if ( format === 'amd' ) return getAmdIntro( options, imports ); if ( format === 'amd' ) return getAmdIntro( options, imports );
if ( format === 'cjs' ) return getCjsIntro( options, imports ); if ( format === 'cjs' ) return getCjsIntro( options, imports );
@ -12,7 +31,7 @@ export default function getIntro ( format, options, imports ) {
throw new Error( `Not implemented: ${format}` ); throw new Error( `Not implemented: ${format}` );
} }
function getAmdIntro ( options, imports ) { function getAmdIntro ( options: Options, imports: Declaration[] ) {
const sourceString = imports.length ? const sourceString = imports.length ?
`[ ${imports.map( declaration => `'${removeExtension( declaration.source.value )}'` ).join( ', ' )} ], ` : `[ ${imports.map( declaration => `'${removeExtension( declaration.source.value )}'` ).join( ', ' )} ], ` :
''; '';
@ -22,7 +41,7 @@ function getAmdIntro ( options, imports ) {
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, imports ) { function getCjsIntro ( options: Options, imports: Declaration[] ) {
const requireBlock = imports const requireBlock = imports
.map( declaration => `var ${declaration.name} = require( '${declaration.source.value}' );` ) .map( declaration => `var ${declaration.name} = require( '${declaration.source.value}' );` )
.join( '\n\n' ); .join( '\n\n' );
@ -34,7 +53,7 @@ function getCjsIntro ( options, imports ) {
return `'use strict';\n\n`; return `'use strict';\n\n`;
} }
function getIifeIntro ( options, imports ) { function getIifeIntro ( options: Options, imports: Declaration[] ) {
if ( !options.name ) { if ( !options.name ) {
throw new Error( `Missing required 'name' option for IIFE export` ); throw new Error( `Missing required 'name' option for IIFE export` );
} }
@ -42,7 +61,7 @@ function getIifeIntro ( options, imports ) {
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, imports ) { function getUmdIntro ( options: Options, imports: Declaration[] ) {
if ( !options.name ) { if ( !options.name ) {
throw new Error( `Missing required 'name' option for UMD export` ); throw new Error( `Missing required 'name' option for UMD export` );
} }
@ -61,15 +80,15 @@ function getUmdIntro ( options, imports ) {
}(this, (function (${paramString( imports )}) { 'use strict';` + '\n\n'; }(this, (function (${paramString( imports )}) { 'use strict';` + '\n\n';
} }
function getEvalIntro ( options, imports ) { function getEvalIntro ( options: Options, imports: Declaration[] ) {
return `(function (${paramString( imports )}) { 'use strict';\n\n`; return `(function (${paramString( imports )}) { 'use strict';\n\n`;
} }
function paramString ( imports ) { function paramString ( imports: Declaration[] ) {
return imports.length ? ` ${imports.map( dep => dep.name ).join( ', ' )} ` : ''; return imports.length ? ` ${imports.map( dep => dep.name ).join( ', ' )} ` : '';
} }
function removeExtension ( file ) { function removeExtension ( file: string ) {
const index = file.lastIndexOf( '.' ); const index = file.lastIndexOf( '.' );
return ~index ? file.slice( 0, index ) : file; return ~index ? file.slice( 0, index ) : file;
} }

@ -1,6 +1,6 @@
import getGlobals from './getGlobals.js'; import getGlobals from './getGlobals';
export default function getOutro ( format, name, options, imports ) { export default function getOutro ( format: string, name: string, options, imports ) {
if ( format === 'es' ) { if ( format === 'es' ) {
return `export default ${name};`; return `export default ${name};`;
} }

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

@ -1,18 +1,19 @@
import parse from './parse/index.js'; import parse from './parse/index';
import validate from './validate/index.js'; import validate from './validate/index';
import generate from './generators/dom/index.js'; import generate from './generators/dom/index';
import generateSSR from './generators/server-side-rendering/index.js'; import generateSSR from './generators/server-side-rendering/index';
import { assign } from './shared/index.js'; import { assign } from './shared/index.js';
import { version } from '../package.json'; import { version } from '../package.json';
import { Parsed, CompileOptions, Warning } from './interfaces';
function normalizeOptions ( options ) { function normalizeOptions ( options: CompileOptions ) :CompileOptions {
return assign( { return assign({
generate: 'dom', generate: 'dom',
// a filename is necessary for sourcemap generation // a filename is necessary for sourcemap generation
filename: 'SvelteComponent.html', filename: 'SvelteComponent.html',
onwarn: warning => { onwarn: ( warning: Warning ) => {
if ( warning.loc ) { if ( warning.loc ) {
console.warn( `(${warning.loc.line}:${warning.loc.column}) ${warning.message}` ); // eslint-disable-line no-console console.warn( `(${warning.loc.line}:${warning.loc.column}) ${warning.message}` ); // eslint-disable-line no-console
} else { } else {
@ -20,16 +21,16 @@ function normalizeOptions ( options ) {
} }
}, },
onerror: error => { onerror: ( error: Error ) => {
throw error; throw error;
} }
}, options ); }, options );
} }
export function compile ( source, _options ) { export function compile ( source: string, _options: CompileOptions ) {
const options = normalizeOptions( _options ); const options = normalizeOptions( _options );
let parsed; let parsed: Parsed;
try { try {
parsed = parse( source, options ); parsed = parse( source, options );
@ -47,7 +48,7 @@ export function compile ( source, _options ) {
return compiler( parsed, source, options ); return compiler( parsed, source, options );
} }
export function create ( source, _options = {} ) { export function create ( source: string, _options: CompileOptions = {} ) {
_options.format = 'eval'; _options.format = 'eval';
const compiled = compile( source, _options ); const compiled = compile( source, _options );

@ -0,0 +1,46 @@
export interface Node {
start: number;
end: number;
type: string;
[propName: string]: any;
}
export interface Parser {
readonly template: string;
readonly filename?: string;
index: number;
stack: Array<Node>;
html: Node;
css: Node;
js: Node;
metaTags: {};
}
export interface Parsed {
hash: number;
html: Node;
css: Node;
js: Node;
}
export interface Warning {
loc?: {line: number, column: number, pos: number};
message: string
filename?: string
toString: () => string
}
export interface CompileOptions {
format?: string;
name?: string;
filename?: string;
generate?: string;
dev?: boolean;
shared?: boolean | string;
onerror?: (error: Error) => void
onwarn?: (warning: Warning) => void
}

@ -1,179 +0,0 @@
import { locate } from 'locate-character';
import fragment from './state/fragment.js';
import { whitespace } from '../utils/patterns.js';
import { trimStart, trimEnd } from '../utils/trim.js';
import getCodeFrame from '../utils/getCodeFrame.js';
import hash from './utils/hash.js';
function ParseError ( message, template, index, filename ) {
const { line, column } = locate( template, index );
this.name = 'ParseError';
this.message = message;
this.frame = getCodeFrame( template, line, column );
this.loc = { line: line + 1, column };
this.pos = index;
this.filename = filename;
}
ParseError.prototype.toString = function () {
return `${this.message} (${this.loc.line}:${this.loc.column})\n${this.frame}`;
};
export default function parse ( template, options = {} ) {
if ( typeof template !== 'string' ) {
throw new TypeError( 'Template must be a string' );
}
template = template.replace( /\s+$/, '' );
const parser = {
index: 0,
template,
stack: [],
metaTags: {},
current () {
return this.stack[ this.stack.length - 1 ];
},
acornError ( err ) {
parser.error( err.message.replace( / \(\d+:\d+\)$/, '' ), err.pos );
},
error ( message, index = this.index ) {
throw new ParseError( message, this.template, index, options.filename );
},
eat ( str, required ) {
if ( this.match( str ) ) {
this.index += str.length;
return true;
}
if ( required ) {
this.error( `Expected ${str}` );
}
},
match ( str ) {
return this.template.slice( this.index, this.index + str.length ) === str;
},
allowWhitespace () {
while ( this.index < this.template.length && whitespace.test( this.template[ this.index ] ) ) {
this.index++;
}
},
read ( pattern ) {
const match = pattern.exec( this.template.slice( this.index ) );
if ( !match || match.index !== 0 ) return null;
parser.index += match[0].length;
return match[0];
},
readUntil ( pattern ) {
if ( this.index >= this.template.length ) parser.error( 'Unexpected end of input' );
const start = this.index;
const match = pattern.exec( this.template.slice( start ) );
if ( match ) {
const start = this.index;
this.index = start + match.index;
return this.template.slice( start, this.index );
}
this.index = this.template.length;
return this.template.slice( start );
},
remaining () {
return this.template.slice( this.index );
},
requireWhitespace () {
if ( !whitespace.test( this.template[ this.index ] ) ) {
this.error( `Expected whitespace` );
}
this.allowWhitespace();
},
html: {
start: null,
end: null,
type: 'Fragment',
children: []
},
css: null,
js: null
};
parser.stack.push( parser.html );
let state = fragment;
while ( parser.index < parser.template.length ) {
state = state( parser ) || fragment;
}
if ( parser.stack.length > 1 ) {
const current = parser.current();
const type = current.type === 'Element' ? `<${current.name}>` : 'Block';
parser.error( `${type} was left open`, current.start );
}
if ( state !== fragment ) {
parser.error( 'Unexpected end of input' );
}
// trim unnecessary whitespace
while ( parser.html.children.length ) {
const firstChild = parser.html.children[0];
parser.html.start = firstChild.start;
if ( firstChild.type !== 'Text' ) break;
const length = firstChild.data.length;
firstChild.data = trimStart( firstChild.data );
if ( firstChild.data === '' ) {
parser.html.children.shift();
} else {
parser.html.start += length - firstChild.data.length;
break;
}
}
while ( parser.html.children.length ) {
const lastChild = parser.html.children[ parser.html.children.length - 1 ];
parser.html.end = lastChild.end;
if ( lastChild.type !== 'Text' ) break;
const length = lastChild.data.length;
lastChild.data = trimEnd( lastChild.data );
if ( lastChild.data === '' ) {
parser.html.children.pop();
} else {
parser.html.end -= length - lastChild.data.length;
break;
}
}
return {
hash: hash( template ),
html: parser.html,
css: parser.css,
js: parser.js
};
}

@ -0,0 +1,190 @@
import { locate, Location } from 'locate-character';
import fragment from './state/fragment';
import { whitespace } from '../utils/patterns';
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'
class ParseError extends CompileError {
constructor ( message: string, template: string, index: number, filename: string ) {
super( message, template, index, filename );
this.name = 'ParseError';
}
}
interface ParserOptions {
filename?: string
}
export class Parser {
readonly template: string;
readonly filename?: string;
index: number;
stack: Array<Node>;
html: Node;
css: Node;
js: Node;
metaTags: {}
constructor ( template: string, options: ParserOptions ) {
if ( typeof template !== 'string' ) {
throw new TypeError( 'Template must be a string' );
}
this.template = template.replace( /\s+$/, '' );
this.filename = options.filename;
this.index = 0;
this.stack = [];
this.metaTags = {};
this.html = {
start: null,
end: null,
type: 'Fragment',
children: []
};
this.css = null;
this.js = null;
this.stack.push( this.html );
let state = fragment;
while ( this.index < this.template.length ) {
state = state( this ) || fragment;
}
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 );
}
if ( state !== fragment ) {
this.error( 'Unexpected end of input' );
}
// trim unnecessary whitespace
while ( this.html.children.length ) {
const firstChild = this.html.children[0];
this.html.start = firstChild.start;
if ( firstChild.type !== 'Text' ) break;
const length = firstChild.data.length;
firstChild.data = trimStart( firstChild.data );
if ( firstChild.data === '' ) {
this.html.children.shift();
} else {
this.html.start += length - firstChild.data.length;
break;
}
}
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;
const length = lastChild.data.length;
lastChild.data = trimEnd( lastChild.data );
if ( lastChild.data === '' ) {
this.html.children.pop();
} else {
this.html.end -= length - lastChild.data.length;
break;
}
}
}
current () {
return this.stack[ this.stack.length - 1 ];
}
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 );
}
eat ( str: string, required?: boolean ) {
if ( this.match( str ) ) {
this.index += str.length;
return true;
}
if ( required ) {
this.error( `Expected ${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 ] ) ) {
this.index++;
}
}
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' );
const start = this.index;
const match = pattern.exec( this.template.slice( start ) );
if ( match ) {
const start = this.index;
this.index = start + match.index;
return this.template.slice( start, this.index );
}
this.index = this.template.length;
return this.template.slice( start );
}
remaining () {
return this.template.slice( this.index );
}
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 );
return {
hash: hash( parser.template ),
html: parser.html,
css: parser.css,
js: parser.js
};
}

@ -1,7 +1,8 @@
import { parseExpressionAt } from 'acorn'; import { parseExpressionAt } from 'acorn';
import spaces from '../../utils/spaces.js'; import spaces from '../../utils/spaces.js';
import { Parser } from '../index';
function readExpression ( parser, start, quoteMark ) { function readExpression ( parser: Parser, start: number, quoteMark ) {
let str = ''; let str = '';
let escaped = false; let escaped = false;
@ -43,7 +44,7 @@ function readExpression ( parser, start, quoteMark ) {
return expression; return expression;
} }
export function readEventHandlerDirective ( parser, start, name ) { export function readEventHandlerDirective ( parser: Parser, start: number, name: string ) {
const quoteMark = ( const quoteMark = (
parser.eat( `'` ) ? `'` : parser.eat( `'` ) ? `'` :
parser.eat( `"` ) ? `"` : parser.eat( `"` ) ? `"` :
@ -67,7 +68,7 @@ export function readEventHandlerDirective ( parser, start, name ) {
}; };
} }
export function readBindingDirective ( parser, start, name ) { export function readBindingDirective ( parser: Parser, start: number, name: string ) {
let value; let value;
if ( parser.eat( '=' ) ) { if ( parser.eat( '=' ) ) {
@ -130,7 +131,7 @@ export function readBindingDirective ( parser, start, name ) {
}; };
} }
export function readTransitionDirective ( parser, start, name, type ) { export function readTransitionDirective ( parser: Parser, start: number, name: string, type: string ) {
let expression = null; let expression = null;
if ( parser.eat( '=' ) ) { if ( parser.eat( '=' ) ) {

@ -1,4 +1,5 @@
import { parseExpressionAt } from 'acorn'; import { parseExpressionAt } from 'acorn';
import { Parser } from '../index';
const literals = new Map([ const literals = new Map([
[ 'true', true ], [ 'true', true ],
@ -6,7 +7,7 @@ const literals = new Map([
[ 'null', null ] [ 'null', null ]
]); ]);
export default function readExpression ( parser ) { export default function readExpression ( parser: Parser ) {
const start = parser.index; const start = parser.index;
const name = parser.readUntil( /\s*}}/ ); const name = parser.readUntil( /\s*}}/ );

@ -1,9 +1,10 @@
import { parse } from 'acorn'; import { parse } from 'acorn';
import spaces from '../../utils/spaces.js'; import spaces from '../../utils/spaces.js';
import { Parser } from '../index';
const scriptClosingTag = '<\/script>'; const scriptClosingTag = '<\/script>';
export default function readScript ( parser, start, attributes ) { export default function readScript ( parser: Parser, start: number, attributes ) {
const scriptStart = parser.index; const scriptStart = parser.index;
const scriptEnd = parser.template.indexOf( scriptClosingTag, scriptStart ); const scriptEnd = parser.template.indexOf( scriptClosingTag, scriptStart );

@ -1,7 +1,8 @@
import parse from 'css-tree/lib/parser/index.js'; import parse from 'css-tree/lib/parser/index.js';
import walk from 'css-tree/lib/utils/walk.js'; import walk from 'css-tree/lib/utils/walk.js';
import { Parser } from '../index';
export default function readStyle ( parser, start, attributes ) { export default function readStyle ( parser: Parser, start: number, attributes ) {
const contentStart = parser.index; const contentStart = parser.index;
const styles = parser.readUntil( /<\/style>/ ); const styles = parser.readUntil( /<\/style>/ );
const contentEnd = parser.index; const contentEnd = parser.index;

@ -1,15 +0,0 @@
import tag from './tag.js';
import mustache from './mustache.js';
import text from './text.js';
export default function fragment ( parser ) {
if ( parser.match( '<' ) ) {
return tag;
}
if ( parser.match( '{{' ) ) {
return mustache;
}
return text;
}

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

@ -1,6 +1,8 @@
import readExpression from '../read/expression.js'; import readExpression from '../read/expression';
import { whitespace } from '../../utils/patterns.js'; import { whitespace } from '../../utils/patterns';
import { trimStart, trimEnd } from '../../utils/trim.js'; import { trimStart, trimEnd } from '../../utils/trim';
import { Parser } from '../index';
import { Node } from '../../interfaces';
const validIdentifier = /[a-zA-Z_$][a-zA-Z0-9_$]*/; const validIdentifier = /[a-zA-Z_$][a-zA-Z0-9_$]*/;
@ -27,7 +29,7 @@ function trimWhitespace ( block, trimBefore, trimAfter ) {
} }
} }
export default function mustache ( parser ) { export default function mustache ( parser: Parser ) {
const start = parser.index; const start = parser.index;
parser.index += 2; parser.index += 2;
@ -145,7 +147,7 @@ export default function mustache ( parser ) {
const expression = readExpression( parser ); const expression = readExpression( parser );
const block = { const block: Node = {
start, start,
end: null, end: null,
type, type,

@ -1,10 +1,12 @@
import readExpression from '../read/expression.js'; import readExpression from '../read/expression';
import readScript from '../read/script.js'; import readScript from '../read/script';
import readStyle from '../read/style.js'; import readStyle from '../read/style';
import { readEventHandlerDirective, readBindingDirective, readTransitionDirective } from '../read/directives.js'; import { readEventHandlerDirective, readBindingDirective, readTransitionDirective } from '../read/directives';
import { trimStart, trimEnd } from '../../utils/trim.js'; import { trimStart, trimEnd } from '../../utils/trim';
import { decodeCharacterReferences } from '../utils/html.js'; import { decodeCharacterReferences } from '../utils/html';
import isVoidElementName from '../../utils/isVoidElementName.js'; import isVoidElementName from '../../utils/isVoidElementName';
import { Parser } from '../index';
import { Node } from '../../interfaces';
const validTagName = /^\!?[a-zA-Z]{1,}:?[a-zA-Z0-9\-]*/; const validTagName = /^\!?[a-zA-Z]{1,}:?[a-zA-Z0-9\-]*/;
const invalidUnquotedAttributeCharacters = /[\s"'=<>\/`]/; const invalidUnquotedAttributeCharacters = /[\s"'=<>\/`]/;
@ -61,7 +63,7 @@ function stripWhitespace ( element ) {
} }
} }
export default function tag ( parser ) { export default function tag ( parser: Parser ) {
const start = parser.index++; const start = parser.index++;
let parent = parser.current(); let parent = parser.current();
@ -162,7 +164,7 @@ export default function tag ( parser ) {
return; return;
} }
const element = { const element: Node = {
start, start,
end: null, // filled in later end: null, // filled in later
type: 'Element', type: 'Element',
@ -187,7 +189,7 @@ export default function tag ( parser ) {
return null; return null;
} }
function readTagName ( parser ) { function readTagName ( parser: Parser ) {
const start = parser.index; const start = parser.index;
if ( parser.eat( SELF ) ) { if ( parser.eat( SELF ) ) {
@ -222,7 +224,7 @@ function readTagName ( parser ) {
return name; return name;
} }
function readAttribute ( parser, uniqueNames ) { function readAttribute ( parser: Parser, uniqueNames ) {
const start = parser.index; const start = parser.index;
let name = parser.readUntil( /(\s|=|\/|>)/ ); let name = parser.readUntil( /(\s|=|\/|>)/ );
@ -277,13 +279,13 @@ function readAttribute ( parser, uniqueNames ) {
}; };
} }
function readAttributeValue ( parser ) { function readAttributeValue ( parser: Parser ) {
let quoteMark; let quoteMark;
if ( parser.eat( `'` ) ) quoteMark = `'`; if ( parser.eat( `'` ) ) quoteMark = `'`;
if ( parser.eat( `"` ) ) quoteMark = `"`; if ( parser.eat( `"` ) ) quoteMark = `"`;
let currentChunk = { let currentChunk: Node = {
start: parser.index, start: parser.index,
end: null, end: null,
type: 'Text', type: 'Text',
@ -347,7 +349,7 @@ function readAttributeValue ( parser ) {
parser.error( `Unexpected end of input` ); parser.error( `Unexpected end of input` );
} }
function getShorthandValue ( start, name ) { function getShorthandValue ( start: number, name: string ) {
const end = start + name.length; const end = start + name.length;
return [{ return [{

@ -1,6 +1,7 @@
import { decodeCharacterReferences } from '../utils/html.js'; import { decodeCharacterReferences } from '../utils/html';
import { Parser } from '../index';
export default function text ( parser ) { export default function text ( parser: Parser ) {
const start = parser.index; const start = parser.index;
let data = ''; let data = '';

@ -1,5 +1,5 @@
// https://github.com/darkskyapp/string-hash/blob/master/index.js // https://github.com/darkskyapp/string-hash/blob/master/index.js
export default function hash ( str ) { export default function hash ( str: string ) :number {
let hash = 5381; let hash = 5381;
let i = str.length; let i = str.length;

@ -1,9 +1,9 @@
import htmlEntities from './entities.js'; import htmlEntities from './entities';
const windows1252 = [ 8364, 129, 8218, 402, 8222, 8230, 8224, 8225, 710, 8240, 352, 8249, 338, 141, 381, 143, 144, 8216, 8217, 8220, 8221, 8226, 8211, 8212, 732, 8482, 353, 8250, 339, 157, 382, 376 ]; const windows1252 = [ 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 entityPattern = new RegExp( `&(#?(?:x[\\w\\d]+|\\d+|${Object.keys( htmlEntities ).join( '|' )}));?`, 'g' );
export function decodeCharacterReferences ( html ) { export function decodeCharacterReferences ( html: string ) {
return html.replace( entityPattern, ( match, entity ) => { return html.replace( entityPattern, ( match, entity ) => {
let code; let code;
@ -31,7 +31,7 @@ const NUL = 0;
// to replace them ourselves // to replace them ourselves
// //
// Source: http://en.wikipedia.org/wiki/Character_encodings_in_HTML#Illegal_characters // Source: http://en.wikipedia.org/wiki/Character_encodings_in_HTML#Illegal_characters
function validateCode ( code ) { function validateCode ( code: number ) {
// line feed becomes generic whitespace // line feed becomes generic whitespace
if ( code === 10 ) { if ( code === 10 ) {
return 32; return 32;

@ -1,6 +1,6 @@
import * as fs from 'fs'; import * as fs from 'fs';
import * as path from 'path'; import * as path from 'path';
import { compile } from '../index.js'; import { compile } from '../index.ts';
function capitalise ( name ) { function capitalise ( name ) {
return name[0].toUpperCase() + name.slice( 1 ); return name[0].toUpperCase() + name.slice( 1 );

@ -31,5 +31,5 @@ fs.readdirSync( __dirname ).forEach( file => {
}); });
}); });
fs.writeFileSync( 'src/generators/dom/shared.js', `// this file is auto-generated, do not edit it fs.writeFileSync( 'src/generators/dom/shared.ts', `// this file is auto-generated, do not edit it
export default ${JSON.stringify( declarations, null, '\t' )};` ); export default ${JSON.stringify( declarations, null, '\t' )};` );

@ -18,7 +18,7 @@ export function generateKeyframes ( a, b, delta, duration, ease, fn, node, style
document.head.appendChild( style ); document.head.appendChild( style );
node.style.animation = node.style.animation.split( ',' ) node.style.animation = ( node.style.animation || '' ).split( ',' )
.filter( function ( anim ) { .filter( function ( anim ) {
// when introing, discard old animations if there are any // when introing, discard old animations if there are any
return anim && ( delta < 0 || !/__svelte/.test( anim ) ); return anim && ( delta < 0 || !/__svelte/.test( anim ) );
@ -104,9 +104,10 @@ export function wrapTransition ( node, fn, params, intro, outgroup ) {
export var transitionManager = { export var transitionManager = {
running: false, running: false,
transitions: [], transitions: [],
bound: null,
add: function ( transition ) { add: function ( transition ) {
transitionManager.transitions.push( transition ); this.transitions.push( transition );
if ( !this.running ) { if ( !this.running ) {
this.running = true; this.running = true;
@ -115,13 +116,13 @@ export var transitionManager = {
}, },
next: function () { next: function () {
transitionManager.running = false; this.running = false;
var now = window.performance.now(); var now = window.performance.now();
var i = transitionManager.transitions.length; var i = this.transitions.length;
while ( i-- ) { while ( i-- ) {
var transition = transitionManager.transitions[i]; var transition = this.transitions[i];
if ( transition.program && now >= transition.program.end ) { if ( transition.program && now >= transition.program.end ) {
transition.done(); transition.done();
@ -133,14 +134,14 @@ export var transitionManager = {
if ( transition.running ) { if ( transition.running ) {
transition.update( now ); transition.update( now );
transitionManager.running = true; this.running = true;
} else if ( !transition.pending ) { } else if ( !transition.pending ) {
transitionManager.transitions.splice( i, 1 ); this.transitions.splice( i, 1 );
} }
} }
if ( transitionManager.running ) { if ( this.running ) {
requestAnimationFrame( transitionManager.next ); requestAnimationFrame( this.bound || ( this.bound = this.next.bind( this ) ) );
} }
} }
}; };

@ -1,18 +1,25 @@
const LINE = {}; enum ChunkType {
const BLOCK = {}; Line,
Block
}
export default class CodeBuilder { export default class CodeBuilder {
result: string
first: ChunkType
last: ChunkType
lastCondition: string
constructor ( str = '' ) { constructor ( str = '' ) {
this.result = str; this.result = str;
const initial = str ? ( /\n/.test( str ) ? BLOCK : LINE ) : null; const initial = str ? ( /\n/.test( str ) ? ChunkType.Block : ChunkType.Line ) : null;
this.first = initial; this.first = initial;
this.last = initial; this.last = initial;
this.lastCondition = null; this.lastCondition = null;
} }
addConditionalLine ( condition, line ) { addConditionalLine ( condition: string, line: string ) {
if ( condition === this.lastCondition ) { if ( condition === this.lastCondition ) {
this.result += `\n\t${line}`; this.result += `\n\t${line}`;
} else { } else {
@ -24,41 +31,41 @@ export default class CodeBuilder {
this.lastCondition = condition; this.lastCondition = condition;
} }
this.last = BLOCK; this.last = ChunkType.Block;
} }
addLine ( line ) { addLine ( line: string ) {
if ( this.lastCondition ) { if ( this.lastCondition ) {
this.result += `\n}`; this.result += `\n}`;
this.lastCondition = null; this.lastCondition = null;
} }
if ( this.last === BLOCK ) { if ( this.last === ChunkType.Block ) {
this.result += `\n\n${line}`; this.result += `\n\n${line}`;
} else if ( this.last === LINE ) { } else if ( this.last === ChunkType.Line ) {
this.result += `\n${line}`; this.result += `\n${line}`;
} else { } else {
this.result += line; this.result += line;
} }
this.last = LINE; this.last = ChunkType.Line;
if ( !this.first ) this.first = LINE; if ( !this.first ) this.first = ChunkType.Line;
} }
addLineAtStart ( line ) { addLineAtStart ( line: string ) {
if ( this.first === BLOCK ) { if ( this.first === ChunkType.Block ) {
this.result = `${line}\n\n${this.result}`; this.result = `${line}\n\n${this.result}`;
} else if ( this.first === LINE ) { } else if ( this.first === ChunkType.Line ) {
this.result = `${line}\n${this.result}`; this.result = `${line}\n${this.result}`;
} else { } else {
this.result += line; this.result += line;
} }
this.first = LINE; this.first = ChunkType.Line;
if ( !this.last ) this.last = LINE; if ( !this.last ) this.last = ChunkType.Line;
} }
addBlock ( block ) { addBlock ( block: string ) {
if ( this.lastCondition ) { if ( this.lastCondition ) {
this.result += `\n}`; this.result += `\n}`;
this.lastCondition = null; this.lastCondition = null;
@ -70,19 +77,19 @@ export default class CodeBuilder {
this.result += block; this.result += block;
} }
this.last = BLOCK; this.last = ChunkType.Block;
if ( !this.first ) this.first = BLOCK; if ( !this.first ) this.first = ChunkType.Block;
} }
addBlockAtStart ( block ) { addBlockAtStart ( block: string ) {
if ( this.result ) { if ( this.result ) {
this.result = `${block}\n\n${this.result}`; this.result = `${block}\n\n${this.result}`;
} else { } else {
this.result += block; this.result += block;
} }
this.first = BLOCK; this.first = ChunkType.Block;
if ( !this.last ) this.last = BLOCK; if ( !this.last ) this.last = ChunkType.Block;
} }
isEmpty () { isEmpty () {

@ -0,0 +1,25 @@
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
constructor ( message: string, template: string, index: number, filename: string ) {
super( message );
const { line, column } = locate( template, index );
this.loc = { line: line + 1, column };
this.pos = index;
this.filename = filename;
this.frame = getCodeFrame( template, line, column );
}
toString () {
return `${this.message} (${this.loc.line}:${this.loc.column})\n${this.frame}`;
}
}

@ -1,6 +1,6 @@
import * as assert from 'assert'; import * as assert from 'assert';
import deindent from './deindent.js'; import deindent from './deindent.js';
import CodeBuilder from './CodeBuilder.js'; import CodeBuilder from './CodeBuilder';
describe( 'deindent', () => { describe( 'deindent', () => {
it( 'deindents a simple string', () => { it( 'deindents a simple string', () => {

@ -1,10 +1,11 @@
import { walk } from 'estree-walker'; import { walk } from 'estree-walker';
import { Node } from '../interfaces';
export default function annotateWithScopes ( expression ) { export default function annotateWithScopes ( expression: Node ) {
let scope = new Scope( null, false ); let scope = new Scope( null, false );
walk( expression, { walk( expression, {
enter ( node ) { enter ( node: Node ) {
if ( /Function/.test( node.type ) ) { if ( /Function/.test( node.type ) ) {
if ( node.type === 'FunctionDeclaration' ) { if ( node.type === 'FunctionDeclaration' ) {
scope.declarations.add( node.id.name ); scope.declarations.add( node.id.name );
@ -13,7 +14,7 @@ export default function annotateWithScopes ( expression ) {
if ( node.id ) scope.declarations.add( node.id.name ); if ( node.id ) scope.declarations.add( node.id.name );
} }
node.params.forEach( param => { node.params.forEach( ( param: Node ) => {
extractNames( param ).forEach( name => { extractNames( param ).forEach( name => {
scope.declarations.add( name ); scope.declarations.add( name );
}); });
@ -33,7 +34,7 @@ export default function annotateWithScopes ( expression ) {
} }
}, },
leave ( node ) { leave ( node: Node ) {
if ( node._scope ) { if ( node._scope ) {
scope = scope.parent; scope = scope.parent;
} }
@ -44,17 +45,21 @@ export default function annotateWithScopes ( expression ) {
} }
class Scope { class Scope {
constructor ( parent, block ) { parent: Scope
block: boolean
declarations: Set<string>
constructor ( parent: Scope, block: boolean ) {
this.parent = parent; this.parent = parent;
this.block = block; this.block = block;
this.declarations = new Set(); this.declarations = new Set();
} }
addDeclaration ( node ) { addDeclaration ( node: Node ) {
if ( node.kind === 'var' && !this.block && this.parent ) { if ( node.kind === 'var' && !this.block && this.parent ) {
this.parent.addDeclaration( node ); this.parent.addDeclaration( node );
} else if ( node.type === 'VariableDeclaration' ) { } else if ( node.type === 'VariableDeclaration' ) {
node.declarations.forEach( declarator => { node.declarations.forEach( ( declarator: Node ) => {
extractNames( declarator.id ).forEach( name => { extractNames( declarator.id ).forEach( name => {
this.declarations.add( name ); this.declarations.add( name );
}); });
@ -64,39 +69,39 @@ class Scope {
} }
} }
has ( name ) { has ( name: string ) :boolean {
return this.declarations.has( name ) || this.parent && this.parent.has( name ); return this.declarations.has( name ) || this.parent && this.parent.has( name );
} }
} }
function extractNames ( param ) { function extractNames ( param: Node ) {
const names = []; const names: string[] = [];
extractors[ param.type ]( names, param ); extractors[ param.type ]( names, param );
return names; return names;
} }
const extractors = { const extractors = {
Identifier ( names, param ) { Identifier ( names: string[], param: Node ) {
names.push( param.name ); names.push( param.name );
}, },
ObjectPattern ( names, param ) { ObjectPattern ( names: string[], param: Node ) {
param.properties.forEach( prop => { param.properties.forEach( ( prop: Node ) => {
extractors[ prop.value.type ]( names, prop.value ); extractors[ prop.value.type ]( names, prop.value );
}); });
}, },
ArrayPattern ( names, param ) { ArrayPattern ( names: string[], param: Node ) {
param.elements.forEach( element => { param.elements.forEach( ( element: Node ) => {
if ( element ) extractors[ element.type ]( names, element ); if ( element ) extractors[ element.type ]( names, element );
}); });
}, },
RestElement ( names, param ) { RestElement ( names: string[], param: Node ) {
extractors[ param.argument.type ]( names, param.argument ); extractors[ param.argument.type ]( names, param.argument );
}, },
AssignmentPattern ( names, param ) { AssignmentPattern ( names: string[], param: Node ) {
extractors[ param.left.type ]( names, param.left ); extractors[ param.left.type ]( names, param.left );
} }
}; };

@ -1,4 +1,6 @@
export default function flatten ( node ) { import { Node } from '../interfaces';
export default function flatten ( node: Node ) {
const parts = []; const parts = [];
const propEnd = node.end; const propEnd = node.end;

@ -1,10 +1,10 @@
import spaces from './spaces.js'; import spaces from './spaces.js';
function tabsToSpaces ( str ) { function tabsToSpaces ( str: string ) {
return str.replace( /^\t+/, match => match.split( '\t' ).join( ' ' ) ); return str.replace( /^\t+/, match => match.split( '\t' ).join( ' ' ) );
} }
export default function getCodeFrame ( source, line, column ) { export default function getCodeFrame ( source: string, line: number, column: number ) {
const lines = source.split( '\n' ); const lines = source.split( '\n' );
const frameStart = Math.max( 0, line - 2 ); const frameStart = Math.max( 0, line - 2 );

@ -1,4 +1,6 @@
export default function isReference ( node, parent ) { import { Node } from '../interfaces';
export default function isReference ( node: Node, parent: Node ): boolean {
if ( node.type === 'MemberExpression' ) { if ( node.type === 'MemberExpression' ) {
return !node.computed && isReference( node.object, node ); return !node.computed && isReference( node.object, node );
} }

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

@ -1,3 +1,5 @@
import { Node } from '../interfaces';
const keys = { const keys = {
ObjectExpression: 'properties', ObjectExpression: 'properties',
Program: 'body' Program: 'body'
@ -8,7 +10,7 @@ const offsets = {
Program: [ 0, 0 ] Program: [ 0, 0 ]
}; };
export function removeNode ( code, parent, node ) { export function removeNode ( code, parent: Node, node: Node ) {
const key = keys[ parent.type ]; const key = keys[ parent.type ];
const offset = offsets[ parent.type ]; const offset = offsets[ parent.type ];
if ( !key || !offset ) throw new Error( `not implemented: ${parent.type}` ); if ( !key || !offset ) throw new Error( `not implemented: ${parent.type}` );

@ -1,44 +0,0 @@
import deindent from './deindent.js';
export default function toSource ( thing ) {
if ( typeof thing === 'function' ) {
return normaliseIndentation( thing.toString() );
}
if ( Array.isArray( thing ) ) {
if ( thing.length === 0 ) return '[]';
throw new Error( 'TODO' ); // not currently needed
}
if ( thing && typeof thing === 'object' ) {
const keys = Object.keys( thing );
if ( keys.length === 0 ) return '{}';
const props = keys.map( key => `${key}: ${toSource( thing[ key ] )}` ).join( ',\n' );
return deindent`
{
${props}
}
`;
}
return JSON.stringify( thing );
}
function normaliseIndentation ( str ) {
const lines = str.split( '\n' ).slice( 1, -1 );
let minIndentation = Infinity;
lines.forEach( line => {
if ( !/\S/.test( line ) ) return;
const indentation = /^\t*/.exec( line )[0].length;
if ( indentation < minIndentation ) minIndentation = indentation;
});
if ( minIndentation !== Infinity && minIndentation !== 1 ) {
const pattern = new RegExp( `^\\t{${minIndentation - 1}}`, 'gm' );
return str.replace( pattern, '' );
}
return str;
}

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

@ -1,6 +1,8 @@
import * as namespaces from '../../utils/namespaces.js'; import * as namespaces from '../../utils/namespaces';
import validateElement from './validateElement.js'; import validateElement from './validateElement';
import validateWindow from './validateWindow.js'; import validateWindow from './validateWindow';
import { Validator } from '../index';
import { Node } from '../../interfaces';
const svg = /^(?:altGlyph|altGlyphDef|altGlyphItem|animate|animateColor|animateMotion|animateTransform|circle|clipPath|color-profile|cursor|defs|desc|discard|ellipse|feBlend|feColorMatrix|feComponentTransfer|feComposite|feConvolveMatrix|feDiffuseLighting|feDisplacementMap|feDistantLight|feDropShadow|feFlood|feFuncA|feFuncB|feFuncG|feFuncR|feGaussianBlur|feImage|feMerge|feMergeNode|feMorphology|feOffset|fePointLight|feSpecularLighting|feSpotLight|feTile|feTurbulence|filter|font|font-face|font-face-format|font-face-name|font-face-src|font-face-uri|foreignObject|g|glyph|glyphRef|hatch|hatchpath|hkern|image|line|linearGradient|marker|mask|mesh|meshgradient|meshpatch|meshrow|metadata|missing-glyph|mpath|path|pattern|polygon|polyline|radialGradient|rect|set|solidcolor|stop|switch|symbol|text|textPath|title|tref|tspan|unknown|use|view|vkern)$/; const svg = /^(?:altGlyph|altGlyphDef|altGlyphItem|animate|animateColor|animateMotion|animateTransform|circle|clipPath|color-profile|cursor|defs|desc|discard|ellipse|feBlend|feColorMatrix|feComponentTransfer|feComposite|feConvolveMatrix|feDiffuseLighting|feDisplacementMap|feDistantLight|feDropShadow|feFlood|feFuncA|feFuncB|feFuncG|feFuncR|feGaussianBlur|feImage|feMerge|feMergeNode|feMorphology|feOffset|fePointLight|feSpecularLighting|feSpotLight|feTile|feTurbulence|filter|font|font-face|font-face-format|font-face-name|font-face-src|font-face-uri|foreignObject|g|glyph|glyphRef|hatch|hatchpath|hkern|image|line|linearGradient|marker|mask|mesh|meshgradient|meshpatch|meshrow|metadata|missing-glyph|mpath|path|pattern|polygon|polyline|radialGradient|rect|set|solidcolor|stop|switch|symbol|text|textPath|title|tref|tspan|unknown|use|view|vkern)$/;
@ -8,10 +10,10 @@ const meta = new Map([
[ ':Window', validateWindow ] [ ':Window', validateWindow ]
]); ]);
export default function validateHtml ( validator, html ) { export default function validateHtml ( validator: Validator, html: Node ) {
let elementDepth = 0; let elementDepth = 0;
function visit ( node ) { function visit ( node: Node ) {
if ( node.type === 'Element' ) { if ( node.type === 'Element' ) {
if ( elementDepth === 0 && validator.namespace !== namespaces.svg && svg.test( node.name ) ) { if ( 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 ); validator.warn( `<${node.name}> is an SVG element did you forget to add { namespace: 'svg' } ?`, node.start );

@ -1,13 +1,15 @@
import validateEventHandler from './validateEventHandler.js'; import validateEventHandler from './validateEventHandler';
import { Validator } from '../index';
import { Node } from '../../interfaces';
export default function validateElement ( validator, node ) { export default function validateElement ( validator: Validator, node: Node ) {
const isComponent = node.name === ':Self' || validator.components.has( node.name ); const isComponent = node.name === ':Self' || validator.components.has( node.name );
let hasIntro; let hasIntro: boolean;
let hasOutro; let hasOutro: boolean;
let hasTransition; let hasTransition: boolean;
node.attributes.forEach( attribute => { node.attributes.forEach( ( attribute: Node ) => {
if ( !isComponent && attribute.type === 'Binding' ) { if ( !isComponent && attribute.type === 'Binding' ) {
const { name } = attribute; const { name } = attribute;
@ -35,13 +37,13 @@ export default function validateElement ( validator, node ) {
const type = getType( validator, node ); const type = getType( validator, node );
if ( type !== 'checkbox' && type !== 'radio' ) { if ( type !== 'checkbox' && type !== 'radio' ) {
validator.error( `'checked' binding can only be used with <input type="checkbox"> or <input type="radio">` ); 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' ) { else if ( name === 'currentTime' || name === 'duration' || name === 'paused' ) {
if ( node.name !== 'audio' && node.name !== 'video' ) { if ( node.name !== 'audio' && node.name !== 'video' ) {
validator.error( `'${name}' binding can only be used with <audio> or <video>` ); validator.error( `'${name}' binding can only be used with <audio> or <video>`, attribute.start );
} }
} }
@ -78,8 +80,8 @@ export default function validateElement ( validator, node ) {
}); });
} }
function getType ( validator, node ) { function getType ( validator: Validator, node: Node ) {
const attribute = node.attributes.find( attribute => attribute.name === 'type' ); const attribute = node.attributes.find( ( attribute: Node ) => attribute.name === 'type' );
if ( !attribute ) return null; if ( !attribute ) return null;
if ( attribute.value === true ) { if ( attribute.value === true ) {

@ -1,5 +1,7 @@
import flattenReference from '../../utils/flattenReference.js'; import flattenReference from '../../utils/flattenReference';
import list from '../utils/list.js'; import list from '../utils/list';
import { Validator } from '../index';
import { Node } from '../../interfaces';
const validBuiltins = new Set([ const validBuiltins = new Set([
'set', 'set',
@ -7,7 +9,7 @@ const validBuiltins = new Set([
'destroy' 'destroy'
]); ]);
export default function validateEventHandlerCallee ( validator, attribute ) { export default function validateEventHandlerCallee ( validator: Validator, attribute: Node ) {
const { callee, start, type } = attribute.expression; const { callee, start, type } = attribute.expression;
if ( type !== 'CallExpression' ) { if ( type !== 'CallExpression' ) {
@ -31,5 +33,5 @@ export default function validateEventHandlerCallee ( validator, attribute ) {
message += `. '${callee.name}' exists on 'helpers', did you put it in the wrong place?`; message += `. '${callee.name}' exists on 'helpers', did you put it in the wrong place?`;
} }
validator.error( message, start ); validator.warn( message, start );
} }

@ -1,7 +1,9 @@
import flattenReference from '../../utils/flattenReference.js'; import flattenReference from '../../utils/flattenReference';
import fuzzymatch from '../utils/fuzzymatch.js'; import fuzzymatch from '../utils/fuzzymatch';
import list from '../utils/list.js'; import list from '../utils/list';
import validateEventHandler from './validateEventHandler.js'; import validateEventHandler from './validateEventHandler';
import { Validator } from '../index';
import { Node } from '../../interfaces';
const validBindings = [ const validBindings = [
'innerWidth', 'innerWidth',
@ -12,8 +14,8 @@ const validBindings = [
'scrollY' 'scrollY'
]; ];
export default function validateWindow ( validator, node ) { export default function validateWindow ( validator: Validator, node: Node ) {
node.attributes.forEach( attribute => { node.attributes.forEach( ( attribute: Node ) => {
if ( attribute.type === 'Binding' ) { if ( attribute.type === 'Binding' ) {
if ( attribute.value.type !== 'Identifier' ) { if ( attribute.value.type !== 'Identifier' ) {
const { parts } = flattenReference( attribute.value ); const { parts } = flattenReference( attribute.value );

@ -1,79 +0,0 @@
import validateJs from './js/index.js';
import validateHtml from './html/index.js';
import { getLocator } from 'locate-character';
import getCodeFrame from '../utils/getCodeFrame.js';
export default function validate ( parsed, source, { onerror, onwarn, name, filename } ) {
const locator = getLocator( source );
const validator = {
error: ( message, pos ) => {
const { line, column } = locator( pos );
const error = new Error( message );
error.frame = getCodeFrame( source, line, column );
error.loc = { line: line + 1, column };
error.pos = pos;
error.filename = filename;
error.toString = () => `${error.message} (${error.loc.line}:${error.loc.column})\n${error.frame}`;
throw error;
},
warn: ( message, pos ) => {
const { line, column } = locator( pos );
const frame = getCodeFrame( source, line, column );
onwarn({
message,
frame,
loc: { line: line + 1, column },
pos,
filename,
toString: () => `${message} (${line + 1}:${column})\n${frame}`
});
},
source,
namespace: null,
defaultExport: null,
properties: {},
components: new Map(),
methods: new Map(),
helpers: new Map(),
transitions: new Map()
};
try {
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 ) ) {
const message = `options.name should be capitalised`;
onwarn({
message,
filename,
toString: () => message
});
}
if ( parsed.js ) {
validateJs( validator, parsed.js );
}
if ( parsed.html ) {
validateHtml( validator, parsed.html );
}
} catch ( err ) {
if ( onerror ) {
onerror( err );
} else {
throw err;
}
}
}

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

Loading…
Cancel
Save