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, - indexes: Map, - expression: Node, - context: string, - isEventHandler: boolean - ): { - contexts: Set, - indexes: Set - } { - // this.addSourcemapLocations(expression); - - const usedContexts: Set = new Set(); - const usedIndexes: Set = 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, + // indexes: Map, + // expression: Node, + // context: string, + // isEventHandler: boolean + // ): { + // contexts: Set, + // indexes: Set + // } { + // // this.addSourcemapLocations(expression); + + // const usedContexts: Set = new Set(); + // const usedIndexes: Set = 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, - indexes: Set, - 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 = 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[] = [contextDependencies]; - - let indexes = new Set(); - const indexesStack: Set[] = [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: - // - // - 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, + // indexes: Set, + // 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 = 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[] = [contextDependencies]; + + // let indexes = new Set(); + // const indexesStack: Set[] = [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: + // // + // // + // 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) { 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; 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. `` // and `selected` is an object chosen with a 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) { +function mungeEventHandler(compiler: DomGenerator, node: Node, handler: Node, block: Block, allContexts: Set) { 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 —