mirror of https://github.com/sveltejs/svelte
commit
6242038be5
@ -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
|
File diff suppressed because it is too large
Load Diff
@ -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,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,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,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,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 = {
|
||||
'"': '"',
|
||||
"'": ''',
|
||||
'&': '&',
|
||||
'<': '<',
|
||||
'>': '>'
|
||||
};
|
||||
|
||||
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 = {
|
||||
'"': '"',
|
||||
"'": ''',
|
||||
'&': '&',
|
||||
'<': '<',
|
||||
'>': '>'
|
||||
};
|
||||
|
||||
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,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…
Reference in new issue