diff --git a/src/generators/Generator.ts b/src/generators/Generator.ts index 0aa96ca9d2..c91c36caf2 100644 --- a/src/generators/Generator.ts +++ b/src/generators/Generator.ts @@ -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 + // })); + // } + // } + // } + // }); + // } } diff --git a/src/generators/dom/Block.ts b/src/generators/dom/Block.ts index edf505c7b3..f554d49097 100644 --- a/src/generators/dom/Block.ts +++ b/src/generators/dom/Block.ts @@ -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 && diff --git a/src/generators/nodes/Attribute.ts b/src/generators/nodes/Attribute.ts index 074cc18352..e44aad898c 100644 --- a/src/generators/nodes/Attribute.ts +++ b/src/generators/nodes/Attribute.ts @@ -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 diff --git a/src/generators/nodes/Binding.ts b/src/generators/nodes/Binding.ts index 1ac182102b..4a452d6ee6 100644 --- a/src/generators/nodes/Binding.ts +++ b/src/generators/nodes/Binding.ts @@ -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}])`; } diff --git a/src/generators/nodes/Component.ts b/src/generators/nodes/Component.ts index a1861664eb..d5512429a4 100644 --- a/src/generators/nodes/Component.ts +++ b/src/generators/nodes/Component.ts @@ -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')}.` ); diff --git a/src/generators/nodes/EachBlock.ts b/src/generators/nodes/EachBlock.ts index e74b8a01c2..3d7ca27469 100644 --- a/src/generators/nodes/EachBlock.ts +++ b/src/generators/nodes/EachBlock.ts @@ -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')} }); diff --git a/src/generators/nodes/Element.ts b/src/generators/nodes/Element.ts index eb6c01e351..7e848a4c1e 100644 --- a/src/generators/nodes/Element.ts +++ b/src/generators/nodes/Element.ts @@ -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 diff --git a/src/generators/nodes/ElseBlock.ts b/src/generators/nodes/ElseBlock.ts index 0a48881fbb..917e3aa026 100644 --- a/src/generators/nodes/ElseBlock.ts +++ b/src/generators/nodes/ElseBlock.ts @@ -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); + } } \ No newline at end of file diff --git a/src/generators/nodes/EventHandler.ts b/src/generators/nodes/EventHandler.ts index fae628e58c..34bc200b3d 100644 --- a/src/generators/nodes/EventHandler.ts +++ b/src/generators/nodes/EventHandler.ts @@ -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')}.` + ); + } + } + } } \ No newline at end of file diff --git a/src/generators/nodes/Fragment.ts b/src/generators/nodes/Fragment.ts index f82b72b3d1..214c54b43d 100644 --- a/src/generators/nodes/Fragment.ts +++ b/src/generators/nodes/Fragment.ts @@ -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; diff --git a/src/generators/nodes/IfBlock.ts b/src/generators/nodes/IfBlock.ts index e519ca63f2..2167ea8baf 100644 --- a/src/generators/nodes/IfBlock.ts +++ b/src/generators/nodes/IfBlock.ts @@ -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, diff --git a/src/generators/nodes/Text.ts b/src/generators/nodes/Text.ts index 457c8538ca..d8c468224d 100644 --- a/src/generators/nodes/Text.ts +++ b/src/generators/nodes/Text.ts @@ -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)/); diff --git a/src/generators/nodes/Transition.ts b/src/generators/nodes/Transition.ts index d110b5878d..9ed9f87dab 100644 --- a/src/generators/nodes/Transition.ts +++ b/src/generators/nodes/Transition.ts @@ -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; + } } \ No newline at end of file diff --git a/src/generators/nodes/Window.ts b/src/generators/nodes/Window.ts index 38a38c1084..ab532a0081 100644 --- a/src/generators/nodes/Window.ts +++ b/src/generators/nodes/Window.ts @@ -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`); diff --git a/src/generators/nodes/shared/Expression.ts b/src/generators/nodes/shared/Expression.ts index 868ec45ab5..3e96677fa2 100644 --- a/src/generators/nodes/shared/Expression.ts +++ b/src/generators/nodes/shared/Expression.ts @@ -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... } } \ No newline at end of file diff --git a/src/generators/nodes/shared/Node.ts b/src/generators/nodes/shared/Node.ts index 6fb2f8cc15..a07b4fdd8b 100644 --- a/src/generators/nodes/shared/Node.ts +++ b/src/generators/nodes/shared/Node.ts @@ -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); diff --git a/src/generators/nodes/shared/Tag.ts b/src/generators/nodes/shared/Tag.ts index b762e09311..267d93d28d 100644 --- a/src/generators/nodes/shared/Tag.ts +++ b/src/generators/nodes/shared/Tag.ts @@ -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( diff --git a/src/generators/nodes/shared/mapChildren.ts b/src/generators/nodes/shared/mapChildren.ts index aaac0edc2d..829a7ea962 100644 --- a/src/generators/nodes/shared/mapChildren.ts +++ b/src/generators/nodes/shared/mapChildren.ts @@ -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}`); } } diff --git a/src/generators/server-side-rendering/Block.ts b/src/generators/server-side-rendering/Block.ts index 5b727dc828..1e85b0c1d8 100644 --- a/src/generators/server-side-rendering/Block.ts +++ b/src/generators/server-side-rendering/Block.ts @@ -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); + // } } diff --git a/src/generators/server-side-rendering/index.ts b/src/generators/server-side-rendering/index.ts index a0eade1664..c8e9cda42f 100644 --- a/src/generators/server-side-rendering/index.ts +++ b/src/generators/server-side-rendering/index.ts @@ -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 && diff --git a/src/generators/server-side-rendering/visitors/AwaitBlock.ts b/src/generators/server-side-rendering/visitors/AwaitBlock.ts index 1570647db5..c77bd8dbf8 100644 --- a/src/generators/server-side-rendering/visitors/AwaitBlock.ts +++ b/src/generators/server-side-rendering/visitors/AwaitBlock.ts @@ -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 diff --git a/src/generators/server-side-rendering/visitors/Component.ts b/src/generators/server-side-rendering/visitors/Component.ts index 7ff6969983..e6a7707004 100644 --- a/src/generators/server-side-rendering/visitors/Component.ts +++ b/src/generators/server-side-rendering/visitors/Component.ts @@ -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 + ')}'; } } diff --git a/src/generators/server-side-rendering/visitors/IfBlock.ts b/src/generators/server-side-rendering/visitors/IfBlock.ts index 88de7f0b5a..dc5b908b33 100644 --- a/src/generators/server-side-rendering/visitors/IfBlock.ts +++ b/src/generators/server-side-rendering/visitors/IfBlock.ts @@ -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 + ' ? `'); diff --git a/src/generators/server-side-rendering/visitors/MustacheTag.ts b/src/generators/server-side-rendering/visitors/MustacheTag.ts index 795e78dd33..713493ac79 100644 --- a/src/generators/server-side-rendering/visitors/MustacheTag.ts +++ b/src/generators/server-side-rendering/visitors/MustacheTag.ts @@ -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 && diff --git a/src/parse/state/tag.ts b/src/parse/state/tag.ts index cf0267c240..2eb8b3d068 100644 --- a/src/parse/state/tag.ts +++ b/src/parse/state/tag.ts @@ -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, diff --git a/src/utils/addToSet.ts b/src/utils/addToSet.ts new file mode 100644 index 0000000000..5197e96972 --- /dev/null +++ b/src/utils/addToSet.ts @@ -0,0 +1,5 @@ +export default function addToSet(a: Set<any>, b: Set<any>) { + b.forEach(item => { + a.add(item); + }); +} \ No newline at end of file diff --git a/src/utils/annotateWithScopes.ts b/src/utils/annotateWithScopes.ts index 41e734b6e9..408de776b2 100644 --- a/src/utils/annotateWithScopes.ts +++ b/src/utils/annotateWithScopes.ts @@ -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); diff --git a/src/utils/createDebuggingComment.ts b/src/utils/createDebuggingComment.ts index 1eadfd1c93..61dc2ddba0 100644 --- a/src/utils/createDebuggingComment.ts +++ b/src/utils/createDebuggingComment.ts @@ -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, ' '); } diff --git a/test/runtime/index.js b/test/runtime/index.js index 199a9a0c9b..8f37b7bb84 100644 --- a/test/runtime/index.js +++ b/test/runtime/index.js @@ -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); diff --git a/test/runtime/samples/attribute-dynamic/_config.js b/test/runtime/samples/attribute-dynamic/_config.js index f6afbeca10..9b33022e62 100644 --- a/test/runtime/samples/attribute-dynamic/_config.js +++ b/test/runtime/samples/attribute-dynamic/_config.js @@ -1,5 +1,4 @@ export default { - solo: true, html: `<div style="color: red;">red</div>`, test ( assert, component, target ) { diff --git a/test/runtime/samples/binding-input-text/_config.js b/test/runtime/samples/binding-input-text/_config.js index ccbe234d12..2c54fc8c32 100644 --- a/test/runtime/samples/binding-input-text/_config.js +++ b/test/runtime/samples/binding-input-text/_config.js @@ -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> + `); + }, }; diff --git a/test/runtime/samples/trait-function/main.html b/test/runtime/samples/trait-function/main.html deleted file mode 100644 index cde902caad..0000000000 --- a/test/runtime/samples/trait-function/main.html +++ /dev/null @@ -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> \ No newline at end of file