pull/1367/head
Rich Harris 7 years ago
parent 64843331d4
commit 9ff1beec48

@ -194,7 +194,7 @@ export default class Generator {
} }
this.fragment = new Fragment(this, parsed.html); this.fragment = new Fragment(this, parsed.html);
this.walkTemplate(); // this.walkTemplate();
if (!this.customElement) this.stylesheet.reify(); if (!this.customElement) this.stylesheet.reify();
} }
@ -215,107 +215,107 @@ export default class Generator {
return this.aliases.get(name); return this.aliases.get(name);
} }
contextualise( // contextualise(
contexts: Map<string, string>, // contexts: Map<string, string>,
indexes: Map<string, string>, // indexes: Map<string, string>,
expression: Node, // expression: Node,
context: string, // context: string,
isEventHandler: boolean // isEventHandler: boolean
): { // ): {
contexts: Set<string>, // contexts: Set<string>,
indexes: Set<string> // indexes: Set<string>
} { // } {
// this.addSourcemapLocations(expression); // // this.addSourcemapLocations(expression);
const usedContexts: Set<string> = new Set(); // const usedContexts: Set<string> = new Set();
const usedIndexes: Set<string> = new Set(); // const usedIndexes: Set<string> = new Set();
const { code, helpers } = this; // const { code, helpers } = this;
let scope: Scope; // let scope: Scope;
let lexicalDepth = 0; // let lexicalDepth = 0;
const self = this; // const self = this;
walk(expression, { // walk(expression, {
enter(node: Node, parent: Node, key: string) { // enter(node: Node, parent: Node, key: string) {
if (/^Function/.test(node.type)) lexicalDepth += 1; // if (/^Function/.test(node.type)) lexicalDepth += 1;
if (node._scope) { // if (node._scope) {
scope = node._scope; // scope = node._scope;
return; // return;
} // }
if (node.type === 'ThisExpression') { // if (node.type === 'ThisExpression') {
if (lexicalDepth === 0 && context) // if (lexicalDepth === 0 && context)
code.overwrite(node.start, node.end, context, { // code.overwrite(node.start, node.end, context, {
storeName: true, // storeName: true,
contentOnly: false, // contentOnly: false,
}); // });
} else if (isReference(node, parent)) { // } else if (isReference(node, parent)) {
const { name } = flattenReference(node); // const { name } = flattenReference(node);
if (scope && scope.has(name)) return; // if (scope && scope.has(name)) return;
if (name === 'event' && isEventHandler) { // if (name === 'event' && isEventHandler) {
// noop // // noop
} else if (contexts.has(name)) { // } else if (contexts.has(name)) {
const contextName = contexts.get(name); // const contextName = contexts.get(name);
if (contextName !== name) { // if (contextName !== name) {
// this is true for 'reserved' names like `state` and `component`, // // this is true for 'reserved' names like `state` and `component`,
// also destructured contexts // // also destructured contexts
code.overwrite( // code.overwrite(
node.start, // node.start,
node.start + name.length, // node.start + name.length,
contextName, // contextName,
{ storeName: true, contentOnly: false } // { storeName: true, contentOnly: false }
); // );
const destructuredName = contextName.replace(/\[\d+\]/, ''); // const destructuredName = contextName.replace(/\[\d+\]/, '');
if (destructuredName !== contextName) { // if (destructuredName !== contextName) {
// so that hoisting the context works correctly // // so that hoisting the context works correctly
usedContexts.add(destructuredName); // usedContexts.add(destructuredName);
} // }
} // }
usedContexts.add(name); // usedContexts.add(name);
} else if (helpers.has(name)) { // } else if (helpers.has(name)) {
let object = node; // let object = node;
while (object.type === 'MemberExpression') object = object.object; // while (object.type === 'MemberExpression') object = object.object;
const alias = self.templateVars.get(`helpers-${name}`); // const alias = self.templateVars.get(`helpers-${name}`);
if (alias !== name) code.overwrite(object.start, object.end, alias); // if (alias !== name) code.overwrite(object.start, object.end, alias);
} else if (indexes.has(name)) { // } else if (indexes.has(name)) {
const context = indexes.get(name); // const context = indexes.get(name);
usedContexts.add(context); // TODO is this right? // usedContexts.add(context); // TODO is this right?
usedIndexes.add(name); // usedIndexes.add(name);
} else { // } else {
// handle shorthand properties // // handle shorthand properties
if (parent && parent.type === 'Property' && parent.shorthand) { // if (parent && parent.type === 'Property' && parent.shorthand) {
if (key === 'key') { // if (key === 'key') {
code.appendLeft(node.start, `${name}: `); // code.appendLeft(node.start, `${name}: `);
return; // return;
} // }
} // }
code.prependRight(node.start, `state.`); // code.prependRight(node.start, `state.`);
usedContexts.add('state'); // usedContexts.add('state');
} // }
this.skip(); // this.skip();
} // }
}, // },
leave(node: Node) { // leave(node: Node) {
if (/^Function/.test(node.type)) lexicalDepth -= 1; // if (/^Function/.test(node.type)) lexicalDepth -= 1;
if (node._scope) scope = scope.parent; // if (node._scope) scope = scope.parent;
}, // },
}); // });
return { // return {
contexts: usedContexts, // contexts: usedContexts,
indexes: usedIndexes // indexes: usedIndexes
}; // };
} // }
generate(result: string, options: CompileOptions, { banner = '', sharedPath, helpers, name, format }: GenerateOptions ) { generate(result: string, options: CompileOptions, { banner = '', sharedPath, helpers, name, format }: GenerateOptions ) {
const pattern = /\[✂(\d+)-(\d+)$/; const pattern = /\[✂(\d+)-(\d+)$/;
@ -707,211 +707,211 @@ export default class Generator {
} }
} }
walkTemplate() { // walkTemplate() {
const generator = this; // const generator = this;
const { // const {
code, // code,
expectedProperties, // expectedProperties,
helpers // helpers
} = this; // } = this;
const contextualise = ( // const contextualise = (
node: Node, contextDependencies: Map<string, string[]>, // node: Node, contextDependencies: Map<string, string[]>,
indexes: Set<string>, // indexes: Set<string>,
isEventHandler: boolean // isEventHandler: boolean
) => { // ) => {
this.addSourcemapLocations(node); // TODO this involves an additional walk — can we roll it in somewhere else? // this.addSourcemapLocations(node); // TODO this involves an additional walk — can we roll it in somewhere else?
let { scope } = annotateWithScopes(node); // let { scope } = annotateWithScopes(node);
const dependencies: Set<string> = new Set(); // const dependencies: Set<string> = new Set();
walk(node, { // walk(node, {
enter(node: Node, parent: Node) { // enter(node: Node, parent: Node) {
code.addSourcemapLocation(node.start); // code.addSourcemapLocation(node.start);
code.addSourcemapLocation(node.end); // code.addSourcemapLocation(node.end);
if (node._scope) { // if (node._scope) {
scope = node._scope; // scope = node._scope;
return; // return;
} // }
if (isReference(node, parent)) { // if (isReference(node, parent)) {
const { name } = flattenReference(node); // const { name } = flattenReference(node);
if (scope && scope.has(name) || helpers.has(name) || (name === 'event' && isEventHandler)) return; // if (scope && scope.has(name) || helpers.has(name) || (name === 'event' && isEventHandler)) return;
if (contextDependencies.has(name)) { // if (contextDependencies.has(name)) {
contextDependencies.get(name).forEach(dependency => { // contextDependencies.get(name).forEach(dependency => {
dependencies.add(dependency); // dependencies.add(dependency);
}); // });
} else if (!indexes.has(name)) { // } else if (!indexes.has(name)) {
dependencies.add(name); // dependencies.add(name);
} // }
this.skip(); // this.skip();
} // }
}, // },
leave(node: Node, parent: Node) { // leave(node: Node, parent: Node) {
if (node._scope) scope = scope.parent; // if (node._scope) scope = scope.parent;
} // }
}); // });
dependencies.forEach(dependency => { // dependencies.forEach(dependency => {
expectedProperties.add(dependency); // expectedProperties.add(dependency);
}); // });
return { // return {
snippet: `[✂${node.start}-${node.end}✂]`, // snippet: `[✂${node.start}-${node.end}✂]`,
dependencies: Array.from(dependencies) // dependencies: Array.from(dependencies)
}; // };
} // }
const contextStack = []; // const contextStack = [];
const indexStack = []; // const indexStack = [];
const dependenciesStack = []; // const dependenciesStack = [];
let contextDependencies = new Map(); // let contextDependencies = new Map();
const contextDependenciesStack: Map<string, string[]>[] = [contextDependencies]; // const contextDependenciesStack: Map<string, string[]>[] = [contextDependencies];
let indexes = new Set(); // let indexes = new Set();
const indexesStack: Set<string>[] = [indexes]; // const indexesStack: Set<string>[] = [indexes];
function parentIsHead(node) { // function parentIsHead(node) {
if (!node) return false; // if (!node) return false;
if (node.type === 'Component' || node.type === 'Element') return false; // if (node.type === 'Component' || node.type === 'Element') return false;
if (node.type === 'Head') return true; // if (node.type === 'Head') return true;
return parentIsHead(node.parent); // return parentIsHead(node.parent);
} // }
walk(this.fragment, { // walk(this.fragment, {
enter(node: Node, parent: Node, key: string) { // enter(node: Node, parent: Node, key: string) {
// TODO this is hacky as hell // // TODO this is hacky as hell
if (key === 'parent') return this.skip(); // if (key === 'parent') return this.skip();
node.parent = parent; // node.parent = parent;
node.generator = generator; // node.generator = generator;
if (node.type === 'Element' && (node.name === 'svelte:component' || node.name === 'svelte:self' || generator.components.has(node.name))) { // if (node.type === 'Element' && (node.name === 'svelte:component' || node.name === 'svelte:self' || generator.components.has(node.name))) {
node.type = 'Component'; // node.type = 'Component';
Object.setPrototypeOf(node, nodes.Component.prototype); // Object.setPrototypeOf(node, nodes.Component.prototype);
} else if (node.type === 'Element' && node.name === 'title' && parentIsHead(parent)) { // TODO do this in parse? // } else if (node.type === 'Element' && node.name === 'title' && parentIsHead(parent)) { // TODO do this in parse?
node.type = 'Title'; // node.type = 'Title';
Object.setPrototypeOf(node, nodes.Title.prototype); // Object.setPrototypeOf(node, nodes.Title.prototype);
} else if (node.type === 'Element' && node.name === 'slot' && !generator.customElement) { // } else if (node.type === 'Element' && node.name === 'slot' && !generator.customElement) {
node.type = 'Slot'; // node.type = 'Slot';
Object.setPrototypeOf(node, nodes.Slot.prototype); // Object.setPrototypeOf(node, nodes.Slot.prototype);
} else if (node.type in nodes) { // } else if (node.type in nodes) {
Object.setPrototypeOf(node, nodes[node.type].prototype); // Object.setPrototypeOf(node, nodes[node.type].prototype);
} // }
if (node.type === 'Element') { // if (node.type === 'Element') {
generator.stylesheet.apply(node); // generator.stylesheet.apply(node);
} // }
if (node.type === 'EachBlock') { // if (node.type === 'EachBlock') {
node.metadata = contextualise(node.expression, contextDependencies, indexes, false); // node.metadata = contextualise(node.expression, contextDependencies, indexes, false);
contextDependencies = new Map(contextDependencies); // contextDependencies = new Map(contextDependencies);
contextDependencies.set(node.context, node.metadata.dependencies); // contextDependencies.set(node.context, node.metadata.dependencies);
if (node.destructuredContexts) { // if (node.destructuredContexts) {
node.destructuredContexts.forEach((name: string) => { // node.destructuredContexts.forEach((name: string) => {
contextDependencies.set(name, node.metadata.dependencies); // contextDependencies.set(name, node.metadata.dependencies);
}); // });
} // }
contextDependenciesStack.push(contextDependencies); // contextDependenciesStack.push(contextDependencies);
if (node.index) { // if (node.index) {
indexes = new Set(indexes); // indexes = new Set(indexes);
indexes.add(node.index); // indexes.add(node.index);
indexesStack.push(indexes); // indexesStack.push(indexes);
} // }
} // }
if (node.type === 'AwaitBlock') { // if (node.type === 'AwaitBlock') {
node.metadata = contextualise(node.expression, contextDependencies, indexes, false); // node.metadata = contextualise(node.expression, contextDependencies, indexes, false);
contextDependencies = new Map(contextDependencies); // contextDependencies = new Map(contextDependencies);
contextDependencies.set(node.value, node.metadata.dependencies); // contextDependencies.set(node.value, node.metadata.dependencies);
contextDependencies.set(node.error, node.metadata.dependencies); // contextDependencies.set(node.error, node.metadata.dependencies);
contextDependenciesStack.push(contextDependencies); // contextDependenciesStack.push(contextDependencies);
} // }
if (node.type === 'IfBlock') { // if (node.type === 'IfBlock') {
node.metadata = contextualise(node.expression, contextDependencies, indexes, false); // node.metadata = contextualise(node.expression, contextDependencies, indexes, false);
} // }
if (node.type === 'MustacheTag' || node.type === 'RawMustacheTag' || node.type === 'AttributeShorthand') { // if (node.type === 'MustacheTag' || node.type === 'RawMustacheTag' || node.type === 'AttributeShorthand') {
node.metadata = contextualise(node.expression, contextDependencies, indexes, false); // node.metadata = contextualise(node.expression, contextDependencies, indexes, false);
this.skip(); // this.skip();
} // }
if (node.type === 'Binding') { // if (node.type === 'Binding') {
node.metadata = contextualise(node.value, contextDependencies, indexes, false); // node.metadata = contextualise(node.value, contextDependencies, indexes, false);
this.skip(); // this.skip();
} // }
if (node.type === 'EventHandler' && node.expression) { // if (node.type === 'EventHandler' && node.expression) {
node.expression.arguments.forEach((arg: Node) => { // node.expression.arguments.forEach((arg: Node) => {
arg.metadata = contextualise(arg, contextDependencies, indexes, true); // arg.metadata = contextualise(arg, contextDependencies, indexes, true);
}); // });
this.skip(); // this.skip();
} // }
if (node.type === 'Transition' && node.expression) { // if (node.type === 'Transition' && node.expression) {
node.metadata = contextualise(node.expression, contextDependencies, indexes, false); // node.metadata = contextualise(node.expression, contextDependencies, indexes, false);
this.skip(); // this.skip();
} // }
if (node.type === 'Action' && node.expression) { // if (node.type === 'Action' && node.expression) {
node.metadata = contextualise(node.expression, contextDependencies, indexes, false); // node.metadata = contextualise(node.expression, contextDependencies, indexes, false);
if (node.expression.type === 'CallExpression') { // if (node.expression.type === 'CallExpression') {
node.expression.arguments.forEach((arg: Node) => { // node.expression.arguments.forEach((arg: Node) => {
arg.metadata = contextualise(arg, contextDependencies, indexes, true); // arg.metadata = contextualise(arg, contextDependencies, indexes, true);
}); // });
} // }
this.skip(); // this.skip();
} // }
if (node.type === 'Component' && node.name === 'svelte:component') { // if (node.type === 'Component' && node.name === 'svelte:component') {
node.metadata = contextualise(node.expression, contextDependencies, indexes, false); // node.metadata = contextualise(node.expression, contextDependencies, indexes, false);
} // }
if (node.type === 'Spread') { // if (node.type === 'Spread') {
node.metadata = contextualise(node.expression, contextDependencies, indexes, false); // node.metadata = contextualise(node.expression, contextDependencies, indexes, false);
} // }
}, // },
leave(node: Node, parent: Node) { // leave(node: Node, parent: Node) {
if (node.type === 'EachBlock') { // if (node.type === 'EachBlock') {
contextDependenciesStack.pop(); // contextDependenciesStack.pop();
contextDependencies = contextDependenciesStack[contextDependenciesStack.length - 1]; // contextDependencies = contextDependenciesStack[contextDependenciesStack.length - 1];
if (node.index) { // if (node.index) {
indexesStack.pop(); // indexesStack.pop();
indexes = indexesStack[indexesStack.length - 1]; // indexes = indexesStack[indexesStack.length - 1];
} // }
} // }
if (node.type === 'Element' && node.name === 'option') { // if (node.type === 'Element' && node.name === 'option') {
// Special case — treat these the same way: // // Special case — treat these the same way:
// <option>{{foo}}</option> // // <option>{{foo}}</option>
// <option value='{{foo}}'>{{foo}}</option> // // <option value='{{foo}}'>{{foo}}</option>
const valueAttribute = node.attributes.find((attribute: Node) => attribute.name === 'value'); // const valueAttribute = node.attributes.find((attribute: Node) => attribute.name === 'value');
if (!valueAttribute) { // if (!valueAttribute) {
node.attributes.push(new nodes.Attribute({ // node.attributes.push(new nodes.Attribute({
generator, // generator,
name: 'value', // name: 'value',
value: node.children, // value: node.children,
parent: node // parent: node
})); // }));
} // }
} // }
} // }
}); // });
} // }
} }

@ -110,13 +110,13 @@ export default class Block {
this.aliases = new Map() this.aliases = new Map()
.set('component', this.getUniqueName('component')) .set('component', this.getUniqueName('component'))
.set('state', this.getUniqueName('state')); .set('ctx', this.getUniqueName('ctx'));
if (this.key) this.aliases.set('key', this.getUniqueName('key')); if (this.key) this.aliases.set('key', this.getUniqueName('key'));
this.hasUpdateMethod = false; // determined later this.hasUpdateMethod = false; // determined later
} }
addDependencies(dependencies: string[]) { addDependencies(dependencies: Set<string>) {
dependencies.forEach(dependency => { dependencies.forEach(dependency => {
this.dependencies.add(dependency); this.dependencies.add(dependency);
}); });
@ -163,10 +163,6 @@ export default class Block {
return new Block(Object.assign({}, this, { key: null }, options, { parent: this })); return new Block(Object.assign({}, this, { key: null }, options, { parent: this }));
} }
contextualise(expression: Node, context?: string, isEventHandler?: boolean) {
return this.generator.contextualise(this.contexts, this.indexes, expression, context, isEventHandler);
}
toString() { toString() {
let introing; let introing;
const hasIntros = !this.builders.intro.isEmpty(); const hasIntros = !this.builders.intro.isEmpty();
@ -195,9 +191,9 @@ export default class Block {
const indexName = this.indexNames.get(context); const indexName = this.indexNames.get(context);
initializers.push( initializers.push(
`${name} = state.${context}`, `${name} = ctx.${context}`,
`${listName} = state.${listName}`, `${listName} = ctx.${listName}`,
`${indexName} = state.${indexName}` `${indexName} = ctx.${indexName}`
); );
this.hasUpdateMethod = true; this.hasUpdateMethod = true;
@ -266,7 +262,7 @@ export default class Block {
properties.addBlock(`p: @noop,`); properties.addBlock(`p: @noop,`);
} else { } else {
properties.addBlock(deindent` properties.addBlock(deindent`
p: function update(changed, state) { p: function update(changed, ctx) {
${initializers.map(str => `${str};`)} ${initializers.map(str => `${str};`)}
${this.builders.update} ${this.builders.update}
}, },
@ -338,7 +334,7 @@ export default class Block {
return deindent` return deindent`
${this.comment && `// ${escape(this.comment)}`} ${this.comment && `// ${escape(this.comment)}`}
function ${this.name}(#component${this.key ? `, ${localKey}` : ''}, state) { function ${this.name}(#component${this.key ? `, ${localKey}` : ''}, ctx) {
${initializers.length > 0 && ${initializers.length > 0 &&
`var ${initializers.join(', ')};`} `var ${initializers.join(', ')};`}
${this.variables.size > 0 && ${this.variables.size > 0 &&

@ -2,10 +2,12 @@ import deindent from '../../utils/deindent';
import { stringify } from '../../utils/stringify'; import { stringify } from '../../utils/stringify';
import fixAttributeCasing from '../../utils/fixAttributeCasing'; import fixAttributeCasing from '../../utils/fixAttributeCasing';
import getExpressionPrecedence from '../../utils/getExpressionPrecedence'; import getExpressionPrecedence from '../../utils/getExpressionPrecedence';
import addToSet from '../../utils/addToSet';
import { DomGenerator } from '../dom/index'; import { DomGenerator } from '../dom/index';
import Node from './shared/Node'; import Node from './shared/Node';
import Element from './Element'; import Element from './Element';
import Block from '../dom/Block'; import Block from '../dom/Block';
import Expression from './shared/Expression';
export interface StyleProp { export interface StyleProp {
key: string; key: string;
@ -20,14 +22,32 @@ export default class Attribute extends Node {
compiler: DomGenerator; compiler: DomGenerator;
parent: Element; parent: Element;
name: string; name: string;
value: true | Node[] isTrue: boolean;
isDynamic: boolean;
chunks: Node[];
dependencies: Set<string>;
expression: Node; expression: Node;
constructor(compiler, parent, info) { constructor(compiler, parent, info) {
super(compiler, parent, info); super(compiler, parent, info);
this.name = info.name; this.name = info.name;
this.value = info.value; this.isTrue = info.value === true;
this.dependencies = new Set();
this.chunks = this.isTrue
? []
: info.value.map(node => {
if (node.type === 'Text') return node;
const expression = new Expression(compiler, this, node.expression);
addToSet(this.dependencies, expression.dependencies);
return expression;
});
this.isDynamic = this.dependencies.size > 0;
} }
render(block: Block) { render(block: Block) {
@ -35,7 +55,7 @@ export default class Attribute extends Node {
const name = fixAttributeCasing(this.name); const name = fixAttributeCasing(this.name);
if (name === 'style') { if (name === 'style') {
const styleProps = optimizeStyle(this.value); const styleProps = optimizeStyle(this.chunks);
if (styleProps) { if (styleProps) {
this.renderStyle(block, styleProps); this.renderStyle(block, styleProps);
return; return;
@ -66,15 +86,14 @@ export default class Attribute extends Node {
? '@setXlinkAttribute' ? '@setXlinkAttribute'
: '@setAttribute'; : '@setAttribute';
const isDynamic = this.isDynamic(); const isLegacyInputType = this.compiler.legacy && name === 'type' && this.parent.name === 'input';
const isLegacyInputType = this.generator.legacy && name === 'type' && this.parent.name === 'input';
const isDataSet = /^data-/.test(name) && !this.generator.legacy && !node.namespace; const isDataSet = /^data-/.test(name) && !this.compiler.legacy && !node.namespace;
const camelCaseName = isDataSet ? name.replace('data-', '').replace(/(-\w)/g, function (m) { const camelCaseName = isDataSet ? name.replace('data-', '').replace(/(-\w)/g, function (m) {
return m[1].toUpperCase(); return m[1].toUpperCase();
}) : name; }) : name;
if (isDynamic) { if (this.isDynamic) {
let value; let value;
const allDependencies = new Set(); const allDependencies = new Set();
@ -83,11 +102,10 @@ export default class Attribute extends Node {
// TODO some of this code is repeated in Tag.ts — would be good to // TODO some of this code is repeated in Tag.ts — would be good to
// DRY it out if that's possible without introducing crazy indirection // DRY it out if that's possible without introducing crazy indirection
if (this.value.length === 1) { if (this.chunks.length === 1) {
// single {{tag}} — may be a non-string // single {tag} — may be a non-string
const { expression } = this.value[0]; const expression = this.chunks[0];
const { indexes } = block.contextualise(expression); const { dependencies, snippet, indexes } = expression;
const { dependencies, snippet } = this.value[0].metadata;
value = snippet; value = snippet;
dependencies.forEach(d => { dependencies.forEach(d => {
@ -104,14 +122,13 @@ export default class Attribute extends Node {
} else { } else {
// '{{foo}} {{bar}}' — treat as string concatenation // '{{foo}} {{bar}}' — treat as string concatenation
value = value =
(this.value[0].type === 'Text' ? '' : `"" + `) + (this.chunks[0].type === 'Text' ? '' : `"" + `) +
this.value this.chunks
.map((chunk: Node) => { .map((chunk: Node) => {
if (chunk.type === 'Text') { if (chunk.type === 'Text') {
return stringify(chunk.data); return stringify(chunk.data);
} else { } else {
const { indexes } = block.contextualise(chunk.expression); const { dependencies, snippet, indexes } = chunk;
const { dependencies, snippet } = chunk.metadata;
if (Array.from(indexes).some(index => block.changeableIndexes.get(index))) { if (Array.from(indexes).some(index => block.changeableIndexes.get(index))) {
hasChangeableIndex = true; hasChangeableIndex = true;
@ -121,7 +138,7 @@ export default class Attribute extends Node {
allDependencies.add(d); allDependencies.add(d);
}); });
return getExpressionPrecedence(chunk.expression) <= 13 ? `(${snippet})` : snippet; return getExpressionPrecedence(chunk) <= 13 ? `(${snippet})` : snippet;
} }
}) })
.join(' + '); .join(' + ');
@ -211,9 +228,9 @@ export default class Attribute extends Node {
); );
} }
} else { } else {
const value = this.value === true const value = this.isTrue
? 'true' ? 'true'
: this.value.length === 0 ? `""` : stringify(this.value[0].data); : this.chunks.length === 0 ? `""` : stringify(this.chunks[0].data);
const statement = ( const statement = (
isLegacyInputType isLegacyInputType
@ -237,7 +254,7 @@ export default class Attribute extends Node {
const updateValue = `${node.var}.value = ${node.var}.__value;`; const updateValue = `${node.var}.value = ${node.var}.__value;`;
block.builders.hydrate.addLine(updateValue); block.builders.hydrate.addLine(updateValue);
if (isDynamic) block.builders.update.addLine(updateValue); if (this.isDynamic) block.builders.update.addLine(updateValue);
} }
} }
@ -260,8 +277,7 @@ export default class Attribute extends Node {
if (chunk.type === 'Text') { if (chunk.type === 'Text') {
return stringify(chunk.data); return stringify(chunk.data);
} else { } else {
const { indexes } = block.contextualise(chunk.expression); const { dependencies, snippet, indexes } = chunk;
const { dependencies, snippet } = chunk.metadata;
if (Array.from(indexes).some(index => block.changeableIndexes.get(index))) { if (Array.from(indexes).some(index => block.changeableIndexes.get(index))) {
hasChangeableIndex = true; hasChangeableIndex = true;
@ -297,12 +313,6 @@ export default class Attribute extends Node {
); );
}); });
} }
isDynamic() {
if (this.value === true || this.value.length === 0) return false;
if (this.value.length > 1) return true;
return this.value[0].type !== 'Text';
}
} }
// source: https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes // source: https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes

@ -5,6 +5,7 @@ import getTailSnippet from '../../utils/getTailSnippet';
import flattenReference from '../../utils/flattenReference'; import flattenReference from '../../utils/flattenReference';
import { DomGenerator } from '../dom/index'; import { DomGenerator } from '../dom/index';
import Block from '../dom/Block'; import Block from '../dom/Block';
import Expression from './shared/Expression';
const readOnlyMediaAttributes = new Set([ const readOnlyMediaAttributes = new Set([
'duration', 'duration',
@ -15,8 +16,14 @@ const readOnlyMediaAttributes = new Set([
export default class Binding extends Node { export default class Binding extends Node {
name: string; name: string;
value: Node; value: Expression;
expression: Node;
constructor(compiler, parent, info) {
super(compiler, parent, info);
this.name = info.name;
this.value = new Expression(compiler, this, info.value);
}
munge( munge(
block: Block, block: Block,
@ -29,21 +36,20 @@ export default class Binding extends Node {
let updateCondition: string; let updateCondition: string;
const { name } = getObject(this.value); const { name } = getObject(this.value.node);
const { contexts } = block.contextualise(this.value); const { contexts, snippet } = this.value;
const { snippet } = this.metadata;
// special case: if you have e.g. `<input type=checkbox bind:checked=selected.done>` // special case: if you have e.g. `<input type=checkbox bind:checked=selected.done>`
// and `selected` is an object chosen with a <select>, then when `checked` changes, // and `selected` is an object chosen with a <select>, then when `checked` changes,
// we need to tell the component to update all the values `selected` might be // we need to tell the component to update all the values `selected` might be
// pointing to // pointing to
// TODO should this happen in preprocess? // TODO should this happen in preprocess?
const dependencies = this.metadata.dependencies.slice(); const dependencies = new Set(this.value.dependencies);
this.metadata.dependencies.forEach((prop: string) => { this.value.dependencies.forEach((prop: string) => {
const indirectDependencies = this.generator.indirectDependencies.get(prop); const indirectDependencies = this.compiler.indirectDependencies.get(prop);
if (indirectDependencies) { if (indirectDependencies) {
indirectDependencies.forEach(indirectDependency => { indirectDependencies.forEach(indirectDependency => {
if (!~dependencies.indexOf(indirectDependency)) dependencies.push(indirectDependency); dependencies.add(indirectDependency);
}); });
} }
}); });
@ -53,8 +59,8 @@ export default class Binding extends Node {
}); });
// view to model // view to model
const valueFromDom = getValueFromDom(this.generator, node, this); const valueFromDom = getValueFromDom(this.compiler, node, this);
const handler = getEventHandler(this.generator, block, name, snippet, this, dependencies, valueFromDom); const handler = getEventHandler(this.compiler, block, name, snippet, this, dependencies, valueFromDom);
// model to view // model to view
let updateDom = getDomUpdater(node, this, snippet); let updateDom = getDomUpdater(node, this, snippet);
@ -62,7 +68,7 @@ export default class Binding extends Node {
// special cases // special cases
if (this.name === 'group') { if (this.name === 'group') {
const bindingGroup = getBindingGroup(this.generator, this.value); const bindingGroup = getBindingGroup(this.compiler, this.value);
block.builders.hydrate.addLine( block.builders.hydrate.addLine(
`#component._bindingGroups[${bindingGroup}].push(${node.var});` `#component._bindingGroups[${bindingGroup}].push(${node.var});`
@ -135,23 +141,23 @@ function getDomUpdater(
return `${node.var}.${binding.name} = ${snippet};`; return `${node.var}.${binding.name} = ${snippet};`;
} }
function getBindingGroup(generator: DomGenerator, value: Node) { function getBindingGroup(compiler: DomGenerator, value: Node) {
const { parts } = flattenReference(value); // TODO handle cases involving computed member expressions const { parts } = flattenReference(value); // TODO handle cases involving computed member expressions
const keypath = parts.join('.'); const keypath = parts.join('.');
// TODO handle contextual bindings — `keypath` should include unique ID of // TODO handle contextual bindings — `keypath` should include unique ID of
// each block that provides context // each block that provides context
let index = generator.bindingGroups.indexOf(keypath); let index = compiler.bindingGroups.indexOf(keypath);
if (index === -1) { if (index === -1) {
index = generator.bindingGroups.length; index = compiler.bindingGroups.length;
generator.bindingGroups.push(keypath); compiler.bindingGroups.push(keypath);
} }
return index; return index;
} }
function getEventHandler( function getEventHandler(
generator: DomGenerator, compiler: DomGenerator,
block: Block, block: Block,
name: string, name: string,
snippet: string, snippet: string,
@ -159,8 +165,8 @@ function getEventHandler(
dependencies: string[], dependencies: string[],
value: string, value: string,
) { ) {
const storeDependencies = dependencies.filter(prop => prop[0] === '$').map(prop => prop.slice(1)); const storeDependencies = [...dependencies].filter(prop => prop[0] === '$').map(prop => prop.slice(1));
dependencies = dependencies.filter(prop => prop[0] !== '$'); dependencies = [...dependencies].filter(prop => prop[0] !== '$');
if (block.contexts.has(name)) { if (block.contexts.has(name)) {
const tail = attribute.value.type === 'MemberExpression' const tail = attribute.value.type === 'MemberExpression'
@ -186,9 +192,9 @@ function getEventHandler(
// Svelte tries to `set()` a computed property, which throws an // Svelte tries to `set()` a computed property, which throws an
// error in dev mode. a) it's possible that we should be // error in dev mode. a) it's possible that we should be
// replacing computations with *their* dependencies, and b) // replacing computations with *their* dependencies, and b)
// we should probably populate `generator.readonly` sooner so // we should probably populate `compiler.readonly` sooner so
// that we don't have to do the `.some()` here // that we don't have to do the `.some()` here
dependencies = dependencies.filter(prop => !generator.computations.some(computation => computation.key === prop)); dependencies = dependencies.filter(prop => !compiler.computations.some(computation => computation.key === prop));
return { return {
usesContext: false, usesContext: false,
@ -222,7 +228,7 @@ function getEventHandler(
} }
function getValueFromDom( function getValueFromDom(
generator: DomGenerator, compiler: DomGenerator,
node: Element, node: Element,
binding: Node binding: Node
) { ) {
@ -237,7 +243,7 @@ function getValueFromDom(
// <input type='checkbox' bind:group='foo'> // <input type='checkbox' bind:group='foo'>
if (binding.name === 'group') { if (binding.name === 'group') {
const bindingGroup = getBindingGroup(generator, binding.value); const bindingGroup = getBindingGroup(compiler, binding.value);
if (type === 'checkbox') { if (type === 'checkbox') {
return `@getBindingGroupValue(#component._bindingGroups[${bindingGroup}])`; return `@getBindingGroupValue(#component._bindingGroups[${bindingGroup}])`;
} }

@ -11,6 +11,7 @@ import Node from './shared/Node';
import Block from '../dom/Block'; import Block from '../dom/Block';
import Attribute from './Attribute'; import Attribute from './Attribute';
import usesThisOrArguments from '../../validate/js/utils/usesThisOrArguments'; import usesThisOrArguments from '../../validate/js/utils/usesThisOrArguments';
import mapChildren from './shared/mapChildren';
export default class Component extends Node { export default class Component extends Node {
type: 'Component'; type: 'Component';
@ -18,6 +19,31 @@ export default class Component extends Node {
attributes: Attribute[]; attributes: Attribute[];
children: Node[]; children: Node[];
constructor(compiler, parent, info) {
super(compiler, parent, info);
compiler.hasComponents = true;
this.name = info.name;
this.attributes = [];
// TODO bindings etc
info.attributes.forEach(node => {
switch (node.type) {
case 'Attribute':
// TODO spread
this.attributes.push(new Attribute(compiler, this, node));
break;
default:
throw new Error(`Not implemented: ${node.type}`);
}
});
this.children = mapChildren(compiler, this, info.children);
}
init( init(
block: Block, block: Block,
stripWhitespace: boolean, stripWhitespace: boolean,
@ -46,7 +72,7 @@ export default class Component extends Node {
this.var = block.getUniqueName( this.var = block.getUniqueName(
( (
this.name === 'svelte:self' ? this.generator.name : this.name === 'svelte:self' ? this.compiler.name :
this.name === 'svelte:component' ? 'switch_instance' : this.name === 'svelte:component' ? 'switch_instance' :
this.name this.name
).toLowerCase() ).toLowerCase()
@ -66,8 +92,7 @@ export default class Component extends Node {
parentNode: string, parentNode: string,
parentNodes: string parentNodes: string
) { ) {
const { generator } = this; const { compiler } = this;
generator.hasComponents = true;
const name = this.var; const name = this.var;
@ -100,10 +125,10 @@ export default class Component extends Node {
const eventHandlers = this.attributes const eventHandlers = this.attributes
.filter((a: Node) => a.type === 'EventHandler') .filter((a: Node) => a.type === 'EventHandler')
.map(a => mungeEventHandler(generator, this, a, block, allContexts)); .map(a => mungeEventHandler(compiler, this, a, block, allContexts));
const ref = this.attributes.find((a: Node) => a.type === 'Ref'); const ref = this.attributes.find((a: Node) => a.type === 'Ref');
if (ref) generator.usesRefs = true; if (ref) compiler.usesRefs = true;
const updates: string[] = []; const updates: string[] = [];
@ -187,7 +212,7 @@ export default class Component extends Node {
} }
if (bindings.length) { if (bindings.length) {
generator.hasComplexBindings = true; compiler.hasComplexBindings = true;
name_updating = block.alias(`${name}_updating`); name_updating = block.alias(`${name}_updating`);
block.addVariable(name_updating, '{}'); block.addVariable(name_updating, '{}');
@ -389,7 +414,7 @@ export default class Component extends Node {
block.builders.destroy.addLine(`if (${name}) ${name}.destroy(false);`); block.builders.destroy.addLine(`if (${name}) ${name}.destroy(false);`);
} else { } else {
const expression = this.name === 'svelte:self' const expression = this.name === 'svelte:self'
? generator.name ? compiler.name
: `%components-${this.name}`; : `%components-${this.name}`;
block.builders.init.addBlock(deindent` block.builders.init.addBlock(deindent`
@ -478,18 +503,18 @@ function mungeBinding(binding: Node, block: Block): Binding {
}; };
} }
function mungeEventHandler(generator: DomGenerator, node: Node, handler: Node, block: Block, allContexts: Set<string>) { function mungeEventHandler(compiler: DomGenerator, node: Node, handler: Node, block: Block, allContexts: Set<string>) {
let body; let body;
if (handler.expression) { if (handler.expression) {
generator.addSourcemapLocations(handler.expression); compiler.addSourcemapLocations(handler.expression);
// TODO try out repetition between this and element counterpart // TODO try out repetition between this and element counterpart
const flattened = flattenReference(handler.expression.callee); const flattened = flattenReference(handler.expression.callee);
if (!validCalleeObjects.has(flattened.name)) { if (!validCalleeObjects.has(flattened.name)) {
// allow event.stopPropagation(), this.select() etc // allow event.stopPropagation(), this.select() etc
// TODO verify that it's a valid callee (i.e. built-in or declared method) // TODO verify that it's a valid callee (i.e. built-in or declared method)
generator.code.prependRight( compiler.code.prependRight(
handler.expression.start, handler.expression.start,
`${block.alias('component')}.` `${block.alias('component')}.`
); );

@ -3,12 +3,14 @@ import Node from './shared/Node';
import ElseBlock from './ElseBlock'; import ElseBlock from './ElseBlock';
import Block from '../dom/Block'; import Block from '../dom/Block';
import createDebuggingComment from '../../utils/createDebuggingComment'; import createDebuggingComment from '../../utils/createDebuggingComment';
import Expression from './shared/Expression';
import mapChildren from './shared/mapChildren';
export default class EachBlock extends Node { export default class EachBlock extends Node {
type: 'EachBlock'; type: 'EachBlock';
block: Block; block: Block;
expression: Node; expression: Expression;
iterations: string; iterations: string;
index: string; index: string;
@ -19,6 +21,16 @@ export default class EachBlock extends Node {
children: Node[]; children: Node[];
else?: ElseBlock; else?: ElseBlock;
constructor(compiler, parent, info) {
super(compiler, parent, info);
this.expression = new Expression(compiler, this, info.expression);
this.context = info.context;
this.key = info.key;
this.children = mapChildren(compiler, this, info.children);
}
init( init(
block: Block, block: Block,
stripWhitespace: boolean, stripWhitespace: boolean,
@ -30,12 +42,12 @@ export default class EachBlock extends Node {
this.iterations = block.getUniqueName(`${this.var}_blocks`); this.iterations = block.getUniqueName(`${this.var}_blocks`);
this.each_context = block.getUniqueName(`${this.var}_context`); this.each_context = block.getUniqueName(`${this.var}_context`);
const { dependencies } = this.metadata; const { dependencies } = this.expression;
block.addDependencies(dependencies); block.addDependencies(dependencies);
this.block = block.child({ this.block = block.child({
comment: createDebuggingComment(this, this.generator), comment: createDebuggingComment(this, this.compiler),
name: this.generator.getUniqueName('create_each_block'), name: this.compiler.getUniqueName('create_each_block'),
context: this.context, context: this.context,
key: this.key, key: this.key,
@ -48,8 +60,8 @@ export default class EachBlock extends Node {
listNames: new Map(block.listNames) listNames: new Map(block.listNames)
}); });
const listName = this.generator.getUniqueName('each_value'); const listName = this.compiler.getUniqueName('each_value');
const indexName = this.index || this.generator.getUniqueName(`${this.context}_index`); const indexName = this.index || this.compiler.getUniqueName(`${this.context}_index`);
this.block.contextTypes.set(this.context, 'each'); this.block.contextTypes.set(this.context, 'each');
this.block.indexNames.set(this.context, indexName); this.block.indexNames.set(this.context, indexName);
@ -83,18 +95,18 @@ export default class EachBlock extends Node {
} }
} }
this.generator.blocks.push(this.block); this.compiler.blocks.push(this.block);
this.initChildren(this.block, stripWhitespace, nextSibling); this.initChildren(this.block, stripWhitespace, nextSibling);
block.addDependencies(this.block.dependencies); block.addDependencies(this.block.dependencies);
this.block.hasUpdateMethod = this.block.dependencies.size > 0; this.block.hasUpdateMethod = this.block.dependencies.size > 0;
if (this.else) { if (this.else) {
this.else.block = block.child({ this.else.block = block.child({
comment: createDebuggingComment(this.else, this.generator), comment: createDebuggingComment(this.else, this.compiler),
name: this.generator.getUniqueName(`${this.block.name}_else`), name: this.compiler.getUniqueName(`${this.block.name}_else`),
}); });
this.generator.blocks.push(this.else.block); this.compiler.blocks.push(this.else.block);
this.else.initChildren( this.else.initChildren(
this.else.block, this.else.block,
stripWhitespace, stripWhitespace,
@ -111,7 +123,7 @@ export default class EachBlock extends Node {
) { ) {
if (this.children.length === 0) return; if (this.children.length === 0) return;
const { generator } = this; const { compiler } = this;
const each = this.var; const each = this.var;
@ -127,8 +139,8 @@ export default class EachBlock extends Node {
// hack the sourcemap, so that if data is missing the bug // hack the sourcemap, so that if data is missing the bug
// is easy to find // is easy to find
let c = this.start + 2; let c = this.start + 2;
while (generator.source[c] !== 'e') c += 1; while (compiler.source[c] !== 'e') c += 1;
generator.code.overwrite(c, c + 4, 'length'); compiler.code.overwrite(c, c + 4, 'length');
const length = `[✂${c}-${c+4}✂]`; const length = `[✂${c}-${c+4}✂]`;
const mountOrIntro = this.block.hasIntroMethod ? 'i' : 'm'; const mountOrIntro = this.block.hasIntroMethod ? 'i' : 'm';
@ -142,8 +154,7 @@ export default class EachBlock extends Node {
mountOrIntro, mountOrIntro,
}; };
block.contextualise(this.expression); const { snippet } = this.expression;
const { snippet } = this.metadata;
block.builders.init.addLine(`var ${each_block_value} = ${snippet};`); block.builders.init.addLine(`var ${each_block_value} = ${snippet};`);
@ -163,14 +174,14 @@ export default class EachBlock extends Node {
} }
if (this.else) { if (this.else) {
const each_block_else = generator.getUniqueName(`${each}_else`); const each_block_else = compiler.getUniqueName(`${each}_else`);
block.builders.init.addLine(`var ${each_block_else} = null;`); block.builders.init.addLine(`var ${each_block_else} = null;`);
// TODO neaten this up... will end up with an empty line in the block // TODO neaten this up... will end up with an empty line in the block
block.builders.init.addBlock(deindent` block.builders.init.addBlock(deindent`
if (!${each_block_value}.${length}) { if (!${each_block_value}.${length}) {
${each_block_else} = ${this.else.block.name}(#component, state); ${each_block_else} = ${this.else.block.name}(#component, ctx);
${each_block_else}.c(); ${each_block_else}.c();
} }
`); `);
@ -186,9 +197,9 @@ export default class EachBlock extends Node {
if (this.else.block.hasUpdateMethod) { if (this.else.block.hasUpdateMethod) {
block.builders.update.addBlock(deindent` block.builders.update.addBlock(deindent`
if (!${each_block_value}.${length} && ${each_block_else}) { if (!${each_block_value}.${length} && ${each_block_else}) {
${each_block_else}.p(changed, state); ${each_block_else}.p(changed, ctx);
} else if (!${each_block_value}.${length}) { } else if (!${each_block_value}.${length}) {
${each_block_else} = ${this.else.block.name}(#component, state); ${each_block_else} = ${this.else.block.name}(#component, ctx);
${each_block_else}.c(); ${each_block_else}.c();
${each_block_else}.${mountOrIntro}(${initialMountNode}, ${anchor}); ${each_block_else}.${mountOrIntro}(${initialMountNode}, ${anchor});
} else if (${each_block_else}) { } else if (${each_block_else}) {
@ -206,7 +217,7 @@ export default class EachBlock extends Node {
${each_block_else} = null; ${each_block_else} = null;
} }
} else if (!${each_block_else}) { } else if (!${each_block_else}) {
${each_block_else} = ${this.else.block.name}(#component, state); ${each_block_else} = ${this.else.block.name}(#component, ctx);
${each_block_else}.c(); ${each_block_else}.c();
${each_block_else}.${mountOrIntro}(${initialMountNode}, ${anchor}); ${each_block_else}.${mountOrIntro}(${initialMountNode}, ${anchor});
} }
@ -269,7 +280,7 @@ export default class EachBlock extends Node {
block.builders.init.addBlock(deindent` block.builders.init.addBlock(deindent`
for (var #i = 0; #i < ${each_block_value}.${length}; #i += 1) { for (var #i = 0; #i < ${each_block_value}.${length}; #i += 1) {
var ${key} = ${each_block_value}[#i].${this.key}; var ${key} = ${each_block_value}[#i].${this.key};
${blocks}[#i] = ${lookup}[${key}] = ${create_each_block}(#component, ${key}, @assign(@assign({}, state), { ${blocks}[#i] = ${lookup}[${key}] = ${create_each_block}(#component, ${key}, @assign(@assign({}, ctx), {
${this.contextProps.join(',\n')} ${this.contextProps.join(',\n')}
})); }));
} }
@ -299,7 +310,7 @@ export default class EachBlock extends Node {
var ${each_block_value} = ${snippet}; var ${each_block_value} = ${snippet};
${blocks} = @updateKeyedEach(${blocks}, #component, changed, "${this.key}", ${dynamic ? '1' : '0'}, ${each_block_value}, ${lookup}, ${updateMountNode}, ${String(this.block.hasOutroMethod)}, ${create_each_block}, "${mountOrIntro}", ${anchor}, function(#i) { ${blocks} = @updateKeyedEach(${blocks}, #component, changed, "${this.key}", ${dynamic ? '1' : '0'}, ${each_block_value}, ${lookup}, ${updateMountNode}, ${String(this.block.hasOutroMethod)}, ${create_each_block}, "${mountOrIntro}", ${anchor}, function(#i) {
return @assign(@assign({}, state), { return @assign(@assign({}, ctx), {
${this.contextProps.join(',\n')} ${this.contextProps.join(',\n')}
}); });
}); });
@ -334,7 +345,7 @@ export default class EachBlock extends Node {
var ${iterations} = []; var ${iterations} = [];
for (var #i = 0; #i < ${each_block_value}.${length}; #i += 1) { for (var #i = 0; #i < ${each_block_value}.${length}; #i += 1) {
${iterations}[#i] = ${create_each_block}(#component, @assign(@assign({}, state), { ${iterations}[#i] = ${create_each_block}(#component, @assign(@assign({}, ctx), {
${this.contextProps.join(',\n')} ${this.contextProps.join(',\n')}
})); }));
} }
@ -365,7 +376,7 @@ export default class EachBlock extends Node {
`); `);
const allDependencies = new Set(this.block.dependencies); const allDependencies = new Set(this.block.dependencies);
const { dependencies } = this.metadata; const { dependencies } = this.expression;
dependencies.forEach((dependency: string) => { dependencies.forEach((dependency: string) => {
allDependencies.add(dependency); allDependencies.add(dependency);
}); });
@ -432,7 +443,7 @@ export default class EachBlock extends Node {
if (${condition}) { if (${condition}) {
for (var #i = ${start}; #i < ${each_block_value}.${length}; #i += 1) { for (var #i = ${start}; #i < ${each_block_value}.${length}; #i += 1) {
var ${this.each_context} = @assign(@assign({}, state), { var ${this.each_context} = @assign(@assign({}, ctx), {
${this.contextProps.join(',\n')} ${this.contextProps.join(',\n')}
}); });

@ -22,30 +22,71 @@ import mapChildren from './shared/mapChildren';
export default class Element extends Node { export default class Element extends Node {
type: 'Element'; type: 'Element';
name: string; name: string;
attributes: (Attribute | Binding | EventHandler | Ref | Transition | Action)[]; // TODO split these up sooner attributes: Attribute[];
bindings: Binding[];
handlers: EventHandler[];
intro: Transition;
outro: Transition;
children: Node[]; children: Node[];
ref: string;
namespace: string;
constructor(compiler, parent, info: any) { constructor(compiler, parent, info: any) {
super(compiler, parent, info); super(compiler, parent, info);
this.name = info.name; this.name = info.name;
this.children = mapChildren(compiler, parent, info.children);
const parentElement = parent.findNearest(/^Element/);
this.namespace = this.name === 'svg' ?
namespaces.svg :
parentElement ? parentElement.namespace : this.compiler.namespace;
this.attributes = []; this.attributes = [];
// TODO bindings etc this.bindings = [];
this.handlers = [];
this.intro = null;
this.outro = null;
info.attributes.forEach(node => { info.attributes.forEach(node => {
switch (node.type) { switch (node.type) {
case 'Attribute': case 'Attribute':
case 'Spread': // special case
if (node.name === 'xmlns') this.namespace = node.value[0].data;
this.attributes.push(new Attribute(compiler, this, node)); this.attributes.push(new Attribute(compiler, this, node));
break; break;
case 'Binding':
this.bindings.push(new Binding(compiler, this, node));
break;
case 'EventHandler':
this.handlers.push(new EventHandler(compiler, this, node));
break;
case 'Transition':
const transition = new Transition(compiler, this, node);
if (node.intro) this.intro = transition;
if (node.outro) this.outro = transition;
break;
case 'Ref':
// TODO catch this in validation
if (this.ref) throw new Error(`Duplicate refs`);
compiler.usesRefs = true
this.ref = node.name;
break;
default: default:
throw new Error(`Not implemented: ${node.type}`); throw new Error(`Not implemented: ${node.type}`);
} }
}); });
// TODO break out attributes and directives here // TODO break out attributes and directives here
this.children = mapChildren(compiler, this, info.children);
} }
init( init(
@ -57,61 +98,53 @@ export default class Element extends Node {
this.cannotUseInnerHTML(); this.cannotUseInnerHTML();
} }
const parentElement = this.parent && this.parent.findNearest(/^Element/); this.attributes.forEach(attr => {
this.namespace = this.name === 'svg' ? if (attr.dependencies.size) {
namespaces.svg : this.parent.cannotUseInnerHTML();
parentElement ? parentElement.namespace : this.generator.namespace; block.addDependencies(attr.dependencies);
this.attributes.forEach(attribute => {
if (attribute.type === 'Attribute' && attribute.value !== true) {
// special case — xmlns
if (attribute.name === 'xmlns') {
// TODO this attribute must be static enforce at compile time
this.namespace = attribute.value[0].data;
}
attribute.value.forEach((chunk: Node) => { // special case — <option value={foo}> — see below
if (chunk.type !== 'Text') { if (this.name === 'option' && attr.name === 'value') {
if (this.parent) this.parent.cannotUseInnerHTML();
const dependencies = chunk.metadata.dependencies;
block.addDependencies(dependencies);
// special case — <option value='{{foo}}'> — see below
if (
this.name === 'option' &&
attribute.name === 'value'
) {
let select = this.parent; let select = this.parent;
while (select && (select.type !== 'Element' || select.name !== 'select')) select = select.parent; while (select && (select.type !== 'Element' || select.name !== 'select')) select = select.parent;
if (select && select.selectBindingDependencies) { if (select && select.selectBindingDependencies) {
select.selectBindingDependencies.forEach(prop => { select.selectBindingDependencies.forEach(prop => {
dependencies.forEach((dependency: string) => { dependencies.forEach((dependency: string) => {
this.generator.indirectDependencies.get(prop).add(dependency); this.compiler.indirectDependencies.get(prop).add(dependency);
}); });
}); });
} }
} }
} }
}); });
} else {
if (this.parent) this.parent.cannotUseInnerHTML();
if (attribute.type === 'EventHandler' && attribute.expression) { this.bindings.forEach(binding => {
attribute.expression.arguments.forEach((arg: Node) => { this.cannotUseInnerHTML();
block.addDependencies(arg.metadata.dependencies); block.addDependencies(binding.value.dependencies);
}); });
} else if (attribute.type === 'Binding') {
block.addDependencies(attribute.metadata.dependencies); this.handlers.forEach(handler => {
} else if (attribute.type === 'Transition') { this.cannotUseInnerHTML();
if (attribute.intro) block.addDependencies(handler.dependencies);
this.generator.hasIntroTransitions = block.hasIntroMethod = true; });
if (attribute.outro) {
this.generator.hasOutroTransitions = block.hasOutroMethod = true; if (this.intro) {
this.compiler.hasIntroTransitions = block.hasIntroMethod = true;
}
if (this.outro) {
this.compiler.hasOutroTransitions = block.hasOutroMethod = true;
block.outros += 1; block.outros += 1;
} }
} else if (attribute.type === 'Action' && attribute.expression) {
this.attributes.forEach(attribute => {
if (attribute.type === 'Attribute' && attribute.value !== true) {
// removed
} else {
if (this.parent) this.parent.cannotUseInnerHTML();
if (attribute.type === 'Action' && attribute.expression) {
block.addDependencies(attribute.metadata.dependencies); block.addDependencies(attribute.metadata.dependencies);
} else if (attribute.type === 'Spread') { } else if (attribute.type === 'Spread') {
block.addDependencies(attribute.metadata.dependencies); block.addDependencies(attribute.metadata.dependencies);
@ -125,11 +158,9 @@ export default class Element extends Node {
// this is an egregious hack, but it's the easiest way to get <textarea> // this is an egregious hack, but it's the easiest way to get <textarea>
// children treated the same way as a value attribute // children treated the same way as a value attribute
if (this.children.length > 0) { if (this.children.length > 0) {
this.attributes.push(new Attribute({ this.attributes.push(new Attribute(this.compiler, this, {
generator: this.generator,
name: 'value', name: 'value',
value: this.children, value: this.children
parent: this
})); }));
this.children = []; this.children = [];
@ -153,7 +184,7 @@ export default class Element extends Node {
const dependencies = binding.metadata.dependencies; const dependencies = binding.metadata.dependencies;
this.selectBindingDependencies = dependencies; this.selectBindingDependencies = dependencies;
dependencies.forEach((prop: string) => { dependencies.forEach((prop: string) => {
this.generator.indirectDependencies.set(prop, new Set()); this.compiler.indirectDependencies.set(prop, new Set());
}); });
} else { } else {
this.selectBindingDependencies = null; this.selectBindingDependencies = null;
@ -188,11 +219,11 @@ export default class Element extends Node {
parentNode: string, parentNode: string,
parentNodes: string parentNodes: string
) { ) {
const { generator } = this; const { compiler } = this;
if (this.name === 'slot') { if (this.name === 'slot') {
const slotName = this.getStaticAttributeValue('name') || 'default'; const slotName = this.getStaticAttributeValue('name') || 'default';
this.generator.slots.add(slotName); this.compiler.slots.add(slotName);
} }
if (this.name === 'noscript') return; if (this.name === 'noscript') return;
@ -211,15 +242,15 @@ export default class Element extends Node {
parentNode; parentNode;
block.addVariable(name); block.addVariable(name);
const renderStatement = getRenderStatement(this.generator, this.namespace, this.name); const renderStatement = getRenderStatement(this.compiler, this.namespace, this.name);
block.builders.create.addLine( block.builders.create.addLine(
`${name} = ${renderStatement};` `${name} = ${renderStatement};`
); );
if (this.generator.hydratable) { if (this.compiler.hydratable) {
if (parentNodes) { if (parentNodes) {
block.builders.claim.addBlock(deindent` block.builders.claim.addBlock(deindent`
${name} = ${getClaimStatement(generator, this.namespace, parentNodes, this)}; ${name} = ${getClaimStatement(compiler, this.namespace, parentNodes, this)};
var ${childState.parentNodes} = @children(${name}); var ${childState.parentNodes} = @children(${name});
`); `);
} else { } else {
@ -271,7 +302,7 @@ export default class Element extends Node {
this.addBindings(block, allUsedContexts); this.addBindings(block, allUsedContexts);
const eventHandlerUsesComponent = this.addEventHandlers(block, allUsedContexts); const eventHandlerUsesComponent = this.addEventHandlers(block, allUsedContexts);
this.addRefs(block); if (this.ref) this.addRef(block);
this.addAttributes(block); this.addAttributes(block);
this.addTransitions(block); this.addTransitions(block);
this.addActions(block); this.addActions(block);
@ -353,14 +384,14 @@ export default class Element extends Node {
block: Block, block: Block,
allUsedContexts: Set<string> allUsedContexts: Set<string>
) { ) {
const bindings: Binding[] = this.attributes.filter((a: Binding) => a.type === 'Binding'); if (this.bindings.length === 0) return;
if (bindings.length === 0) return;
if (this.name === 'select' || this.isMediaNode()) this.generator.hasComplexBindings = true; if (this.name === 'select' || this.isMediaNode()) this.compiler.hasComplexBindings = true;
const needsLock = this.name !== 'input' || !/radio|checkbox|range|color/.test(this.getStaticAttributeValue('type')); const needsLock = this.name !== 'input' || !/radio|checkbox|range|color/.test(this.getStaticAttributeValue('type'));
const mungedBindings = bindings.map(binding => binding.munge(block, allUsedContexts)); // TODO munge in constructor
const mungedBindings = this.bindings.map(binding => binding.munge(block, allUsedContexts));
const lock = mungedBindings.some(binding => binding.needsLock) ? const lock = mungedBindings.some(binding => binding.needsLock) ?
block.getUniqueName(`${this.var}_updating`) : block.getUniqueName(`${this.var}_updating`) :
@ -452,7 +483,7 @@ export default class Element extends Node {
.join(' && '); .join(' && ');
if (this.name === 'select' || group.bindings.find(binding => binding.name === 'indeterminate' || binding.isReadOnlyMediaAttribute)) { if (this.name === 'select' || group.bindings.find(binding => binding.name === 'indeterminate' || binding.isReadOnlyMediaAttribute)) {
this.generator.hasComplexBindings = true; this.compiler.hasComplexBindings = true;
block.builders.hydrate.addLine( block.builders.hydrate.addLine(
`if (!(${allInitialStateIsDefined})) #component.root._beforecreate.push(${handler});` `if (!(${allInitialStateIsDefined})) #component.root._beforecreate.push(${handler});`
@ -469,7 +500,7 @@ export default class Element extends Node {
return; return;
} }
this.attributes.filter((a: Attribute) => a.type === 'Attribute').forEach((attribute: Attribute) => { this.attributes.forEach((attribute: Attribute) => {
attribute.render(block); attribute.render(block);
}); });
} }
@ -528,89 +559,58 @@ export default class Element extends Node {
} }
addEventHandlers(block: Block, allUsedContexts) { addEventHandlers(block: Block, allUsedContexts) {
const { generator } = this; const { compiler } = this;
let eventHandlerUsesComponent = false; let eventHandlerUsesComponent = false;
this.attributes.filter((a: EventHandler) => a.type === 'EventHandler').forEach((attribute: EventHandler) => { this.handlers.forEach(handler => {
const isCustomEvent = generator.events.has(attribute.name); const isCustomEvent = compiler.events.has(handler.name);
const shouldHoist = !isCustomEvent && this.hasAncestor('EachBlock'); const shouldHoist = !isCustomEvent && this.hasAncestor('EachBlock');
const context = shouldHoist ? null : this.var; const context = shouldHoist ? null : this.var;
const usedContexts: string[] = []; const usedContexts: string[] = [];
if (attribute.expression) { if (handler.callee) {
generator.addSourcemapLocations(attribute.expression); handler.render(this.compiler, block);
const flattened = flattenReference(attribute.expression.callee);
if (!validCalleeObjects.has(flattened.name)) {
// allow event.stopPropagation(), this.select() etc
// TODO verify that it's a valid callee (i.e. built-in or declared method)
if (flattened.name[0] === '$' && !generator.methods.has(flattened.name)) {
generator.code.overwrite(
attribute.expression.start,
attribute.expression.start + 1,
`${block.alias('component')}.store.`
);
} else {
generator.code.prependRight(
attribute.expression.start,
`${block.alias('component')}.`
);
}
if (!validCalleeObjects.has(handler.callee.name)) {
if (shouldHoist) eventHandlerUsesComponent = true; // this feels a bit hacky but it works! if (shouldHoist) eventHandlerUsesComponent = true; // this feels a bit hacky but it works!
} }
attribute.expression.arguments.forEach((arg: Node) => { // handler.expression.arguments.forEach((arg: Node) => {
const { contexts } = block.contextualise(arg, context, true); // const { contexts } = block.contextualise(arg, context, true);
contexts.forEach(context => { // contexts.forEach(context => {
if (!~usedContexts.indexOf(context)) usedContexts.push(context); // if (!~usedContexts.indexOf(context)) usedContexts.push(context);
allUsedContexts.add(context); // allUsedContexts.add(context);
}); // });
}); // });
} }
const ctx = context || 'this'; const ctx = context || 'this';
const declarations = usedContexts
.map(name => {
if (name === 'state') {
if (shouldHoist) eventHandlerUsesComponent = true;
return `var state = ${block.alias('component')}.get();`;
}
const contextType = block.contextTypes.get(name);
if (contextType === 'each') {
const listName = block.listNames.get(name);
const indexName = block.indexNames.get(name);
const contextName = block.contexts.get(name);
return `var ${listName} = ${ctx}._svelte.${listName}, ${indexName} = ${ctx}._svelte.${indexName}, ${contextName} = ${listName}[${indexName}];`;
}
})
.filter(Boolean);
// get a name for the event handler that is globally unique // get a name for the event handler that is globally unique
// if hoisted, locally unique otherwise // if hoisted, locally unique otherwise
const handlerName = (shouldHoist ? generator : block).getUniqueName( const handlerName = (shouldHoist ? compiler : block).getUniqueName(
`${attribute.name.replace(/[^a-zA-Z0-9_$]/g, '_')}_handler` `${handler.name.replace(/[^a-zA-Z0-9_$]/g, '_')}_handler`
); );
const component = block.alias('component'); // can't use #component, might be hoisted
// create the handler body // create the handler body
const handlerBody = deindent` const handlerBody = deindent`
${eventHandlerUsesComponent && ${eventHandlerUsesComponent &&
`var ${block.alias('component')} = ${ctx}._svelte.component;`} `var #component = ${ctx}._svelte.component;`}
${declarations} ${handler.dependencies.size > 0 && `const ctx = #component.get();`}
${attribute.expression ? ${handler.snippet ?
`[✂${attribute.expression.start}-${attribute.expression.end}✂];` : handler.snippet :
`${block.alias('component')}.fire("${attribute.name}", event);`} `#component.fire("${handler.name}", event);`}
`; `;
if (isCustomEvent) { if (isCustomEvent) {
block.addVariable(handlerName); block.addVariable(handlerName);
block.builders.hydrate.addBlock(deindent` block.builders.hydrate.addBlock(deindent`
${handlerName} = %events-${attribute.name}.call(#component, ${this.var}, function(event) { ${handlerName} = %events-${handler.name}.call(#component, ${this.var}, function(event) {
${handlerBody} ${handlerBody}
}); });
`); `);
@ -619,34 +619,32 @@ export default class Element extends Node {
${handlerName}.destroy(); ${handlerName}.destroy();
`); `);
} else { } else {
const handler = deindent` const handlerFunction = deindent`
function ${handlerName}(event) { function ${handlerName}(event) {
${handlerBody} ${handlerBody}
} }
`; `;
if (shouldHoist) { if (shouldHoist) {
generator.blocks.push(handler); compiler.blocks.push(handlerFunction);
} else { } else {
block.builders.init.addBlock(handler); block.builders.init.addBlock(handlerFunction);
} }
block.builders.hydrate.addLine( block.builders.hydrate.addLine(
`@addListener(${this.var}, "${attribute.name}", ${handlerName});` `@addListener(${this.var}, "${handler.name}", ${handlerName});`
); );
block.builders.destroy.addLine( block.builders.destroy.addLine(
`@removeListener(${this.var}, "${attribute.name}", ${handlerName});` `@removeListener(${this.var}, "${handler.name}", ${handlerName});`
); );
} }
}); });
return eventHandlerUsesComponent; return eventHandlerUsesComponent;
} }
addRefs(block: Block) { addRef(block: Block) {
// TODO it should surely be an error to have more than one ref const ref = `#component.refs.${this.ref}`;
this.attributes.filter((a: Ref) => a.type === 'Ref').forEach((attribute: Ref) => {
const ref = `#component.refs.${attribute.name}`;
block.builders.mount.addLine( block.builders.mount.addLine(
`${ref} = ${this.var};` `${ref} = ${this.var};`
@ -655,25 +653,19 @@ export default class Element extends Node {
block.builders.destroy.addLine( block.builders.destroy.addLine(
`if (${ref} === ${this.var}) ${ref} = null;` `if (${ref} === ${this.var}) ${ref} = null;`
); );
this.generator.usesRefs = true; // so component.refs object is created
});
} }
addTransitions( addTransitions(
block: Block block: Block
) { ) {
const intro = this.attributes.find((a: Transition) => a.type === 'Transition' && a.intro); const { intro, outro } = this;
const outro = this.attributes.find((a: Transition) => a.type === 'Transition' && a.outro);
if (!intro && !outro) return; if (!intro && !outro) return;
if (intro === outro) { if (intro === outro) {
block.contextualise(intro.expression); // TODO remove all these
const name = block.getUniqueName(`${this.var}_transition`); const name = block.getUniqueName(`${this.var}_transition`);
const snippet = intro.expression const snippet = intro.expression
? intro.metadata.snippet ? intro.expression.snippet
: '{}'; : '{}';
block.addVariable(name); block.addVariable(name);
@ -701,11 +693,9 @@ export default class Element extends Node {
const outroName = outro && block.getUniqueName(`${this.var}_outro`); const outroName = outro && block.getUniqueName(`${this.var}_outro`);
if (intro) { if (intro) {
block.contextualise(intro.expression);
block.addVariable(introName); block.addVariable(introName);
const snippet = intro.expression const snippet = intro.expression
? intro.metadata.snippet ? intro.expression.snippet
: '{}'; : '{}';
const fn = `%transitions-${intro.name}`; // TODO add built-in transitions? const fn = `%transitions-${intro.name}`; // TODO add built-in transitions?
@ -728,11 +718,9 @@ export default class Element extends Node {
} }
if (outro) { if (outro) {
block.contextualise(outro.expression);
block.addVariable(outroName); block.addVariable(outroName);
const snippet = outro.expression const snippet = outro.expression
? outro.metadata.snippet ? outro.expression.snippet
: '{}'; : '{}';
const fn = `%transitions-${outro.name}`; const fn = `%transitions-${outro.name}`;
@ -755,7 +743,7 @@ export default class Element extends Node {
const { expression } = attribute; const { expression } = attribute;
let snippet, dependencies; let snippet, dependencies;
if (expression) { if (expression) {
this.generator.addSourcemapLocations(expression); this.compiler.addSourcemapLocations(expression);
block.contextualise(expression); block.contextualise(expression);
snippet = attribute.metadata.snippet; snippet = attribute.metadata.snippet;
dependencies = attribute.metadata.dependencies; dependencies = attribute.metadata.dependencies;
@ -823,18 +811,18 @@ export default class Element extends Node {
const classAttribute = this.attributes.find(a => a.name === 'class'); const classAttribute = this.attributes.find(a => a.name === 'class');
if (classAttribute && classAttribute.value !== true) { if (classAttribute && classAttribute.value !== true) {
if (classAttribute.value.length === 1 && classAttribute.value[0].type === 'Text') { if (classAttribute.value.length === 1 && classAttribute.value[0].type === 'Text') {
classAttribute.value[0].data += ` ${this.generator.stylesheet.id}`; classAttribute.value[0].data += ` ${this.compiler.stylesheet.id}`;
} else { } else {
(<Node[]>classAttribute.value).push( (<Node[]>classAttribute.value).push(
new Node({ type: 'Text', data: ` ${this.generator.stylesheet.id}` }) new Node({ type: 'Text', data: ` ${this.compiler.stylesheet.id}` })
); );
} }
} else { } else {
this.attributes.push( this.attributes.push(
new Attribute({ new Attribute({
generator: this.generator, compiler: this.compiler,
name: 'class', name: 'class',
value: [new Node({ type: 'Text', data: `${this.generator.stylesheet.id}` })], value: [new Node({ type: 'Text', data: `${this.compiler.stylesheet.id}` })],
parent: this, parent: this,
}) })
); );
@ -843,7 +831,7 @@ export default class Element extends Node {
} }
function getRenderStatement( function getRenderStatement(
generator: DomGenerator, compiler: DomGenerator,
namespace: string, namespace: string,
name: string name: string
) { ) {
@ -859,7 +847,7 @@ function getRenderStatement(
} }
function getClaimStatement( function getClaimStatement(
generator: DomGenerator, compiler: DomGenerator,
namespace: string, namespace: string,
nodes: string, nodes: string,
node: Node node: Node

@ -1,8 +1,13 @@
import Node from './shared/Node'; import Node from './shared/Node';
import Block from '../dom/Block'; import Block from '../dom/Block';
import mapChildren from './shared/mapChildren';
export default class ElseBlock extends Node { export default class ElseBlock extends Node {
type: 'ElseBlock'; type: 'ElseBlock';
children: Node[]; children: Node[];
block: Block;
constructor(compiler, parent, info) {
super(compiler, parent, info);
this.children = mapChildren(compiler, this, info.children);
}
} }

@ -1,7 +1,61 @@
import Node from './shared/Node'; import Node from './shared/Node';
import Expression from './shared/Expression';
import addToSet from '../../utils/addToSet';
import flattenReference from '../../utils/flattenReference';
import validCalleeObjects from '../../utils/validCalleeObjects';
export default class EventHandler extends Node { export default class EventHandler extends Node {
name: string; name: string;
value: Node[] dependencies: Set<string>;
expression: Node expression: Node;
callee: any; // TODO
insertionPoint: number;
args: Expression[];
snippet: string;
constructor(compiler, parent, info) {
super(compiler, parent, info);
this.name = info.name;
this.dependencies = new Set();
if (info.expression) {
this.callee = flattenReference(info.expression.callee);
this.insertionPoint = info.expression.start;
this.args = info.expression.arguments.map(param => {
const expression = new Expression(compiler, this, param);
addToSet(this.dependencies, expression.dependencies);
return expression;
});
this.snippet = `[✂${info.expression.start}-${info.expression.end}✂]`;
} else {
this.callee = null;
this.insertionPoint = null;
this.args = null;
this.snippet = null; // TODO handle shorthand events here?
}
}
render(compiler, block) {
if (this.insertionPoint === null) return; // TODO handle shorthand events here?
if (!validCalleeObjects.has(this.callee.name)) {
// allow event.stopPropagation(), this.select() etc
// TODO verify that it's a valid callee (i.e. built-in or declared method)
if (this.callee.name[0] === '$' && !compiler.methods.has(this.callee.name)) {
compiler.code.overwrite(
this.insertionPoint,
this.insertionPoint + 1,
`${block.alias('component')}.store.`
);
} else {
compiler.code.prependRight(
this.insertionPoint,
`${block.alias('component')}.`
);
}
}
}
} }

@ -9,13 +9,13 @@ export default class Fragment extends Node {
children: Node[]; children: Node[];
constructor(compiler: Generator, info: any) { constructor(compiler: Generator, info: any) {
super(compiler, info); super(compiler, null, info);
this.children = mapChildren(compiler, this, info.children); this.children = mapChildren(compiler, this, info.children);
} }
init() { init() {
this.block = new Block({ this.block = new Block({
generator: this.generator, generator: this.compiler,
name: '@create_main_fragment', name: '@create_main_fragment',
key: null, key: null,
@ -29,7 +29,7 @@ export default class Fragment extends Node {
dependencies: new Set(), dependencies: new Set(),
}); });
this.generator.blocks.push(this.block); this.compiler.blocks.push(this.block);
this.initChildren(this.block, true, null); this.initChildren(this.block, true, null);
this.block.hasUpdateMethod = true; this.block.hasUpdateMethod = true;

@ -4,6 +4,8 @@ import ElseBlock from './ElseBlock';
import { DomGenerator } from '../dom/index'; import { DomGenerator } from '../dom/index';
import Block from '../dom/Block'; import Block from '../dom/Block';
import createDebuggingComment from '../../utils/createDebuggingComment'; import createDebuggingComment from '../../utils/createDebuggingComment';
import Expression from './shared/Expression';
import mapChildren from './shared/mapChildren';
function isElseIf(node: ElseBlock) { function isElseIf(node: ElseBlock) {
return ( return (
@ -17,16 +19,29 @@ function isElseBranch(branch) {
export default class IfBlock extends Node { export default class IfBlock extends Node {
type: 'IfBlock'; type: 'IfBlock';
expression: Expression;
children: any[];
else: ElseBlock; else: ElseBlock;
block: Block; block: Block;
constructor(compiler, parent, info) {
super(compiler, parent, info);
this.expression = new Expression(compiler, this, info.expression);
this.children = mapChildren(compiler, this, info.children);
this.else = info.else
? new ElseBlock(compiler, this, info.else)
: null;
}
init( init(
block: Block, block: Block,
stripWhitespace: boolean, stripWhitespace: boolean,
nextSibling: Node nextSibling: Node
) { ) {
const { generator } = this; const { compiler } = this;
this.cannotUseInnerHTML(); this.cannotUseInnerHTML();
@ -38,11 +53,11 @@ export default class IfBlock extends Node {
function attachBlocks(node: IfBlock) { function attachBlocks(node: IfBlock) {
node.var = block.getUniqueName(`if_block`); node.var = block.getUniqueName(`if_block`);
block.addDependencies(node.metadata.dependencies); block.addDependencies(node.expression.dependencies);
node.block = block.child({ node.block = block.child({
comment: createDebuggingComment(node, generator), comment: createDebuggingComment(node, compiler),
name: generator.getUniqueName(`create_if_block`), name: compiler.getUniqueName(`create_if_block`),
}); });
blocks.push(node.block); blocks.push(node.block);
@ -60,8 +75,8 @@ export default class IfBlock extends Node {
attachBlocks(node.else.children[0]); attachBlocks(node.else.children[0]);
} else if (node.else) { } else if (node.else) {
node.else.block = block.child({ node.else.block = block.child({
comment: createDebuggingComment(node.else, generator), comment: createDebuggingComment(node.else, compiler),
name: generator.getUniqueName(`create_if_block`), name: compiler.getUniqueName(`create_if_block`),
}); });
blocks.push(node.else.block); blocks.push(node.else.block);
@ -86,7 +101,7 @@ export default class IfBlock extends Node {
block.hasOutroMethod = hasOutros; block.hasOutroMethod = hasOutros;
}); });
generator.blocks.push(...blocks); compiler.blocks.push(...blocks);
} }
build( build(
@ -147,12 +162,12 @@ export default class IfBlock extends Node {
dynamic, dynamic,
{ name, anchor, hasElse, if_name } { name, anchor, hasElse, if_name }
) { ) {
const select_block_type = this.generator.getUniqueName(`select_block_type`); const select_block_type = this.compiler.getUniqueName(`select_block_type`);
const current_block_type = block.getUniqueName(`current_block_type`); const current_block_type = block.getUniqueName(`current_block_type`);
const current_block_type_and = hasElse ? '' : `${current_block_type} && `; const current_block_type_and = hasElse ? '' : `${current_block_type} && `;
block.builders.init.addBlock(deindent` block.builders.init.addBlock(deindent`
function ${select_block_type}(state) { function ${select_block_type}(ctx) {
${branches ${branches
.map(({ condition, block }) => `${condition ? `if (${condition}) ` : ''}return ${block};`) .map(({ condition, block }) => `${condition ? `if (${condition}) ` : ''}return ${block};`)
.join('\n')} .join('\n')}
@ -160,8 +175,8 @@ export default class IfBlock extends Node {
`); `);
block.builders.init.addBlock(deindent` block.builders.init.addBlock(deindent`
var ${current_block_type} = ${select_block_type}(state); var ${current_block_type} = ${select_block_type}(ctx);
var ${name} = ${current_block_type_and}${current_block_type}(#component, state); var ${name} = ${current_block_type_and}${current_block_type}(#component, ctx);
`); `);
const mountOrIntro = branches[0].hasIntroMethod ? 'i' : 'm'; const mountOrIntro = branches[0].hasIntroMethod ? 'i' : 'm';
@ -185,22 +200,22 @@ export default class IfBlock extends Node {
${name}.u(); ${name}.u();
${name}.d(); ${name}.d();
}`} }`}
${name} = ${current_block_type_and}${current_block_type}(#component, state); ${name} = ${current_block_type_and}${current_block_type}(#component, ctx);
${if_name}${name}.c(); ${if_name}${name}.c();
${if_name}${name}.${mountOrIntro}(${updateMountNode}, ${anchor}); ${if_name}${name}.${mountOrIntro}(${updateMountNode}, ${anchor});
`; `;
if (dynamic) { if (dynamic) {
block.builders.update.addBlock(deindent` block.builders.update.addBlock(deindent`
if (${current_block_type} === (${current_block_type} = ${select_block_type}(state)) && ${name}) { if (${current_block_type} === (${current_block_type} = ${select_block_type}(ctx)) && ${name}) {
${name}.p(changed, state); ${name}.p(changed, ctx);
} else { } else {
${changeBlock} ${changeBlock}
} }
`); `);
} else { } else {
block.builders.update.addBlock(deindent` block.builders.update.addBlock(deindent`
if (${current_block_type} !== (${current_block_type} = ${select_block_type}(state))) { if (${current_block_type} !== (${current_block_type} = ${select_block_type}(ctx))) {
${changeBlock} ${changeBlock}
} }
`); `);
@ -241,7 +256,7 @@ export default class IfBlock extends Node {
var ${if_blocks} = []; var ${if_blocks} = [];
function ${select_block_type}(state) { function ${select_block_type}(ctx) {
${branches ${branches
.map(({ condition, block }, i) => `${condition ? `if (${condition}) ` : ''}return ${block ? i : -1};`) .map(({ condition, block }, i) => `${condition ? `if (${condition}) ` : ''}return ${block ? i : -1};`)
.join('\n')} .join('\n')}
@ -250,13 +265,13 @@ export default class IfBlock extends Node {
if (hasElse) { if (hasElse) {
block.builders.init.addBlock(deindent` block.builders.init.addBlock(deindent`
${current_block_type_index} = ${select_block_type}(state); ${current_block_type_index} = ${select_block_type}(ctx);
${name} = ${if_blocks}[${current_block_type_index}] = ${if_block_creators}[${current_block_type_index}](#component, state); ${name} = ${if_blocks}[${current_block_type_index}] = ${if_block_creators}[${current_block_type_index}](#component, ctx);
`); `);
} else { } else {
block.builders.init.addBlock(deindent` block.builders.init.addBlock(deindent`
if (~(${current_block_type_index} = ${select_block_type}(state))) { if (~(${current_block_type_index} = ${select_block_type}(ctx))) {
${name} = ${if_blocks}[${current_block_type_index}] = ${if_block_creators}[${current_block_type_index}](#component, state); ${name} = ${if_blocks}[${current_block_type_index}] = ${if_block_creators}[${current_block_type_index}](#component, ctx);
} }
`); `);
} }
@ -282,7 +297,7 @@ export default class IfBlock extends Node {
const createNewBlock = deindent` const createNewBlock = deindent`
${name} = ${if_blocks}[${current_block_type_index}]; ${name} = ${if_blocks}[${current_block_type_index}];
if (!${name}) { if (!${name}) {
${name} = ${if_blocks}[${current_block_type_index}] = ${if_block_creators}[${current_block_type_index}](#component, state); ${name} = ${if_blocks}[${current_block_type_index}] = ${if_block_creators}[${current_block_type_index}](#component, ctx);
${name}.c(); ${name}.c();
} }
${name}.${mountOrIntro}(${updateMountNode}, ${anchor}); ${name}.${mountOrIntro}(${updateMountNode}, ${anchor});
@ -309,9 +324,9 @@ export default class IfBlock extends Node {
if (dynamic) { if (dynamic) {
block.builders.update.addBlock(deindent` block.builders.update.addBlock(deindent`
var ${previous_block_index} = ${current_block_type_index}; var ${previous_block_index} = ${current_block_type_index};
${current_block_type_index} = ${select_block_type}(state); ${current_block_type_index} = ${select_block_type}(ctx);
if (${current_block_type_index} === ${previous_block_index}) { if (${current_block_type_index} === ${previous_block_index}) {
${if_current_block_type_index}${if_blocks}[${current_block_type_index}].p(changed, state); ${if_current_block_type_index}${if_blocks}[${current_block_type_index}].p(changed, ctx);
} else { } else {
${changeBlock} ${changeBlock}
} }
@ -319,7 +334,7 @@ export default class IfBlock extends Node {
} else { } else {
block.builders.update.addBlock(deindent` block.builders.update.addBlock(deindent`
var ${previous_block_index} = ${current_block_type_index}; var ${previous_block_index} = ${current_block_type_index};
${current_block_type_index} = ${select_block_type}(state); ${current_block_type_index} = ${select_block_type}(ctx);
if (${current_block_type_index} !== ${previous_block_index}) { if (${current_block_type_index} !== ${previous_block_index}) {
${changeBlock} ${changeBlock}
} }
@ -343,7 +358,7 @@ export default class IfBlock extends Node {
{ name, anchor, if_name } { name, anchor, if_name }
) { ) {
block.builders.init.addBlock(deindent` block.builders.init.addBlock(deindent`
var ${name} = (${branch.condition}) && ${branch.block}(#component, state); var ${name} = (${branch.condition}) && ${branch.block}(#component, ctx);
`); `);
const mountOrIntro = branch.hasIntroMethod ? 'i' : 'm'; const mountOrIntro = branch.hasIntroMethod ? 'i' : 'm';
@ -360,9 +375,9 @@ export default class IfBlock extends Node {
? branch.hasIntroMethod ? branch.hasIntroMethod
? deindent` ? deindent`
if (${name}) { if (${name}) {
${name}.p(changed, state); ${name}.p(changed, ctx);
} else { } else {
${name} = ${branch.block}(#component, state); ${name} = ${branch.block}(#component, ctx);
if (${name}) ${name}.c(); if (${name}) ${name}.c();
} }
@ -370,9 +385,9 @@ export default class IfBlock extends Node {
` `
: deindent` : deindent`
if (${name}) { if (${name}) {
${name}.p(changed, state); ${name}.p(changed, ctx);
} else { } else {
${name} = ${branch.block}(#component, state); ${name} = ${branch.block}(#component, ctx);
${name}.c(); ${name}.c();
${name}.m(${updateMountNode}, ${anchor}); ${name}.m(${updateMountNode}, ${anchor});
} }
@ -380,14 +395,14 @@ export default class IfBlock extends Node {
: branch.hasIntroMethod : branch.hasIntroMethod
? deindent` ? deindent`
if (!${name}) { if (!${name}) {
${name} = ${branch.block}(#component, state); ${name} = ${branch.block}(#component, ctx);
${name}.c(); ${name}.c();
} }
${name}.i(${updateMountNode}, ${anchor}); ${name}.i(${updateMountNode}, ${anchor});
` `
: deindent` : deindent`
if (!${name}) { if (!${name}) {
${name} = ${branch.block}(#component, state); ${name} = ${branch.block}(#component, ctx);
${name}.c(); ${name}.c();
${name}.m(${updateMountNode}, ${anchor}); ${name}.m(${updateMountNode}, ${anchor});
} }
@ -426,13 +441,11 @@ export default class IfBlock extends Node {
block: Block, block: Block,
parentNode: string, parentNode: string,
parentNodes: string, parentNodes: string,
node: Node node: IfBlock
) { ) {
block.contextualise(node.expression); // TODO remove
const branches = [ const branches = [
{ {
condition: node.metadata.snippet, condition: node.expression.snippet,
block: node.block.name, block: node.block.name,
hasUpdateMethod: node.block.hasUpdateMethod, hasUpdateMethod: node.block.hasUpdateMethod,
hasIntroMethod: node.block.hasIntroMethod, hasIntroMethod: node.block.hasIntroMethod,

@ -33,6 +33,11 @@ export default class Text extends Node {
data: string; data: string;
shouldSkip: boolean; shouldSkip: boolean;
constructor(compiler, parent, info) {
super(compiler, parent, info);
this.data = info.data;
}
init(block: Block) { init(block: Block) {
const parentElement = this.findNearest(/(?:Element|Component)/); const parentElement = this.findNearest(/(?:Element|Component)/);

@ -1,7 +1,18 @@
import Node from './shared/Node'; import Node from './shared/Node';
import Expression from './shared/Expression';
export default class Transition extends Node { export default class Transition extends Node {
type: 'Transition';
name: string; name: string;
value: Node[] expression: Expression;
expression: Node
constructor(compiler, parent, info) {
super(compiler, parent, info);
this.name = info.name;
this.expression = info.expression
? new Expression(compiler, this, info.expression)
: null;
}
} }

@ -7,7 +7,8 @@ import validCalleeObjects from '../../utils/validCalleeObjects';
import reservedNames from '../../utils/reservedNames'; import reservedNames from '../../utils/reservedNames';
import Node from './shared/Node'; import Node from './shared/Node';
import Block from '../dom/Block'; import Block from '../dom/Block';
import Attribute from './Attribute'; import Binding from './Binding';
import EventHandler from './EventHandler';
const associatedEvents = { const associatedEvents = {
innerWidth: 'resize', innerWidth: 'resize',
@ -34,46 +35,55 @@ const readonly = new Set([
export default class Window extends Node { export default class Window extends Node {
type: 'Window'; type: 'Window';
attributes: Attribute[]; handlers: EventHandler[];
bindings: Binding[];
constructor(compiler, parent, info) {
super(compiler, parent, info);
this.handlers = [];
this.bindings = [];
info.attributes.forEach(node => {
if (node.type === 'EventHandler') {
this.handlers.push(new EventHandler(compiler, this, node));
} else if (node.type === 'Binding') {
this.bindings.push(new Binding(compiler, this, node));
}
});
}
build( build(
block: Block, block: Block,
parentNode: string, parentNode: string,
parentNodes: string parentNodes: string
) { ) {
const { generator } = this; const { compiler } = this;
const events = {}; const events = {};
const bindings: Record<string, string> = {}; const bindings: Record<string, string> = {};
this.attributes.forEach((attribute: Node) => { this.handlers.forEach(handler => {
if (attribute.type === 'EventHandler') {
// TODO verify that it's a valid callee (i.e. built-in or declared method) // TODO verify that it's a valid callee (i.e. built-in or declared method)
generator.addSourcemapLocations(attribute.expression); compiler.addSourcemapLocations(handler.expression);
const isCustomEvent = generator.events.has(attribute.name);
let usesState = false; const isCustomEvent = compiler.events.has(handler.name);
attribute.expression.arguments.forEach((arg: Node) => { let usesState = handler.dependencies.size > 0;
block.contextualise(arg, null, true);
const { dependencies } = arg.metadata;
if (dependencies.length) usesState = true;
});
const flattened = flattenReference(attribute.expression.callee); // const flattened = flattenReference(handler.expression.callee);
if (flattened.name !== 'event' && flattened.name !== 'this') { // if (flattened.name !== 'event' && flattened.name !== 'this') {
// allow event.stopPropagation(), this.select() etc // // allow event.stopPropagation(), this.select() etc
generator.code.prependRight( // compiler.code.prependRight(
attribute.expression.start, // handler.expression.start,
`${block.alias('component')}.` // `${block.alias('component')}.`
); // );
} // }
const handlerName = block.getUniqueName(`onwindow${attribute.name}`); const handlerName = block.getUniqueName(`onwindow${handler.name}`);
const handlerBody = deindent` const handlerBody = deindent`
${usesState && `var state = #component.get();`} ${usesState && `var ctx = #component.get();`}
[${attribute.expression.start}-${attribute.expression.end}]; ${handler.snippet};
`; `;
if (isCustomEvent) { if (isCustomEvent) {
@ -81,7 +91,7 @@ export default class Window extends Node {
block.addVariable(handlerName); block.addVariable(handlerName);
block.builders.hydrate.addBlock(deindent` block.builders.hydrate.addBlock(deindent`
${handlerName} = %events-${attribute.name}.call(#component, window, function(event) { ${handlerName} = %events-${handler.name}.call(#component, window, function(event) {
${handlerBody} ${handlerBody}
}); });
`); `);
@ -94,39 +104,38 @@ export default class Window extends Node {
function ${handlerName}(event) { function ${handlerName}(event) {
${handlerBody} ${handlerBody}
} }
window.addEventListener("${attribute.name}", ${handlerName}); window.addEventListener("${handler.name}", ${handlerName});
`); `);
block.builders.destroy.addBlock(deindent` block.builders.destroy.addBlock(deindent`
window.removeEventListener("${attribute.name}", ${handlerName}); window.removeEventListener("${handler.name}", ${handlerName});
`); `);
} }
} });
if (attribute.type === 'Binding') { this.bindings.forEach(binding => {
// in dev mode, throw if read-only values are written to // in dev mode, throw if read-only values are written to
if (readonly.has(attribute.name)) { if (readonly.has(binding.name)) {
generator.readonly.add(attribute.value.name); compiler.readonly.add(binding.value.name);
} }
bindings[attribute.name] = attribute.value.name; bindings[binding.name] = binding.value.name;
// bind:online is a special case, we need to listen for two separate events // bind:online is a special case, we need to listen for two separate events
if (attribute.name === 'online') return; if (binding.name === 'online') return;
const associatedEvent = associatedEvents[attribute.name]; const associatedEvent = associatedEvents[binding.name];
const property = properties[attribute.name] || attribute.name; const property = properties[binding.name] || binding.name;
if (!events[associatedEvent]) events[associatedEvent] = []; if (!events[associatedEvent]) events[associatedEvent] = [];
events[associatedEvent].push( events[associatedEvent].push(
`${attribute.value.name}: this.${property}` `${binding.value.name}: this.${property}`
); );
// add initial value // add initial value
generator.metaBindings.push( compiler.metaBindings.push(
`this._state.${attribute.value.name} = window.${property};` `this._state.${binding.value.name} = window.${property};`
); );
}
}); });
const lock = block.getUniqueName(`window_updating`); const lock = block.getUniqueName(`window_updating`);

@ -1,11 +1,68 @@
import Generator from '../../Generator'; import Generator from '../../Generator';
import { walk } from 'estree-walker';
import isReference from 'is-reference';
import flattenReference from '../../../utils/flattenReference';
import { createScopes } from '../../../utils/annotateWithScopes';
export default class Expression { export default class Expression {
compiler: Generator; compiler: Generator;
info: any; node: any;
snippet: string;
constructor(compiler, info) { references: Set<string>;
dependencies: Set<string>;
constructor(compiler, parent, info) {
this.compiler = compiler; this.compiler = compiler;
this.info = info; this.node = info;
this.snippet = `[✂${info.start}-${info.end}✂]`;
const contextDependencies = new Map(); // TODO
const indexes = new Map();
const dependencies = new Set();
const { code, helpers } = compiler;
let { map, scope } = createScopes(info);
walk(info, {
enter(node: any, parent: any) {
code.addSourcemapLocation(node.start);
code.addSourcemapLocation(node.end);
if (map.has(node)) {
scope = map.get(node);
return;
}
if (isReference(node, parent)) {
code.prependRight(node.start, 'ctx.');
const { name } = flattenReference(node);
if (scope && scope.has(name) || helpers.has(name) || (name === 'event' && isEventHandler)) return;
if (contextDependencies.has(name)) {
contextDependencies.get(name).forEach(dependency => {
dependencies.add(dependency);
});
} else if (!indexes.has(name)) {
dependencies.add(name);
}
this.skip();
}
},
leave(node: Node, parent: Node) {
if (map.has(node)) scope = scope.parent;
}
});
this.dependencies = dependencies;
this.contexts = new Set(); // TODO...
this.indexes = new Set(); // TODO...
} }
} }

@ -4,8 +4,12 @@ import Block from '../../dom/Block';
import { trimStart, trimEnd } from '../../../utils/trim'; import { trimStart, trimEnd } from '../../../utils/trim';
export default class Node { export default class Node {
compiler: Generator; readonly start: number;
parent: Node; readonly end: number;
readonly compiler: Generator;
readonly parent: Node;
readonly type: string;
prev?: Node; prev?: Node;
next?: Node; next?: Node;
@ -13,8 +17,11 @@ export default class Node {
var: string; var: string;
constructor(compiler: Generator, parent, info: any) { constructor(compiler: Generator, parent, info: any) {
this.start = info.start;
this.end = info.end;
this.compiler = compiler; this.compiler = compiler;
this.parent = parent; this.parent = parent;
this.type = info.type;
} }
cannotUseInnerHTML() { cannotUseInnerHTML() {
@ -74,7 +81,7 @@ export default class Node {
lastChild = null; lastChild = null;
cleaned.forEach((child: Node, i: number) => { cleaned.forEach((child: Node, i: number) => {
child.canUseInnerHTML = !this.generator.hydratable; child.canUseInnerHTML = !this.compiler.hydratable;
child.init(block, stripWhitespace, cleaned[i + 1] || nextSibling); child.init(block, stripWhitespace, cleaned[i + 1] || nextSibling);

@ -7,15 +7,14 @@ export default class Tag extends Node {
constructor(compiler, parent, info) { constructor(compiler, parent, info) {
super(compiler, parent, info); super(compiler, parent, info);
this.expression = new Expression(compiler, info.expression); this.expression = new Expression(compiler, this, info.expression);
} }
renameThisMethod( renameThisMethod(
block: Block, block: Block,
update: ((value: string) => string) update: ((value: string) => string)
) { ) {
const { indexes } = block.contextualise(this.expression); const { snippet, dependencies, indexes } = this.expression;
const { dependencies, snippet } = this.metadata;
const hasChangeableIndex = Array.from(indexes).some(index => block.changeableIndexes.get(index)); const hasChangeableIndex = Array.from(indexes).some(index => block.changeableIndexes.get(index));
@ -30,16 +29,16 @@ export default class Tag extends Node {
if (shouldCache) block.addVariable(value, snippet); if (shouldCache) block.addVariable(value, snippet);
if (dependencies.length || hasChangeableIndex) { if (dependencies.size || hasChangeableIndex) {
const changedCheck = ( const changedCheck = (
(block.hasOutroMethod ? `#outroing || ` : '') + (block.hasOutroMethod ? `#outroing || ` : '') +
dependencies.map((dependency: string) => `changed.${dependency}`).join(' || ') [...dependencies].map((dependency: string) => `changed.${dependency}`).join(' || ')
); );
const updateCachedValue = `${value} !== (${value} = ${snippet})`; const updateCachedValue = `${value} !== (${value} = ${snippet})`;
const condition = shouldCache ? const condition = shouldCache ?
(dependencies.length ? `(${changedCheck}) && ${updateCachedValue}` : updateCachedValue) : (dependencies.size ? `(${changedCheck}) && ${updateCachedValue}` : updateCachedValue) :
changedCheck; changedCheck;
block.builders.update.addConditional( block.builders.update.addConditional(

@ -1,13 +1,21 @@
import Component from '../Component';
import EachBlock from '../EachBlock';
import Element from '../Element'; import Element from '../Element';
import IfBlock from '../IfBlock';
import Text from '../Text'; import Text from '../Text';
import MustacheTag from '../MustacheTag'; import MustacheTag from '../MustacheTag';
import Window from '../Window';
import Node from './Node'; import Node from './Node';
function getConstructor(type): typeof Node { function getConstructor(type): typeof Node {
switch (type) { switch (type) {
case 'Component': return Component;
case 'EachBlock': return EachBlock;
case 'Element': return Element; case 'Element': return Element;
case 'IfBlock': return IfBlock;
case 'Text': return Text; case 'Text': return Text;
case 'MustacheTag': return MustacheTag; case 'MustacheTag': return MustacheTag;
case 'Window': return Window;
default: throw new Error(`Not implemented: ${type}`); default: throw new Error(`Not implemented: ${type}`);
} }
} }

@ -42,7 +42,7 @@ export default class Block {
return new Block(Object.assign({}, this, options, { parent: this })); return new Block(Object.assign({}, this, options, { parent: this }));
} }
contextualise(expression: Node, context?: string, isEventHandler?: boolean) { // contextualise(expression: Node, context?: string, isEventHandler?: boolean) {
return this.generator.contextualise(this.contexts, this.indexes, expression, context, isEventHandler); // return this.generator.contextualise(this.contexts, this.indexes, expression, context, isEventHandler);
} // }
} }

@ -64,7 +64,7 @@ export default function ssr(
conditions: [], conditions: [],
}); });
trim(parsed.html.children).forEach((node: Node) => { trim(generator.fragment.children).forEach((node: Node) => {
visit(generator, mainBlock, node); visit(generator, mainBlock, node);
}); });
@ -93,7 +93,7 @@ export default function ssr(
initialState.push('{}'); initialState.push('{}');
} }
initialState.push('state'); initialState.push('ctx');
// TODO concatenate CSS maps // TODO concatenate CSS maps
const result = deindent` const result = deindent`
@ -129,15 +129,15 @@ export default function ssr(
}; };
} }
${name}._render = function(__result, state, options) { ${name}._render = function(__result, ctx, options) {
${templateProperties.store && `options.store = %store();`} ${templateProperties.store && `options.store = %store();`}
__result.addComponent(${name}); __result.addComponent(${name});
state = Object.assign(${initialState.join(', ')}); ctx = Object.assign(${initialState.join(', ')});
${computations.map( ${computations.map(
({ key, deps }) => ({ key, deps }) =>
`state.${key} = %computed-${key}(state);` `ctx.${key} = %computed-${key}(ctx);`
)} )}
${generator.bindings.length && ${generator.bindings.length &&

@ -8,8 +8,7 @@ export default function visitAwaitBlock(
block: Block, block: Block,
node: Node node: Node
) { ) {
block.contextualise(node.expression); const { snippet } = node.expression;
const { snippet } = node.metadata;
// TODO should this be the generator's job? It's duplicated between // TODO should this be the generator's job? It's duplicated between
// here and the equivalent DOM compiler visitor // here and the equivalent DOM compiler visitor

@ -18,8 +18,7 @@ export default function visitComponent(
return escapeTemplate(escape(chunk.data)); return escapeTemplate(escape(chunk.data));
} }
if (chunk.type === 'MustacheTag') { if (chunk.type === 'MustacheTag') {
block.contextualise(chunk.expression); const { snippet } = chunk.expression;
const { snippet } = chunk.metadata;
return '${__escape( ' + snippet + ')}'; return '${__escape( ' + snippet + ')}';
} }
} }

@ -8,8 +8,7 @@ export default function visitIfBlock(
block: Block, block: Block,
node: Node node: Node
) { ) {
block.contextualise(node.expression); const { snippet } = node.expression;
const { snippet } = node.metadata;
generator.append('${ ' + snippet + ' ? `'); generator.append('${ ' + snippet + ' ? `');

@ -7,8 +7,7 @@ export default function visitMustacheTag(
block: Block, block: Block,
node: Node node: Node
) { ) {
block.contextualise(node.expression); const { snippet } = node.expression;
const { snippet } = node.metadata;
generator.append( generator.append(
node.parent && node.parent &&

@ -113,7 +113,7 @@ export default function tag(parser: Parser) {
const type = metaTags.has(name) const type = metaTags.has(name)
? metaTags.get(name) ? metaTags.get(name)
: 'Element'; // TODO in v2, capitalised name means 'Component' : /[A-Z]/.test(name[0]) ? 'Component' : 'Element';
const element: Node = { const element: Node = {
start, start,

@ -0,0 +1,5 @@
export default function addToSet(a: Set<any>, b: Set<any>) {
b.forEach(item => {
a.add(item);
});
}

@ -2,6 +2,54 @@ import { walk } from 'estree-walker';
import isReference from 'is-reference'; import isReference from 'is-reference';
import { Node } from '../interfaces'; import { Node } from '../interfaces';
export function createScopes(expression: Node) {
const map = new WeakMap();
const globals = new Set();
let scope = new Scope(null, false);
walk(expression, {
enter(node: Node, parent: Node) {
if (/Function/.test(node.type)) {
if (node.type === 'FunctionDeclaration') {
scope.declarations.add(node.id.name);
} else {
scope = new Scope(scope, false);
map.set(node, scope);
if (node.id) scope.declarations.add(node.id.name);
}
node.params.forEach((param: Node) => {
extractNames(param).forEach(name => {
scope.declarations.add(name);
});
});
} else if (/For(?:In|Of)Statement/.test(node.type)) {
scope = new Scope(scope, true);
map.set(node, scope);
} else if (node.type === 'BlockStatement') {
scope = new Scope(scope, true);
map.set(node, scope);
} else if (/(Function|Class|Variable)Declaration/.test(node.type)) {
scope.addDeclaration(node);
} else if (isReference(node, parent)) {
if (!scope.has(node.name)) {
globals.add(node.name);
}
}
},
leave(node: Node) {
if (map.has(node)) {
scope = scope.parent;
}
},
});
return { map, scope, globals };
}
// TODO remove this in favour of weakmap version
export default function annotateWithScopes(expression: Node) { export default function annotateWithScopes(expression: Node) {
const globals = new Set(); const globals = new Set();
let scope = new Scope(null, false); let scope = new Scope(null, false);

@ -2,20 +2,21 @@ import { DomGenerator } from '../generators/dom/index';
import { Node } from '../interfaces'; import { Node } from '../interfaces';
export default function createDebuggingComment(node: Node, generator: DomGenerator) { export default function createDebuggingComment(node: Node, generator: DomGenerator) {
const { locate, source } = generator; return `TODO ${node.start}-${node.end}`;
// const { locate, source } = generator;
let c = node.start; // let c = node.start;
if (node.type === 'ElseBlock') { // if (node.type === 'ElseBlock') {
while (source[c - 1] !== '{') c -= 1; // while (source[c - 1] !== '{') c -= 1;
while (source[c - 1] === '{') c -= 1; // while (source[c - 1] === '{') c -= 1;
} // }
let d = node.expression ? node.expression.end : c; // let d = node.expression ? node.expression.end : c;
while (source[d] !== '}') d += 1; // while (source[d] !== '}') d += 1;
while (source[d] === '}') d += 1; // while (source[d] === '}') d += 1;
const start = locate(c); // const start = locate(c);
const loc = `(${start.line + 1}:${start.column})`; // const loc = `(${start.line + 1}:${start.column})`;
return `${loc} ${source.slice(c, d)}`.replace(/\s/g, ' '); // return `${loc} ${source.slice(c, d)}`.replace(/\s/g, ' ');
} }

@ -25,7 +25,7 @@ function getName(filename) {
return base[0].toUpperCase() + base.slice(1); return base[0].toUpperCase() + base.slice(1);
} }
describe("runtime", () => { describe.only("runtime", () => {
before(() => { before(() => {
svelte = loadSvelte(false); svelte = loadSvelte(false);
svelte$ = loadSvelte(true); svelte$ = loadSvelte(true);

@ -1,5 +1,4 @@
export default { export default {
solo: true,
html: `<div style="color: red;">red</div>`, html: `<div style="color: red;">red</div>`,
test ( assert, component, target ) { test ( assert, component, target ) {

@ -1,21 +1,32 @@
export default { export default {
data: { data: {
name: 'world' name: 'world',
}, },
html: `<input>\n<p>hello world</p>`,
test ( assert, component, target, window ) {
const input = target.querySelector( 'input' );
assert.equal( input.value, 'world' );
const event = new window.Event( 'input' ); html: `
<input>
<p>hello world</p>
`,
test(assert, component, target, window) {
const input = target.querySelector('input');
assert.equal(input.value, 'world');
const event = new window.Event('input');
input.value = 'everybody'; input.value = 'everybody';
input.dispatchEvent( event ); input.dispatchEvent(event);
assert.equal( target.innerHTML, `<input>\n<p>hello everybody</p>` ); assert.htmlEqual(target.innerHTML, `
<input>
<p>hello everybody</p>
`);
component.set({ name: 'goodbye' }); component.set({ name: 'goodbye' });
assert.equal( input.value, 'goodbye' ); assert.equal(input.value, 'goodbye');
assert.equal( target.innerHTML, `<input>\n<p>hello goodbye</p>` ); assert.htmlEqual(target.innerHTML, `
} <input>
<p>hello goodbye</p>
`);
},
}; };

@ -1,46 +0,0 @@
<button use:tooltip="t(actionTransKey)">action</button>
<script>
const translations = {
perform_action: 'Perform an Action'
};
function t(key) {
return translations[key] || `{{${key}}}`;
}
export default {
data() {
return { t, actionTransKey: 'perform_action' };
},
actions: {
tooltip(node, text) {
let tooltip = null;
function onMouseEnter() {
tooltip = document.createElement('div');
tooltip.classList.add('tooltip');
tooltip.textContent = text;
node.parentNode.appendChild(tooltip);
}
function onMouseLeave() {
if (!tooltip) return;
tooltip.remove();
tooltip = null;
}
node.addEventListener('mouseenter', onMouseEnter);
node.addEventListener('mouseleave', onMouseLeave);
return {
destroy() {
node.removeEventListener('mouseenter', onMouseEnter);
node.removeEventListener('mouseleave', onMouseLeave);
}
}
}
}
}
</script>
Loading…
Cancel
Save