From d3afc4c0f3285bf7c5d0a3a0deb442e28bb9296a Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sun, 22 Apr 2018 15:55:37 -0400 Subject: [PATCH 01/85] baby steps towards #1316 --- src/generators/Generator.ts | 6 +++-- src/generators/dom/index.ts | 4 +-- src/generators/nodes/Attribute.ts | 26 +++++------------- src/generators/nodes/Element.ts | 24 +++++++++++++++++ src/generators/nodes/Fragment.ts | 7 +++++ src/generators/nodes/MustacheTag.ts | 2 +- src/generators/nodes/shared/Expression.ts | 11 ++++++++ src/generators/nodes/shared/Node.ts | 18 ++++--------- src/generators/nodes/shared/Tag.ts | 8 ++++++ src/generators/nodes/shared/mapChildren.ts | 27 +++++++++++++++++++ .../samples/attribute-dynamic/_config.js | 1 + tsconfig.json | 3 ++- 12 files changed, 99 insertions(+), 38 deletions(-) create mode 100644 src/generators/nodes/shared/Expression.ts create mode 100644 src/generators/nodes/shared/mapChildren.ts diff --git a/src/generators/Generator.ts b/src/generators/Generator.ts index b3cdd96402c3..0aa96ca9d217 100644 --- a/src/generators/Generator.ts +++ b/src/generators/Generator.ts @@ -17,6 +17,7 @@ import clone from '../utils/clone'; import Stylesheet from '../css/Stylesheet'; import { test } from '../config'; import nodes from './nodes/index'; +import Fragment from './nodes/Fragment'; import { Node, GenerateOptions, ShorthandImport, Parsed, CompileOptions, CustomElementOptions } from '../interfaces'; interface Computation { @@ -83,6 +84,7 @@ export default class Generator { source: string; name: string; options: CompileOptions; + fragment: Fragment; customElement: CustomElementOptions; tag: string; @@ -191,6 +193,7 @@ export default class Generator { throw new Error(`No tag name specified`); // TODO better error } + this.fragment = new Fragment(this, parsed.html); this.walkTemplate(); if (!this.customElement) this.stylesheet.reify(); } @@ -711,7 +714,6 @@ export default class Generator { expectedProperties, helpers } = this; - const { html } = this.parsed; const contextualise = ( node: Node, contextDependencies: Map, @@ -782,7 +784,7 @@ export default class Generator { return parentIsHead(node.parent); } - walk(html, { + walk(this.fragment, { enter(node: Node, parent: Node, key: string) { // TODO this is hacky as hell if (key === 'parent') return this.skip(); diff --git a/src/generators/dom/index.ts b/src/generators/dom/index.ts index 337d23d2ed08..151dea4109ce 100644 --- a/src/generators/dom/index.ts +++ b/src/generators/dom/index.ts @@ -70,8 +70,8 @@ export default function dom( namespace, } = generator; - parsed.html.build(); - const { block } = parsed.html; + generator.fragment.build(); + const { block } = generator.fragment; // prevent fragment being created twice (#1063) if (options.customElement) block.builders.create.addLine(`this.c = @noop;`); diff --git a/src/generators/nodes/Attribute.ts b/src/generators/nodes/Attribute.ts index 904c971c1df3..074cc18352a4 100644 --- a/src/generators/nodes/Attribute.ts +++ b/src/generators/nodes/Attribute.ts @@ -12,34 +12,22 @@ export interface StyleProp { value: Node[]; } -export default class Attribute { +export default class Attribute extends Node { type: 'Attribute'; start: number; end: number; - generator: DomGenerator; + compiler: DomGenerator; parent: Element; name: string; value: true | Node[] expression: Node; - constructor({ - generator, - name, - value, - parent - }: { - generator: DomGenerator, - name: string, - value: Node[], - parent: Element - }) { - this.type = 'Attribute'; - this.generator = generator; - this.parent = parent; - - this.name = name; - this.value = value; + constructor(compiler, parent, info) { + super(compiler, parent, info); + + this.name = info.name; + this.value = info.value; } render(block: Block) { diff --git a/src/generators/nodes/Element.ts b/src/generators/nodes/Element.ts index a44c94981621..eb6c01e351d8 100644 --- a/src/generators/nodes/Element.ts +++ b/src/generators/nodes/Element.ts @@ -17,6 +17,7 @@ import Transition from './Transition'; import Action from './Action'; import Text from './Text'; import * as namespaces from '../../utils/namespaces'; +import mapChildren from './shared/mapChildren'; export default class Element extends Node { type: 'Element'; @@ -24,6 +25,29 @@ export default class Element extends Node { attributes: (Attribute | Binding | EventHandler | Ref | Transition | Action)[]; // TODO split these up sooner children: Node[]; + constructor(compiler, parent, info: any) { + super(compiler, parent, info); + this.name = info.name; + this.children = mapChildren(compiler, parent, info.children); + + this.attributes = []; + // TODO bindings etc + + info.attributes.forEach(node => { + switch (node.type) { + case 'Attribute': + case 'Spread': + this.attributes.push(new Attribute(compiler, this, node)); + break; + + default: + throw new Error(`Not implemented: ${node.type}`); + } + }); + + // TODO break out attributes and directives here + } + init( block: Block, stripWhitespace: boolean, diff --git a/src/generators/nodes/Fragment.ts b/src/generators/nodes/Fragment.ts index e4eb55967981..f82b72b3d173 100644 --- a/src/generators/nodes/Fragment.ts +++ b/src/generators/nodes/Fragment.ts @@ -1,11 +1,18 @@ import Node from './shared/Node'; import { DomGenerator } from '../dom/index'; +import Generator from '../Generator'; +import mapChildren from './shared/mapChildren'; import Block from '../dom/Block'; export default class Fragment extends Node { block: Block; children: Node[]; + constructor(compiler: Generator, info: any) { + super(compiler, info); + this.children = mapChildren(compiler, this, info.children); + } + init() { this.block = new Block({ generator: this.generator, diff --git a/src/generators/nodes/MustacheTag.ts b/src/generators/nodes/MustacheTag.ts index 06c854813628..adf806c061a0 100644 --- a/src/generators/nodes/MustacheTag.ts +++ b/src/generators/nodes/MustacheTag.ts @@ -6,7 +6,7 @@ export default class MustacheTag extends Tag { init(block: Block) { this.cannotUseInnerHTML(); this.var = block.getUniqueName('text'); - block.addDependencies(this.metadata.dependencies); + block.addDependencies(this.expression.dependencies); } build( diff --git a/src/generators/nodes/shared/Expression.ts b/src/generators/nodes/shared/Expression.ts new file mode 100644 index 000000000000..868ec45ab5d1 --- /dev/null +++ b/src/generators/nodes/shared/Expression.ts @@ -0,0 +1,11 @@ +import Generator from '../../Generator'; + +export default class Expression { + compiler: Generator; + info: any; + + constructor(compiler, info) { + this.compiler = compiler; + this.info = info; + } +} \ No newline at end of file diff --git a/src/generators/nodes/shared/Node.ts b/src/generators/nodes/shared/Node.ts index 0d268f0c137f..6fb2f8cc156a 100644 --- a/src/generators/nodes/shared/Node.ts +++ b/src/generators/nodes/shared/Node.ts @@ -1,28 +1,20 @@ import { DomGenerator } from '../../dom/index'; +import Generator from './../../Generator'; import Block from '../../dom/Block'; import { trimStart, trimEnd } from '../../../utils/trim'; export default class Node { - type: string; - start: number; - end: number; - [key: string]: any; - - metadata?: { - dependencies: string[]; - snippet: string; - }; - + compiler: Generator; parent: Node; prev?: Node; next?: Node; - generator: DomGenerator; canUseInnerHTML: boolean; var: string; - constructor(data: Record) { - Object.assign(this, data); + constructor(compiler: Generator, parent, info: any) { + this.compiler = compiler; + this.parent = parent; } cannotUseInnerHTML() { diff --git a/src/generators/nodes/shared/Tag.ts b/src/generators/nodes/shared/Tag.ts index e4008435197b..b762e0931133 100644 --- a/src/generators/nodes/shared/Tag.ts +++ b/src/generators/nodes/shared/Tag.ts @@ -1,7 +1,15 @@ import Node from './Node'; +import Expression from './Expression'; import Block from '../../dom/Block'; export default class Tag extends Node { + expression: Expression; + + constructor(compiler, parent, info) { + super(compiler, parent, info); + this.expression = new Expression(compiler, info.expression); + } + renameThisMethod( block: Block, update: ((value: string) => string) diff --git a/src/generators/nodes/shared/mapChildren.ts b/src/generators/nodes/shared/mapChildren.ts new file mode 100644 index 000000000000..aaac0edc2d57 --- /dev/null +++ b/src/generators/nodes/shared/mapChildren.ts @@ -0,0 +1,27 @@ +import Element from '../Element'; +import Text from '../Text'; +import MustacheTag from '../MustacheTag'; +import Node from './Node'; + +function getConstructor(type): typeof Node { + switch (type) { + case 'Element': return Element; + case 'Text': return Text; + case 'MustacheTag': return MustacheTag; + default: throw new Error(`Not implemented: ${type}`); + } +} + +export default function mapChildren(compiler, parent, children: any[]) { + let last = null; + return children.map(child => { + const constructor = getConstructor(child.type); + const node = new constructor(compiler, parent, child); + + if (last) last.next = node; + node.prev = last; + last = node; + + return node; + }); +} \ No newline at end of file diff --git a/test/runtime/samples/attribute-dynamic/_config.js b/test/runtime/samples/attribute-dynamic/_config.js index 9b33022e6207..f6afbeca1082 100644 --- a/test/runtime/samples/attribute-dynamic/_config.js +++ b/test/runtime/samples/attribute-dynamic/_config.js @@ -1,4 +1,5 @@ export default { + solo: true, html: `
red
`, test ( assert, component, target ) { diff --git a/tsconfig.json b/tsconfig.json index 14c8034b76f3..fdb7367e054d 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -6,7 +6,8 @@ "noEmitOnError": true, "allowJs": true, "lib": ["es5", "es6", "dom"], - "importHelpers": true + "importHelpers": true, + "moduleResolution": "node" }, "include": [ "src" From fb09c2548f367779341706938969355748da1882 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sun, 22 Apr 2018 15:57:55 -0400 Subject: [PATCH 02/85] remove unused contextDependencies references --- src/generators/server-side-rendering/Block.ts | 1 - .../server-side-rendering/visitors/AwaitBlock.ts | 4 ---- src/generators/server-side-rendering/visitors/EachBlock.ts | 7 +------ 3 files changed, 1 insertion(+), 11 deletions(-) diff --git a/src/generators/server-side-rendering/Block.ts b/src/generators/server-side-rendering/Block.ts index 24ef56785422..5b727dc828ea 100644 --- a/src/generators/server-side-rendering/Block.ts +++ b/src/generators/server-side-rendering/Block.ts @@ -14,7 +14,6 @@ export default class Block { contexts: Map; indexes: Map; - contextDependencies: Map; constructor(options: BlockOptions) { Object.assign(this, options); diff --git a/src/generators/server-side-rendering/visitors/AwaitBlock.ts b/src/generators/server-side-rendering/visitors/AwaitBlock.ts index 6251eb15c037..b24e8b363612 100644 --- a/src/generators/server-side-rendering/visitors/AwaitBlock.ts +++ b/src/generators/server-side-rendering/visitors/AwaitBlock.ts @@ -16,11 +16,7 @@ export default function visitAwaitBlock( const contexts = new Map(block.contexts); contexts.set(node.value, '__value'); - const contextDependencies = new Map(block.contextDependencies); - contextDependencies.set(node.value, dependencies); - const childBlock = block.child({ - contextDependencies, contexts }); diff --git a/src/generators/server-side-rendering/visitors/EachBlock.ts b/src/generators/server-side-rendering/visitors/EachBlock.ts index 491a9fbc1c9e..4818ad548fb9 100644 --- a/src/generators/server-side-rendering/visitors/EachBlock.ts +++ b/src/generators/server-side-rendering/visitors/EachBlock.ts @@ -22,20 +22,15 @@ export default function visitEachBlock( const indexes = new Map(block.indexes); if (node.index) indexes.set(node.index, node.context); - const contextDependencies = new Map(block.contextDependencies); - contextDependencies.set(node.context, dependencies); - if (node.destructuredContexts) { for (let i = 0; i < node.destructuredContexts.length; i += 1) { contexts.set(node.destructuredContexts[i], `${node.context}[${i}]`); - contextDependencies.set(node.destructuredContexts[i], dependencies); } } const childBlock = block.child({ contexts, - indexes, - contextDependencies, + indexes }); node.children.forEach((child: Node) => { From 83e62ea0b9917955928b077a6451a2e6df9a75b8 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sun, 22 Apr 2018 16:00:32 -0400 Subject: [PATCH 03/85] remove more unused references --- src/generators/server-side-rendering/visitors/AwaitBlock.ts | 2 +- src/generators/server-side-rendering/visitors/EachBlock.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/generators/server-side-rendering/visitors/AwaitBlock.ts b/src/generators/server-side-rendering/visitors/AwaitBlock.ts index b24e8b363612..1570647db545 100644 --- a/src/generators/server-side-rendering/visitors/AwaitBlock.ts +++ b/src/generators/server-side-rendering/visitors/AwaitBlock.ts @@ -9,7 +9,7 @@ export default function visitAwaitBlock( node: Node ) { block.contextualise(node.expression); - const { dependencies, snippet } = node.metadata; + const { snippet } = node.metadata; // 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/EachBlock.ts b/src/generators/server-side-rendering/visitors/EachBlock.ts index 4818ad548fb9..eaf43f8bd344 100644 --- a/src/generators/server-side-rendering/visitors/EachBlock.ts +++ b/src/generators/server-side-rendering/visitors/EachBlock.ts @@ -9,7 +9,7 @@ export default function visitEachBlock( node: Node ) { block.contextualise(node.expression); - const { dependencies, snippet } = node.metadata; + const { snippet } = node.metadata; const open = `\${ ${node.else ? `${snippet}.length ? ` : ''}${snippet}.map(${node.index ? `(${node.context}, ${node.index})` : `(${node.context})`} => \``; generator.append(open); From 9ff1beec489033c40d76e52d56669a25fcbdeb85 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sun, 22 Apr 2018 21:09:04 -0400 Subject: [PATCH 04/85] WIP --- src/generators/Generator.ts | 618 +++++++++--------- src/generators/dom/Block.ts | 18 +- src/generators/nodes/Attribute.ts | 66 +- src/generators/nodes/Binding.ts | 52 +- src/generators/nodes/Component.ts | 45 +- src/generators/nodes/EachBlock.ts | 61 +- src/generators/nodes/Element.ts | 308 +++++---- src/generators/nodes/ElseBlock.ts | 7 +- src/generators/nodes/EventHandler.ts | 58 +- src/generators/nodes/Fragment.ts | 6 +- src/generators/nodes/IfBlock.ts | 83 ++- src/generators/nodes/Text.ts | 5 + src/generators/nodes/Transition.ts | 15 +- src/generators/nodes/Window.ts | 151 +++-- src/generators/nodes/shared/Expression.ts | 63 +- src/generators/nodes/shared/Node.ts | 13 +- src/generators/nodes/shared/Tag.ts | 11 +- src/generators/nodes/shared/mapChildren.ts | 8 + src/generators/server-side-rendering/Block.ts | 6 +- src/generators/server-side-rendering/index.ts | 10 +- .../visitors/AwaitBlock.ts | 3 +- .../visitors/Component.ts | 3 +- .../server-side-rendering/visitors/IfBlock.ts | 3 +- .../visitors/MustacheTag.ts | 3 +- src/parse/state/tag.ts | 2 +- src/utils/addToSet.ts | 5 + src/utils/annotateWithScopes.ts | 48 ++ src/utils/createDebuggingComment.ts | 25 +- test/runtime/index.js | 2 +- .../samples/attribute-dynamic/_config.js | 1 - .../samples/binding-input-text/_config.js | 33 +- test/runtime/samples/trait-function/main.html | 46 -- 32 files changed, 998 insertions(+), 780 deletions(-) create mode 100644 src/utils/addToSet.ts delete mode 100644 test/runtime/samples/trait-function/main.html diff --git a/src/generators/Generator.ts b/src/generators/Generator.ts index 0aa96ca9d217..c91c36caf2f2 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 edf505c7b359..f554d4909738 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 074cc18352a4..e44aad898c48 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 1ac182102b2e..4a452d6ee693 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 a1861664ebbe..d5512429a4e5 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 e74b8a01c2da..3d7ca27469d2 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 eb6c01e351d8..7e848a4c1e0d 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 —