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

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

@ -2,10 +2,12 @@ import deindent from '../../utils/deindent';
import { stringify } from '../../utils/stringify';
import fixAttributeCasing from '../../utils/fixAttributeCasing';
import getExpressionPrecedence from '../../utils/getExpressionPrecedence';
import addToSet from '../../utils/addToSet';
import { DomGenerator } from '../dom/index';
import Node from './shared/Node';
import Element from './Element';
import Block from '../dom/Block';
import Expression from './shared/Expression';
export interface StyleProp {
key: string;
@ -20,14 +22,32 @@ export default class Attribute extends Node {
compiler: DomGenerator;
parent: Element;
name: string;
value: true | Node[]
isTrue: boolean;
isDynamic: boolean;
chunks: Node[];
dependencies: Set<string>;
expression: Node;
constructor(compiler, parent, info) {
super(compiler, parent, info);
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) {
@ -35,7 +55,7 @@ export default class Attribute extends Node {
const name = fixAttributeCasing(this.name);
if (name === 'style') {
const styleProps = optimizeStyle(this.value);
const styleProps = optimizeStyle(this.chunks);
if (styleProps) {
this.renderStyle(block, styleProps);
return;
@ -66,15 +86,14 @@ export default class Attribute extends Node {
? '@setXlinkAttribute'
: '@setAttribute';
const isDynamic = this.isDynamic();
const isLegacyInputType = this.generator.legacy && name === 'type' && this.parent.name === 'input';
const isLegacyInputType = this.compiler.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) {
return m[1].toUpperCase();
}) : name;
if (isDynamic) {
if (this.isDynamic) {
let value;
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
// DRY it out if that's possible without introducing crazy indirection
if (this.value.length === 1) {
// single {{tag}} — may be a non-string
const { expression } = this.value[0];
const { indexes } = block.contextualise(expression);
const { dependencies, snippet } = this.value[0].metadata;
if (this.chunks.length === 1) {
// single {tag} — may be a non-string
const expression = this.chunks[0];
const { dependencies, snippet, indexes } = expression;
value = snippet;
dependencies.forEach(d => {
@ -104,14 +122,13 @@ export default class Attribute extends Node {
} else {
// '{{foo}} {{bar}}' — treat as string concatenation
value =
(this.value[0].type === 'Text' ? '' : `"" + `) +
this.value
(this.chunks[0].type === 'Text' ? '' : `"" + `) +
this.chunks
.map((chunk: Node) => {
if (chunk.type === 'Text') {
return stringify(chunk.data);
} else {
const { indexes } = block.contextualise(chunk.expression);
const { dependencies, snippet } = chunk.metadata;
const { dependencies, snippet, indexes } = chunk;
if (Array.from(indexes).some(index => block.changeableIndexes.get(index))) {
hasChangeableIndex = true;
@ -121,7 +138,7 @@ export default class Attribute extends Node {
allDependencies.add(d);
});
return getExpressionPrecedence(chunk.expression) <= 13 ? `(${snippet})` : snippet;
return getExpressionPrecedence(chunk) <= 13 ? `(${snippet})` : snippet;
}
})
.join(' + ');
@ -211,9 +228,9 @@ export default class Attribute extends Node {
);
}
} else {
const value = this.value === true
const value = this.isTrue
? 'true'
: this.value.length === 0 ? `""` : stringify(this.value[0].data);
: this.chunks.length === 0 ? `""` : stringify(this.chunks[0].data);
const statement = (
isLegacyInputType
@ -237,7 +254,7 @@ export default class Attribute extends Node {
const updateValue = `${node.var}.value = ${node.var}.__value;`;
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') {
return stringify(chunk.data);
} else {
const { indexes } = block.contextualise(chunk.expression);
const { dependencies, snippet } = chunk.metadata;
const { dependencies, snippet, indexes } = chunk;
if (Array.from(indexes).some(index => block.changeableIndexes.get(index))) {
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

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

@ -11,6 +11,7 @@ import Node from './shared/Node';
import Block from '../dom/Block';
import Attribute from './Attribute';
import usesThisOrArguments from '../../validate/js/utils/usesThisOrArguments';
import mapChildren from './shared/mapChildren';
export default class Component extends Node {
type: 'Component';
@ -18,6 +19,31 @@ export default class Component extends Node {
attributes: Attribute[];
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(
block: Block,
stripWhitespace: boolean,
@ -46,7 +72,7 @@ export default class Component extends Node {
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
).toLowerCase()
@ -66,8 +92,7 @@ export default class Component extends Node {
parentNode: string,
parentNodes: string
) {
const { generator } = this;
generator.hasComponents = true;
const { compiler } = this;
const name = this.var;
@ -100,10 +125,10 @@ export default class Component extends Node {
const eventHandlers = this.attributes
.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');
if (ref) generator.usesRefs = true;
if (ref) compiler.usesRefs = true;
const updates: string[] = [];
@ -187,7 +212,7 @@ export default class Component extends Node {
}
if (bindings.length) {
generator.hasComplexBindings = true;
compiler.hasComplexBindings = true;
name_updating = block.alias(`${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);`);
} else {
const expression = this.name === 'svelte:self'
? generator.name
? compiler.name
: `%components-${this.name}`;
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;
if (handler.expression) {
generator.addSourcemapLocations(handler.expression);
compiler.addSourcemapLocations(handler.expression);
// TODO try out repetition between this and element counterpart
const flattened = flattenReference(handler.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)
generator.code.prependRight(
compiler.code.prependRight(
handler.expression.start,
`${block.alias('component')}.`
);

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

@ -22,30 +22,71 @@ import mapChildren from './shared/mapChildren';
export default class Element extends Node {
type: 'Element';
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[];
ref: string;
namespace: string;
constructor(compiler, parent, info: any) {
super(compiler, parent, info);
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 = [];
// TODO bindings etc
this.bindings = [];
this.handlers = [];
this.intro = null;
this.outro = null;
info.attributes.forEach(node => {
switch (node.type) {
case 'Attribute':
case 'Spread':
// special case
if (node.name === 'xmlns') this.namespace = node.value[0].data;
this.attributes.push(new Attribute(compiler, this, node));
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:
throw new Error(`Not implemented: ${node.type}`);
}
});
// TODO break out attributes and directives here
this.children = mapChildren(compiler, this, info.children);
}
init(
@ -57,61 +98,53 @@ export default class Element extends Node {
this.cannotUseInnerHTML();
}
const parentElement = this.parent && this.parent.findNearest(/^Element/);
this.namespace = this.name === 'svg' ?
namespaces.svg :
parentElement ? parentElement.namespace : this.generator.namespace;
this.attributes.forEach(attr => {
if (attr.dependencies.size) {
this.parent.cannotUseInnerHTML();
block.addDependencies(attr.dependencies);
// special case — <option value={foo}> — see below
if (this.name === 'option' && attr.name === 'value') {
let select = this.parent;
while (select && (select.type !== 'Element' || select.name !== 'select')) select = select.parent;
if (select && select.selectBindingDependencies) {
select.selectBindingDependencies.forEach(prop => {
dependencies.forEach((dependency: string) => {
this.compiler.indirectDependencies.get(prop).add(dependency);
});
});
}
}
}
});
this.bindings.forEach(binding => {
this.cannotUseInnerHTML();
block.addDependencies(binding.value.dependencies);
});
this.handlers.forEach(handler => {
this.cannotUseInnerHTML();
block.addDependencies(handler.dependencies);
});
if (this.intro) {
this.compiler.hasIntroTransitions = block.hasIntroMethod = true;
}
if (this.outro) {
this.compiler.hasOutroTransitions = block.hasOutroMethod = true;
block.outros += 1;
}
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) => {
if (chunk.type !== 'Text') {
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;
while (select && (select.type !== 'Element' || select.name !== 'select')) select = select.parent;
if (select && select.selectBindingDependencies) {
select.selectBindingDependencies.forEach(prop => {
dependencies.forEach((dependency: string) => {
this.generator.indirectDependencies.get(prop).add(dependency);
});
});
}
}
}
});
// removed
} else {
if (this.parent) this.parent.cannotUseInnerHTML();
if (attribute.type === 'EventHandler' && attribute.expression) {
attribute.expression.arguments.forEach((arg: Node) => {
block.addDependencies(arg.metadata.dependencies);
});
} else if (attribute.type === 'Binding') {
block.addDependencies(attribute.metadata.dependencies);
} else if (attribute.type === 'Transition') {
if (attribute.intro)
this.generator.hasIntroTransitions = block.hasIntroMethod = true;
if (attribute.outro) {
this.generator.hasOutroTransitions = block.hasOutroMethod = true;
block.outros += 1;
}
} else if (attribute.type === 'Action' && attribute.expression) {
if (attribute.type === 'Action' && attribute.expression) {
block.addDependencies(attribute.metadata.dependencies);
} else if (attribute.type === 'Spread') {
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>
// children treated the same way as a value attribute
if (this.children.length > 0) {
this.attributes.push(new Attribute({
generator: this.generator,
this.attributes.push(new Attribute(this.compiler, this, {
name: 'value',
value: this.children,
parent: this
value: this.children
}));
this.children = [];
@ -153,7 +184,7 @@ export default class Element extends Node {
const dependencies = binding.metadata.dependencies;
this.selectBindingDependencies = dependencies;
dependencies.forEach((prop: string) => {
this.generator.indirectDependencies.set(prop, new Set());
this.compiler.indirectDependencies.set(prop, new Set());
});
} else {
this.selectBindingDependencies = null;
@ -188,11 +219,11 @@ export default class Element extends Node {
parentNode: string,
parentNodes: string
) {
const { generator } = this;
const { compiler } = this;
if (this.name === 'slot') {
const slotName = this.getStaticAttributeValue('name') || 'default';
this.generator.slots.add(slotName);
this.compiler.slots.add(slotName);
}
if (this.name === 'noscript') return;
@ -211,15 +242,15 @@ export default class Element extends Node {
parentNode;
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(
`${name} = ${renderStatement};`
);
if (this.generator.hydratable) {
if (this.compiler.hydratable) {
if (parentNodes) {
block.builders.claim.addBlock(deindent`
${name} = ${getClaimStatement(generator, this.namespace, parentNodes, this)};
${name} = ${getClaimStatement(compiler, this.namespace, parentNodes, this)};
var ${childState.parentNodes} = @children(${name});
`);
} else {
@ -271,7 +302,7 @@ export default class Element extends Node {
this.addBindings(block, allUsedContexts);
const eventHandlerUsesComponent = this.addEventHandlers(block, allUsedContexts);
this.addRefs(block);
if (this.ref) this.addRef(block);
this.addAttributes(block);
this.addTransitions(block);
this.addActions(block);
@ -353,14 +384,14 @@ export default class Element extends Node {
block: Block,
allUsedContexts: Set<string>
) {
const bindings: Binding[] = this.attributes.filter((a: Binding) => a.type === 'Binding');
if (bindings.length === 0) return;
if (this.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 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) ?
block.getUniqueName(`${this.var}_updating`) :
@ -452,7 +483,7 @@ export default class Element extends Node {
.join(' && ');
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(
`if (!(${allInitialStateIsDefined})) #component.root._beforecreate.push(${handler});`
@ -469,7 +500,7 @@ export default class Element extends Node {
return;
}
this.attributes.filter((a: Attribute) => a.type === 'Attribute').forEach((attribute: Attribute) => {
this.attributes.forEach((attribute: Attribute) => {
attribute.render(block);
});
}
@ -528,89 +559,58 @@ export default class Element extends Node {
}
addEventHandlers(block: Block, allUsedContexts) {
const { generator } = this;
const { compiler } = this;
let eventHandlerUsesComponent = false;
this.attributes.filter((a: EventHandler) => a.type === 'EventHandler').forEach((attribute: EventHandler) => {
const isCustomEvent = generator.events.has(attribute.name);
this.handlers.forEach(handler => {
const isCustomEvent = compiler.events.has(handler.name);
const shouldHoist = !isCustomEvent && this.hasAncestor('EachBlock');
const context = shouldHoist ? null : this.var;
const usedContexts: string[] = [];
if (attribute.expression) {
generator.addSourcemapLocations(attribute.expression);
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 (handler.callee) {
handler.render(this.compiler, block);
if (!validCalleeObjects.has(handler.callee.name)) {
if (shouldHoist) eventHandlerUsesComponent = true; // this feels a bit hacky but it works!
}
attribute.expression.arguments.forEach((arg: Node) => {
const { contexts } = block.contextualise(arg, context, true);
// handler.expression.arguments.forEach((arg: Node) => {
// const { contexts } = block.contextualise(arg, context, true);
contexts.forEach(context => {
if (!~usedContexts.indexOf(context)) usedContexts.push(context);
allUsedContexts.add(context);
});
});
// contexts.forEach(context => {
// if (!~usedContexts.indexOf(context)) usedContexts.push(context);
// allUsedContexts.add(context);
// });
// });
}
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
// if hoisted, locally unique otherwise
const handlerName = (shouldHoist ? generator : block).getUniqueName(
`${attribute.name.replace(/[^a-zA-Z0-9_$]/g, '_')}_handler`
const handlerName = (shouldHoist ? compiler : block).getUniqueName(
`${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
const handlerBody = deindent`
${eventHandlerUsesComponent &&
`var ${block.alias('component')} = ${ctx}._svelte.component;`}
${declarations}
${attribute.expression ?
`[✂${attribute.expression.start}-${attribute.expression.end}✂];` :
`${block.alias('component')}.fire("${attribute.name}", event);`}
`var #component = ${ctx}._svelte.component;`}
${handler.dependencies.size > 0 && `const ctx = #component.get();`}
${handler.snippet ?
handler.snippet :
`#component.fire("${handler.name}", event);`}
`;
if (isCustomEvent) {
block.addVariable(handlerName);
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}
});
`);
@ -619,61 +619,53 @@ export default class Element extends Node {
${handlerName}.destroy();
`);
} else {
const handler = deindent`
const handlerFunction = deindent`
function ${handlerName}(event) {
${handlerBody}
}
`;
if (shouldHoist) {
generator.blocks.push(handler);
compiler.blocks.push(handlerFunction);
} else {
block.builders.init.addBlock(handler);
block.builders.init.addBlock(handlerFunction);
}
block.builders.hydrate.addLine(
`@addListener(${this.var}, "${attribute.name}", ${handlerName});`
`@addListener(${this.var}, "${handler.name}", ${handlerName});`
);
block.builders.destroy.addLine(
`@removeListener(${this.var}, "${attribute.name}", ${handlerName});`
`@removeListener(${this.var}, "${handler.name}", ${handlerName});`
);
}
});
return eventHandlerUsesComponent;
}
addRefs(block: Block) {
// TODO it should surely be an error to have more than one ref
this.attributes.filter((a: Ref) => a.type === 'Ref').forEach((attribute: Ref) => {
const ref = `#component.refs.${attribute.name}`;
addRef(block: Block) {
const ref = `#component.refs.${this.ref}`;
block.builders.mount.addLine(
`${ref} = ${this.var};`
);
block.builders.destroy.addLine(
`if (${ref} === ${this.var}) ${ref} = null;`
);
block.builders.mount.addLine(
`${ref} = ${this.var};`
);
this.generator.usesRefs = true; // so component.refs object is created
});
block.builders.destroy.addLine(
`if (${ref} === ${this.var}) ${ref} = null;`
);
}
addTransitions(
block: Block
) {
const intro = this.attributes.find((a: Transition) => a.type === 'Transition' && a.intro);
const outro = this.attributes.find((a: Transition) => a.type === 'Transition' && a.outro);
const { intro, outro } = this;
if (!intro && !outro) return;
if (intro === outro) {
block.contextualise(intro.expression); // TODO remove all these
const name = block.getUniqueName(`${this.var}_transition`);
const snippet = intro.expression
? intro.metadata.snippet
? intro.expression.snippet
: '{}';
block.addVariable(name);
@ -701,11 +693,9 @@ export default class Element extends Node {
const outroName = outro && block.getUniqueName(`${this.var}_outro`);
if (intro) {
block.contextualise(intro.expression);
block.addVariable(introName);
const snippet = intro.expression
? intro.metadata.snippet
? intro.expression.snippet
: '{}';
const fn = `%transitions-${intro.name}`; // TODO add built-in transitions?
@ -728,11 +718,9 @@ export default class Element extends Node {
}
if (outro) {
block.contextualise(outro.expression);
block.addVariable(outroName);
const snippet = outro.expression
? outro.metadata.snippet
? outro.expression.snippet
: '{}';
const fn = `%transitions-${outro.name}`;
@ -755,7 +743,7 @@ export default class Element extends Node {
const { expression } = attribute;
let snippet, dependencies;
if (expression) {
this.generator.addSourcemapLocations(expression);
this.compiler.addSourcemapLocations(expression);
block.contextualise(expression);
snippet = attribute.metadata.snippet;
dependencies = attribute.metadata.dependencies;
@ -823,18 +811,18 @@ export default class Element extends Node {
const classAttribute = this.attributes.find(a => a.name === 'class');
if (classAttribute && classAttribute.value !== true) {
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 {
(<Node[]>classAttribute.value).push(
new Node({ type: 'Text', data: ` ${this.generator.stylesheet.id}` })
new Node({ type: 'Text', data: ` ${this.compiler.stylesheet.id}` })
);
}
} else {
this.attributes.push(
new Attribute({
generator: this.generator,
compiler: this.compiler,
name: 'class',
value: [new Node({ type: 'Text', data: `${this.generator.stylesheet.id}` })],
value: [new Node({ type: 'Text', data: `${this.compiler.stylesheet.id}` })],
parent: this,
})
);
@ -843,7 +831,7 @@ export default class Element extends Node {
}
function getRenderStatement(
generator: DomGenerator,
compiler: DomGenerator,
namespace: string,
name: string
) {
@ -859,7 +847,7 @@ function getRenderStatement(
}
function getClaimStatement(
generator: DomGenerator,
compiler: DomGenerator,
namespace: string,
nodes: string,
node: Node

@ -1,8 +1,13 @@
import Node from './shared/Node';
import Block from '../dom/Block';
import mapChildren from './shared/mapChildren';
export default class ElseBlock extends Node {
type: 'ElseBlock';
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 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 {
name: string;
value: Node[]
expression: Node
dependencies: Set<string>;
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[];
constructor(compiler: Generator, info: any) {
super(compiler, info);
super(compiler, null, info);
this.children = mapChildren(compiler, this, info.children);
}
init() {
this.block = new Block({
generator: this.generator,
generator: this.compiler,
name: '@create_main_fragment',
key: null,
@ -29,7 +29,7 @@ export default class Fragment extends Node {
dependencies: new Set(),
});
this.generator.blocks.push(this.block);
this.compiler.blocks.push(this.block);
this.initChildren(this.block, true, null);
this.block.hasUpdateMethod = true;

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

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

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

@ -1,11 +1,68 @@
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 {
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.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';
export default class Node {
compiler: Generator;
parent: Node;
readonly start: number;
readonly end: number;
readonly compiler: Generator;
readonly parent: Node;
readonly type: string;
prev?: Node;
next?: Node;
@ -13,8 +17,11 @@ export default class Node {
var: string;
constructor(compiler: Generator, parent, info: any) {
this.start = info.start;
this.end = info.end;
this.compiler = compiler;
this.parent = parent;
this.type = info.type;
}
cannotUseInnerHTML() {
@ -74,7 +81,7 @@ export default class Node {
lastChild = null;
cleaned.forEach((child: Node, i: number) => {
child.canUseInnerHTML = !this.generator.hydratable;
child.canUseInnerHTML = !this.compiler.hydratable;
child.init(block, stripWhitespace, cleaned[i + 1] || nextSibling);

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

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

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

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

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

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

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

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

@ -113,7 +113,7 @@ export default function tag(parser: Parser) {
const type = metaTags.has(name)
? metaTags.get(name)
: 'Element'; // TODO in v2, capitalised name means 'Component'
: /[A-Z]/.test(name[0]) ? 'Component' : 'Element';
const element: Node = {
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 { 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) {
const globals = new Set();
let scope = new Scope(null, false);

@ -2,20 +2,21 @@ import { DomGenerator } from '../generators/dom/index';
import { Node } from '../interfaces';
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;
if (node.type === 'ElseBlock') {
while (source[c - 1] !== '{') c -= 1;
while (source[c - 1] === '{') c -= 1;
}
// let c = node.start;
// if (node.type === 'ElseBlock') {
// while (source[c - 1] !== '{') c -= 1;
// while (source[c - 1] === '{') c -= 1;
// }
let d = node.expression ? node.expression.end : c;
while (source[d] !== '}') d += 1;
while (source[d] === '}') d += 1;
// let d = node.expression ? node.expression.end : c;
// while (source[d] !== '}') d += 1;
// while (source[d] === '}') d += 1;
const start = locate(c);
const loc = `(${start.line + 1}:${start.column})`;
// const start = locate(c);
// 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);
}
describe("runtime", () => {
describe.only("runtime", () => {
before(() => {
svelte = loadSvelte(false);
svelte$ = loadSvelte(true);

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

@ -1,21 +1,32 @@
export default {
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.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' });
assert.equal( input.value, 'goodbye' );
assert.equal( target.innerHTML, `<input>\n<p>hello goodbye</p>` );
}
assert.equal(input.value, 'goodbye');
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