Resolve merge conflicts

pull/1786/head
Paul Sauve 8 years ago
commit 6242038be5

@ -5,7 +5,6 @@
"semi": [ 2, "always" ],
"keyword-spacing": [ 2, { "before": true, "after": true } ],
"space-before-blocks": [ 2, "always" ],
"space-before-function-paren": [ 2, "always" ],
"no-mixed-spaces-and-tabs": [ 2, "smart-tabs" ],
"no-cond-assign": 0,
"no-unused-vars": 2,
@ -34,6 +33,9 @@
"plugin:import/errors",
"plugin:import/warnings"
],
"plugins": [
"html"
],
"parserOptions": {
"ecmaVersion": 6,
"sourceType": "module"

2
.gitignore vendored

@ -13,3 +13,5 @@ test/sourcemaps/samples/*/output.js
test/sourcemaps/samples/*/output.js.map
_actual.*
tmp
_actual-bundle.*
src/generators/dom/shared.ts

@ -1,5 +1,124 @@
# Svelte changelog
## 1.23.4
* Don't recreate if blocks incorrectly ([#669](https://github.com/sveltejs/svelte/pull/669))
## 1.23.3
* Pass parameters to `get_block` ([#667](https://github.com/sveltejs/svelte/issues/667))
## 1.23.2
* Fix if blocks being recreated on update ([#665](https://github.com/sveltejs/svelte/issues/665))
## 1.23.1
* Fix each-else blocks that are empty on initial render ([#662](https://github.com/sveltejs/svelte/issues/662))
## 1.23.0
* Hydration ([#649](https://github.com/sveltejs/svelte/pull/649))
* Correctly transform CSS selectors with pseudo-elements ([#658](https://github.com/sveltejs/svelte/issues/658))
## 1.22.5
* Fix nested component unmounting bug ([#643](https://github.com/sveltejs/svelte/issues/643))
## 1.22.4
* Include `ast` in `svelte.compile` return value ([#632](https://github.com/sveltejs/svelte/issues/632))
* Set initial value of `<select>` binding, if unspecified ([#639](https://github.com/sveltejs/svelte/issues/639))
* Mark indirect dependencies of `<select>` bindings (i.e. the dependencies of their `<option>` values) ([#639](https://github.com/sveltejs/svelte/issues/639))
## 1.22.3
* Fix nested component unmounting bug ([#625](https://github.com/sveltejs/svelte/issues/625))
* Allow components to have computed member expression bindings ([#624](https://github.com/sveltejs/svelte/issues/624))
* Handle empty `<style>` tags ([#634](https://github.com/sveltejs/svelte/issues/634))
* Warn on missing component ([#623](https://github.com/sveltejs/svelte/issues/623))
* Allow dynamic `type` attribute for unbound inputs ([#620](https://github.com/sveltejs/svelte/issues/620))
* Rename `addEventListener` and `removeEventListener` directives ([#621](https://github.com/sveltejs/svelte/issues/621))
## 1.22.2
* Escape template strings correctly in SSR output ([#616](https://github.com/sveltejs/svelte/issues/616))
* Prevent magic-string deprecation warning ([#617](https://github.com/sveltejs/svelte/pull/617))
## 1.22.1
* Sanitise event handler names ([#612](https://github.com/sveltejs/svelte/issues/612))
## 1.22.0
* Symmetry between `mount` and `unmount`. This is potentially a breaking change if your components import other components that were precompiled with an earlier version of Svelte ([#592](https://github.com/sveltejs/svelte/issues/592))
* Add `cascade` option, which prevents styles affecting child components if `false`, unless selectors are wrapped in `:global(...)` and keyframe declaration IDs are prefixed with `-global-`. This will become the default behaviour in v2 ([#583](https://github.com/sveltejs/svelte/issues/583))
* Support binding to computed member expressions ([#602](https://github.com/sveltejs/svelte/issues/602))
* Coerce empty string in `number`/`range` inputs to `undefined`, not `0` ([#584](https://github.com/sveltejs/svelte/issues/584))
* Fix insert location of DOM elements in each/if/nested component edge cases ([#610](https://github.com/sveltejs/svelte/issues/610))
## 1.21.0
* Always use `helpers` if referenced, not just for call expressions ([#575](https://github.com/sveltejs/svelte/issues/575))
* Fix parsing of `<textarea>` children ([#599](https://github.com/sveltejs/svelte/pull/599))
* Treat `<textarea>` value attributes and children as equivalent, and fail validation if both are present ([#599](https://github.com/sveltejs/svelte/pull/599))
* Fix `<textarea>` SSR ([#599](https://github.com/sveltejs/svelte/pull/599))
* Apply CSS transition styles immediately if transition has delay ([#574](https://github.com/sveltejs/svelte/issues/574))
* Ensure `transitionManager` is treeshakeable ([#593](https://github.com/sveltejs/svelte/issues/593))
* Fix for environments where `node.style.animation` is undefined ([#587](https://github.com/sveltejs/svelte/issues/587))
* Fix order of operations when dealing with `<select>` elements ([#590](https://github.com/sveltejs/svelte/issues/590))
* Downgrade 'invalid callee' to a warning ([#579](https://github.com/sveltejs/svelte/issues/579))
* Convert to TypeScript ([#573](https://github.com/sveltejs/svelte/pull/573))
## 1.20.2
* Fix destruction of compound if-blocks with outros ([#572](https://github.com/sveltejs/svelte/pull/572))
## 1.20.1
* Fix insertion order of `if` blocks and their anchors ([#569](https://github.com/sveltejs/svelte/issues/569))
## 1.20.0
* Faster, better updates of keyed each blocks ([#373](https://github.com/sveltejs/svelte/issues/373), [#543](https://github.com/sveltejs/svelte/issues/543))
* Use element IDs to robustly track dynamically injected `<style>` tags ([#554](https://github.com/sveltejs/svelte/issues/554))
* Abort outros before corresponding intros ([#546](https://github.com/sveltejs/svelte/issues/546))
* Generate less code for `if` blocks with `else` blocks ([#540](https://github.com/sveltejs/svelte/issues/540))
* Ensure `{{yield}}` block content is injected into the right place ([#561](https://github.com/sveltejs/svelte/issues/561))
* Simpler, more readable codegen code ([#559](https://github.com/sveltejs/svelte/pull/559))
* Validate transition directives ([#564](https://github.com/sveltejs/svelte/issues/564))
* Apply delays to bidirectional transitions ([#562](https://github.com/sveltejs/svelte/issues/562))
* Handle all valid HTML entities ([#565](https://github.com/sveltejs/svelte/pull/565))
* Fix outros on compound `if` blocks ([#565](https://github.com/sveltejs/svelte/pull/565))
* Validation for `<:Window>` tags ([#565](https://github.com/sveltejs/svelte/pull/565))
* Increased test coverage ([#565](https://github.com/sveltejs/svelte/pull/565))
## 1.19.1
* Export `generateKeyframes`, so that CSS transitions work
## 1.19.0
* Experimental support for transitions ([#7](https://github.com/sveltejs/svelte/issues/7))
* Use `querySelector(':checked')` instead of `selectedOptions` ([#539](https://github.com/sveltejs/svelte/issues/539))
* Stringify helpers before bundling them, to avoid renaming errors ([#538](https://github.com/sveltejs/svelte/issues/538))
## 1.18.2
* Parenthesize if-block conditions ([#532](https://github.com/sveltejs/svelte/issues/532))
* Fix parsing of parenthesized expressions ([#534](https://github.com/sveltejs/svelte/issues/534))
* Fix error on `bind:checked` that doesn't belong to a checkbox input ([#529](https://github.com/sveltejs/svelte/pull/529))
## 1.18.1
* Allow `destroy()` in event handlers ([#523](https://github.com/sveltejs/svelte/issues/523))
* Fix bug with `{{yield}}` blocks following elements ([#524](https://github.com/sveltejs/svelte/issues/524))
## 1.18.0
* Visit `<select>` attributes after children, to ensure options are in the right state ([#521](https://github.com/sveltejs/svelte/pull/521))
* Use sibling elements as anchors rather than creating comment nodes wherever possible ([#3](https://github.com/sveltejs/svelte/issues/3))
## 1.17.2
* Replace bad characters when creating variable names based on element names ([#516](https://github.com/sveltejs/svelte/issues/516))

@ -62,7 +62,7 @@ const { code, map } = svelte.compile( source, {
The Svelte compiler exposes the following API:
* `compile( source [, options ] ) => { code, map }` - Compile the component with the given options (see below). Returns an object containing the compiled JavaScript and a sourcemap.
* `compile( source [, options ] ) => { code, map, ast, css }` - Compile the component with the given options (see below). Returns an object containing the compiled JavaScript, a sourcemap, an AST and transformed CSS.
* `create( source [, options ] ) => function` - Compile the component and return the component itself.
* `VERSION` - The version of this copy of the Svelte compiler as a string, `'x.x.x'`.
@ -77,6 +77,7 @@ The Svelte compiler optionally takes a second argument, an object of configurati
| `name` | `string` | The name of the constructor in the compiled component. | `'SvelteComponent'` |
| `filename` | `string` | The filename to use in sourcemaps and compiler error and warning messages. | `'SvelteComponent.html'` |
| `amd`.`id` | `string` | The AMD module ID to use for the `'amd'` and `'umd'` output formats. | `undefined` |
| `cascade` | `true`, `false` | Whether to cascade all of the component's styles to child components. If `false`, only selectors wrapped in `:global(...)` and keyframe IDs beginning with `-global-` are cascaded. | `true` |
| `shared` | `true`, `false`, `string` | Whether to import various helpers from a shared external library. When you have a project with multiple components, this reduces the overall size of your JavaScript bundle, at the expense of having immediately-usable component. You can pass a string of the module path to use, or `true` will import from `'svelte/shared.js'`. | `false` |
| `dev` | `true`, `false` | Whether to enable run-time checks in the compiled component. These are helpful during development, but slow your component down. | `false` |
| `css` | `true`, `false` | Whether to include code to inject your component's styles into the DOM. | `true` |

@ -1,5 +1,5 @@
--require babel-register
--require reify
--recursive
--recursive
./**/__test__.js
test/*/index.js

@ -1 +1,2 @@
test/test.js
--bail
test/test.js

1449
package-lock.json generated

File diff suppressed because it is too large Load Diff

@ -1,6 +1,6 @@
{
"name": "svelte",
"version": "1.17.2",
"version": "1.23.4",
"description": "The magical disappearing UI framework",
"main": "compiler/svelte.js",
"files": [
@ -17,12 +17,11 @@
"precodecov": "npm run coverage",
"lint": "eslint src test/*.js",
"benchmark": "./scripts/benchmark.sh",
"build": "npm run build:main && npm run build:shared && npm run build:ssr",
"build:main": "rollup -c rollup/rollup.config.main.js",
"build:shared": "rollup -c rollup/rollup.config.shared.js",
"build:ssr": "rollup -c rollup/rollup.config.ssr.js",
"build": "node src/shared/_build.js && rollup -c",
"dev": "node src/shared/_build.js && rollup -c -w",
"pretest": "npm run build",
"prepublish": "npm run lint && npm run build"
"prepublish": "npm run build && npm run lint",
"prettier": "prettier --use-tabs --single-quote --trailing-comma es5 --write \"src/**/*.ts\""
},
"repository": {
"type": "git",
@ -41,6 +40,8 @@
},
"homepage": "https://github.com/sveltejs/svelte#README",
"devDependencies": {
"@types/mocha": "^2.2.41",
"@types/node": "^7.0.22",
"acorn": "^4.0.4",
"babel": "^6.23.0",
"babel-core": "^6.23.1",
@ -53,35 +54,43 @@
"babel-plugin-transform-es2015-spread": "^6.22.0",
"babel-preset-env": "^1.2.1",
"babel-register": "^6.23.0",
"chalk": "^1.1.3",
"codecov": "^1.0.1",
"console-group": "^0.3.2",
"css-tree": "1.0.0-alpha16",
"eslint": "^3.12.2",
"eslint-plugin-html": "^3.0.0",
"eslint-plugin-import": "^2.2.0",
"estree-walker": "^0.3.0",
"fuzzyset.js": "0.0.1",
"glob": "^7.1.1",
"jsdom": "^9.9.1",
"locate-character": "^2.0.0",
"magic-string": "^0.19.0",
"magic-string": "^0.21.1",
"mocha": "^3.2.0",
"node-resolve": "^1.3.3",
"nyc": "^10.0.0",
"prettier": "^1.4.1",
"reify": "^0.4.4",
"rollup": "^0.39.0",
"rollup": "^0.43.0",
"rollup-plugin-buble": "^0.15.0",
"rollup-plugin-commonjs": "^7.0.0",
"rollup-plugin-json": "^2.1.0",
"rollup-plugin-node-resolve": "^2.0.0",
"rollup-plugin-typescript": "^0.8.1",
"rollup-watch": "^3.2.2",
"source-map": "^0.5.6",
"source-map-support": "^0.4.8"
"source-map-support": "^0.4.8",
"typescript": "^2.3.2"
},
"nyc": {
"include": [
"src/**/*.js"
"src/**/*.js",
"shared.js"
],
"exclude": [
"src/**/__test__.js"
"src/**/__test__.js",
"src/shared/**"
]
},
"babel": {

@ -0,0 +1,74 @@
import path from 'path';
import nodeResolve from 'rollup-plugin-node-resolve';
import commonjs from 'rollup-plugin-commonjs';
import json from 'rollup-plugin-json';
import typescript from 'rollup-plugin-typescript';
import buble from 'rollup-plugin-buble';
const src = path.resolve('src');
export default [
/* compiler/svelte.js */
{
entry: 'src/index.ts',
dest: 'compiler/svelte.js',
format: 'umd',
moduleName: 'svelte',
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 }),
commonjs(),
json(),
typescript({
include: 'src/**',
exclude: 'src/shared/**',
typescript: require('typescript')
})
],
sourceMap: true
},
/* ssr/register.js */
{
entry: 'src/server-side-rendering/register.js',
dest: 'ssr/register.js',
format: 'cjs',
plugins: [
nodeResolve({ jsnext: true, module: true }),
commonjs(),
buble({
include: 'src/**',
exclude: 'src/shared/**',
target: {
node: 4
}
})
],
external: [path.resolve('src/index.ts'), 'fs', 'path'],
paths: {
[path.resolve('src/index.ts')]: '../compiler/svelte.js'
},
sourceMap: true
},
/* shared.js */
{
entry: 'src/shared/index.js',
dest: 'shared.js',
format: 'es'
}
];

@ -1,25 +0,0 @@
import nodeResolve from 'rollup-plugin-node-resolve';
import commonjs from 'rollup-plugin-commonjs';
import json from 'rollup-plugin-json';
import buble from 'rollup-plugin-buble';
export default {
entry: 'src/index.js',
moduleName: 'svelte',
targets: [
{ dest: 'compiler/svelte.js', format: 'umd' }
],
plugins: [
nodeResolve({ jsnext: true, module: true }),
commonjs(),
json(),
buble({
include: 'src/**',
exclude: 'src/shared/**',
target: {
node: 4
}
})
],
sourceMap: true
};

@ -1,5 +0,0 @@
export default {
entry: 'src/shared/index.js',
dest: 'shared.js',
format: 'es'
};

@ -1,28 +0,0 @@
import * as path from 'path';
import nodeResolve from 'rollup-plugin-node-resolve';
import commonjs from 'rollup-plugin-commonjs';
import buble from 'rollup-plugin-buble';
export default {
entry: 'src/server-side-rendering/register.js',
moduleName: 'svelte',
targets: [
{ dest: 'ssr/register.js', format: 'cjs' }
],
plugins: [
nodeResolve({ jsnext: true, module: true }),
commonjs(),
buble({
include: 'src/**',
exclude: 'src/shared/**',
target: {
node: 4
}
})
],
external: [ path.resolve( 'src/index.js' ), 'fs', 'path' ],
paths: {
[ path.resolve( 'src/index.js' ) ]: '../compiler/svelte.js'
},
sourceMap: true
};

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

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

@ -1,185 +0,0 @@
import CodeBuilder from '../../utils/CodeBuilder.js';
import deindent from '../../utils/deindent.js';
export default class Block {
constructor ( options ) {
this.generator = options.generator;
this.name = options.name;
this.key = options.key;
this.expression = options.expression;
this.context = options.context;
this.contexts = options.contexts;
this.indexes = options.indexes;
this.contextDependencies = options.contextDependencies;
this.dependencies = new Set();
this.params = options.params;
this.indexNames = options.indexNames;
this.listNames = options.listNames;
this.listName = options.listName;
this.builders = {
create: new CodeBuilder(),
mount: new CodeBuilder(),
update: new CodeBuilder(),
detach: new CodeBuilder(),
detachRaw: new CodeBuilder(),
destroy: new CodeBuilder()
};
this.aliases = new Map();
this.variables = new Map();
this.getUniqueName = this.generator.getUniqueNameMaker( options.params );
// unique names
this.component = this.getUniqueName( 'component' );
this.target = this.getUniqueName( 'target' );
this.hasUpdateMethod = false; // determined later
}
addDependencies ( dependencies ) {
dependencies.forEach( dependency => {
this.dependencies.add( dependency );
});
}
addElement ( name, renderStatement, parentNode, needsIdentifier = false ) {
const isToplevel = !parentNode;
if ( needsIdentifier || isToplevel ) {
this.builders.create.addLine(
`var ${name} = ${renderStatement};`
);
this.mount( name, parentNode );
} else {
this.builders.create.addLine( `${this.generator.helper( 'appendNode' )}( ${renderStatement}, ${parentNode} );` );
}
if ( isToplevel ) {
this.builders.detach.addLine( `${this.generator.helper( 'detachNode' )}( ${name} );` );
}
}
addVariable ( name, init ) {
if ( this.variables.has( name ) && this.variables.get( name ) !== init ) {
throw new Error( `Variable '${name}' already initialised with a different value` );
}
this.variables.set( name, init );
}
alias ( name ) {
if ( !this.aliases.has( name ) ) {
this.aliases.set( name, this.getUniqueName( name ) );
}
return this.aliases.get( name );
}
child ( options ) {
return new Block( Object.assign( {}, this, options, { parent: this } ) );
}
contextualise ( expression, context, isEventHandler ) {
return this.generator.contextualise( this, expression, context, isEventHandler );
}
createAnchor ( name, parentNode ) {
const renderStatement = `${this.generator.helper( 'createComment' )}()`;
this.addElement( name, renderStatement, parentNode, true );
}
findDependencies ( expression ) {
return this.generator.findDependencies( this.contextDependencies, this.indexes, expression );
}
mount ( name, parentNode ) {
if ( parentNode ) {
this.builders.create.addLine( `${this.generator.helper( 'appendNode' )}( ${name}, ${parentNode} );` );
} else {
this.builders.mount.addLine( `${this.generator.helper( 'insertNode' )}( ${name}, ${this.target}, anchor );` );
}
}
render () {
if ( this.variables.size ) {
const variables = Array.from( this.variables.keys() )
.map( key => {
const init = this.variables.get( key );
return init !== undefined ? `${key} = ${init}` : key;
})
.join( ', ' );
this.builders.create.addBlockAtStart( `var ${variables};` );
}
if ( this.autofocus ) {
this.builders.create.addLine( `${this.autofocus}.focus();` );
}
// minor hack we need to ensure that any {{{triples}}} are detached
// first, so we append normal detach statements to detachRaw
this.builders.detachRaw.addBlock( this.builders.detach );
if ( !this.builders.detachRaw.isEmpty() ) {
this.builders.destroy.addBlock( deindent`
if ( detach ) {
${this.builders.detachRaw}
}
` );
}
const properties = new CodeBuilder();
let localKey;
if ( this.key ) {
localKey = this.getUniqueName( 'key' );
properties.addBlock( `key: ${localKey},` );
}
if ( this.builders.mount.isEmpty() ) {
properties.addBlock( `mount: ${this.generator.helper( 'noop' )},` );
} else {
properties.addBlock( deindent`
mount: function ( ${this.target}, anchor ) {
${this.builders.mount}
},
` );
}
if ( this.hasUpdateMethod ) {
if ( this.builders.update.isEmpty() ) {
properties.addBlock( `update: ${this.generator.helper( 'noop' )},` );
} else {
properties.addBlock( deindent`
update: function ( changed, ${this.params.join( ', ' )} ) {
${this.builders.update}
},
` );
}
}
if ( this.builders.destroy.isEmpty() ) {
properties.addBlock( `destroy: ${this.generator.helper( 'noop' )}` );
} else {
properties.addBlock( deindent`
destroy: function ( detach ) {
${this.builders.destroy}
}
` );
}
return deindent`
function ${this.name} ( ${this.params.join( ', ' )}, ${this.component}${this.key ? `, ${localKey}` : ''} ) {
${this.builders.create}
return {
${properties}
};
}
`;
}
}

@ -0,0 +1,362 @@
import CodeBuilder from '../../utils/CodeBuilder';
import deindent from '../../utils/deindent';
import { DomGenerator } from './index';
import { Node } from '../../interfaces';
import shared from './shared';
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 {
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: {
init: CodeBuilder;
create: CodeBuilder;
claim: CodeBuilder;
hydrate: CodeBuilder;
mount: CodeBuilder;
intro: CodeBuilder;
update: CodeBuilder;
outro: CodeBuilder;
unmount: CodeBuilder;
detachRaw: CodeBuilder;
destroy: CodeBuilder;
};
hasIntroMethod: boolean;
hasOutroMethod: boolean;
outros: number;
aliases: Map<string, string>;
variables: Map<string, string>;
getUniqueName: (name: string) => string;
hasUpdateMethod: boolean;
autofocus: string;
constructor(options: BlockOptions) {
this.generator = options.generator;
this.name = options.name;
this.expression = options.expression;
this.context = options.context;
// for keyed each blocks
this.key = options.key;
this.first = null;
this.contexts = options.contexts;
this.indexes = options.indexes;
this.contextDependencies = options.contextDependencies;
this.dependencies = new Set();
this.params = options.params;
this.indexNames = options.indexNames;
this.listNames = options.listNames;
this.listName = options.listName;
this.builders = {
init: new CodeBuilder(),
create: new CodeBuilder(),
claim: new CodeBuilder(),
hydrate: new CodeBuilder(),
mount: new CodeBuilder(),
intro: new CodeBuilder(),
update: new CodeBuilder(),
outro: new CodeBuilder(),
unmount: new CodeBuilder(),
detachRaw: new CodeBuilder(),
destroy: new CodeBuilder(),
};
this.hasIntroMethod = false; // a block could have an intro method but not intro transitions, e.g. if a sibling block has intros
this.hasOutroMethod = false;
this.outros = 0;
this.aliases = new Map();
this.variables = new Map();
this.getUniqueName = this.generator.getUniqueNameMaker(options.params);
this.hasUpdateMethod = false; // determined later
}
addDependencies(dependencies: string[]) {
dependencies.forEach(dependency => {
this.dependencies.add(dependency);
});
}
addElement(
name: string,
renderStatement: string,
claimStatement: string,
parentNode: string,
needsIdentifier = false
) {
const isToplevel = !parentNode;
this.addVariable(name);
this.builders.create.addLine(`${name} = ${renderStatement};`);
this.builders.claim.addLine(`${name} = ${claimStatement};`);
this.mount(name, parentNode);
if (isToplevel) {
this.builders.unmount.addLine(`@detachNode( ${name} );`);
}
}
addVariable(name: string, init?: string) {
if (this.variables.has(name) && this.variables.get(name) !== init) {
throw new Error(
`Variable '${name}' already initialised with a different value`
);
}
this.variables.set(name, init);
}
alias(name: string) {
if (!this.aliases.has(name)) {
this.aliases.set(name, this.getUniqueName(name));
}
return this.aliases.get(name);
}
child(options: BlockOptions) {
return new Block(Object.assign({}, this, options, { parent: this }));
}
contextualise(expression: Node, context?: string, isEventHandler?: boolean) {
return this.generator.contextualise(
this,
expression,
context,
isEventHandler
);
}
findDependencies(expression: Node) {
return this.generator.findDependencies(
this.contextDependencies,
this.indexes,
expression
);
}
mount(name: string, parentNode: string) {
if (parentNode) {
this.builders.mount.addLine(`@appendNode( ${name}, ${parentNode} );`);
} else {
this.builders.mount.addLine(`@insertNode( ${name}, #target, anchor );`);
}
}
render() {
let introing;
const hasIntros = !this.builders.intro.isEmpty();
if (hasIntros) {
introing = this.getUniqueName('introing');
this.addVariable(introing);
}
let outroing;
const hasOutros = !this.builders.outro.isEmpty();
if (hasOutros) {
outroing = this.getUniqueName('outroing');
this.addVariable(outroing);
}
if (this.autofocus) {
this.builders.mount.addLine(`${this.autofocus}.focus();`);
}
// minor hack we need to ensure that any {{{triples}}} are detached first
this.builders.unmount.addBlockAtStart(this.builders.detachRaw);
const properties = new CodeBuilder();
let localKey;
if (this.key) {
localKey = this.getUniqueName('key');
properties.addBlock(`key: ${localKey},`);
}
if (this.first) {
properties.addBlock(`first: null,`);
this.builders.hydrate.addLine(`this.first = ${this.first};`);
}
if (this.builders.create.isEmpty()) {
properties.addBlock(`create: @noop,`);
} else {
properties.addBlock(deindent`
create: function () {
${this.builders.create}
${!this.builders.hydrate.isEmpty() && `this.hydrate();`}
},
`);
}
if (this.generator.hydratable) {
if (this.builders.claim.isEmpty()) {
properties.addBlock(`claim: @noop,`);
} else {
properties.addBlock(deindent`
claim: function ( nodes ) {
${this.builders.claim}
${!this.builders.hydrate.isEmpty() && `this.hydrate();`}
},
`);
}
}
if (!this.builders.hydrate.isEmpty()) {
properties.addBlock(deindent`
hydrate: function ( nodes ) {
${this.builders.hydrate}
},
`);
}
if (this.builders.mount.isEmpty()) {
properties.addBlock(`mount: @noop,`);
} else {
properties.addBlock(deindent`
mount: function ( #target, anchor ) {
${this.builders.mount}
},
`);
}
if (this.hasUpdateMethod) {
if (this.builders.update.isEmpty()) {
properties.addBlock(`update: @noop,`);
} else {
properties.addBlock(deindent`
update: function ( changed, ${this.params.join(', ')} ) {
${this.builders.update}
},
`);
}
}
if (this.hasIntroMethod) {
if (hasIntros) {
properties.addBlock(deindent`
intro: function ( #target, anchor ) {
if ( ${introing} ) return;
${introing} = true;
${hasOutros && `${outroing} = false;`}
${this.builders.intro}
this.mount( #target, anchor );
},
`);
} else {
properties.addBlock(deindent`
intro: function ( #target, anchor ) {
this.mount( #target, anchor );
},
`);
}
}
if (this.hasOutroMethod) {
if (hasOutros) {
properties.addBlock(deindent`
outro: function ( ${this.alias('outrocallback')} ) {
if ( ${outroing} ) return;
${outroing} = true;
${hasIntros && `${introing} = false;`}
var ${this.alias('outros')} = ${this.outros};
${this.builders.outro}
},
`);
} else {
properties.addBlock(deindent`
outro: function ( outrocallback ) {
outrocallback();
},
`);
}
}
if (this.builders.unmount.isEmpty()) {
properties.addBlock(`unmount: @noop,`);
} else {
properties.addBlock(deindent`
unmount: function () {
${this.builders.unmount}
},
`);
}
if (this.builders.destroy.isEmpty()) {
properties.addBlock(`destroy: @noop`);
} else {
properties.addBlock(deindent`
destroy: function () {
${this.builders.destroy}
}
`);
}
return deindent`
function ${this.name} ( ${this.params.join(', ')}, #component${this.key
? `, ${localKey}`
: ''} ) {
${this.variables.size > 0 &&
`var ${Array.from(this.variables.keys())
.map(key => {
const init = this.variables.get(key);
return init !== undefined ? `${key} = ${init}` : key;
})
.join(', ')};`}
${!this.builders.init.isEmpty() && this.builders.init}
return {
${properties}
};
}
`.replace(/(\\)?#(\w*)/g, (match, escaped, name) => {
return escaped ? match.slice(1) : this.alias(name);
});
}
}

@ -1,312 +0,0 @@
import MagicString from 'magic-string';
import { parse } from 'acorn';
import annotateWithScopes from '../../utils/annotateWithScopes.js';
import isReference from '../../utils/isReference.js';
import { walk } from 'estree-walker';
import deindent from '../../utils/deindent.js';
import CodeBuilder from '../../utils/CodeBuilder.js';
import visit from './visit.js';
import Generator from '../Generator.js';
import preprocess from './preprocess.js';
import * as shared from '../../shared/index.js';
class DomGenerator extends Generator {
constructor ( parsed, source, name, options ) {
super( parsed, source, name, options );
this.blocks = [];
this.uses = new Set();
this.readonly = new Set();
// initial values for e.g. window.innerWidth, if there's a <:Window> meta tag
this.builders = {
metaBindings: new CodeBuilder()
};
}
helper ( name ) {
if ( this.options.dev && `${name}Dev` in shared ) {
name = `${name}Dev`;
}
this.uses.add( name );
return this.alias( name );
}
}
export default function dom ( parsed, source, options ) {
const format = options.format || 'es';
const name = options.name || 'SvelteComponent';
const generator = new DomGenerator( parsed, source, name, options );
const { computations, hasJs, templateProperties, namespace } = generator.parseJs();
const block = preprocess( generator, parsed.html );
const state = {
namespace,
parentNode: null,
isTopLevel: true
};
parsed.html.children.forEach( node => {
visit( generator, block, state, node );
});
const builders = {
main: new CodeBuilder(),
init: new CodeBuilder(),
_set: new CodeBuilder()
};
if ( options.dev ) {
builders._set.addBlock( deindent`
if ( typeof newState !== 'object' ) {
throw new Error( 'Component .set was called without an object of data key-values to update.' );
}
`);
}
builders._set.addLine( 'var oldState = this._state;' );
builders._set.addLine( `this._state = ${generator.helper( 'assign' )}( {}, oldState, newState );` );
if ( computations.length ) {
const builder = new CodeBuilder();
const differs = generator.helper( 'differs' );
computations.forEach( ({ key, deps }) => {
if ( generator.readonly.has( key ) ) {
// <:Window> bindings
throw new Error( `Cannot have a computed value '${key}' that clashes with a read-only property` );
}
generator.readonly.add( key );
const condition = `isInitial || ${deps.map( dep => `( '${dep}' in newState && ${differs}( state.${dep}, oldState.${dep} ) )` ).join( ' || ' )}`;
const statement = `state.${key} = newState.${key} = ${generator.alias( 'template' )}.computed.${key}( ${deps.map( dep => `state.${dep}` ).join( ', ' )} );`;
builder.addConditionalLine( condition, statement );
});
builders.main.addBlock( deindent`
function ${generator.alias( 'recompute' )} ( state, newState, oldState, isInitial ) {
${builder}
}
` );
}
if ( options.dev ) {
Array.from( generator.readonly ).forEach( prop => {
builders._set.addLine( `if ( '${prop}' in newState && !this._updatingReadonlyProperty ) throw new Error( "Cannot set read-only property '${prop}'" );` );
});
}
if ( computations.length ) {
builders._set.addLine( `${generator.alias( 'recompute' )}( this._state, newState, oldState, false )` );
}
builders._set.addLine( `${generator.helper( 'dispatchObservers' )}( this, this._observers.pre, newState, oldState );` );
if ( block.hasUpdateMethod ) builders._set.addLine( `if ( this._fragment ) this._fragment.update( newState, this._state );` ); // TODO is the condition necessary?
builders._set.addLine( `${generator.helper( 'dispatchObservers' )}( this, this._observers.post, newState, oldState );` );
if ( hasJs ) {
builders.main.addBlock( `[✂${parsed.js.content.start}-${parsed.js.content.end}✂]` );
}
if ( generator.css && options.css !== false ) {
builders.main.addBlock( deindent`
var ${generator.alias( 'added_css' )} = false;
function ${generator.alias( 'add_css' )} () {
var style = ${generator.helper( 'createElement' )}( 'style' );
style.textContent = ${JSON.stringify( generator.css )};
${generator.helper( 'appendNode' )}( style, document.head );
${generator.alias( 'added_css' )} = true;
}
` );
}
generator.blocks.forEach( block => {
builders.main.addBlock( block.render() );
});
builders.init.addLine( `this._torndown = false;` );
if ( parsed.css && options.css !== false ) {
builders.init.addLine( `if ( !${generator.alias( 'added_css' )} ) ${generator.alias( 'add_css' )}();` );
}
if ( generator.hasComponents ) {
builders.init.addLine( `this._renderHooks = [];` );
}
if ( generator.hasComplexBindings ) {
builders.init.addBlock( deindent`
this._bindings = [];
this._fragment = ${generator.alias( 'create_main_fragment' )}( this._state, this );
if ( options.target ) this._fragment.mount( options.target, null );
while ( this._bindings.length ) this._bindings.pop()();
` );
builders._set.addLine( `while ( this._bindings.length ) this._bindings.pop()();` );
} else {
builders.init.addBlock( deindent`
this._fragment = ${generator.alias( 'create_main_fragment' )}( this._state, this );
if ( options.target ) this._fragment.mount( options.target, null );
` );
}
if ( generator.hasComponents ) {
const statement = `this._flush();`;
builders.init.addBlock( statement );
builders._set.addBlock( statement );
}
if ( templateProperties.oncreate ) {
builders.init.addBlock( deindent`
if ( options._root ) {
options._root._renderHooks.push({ fn: ${generator.alias( 'template' )}.oncreate, context: this });
} else {
${generator.alias( 'template' )}.oncreate.call( this );
}
` );
}
const constructorBlock = new CodeBuilder();
constructorBlock.addLine( `options = options || {};` );
if ( generator.usesRefs ) constructorBlock.addLine( `this.refs = {};` );
constructorBlock.addLine(
`this._state = ${templateProperties.data ? `${generator.helper( 'assign' )}( ${generator.alias( 'template' )}.data(), options.data )` : `options.data || {}`};`
);
if ( !generator.builders.metaBindings.isEmpty() ) {
constructorBlock.addBlock( generator.builders.metaBindings );
}
if ( computations.length ) {
constructorBlock.addLine(
`${generator.alias( 'recompute' )}( this._state, this._state, {}, true );`
);
}
if ( options.dev ) {
generator.expectedProperties.forEach( prop => {
constructorBlock.addLine(
`if ( !( '${prop}' in this._state ) ) console.warn( "Component was created without expected data property '${prop}'" );`
);
});
constructorBlock.addBlock(
`if ( !options.target && !options._root ) throw new Error( "'target' is a required option" );`
);
}
if ( generator.bindingGroups.length ) {
constructorBlock.addLine( `this._bindingGroups = [ ${Array( generator.bindingGroups.length ).fill( '[]' ).join( ', ' )} ];` );
}
constructorBlock.addBlock( deindent`
this._observers = {
pre: Object.create( null ),
post: Object.create( null )
};
this._handlers = Object.create( null );
this._root = options._root;
this._yield = options._yield;
${builders.init}
` );
builders.main.addBlock( deindent`
function ${name} ( options ) {
${constructorBlock}
}
` );
const sharedPath = options.shared === true ? 'svelte/shared.js' : options.shared;
const prototypeBase = `${name}.prototype` + ( templateProperties.methods ? `, ${generator.alias( 'template' )}.methods` : '' );
const proto = sharedPath ? `${generator.helper( 'proto' )} ` : deindent`
{
${
[ 'get', 'fire', 'observe', 'on', 'set', '_flush' ]
.map( n => `${n}: ${generator.helper( n )}` )
.join( ',\n' )
}
}`;
builders.main.addBlock( `${generator.helper( 'assign' )}( ${prototypeBase}, ${proto});` );
// TODO deprecate component.teardown()
builders.main.addBlock( deindent`
${name}.prototype._set = function _set ( newState ) {
${builders._set}
};
${name}.prototype.teardown = ${name}.prototype.destroy = function destroy ( detach ) {
this.fire( 'destroy' );${templateProperties.ondestroy ? `\n${generator.alias( 'template' )}.ondestroy.call( this );` : ``}
this._fragment.destroy( detach !== false );
this._fragment = null;
this._state = {};
this._torndown = true;
};
` );
if ( sharedPath ) {
if ( format !== 'es' ) {
throw new Error( `Components with shared helpers must be compiled to ES2015 modules (format: 'es')` );
}
const names = Array.from( generator.uses ).sort().map( name => {
return name !== generator.alias( name ) ? `${name} as ${generator.alias( name )}` : name;
});
builders.main.addLineAtStart(
`import { ${names.join( ', ' )} } from ${JSON.stringify( sharedPath )};`
);
} else {
generator.uses.forEach( key => {
const str = shared[ key ].toString(); // eslint-disable-line import/namespace
const code = new MagicString( str );
const fn = parse( str ).body[0];
let scope = annotateWithScopes( fn );
walk( fn, {
enter ( node, parent ) {
if ( node._scope ) scope = node._scope;
if ( node.type === 'Identifier' && isReference( node, parent ) && !scope.has( node.name ) ) {
if ( node.name in shared ) {
// this helper function depends on another one
generator.uses.add( node.name );
const alias = generator.alias( node.name );
if ( alias !== node.name ) code.overwrite( node.start, node.end, alias );
}
}
},
leave ( node ) {
if ( node._scope ) scope = scope.parent;
}
});
const alias = generator.alias( fn.id.name );
if ( alias !== fn.id.name ) code.overwrite( fn.id.start, fn.id.end, alias );
builders.main.addBlock( code.toString() );
});
}
return generator.generate( builders.main.toString(), options, { name, format } );
}

@ -0,0 +1,340 @@
import MagicString from 'magic-string';
import { parseExpressionAt } from 'acorn';
import annotateWithScopes from '../../utils/annotateWithScopes';
import isReference from '../../utils/isReference';
import { walk } from 'estree-walker';
import deindent from '../../utils/deindent';
import stringify from '../../utils/stringify';
import CodeBuilder from '../../utils/CodeBuilder';
import visit from './visit';
import shared from './shared';
import Generator from '../Generator';
import preprocess from './preprocess';
import Block from './Block';
import { Parsed, CompileOptions, Node } from '../../interfaces';
export class DomGenerator extends Generator {
blocks: Block[];
readonly: Set<string>;
metaBindings: string[];
hydratable: boolean;
hasIntroTransitions: boolean;
hasOutroTransitions: boolean;
hasComplexBindings: boolean;
constructor(
parsed: Parsed,
source: string,
name: string,
options: CompileOptions
) {
super(parsed, source, name, options);
this.blocks = [];
this.readonly = new Set();
this.hydratable = options.hydratable;
// initial values for e.g. window.innerWidth, if there's a <:Window> meta tag
this.metaBindings = [];
}
}
export default function dom(
parsed: Parsed,
source: string,
options: CompileOptions
) {
const format = options.format || 'es';
const generator = new DomGenerator(parsed, source, options.name || 'SvelteComponent', options);
const {
computations,
hasJs,
name,
templateProperties,
namespace,
} = generator;
const { block, state } = preprocess(generator, namespace, parsed.html);
parsed.html.children.forEach((node: Node) => {
visit(generator, block, state, node);
});
const builder = new CodeBuilder();
if (computations.length) {
const computationBuilder = new CodeBuilder();
computations.forEach(({ key, deps }) => {
if (generator.readonly.has(key)) {
// <:Window> bindings
throw new Error(
`Cannot have a computed value '${key}' that clashes with a read-only property`
);
}
generator.readonly.add(key);
const condition = `isInitial || ${deps
.map(
dep =>
`( '${dep}' in newState && @differs( state.${dep}, oldState.${dep} ) )`
)
.join(' || ')}`;
const statement = `state.${key} = newState.${key} = @template.computed.${key}( ${deps
.map(dep => `state.${dep}`)
.join(', ')} );`;
computationBuilder.addConditionalLine(condition, statement);
});
builder.addBlock(deindent`
function @recompute ( state, newState, oldState, isInitial ) {
${computationBuilder}
}
`);
}
const _set = deindent`
${options.dev &&
deindent`
if ( typeof newState !== 'object' ) {
throw new Error( 'Component .set was called without an object of data key-values to update.' );
}
${Array.from(generator.readonly).map(
prop =>
`if ( '${prop}' in newState && !this._updatingReadonlyProperty ) throw new Error( "Cannot set read-only property '${prop}'" );`
)}
`}
var oldState = this._state;
this._state = @assign( {}, oldState, newState );
${computations.length &&
`@recompute( this._state, newState, oldState, false )`}
@dispatchObservers( this, this._observers.pre, newState, oldState );
${block.hasUpdateMethod && `this._fragment.update( newState, this._state );`}
@dispatchObservers( this, this._observers.post, newState, oldState );
${generator.hasComplexBindings &&
`while ( this._bindings.length ) this._bindings.pop()();`}
${(generator.hasComponents || generator.hasIntroTransitions) &&
`this._flush();`}
`;
if (hasJs) {
builder.addBlock(`[✂${parsed.js.content.start}-${parsed.js.content.end}✂]`);
}
if (generator.css && options.css !== false) {
builder.addBlock(deindent`
function @add_css () {
var style = @createElement( 'style' );
style.id = '${generator.cssId}-style';
style.textContent = ${stringify(generator.css)};
@appendNode( style, document.head );
}
`);
}
generator.blocks.forEach(block => {
builder.addBlock(block.render());
});
const sharedPath = options.shared === true
? 'svelte/shared.js'
: options.shared;
const prototypeBase =
`${name}.prototype` +
(templateProperties.methods ? `, @template.methods` : '');
const proto = sharedPath
? `@proto `
: deindent`
{
${['get', 'fire', 'observe', 'on', 'set', '_flush']
.map(n => `${n}: @${n}`)
.join(',\n')}
}`;
// TODO deprecate component.teardown()
builder.addBlock(deindent`
function ${name} ( options ) {
options = options || {};
${options.dev &&
`if ( !options.target && !options._root ) throw new Error( "'target' is a required option" );`}
${generator.usesRefs && `this.refs = {};`}
this._state = ${templateProperties.data
? `@assign( @template.data(), options.data )`
: `options.data || {}`};
${generator.metaBindings}
${computations.length && `@recompute( this._state, this._state, {}, true );`}
${options.dev &&
Array.from(generator.expectedProperties).map(
prop =>
`if ( !( '${prop}' in this._state ) ) console.warn( "Component was created without expected data property '${prop}'" );`
)}
${generator.bindingGroups.length &&
`this._bindingGroups = [ ${Array(generator.bindingGroups.length)
.fill('[]')
.join(', ')} ];`}
this._observers = {
pre: Object.create( null ),
post: Object.create( null )
};
this._handlers = Object.create( null );
this._root = options._root || this;
this._yield = options._yield;
this._torndown = false;
${generator.css &&
options.css !== false &&
`if ( !document.getElementById( '${generator.cssId}-style' ) ) @add_css();`}
${(generator.hasComponents || generator.hasIntroTransitions) &&
`this._renderHooks = [];`}
${generator.hasComplexBindings && `this._bindings = [];`}
this._fragment = @create_main_fragment( this._state, this );
if ( options.target ) {
${generator.hydratable
? deindent`
var nodes = @children( options.target );
options.hydrate ? this._fragment.claim( nodes ) : this._fragment.create();
nodes.forEach( @detachNode );
` :
deindent`
${options.dev && `if ( options.hydrate ) throw new Error( 'options.hydrate only works if the component was compiled with the \`hydratable: true\` option' );`}
this._fragment.create();
`}
this._fragment.${block.hasIntroMethod ? 'intro' : 'mount'}( options.target, null );
}
${generator.hasComplexBindings &&
`while ( this._bindings.length ) this._bindings.pop()();`}
${(generator.hasComponents || generator.hasIntroTransitions) &&
`this._flush();`}
${templateProperties.oncreate &&
deindent`
if ( options._root ) {
options._root._renderHooks.push( @template.oncreate.bind( this ) );
} else {
@template.oncreate.call( this );
}
`}
}
@assign( ${prototypeBase}, ${proto});
${name}.prototype._set = function _set ( newState ) {
${_set}
};
${name}.prototype.teardown = ${name}.prototype.destroy = function destroy ( detach ) {
this.fire( 'destroy' );
${templateProperties.ondestroy && `@template.ondestroy.call( this );`}
if ( detach !== false ) this._fragment.unmount();
this._fragment.destroy();
this._fragment = null;
this._state = {};
this._torndown = true;
};
`);
const usedHelpers = new Set();
let result = builder
.toString()
.replace(/(\\)?@(\w*)/g, (match: string, escaped: string, name: string) => {
if (escaped) return match.slice(1);
if (name in shared) {
if (options.dev && `${name}Dev` in shared) name = `${name}Dev`;
usedHelpers.add(name);
}
return generator.alias(name);
});
if (sharedPath) {
if (format !== 'es') {
throw new Error(
`Components with shared helpers must be compiled to ES2015 modules (format: 'es')`
);
}
const names = Array.from(usedHelpers).sort().map(name => {
return name !== generator.alias(name)
? `${name} as ${generator.alias(name)}`
: name;
});
result =
`import { ${names.join(', ')} } from ${stringify(sharedPath)};\n\n` +
result;
} else {
usedHelpers.forEach(key => {
const str = shared[key];
const code = new MagicString(str);
const expression = parseExpressionAt(str, 0);
let scope = annotateWithScopes(expression);
walk(expression, {
enter(node, parent) {
if (node._scope) scope = node._scope;
if (
node.type === 'Identifier' &&
isReference(node, parent) &&
!scope.has(node.name)
) {
if (node.name in shared) {
// this helper function depends on another one
const dependency = node.name;
usedHelpers.add(dependency);
const alias = generator.alias(dependency);
if (alias !== node.name)
code.overwrite(node.start, node.end, alias);
}
}
},
leave(node) {
if (node._scope) scope = scope.parent;
},
});
if (key === 'transitionManager') {
// special case
const global = `_svelteTransitionManager`;
result += `\n\nvar ${generator.alias(
'transitionManager'
)} = window.${global} || ( window.${global} = ${code});`;
} else {
const alias = generator.alias(expression.id.name);
if (alias !== expression.id.name)
code.overwrite(expression.id.start, expression.id.end, alias);
result += `\n\n${code}`;
}
});
}
return generator.generate(result, options, {
name,
format,
});
}

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

@ -1,219 +0,0 @@
import Block from './Block.js';
import { trimStart, trimEnd } from '../../utils/trim.js';
function isElseIf ( node ) {
return node && node.children.length === 1 && node.children[0].type === 'IfBlock';
}
const preprocessors = {
MustacheTag: ( generator, block, node ) => {
const dependencies = block.findDependencies( node.expression );
block.addDependencies( dependencies );
},
IfBlock: ( generator, block, node ) => {
const blocks = [];
let dynamic = false;
function attachBlocks ( node ) {
const dependencies = block.findDependencies( node.expression );
block.addDependencies( dependencies );
node._block = block.child({
name: generator.getUniqueName( `create_if_block` )
});
blocks.push( node._block );
preprocessChildren( generator, node._block, node );
if ( node._block.dependencies.size > 0 ) {
dynamic = true;
block.addDependencies( node._block.dependencies );
}
if ( isElseIf( node.else ) ) {
attachBlocks( node.else.children[0] );
} else if ( node.else ) {
node.else._block = block.child({
name: generator.getUniqueName( `create_if_block` )
});
blocks.push( node.else._block );
preprocessChildren( generator, node.else._block, node.else );
if ( node.else._block.dependencies.size > 0 ) {
dynamic = true;
block.addDependencies( node.else._block.dependencies );
}
}
}
attachBlocks( node );
blocks.forEach( block => {
block.hasUpdateMethod = dynamic;
});
generator.blocks.push( ...blocks );
},
EachBlock: ( generator, block, node ) => {
const dependencies = block.findDependencies( node.expression );
block.addDependencies( dependencies );
const indexNames = new Map( block.indexNames );
const indexName = node.index || block.getUniqueName( `${node.context}_index` );
indexNames.set( node.context, indexName );
const listNames = new Map( block.listNames );
const listName = block.getUniqueName( `each_block_value` );
listNames.set( node.context, listName );
const context = generator.getUniqueName( node.context );
const contexts = new Map( block.contexts );
contexts.set( node.context, context );
const indexes = new Map( block.indexes );
if ( node.index ) indexes.set( indexName, node.context );
const contextDependencies = new Map( block.contextDependencies );
contextDependencies.set( node.context, dependencies );
node._block = block.child({
name: generator.getUniqueName( 'create_each_block' ),
expression: node.expression,
context: node.context,
key: node.key,
contextDependencies,
contexts,
indexes,
listName,
indexName,
indexNames,
listNames,
params: block.params.concat( listName, context, indexName )
});
generator.blocks.push( node._block );
preprocessChildren( generator, node._block, node );
block.addDependencies( node._block.dependencies );
node._block.hasUpdateMethod = node._block.dependencies.size > 0;
if ( node.else ) {
node.else._block = block.child({
name: generator.getUniqueName( `${node._block.name}_else` )
});
generator.blocks.push( node.else._block );
preprocessChildren( generator, node.else._block, node.else );
node.else._block.hasUpdateMethod = node.else._block.dependencies.size > 0;
}
},
Element: ( generator, block, node ) => {
node.attributes.forEach( attribute => {
if ( attribute.type === 'Attribute' && attribute.value !== true ) {
attribute.value.forEach( chunk => {
if ( chunk.type !== 'Text' ) {
const dependencies = block.findDependencies( chunk.expression );
block.addDependencies( dependencies );
}
});
}
else if ( attribute.type === 'Binding' ) {
const dependencies = block.findDependencies( attribute.value );
block.addDependencies( dependencies );
}
});
const isComponent = generator.components.has( node.name ) || node.name === ':Self';
if ( node.children.length ) {
if ( isComponent ) {
const name = block.getUniqueName( ( node.name === ':Self' ? generator.name : node.name ).toLowerCase() );
node._block = block.child({
name: generator.getUniqueName( `create_${name}_yield_fragment` )
});
generator.blocks.push( node._block );
preprocessChildren( generator, node._block, node );
block.addDependencies( node._block.dependencies );
node._block.hasUpdateMethod = node._block.dependencies.size > 0;
}
else {
preprocessChildren( generator, block, node );
}
}
}
};
preprocessors.RawMustacheTag = preprocessors.MustacheTag;
function preprocessChildren ( generator, block, node ) {
// glue text nodes together
const cleaned = [];
let lastChild;
node.children.forEach( child => {
if ( child.type === 'Comment' ) return;
if ( child.type === 'Text' && lastChild && lastChild.type === 'Text' ) {
lastChild.data += child.data;
lastChild.end = child.end;
} else {
cleaned.push( child );
}
lastChild = child;
});
node.children = cleaned;
cleaned.forEach( child => {
const preprocess = preprocessors[ child.type ];
if ( preprocess ) preprocess( generator, block, child );
});
}
export default function preprocess ( generator, node ) {
const block = new Block({
generator,
name: generator.alias( 'create_main_fragment' ),
key: null,
contexts: new Map(),
indexes: new Map(),
contextDependencies: new Map(),
params: [ 'state' ],
indexNames: new Map(),
listNames: new Map(),
dependencies: new Set()
});
generator.blocks.push( block );
preprocessChildren( generator, block, node );
block.hasUpdateMethod = block.dependencies.size > 0;
// trim leading and trailing whitespace from the top level
const firstChild = node.children[0];
if ( firstChild && firstChild.type === 'Text' ) {
firstChild.data = trimStart( firstChild.data );
if ( !firstChild.data ) node.children.shift();
}
const lastChild = node.children[ node.children.length - 1 ];
if ( lastChild && lastChild.type === 'Text' ) {
lastChild.data = trimEnd( lastChild.data );
if ( !lastChild.data ) node.children.pop();
}
return block;
}

@ -0,0 +1,439 @@
import Block from './Block';
import { trimStart, trimEnd } from '../../utils/trim';
import { assign } from '../../shared/index.js';
import { DomGenerator } from './index';
import { Node } from '../../interfaces';
import { State } from './interfaces';
function isElseIf(node: Node) {
return (
node && node.children.length === 1 && node.children[0].type === 'IfBlock'
);
}
function getChildState(parent: State, child = {}) {
return assign(
{},
parent,
{ name: null, parentNode: null, parentNodes: 'nodes' },
child || {}
);
}
// Whitespace inside one of these elements will not result in
// a whitespace node being created in any circumstances. (This
// list is almost certainly very incomplete)
const elementsWithoutText = new Set([
'audio',
'datalist',
'dl',
'ol',
'optgroup',
'select',
'ul',
'video',
]);
const preprocessors = {
MustacheTag: (
generator: DomGenerator,
block: Block,
state: State,
node: Node,
stripWhitespace: boolean
) => {
const dependencies = block.findDependencies(node.expression);
block.addDependencies(dependencies);
node._state = getChildState(state, {
name: block.getUniqueName('text'),
});
},
RawMustacheTag: (
generator: DomGenerator,
block: Block,
state: State,
node: Node,
stripWhitespace: boolean
) => {
const dependencies = block.findDependencies(node.expression);
block.addDependencies(dependencies);
const basename = block.getUniqueName('raw');
const name = block.getUniqueName(`${basename}_before`);
node._state = getChildState(state, { basename, name });
},
Text: (generator: DomGenerator, block: Block, state: State, node: Node, stripWhitespace: boolean) => {
node._state = getChildState(state);
if (!/\S/.test(node.data)) {
if (state.namespace) return;
if (elementsWithoutText.has(state.parentNodeName)) return;
}
node._state.shouldCreate = true;
node._state.name = block.getUniqueName(`text`);
},
IfBlock: (
generator: DomGenerator,
block: Block,
state: State,
node: Node,
stripWhitespace: boolean,
nextSibling: Node
) => {
const blocks: Block[] = [];
let dynamic = false;
let hasIntros = false;
let hasOutros = false;
function attachBlocks(node: Node) {
const dependencies = block.findDependencies(node.expression);
block.addDependencies(dependencies);
node._block = block.child({
name: generator.getUniqueName(`create_if_block`),
});
node._state = getChildState(state);
blocks.push(node._block);
preprocessChildren(generator, node._block, node._state, node, stripWhitespace, node);
if (node._block.dependencies.size > 0) {
dynamic = true;
block.addDependencies(node._block.dependencies);
}
if (node._block.hasIntroMethod) hasIntros = true;
if (node._block.hasOutroMethod) hasOutros = true;
if (isElseIf(node.else)) {
attachBlocks(node.else.children[0]);
} else if (node.else) {
node.else._block = block.child({
name: generator.getUniqueName(`create_if_block`),
});
node.else._state = getChildState(state);
blocks.push(node.else._block);
preprocessChildren(
generator,
node.else._block,
node.else._state,
node.else,
stripWhitespace,
nextSibling
);
if (node.else._block.dependencies.size > 0) {
dynamic = true;
block.addDependencies(node.else._block.dependencies);
}
}
}
attachBlocks(node);
blocks.forEach(block => {
block.hasUpdateMethod = dynamic;
block.hasIntroMethod = hasIntros;
block.hasOutroMethod = hasOutros;
});
generator.blocks.push(...blocks);
},
EachBlock: (
generator: DomGenerator,
block: Block,
state: State,
node: Node,
stripWhitespace: boolean,
nextSibling: Node
) => {
const dependencies = block.findDependencies(node.expression);
block.addDependencies(dependencies);
const indexNames = new Map(block.indexNames);
const indexName =
node.index || block.getUniqueName(`${node.context}_index`);
indexNames.set(node.context, indexName);
const listNames = new Map(block.listNames);
const listName = block.getUniqueName(`each_block_value`);
listNames.set(node.context, listName);
const context = generator.getUniqueName(node.context);
const contexts = new Map(block.contexts);
contexts.set(node.context, context);
const indexes = new Map(block.indexes);
if (node.index) indexes.set(indexName, node.context);
const contextDependencies = new Map(block.contextDependencies);
contextDependencies.set(node.context, dependencies);
node._block = block.child({
name: generator.getUniqueName('create_each_block'),
expression: node.expression,
context: node.context,
key: node.key,
contextDependencies,
contexts,
indexes,
listName,
indexName,
indexNames,
listNames,
params: block.params.concat(listName, context, indexName),
});
node._state = getChildState(state, {
inEachBlock: true,
});
generator.blocks.push(node._block);
preprocessChildren(generator, node._block, node._state, node, stripWhitespace, nextSibling);
block.addDependencies(node._block.dependencies);
node._block.hasUpdateMethod = node._block.dependencies.size > 0;
if (node.else) {
node.else._block = block.child({
name: generator.getUniqueName(`${node._block.name}_else`),
});
node.else._state = getChildState(state);
generator.blocks.push(node.else._block);
preprocessChildren(
generator,
node.else._block,
node.else._state,
node.else,
stripWhitespace,
nextSibling
);
node.else._block.hasUpdateMethod = node.else._block.dependencies.size > 0;
}
},
Element: (
generator: DomGenerator,
block: Block,
state: State,
node: Node,
stripWhitespace: boolean,
nextSibling: Node
) => {
node.attributes.forEach((attribute: Node) => {
if (attribute.type === 'Attribute' && attribute.value !== true) {
attribute.value.forEach((chunk: Node) => {
if (chunk.type !== 'Text') {
const dependencies = block.findDependencies(chunk.expression);
block.addDependencies(dependencies);
// special case — <option value='{{foo}}'> — see below
if (
node.name === 'option' &&
attribute.name === 'value' &&
state.selectBindingDependencies
) {
state.selectBindingDependencies.forEach(prop => {
dependencies.forEach((dependency: string) => {
generator.indirectDependencies.get(prop).add(dependency);
});
});
}
}
});
} else if (attribute.type === 'Binding') {
const dependencies = block.findDependencies(attribute.value);
block.addDependencies(dependencies);
} else if (attribute.type === 'Transition') {
if (attribute.intro)
generator.hasIntroTransitions = block.hasIntroMethod = true;
if (attribute.outro) {
generator.hasOutroTransitions = block.hasOutroMethod = true;
block.outros += 1;
}
}
});
// special case — in a case like this...
//
// <select bind:value='foo'>
// <option value='{{bar}}'>bar</option>
// <option value='{{baz}}'>baz</option>
// </option>
//
// ...we need to know that `foo` depends on `bar` and `baz`,
// so that if `foo.qux` changes, we know that we need to
// mark `bar` and `baz` as dirty too
if (node.name === 'select') {
const value = node.attributes.find(
(attribute: Node) => attribute.name === 'value'
);
if (value) {
// TODO does this also apply to e.g. `<input type='checkbox' bind:group='foo'>`?
const dependencies = block.findDependencies(value.value);
state.selectBindingDependencies = dependencies;
dependencies.forEach((prop: string) => {
generator.indirectDependencies.set(prop, new Set());
});
} else {
state.selectBindingDependencies = null;
}
}
const isComponent =
generator.components.has(node.name) || node.name === ':Self';
if (isComponent) {
node._state = getChildState(state);
} else {
const name = block.getUniqueName(
node.name.replace(/[^a-zA-Z0-9_$]/g, '_')
);
node._state = getChildState(state, {
isTopLevel: false,
name,
parentNode: name,
parentNodes: block.getUniqueName(`${name}_nodes`),
parentNodeName: node.name,
namespace: node.name === 'svg'
? 'http://www.w3.org/2000/svg'
: state.namespace,
allUsedContexts: [],
});
}
if (node.children.length) {
if (isComponent) {
const name = block.getUniqueName(
(node.name === ':Self' ? generator.name : node.name).toLowerCase()
);
node._block = block.child({
name: generator.getUniqueName(`create_${name}_yield_fragment`),
});
generator.blocks.push(node._block);
preprocessChildren(generator, node._block, node._state, node, stripWhitespace, nextSibling);
block.addDependencies(node._block.dependencies);
node._block.hasUpdateMethod = node._block.dependencies.size > 0;
} else {
if (node.name === 'pre' || node.name === 'textarea') stripWhitespace = false;
preprocessChildren(generator, block, node._state, node, stripWhitespace, nextSibling);
}
}
},
};
function preprocessChildren(
generator: DomGenerator,
block: Block,
state: State,
node: Node,
stripWhitespace: boolean,
nextSibling: Node
) {
// glue text nodes together
const cleaned: Node[] = [];
let lastChild: Node;
node.children.forEach((child: Node) => {
if (child.type === 'Comment') return;
if (child.type === 'Text' && lastChild && lastChild.type === 'Text') {
lastChild.data += child.data;
lastChild.end = child.end;
} else {
if (child.type === 'Text' && stripWhitespace && cleaned.length === 0) {
child.data = trimStart(child.data);
if (child.data) cleaned.push(child);
} else {
cleaned.push(child);
}
}
lastChild = child;
});
lastChild = null;
cleaned.forEach((child: Node, i: number) => {
const preprocessor = preprocessors[child.type];
if (preprocessor) preprocessor(generator, block, state, child, stripWhitespace, cleaned[i + 1] || nextSibling);
if (lastChild) {
lastChild.next = child;
lastChild.needsAnchor = !child._state || !child._state.name;
}
lastChild = child;
});
// We want to remove trailing whitespace inside an element/component/block,
// *unless* there is no whitespace between this node and its next sibling
if (lastChild && lastChild.type === 'Text') {
if (stripWhitespace && (!nextSibling || (nextSibling.type === 'Text' && /^\s/.test(nextSibling.data)))) {
lastChild.data = trimEnd(lastChild.data);
if (!lastChild.data) {
cleaned.pop();
lastChild = cleaned[cleaned.length - 1];
lastChild.next = null;
}
}
}
if (lastChild) {
lastChild.needsAnchor = !state.parentNode;
}
node.children = cleaned;
}
export default function preprocess(
generator: DomGenerator,
namespace: string,
node: Node
) {
const block = new Block({
generator,
name: '@create_main_fragment',
key: null,
contexts: new Map(),
indexes: new Map(),
contextDependencies: new Map(),
params: ['state'],
indexNames: new Map(),
listNames: new Map(),
dependencies: new Set(),
});
const state: State = {
namespace,
parentNode: null,
parentNodes: 'nodes',
isTopLevel: true,
};
generator.blocks.push(block);
preprocessChildren(generator, block, state, node, true, null);
block.hasUpdateMethod = block.dependencies.size > 0;
return { block, state };
}

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

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

@ -1,64 +0,0 @@
import deindent from '../../../../utils/deindent.js';
import flattenReference from '../../../../utils/flattenReference.js';
import getSetter from '../shared/binding/getSetter.js';
export default function visitBinding ( generator, block, state, node, attribute, local ) {
const { name } = flattenReference( attribute.value );
const { snippet, contexts, dependencies } = block.contextualise( attribute.value );
if ( dependencies.length > 1 ) throw new Error( 'An unexpected situation arose. Please raise an issue at https://github.com/sveltejs/svelte/issues — thanks!' );
contexts.forEach( context => {
if ( !~local.allUsedContexts.indexOf( context ) ) local.allUsedContexts.push( context );
});
const contextual = block.contexts.has( name );
let obj;
let prop;
if ( contextual ) {
obj = block.listNames.get( name );
prop = block.indexNames.get( name );
} else if ( attribute.value.type === 'MemberExpression' ) {
prop = `'[✂${attribute.value.property.start}-${attribute.value.property.end}✂]'`;
obj = `[✂${attribute.value.object.start}-${attribute.value.object.end}✂]`;
} else {
obj = 'state';
prop = `'${name}'`;
}
local.bindings.push({
name: attribute.name,
value: snippet,
obj,
prop
});
const setter = getSetter({ block, name, context: '_context', attribute, dependencies, value: 'value' });
generator.hasComplexBindings = true;
const updating = block.getUniqueName( `${local.name}_updating` );
block.addVariable( updating, 'false' );
local.create.addBlock( deindent`
${block.component}._bindings.push( function () {
if ( ${local.name}._torndown ) return;
${local.name}.observe( '${attribute.name}', function ( value ) {
if ( ${updating} ) return;
${updating} = true;
${setter}
${updating} = false;
}, { init: ${generator.helper( 'differs' )}( ${local.name}.get( '${attribute.name}' ), ${snippet} ) });
});
` );
local.update.addBlock( deindent`
if ( !${updating} && ${dependencies.map( dependency => `'${dependency}' in changed` ).join( '||' )} ) {
${updating} = true;
${local.name}._set({ ${attribute.name}: ${snippet} });
${updating} = false;
}
` );
}

@ -0,0 +1,89 @@
import deindent from '../../../../utils/deindent';
import flattenReference from '../../../../utils/flattenReference';
import getSetter from '../shared/binding/getSetter';
import { DomGenerator } from '../../index';
import Block from '../../Block';
import { Node } from '../../../../interfaces';
import { State } from '../../interfaces';
import getObject from '../../../../utils/getObject';
export default function visitBinding(
generator: DomGenerator,
block: Block,
state: State,
node: Node,
attribute,
local
) {
const { name } = getObject(attribute.value);
const { snippet, contexts, dependencies } = block.contextualise(
attribute.value
);
contexts.forEach(context => {
if (!~local.allUsedContexts.indexOf(context))
local.allUsedContexts.push(context);
});
const contextual = block.contexts.has(name);
let obj;
let prop;
if (contextual) {
obj = block.listNames.get(name);
prop = block.indexNames.get(name);
} else if (attribute.value.type === 'MemberExpression') {
prop = `[✂${attribute.value.property.start}-${attribute.value.property
.end}]`;
if (!attribute.value.computed) prop = `'${prop}'`;
obj = `[✂${attribute.value.object.start}-${attribute.value.object.end}✂]`;
} else {
obj = 'state';
prop = `'${name}'`;
}
local.bindings.push({
name: attribute.name,
value: snippet,
obj,
prop,
});
const setter = getSetter({
block,
name,
snippet,
context: '_context',
attribute,
dependencies,
value: 'value',
});
generator.hasComplexBindings = true;
const updating = block.getUniqueName(`${local.name}_updating`);
block.addVariable(updating, 'false');
local.create.addBlock(deindent`
#component._bindings.push( function () {
if ( ${local.name}._torndown ) return;
${local.name}.observe( '${attribute.name}', function ( value ) {
if ( ${updating} ) return;
${updating} = true;
${setter}
${updating} = false;
}, { init: @differs( ${local.name}.get( '${attribute.name}' ), ${snippet} ) });
});
`);
local.update.addBlock(deindent`
if ( !${updating} && ${dependencies
.map(dependency => `'${dependency}' in changed`)
.join(' || ')} ) {
${updating} = true;
${local.name}._set({ ${attribute.name}: ${snippet} });
${updating} = false;
}
`);
}

@ -1,188 +0,0 @@
import deindent from '../../../../utils/deindent.js';
import CodeBuilder from '../../../../utils/CodeBuilder.js';
import visit from '../../visit.js';
import visitAttribute from './Attribute.js';
import visitEventHandler from './EventHandler.js';
import visitBinding from './Binding.js';
import visitRef from './Ref.js';
function stringifyProps ( props ) {
if ( !props.length ) return '{}';
const joined = props.join( ', ' );
if ( joined.length > 40 ) {
// make larger data objects readable
return `{\n\t${props.join( ',\n\t' )}\n}`;
}
return `{ ${joined} }`;
}
const order = {
Attribute: 1,
EventHandler: 2,
Binding: 3,
Ref: 4
};
const visitors = {
Attribute: visitAttribute,
EventHandler: visitEventHandler,
Binding: visitBinding,
Ref: visitRef
};
export default function visitComponent ( generator, block, state, node ) {
const hasChildren = node.children.length > 0;
const name = block.getUniqueName( ( node.name === ':Self' ? generator.name : node.name ).toLowerCase() );
const childState = Object.assign( {}, state, {
parentNode: null
});
const local = {
name,
namespace: state.namespace,
isComponent: true,
allUsedContexts: [],
staticAttributes: [],
dynamicAttributes: [],
bindings: [],
create: new CodeBuilder(),
update: new CodeBuilder()
};
const isToplevel = !state.parentNode;
generator.hasComponents = true;
node.attributes
.sort( ( a, b ) => order[ a.type ] - order[ b.type ] )
.forEach( attribute => {
visitors[ attribute.type ]( generator, block, childState, node, attribute, local );
});
if ( local.allUsedContexts.length ) {
const initialProps = local.allUsedContexts.map( contextName => {
if ( contextName === 'state' ) return `state: state`;
const listName = block.listNames.get( contextName );
const indexName = block.indexNames.get( contextName );
return `${listName}: ${listName},\n${indexName}: ${indexName}`;
}).join( ',\n' );
const updates = local.allUsedContexts.map( contextName => {
if ( contextName === 'state' ) return `${name}._context.state = state;`;
const listName = block.listNames.get( contextName );
const indexName = block.indexNames.get( contextName );
return `${name}._context.${listName} = ${listName};\n${name}._context.${indexName} = ${indexName};`;
}).join( '\n' );
local.create.addBlock( deindent`
${name}._context = {
${initialProps}
};
` );
local.update.addBlock( updates );
}
const componentInitProperties = [
`target: ${!isToplevel ? state.parentNode: 'null'}`,
`_root: ${block.component}._root || ${block.component}`
];
// Component has children, put them in a separate {{yield}} block
if ( hasChildren ) {
const params = block.params.join( ', ' );
const childBlock = node._block;
node.children.forEach( child => {
visit( generator, childBlock, childState, child );
});
const yieldFragment = block.getUniqueName( `${name}_yield_fragment` );
block.builders.create.addLine(
`var ${yieldFragment} = ${childBlock.name}( ${params}, ${block.component} );`
);
if ( childBlock.hasUpdateMethod ) {
block.builders.update.addLine(
`${yieldFragment}.update( changed, ${params} );`
);
}
componentInitProperties.push( `_yield: ${yieldFragment}`);
}
const statements = [];
if ( local.staticAttributes.length || local.dynamicAttributes.length || local.bindings.length ) {
const initialProps = local.staticAttributes
.concat( local.dynamicAttributes )
.map( attribute => `${attribute.name}: ${attribute.value}` );
const initialPropString = stringifyProps( initialProps );
if ( local.bindings.length ) {
const initialData = block.getUniqueName( `${name}_initial_data` );
statements.push( `var ${initialData} = ${initialPropString};` );
local.bindings.forEach( binding => {
statements.push( `if ( ${binding.prop} in ${binding.obj} ) ${initialData}.${binding.name} = ${binding.value};` );
});
componentInitProperties.push( `data: ${initialData}` );
} else if ( initialProps.length ) {
componentInitProperties.push( `data: ${initialPropString}` );
}
}
const expression = node.name === ':Self' ? generator.name : generator.importedComponents.get( node.name ) || `${generator.alias( 'template' )}.components.${node.name}`;
local.create.addBlockAtStart( deindent`
${statements.join( '\n' )}
var ${name} = new ${expression}({
${componentInitProperties.join(',\n')}
});
` );
if ( isToplevel ) {
block.builders.mount.addLine( `${name}._fragment.mount( ${block.target}, anchor );` );
}
if ( local.dynamicAttributes.length ) {
const updates = local.dynamicAttributes.map( attribute => {
if ( attribute.dependencies.length ) {
return deindent`
if ( ${attribute.dependencies.map( dependency => `'${dependency}' in changed` ).join( '||' )} ) ${name}_changes.${attribute.name} = ${attribute.value};
`;
}
// TODO this is an odd situation to encounter I *think* it should only happen with
// each block indices, in which case it may be possible to optimise this
return `${name}_changes.${attribute.name} = ${attribute.value};`;
});
local.update.addBlock( deindent`
var ${name}_changes = {};
${updates.join( '\n' )}
if ( Object.keys( ${name}_changes ).length ) ${name}.set( ${name}_changes );
` );
}
block.builders.destroy.addLine( `${name}.destroy( ${isToplevel ? 'detach' : 'false'} );` );
block.builders.create.addBlock( local.create );
if ( !local.update.isEmpty() ) block.builders.update.addBlock( local.update );
}

@ -0,0 +1,236 @@
import deindent from '../../../../utils/deindent';
import CodeBuilder from '../../../../utils/CodeBuilder';
import visit from '../../visit';
import visitAttribute from './Attribute';
import visitEventHandler from './EventHandler';
import visitBinding from './Binding';
import visitRef from './Ref';
import { DomGenerator } from '../../index';
import Block from '../../Block';
import { Node } from '../../../../interfaces';
import { State } from '../../interfaces';
function stringifyProps(props: string[]) {
if (!props.length) return '{}';
const joined = props.join(', ');
if (joined.length > 40) {
// make larger data objects readable
return `{\n\t${props.join(',\n\t')}\n}`;
}
return `{ ${joined} }`;
}
const order = {
Attribute: 1,
EventHandler: 2,
Binding: 3,
Ref: 4,
};
const visitors = {
Attribute: visitAttribute,
EventHandler: visitEventHandler,
Binding: visitBinding,
Ref: visitRef,
};
export default function visitComponent(
generator: DomGenerator,
block: Block,
state: State,
node: Node
) {
const hasChildren = node.children.length > 0;
const name = block.getUniqueName(
(node.name === ':Self' ? generator.name : node.name).toLowerCase()
);
const childState = node._state;
const local = {
name,
namespace: state.namespace,
isComponent: true,
allUsedContexts: [],
staticAttributes: [],
dynamicAttributes: [],
bindings: [],
create: new CodeBuilder(),
update: new CodeBuilder(),
};
const isTopLevel = !state.parentNode;
generator.hasComponents = true;
node.attributes
.sort((a, b) => order[a.type] - order[b.type])
.forEach(attribute => {
visitors[attribute.type](
generator,
block,
childState,
node,
attribute,
local
);
});
if (local.allUsedContexts.length) {
const initialProps = local.allUsedContexts
.map(contextName => {
if (contextName === 'state') return `state: state`;
const listName = block.listNames.get(contextName);
const indexName = block.indexNames.get(contextName);
return `${listName}: ${listName},\n${indexName}: ${indexName}`;
})
.join(',\n');
const updates = local.allUsedContexts
.map(contextName => {
if (contextName === 'state') return `${name}._context.state = state;`;
const listName = block.listNames.get(contextName);
const indexName = block.indexNames.get(contextName);
return `${name}._context.${listName} = ${listName};\n${name}._context.${indexName} = ${indexName};`;
})
.join('\n');
local.create.addBlock(deindent`
${name}._context = {
${initialProps}
};
`);
local.update.addBlock(updates);
}
const componentInitProperties = [`_root: #component._root`];
// Component has children, put them in a separate {{yield}} block
if (hasChildren) {
const params = block.params.join(', ');
const childBlock = node._block;
node.children.forEach((child: Node) => {
visit(generator, childBlock, childState, child);
});
const yieldFragment = block.getUniqueName(`${name}_yield_fragment`);
block.builders.init.addLine(
`var ${yieldFragment} = ${childBlock.name}( ${params}, #component );`
);
block.builders.create.addLine(`${yieldFragment}.create();`);
block.builders.claim.addLine(
`${yieldFragment}.claim( ${state.parentNodes} );`
);
if (childBlock.hasUpdateMethod) {
block.builders.update.addLine(
`${yieldFragment}.update( changed, ${params} );`
);
}
block.builders.destroy.addLine(`${yieldFragment}.destroy();`);
componentInitProperties.push(`_yield: ${yieldFragment}`);
}
const statements: string[] = [];
if (
local.staticAttributes.length ||
local.dynamicAttributes.length ||
local.bindings.length
) {
const initialProps = local.staticAttributes
.concat(local.dynamicAttributes)
.map(attribute => `${attribute.name}: ${attribute.value}`);
const initialPropString = stringifyProps(initialProps);
if (local.bindings.length) {
const initialData = block.getUniqueName(`${name}_initial_data`);
statements.push(`var ${initialData} = ${initialPropString};`);
local.bindings.forEach(binding => {
statements.push(
`if ( ${binding.prop} in ${binding.obj} ) ${initialData}.${binding.name} = ${binding.value};`
);
});
componentInitProperties.push(`data: ${initialData}`);
} else if (initialProps.length) {
componentInitProperties.push(`data: ${initialPropString}`);
}
}
const expression = node.name === ':Self'
? generator.name
: generator.importedComponents.get(node.name) ||
`@template.components.${node.name}`;
local.create.addBlockAtStart(deindent`
${statements.join('\n')}
var ${name} = new ${expression}({
${componentInitProperties.join(',\n')}
});
`);
if (local.dynamicAttributes.length) {
const updates = local.dynamicAttributes.map(attribute => {
if (attribute.dependencies.length) {
return deindent`
if ( ${attribute.dependencies
.map(dependency => `'${dependency}' in changed`)
.join(
'||'
)} ) ${name}_changes.${attribute.name} = ${attribute.value};
`;
}
// TODO this is an odd situation to encounter I *think* it should only happen with
// each block indices, in which case it may be possible to optimise this
return `${name}_changes.${attribute.name} = ${attribute.value};`;
});
local.update.addBlock(deindent`
var ${name}_changes = {};
${updates.join('\n')}
if ( Object.keys( ${name}_changes ).length ) ${name}.set( ${name}_changes );
`);
}
if (isTopLevel)
block.builders.unmount.addLine(`${name}._fragment.unmount();`);
block.builders.destroy.addLine(`${name}.destroy( false );`);
block.builders.init.addBlock(local.create);
const targetNode = state.parentNode || '#target';
const anchorNode = state.parentNode ? 'null' : 'anchor';
block.builders.create.addLine(`${name}._fragment.create();`);
block.builders.claim.addLine(
`${name}._fragment.claim( ${state.parentNodes} );`
);
block.builders.mount.addLine(
`${name}._fragment.mount( ${targetNode}, ${anchorNode} );`
);
if (!local.update.isEmpty()) block.builders.update.addBlock(local.update);
}

@ -1,35 +0,0 @@
import deindent from '../../../../utils/deindent.js';
export default function visitEventHandler ( generator, block, state, node, attribute, local ) {
// TODO verify that it's a valid callee (i.e. built-in or declared method)
generator.addSourcemapLocations( attribute.expression );
generator.code.prependRight( attribute.expression.start, `${block.component}.` );
const usedContexts = [];
attribute.expression.arguments.forEach( arg => {
const { contexts } = block.contextualise( arg, null, true );
contexts.forEach( context => {
if ( !~usedContexts.indexOf( context ) ) usedContexts.push( context );
if ( !~local.allUsedContexts.indexOf( context ) ) local.allUsedContexts.push( context );
});
});
// TODO hoist event handlers? can do `this.__component.method(...)`
const declarations = usedContexts.map( name => {
if ( name === 'state' ) return 'var state = this._context.state;';
const listName = block.listNames.get( name );
const indexName = block.indexNames.get( name );
return `var ${listName} = this._context.${listName}, ${indexName} = this._context.${indexName}, ${name} = ${listName}[${indexName}]`;
});
const handlerBody = ( declarations.length ? declarations.join( '\n' ) + '\n\n' : '' ) + `[✂${attribute.expression.start}-${attribute.expression.end}✂];`;
local.create.addBlock( deindent`
${local.name}.on( '${attribute.name}', function ( event ) {
${handlerBody}
});
` );
}

@ -0,0 +1,52 @@
import deindent from '../../../../utils/deindent';
import { DomGenerator } from '../../index';
import Block from '../../Block';
import { Node } from '../../../../interfaces';
import { State } from '../../interfaces';
export default function visitEventHandler(
generator: DomGenerator,
block: Block,
state: State,
node: Node,
attribute: Node,
local
) {
// TODO verify that it's a valid callee (i.e. built-in or declared method)
generator.addSourcemapLocations(attribute.expression);
generator.code.prependRight(
attribute.expression.start,
`${block.alias('component')}.`
);
const usedContexts: string[] = [];
attribute.expression.arguments.forEach((arg: Node) => {
const { contexts } = block.contextualise(arg, null, true);
contexts.forEach(context => {
if (!~usedContexts.indexOf(context)) usedContexts.push(context);
if (!~local.allUsedContexts.indexOf(context))
local.allUsedContexts.push(context);
});
});
// TODO hoist event handlers? can do `this.__component.method(...)`
const declarations = usedContexts.map(name => {
if (name === 'state') return 'var state = this._context.state;';
const listName = block.listNames.get(name);
const indexName = block.indexNames.get(name);
return `var ${listName} = this._context.${listName}, ${indexName} = this._context.${indexName}, ${name} = ${listName}[${indexName}]`;
});
const handlerBody =
(declarations.length ? declarations.join('\n') + '\n\n' : '') +
`[✂${attribute.expression.start}-${attribute.expression.end}✂];`;
local.create.addBlock(deindent`
${local.name}.on( '${attribute.name}', function ( event ) {
${handlerBody}
});
`);
}

@ -1,13 +0,0 @@
import deindent from '../../../../utils/deindent.js';
export default function visitRef ( generator, block, state, node, attribute, local ) {
generator.usesRefs = true;
local.create.addLine(
`${block.component}.refs.${attribute.name} = ${local.name};`
);
block.builders.destroy.addLine( deindent`
if ( ${block.component}.refs.${attribute.name} === ${local.name} ) ${block.component}.refs.${attribute.name} = null;
` );
}

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

@ -1,247 +0,0 @@
import CodeBuilder from '../../../utils/CodeBuilder.js';
import deindent from '../../../utils/deindent.js';
import visit from '../visit.js';
export default function visitEachBlock ( generator, block, state, node ) {
const each_block = generator.getUniqueName( `each_block` );
const create_each_block = node._block.name;
const each_block_value = node._block.listName;
const iterations = block.getUniqueName( `${each_block}_iterations` );
const i = block.alias( `i` );
const params = block.params.join( ', ' );
const anchor = block.getUniqueName( `${each_block}_anchor` );
const vars = { each_block, create_each_block, each_block_value, iterations, i, params, anchor };
const { snippet } = block.contextualise( node.expression );
block.createAnchor( anchor, state.parentNode );
block.builders.create.addLine( `var ${each_block_value} = ${snippet};` );
block.builders.create.addLine( `var ${iterations} = [];` );
if ( node.key ) {
keyed( generator, block, state, node, snippet, vars );
} else {
unkeyed( generator, block, state, node, snippet, vars );
}
const isToplevel = !state.parentNode;
if ( isToplevel ) {
block.builders.mount.addBlock( deindent`
for ( var ${i} = 0; ${i} < ${iterations}.length; ${i} += 1 ) {
${iterations}[${i}].mount( ${block.target}, ${anchor} );
}
` );
}
block.builders.destroy.addBlock(
`${generator.helper( 'destroyEach' )}( ${iterations}, ${isToplevel ? 'detach' : 'false'}, 0 );` );
if ( node.else ) {
const each_block_else = generator.getUniqueName( `${each_block}_else` );
block.builders.create.addLine( `var ${each_block_else} = null;` );
// TODO neaten this up... will end up with an empty line in the block
block.builders.create.addBlock( deindent`
if ( !${each_block_value}.length ) {
${each_block_else} = ${node.else._block.name}( ${params}, ${block.component} );
${!isToplevel ? `${each_block_else}.mount( ${state.parentNode}, ${anchor} );` : ''}
}
` );
block.builders.mount.addBlock( deindent`
if ( ${each_block_else} ) {
${each_block_else}.mount( ${state.parentNode || block.target}, ${anchor} );
}
` );
if ( node.else._block.hasUpdateMethod ) {
block.builders.update.addBlock( deindent`
if ( !${each_block_value}.length && ${each_block_else} ) {
${each_block_else}.update( changed, ${params} );
} else if ( !${each_block_value}.length ) {
${each_block_else} = ${node.else._block.name}( ${params}, ${block.component} );
${each_block_else}.mount( ${anchor}.parentNode, ${anchor} );
} else if ( ${each_block_else} ) {
${each_block_else}.destroy( true );
${each_block_else} = null;
}
` );
} else {
block.builders.update.addBlock( deindent`
if ( ${each_block_value}.length ) {
if ( ${each_block_else} ) {
${each_block_else}.destroy( true );
${each_block_else} = null;
}
} else if ( !${each_block_else} ) {
${each_block_else} = ${node.else._block.name}( ${params}, ${block.component} );
${each_block_else}.mount( ${anchor}.parentNode, ${anchor} );
}
` );
}
block.builders.destroy.addBlock( deindent`
if ( ${each_block_else} ) {
${each_block_else}.destroy( ${isToplevel ? 'detach' : 'false'} );
}
` );
}
const childState = Object.assign( {}, state, {
parentNode: null,
inEachBlock: true
});
node.children.forEach( child => {
visit( generator, node._block, childState, child );
});
if ( node.else ) {
const childState = Object.assign( {}, state, {
parentNode: null
});
node.else.children.forEach( child => {
visit( generator, node.else._block, childState, child );
});
}
}
function keyed ( generator, block, state, node, snippet, { each_block, create_each_block, each_block_value, iterations, i, params, anchor } ) {
const fragment = block.getUniqueName( 'fragment' );
const value = block.getUniqueName( 'value' );
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 _iterations = block.getUniqueName( `_${each_block}_iterations` );
block.builders.create.addLine( `var ${lookup} = Object.create( null );` );
const create = new CodeBuilder();
create.addBlock( deindent`
var ${key} = ${each_block_value}[${i}].${node.key};
${iterations}[${i}] = ${lookup}[ ${key} ] = ${create_each_block}( ${params}, ${each_block_value}, ${each_block_value}[${i}], ${i}, ${block.component}${node.key ? `, ${key}` : `` } );
` );
if ( state.parentNode ) {
create.addLine(
`${iterations}[${i}].mount( ${state.parentNode}, ${anchor} );`
);
}
block.builders.create.addBlock( deindent`
for ( var ${i} = 0; ${i} < ${each_block_value}.length; ${i} += 1 ) {
${create}
}
` );
const consequent = node._block.hasUpdateMethod ?
deindent`
${_iterations}[${i}] = ${_lookup}[ ${key} ] = ${lookup}[ ${key} ];
${_lookup}[ ${key} ].update( changed, ${params}, ${each_block_value}, ${each_block_value}[${i}], ${i} );
` :
`${_iterations}[${i}] = ${_lookup}[ ${key} ] = ${lookup}[ ${key} ];`;
block.builders.update.addBlock( deindent`
var ${each_block_value} = ${snippet};
var ${_iterations} = [];
var ${_lookup} = Object.create( null );
var ${fragment} = document.createDocumentFragment();
// create new iterations as necessary
for ( var ${i} = 0; ${i} < ${each_block_value}.length; ${i} += 1 ) {
var ${value} = ${each_block_value}[${i}];
var ${key} = ${value}.${node.key};
if ( ${lookup}[ ${key} ] ) {
${consequent}
} else {
${_iterations}[${i}] = ${_lookup}[ ${key} ] = ${create_each_block}( ${params}, ${each_block_value}, ${each_block_value}[${i}], ${i}, ${block.component}${node.key ? `, ${key}` : `` } );
}
${_iterations}[${i}].mount( ${fragment}, null );
}
// remove old iterations
for ( var ${i} = 0; ${i} < ${iterations}.length; ${i} += 1 ) {
var ${iteration} = ${iterations}[${i}];
if ( !${_lookup}[ ${iteration}.key ] ) {
${iteration}.destroy( true );
}
}
${anchor}.parentNode.insertBefore( ${fragment}, ${anchor} );
${iterations} = ${_iterations};
${lookup} = ${_lookup};
` );
}
function unkeyed ( generator, block, state, node, snippet, { create_each_block, each_block_value, iterations, i, params, anchor } ) {
const create = new CodeBuilder();
create.addLine(
`${iterations}[${i}] = ${create_each_block}( ${params}, ${each_block_value}, ${each_block_value}[${i}], ${i}, ${block.component} );`
);
if ( state.parentNode ) {
create.addLine(
`${iterations}[${i}].mount( ${state.parentNode}, ${anchor} );`
);
}
block.builders.create.addBlock( deindent`
for ( var ${i} = 0; ${i} < ${each_block_value}.length; ${i} += 1 ) {
${create}
}
` );
const dependencies = block.findDependencies( node.expression );
const allDependencies = new Set( node._block.dependencies );
dependencies.forEach( dependency => {
allDependencies.add( dependency );
});
const condition = Array.from( allDependencies )
.map( dependency => `'${dependency}' in changed` )
.join( ' || ' );
if ( condition !== '' ) {
const forLoopBody = node._block.hasUpdateMethod ?
deindent`
if ( ${iterations}[${i}] ) {
${iterations}[${i}].update( changed, ${params}, ${each_block_value}, ${each_block_value}[${i}], ${i} );
} else {
${iterations}[${i}] = ${create_each_block}( ${params}, ${each_block_value}, ${each_block_value}[${i}], ${i}, ${block.component} );
${iterations}[${i}].mount( ${anchor}.parentNode, ${anchor} );
}
` :
deindent`
${iterations}[${i}] = ${create_each_block}( ${params}, ${each_block_value}, ${each_block_value}[${i}], ${i}, ${block.component} );
${iterations}[${i}].mount( ${anchor}.parentNode, ${anchor} );
`;
const start = node._block.hasUpdateMethod ? '0' : `${iterations}.length`;
block.builders.update.addBlock( deindent`
var ${each_block_value} = ${snippet};
if ( ${condition} ) {
for ( var ${i} = ${start}; ${i} < ${each_block_value}.length; ${i} += 1 ) {
${forLoopBody}
}
${generator.helper( 'destroyEach' )}( ${iterations}, true, ${each_block_value}.length );
${iterations}.length = ${each_block_value}.length;
}
` );
}
}

@ -0,0 +1,479 @@
import deindent from '../../../utils/deindent';
import visit from '../visit';
import { DomGenerator } from '../index';
import Block from '../Block';
import { Node } from '../../../interfaces';
import { State } from '../interfaces';
export default function visitEachBlock(
generator: DomGenerator,
block: Block,
state: State,
node: Node
) {
const each_block = generator.getUniqueName(`each_block`);
const create_each_block = node._block.name;
const each_block_value = node._block.listName;
const iterations = block.getUniqueName(`${each_block}_iterations`);
const params = block.params.join(', ');
const anchor = node.needsAnchor
? block.getUniqueName(`${each_block}_anchor`)
: (node.next && node.next._state.name) || 'null';
const mountOrIntro = node._block.hasIntroMethod ? 'intro' : 'mount';
const vars = {
each_block,
create_each_block,
each_block_value,
iterations,
params,
anchor,
mountOrIntro,
};
const { snippet } = block.contextualise(node.expression);
block.builders.init.addLine(`var ${each_block_value} = ${snippet};`);
if (node.key) {
keyed(generator, block, state, node, snippet, vars);
} else {
unkeyed(generator, block, state, node, snippet, vars);
}
const isToplevel = !state.parentNode;
if (node.needsAnchor) {
block.addElement(
anchor,
`@createComment()`,
`@createComment()`,
state.parentNode,
true
);
} else if (node.next) {
node.next.usedAsAnchor = true;
}
if (node.else) {
const each_block_else = generator.getUniqueName(`${each_block}_else`);
block.builders.init.addLine(`var ${each_block_else} = null;`);
// TODO neaten this up... will end up with an empty line in the block
block.builders.init.addBlock(deindent`
if ( !${each_block_value}.length ) {
${each_block_else} = ${node.else._block.name}( ${params}, #component );
${each_block_else}.create();
}
`);
block.builders.mount.addBlock(deindent`
if ( ${each_block_else} ) {
${each_block_else}.${mountOrIntro}( ${state.parentNode ||
'#target'}, null );
}
`);
const parentNode = state.parentNode || `${anchor}.parentNode`;
if (node.else._block.hasUpdateMethod) {
block.builders.update.addBlock(deindent`
if ( !${each_block_value}.length && ${each_block_else} ) {
${each_block_else}.update( changed, ${params} );
} else if ( !${each_block_value}.length ) {
${each_block_else} = ${node.else._block.name}( ${params}, #component );
${each_block_else}.create();
${each_block_else}.${mountOrIntro}( ${parentNode}, ${anchor} );
} else if ( ${each_block_else} ) {
${each_block_else}.unmount();
${each_block_else}.destroy();
${each_block_else} = null;
}
`);
} else {
block.builders.update.addBlock(deindent`
if ( ${each_block_value}.length ) {
if ( ${each_block_else} ) {
${each_block_else}.unmount();
${each_block_else}.destroy();
${each_block_else} = null;
}
} else if ( !${each_block_else} ) {
${each_block_else} = ${node.else._block.name}( ${params}, #component );
${each_block_else}.create();
${each_block_else}.${mountOrIntro}( ${parentNode}, ${anchor} );
}
`);
}
block.builders.unmount.addLine(
`if ( ${each_block_else} ) ${each_block_else}.unmount()`
);
block.builders.destroy.addBlock(deindent`
if ( ${each_block_else} ) ${each_block_else}.destroy( false );
`);
}
node.children.forEach((child: Node) => {
visit(generator, node._block, node._state, child);
});
if (node.else) {
node.else.children.forEach((child: Node) => {
visit(generator, node.else._block, node.else._state, child);
});
}
}
function keyed(
generator: DomGenerator,
block: Block,
state: State,
node: Node,
snippet: string,
{
each_block,
create_each_block,
each_block_value,
params,
anchor,
mountOrIntro,
}
) {
const key = block.getUniqueName('key');
const lookup = block.getUniqueName(`${each_block}_lookup`);
const iteration = block.getUniqueName(`${each_block}_iteration`);
const head = block.getUniqueName(`${each_block}_head`);
const last = block.getUniqueName(`${each_block}_last`);
const expected = block.getUniqueName(`${each_block}_expected`);
block.addVariable(lookup, `Object.create( null )`);
block.addVariable(head);
block.addVariable(last);
if (node.children[0] && node.children[0].type === 'Element') {
// TODO or text/tag/raw
node._block.first = node.children[0]._state.parentNode; // TODO this is highly confusing
} else {
node._block.first = node._block.getUniqueName('first');
node._block.addElement(
node._block.first,
`@createComment()`,
`@createComment()`,
null,
true
);
}
block.builders.init.addBlock(deindent`
for ( var #i = 0; #i < ${each_block_value}.length; #i += 1 ) {
var ${key} = ${each_block_value}[#i].${node.key};
var ${iteration} = ${lookup}[${key}] = ${create_each_block}( ${params}, ${each_block_value}, ${each_block_value}[#i], #i, #component, ${key} );
if ( ${last} ) ${last}.next = ${iteration};
${iteration}.last = ${last};
${last} = ${iteration};
if ( #i === 0 ) ${head} = ${iteration};
}
`);
const targetNode = state.parentNode || '#target';
const anchorNode = state.parentNode ? 'null' : 'anchor';
block.builders.create.addBlock(deindent`
var ${iteration} = ${head};
while ( ${iteration} ) {
${iteration}.create();
${iteration} = ${iteration}.next;
}
`);
block.builders.claim.addBlock(deindent`
var ${iteration} = ${head};
while ( ${iteration} ) {
${iteration}.claim( ${state.parentNodes} );
${iteration} = ${iteration}.next;
}
`);
block.builders.mount.addBlock(deindent`
var ${iteration} = ${head};
while ( ${iteration} ) {
${iteration}.${mountOrIntro}( ${targetNode}, ${anchorNode} );
${iteration} = ${iteration}.next;
}
`);
const dynamic = node._block.hasUpdateMethod;
const parentNode = state.parentNode || `${anchor}.parentNode`;
let destroy;
if (node._block.hasOutroMethod) {
const fn = block.getUniqueName(`${each_block}_outro`);
block.builders.init.addBlock(deindent`
function ${fn} ( iteration ) {
iteration.outro( function () {
iteration.unmount();
iteration.destroy();
${lookup}[iteration.key] = null;
});
}
`);
destroy = deindent`
while ( ${expected} ) {
${fn}( ${expected} );
${expected} = ${expected}.next;
}
for ( #i = 0; #i < discard_pile.length; #i += 1 ) {
if ( discard_pile[#i].discard ) {
${fn}( discard_pile[#i] );
}
}
`;
} else {
const fn = block.getUniqueName(`${each_block}_destroy`);
block.builders.init.addBlock(deindent`
function ${fn} ( iteration ) {
iteration.unmount();
iteration.destroy();
${lookup}[iteration.key] = null;
}
`);
destroy = deindent`
while ( ${expected} ) {
${fn}( ${expected} );
${expected} = ${expected}.next;
}
for ( #i = 0; #i < discard_pile.length; #i += 1 ) {
var ${iteration} = discard_pile[#i];
if ( ${iteration}.discard ) {
${fn}( ${iteration} );
}
}
`;
}
block.builders.update.addBlock(deindent`
var ${each_block_value} = ${snippet};
var ${expected} = ${head};
var ${last} = null;
var discard_pile = [];
for ( #i = 0; #i < ${each_block_value}.length; #i += 1 ) {
var ${key} = ${each_block_value}[#i].${node.key};
var ${iteration} = ${lookup}[${key}];
${dynamic &&
`if ( ${iteration} ) ${iteration}.update( changed, ${params}, ${each_block_value}, ${each_block_value}[#i], #i );`}
if ( ${expected} ) {
if ( ${key} === ${expected}.key ) {
${expected} = ${expected}.next;
} else {
if ( ${iteration} ) {
// probably a deletion
while ( ${expected} && ${expected}.key !== ${key} ) {
${expected}.discard = true;
discard_pile.push( ${expected} );
${expected} = ${expected}.next;
};
${expected} = ${expected} && ${expected}.next;
${iteration}.discard = false;
${iteration}.last = ${last};
if (!${expected}) ${iteration}.mount( ${parentNode}, ${anchor} );
} else {
// key is being inserted
${iteration} = ${lookup}[${key}] = ${create_each_block}( ${params}, ${each_block_value}, ${each_block_value}[#i], #i, #component, ${key} );
${iteration}.create();
${iteration}.${mountOrIntro}( ${parentNode}, ${expected}.first );
${expected}.last = ${iteration};
${iteration}.next = ${expected};
}
}
} else {
// we're appending from this point forward
if ( ${iteration} ) {
${iteration}.discard = false;
${iteration}.next = null;
${iteration}.mount( ${parentNode}, ${anchor} );
} else {
${iteration} = ${lookup}[${key}] = ${create_each_block}( ${params}, ${each_block_value}, ${each_block_value}[#i], #i, #component, ${key} );
${iteration}.create();
${iteration}.${mountOrIntro}( ${parentNode}, ${anchor} );
}
}
if ( ${last} ) ${last}.next = ${iteration};
${iteration}.last = ${last};
${node._block.hasIntroMethod &&
`${iteration}.intro( ${parentNode}, ${anchor} );`}
${last} = ${iteration};
}
if ( ${last} ) ${last}.next = null;
${destroy}
${head} = ${lookup}[${each_block_value}[0] && ${each_block_value}[0].${node.key}];
`);
if (!state.parentNode) {
block.builders.unmount.addBlock(deindent`
var ${iteration} = ${head};
while ( ${iteration} ) {
${iteration}.unmount();
${iteration} = ${iteration}.next;
}
`);
}
block.builders.destroy.addBlock(deindent`
var ${iteration} = ${head};
while ( ${iteration} ) {
${iteration}.destroy( false );
${iteration} = ${iteration}.next;
}
`);
}
function unkeyed(
generator: DomGenerator,
block: Block,
state: State,
node: Node,
snippet,
{
create_each_block,
each_block_value,
iterations,
params,
anchor,
mountOrIntro,
}
) {
block.builders.init.addBlock(deindent`
var ${iterations} = [];
for ( var #i = 0; #i < ${each_block_value}.length; #i += 1 ) {
${iterations}[#i] = ${create_each_block}( ${params}, ${each_block_value}, ${each_block_value}[#i], #i, #component );
}
`);
const targetNode = state.parentNode || '#target';
const anchorNode = state.parentNode ? 'null' : 'anchor';
block.builders.create.addBlock(deindent`
for ( var #i = 0; #i < ${iterations}.length; #i += 1 ) {
${iterations}[#i].create();
}
`);
block.builders.claim.addBlock(deindent`
for ( var #i = 0; #i < ${iterations}.length; #i += 1 ) {
${iterations}[#i].claim( ${state.parentNodes} );
}
`);
block.builders.mount.addBlock(deindent`
for ( var #i = 0; #i < ${iterations}.length; #i += 1 ) {
${iterations}[#i].${mountOrIntro}( ${targetNode}, ${anchorNode} );
}
`);
const dependencies = block.findDependencies(node.expression);
const allDependencies = new Set(node._block.dependencies);
dependencies.forEach((dependency: string) => {
allDependencies.add(dependency);
});
// TODO do this for keyed blocks as well
const condition = Array.from(allDependencies)
.map(dependency => `'${dependency}' in changed`)
.join(' || ');
const parentNode = state.parentNode || `${anchor}.parentNode`;
if (condition !== '') {
const forLoopBody = node._block.hasUpdateMethod
? node._block.hasIntroMethod
? deindent`
if ( ${iterations}[#i] ) {
${iterations}[#i].update( changed, ${params}, ${each_block_value}, ${each_block_value}[#i], #i );
} else {
${iterations}[#i] = ${create_each_block}( ${params}, ${each_block_value}, ${each_block_value}[#i], #i, #component );
${iterations}[#i].create();
}
${iterations}[#i].intro( ${parentNode}, ${anchor} );
`
: deindent`
if ( ${iterations}[#i] ) {
${iterations}[#i].update( changed, ${params}, ${each_block_value}, ${each_block_value}[#i], #i );
} else {
${iterations}[#i] = ${create_each_block}( ${params}, ${each_block_value}, ${each_block_value}[#i], #i, #component );
${iterations}[#i].create();
${iterations}[#i].mount( ${parentNode}, ${anchor} );
}
`
: deindent`
${iterations}[#i] = ${create_each_block}( ${params}, ${each_block_value}, ${each_block_value}[#i], #i, #component );
${iterations}[#i].${mountOrIntro}( ${parentNode}, ${anchor} );
`;
const start = node._block.hasUpdateMethod ? '0' : `${iterations}.length`;
const outro = block.getUniqueName('outro');
const destroy = node._block.hasOutroMethod
? deindent`
function ${outro} ( i ) {
if ( ${iterations}[i] ) {
${iterations}[i].outro( function () {
${iterations}[i].unmount();
${iterations}[i].destroy();
${iterations}[i] = null;
});
}
}
for ( ; #i < ${iterations}.length; #i += 1 ) ${outro}( #i );
`
: deindent`
for ( ; #i < ${iterations}.length; #i += 1 ) {
${iterations}[#i].unmount();
${iterations}[#i].destroy();
}
${iterations}.length = ${each_block_value}.length;
`;
block.builders.update.addBlock(deindent`
var ${each_block_value} = ${snippet};
if ( ${condition} ) {
for ( var #i = ${start}; #i < ${each_block_value}.length; #i += 1 ) {
${forLoopBody}
}
${destroy}
}
`);
}
block.builders.unmount.addBlock(deindent`
for ( var #i = 0; #i < ${iterations}.length; #i += 1 ) {
${iterations}[#i].unmount();
}
`);
block.builders.destroy.addBlock(`@destroyEach( ${iterations}, false, 0 );`);
}

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

@ -0,0 +1,159 @@
import attributeLookup from './lookup';
import deindent from '../../../../utils/deindent';
import stringify from '../../../../utils/stringify';
import getStaticAttributeValue from './getStaticAttributeValue';
import { DomGenerator } from '../../index';
import Block from '../../Block';
import { Node } from '../../../../interfaces';
import { State } from '../../interfaces';
export default function visitAttribute(
generator: DomGenerator,
block: Block,
state: State,
node: Node,
attribute: Node
) {
const name = attribute.name;
let metadata = state.namespace ? null : attributeLookup[name];
if (metadata && metadata.appliesTo && !~metadata.appliesTo.indexOf(node.name))
metadata = null;
const isIndirectlyBoundValue =
name === 'value' &&
(node.name === 'option' || // TODO check it's actually bound
(node.name === 'input' &&
node.attributes.find(
(attribute: Node) =>
attribute.type === 'Binding' && /checked|group/.test(attribute.name)
)));
const propertyName = isIndirectlyBoundValue
? '__value'
: metadata && metadata.propertyName;
// xlink is a special case... we could maybe extend this to generic
// namespaced attributes but I'm not sure that's applicable in
// HTML5?
const method = name.slice(0, 6) === 'xlink:'
? '@setXlinkAttribute'
: '@setAttribute';
const isDynamic =
(attribute.value !== true && attribute.value.length > 1) ||
(attribute.value.length === 1 && attribute.value[0].type !== 'Text');
if (isDynamic) {
let value;
if (attribute.value.length === 1) {
// single {{tag}} — may be a non-string
const { snippet } = block.contextualise(attribute.value[0].expression);
value = snippet;
} else {
// '{{foo}} {{bar}}' — treat as string concatenation
value =
(attribute.value[0].type === 'Text' ? '' : `"" + `) +
attribute.value
.map((chunk: Node) => {
if (chunk.type === 'Text') {
return stringify(chunk.data);
} else {
const { snippet } = block.contextualise(chunk.expression);
return `( ${snippet} )`;
}
})
.join(' + ');
}
const last = block.getUniqueName(
`${state.parentNode}_${name.replace(/[^a-zA-Z_$]/g, '_')}_value`
);
block.addVariable(last);
const isSelectValueAttribute =
name === 'value' && state.parentNodeName === 'select';
let updater;
if (isSelectValueAttribute) {
// annoying special case
const isMultipleSelect =
node.name === 'select' &&
node.attributes.find(
(attr: Node) => attr.name.toLowerCase() === 'multiple'
); // TODO use getStaticAttributeValue
const i = block.getUniqueName('i');
const option = block.getUniqueName('option');
const ifStatement = isMultipleSelect
? deindent`
${option}.selected = ~${last}.indexOf( ${option}.__value );`
: deindent`
if ( ${option}.__value === ${last} ) {
${option}.selected = true;
break;
}`;
updater = deindent`
for ( var ${i} = 0; ${i} < ${state.parentNode}.options.length; ${i} += 1 ) {
var ${option} = ${state.parentNode}.options[${i}];
${ifStatement}
}
`;
block.builders.hydrate.addLine(deindent`
${last} = ${value}
${updater}
`);
} else if (propertyName) {
block.builders.hydrate.addLine(
`${state.parentNode}.${propertyName} = ${last} = ${value};`
);
updater = `${state.parentNode}.${propertyName} = ${last};`;
} else {
block.builders.hydrate.addLine(
`${method}( ${state.parentNode}, '${name}', ${last} = ${value} );`
);
updater = `${method}( ${state.parentNode}, '${name}', ${last} );`;
}
block.builders.update.addBlock(deindent`
if ( ${last} !== ( ${last} = ${value} ) ) {
${updater}
}
`);
} else {
const value = attribute.value === true
? 'true'
: attribute.value.length === 0
? `''`
: stringify(attribute.value[0].data);
const statement = propertyName
? `${state.parentNode}.${propertyName} = ${value};`
: `${method}( ${state.parentNode}, '${name}', ${value} );`;
block.builders.hydrate.addLine(statement);
// special case autofocus. has to be handled in a bit of a weird way
if (attribute.value === true && name === 'autofocus') {
block.autofocus = state.parentNode;
}
// special case — xmlns
if (name === 'xmlns') {
// TODO this attribute must be static enforce at compile time
state.namespace = attribute.value[0].data;
}
}
if (isIndirectlyBoundValue) {
const updateValue = `${state.parentNode}.value = ${state.parentNode}.__value;`;
block.builders.hydrate.addLine(updateValue);
if (isDynamic) block.builders.update.addLine(updateValue);
}
}

@ -1,199 +0,0 @@
import deindent from '../../../../utils/deindent.js';
import flattenReference from '../../../../utils/flattenReference.js';
import getSetter from '../shared/binding/getSetter.js';
import getStaticAttributeValue from './getStaticAttributeValue.js';
export default function visitBinding ( generator, block, state, node, attribute ) {
const { name, parts } = flattenReference( attribute.value );
const { snippet, contexts, dependencies } = block.contextualise( attribute.value );
if ( dependencies.length > 1 ) throw new Error( 'An unexpected situation arose. Please raise an issue at https://github.com/sveltejs/svelte/issues — thanks!' );
contexts.forEach( context => {
if ( !~state.allUsedContexts.indexOf( context ) ) state.allUsedContexts.push( context );
});
const eventName = getBindingEventName( node, attribute );
const handler = block.getUniqueName( `${state.parentNode}_${eventName}_handler` );
const isMultipleSelect = node.name === 'select' && node.attributes.find( attr => attr.name.toLowerCase() === 'multiple' ); // TODO use getStaticAttributeValue
const type = getStaticAttributeValue( node, 'type' );
const bindingGroup = attribute.name === 'group' ? getBindingGroup( generator, parts.join( '.' ) ) : null;
const value = getBindingValue( generator, block, state, node, attribute, isMultipleSelect, bindingGroup, type );
let setter = getSetter({ block, name, context: '_svelte', attribute, dependencies, value });
let updateElement = `${state.parentNode}.${attribute.name} = ${snippet};`;
const lock = block.alias( `${state.parentNode}_updating` );
let updateCondition = `!${lock}`;
block.addVariable( lock, 'false' );
// <select> special case
if ( node.name === 'select' ) {
if ( !isMultipleSelect ) {
setter = `var selectedOption = ${state.parentNode}.selectedOptions[0] || ${state.parentNode}.options[0];\n${setter}`;
}
const value = block.getUniqueName( 'value' );
const i = block.alias( 'i' );
const option = block.getUniqueName( 'option' );
const ifStatement = isMultipleSelect ?
deindent`
${option}.selected = ~${value}.indexOf( ${option}.__value );` :
deindent`
if ( ${option}.__value === ${value} ) {
${option}.selected = true;
break;
}`;
updateElement = deindent`
var ${value} = ${snippet};
for ( var ${i} = 0; ${i} < ${state.parentNode}.options.length; ${i} += 1 ) {
var ${option} = ${state.parentNode}.options[${i}];
${ifStatement}
}
`;
}
// <input type='checkbox|radio' bind:group='selected'> special case
else if ( attribute.name === 'group' ) {
if ( type === 'radio' ) {
setter = deindent`
if ( !${state.parentNode}.checked ) return;
${setter}
`;
}
const condition = type === 'checkbox' ?
`~${snippet}.indexOf( ${state.parentNode}.__value )` :
`${state.parentNode}.__value === ${snippet}`;
block.builders.create.addLine(
`${block.component}._bindingGroups[${bindingGroup}].push( ${state.parentNode} );`
);
block.builders.destroy.addBlock(
`${block.component}._bindingGroups[${bindingGroup}].splice( ${block.component}._bindingGroups[${bindingGroup}].indexOf( ${state.parentNode} ), 1 );`
);
updateElement = `${state.parentNode}.checked = ${condition};`;
}
else if ( node.name === 'audio' || node.name === 'video' ) {
generator.hasComplexBindings = true;
block.builders.create.addBlock( `${block.component}._bindings.push( ${handler} );` );
if ( attribute.name === 'currentTime' ) {
const frame = block.getUniqueName( `${state.parentNode}_animationframe` );
block.addVariable( frame );
setter = deindent`
cancelAnimationFrame( ${frame} );
if ( !${state.parentNode}.paused ) ${frame} = requestAnimationFrame( ${handler} );
${setter}
`;
updateCondition += ` && !isNaN( ${snippet} )`;
}
else if ( attribute.name === 'duration' ) {
updateCondition = null;
}
else if ( attribute.name === 'paused' ) {
// this is necessary to prevent the audio restarting by itself
const last = block.getUniqueName( `${state.parentNode}_paused_value` );
block.addVariable( last, 'true' );
updateCondition = `${last} !== ( ${last} = ${snippet} )`;
updateElement = `${state.parentNode}[ ${last} ? 'pause' : 'play' ]();`;
}
}
block.builders.create.addBlock( deindent`
function ${handler} () {
${lock} = true;
${setter}
${lock} = false;
}
${generator.helper( 'addEventListener' )}( ${state.parentNode}, '${eventName}', ${handler} );
` );
if ( node.name !== 'audio' && node.name !== 'video' ) node.initialUpdate = updateElement;
if ( updateCondition !== null ) {
// audio/video duration is read-only, it never updates
block.builders.update.addBlock( deindent`
if ( ${updateCondition} ) {
${updateElement}
}
` );
}
block.builders.destroy.addLine( deindent`
${generator.helper( 'removeEventListener' )}( ${state.parentNode}, '${eventName}', ${handler} );
` );
if ( attribute.name === 'paused' ) {
block.builders.create.addLine( `${generator.helper( 'addEventListener' )}( ${state.parentNode}, 'play', ${handler} );` );
block.builders.destroy.addLine( `${generator.helper( 'removeEventListener' )}( ${state.parentNode}, 'play', ${handler} );` );
}
}
function getBindingEventName ( node, attribute ) {
if ( node.name === 'input' ) {
const typeAttribute = node.attributes.find( attr => attr.type === 'Attribute' && attr.name === 'type' );
const type = typeAttribute ? typeAttribute.value[0].data : 'text'; // TODO in validation, should throw if type attribute is not static
return type === 'checkbox' || type === 'radio' ? 'change' : 'input';
}
if ( node.name === 'textarea' ) return 'input';
if ( attribute.name === 'currentTime' ) return 'timeupdate';
if ( attribute.name === 'duration' ) return 'durationchange';
if ( attribute.name === 'paused' ) return 'pause';
return 'change';
}
function getBindingValue ( generator, block, state, node, attribute, isMultipleSelect, bindingGroup, type ) {
// <select multiple bind:value='selected>
if ( isMultipleSelect ) {
return `[].map.call( ${state.parentNode}.selectedOptions, function ( option ) { return option.__value; })`;
}
// <select bind:value='selected>
if ( node.name === 'select' ) {
return 'selectedOption && selectedOption.__value';
}
// <input type='checkbox' bind:group='foo'>
if ( attribute.name === 'group' ) {
if ( type === 'checkbox' ) {
return `${generator.helper( 'getBindingGroupValue' )}( ${block.component}._bindingGroups[${bindingGroup}] )`;
}
return `${state.parentNode}.__value`;
}
// <input type='range|number' bind:value>
if ( type === 'range' || type === 'number' ) {
return `+${state.parentNode}.${attribute.name}`;
}
// everything else
return `${state.parentNode}.${attribute.name}`;
}
function getBindingGroup ( generator, keypath ) {
// TODO handle contextual bindings — `keypath` should include unique ID of
// each block that provides context
let index = generator.bindingGroups.indexOf( keypath );
if ( index === -1 ) {
index = generator.bindingGroups.length;
generator.bindingGroups.push( keypath );
}
return index;
}

@ -0,0 +1,253 @@
import deindent from '../../../../utils/deindent';
import flattenReference from '../../../../utils/flattenReference';
import getSetter from '../shared/binding/getSetter';
import getStaticAttributeValue from './getStaticAttributeValue';
import { DomGenerator } from '../../index';
import Block from '../../Block';
import { Node } from '../../../../interfaces';
import { State } from '../../interfaces';
import getObject from '../../../../utils/getObject';
export default function visitBinding(
generator: DomGenerator,
block: Block,
state: State,
node: Node,
attribute: Node
) {
const { name } = getObject(attribute.value);
const { snippet, contexts, dependencies } = block.contextualise(
attribute.value
);
contexts.forEach(context => {
if (!~state.allUsedContexts.indexOf(context))
state.allUsedContexts.push(context);
});
const eventName = getBindingEventName(node, attribute);
const handler = block.getUniqueName(
`${state.parentNode}_${eventName}_handler`
);
const isMultipleSelect =
node.name === 'select' &&
node.attributes.find(
(attr: Node) => attr.name.toLowerCase() === 'multiple'
); // TODO use getStaticAttributeValue
const type = getStaticAttributeValue(node, 'type');
const bindingGroup = attribute.name === 'group'
? getBindingGroup(generator, attribute.value)
: null;
const value = getBindingValue(
generator,
block,
state,
node,
attribute,
isMultipleSelect,
bindingGroup,
type
);
let setter = getSetter({
block,
name,
snippet,
context: '_svelte',
attribute,
dependencies,
value,
});
let updateElement = `${state.parentNode}.${attribute.name} = ${snippet};`;
const lock = `#${state.parentNode}_updating`;
let updateCondition = `!${lock}`;
block.addVariable(lock, 'false');
// <select> special case
if (node.name === 'select') {
if (!isMultipleSelect) {
setter = `var selectedOption = ${state.parentNode}.querySelector(':checked') || ${state.parentNode}.options[0];\n${setter}`;
}
const value = block.getUniqueName('value');
const option = block.getUniqueName('option');
const ifStatement = isMultipleSelect
? deindent`
${option}.selected = ~${value}.indexOf( ${option}.__value );`
: deindent`
if ( ${option}.__value === ${value} ) {
${option}.selected = true;
break;
}`;
updateElement = deindent`
var ${value} = ${snippet};
for ( var #i = 0; #i < ${state.parentNode}.options.length; #i += 1 ) {
var ${option} = ${state.parentNode}.options[#i];
${ifStatement}
}
`;
generator.hasComplexBindings = true;
block.builders.hydrate.addBlock(
`if ( !('${name}' in state) ) #component._bindings.push( ${handler} );`
);
} else if (attribute.name === 'group') {
// <input type='checkbox|radio' bind:group='selected'> special case
if (type === 'radio') {
setter = deindent`
if ( !${state.parentNode}.checked ) return;
${setter}
`;
}
const condition = type === 'checkbox'
? `~${snippet}.indexOf( ${state.parentNode}.__value )`
: `${state.parentNode}.__value === ${snippet}`;
block.builders.hydrate.addLine(
`#component._bindingGroups[${bindingGroup}].push( ${state.parentNode} );`
);
block.builders.destroy.addBlock(
`#component._bindingGroups[${bindingGroup}].splice( #component._bindingGroups[${bindingGroup}].indexOf( ${state.parentNode} ), 1 );`
);
updateElement = `${state.parentNode}.checked = ${condition};`;
} else if (node.name === 'audio' || node.name === 'video') {
generator.hasComplexBindings = true;
block.builders.hydrate.addBlock(`#component._bindings.push( ${handler} );`);
if (attribute.name === 'currentTime') {
const frame = block.getUniqueName(`${state.parentNode}_animationframe`);
block.addVariable(frame);
setter = deindent`
cancelAnimationFrame( ${frame} );
if ( !${state.parentNode}.paused ) ${frame} = requestAnimationFrame( ${handler} );
${setter}
`;
updateCondition += ` && !isNaN( ${snippet} )`;
} else if (attribute.name === 'duration') {
updateCondition = null;
} else if (attribute.name === 'paused') {
// this is necessary to prevent the audio restarting by itself
const last = block.getUniqueName(`${state.parentNode}_paused_value`);
block.addVariable(last, 'true');
updateCondition = `${last} !== ( ${last} = ${snippet} )`;
updateElement = `${state.parentNode}[ ${last} ? 'pause' : 'play' ]();`;
}
}
block.builders.init.addBlock(deindent`
function ${handler} () {
${lock} = true;
${setter}
${lock} = false;
}
`);
block.builders.hydrate.addBlock(
`@addListener( ${state.parentNode}, '${eventName}', ${handler} );`
);
if (node.name !== 'audio' && node.name !== 'video')
node.initialUpdate = updateElement;
if (updateCondition !== null) {
// audio/video duration is read-only, it never updates
block.builders.update.addBlock(deindent`
if ( ${updateCondition} ) {
${updateElement}
}
`);
}
block.builders.destroy.addLine(
`@removeListener( ${state.parentNode}, '${eventName}', ${handler} );`
);
if (attribute.name === 'paused') {
block.builders.create.addLine(
`@addListener( ${state.parentNode}, 'play', ${handler} );`
);
block.builders.destroy.addLine(
`@removeListener( ${state.parentNode}, 'play', ${handler} );`
);
}
}
function getBindingEventName(node: Node, attribute: Node) {
if (node.name === 'input') {
const typeAttribute = node.attributes.find(
(attr: Node) => attr.type === 'Attribute' && attr.name === 'type'
);
const type = typeAttribute ? typeAttribute.value[0].data : 'text'; // TODO in validation, should throw if type attribute is not static
return type === 'checkbox' || type === 'radio' ? 'change' : 'input';
}
if (node.name === 'textarea') return 'input';
if (attribute.name === 'currentTime') return 'timeupdate';
if (attribute.name === 'duration') return 'durationchange';
if (attribute.name === 'paused') return 'pause';
return 'change';
}
function getBindingValue(
generator: DomGenerator,
block: Block,
state: State,
node: Node,
attribute: Node,
isMultipleSelect: boolean,
bindingGroup: number,
type: string
) {
// <select multiple bind:value='selected>
if (isMultipleSelect) {
return `[].map.call( ${state.parentNode}.querySelectorAll(':checked'), function ( option ) { return option.__value; })`;
}
// <select bind:value='selected>
if (node.name === 'select') {
return 'selectedOption && selectedOption.__value';
}
// <input type='checkbox' bind:group='foo'>
if (attribute.name === 'group') {
if (type === 'checkbox') {
return `@getBindingGroupValue( #component._bindingGroups[${bindingGroup}] )`;
}
return `${state.parentNode}.__value`;
}
// <input type='range|number' bind:value>
if (type === 'range' || type === 'number') {
return `@toNumber( ${state.parentNode}.${attribute.name} )`;
}
// everything else
return `${state.parentNode}.${attribute.name}`;
}
function getBindingGroup(generator: DomGenerator, value: Node) {
const { parts } = flattenReference(value); // TODO handle cases involving computed member expressions
const keypath = parts.join('.');
// TODO handle contextual bindings — `keypath` should include unique ID of
// each block that provides context
let index = generator.bindingGroups.indexOf(keypath);
if (index === -1) {
index = generator.bindingGroups.length;
generator.bindingGroups.push(keypath);
}
return index;
}

@ -1,139 +0,0 @@
import deindent from '../../../../utils/deindent.js';
import visit from '../../visit.js';
import visitComponent from '../Component/Component.js';
import visitWindow from './meta/Window.js';
import visitAttribute from './Attribute.js';
import visitEventHandler from './EventHandler.js';
import visitBinding from './Binding.js';
import visitRef from './Ref.js';
const meta = {
':Window': visitWindow
};
const order = {
Attribute: 1,
Binding: 2,
EventHandler: 3,
Ref: 4
};
const visitors = {
Attribute: visitAttribute,
EventHandler: visitEventHandler,
Binding: visitBinding,
Ref: visitRef
};
export default function visitElement ( generator, block, state, node ) {
if ( node.name in meta ) {
return meta[ node.name ]( generator, block, node );
}
if ( generator.components.has( node.name ) || node.name === ':Self' ) {
return visitComponent( generator, block, state, node );
}
const name = block.getUniqueName( node.name.replace( /[^a-zA-Z_$]/g, '_' ) );
const childState = Object.assign( {}, state, {
isTopLevel: false,
parentNode: name,
parentNodeName: node.name,
namespace: node.name === 'svg' ? 'http://www.w3.org/2000/svg' : state.namespace,
allUsedContexts: []
});
block.builders.create.addLine( `var ${name} = ${getRenderStatement( generator, childState.namespace, node.name )};` );
block.mount( name, state.parentNode );
if ( !state.parentNode ) {
block.builders.detach.addLine( `${generator.helper( 'detachNode' )}( ${name} );` );
}
// add CSS encapsulation attribute
if ( generator.cssId && state.isTopLevel ) {
block.builders.create.addLine( `${generator.helper( 'setAttribute' )}( ${name}, '${generator.cssId}', '' );` );
}
let selectValueAttribute;
node.attributes
.sort( ( a, b ) => order[ a.type ] - order[ b.type ] )
.forEach( attribute => {
// <select> value attributes are an annoying special case — it must be handled
// *after* its children have been updated
if ( ( attribute.type === 'Attribute' || attribute.type === 'Binding' ) && attribute.name === 'value' && node.name === 'select' ) {
selectValueAttribute = attribute;
return;
}
visitors[ attribute.type ]( generator, block, childState, node, 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
const statement = `${name}.__value = ${name}.textContent;`;
node.initialUpdate = node.lateUpdate = statement;
}
if ( childState.allUsedContexts.length || childState.usesComponent ) {
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 );
});
if ( node.lateUpdate ) {
block.builders.update.addLine( node.lateUpdate );
}
if ( selectValueAttribute ) {
const visitor = selectValueAttribute.type === 'Attribute' ? visitAttribute : visitBinding;
visitor( generator, block, childState, node, selectValueAttribute );
}
if ( node.initialUpdate ) {
block.builders.create.addBlock( node.initialUpdate );
}
}
function getRenderStatement ( generator, namespace, name ) {
if ( namespace === 'http://www.w3.org/2000/svg' ) {
return `${generator.helper( 'createSvgElement' )}( '${name}' )`;
}
if ( namespace ) {
return `document.createElementNS( '${namespace}', '${name}' )`;
}
return `${generator.helper( 'createElement' )}( '${name}' )`;
}

@ -0,0 +1,241 @@
import deindent from '../../../../utils/deindent';
import visit from '../../visit';
import visitComponent from '../Component/Component';
import visitWindow from './meta/Window';
import visitAttribute from './Attribute';
import visitEventHandler from './EventHandler';
import visitBinding from './Binding';
import visitRef from './Ref';
import * as namespaces from '../../../../utils/namespaces';
import addTransitions from './addTransitions';
import { DomGenerator } from '../../index';
import Block from '../../Block';
import { Node } from '../../../../interfaces';
import { State } from '../../interfaces';
const meta = {
':Window': visitWindow,
};
const order = {
Attribute: 1,
Binding: 2,
EventHandler: 3,
Ref: 4,
};
const visitors = {
Attribute: visitAttribute,
EventHandler: visitEventHandler,
Binding: visitBinding,
Ref: visitRef,
};
export default function visitElement(
generator: DomGenerator,
block: Block,
state: State,
node: Node
) {
if (node.name in meta) {
return meta[node.name](generator, block, node);
}
if (generator.components.has(node.name) || node.name === ':Self') {
return visitComponent(generator, block, state, node);
}
const childState = node._state;
const name = childState.parentNode;
const isToplevel = !state.parentNode;
block.addVariable(name);
block.builders.create.addLine(
`${name} = ${getRenderStatement(
generator,
childState.namespace,
node.name
)};`
);
if (generator.hydratable) {
block.builders.claim.addBlock(deindent`
${name} = ${getClaimStatement(
generator,
childState.namespace,
state.parentNodes,
node
)};
var ${childState.parentNodes} = @children( ${name} );
`);
}
if (state.parentNode) {
block.builders.mount.addLine(
`@appendNode( ${name}, ${state.parentNode} );`
);
} else {
block.builders.mount.addLine(`@insertNode( ${name}, #target, anchor );`);
}
// add CSS encapsulation attribute
if (generator.cssId && (!generator.cascade || state.isTopLevel)) {
block.builders.hydrate.addLine(
`@setAttribute( ${name}, '${generator.cssId}', '' );`
);
}
function visitAttributesAndAddProps() {
let intro;
let outro;
node.attributes
.sort((a: Node, b: Node) => order[a.type] - order[b.type])
.forEach((attribute: Node) => {
if (attribute.type === 'Transition') {
if (attribute.intro) intro = attribute;
if (attribute.outro) outro = attribute;
return;
}
visitors[attribute.type](generator, block, childState, node, attribute);
});
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: #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.hydrate.addBlock(deindent`
${name}._svelte = {
${initialProps.join(',\n')}
};
`);
}
if (updates.length) {
block.builders.update.addBlock(updates.join('\n'));
}
}
}
if (isToplevel) {
// TODO we eventually need to consider what happens to elements
// that belong to the same outgroup as an outroing element...
block.builders.unmount.addLine(`@detachNode( ${name} );`);
}
if (node.name !== 'select') {
if (node.name === 'textarea') {
// this is an egregious hack, but it's the easiest way to get <textarea>
// children treated the same way as a value attribute
if (node.children.length > 0) {
node.attributes.push({
type: 'Attribute',
name: 'value',
value: node.children,
});
node.children = [];
}
}
// <select> value attributes are an annoying special case — it must be handled
// *after* its children have been updated
visitAttributesAndAddProps();
}
// special case bound <option> without a value attribute
if (
node.name === 'option' &&
!node.attributes.find(
(attribute: Node) =>
attribute.type === 'Attribute' && attribute.name === 'value'
)
) {
// TODO check it's bound
const statement = `${name}.__value = ${name}.textContent;`;
node.initialUpdate = node.lateUpdate = statement;
}
node.children.forEach((child: Node) => {
visit(generator, block, childState, child);
});
if (node.lateUpdate) {
block.builders.update.addLine(node.lateUpdate);
}
if (node.name === 'select') {
visitAttributesAndAddProps();
}
if (node.initialUpdate) {
block.builders.mount.addBlock(node.initialUpdate);
}
block.builders.claim.addLine(
`${childState.parentNodes}.forEach( @detachNode );`
);
}
function getRenderStatement(
generator: DomGenerator,
namespace: string,
name: string
) {
if (namespace === 'http://www.w3.org/2000/svg') {
return `@createSvgElement( '${name}' )`;
}
if (namespace) {
return `document.createElementNS( '${namespace}', '${name}' )`;
}
return `@createElement( '${name}' )`;
}
function getClaimStatement(
generator: DomGenerator,
namespace: string,
nodes: string,
node: Node
) {
const attributes = node.attributes
.filter((attr: Node) => attr.type === 'Attribute')
.map((attr: Node) => `${quoteProp(attr.name)}: true`)
.join(', ');
const name = namespace ? node.name : node.name.toUpperCase();
return `@claimElement( ${nodes}, '${name}', ${attributes
? `{ ${attributes} }`
: `{}`}, ${namespace === namespaces.svg ? true : false} )`;
}
function quoteProp(name: string) {
if (/[^a-zA-Z_$0-9]/.test(name)) return `'${name}'`;
return name;
}

@ -1,98 +0,0 @@
import deindent from '../../../../utils/deindent.js';
import CodeBuilder from '../../../../utils/CodeBuilder.js';
import flattenReference from '../../../../utils/flattenReference.js';
export default function visitEventHandler ( generator, block, state, node, attribute ) {
const name = attribute.name;
const isCustomEvent = generator.events.has( name );
const shouldHoist = !isCustomEvent && state.inEachBlock;
generator.addSourcemapLocations( attribute.expression );
const flattened = flattenReference( attribute.expression.callee );
if ( flattened.name !== 'event' && flattened.name !== 'this' ) {
// allow event.stopPropagation(), this.select() etc
// TODO verify that it's a valid callee (i.e. built-in or declared method)
generator.code.prependRight( attribute.expression.start, `${block.component}.` );
if ( shouldHoist ) state.usesComponent = true; // this feels a bit hacky but it works!
}
const context = shouldHoist ? null : state.parentNode;
const usedContexts = [];
attribute.expression.arguments.forEach( arg => {
const { contexts } = block.contextualise( arg, context, true );
contexts.forEach( context => {
if ( !~usedContexts.indexOf( context ) ) usedContexts.push( context );
if ( !~state.allUsedContexts.indexOf( context ) ) state.allUsedContexts.push( context );
});
});
const _this = context || 'this';
const declarations = usedContexts.map( name => {
if ( name === 'state' ) {
if ( shouldHoist ) state.usesComponent = true;
return `var state = ${block.component}.get();`;
}
const listName = block.listNames.get( name );
const indexName = block.indexNames.get( name );
const contextName = block.contexts.get( name );
return `var ${listName} = ${_this}._svelte.${listName}, ${indexName} = ${_this}._svelte.${indexName}, ${contextName} = ${listName}[${indexName}];`;
});
// get a name for the event handler that is globally unique
// if hoisted, locally unique otherwise
const handlerName = shouldHoist ?
generator.getUniqueName( `${name}_handler` ) :
block.getUniqueName( `${name}_handler` );
// create the handler body
const handlerBody = new CodeBuilder();
if ( state.usesComponent ) {
// TODO the element needs to know to create `thing._svelte = { component: component }`
handlerBody.addLine( `var ${block.component} = this._svelte.component;` );
}
declarations.forEach( declaration => {
handlerBody.addLine( declaration );
});
handlerBody.addLine( `[✂${attribute.expression.start}-${attribute.expression.end}✂];` );
const handler = isCustomEvent ?
deindent`
var ${handlerName} = ${generator.alias( 'template' )}.events.${name}.call( ${block.component}, ${state.parentNode}, function ( event ) {
${handlerBody}
});
` :
deindent`
function ${handlerName} ( event ) {
${handlerBody}
}
`;
if ( shouldHoist ) {
generator.blocks.push({
render: () => handler
});
} else {
block.builders.create.addBlock( handler );
}
if ( isCustomEvent ) {
block.builders.destroy.addLine( deindent`
${handlerName}.teardown();
` );
} else {
block.builders.create.addLine( deindent`
${generator.helper( 'addEventListener' )}( ${state.parentNode}, '${name}', ${handlerName} );
` );
block.builders.destroy.addLine( deindent`
${generator.helper( 'removeEventListener' )}( ${state.parentNode}, '${name}', ${handlerName} );
` );
}
}

@ -0,0 +1,109 @@
import deindent from '../../../../utils/deindent';
import flattenReference from '../../../../utils/flattenReference';
import { DomGenerator } from '../../index';
import Block from '../../Block';
import { Node } from '../../../../interfaces';
import { State } from '../../interfaces';
export default function visitEventHandler(
generator: DomGenerator,
block: Block,
state: State,
node: Node,
attribute: Node
) {
const name = attribute.name;
const isCustomEvent = generator.events.has(name);
const shouldHoist = !isCustomEvent && state.inEachBlock;
generator.addSourcemapLocations(attribute.expression);
const flattened = flattenReference(attribute.expression.callee);
if (flattened.name !== 'event' && flattened.name !== 'this') {
// allow event.stopPropagation(), this.select() etc
// TODO verify that it's a valid callee (i.e. built-in or declared method)
generator.code.prependRight(
attribute.expression.start,
`${block.alias('component')}.`
);
if (shouldHoist) state.usesComponent = true; // this feels a bit hacky but it works!
}
const context = shouldHoist ? null : state.parentNode;
const usedContexts: string[] = [];
attribute.expression.arguments.forEach((arg: Node) => {
const { contexts } = block.contextualise(arg, context, true);
contexts.forEach(context => {
if (!~usedContexts.indexOf(context)) usedContexts.push(context);
if (!~state.allUsedContexts.indexOf(context))
state.allUsedContexts.push(context);
});
});
const _this = context || 'this';
const declarations = usedContexts.map(name => {
if (name === 'state') {
if (shouldHoist) state.usesComponent = true;
return `var state = #component.get();`;
}
const listName = block.listNames.get(name);
const indexName = block.indexNames.get(name);
const contextName = block.contexts.get(name);
return `var ${listName} = ${_this}._svelte.${listName}, ${indexName} = ${_this}._svelte.${indexName}, ${contextName} = ${listName}[${indexName}];`;
});
// get a name for the event handler that is globally unique
// if hoisted, locally unique otherwise
const handlerName = (shouldHoist ? generator : block).getUniqueName(
`${name.replace(/[^a-zA-Z0-9_$]/g, '_')}_handler`
);
// create the handler body
const handlerBody = deindent`
${state.usesComponent &&
`var ${block.alias('component')} = this._svelte.component;`}
${declarations}
[${attribute.expression.start}-${attribute.expression.end}];
`;
if (isCustomEvent) {
block.addVariable(handlerName);
block.builders.hydrate.addBlock(deindent`
${handlerName} = @template.events.${name}.call( #component, ${state.parentNode}, function ( event ) {
${handlerBody}
});
`);
block.builders.destroy.addLine(deindent`
${handlerName}.teardown();
`);
} else {
const handler = deindent`
function ${handlerName} ( event ) {
${handlerBody}
}
`;
if (shouldHoist) {
generator.blocks.push(
<Block>{
render: () => handler,
}
);
} else {
block.builders.init.addBlock(handler);
}
block.builders.hydrate.addLine(
`@addListener( ${state.parentNode}, '${name}', ${handlerName} );`
);
block.builders.destroy.addLine(
`@removeListener( ${state.parentNode}, '${name}', ${handlerName} );`
);
}
}

@ -1,15 +0,0 @@
import deindent from '../../../../utils/deindent.js';
export default function visitRef ( generator, block, state, node, attribute ) {
const name = attribute.name;
block.builders.create.addLine(
`${block.component}.refs.${name} = ${state.parentNode};`
);
block.builders.destroy.addLine( deindent`
if ( ${block.component}.refs.${name} === ${state.parentNode} ) ${block.component}.refs.${name} = null;
` );
generator.usesRefs = true; // so this component.refs object is created
}

@ -0,0 +1,25 @@
import deindent from '../../../../utils/deindent';
import { DomGenerator } from '../../index';
import Block from '../../Block';
import { Node } from '../../../../interfaces';
import { State } from '../../interfaces';
export default function visitRef(
generator: DomGenerator,
block: Block,
state: State,
node: Node,
attribute: Node
) {
const name = attribute.name;
block.builders.mount.addLine(
`#component.refs.${name} = ${state.parentNode};`
);
block.builders.unmount.addLine(deindent`
if ( #component.refs.${name} === ${state.parentNode} ) #component.refs.${name} = null;
`);
generator.usesRefs = true; // so this component.refs object is created
}

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

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

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

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

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

@ -1,178 +0,0 @@
import flattenReference from '../../../../../utils/flattenReference.js';
import deindent from '../../../../../utils/deindent.js';
import CodeBuilder from '../../../../../utils/CodeBuilder.js';
const associatedEvents = {
innerWidth: 'resize',
innerHeight: 'resize',
outerWidth: 'resize',
outerHeight: 'resize',
scrollX: 'scroll',
scrollY: 'scroll'
};
const readonly = new Set([
'innerWidth',
'innerHeight',
'outerWidth',
'outerHeight',
'online'
]);
export default function visitWindow ( generator, block, node ) {
const events = {};
const bindings = {};
node.attributes.forEach( attribute => {
if ( attribute.type === 'EventHandler' ) {
// TODO verify that it's a valid callee (i.e. built-in or declared method)
generator.addSourcemapLocations( attribute.expression );
let usesState = false;
attribute.expression.arguments.forEach( arg => {
const { contexts } = block.contextualise( arg, null, true );
if ( contexts.length ) usesState = true;
});
const flattened = flattenReference( attribute.expression.callee );
if ( flattened.name !== 'event' && flattened.name !== 'this' ) {
// allow event.stopPropagation(), this.select() etc
generator.code.prependRight( attribute.expression.start, `${block.component}.` );
}
const handlerName = block.getUniqueName( `onwindow${attribute.name}` );
const handlerBody = ( usesState ? `var state = ${block.component}.get();\n` : '' ) +
`[✂${attribute.expression.start}-${attribute.expression.end}✂];`;
block.builders.create.addBlock( deindent`
function ${handlerName} ( event ) {
${handlerBody}
};
window.addEventListener( '${attribute.name}', ${handlerName} );
` );
block.builders.destroy.addBlock( deindent`
window.removeEventListener( '${attribute.name}', ${handlerName} );
` );
}
if ( attribute.type === 'Binding' ) {
if ( attribute.value.type !== 'Identifier' ) {
const { parts, keypath } = flattenReference( attribute.value );
throw new Error( `Bindings on <:Window/> must be to top-level properties, e.g. '${parts.pop()}' rather than '${keypath}'` );
}
// in dev mode, throw if read-only values are written to
if ( readonly.has( attribute.name ) ) {
generator.readonly.add( attribute.value.name );
}
bindings[ attribute.name ] = attribute.value.name;
// bind:online is a special case, we need to listen for two separate events
if ( attribute.name === 'online' ) return;
const associatedEvent = associatedEvents[ attribute.name ];
if ( !associatedEvent ) {
throw new Error( `Cannot bind to ${attribute.name} on <:Window>` );
}
if ( !events[ associatedEvent ] ) events[ associatedEvent ] = [];
events[ associatedEvent ].push( `${attribute.value.name}: this.${attribute.name}` );
// add initial value
generator.builders.metaBindings.addLine(
`this._state.${attribute.value.name} = window.${attribute.name};`
);
}
});
const lock = block.getUniqueName( `window_updating` );
Object.keys( events ).forEach( event => {
const handlerName = block.getUniqueName( `onwindow${event}` );
const props = events[ event ].join( ',\n' );
const handlerBody = new CodeBuilder();
if ( event === 'scroll' ) { // TODO other bidirectional bindings...
block.addVariable( lock, 'false' );
handlerBody.addLine( `${lock} = true;` );
}
if ( generator.options.dev ) handlerBody.addLine( `component._updatingReadonlyProperty = true;` );
handlerBody.addBlock( deindent`
${block.component}.set({
${props}
});
` );
if ( generator.options.dev ) handlerBody.addLine( `component._updatingReadonlyProperty = false;` );
if ( event === 'scroll' ) {
handlerBody.addLine( `${lock} = false;` );
}
block.builders.create.addBlock( deindent`
function ${handlerName} ( event ) {
${handlerBody}
};
window.addEventListener( '${event}', ${handlerName} );
` );
block.builders.destroy.addBlock( deindent`
window.removeEventListener( '${event}', ${handlerName} );
` );
});
// special case... might need to abstract this out if we add more special cases
if ( bindings.scrollX && bindings.scrollY ) {
const observerCallback = block.getUniqueName( `scrollobserver` );
block.builders.create.addBlock( deindent`
function ${observerCallback} () {
if ( ${lock} ) return;
var x = ${bindings.scrollX ? `${block.component}.get( '${bindings.scrollX}' )` : `window.scrollX`};
var y = ${bindings.scrollY ? `${block.component}.get( '${bindings.scrollY}' )` : `window.scrollY`};
window.scrollTo( x, y );
};
` );
if ( bindings.scrollX ) block.builders.create.addLine( `${block.component}.observe( '${bindings.scrollX}', ${observerCallback} );` );
if ( bindings.scrollY ) block.builders.create.addLine( `${block.component}.observe( '${bindings.scrollY}', ${observerCallback} );` );
} else if ( bindings.scrollX || bindings.scrollY ) {
const isX = !!bindings.scrollX;
block.builders.create.addBlock( deindent`
${block.component}.observe( '${bindings.scrollX || bindings.scrollY}', function ( ${isX ? 'x' : 'y'} ) {
if ( ${lock} ) return;
window.scrollTo( ${isX ? 'x, window.scrollY' : 'window.scrollX, y' } );
});
` );
}
// another special case. (I'm starting to think these are all special cases.)
if ( bindings.online ) {
const handlerName = block.getUniqueName( `onlinestatuschanged` );
block.builders.create.addBlock( deindent`
function ${handlerName} ( event ) {
${block.component}.set({ ${bindings.online}: navigator.onLine });
};
window.addEventListener( 'online', ${handlerName} );
window.addEventListener( 'offline', ${handlerName} );
` );
// add initial value
generator.builders.metaBindings.addLine(
`this._state.${bindings.online} = navigator.onLine;`
);
block.builders.destroy.addBlock( deindent`
window.removeEventListener( 'online', ${handlerName} );
window.removeEventListener( 'offline', ${handlerName} );
` );
}
}

@ -0,0 +1,194 @@
import flattenReference from '../../../../../utils/flattenReference';
import deindent from '../../../../../utils/deindent';
import { DomGenerator } from '../../../index';
import Block from '../../../Block';
import { Node } from '../../../../../interfaces';
const associatedEvents = {
innerWidth: 'resize',
innerHeight: 'resize',
outerWidth: 'resize',
outerHeight: 'resize',
scrollX: 'scroll',
scrollY: 'scroll',
};
const readonly = new Set([
'innerWidth',
'innerHeight',
'outerWidth',
'outerHeight',
'online',
]);
export default function visitWindow(
generator: DomGenerator,
block: Block,
node: Node
) {
const events = {};
const bindings = {};
node.attributes.forEach((attribute: Node) => {
if (attribute.type === 'EventHandler') {
// TODO verify that it's a valid callee (i.e. built-in or declared method)
generator.addSourcemapLocations(attribute.expression);
let usesState = false;
attribute.expression.arguments.forEach((arg: Node) => {
const { contexts } = block.contextualise(arg, null, true);
if (contexts.length) usesState = true;
});
const flattened = flattenReference(attribute.expression.callee);
if (flattened.name !== 'event' && flattened.name !== 'this') {
// allow event.stopPropagation(), this.select() etc
generator.code.prependRight(
attribute.expression.start,
`${block.alias('component')}.`
);
}
const handlerName = block.getUniqueName(`onwindow${attribute.name}`);
const handlerBody = deindent`
${usesState && `var state = #component.get();`}
[${attribute.expression.start}-${attribute.expression.end}];
`;
block.builders.init.addBlock(deindent`
function ${handlerName} ( event ) {
${handlerBody}
};
window.addEventListener( '${attribute.name}', ${handlerName} );
`);
block.builders.destroy.addBlock(deindent`
window.removeEventListener( '${attribute.name}', ${handlerName} );
`);
}
if (attribute.type === 'Binding') {
// in dev mode, throw if read-only values are written to
if (readonly.has(attribute.name)) {
generator.readonly.add(attribute.value.name);
}
bindings[attribute.name] = attribute.value.name;
// bind:online is a special case, we need to listen for two separate events
if (attribute.name === 'online') return;
const associatedEvent = associatedEvents[attribute.name];
if (!associatedEvent) {
throw new Error(`Cannot bind to ${attribute.name} on <:Window>`);
}
if (!events[associatedEvent]) events[associatedEvent] = [];
events[associatedEvent].push(
`${attribute.value.name}: this.${attribute.name}`
);
// add initial value
generator.metaBindings.push(
`this._state.${attribute.value.name} = window.${attribute.name};`
);
}
});
const lock = block.getUniqueName(`window_updating`);
Object.keys(events).forEach(event => {
const handlerName = block.getUniqueName(`onwindow${event}`);
const props = events[event].join(',\n');
if (event === 'scroll') {
// TODO other bidirectional bindings...
block.addVariable(lock, 'false');
}
const handlerBody = deindent`
${event === 'scroll' && `${lock} = true;`}
${generator.options.dev && `component._updatingReadonlyProperty = true;`}
#component.set({
${props}
});
${generator.options.dev && `component._updatingReadonlyProperty = false;`}
${event === 'scroll' && `${lock} = false;`}
`;
block.builders.init.addBlock(deindent`
function ${handlerName} ( event ) {
${handlerBody}
};
window.addEventListener( '${event}', ${handlerName} );
`);
block.builders.destroy.addBlock(deindent`
window.removeEventListener( '${event}', ${handlerName} );
`);
});
// special case... might need to abstract this out if we add more special cases
if (bindings.scrollX && bindings.scrollY) {
const observerCallback = block.getUniqueName(`scrollobserver`);
block.builders.init.addBlock(deindent`
function ${observerCallback} () {
if ( ${lock} ) return;
var x = ${bindings.scrollX
? `#component.get( '${bindings.scrollX}' )`
: `window.scrollX`};
var y = ${bindings.scrollY
? `#component.get( '${bindings.scrollY}' )`
: `window.scrollY`};
window.scrollTo( x, y );
};
`);
if (bindings.scrollX)
block.builders.init.addLine(
`#component.observe( '${bindings.scrollX}', ${observerCallback} );`
);
if (bindings.scrollY)
block.builders.init.addLine(
`#component.observe( '${bindings.scrollY}', ${observerCallback} );`
);
} else if (bindings.scrollX || bindings.scrollY) {
const isX = !!bindings.scrollX;
block.builders.init.addBlock(deindent`
#component.observe( '${bindings.scrollX ||
bindings.scrollY}', function ( ${isX ? 'x' : 'y'} ) {
if ( ${lock} ) return;
window.scrollTo( ${isX ? 'x, window.scrollY' : 'window.scrollX, y'} );
});
`);
}
// another special case. (I'm starting to think these are all special cases.)
if (bindings.online) {
const handlerName = block.getUniqueName(`onlinestatuschanged`);
block.builders.init.addBlock(deindent`
function ${handlerName} ( event ) {
#component.set({ ${bindings.online}: navigator.onLine });
};
window.addEventListener( 'online', ${handlerName} );
window.addEventListener( 'offline', ${handlerName} );
`);
// add initial value
generator.metaBindings.push(
`this._state.${bindings.online} = navigator.onLine;`
);
block.builders.destroy.addBlock(deindent`
window.removeEventListener( 'online', ${handlerName} );
window.removeEventListener( 'offline', ${handlerName} );
`);
}
}

@ -1,153 +0,0 @@
import deindent from '../../../utils/deindent.js';
import visit from '../visit.js';
function isElseIf ( node ) {
return node && node.children.length === 1 && node.children[0].type === 'IfBlock';
}
function getBranches ( generator, block, state, node ) {
const branches = [{
condition: block.contextualise( node.expression ).snippet,
block: node._block.name,
dynamic: node._block.dependencies.size > 0
}];
visitChildren( generator, block, state, node );
if ( isElseIf( node.else ) ) {
branches.push(
...getBranches( generator, block, state, node.else.children[0] )
);
} else {
branches.push({
condition: null,
block: node.else ? node.else._block.name : null,
dynamic: node.else ? node.else._block.dependencies.size > 0 : false
});
if ( node.else ) {
visitChildren( generator, block, state, node.else );
}
}
return branches;
}
function visitChildren ( generator, block, state, node ) {
const childState = Object.assign( {}, state, {
parentNode: null
});
node.children.forEach( child => {
visit( generator, node._block, childState, child );
});
}
export default function visitIfBlock ( generator, block, state, node ) {
const name = generator.getUniqueName( `if_block` );
const anchor = generator.getUniqueName( `${name}_anchor` );
const params = block.params.join( ', ' );
const vars = { name, anchor, params };
block.createAnchor( anchor, state.parentNode );
const branches = getBranches( generator, block, state, node, generator.getUniqueName( `create_if_block` ) );
const dynamic = branches.some( branch => branch.dynamic );
if ( node.else ) {
compound( generator, block, state, node, branches, dynamic, vars );
} else {
simple( generator, block, state, node, branches[0], dynamic, vars );
}
block.builders.destroy.addLine(
`if ( ${name} ) ${name}.destroy( ${state.parentNode ? 'false' : 'detach'} );`
);
}
function simple ( generator, block, state, node, branch, dynamic, { name, anchor, params } ) {
block.builders.create.addBlock( deindent`
var ${name} = ${branch.condition} && ${branch.block}( ${params}, ${block.component} );
` );
const isToplevel = !state.parentNode;
if ( isToplevel ) {
block.builders.mount.addLine( `if ( ${name} ) ${name}.mount( ${block.target}, ${anchor} );` );
} else {
block.builders.create.addLine( `if ( ${name} ) ${name}.mount( ${state.parentNode}, ${anchor} );` );
}
if ( dynamic ) {
block.builders.update.addBlock( deindent`
if ( ${branch.condition} ) {
if ( ${name} ) {
${name}.update( changed, ${params} );
} else {
${name} = ${branch.block}( ${params}, ${block.component} );
${name}.mount( ${anchor}.parentNode, ${anchor} );
}
} else if ( ${name} ) {
${name}.destroy( true );
${name} = null;
}
` );
} else {
block.builders.update.addBlock( deindent`
if ( ${branch.condition} ) {
if ( !${name} ) {
${name} = ${branch.block}( ${params}, ${block.component} );
${name}.mount( ${anchor}.parentNode, ${anchor} );
}
} else if ( ${name} ) {
${name}.destroy( true );
${name} = null;
}
` );
}
}
function compound ( generator, block, state, node, branches, dynamic, { name, anchor, params } ) {
const getBlock = block.getUniqueName( `get_block` );
const current_block = block.getUniqueName( `current_block` );
block.builders.create.addBlock( deindent`
function ${getBlock} ( ${params} ) {
${branches.map( ({ condition, block }) => {
return `${condition ? `if ( ${condition} ) ` : ''}return ${block};`;
} ).join( '\n' )}
}
var ${current_block} = ${getBlock}( ${params} );
var ${name} = ${current_block} && ${current_block}( ${params}, ${block.component} );
` );
const isToplevel = !state.parentNode;
if ( isToplevel ) {
block.builders.mount.addLine( `if ( ${name} ) ${name}.mount( ${block.target}, ${anchor} );` );
} else {
block.builders.create.addLine( `if ( ${name} ) ${name}.mount( ${state.parentNode}, ${anchor} );` );
}
if ( dynamic ) {
block.builders.update.addBlock( deindent`
if ( ${current_block} === ( ${current_block} = ${getBlock}( ${params} ) ) && ${name} ) {
${name}.update( changed, ${params} );
} else {
if ( ${name} ) ${name}.destroy( true );
${name} = ${current_block} && ${current_block}( ${params}, ${block.component} );
if ( ${name} ) ${name}.mount( ${anchor}.parentNode, ${anchor} );
}
` );
} else {
block.builders.update.addBlock( deindent`
if ( ${current_block} !== ( ${current_block} = ${getBlock}( ${params} ) ) ) {
if ( ${name} ) ${name}.destroy( true );
${name} = ${current_block} && ${current_block}( ${params}, ${block.component} );
if ( ${name} ) ${name}.mount( ${anchor}.parentNode, ${anchor} );
}
` );
}
}

@ -0,0 +1,418 @@
import deindent from '../../../utils/deindent';
import visit from '../visit';
import { DomGenerator } from '../index';
import Block from '../Block';
import { Node } from '../../../interfaces';
import { State } from '../interfaces';
function isElseIf(node: Node) {
return (
node && node.children.length === 1 && node.children[0].type === 'IfBlock'
);
}
function isElseBranch(branch) {
return branch.block && !branch.condition;
}
function getBranches(
generator: DomGenerator,
block: Block,
state: State,
node: Node
) {
const branches = [
{
condition: block.contextualise(node.expression).snippet,
block: node._block.name,
hasUpdateMethod: node._block.hasUpdateMethod,
hasIntroMethod: node._block.hasIntroMethod,
hasOutroMethod: node._block.hasOutroMethod,
},
];
visitChildren(generator, block, state, node);
if (isElseIf(node.else)) {
branches.push(
...getBranches(generator, block, state, node.else.children[0])
);
} else {
branches.push({
condition: null,
block: node.else ? node.else._block.name : null,
hasUpdateMethod: node.else ? node.else._block.hasUpdateMethod : false,
hasIntroMethod: node.else ? node.else._block.hasIntroMethod : false,
hasOutroMethod: node.else ? node.else._block.hasOutroMethod : false,
});
if (node.else) {
visitChildren(generator, block, state, node.else);
}
}
return branches;
}
function visitChildren(
generator: DomGenerator,
block: Block,
state: State,
node: Node
) {
node.children.forEach((child: Node) => {
visit(generator, node._block, node._state, child);
});
}
export default function visitIfBlock(
generator: DomGenerator,
block: Block,
state: State,
node: Node
) {
const name = generator.getUniqueName(`if_block`);
const anchor = node.needsAnchor
? block.getUniqueName(`${name}_anchor`)
: (node.next && node.next._state.name) || 'null';
const params = block.params.join(', ');
const branches = getBranches(generator, block, state, node);
const hasElse = isElseBranch(branches[branches.length - 1]);
const if_name = hasElse ? '' : `if ( ${name} ) `;
const dynamic = branches[0].hasUpdateMethod; // can use [0] as proxy for all, since they necessarily have the same value
const hasOutros = branches[0].hasOutroMethod;
const vars = { name, anchor, params, if_name, hasElse };
if (node.else) {
if (hasOutros) {
compoundWithOutros(
generator,
block,
state,
node,
branches,
dynamic,
vars
);
} else {
compound(generator, block, state, node, branches, dynamic, vars);
}
} else {
simple(generator, block, state, node, branches[0], dynamic, vars);
}
block.builders.create.addLine(`${if_name}${name}.create();`);
block.builders.claim.addLine(
`${if_name}${name}.claim( ${state.parentNodes} );`
);
if (node.needsAnchor) {
block.addElement(
anchor,
`@createComment()`,
`@createComment()`,
state.parentNode,
true
);
} else if (node.next) {
node.next.usedAsAnchor = true;
}
}
function simple(
generator: DomGenerator,
block: Block,
state: State,
node: Node,
branch,
dynamic,
{ name, anchor, params, if_name }
) {
block.builders.init.addBlock(deindent`
var ${name} = (${branch.condition}) && ${branch.block}( ${params}, #component );
`);
const isTopLevel = !state.parentNode;
const mountOrIntro = branch.hasIntroMethod ? 'intro' : 'mount';
const targetNode = state.parentNode || '#target';
const anchorNode = state.parentNode ? 'null' : 'anchor';
block.builders.mount.addLine(
`if ( ${name} ) ${name}.${mountOrIntro}( ${targetNode}, ${anchorNode} );`
);
const parentNode = state.parentNode || `${anchor}.parentNode`;
const enter = dynamic
? branch.hasIntroMethod
? deindent`
if ( ${name} ) {
${name}.update( changed, ${params} );
} else {
${name} = ${branch.block}( ${params}, #component );
if ( ${name} ) ${name}.create();
}
${name}.intro( ${parentNode}, ${anchor} );
`
: deindent`
if ( ${name} ) {
${name}.update( changed, ${params} );
} else {
${name} = ${branch.block}( ${params}, #component );
${name}.create();
${name}.mount( ${parentNode}, ${anchor} );
}
`
: branch.hasIntroMethod
? deindent`
if ( !${name} ) {
${name} = ${branch.block}( ${params}, #component );
${name}.create();
}
${name}.intro( ${parentNode}, ${anchor} );
`
: deindent`
if ( !${name} ) {
${name} = ${branch.block}( ${params}, #component );
${name}.create();
${name}.mount( ${parentNode}, ${anchor} );
}
`;
// no `update()` here — we don't want to update outroing nodes,
// as that will typically result in glitching
const exit = branch.hasOutroMethod
? deindent`
${name}.outro( function () {
${name}.unmount();
${name}.destroy();
${name} = null;
});
`
: deindent`
${name}.unmount();
${name}.destroy();
${name} = null;
`;
block.builders.update.addBlock(deindent`
if ( ${branch.condition} ) {
${enter}
} else if ( ${name} ) {
${exit}
}
`);
block.builders.unmount.addLine(`${if_name}${name}.unmount();`);
block.builders.destroy.addLine(`${if_name}${name}.destroy();`);
}
function compound(
generator: DomGenerator,
block: Block,
state: State,
node: Node,
branches,
dynamic,
{ name, anchor, params, hasElse, if_name }
) {
const get_block = block.getUniqueName(`get_block`);
const current_block = block.getUniqueName(`current_block`);
const current_block_and = hasElse ? '' : `${current_block} && `;
block.builders.init.addBlock(deindent`
function ${get_block} ( ${params} ) {
${branches
.map(({ condition, block }) => {
return `${condition ? `if ( ${condition} ) ` : ''}return ${block};`;
})
.join('\n')}
}
var ${current_block} = ${get_block}( ${params} );
var ${name} = ${current_block_and}${current_block}( ${params}, #component );
`);
const isTopLevel = !state.parentNode;
const mountOrIntro = branches[0].hasIntroMethod ? 'intro' : 'mount';
const targetNode = state.parentNode || '#target';
const anchorNode = state.parentNode ? 'null' : 'anchor';
block.builders.mount.addLine(
`${if_name}${name}.${mountOrIntro}( ${targetNode}, ${anchorNode} );`
);
const parentNode = state.parentNode || `${anchor}.parentNode`;
const changeBlock = deindent`
${hasElse
? deindent`
${name}.unmount();
${name}.destroy();
`
: deindent`
if ( ${name} ) {
${name}.unmount();
${name}.destroy();
}`}
${name} = ${current_block_and}${current_block}( ${params}, #component );
${if_name}${name}.create();
${if_name}${name}.${mountOrIntro}( ${parentNode}, ${anchor} );
`;
if (dynamic) {
block.builders.update.addBlock(deindent`
if ( ${current_block} === ( ${current_block} = ${get_block}( ${params} ) ) && ${name} ) {
${name}.update( changed, ${params} );
} else {
${changeBlock}
}
`);
} else {
block.builders.update.addBlock(deindent`
if ( ${current_block} !== ( ${current_block} = ${get_block}( ${params} ) ) ) {
${changeBlock}
}
`);
}
block.builders.unmount.addLine(`${if_name}${name}.unmount();`);
block.builders.destroy.addLine(`${if_name}${name}.destroy();`);
}
// if any of the siblings have outros, we need to keep references to the blocks
// (TODO does this only apply to bidi transitions?)
function compoundWithOutros(
generator: DomGenerator,
block: Block,
state: State,
node: Node,
branches,
dynamic,
{ name, anchor, params, hasElse }
) {
const get_block = block.getUniqueName(`get_block`);
const current_block_index = block.getUniqueName(`current_block_index`);
const previous_block_index = block.getUniqueName(`previous_block_index`);
const if_block_creators = block.getUniqueName(`if_block_creators`);
const if_blocks = block.getUniqueName(`if_blocks`);
const if_current_block_index = hasElse
? ''
: `if ( ~${current_block_index} ) `;
block.addVariable(current_block_index);
block.addVariable(name);
block.builders.init.addBlock(deindent`
var ${if_block_creators} = [
${branches.map(branch => branch.block).join(',\n')}
];
var ${if_blocks} = [];
function ${get_block} ( ${params} ) {
${branches
.map(({ condition, block }, i) => {
return `${condition ? `if ( ${condition} ) ` : ''}return ${block
? i
: -1};`;
})
.join('\n')}
}
`);
if (hasElse) {
block.builders.init.addBlock(deindent`
${current_block_index} = ${get_block}( ${params} );
${name} = ${if_blocks}[ ${current_block_index} ] = ${if_block_creators}[ ${current_block_index} ]( ${params}, #component );
`);
} else {
block.builders.init.addBlock(deindent`
if ( ~( ${current_block_index} = ${get_block}( ${params} ) ) ) {
${name} = ${if_blocks}[ ${current_block_index} ] = ${if_block_creators}[ ${current_block_index} ]( ${params}, #component );
}
`);
}
const isTopLevel = !state.parentNode;
const mountOrIntro = branches[0].hasIntroMethod ? 'intro' : 'mount';
const targetNode = state.parentNode || '#target';
const anchorNode = state.parentNode ? 'null' : 'anchor';
block.builders.mount.addLine(
`${if_current_block_index}${if_blocks}[ ${current_block_index} ].${mountOrIntro}( ${targetNode}, ${anchorNode} );`
);
const parentNode = state.parentNode || `${anchor}.parentNode`;
const destroyOldBlock = deindent`
${name}.outro( function () {
${if_blocks}[ ${previous_block_index} ].unmount();
${if_blocks}[ ${previous_block_index} ].destroy();
${if_blocks}[ ${previous_block_index} ] = null;
});
`;
const createNewBlock = deindent`
${name} = ${if_blocks}[ ${current_block_index} ];
if ( !${name} ) {
${name} = ${if_blocks}[ ${current_block_index} ] = ${if_block_creators}[ ${current_block_index} ]( ${params}, #component );
${name}.create();
}
${name}.${mountOrIntro}( ${parentNode}, ${anchor} );
`;
const changeBlock = hasElse
? deindent`
${destroyOldBlock}
${createNewBlock}
`
: deindent`
if ( ${name} ) {
${destroyOldBlock}
}
if ( ~${current_block_index} ) {
${createNewBlock}
} else {
${name} = null;
}
`;
if (dynamic) {
block.builders.update.addBlock(deindent`
var ${previous_block_index} = ${current_block_index};
${current_block_index} = ${get_block}( ${params} );
if ( ${current_block_index} === ${previous_block_index} ) {
${if_current_block_index}${if_blocks}[ ${current_block_index} ].update( changed, ${params} );
} else {
${changeBlock}
}
`);
} else {
block.builders.update.addBlock(deindent`
var ${previous_block_index} = ${current_block_index};
${current_block_index} = ${get_block}( ${params} );
if ( ${current_block_index} !== ${previous_block_index} ) {
${changeBlock}
}
`);
}
block.builders.destroy.addLine(deindent`
${if_current_block_index}{
${if_blocks}[ ${current_block_index} ].unmount();
${if_blocks}[ ${current_block_index} ].destroy();
}
`);
}

@ -1,17 +0,0 @@
import deindent from '../../../utils/deindent.js';
export default function visitMustacheTag ( generator, block, state, node ) {
const name = block.getUniqueName( 'text' );
const value = block.getUniqueName( `${name}_value` );
const { snippet } = block.contextualise( node.expression );
block.addVariable( value );
block.addElement( name, `${generator.helper( 'createText' )}( ${value} = ${snippet} )`, state.parentNode, true );
block.builders.update.addBlock( deindent`
if ( ${value} !== ( ${value} = ${snippet} ) ) {
${name}.data = ${value};
}
` );
}

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

@ -1,36 +0,0 @@
import deindent from '../../../utils/deindent.js';
export default function visitRawMustacheTag ( generator, block, state, node ) {
const name = block.getUniqueName( 'raw' );
const value = block.getUniqueName( `${name}_value` );
const before = block.getUniqueName( `${name}_before` );
const after = block.getUniqueName( `${name}_after` );
const { snippet } = block.contextualise( node.expression );
// we would have used comments here, but the `insertAdjacentHTML` api only
// exists for `Element`s.
block.addElement( before, `${generator.helper( 'createElement' )}( 'noscript' )`, state.parentNode, true );
block.addElement( after, `${generator.helper( 'createElement' )}( 'noscript' )`, state.parentNode, true );
const isToplevel = !state.parentNode;
block.builders.create.addLine( `var ${value} = ${snippet};` );
const mountStatement = `${before}.insertAdjacentHTML( 'afterend', ${value} );`;
const detachStatement = `${generator.helper( 'detachBetween' )}( ${before}, ${after} );`;
if ( isToplevel ) {
block.builders.mount.addLine( mountStatement );
} else {
block.builders.create.addLine( mountStatement );
}
block.builders.update.addBlock( deindent`
if ( ${value} !== ( ${value} = ${snippet} ) ) {
${detachStatement}
${mountStatement}
}
` );
block.builders.detachRaw.addBlock( detachStatement );
}

@ -0,0 +1,54 @@
import deindent from '../../../utils/deindent';
import { DomGenerator } from '../index';
import Block from '../Block';
import { Node } from '../../../interfaces';
import { State } from '../interfaces';
export default function visitRawMustacheTag(
generator: DomGenerator,
block: Block,
state: State,
node: Node
) {
const name = node._state.basename;
const before = node._state.name;
const value = block.getUniqueName(`${name}_value`);
const after = block.getUniqueName(`${name}_after`);
const { snippet } = block.contextualise(node.expression);
block.addVariable(value);
// we would have used comments here, but the `insertAdjacentHTML` api only
// exists for `Element`s.
block.addElement(
before,
`@createElement( 'noscript' )`,
`@createElement( 'noscript' )`,
state.parentNode,
true
);
block.addElement(
after,
`@createElement( 'noscript' )`,
`@createElement( 'noscript' )`,
state.parentNode,
true
);
const isToplevel = !state.parentNode;
const mountStatement = `${before}.insertAdjacentHTML( 'afterend', ${value} = ${snippet} );`;
const detachStatement = `@detachBetween( ${before}, ${after} );`;
block.builders.mount.addLine(mountStatement);
block.builders.update.addBlock(deindent`
if ( ${value} !== ( ${value} = ${snippet} ) ) {
${detachStatement}
${mountStatement}
}
`);
block.builders.detachRaw.addBlock(detachStatement);
}

@ -1,23 +0,0 @@
// Whitespace inside one of these elements will not result in
// a whitespace node being created in any circumstances. (This
// list is almost certainly very incomplete)
const elementsWithoutText = new Set([
'audio',
'datalist',
'dl',
'ol',
'optgroup',
'select',
'ul',
'video'
]);
export default function visitText ( generator, block, state, node ) {
if ( !/\S/.test( node.data ) ) {
if ( state.namespace ) return;
if ( elementsWithoutText.has( state.parentNodeName) ) return;
}
const name = block.getUniqueName( `text` );
block.addElement( name, `${generator.helper( 'createText' )}( ${JSON.stringify( node.data )} )`, state.parentNode, false );
}

@ -0,0 +1,23 @@
import { DomGenerator } from '../index';
import Block from '../Block';
import { Node } from '../../../interfaces';
import { State } from '../interfaces';
import stringify from '../../../utils/stringify';
export default function visitText(
generator: DomGenerator,
block: Block,
state: State,
node: Node
) {
if (!node._state.shouldCreate) return;
block.addElement(
node._state.name,
`@createText( ${stringify(node.data)} )`,
generator.hydratable
? `@claimText( ${state.parentNodes}, ${stringify(node.data)} )`
: '',
state.parentNode,
node.usedAsAnchor
);
}

@ -1,12 +0,0 @@
export default function visitYieldTag ( generator, block, state ) {
const anchor = `yield_anchor`;
block.createAnchor( anchor, state.parentNode );
block.builders.mount.addLine(
`${block.component}._yield && ${block.component}._yield.mount( ${state.parentNode || block.target}, ${anchor} );`
);
block.builders.destroy.addLine(
`${block.component}._yield && ${block.component}._yield.destroy( detach );`
);
}

@ -0,0 +1,19 @@
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 || '#target';
block.builders.mount.addLine(
`if ( #component._yield ) #component._yield.mount( ${parentNode}, null );`
);
block.builders.unmount.addLine(
`if ( #component._yield ) #component._yield.unmount();`
);
}

@ -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,37 +0,0 @@
import deindent from '../../../../../utils/deindent.js';
export default function getSetter ({ block, name, context, attribute, dependencies, value }) {
const tail = attribute.value.type === 'MemberExpression' ? getTailSnippet( attribute.value ) : '';
if ( block.contexts.has( name ) ) {
const prop = dependencies[0];
return deindent`
var list = this.${context}.${block.listNames.get( name )};
var index = this.${context}.${block.indexNames.get( name )};
list[index]${tail} = ${value};
${block.component}._set({ ${prop}: ${block.component}.get( '${prop}' ) });
`;
}
if ( attribute.value.type === 'MemberExpression' ) {
const alias = block.alias( name );
return deindent`
var ${alias} = ${block.component}.get( '${name}' );
${alias}${tail} = ${value};
${block.component}._set({ ${name}: ${alias} });
`;
}
return `${block.component}._set({ ${name}: ${value} });`;
}
function getTailSnippet ( node ) {
const end = node.end;
while ( node.type === 'MemberExpression' ) node = node.object;
const start = node.end;
return `[✂${start}-${end}✂]`;
}

@ -0,0 +1,58 @@
import deindent from '../../../../../utils/deindent';
import getTailSnippet from '../../../../../utils/getTailSnippet';
import { Node } from '../../../../../interfaces';
export default function getSetter({
block,
name,
snippet,
context,
attribute,
dependencies,
value,
}) {
const tail = attribute.value.type === 'MemberExpression'
? getTailSnippet(attribute.value)
: '';
if (block.contexts.has(name)) {
const prop = dependencies[0];
const computed = isComputed(attribute.value);
return deindent`
var list = this.${context}.${block.listNames.get(name)};
var index = this.${context}.${block.indexNames.get(name)};
${computed && `var state = #component.get();`}
list[index]${tail} = ${value};
${computed
? `#component._set({ ${dependencies
.map((prop: string) => `${prop}: state.${prop}`)
.join(', ')} });`
: `#component._set({ ${dependencies
.map((prop: string) => `${prop}: #component.get( '${prop}' )`)
.join(', ')} });`}
`;
}
if (attribute.value.type === 'MemberExpression') {
return deindent`
var state = #component.get();
${snippet} = ${value};
#component._set({ ${dependencies
.map((prop: string) => `${prop}: state.${prop}`)
.join(', ')} });
`;
}
return `#component._set({ ${name}: ${value} });`;
}
function isComputed(node: Node) {
while (node.type === 'MemberExpression') {
if (node.computed) return true;
node = node.object;
}
return false;
}

@ -1,34 +0,0 @@
import deindent from '../../utils/deindent.js';
import flattenReference from '../../utils/flattenReference.js';
export default class Block {
constructor ( options ) {
Object.assign( this, options );
}
addBinding ( binding, name ) {
const conditions = [ `!( '${binding.name}' in state )`].concat( // TODO handle contextual bindings...
this.conditions.map( c => `(${c})` )
);
const { keypath } = flattenReference( binding.value );
this.generator.bindings.push( deindent`
if ( ${conditions.join( '&&' )} ) {
tmp = ${name}.data();
if ( '${keypath}' in tmp ) {
state.${binding.name} = tmp.${keypath};
settled = false;
}
}
` );
}
child ( options ) {
return new Block( Object.assign( {}, this, options, { parent: this } ) );
}
contextualise ( expression, context, isEventHandler ) {
return this.generator.contextualise( this, expression, context, isEventHandler );
}
}

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

@ -1,156 +0,0 @@
import deindent from '../../utils/deindent.js';
import CodeBuilder from '../../utils/CodeBuilder.js';
import Generator from '../Generator.js';
import Block from './Block.js';
import visit from './visit.js';
class SsrGenerator extends Generator {
constructor ( parsed, source, name, options ) {
super( parsed, source, name, options );
this.bindings = [];
this.renderCode = '';
}
append ( code ) {
this.renderCode += code;
}
}
export default function ssr ( parsed, source, options ) {
const format = options.format || 'cjs';
const name = options.name || 'SvelteComponent';
const generator = new SsrGenerator( parsed, source, name, options );
const { computations, hasJs, templateProperties } = generator.parseJs( true );
const builders = {
main: new CodeBuilder(),
bindings: new CodeBuilder(),
render: new CodeBuilder(),
renderCss: new CodeBuilder()
};
// create main render() function
const mainBlock = new Block({
generator,
contexts: new Map(),
indexes: new Map(),
conditions: []
});
parsed.html.children.forEach( node => {
visit( generator, mainBlock, node );
});
builders.render.addLine(
templateProperties.data ? `state = Object.assign( ${generator.alias( 'template' )}.data(), state || {} );` : `state = state || {};`
);
computations.forEach( ({ key, deps }) => {
builders.render.addLine(
`state.${key} = ${generator.alias( 'template' )}.computed.${key}( ${deps.map( dep => `state.${dep}` ).join( ', ' )} );`
);
});
if ( generator.bindings.length ) {
const bindings = generator.bindings.join( '\n\n' );
builders.render.addBlock( deindent`
var settled = false;
var tmp;
while ( !settled ) {
settled = true;
${bindings}
}
` );
}
builders.render.addBlock(
`return \`${generator.renderCode}\`;`
);
// create renderCss() function
builders.renderCss.addBlock(
`var components = [];`
);
if ( generator.css ) {
builders.renderCss.addBlock( deindent`
components.push({
filename: ${name}.filename,
css: ${JSON.stringify( generator.css )},
map: null // TODO
});
` );
}
if ( templateProperties.components ) {
builders.renderCss.addBlock( deindent`
var seen = {};
function addComponent ( component ) {
var result = component.renderCss();
result.components.forEach( x => {
if ( seen[ x.filename ] ) return;
seen[ x.filename ] = true;
components.push( x );
});
}
` );
templateProperties.components.value.properties.forEach( prop => {
const { name } = prop.key;
const expression = generator.importedComponents.get( name ) || `${generator.alias( 'template' )}.components.${name}`;
builders.renderCss.addLine( `addComponent( ${expression} );` );
});
}
builders.renderCss.addBlock( deindent`
return {
css: components.map( x => x.css ).join( '\\n' ),
map: null,
components
};
` );
if ( hasJs ) {
builders.main.addBlock( `[✂${parsed.js.content.start}-${parsed.js.content.end}✂]` );
}
builders.main.addBlock( deindent`
var ${name} = {};
${name}.filename = ${JSON.stringify( options.filename )};
${name}.data = function () {
return ${templateProperties.data ? `${generator.alias( 'template' )}.data()` : `{}`};
};
${name}.render = function ( state, options ) {
${builders.render}
};
${name}.renderCss = function () {
${builders.renderCss}
};
var escaped = {
'"': '&quot;',
"'": '&#39;',
'&': '&amp;',
'<': '&lt;',
'>': '&gt;'
};
function __escape ( html ) {
return String( html ).replace( /["'&<>]/g, match => escaped[ match ] );
}
` );
const result = builders.main.toString();
return generator.generate( result, options, { name, format } );
}

@ -0,0 +1,174 @@
import deindent from '../../utils/deindent';
import Generator from '../Generator';
import Block from './Block';
import visit from './visit';
import { removeNode, removeObjectKey } from '../../utils/removeNode';
import { Parsed, Node, CompileOptions } from '../../interfaces';
export class SsrGenerator extends Generator {
bindings: string[];
renderCode: string;
elementDepth: number;
constructor(
parsed: Parsed,
source: string,
name: string,
options: CompileOptions
) {
super(parsed, source, name, options);
this.bindings = [];
this.renderCode = '';
this.elementDepth = 0;
// in an SSR context, we don't need to include events, methods, oncreate or ondestroy
const { templateProperties, defaultExport } = this;
if (templateProperties.oncreate)
removeNode(
this.code,
defaultExport.declaration,
templateProperties.oncreate
);
if (templateProperties.ondestroy)
removeNode(
this.code,
defaultExport.declaration,
templateProperties.ondestroy
);
if (templateProperties.methods)
removeNode(
this.code,
defaultExport.declaration,
templateProperties.methods
);
if (templateProperties.events)
removeNode(
this.code,
defaultExport.declaration,
templateProperties.events
);
}
append(code: string) {
this.renderCode += code;
}
}
export default function ssr(
parsed: Parsed,
source: string,
options: CompileOptions
) {
const format = options.format || 'cjs';
const generator = new SsrGenerator(parsed, source, options.name || 'SvelteComponent', options);
const { computations, name, hasJs, templateProperties } = generator;
// create main render() function
const mainBlock = new Block({
generator,
contexts: new Map(),
indexes: new Map(),
conditions: [],
});
parsed.html.children.forEach((node: Node) => {
visit(generator, mainBlock, node);
});
const result = deindent`
${hasJs && `[✂${parsed.js.content.start}-${parsed.js.content.end}✂]`}
var ${name} = {};
${name}.filename = ${JSON.stringify(options.filename)};
${name}.data = function () {
return ${templateProperties.data ? `@template.data()` : `{}`};
};
${name}.render = function ( state, options ) {
${templateProperties.data
? `state = Object.assign( @template.data(), state || {} );`
: `state = state || {};`}
${computations.map(
({ key, deps }) =>
`state.${key} = @template.computed.${key}( ${deps
.map(dep => `state.${dep}`)
.join(', ')} );`
)}
${generator.bindings.length &&
deindent`
var settled = false;
var tmp;
while ( !settled ) {
settled = true;
${generator.bindings.join('\n\n')}
}
`}
return \`${generator.renderCode}\`.trim();
};
${name}.renderCss = function () {
var components = [];
${generator.css &&
deindent`
components.push({
filename: ${name}.filename,
css: ${JSON.stringify(generator.css)},
map: null // TODO
});
`}
${templateProperties.components &&
deindent`
var seen = {};
function addComponent ( component ) {
var result = component.renderCss();
result.components.forEach( x => {
if ( seen[ x.filename ] ) return;
seen[ x.filename ] = true;
components.push( x );
});
}
${templateProperties.components.value.properties.map(prop => {
const { name } = prop.key;
const expression =
generator.importedComponents.get(name) ||
`@template.components.${name}`;
return `addComponent( ${expression} );`;
})}
`}
return {
css: components.map( x => x.css ).join( '\\n' ),
map: null,
components
};
};
var escaped = {
'"': '&quot;',
"'": '&#39;',
'&': '&amp;',
'<': '&lt;',
'>': '&gt;'
};
function __escape ( html ) {
return String( html ).replace( /["'&<>]/g, match => escaped[ match ] );
}
`.replace(/(\\)?@(\w*)/g, (match: string, escaped: string, name: string) => escaped ? match.slice(1) : generator.alias(name));
return generator.generate(result, options, { name, format });
}

@ -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,13 @@
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,3 +0,0 @@
export default function visitComment () {
// do nothing
}

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

@ -1,77 +0,0 @@
import flattenReference from '../../../utils/flattenReference.js';
import visit from '../visit.js';
export default function visitComponent ( generator, block, node ) {
function stringify ( chunk ) {
if ( chunk.type === 'Text' ) return chunk.data;
if ( chunk.type === 'MustacheTag' ) {
const { snippet } = block.contextualise( chunk.expression );
return '${__escape( ' + snippet + ')}';
}
}
const attributes = [];
const bindings = [];
node.attributes.forEach( attribute => {
if ( attribute.type === 'Attribute' ) {
attributes.push( attribute );
} else if ( attribute.type === 'Binding' ) {
bindings.push( attribute );
}
});
const props = attributes
.map( attribute => {
let value;
if ( attribute.value === true ) {
value = `true`;
} else if ( attribute.value.length === 0 ) {
value = `''`;
} else if ( attribute.value.length === 1 ) {
const chunk = attribute.value[0];
if ( chunk.type === 'Text' ) {
value = isNaN( chunk.data ) ? JSON.stringify( chunk.data ) : chunk.data;
} else {
const { snippet } = block.contextualise( chunk.expression );
value = snippet;
}
} else {
value = '`' + attribute.value.map( stringify ).join( '' ) + '`';
}
return `${attribute.name}: ${value}`;
})
.concat( bindings.map( binding => {
const { name, keypath } = flattenReference( binding.value );
const value = block.contexts.has( name ) ? keypath : `state.${keypath}`;
return `${binding.name}: ${value}`;
}))
.join( ', ' );
const expression = node.name === ':Self' ? generator.name : generator.importedComponents.get( node.name ) || `${generator.alias( 'template' )}.components.${node.name}`;
bindings.forEach( binding => {
block.addBinding( binding, expression );
});
let open = `\${${expression}.render({${props}}`;
if ( node.children.length ) {
open += `, { yield: () => \``;
}
generator.append( open );
generator.elementDepth += 1;
node.children.forEach( child => {
visit( generator, block, child );
});
generator.elementDepth -= 1;
const close = node.children.length ? `\` })}` : ')}';
generator.append( close );
}

@ -0,0 +1,97 @@
import flattenReference from '../../../utils/flattenReference';
import visit from '../visit';
import { SsrGenerator } from '../index';
import Block from '../Block';
import { Node } from '../../../interfaces';
import getObject from '../../../utils/getObject';
import getTailSnippet from '../../../utils/getTailSnippet';
export default function visitComponent(
generator: SsrGenerator,
block: Block,
node: Node
) {
function stringifyAttribute(chunk: Node) {
if (chunk.type === 'Text') return chunk.data;
if (chunk.type === 'MustacheTag') {
const { snippet } = block.contextualise(chunk.expression);
return '${__escape( ' + snippet + ')}';
}
}
const attributes: Node[] = [];
const bindings: Node[] = [];
node.attributes.forEach((attribute: Node) => {
if (attribute.type === 'Attribute') {
attributes.push(attribute);
} else if (attribute.type === 'Binding') {
bindings.push(attribute);
}
});
const props = attributes
.map(attribute => {
let value;
if (attribute.value === true) {
value = `true`;
} else if (attribute.value.length === 0) {
value = `''`;
} else if (attribute.value.length === 1) {
const chunk = attribute.value[0];
if (chunk.type === 'Text') {
value = isNaN(chunk.data) ? JSON.stringify(chunk.data) : chunk.data;
} else {
const { snippet } = block.contextualise(chunk.expression);
value = snippet;
}
} else {
value = '`' + attribute.value.map(stringifyAttribute).join('') + '`';
}
return `${attribute.name}: ${value}`;
})
.concat(
bindings.map(binding => {
const { name } = getObject(binding.value);
const tail = binding.value.type === 'MemberExpression'
? getTailSnippet(binding.value)
: '';
const keypath = block.contexts.has(name)
? `${name}${tail}`
: `state.${name}${tail}`;
return `${binding.name}: ${keypath}`;
})
)
.join(', ');
const expression = node.name === ':Self'
? generator.name
: generator.importedComponents.get(node.name) ||
`@template.components.${node.name}`;
bindings.forEach(binding => {
block.addBinding(binding, expression);
});
let open = `\${${expression}.render({${props}}`;
if (node.children.length) {
open += `, { yield: () => \``;
}
generator.append(open);
generator.elementDepth += 1;
node.children.forEach((child: Node) => {
visit(generator, block, child);
});
generator.elementDepth -= 1;
const close = node.children.length ? `\` })}` : ')}';
generator.append(close);
}

@ -1,32 +0,0 @@
import visit from '../visit.js';
export default function visitEachBlock ( generator, block, node ) {
const { dependencies, snippet } = block.contextualise( node.expression );
const open = `\${ ${snippet}.map( ${ node.index ? `( ${node.context}, ${node.index} )` : node.context} => \``;
generator.append( open );
// TODO should this be the generator's job? It's duplicated between
// here and the equivalent DOM compiler visitor
const contexts = new Map( block.contexts );
contexts.set( node.context, node.context );
const indexes = new Map( block.indexes );
if ( node.index ) indexes.set( node.index, node.context );
const contextDependencies = new Map( block.contextDependencies );
contextDependencies.set( node.context, dependencies );
const childBlock = block.child({
contexts,
indexes,
contextDependencies
});
node.children.forEach( child => {
visit( generator, childBlock, child );
});
const close = `\` ).join( '' )}`;
generator.append( close );
}

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

@ -1,60 +0,0 @@
import visitComponent from './Component.js';
import isVoidElementName from '../../../utils/isVoidElementName.js';
import visit from '../visit.js';
import visitWindow from './meta/Window.js';
const meta = {
':Window': visitWindow
};
export default function visitElement ( generator, block, node ) {
if ( node.name in meta ) {
return meta[ node.name ]( generator, block, node );
}
if ( generator.components.has( node.name ) || node.name === ':Self' ) {
visitComponent( generator, block, node );
return;
}
let openingTag = `<${node.name}`;
node.attributes.forEach( attribute => {
if ( attribute.type !== 'Attribute' ) return;
let str = ` ${attribute.name}`;
if ( attribute.value !== true ) {
str += `="` + attribute.value.map( chunk => {
if ( chunk.type === 'Text' ) {
return chunk.data;
}
const { snippet } = block.contextualise( chunk.expression );
return '${' + snippet + '}';
}).join( '' ) + `"`;
}
openingTag += str;
});
if ( generator.cssId && !generator.elementDepth ) {
openingTag += ` ${generator.cssId}`;
}
openingTag += '>';
generator.append( openingTag );
generator.elementDepth += 1;
node.children.forEach( child => {
visit( generator, block, child );
});
generator.elementDepth -= 1;
if ( !isVoidElementName( node.name ) ) {
generator.append( `</${node.name}>` );
}
}

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

@ -1,25 +0,0 @@
import visit from '../visit.js';
export default function visitIfBlock ( generator, block, node ) {
const { snippet } = block.contextualise( node.expression );
generator.append( '${ ' + snippet + ' ? `' );
const childBlock = block.child({
conditions: block.conditions.concat( snippet )
});
node.children.forEach( child => {
visit( generator, childBlock, child );
});
generator.append( '` : `' );
if ( node.else ) {
node.else.children.forEach( child => {
visit( generator, childBlock, child );
});
}
generator.append( '` }' );
}

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

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

@ -0,0 +1,12 @@
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,12 @@
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,11 @@
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').replace(/([^\\])?([@#])/g, '$1\\$2'));
}

@ -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,3 +0,0 @@
export default function visitWindow () {
// noop
}

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

@ -1,94 +0,0 @@
const commentsPattern = /\/\*[\s\S]*?\*\//g;
export default function processCss ( parsed, code ) {
const css = parsed.css.content.styles;
const offset = parsed.css.content.start;
const attr = `[svelte-${parsed.hash}]`;
const keyframes = new Map();
function walkKeyframes ( node ) {
if ( node.type === 'Atrule' && node.name.toLowerCase() === 'keyframes' ) {
node.expression.children.forEach( expression => {
if ( expression.type === 'Identifier' ) {
const newName = `svelte-${parsed.hash}-${expression.name}`;
code.overwrite( expression.start, expression.end, newName );
keyframes.set( expression.name, newName );
}
});
} else if ( node.children ) {
node.children.forEach( walkKeyframes );
} else if ( node.block ) {
walkKeyframes( node.block );
}
}
parsed.css.children.forEach( walkKeyframes );
function transform ( rule ) {
rule.selector.children.forEach( selector => {
const start = selector.start - offset;
const end = selector.end - offset;
const selectorString = css.slice( start, end );
const firstToken = selector.children[0];
let transformed;
if ( firstToken.type === 'TypeSelector' ) {
const insert = firstToken.end - offset;
const head = css.slice( start, insert );
const tail = css.slice( insert, end );
transformed = `${head}${attr}${tail}, ${attr} ${selectorString}`;
} else {
transformed = `${attr}${selectorString}, ${attr} ${selectorString}`;
}
code.overwrite( start + offset, end + offset, transformed );
});
rule.block.children.forEach( block => {
if ( block.type === 'Declaration' ) {
const property = block.property.toLowerCase();
if ( property === 'animation' || property === 'animation-name' ) {
block.value.children.forEach( block => {
if ( block.type === 'Identifier' ) {
const name = block.name;
if ( keyframes.has( name ) ) {
code.overwrite( block.start, block.end, keyframes.get( name ) );
}
}
});
}
}
});
}
function walk ( node ) {
if ( node.type === 'Rule' ) {
transform( node );
} else if ( node.type === 'Atrule' && node.name.toLowerCase() === 'keyframes' ) {
// these have already been processed
} else if ( node.children ) {
node.children.forEach( walk );
} else if ( node.block ) {
walk( node.block );
}
}
parsed.css.children.forEach( walk );
// remove comments. TODO would be nice if this was exposed in css-tree
let match;
while ( match = commentsPattern.exec( css ) ) {
const start = match.index + offset;
const end = start + match[0].length;
code.remove( start, end );
}
return code.slice( parsed.css.content.start, parsed.css.content.end );
}

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

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

@ -0,0 +1,50 @@
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);
return imports.map(x => {
let name = globalFn(x.source.value);
if (!name) {
if (x.name.startsWith('__import')) {
const error = new Error(
`Could not determine name for imported module '${x.source
.value}' use options.globals`
);
if (onerror) {
onerror(error);
} else {
throw error;
}
} else {
const warning = {
message: `No name was supplied for imported module '${x.source
.value}'. Guessing '${x.name}', but you should use options.globals`,
};
if (onwarn) {
onwarn(warning);
} else {
console.warn(warning); // eslint-disable-line no-console
}
}
name = x.name;
}
return name;
});
}
function getGlobalFn(globals: any): Globals {
if (typeof globals === 'function') return globals;
if (typeof globals === 'object') {
return id => globals[id];
}
return () => undefined;
}

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

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

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

Loading…
Cancel
Save