From a8eaa7e95c74208907a7f10317ab38c96a5d5b09 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sun, 10 Dec 2017 09:40:45 -0500 Subject: [PATCH] refactor --- src/css/Stylesheet.ts | 13 +- src/generators/Generator.ts | 59 +- src/generators/dom/Block.ts | 24 +- src/generators/dom/index.ts | 9 +- src/generators/dom/interfaces.ts | 23 - src/generators/dom/preprocess.ts | 607 -------------- src/generators/dom/visit.ts | 17 - src/generators/dom/visitors/AwaitBlock.ts | 159 ---- src/generators/dom/visitors/Component.ts | 588 -------------- src/generators/dom/visitors/EachBlock.ts | 491 ------------ .../dom/visitors/Element/Attribute.ts | 237 ------ .../dom/visitors/Element/Element.ts | 365 --------- .../dom/visitors/Element/StyleAttribute.ts | 189 ----- .../dom/visitors/Element/addBindings.ts | 428 ---------- .../dom/visitors/Element/addTransitions.ts | 98 --- src/generators/dom/visitors/Element/lookup.ts | 236 ------ .../dom/visitors/Element/meta/Window.ts | 190 ----- src/generators/dom/visitors/MustacheTag.ts | 29 - src/generators/dom/visitors/RawMustacheTag.ts | 91 --- src/generators/dom/visitors/Slot.ts | 104 --- src/generators/dom/visitors/Text.ts | 21 - src/generators/dom/visitors/index.ts | 20 - src/generators/dom/visitors/shared/Tag.ts | 50 -- .../dom/visitors/shared/isDomNode.ts | 7 - src/generators/nodes/Attribute.ts | 662 ++++++++++++++++ src/generators/nodes/AwaitBlock.ts | 200 +++++ src/generators/nodes/Binding.ts | 270 +++++++ src/generators/nodes/CatchBlock.ts | 7 + src/generators/nodes/Comment.ts | 5 + src/generators/nodes/Component.ts | 603 ++++++++++++++ src/generators/nodes/EachBlock.ts | 580 ++++++++++++++ src/generators/nodes/Element.ts | 744 ++++++++++++++++++ src/generators/nodes/ElseBlock.ts | 8 + src/generators/nodes/EventHandler.ts | 7 + src/generators/nodes/Fragment.ts | 39 + .../{dom/visitors => nodes}/IfBlock.ts | 303 ++++--- src/generators/nodes/MustacheTag.ts | 29 + src/generators/nodes/PendingBlock.ts | 7 + src/generators/nodes/RawMustacheTag.ts | 92 +++ src/generators/nodes/Ref.ts | 7 + src/generators/nodes/Slot.ts | 139 ++++ src/generators/nodes/Text.ts | 50 ++ src/generators/nodes/ThenBlock.ts | 7 + src/generators/nodes/Transition.ts | 7 + src/generators/nodes/Window.ts | 201 +++++ src/generators/nodes/index.ts | 48 ++ src/generators/nodes/shared/Node.ts | 164 ++++ src/generators/nodes/shared/Tag.ts | 45 ++ src/generators/server-side-rendering/Block.ts | 7 +- src/generators/server-side-rendering/index.ts | 3 - .../server-side-rendering/interfaces.ts | 3 +- .../server-side-rendering/preprocess.ts | 110 --- .../server-side-rendering/visitors/Element.ts | 20 +- .../server-side-rendering/visitors/Slot.ts | 1 - .../visitors/{meta => }/Window.ts | 0 .../server-side-rendering/visitors/index.ts | 8 +- .../shared/utils/isChildOfComponent.ts | 10 - src/generators/shared/utils/walkHtml.ts | 20 - .../{shared/utils => }/wrapModule.ts | 6 +- src/utils/createDebuggingComment.ts | 21 + 60 files changed, 4215 insertions(+), 4273 deletions(-) delete mode 100644 src/generators/dom/interfaces.ts delete mode 100644 src/generators/dom/preprocess.ts delete mode 100644 src/generators/dom/visit.ts delete mode 100644 src/generators/dom/visitors/AwaitBlock.ts delete mode 100644 src/generators/dom/visitors/Component.ts delete mode 100644 src/generators/dom/visitors/EachBlock.ts delete mode 100644 src/generators/dom/visitors/Element/Attribute.ts delete mode 100644 src/generators/dom/visitors/Element/Element.ts delete mode 100644 src/generators/dom/visitors/Element/StyleAttribute.ts delete mode 100644 src/generators/dom/visitors/Element/addBindings.ts delete mode 100644 src/generators/dom/visitors/Element/addTransitions.ts delete mode 100644 src/generators/dom/visitors/Element/lookup.ts delete mode 100644 src/generators/dom/visitors/Element/meta/Window.ts delete mode 100644 src/generators/dom/visitors/MustacheTag.ts delete mode 100644 src/generators/dom/visitors/RawMustacheTag.ts delete mode 100644 src/generators/dom/visitors/Slot.ts delete mode 100644 src/generators/dom/visitors/Text.ts delete mode 100644 src/generators/dom/visitors/index.ts delete mode 100644 src/generators/dom/visitors/shared/Tag.ts delete mode 100644 src/generators/dom/visitors/shared/isDomNode.ts create mode 100644 src/generators/nodes/Attribute.ts create mode 100644 src/generators/nodes/AwaitBlock.ts create mode 100644 src/generators/nodes/Binding.ts create mode 100644 src/generators/nodes/CatchBlock.ts create mode 100644 src/generators/nodes/Comment.ts create mode 100644 src/generators/nodes/Component.ts create mode 100644 src/generators/nodes/EachBlock.ts create mode 100644 src/generators/nodes/Element.ts create mode 100644 src/generators/nodes/ElseBlock.ts create mode 100644 src/generators/nodes/EventHandler.ts create mode 100644 src/generators/nodes/Fragment.ts rename src/generators/{dom/visitors => nodes}/IfBlock.ts (57%) create mode 100644 src/generators/nodes/MustacheTag.ts create mode 100644 src/generators/nodes/PendingBlock.ts create mode 100644 src/generators/nodes/RawMustacheTag.ts create mode 100644 src/generators/nodes/Ref.ts create mode 100644 src/generators/nodes/Slot.ts create mode 100644 src/generators/nodes/Text.ts create mode 100644 src/generators/nodes/ThenBlock.ts create mode 100644 src/generators/nodes/Transition.ts create mode 100644 src/generators/nodes/Window.ts create mode 100644 src/generators/nodes/index.ts create mode 100644 src/generators/nodes/shared/Node.ts create mode 100644 src/generators/nodes/shared/Tag.ts delete mode 100644 src/generators/server-side-rendering/preprocess.ts rename src/generators/server-side-rendering/visitors/{meta => }/Window.ts (100%) delete mode 100644 src/generators/shared/utils/isChildOfComponent.ts delete mode 100644 src/generators/shared/utils/walkHtml.ts rename src/generators/{shared/utils => }/wrapModule.ts (97%) create mode 100644 src/utils/createDebuggingComment.ts diff --git a/src/css/Stylesheet.ts b/src/css/Stylesheet.ts index cb83804f1d..e94dce6a62 100644 --- a/src/css/Stylesheet.ts +++ b/src/css/Stylesheet.ts @@ -3,6 +3,7 @@ import { walk } from 'estree-walker'; import { getLocator } from 'locate-character'; import Selector from './Selector'; import getCodeFrame from '../utils/getCodeFrame'; +import Element from '../generators/nodes/Element'; import { Validator } from '../validate/index'; import { Node, Parsed, Warning } from '../interfaces'; @@ -19,7 +20,7 @@ class Rule { this.declarations = node.block.children.map((node: Node) => new Declaration(node)); } - apply(node: Node, stack: Node[]) { + apply(node: Element, stack: Element[]) { this.selectors.forEach(selector => selector.apply(node, stack)); // TODO move the logic in here? } @@ -159,7 +160,7 @@ class Atrule { this.children = []; } - apply(node: Node, stack: Node[]) { + apply(node: Element, stack: Element[]) { if (this.node.name === 'media') { this.children.forEach(child => { child.apply(node, stack); @@ -330,9 +331,15 @@ export default class Stylesheet { } } - apply(node: Node, stack: Node[]) { + apply(node: Element) { if (!this.hasStyles) return; + const stack: Element[] = []; + let parent: Node = node; + while (parent = parent.parent) { + if (parent.type === 'Element') stack.unshift(parent); + } + if (this.cascade) { if (stack.length === 0) node._needsCssAttribute = true; return; diff --git a/src/generators/Generator.ts b/src/generators/Generator.ts index 65574ee303..2285c1185b 100644 --- a/src/generators/Generator.ts +++ b/src/generators/Generator.ts @@ -9,14 +9,13 @@ import flattenReference from '../utils/flattenReference'; import reservedNames from '../utils/reservedNames'; import namespaces from '../utils/namespaces'; import { removeNode, removeObjectKey } from '../utils/removeNode'; -import wrapModule from './shared/utils/wrapModule'; +import wrapModule from './wrapModule'; import annotateWithScopes, { Scope } from '../utils/annotateWithScopes'; import getName from '../utils/getName'; import clone from '../utils/clone'; -import DomBlock from './dom/Block'; -import SsrBlock from './server-side-rendering/Block'; import Stylesheet from '../css/Stylesheet'; import { test } from '../config'; +import nodes from './nodes/index'; import { Node, GenerateOptions, Parsed, CompileOptions, CustomElementOptions } from '../interfaces'; interface Computation { @@ -162,11 +161,9 @@ export default class Generator { this.computations = []; this.templateProperties = {}; + this.name = this.alias(name); this.walkJs(dom); - this.walkTemplate(); - - this.name = this.alias(name); if (options.customElement === true) { this.customElement = { @@ -180,6 +177,8 @@ export default class Generator { if (this.customElement && !this.customElement.tag) { throw new Error(`No tag name specified`); // TODO better error } + + this.walkTemplate(); } addSourcemapLocations(node: Node) { @@ -200,7 +199,8 @@ export default class Generator { } contextualise( - block: DomBlock | SsrBlock, + contexts: Map, + indexes: Map, expression: Node, context: string, isEventHandler: boolean @@ -214,7 +214,6 @@ export default class Generator { const usedIndexes: Set = new Set(); const { code, helpers } = this; - const { contexts, indexes } = block; let scope: Scope; let lexicalDepth = 0; @@ -638,6 +637,7 @@ export default class Generator { } walkTemplate() { + const generator = this; const { code, expectedProperties, @@ -703,7 +703,30 @@ export default class Generator { const indexesStack: Set[] = [indexes]; walk(html, { - enter(node: Node, parent: Node) { + 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 === ':Component' || node.name === ':Self' || generator.components.has(node.name))) { + node.type = 'Component'; + node.__proto__ = nodes.Component.prototype; + } else if (node.name === ':Window') { // TODO do this in parse? + node.type = 'Window'; + node.__proto__ = nodes.Window.prototype; + } else if (node.type === 'Element' && node.name === 'slot' && !generator.customElement) { + node.type = 'Slot'; + node.__proto__ = nodes.Slot.prototype; + } else if (node.type in nodes) { + node.__proto__ = nodes[node.type].prototype; + } + + if (node.type === 'Element') { + generator.stylesheet.apply(node); + } + if (node.type === 'EachBlock') { node.metadata = contextualise(node.expression, contextDependencies, indexes); @@ -764,7 +787,7 @@ export default class Generator { this.skip(); } - if (node.type === 'Element' && node.name === ':Component') { + if (node.type === 'Component' && node.name === ':Component') { node.metadata = contextualise(node.expression, contextDependencies, indexes); } }, @@ -779,6 +802,22 @@ export default class Generator { 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 51ce51c29f..4d4413d8c8 100644 --- a/src/generators/dom/Block.ts +++ b/src/generators/dom/Block.ts @@ -130,15 +130,14 @@ export default class Block { claimStatement: string, parentNode: string ) { - const isToplevel = !parentNode; - this.addVariable(name); this.builders.create.addLine(`${name} = ${renderStatement};`); this.builders.claim.addLine(`${name} = ${claimStatement};`); - this.mount(name, parentNode); - - if (isToplevel) { + if (parentNode) { + this.builders.mount.addLine(`@appendNode(${name}, ${parentNode});`); + } else { + this.builders.mount.addLine(`@insertNode(${name}, #target, anchor);`); this.builders.unmount.addLine(`@detachNode(${name});`); } } @@ -166,20 +165,7 @@ export default class Block { } contextualise(expression: Node, context?: string, isEventHandler?: boolean) { - return this.generator.contextualise( - this, - expression, - context, - isEventHandler - ); - } - - mount(name: string, parentNode: string) { - if (parentNode) { - this.builders.mount.addLine(`@appendNode(${name}, ${parentNode});`); - } else { - this.builders.mount.addLine(`@insertNode(${name}, #target, anchor);`); - } + return this.generator.contextualise(this.contexts, this.indexes, expression, context, isEventHandler); } toString() { diff --git a/src/generators/dom/index.ts b/src/generators/dom/index.ts index c6d9e94df2..9566ba936f 100644 --- a/src/generators/dom/index.ts +++ b/src/generators/dom/index.ts @@ -8,11 +8,9 @@ import { stringify, escape } from '../../utils/stringify'; import CodeBuilder from '../../utils/CodeBuilder'; import globalWhitelist from '../../utils/globalWhitelist'; import reservedNames from '../../utils/reservedNames'; -import visit from './visit'; import shared from './shared'; import Generator from '../Generator'; import Stylesheet from '../../css/Stylesheet'; -import preprocess from './preprocess'; import Block from './Block'; import { test } from '../../config'; import { Parsed, CompileOptions, Node } from '../../interfaces'; @@ -96,14 +94,11 @@ export default function dom( namespace, } = generator; - const { block, state } = preprocess(generator, namespace, parsed.html); + parsed.html.build(); + const { block } = parsed.html; generator.stylesheet.warnOnUnusedSelectors(options.onwarn); - parsed.html.children.forEach((node: Node) => { - visit(generator, block, state, node, [], []); - }); - const builder = new CodeBuilder(); const computationBuilder = new CodeBuilder(); const computationDeps = new Set(); diff --git a/src/generators/dom/interfaces.ts b/src/generators/dom/interfaces.ts deleted file mode 100644 index a925fb1b91..0000000000 --- a/src/generators/dom/interfaces.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { DomGenerator } from './index'; -import Block from './Block'; -import { Node } from '../../interfaces'; - -export interface State { - namespace: string; - parentNode: string; - parentNodes: string; - parentNodeName?: string; - inEachBlock?: boolean; - allUsedContexts?: string[]; - usesComponent?: boolean; - selectBindingDependencies?: string[]; -} - -export type Visitor = ( - generator: DomGenerator, - block: Block, - state: State, - node: Node, - elementStack: Node[], - componentStack: Node[] -) => void; \ No newline at end of file diff --git a/src/generators/dom/preprocess.ts b/src/generators/dom/preprocess.ts deleted file mode 100644 index 89af90a36e..0000000000 --- a/src/generators/dom/preprocess.ts +++ /dev/null @@ -1,607 +0,0 @@ -import Block from './Block'; -import { trimStart, trimEnd } from '../../utils/trim'; -import { assign } from '../../shared/index.js'; -import getStaticAttributeValue from '../../utils/getStaticAttributeValue'; -import isChildOfComponent from '../shared/utils/isChildOfComponent'; -import { DomGenerator } from './index'; -import { Node } from '../../interfaces'; -import { State } from './interfaces'; - -function isElseIf(node: Node) { - return ( - node && node.children.length === 1 && node.children[0].type === 'IfBlock' - ); -} - -function getChildState(parent: State, child = {}) { - return assign( - {}, - parent, - { parentNode: null, parentNodes: 'nodes' }, - child || {} - ); -} - -function createDebuggingComment(node: Node, generator: DomGenerator) { - const { locate, source } = generator; - - let c = node.start; - if (node.type === 'ElseBlock') { - while (source[c] !== '{') c -= 1; - c -= 1; - } - - let d = node.expression ? node.expression.end : c; - while (source[d] !== '}') d += 1; - d += 2; - - const start = locate(c); - const loc = `(${start.line + 1}:${start.column})`; - - return `${loc} ${source.slice(c, d)}`.replace(/\n/g, ' '); -} - -function cannotUseInnerHTML(node: Node) { - while (node && node.canUseInnerHTML) { - node.canUseInnerHTML = false; - node = node.parent; - } -} - -// Whitespace inside one of these elements will not result in -// a whitespace node being created in any circumstances. (This -// list is almost certainly very incomplete) -const elementsWithoutText = new Set([ - 'audio', - 'datalist', - 'dl', - 'ol', - 'optgroup', - 'select', - 'ul', - 'video', -]); - -const preprocessors = { - MustacheTag: ( - generator: DomGenerator, - block: Block, - state: State, - node: Node, - elementStack: Node[], - componentStack: Node[], - stripWhitespace: boolean - ) => { - cannotUseInnerHTML(node); - node.var = block.getUniqueName('text'); - block.addDependencies(node.metadata.dependencies); - }, - - RawMustacheTag: ( - generator: DomGenerator, - block: Block, - state: State, - node: Node, - elementStack: Node[], - componentStack: Node[], - stripWhitespace: boolean - ) => { - cannotUseInnerHTML(node); - node.var = block.getUniqueName('raw'); - block.addDependencies(node.metadata.dependencies); - }, - - Text: ( - generator: DomGenerator, - block: Block, - state: State, - node: Node, - elementStack: Node[], - componentStack: Node[], - stripWhitespace: boolean - ) => { - if (!/\S/.test(node.data) && (state.namespace || elementsWithoutText.has(state.parentNodeName))) { - node.shouldSkip = true; - return; - } - - node.var = block.getUniqueName(`text`); - }, - - AwaitBlock: ( - generator: DomGenerator, - block: Block, - state: State, - node: Node, - inEachBlock: boolean, - elementStack: Node[], - componentStack: Node[], - stripWhitespace: boolean, - nextSibling: Node - ) => { - cannotUseInnerHTML(node); - - node.var = block.getUniqueName('await_block'); - block.addDependencies(node.metadata.dependencies); - - let dynamic = false; - - [ - ['pending', null], - ['then', node.value], - ['catch', node.error] - ].forEach(([status, arg]) => { - const child = node[status]; - - const context = block.getUniqueName(arg || '_'); - const contexts = new Map(block.contexts); - contexts.set(arg, context); - - child._block = block.child({ - comment: createDebuggingComment(child, generator), - name: generator.getUniqueName(`create_${status}_block`), - params: block.params.concat(context), - context, - contexts - }); - - child._state = getChildState(state); - - preprocessChildren(generator, child._block, child._state, child, inEachBlock, elementStack, componentStack, stripWhitespace, nextSibling); - generator.blocks.push(child._block); - - if (child._block.dependencies.size > 0) { - dynamic = true; - block.addDependencies(child._block.dependencies); - } - }); - - node.pending._block.hasUpdateMethod = dynamic; - node.then._block.hasUpdateMethod = dynamic; - node.catch._block.hasUpdateMethod = dynamic; - }, - - IfBlock: ( - generator: DomGenerator, - block: Block, - state: State, - node: Node, - inEachBlock: boolean, - elementStack: Node[], - componentStack: Node[], - stripWhitespace: boolean, - nextSibling: Node - ) => { - cannotUseInnerHTML(node); - - const blocks: Block[] = []; - let dynamic = false; - let hasIntros = false; - let hasOutros = false; - - function attachBlocks(node: Node) { - node.var = block.getUniqueName(`if_block`); - - block.addDependencies(node.metadata.dependencies); - - node._block = block.child({ - comment: createDebuggingComment(node, generator), - name: generator.getUniqueName(`create_if_block`), - }); - - node._state = getChildState(state); - - blocks.push(node._block); - preprocessChildren(generator, node._block, node._state, node, inEachBlock, elementStack, componentStack, stripWhitespace, nextSibling); - - if (node._block.dependencies.size > 0) { - dynamic = true; - block.addDependencies(node._block.dependencies); - } - - if (node._block.hasIntroMethod) hasIntros = true; - if (node._block.hasOutroMethod) hasOutros = true; - - if (isElseIf(node.else)) { - attachBlocks(node.else.children[0]); - } else if (node.else) { - node.else._block = block.child({ - comment: createDebuggingComment(node.else, generator), - name: generator.getUniqueName(`create_if_block`), - }); - - node.else._state = getChildState(state); - - blocks.push(node.else._block); - preprocessChildren( - generator, - node.else._block, - node.else._state, - node.else, - inEachBlock, - elementStack, - componentStack, - stripWhitespace, - nextSibling - ); - - if (node.else._block.dependencies.size > 0) { - dynamic = true; - block.addDependencies(node.else._block.dependencies); - } - } - } - - attachBlocks(node); - - blocks.forEach(block => { - block.hasUpdateMethod = dynamic; - block.hasIntroMethod = hasIntros; - block.hasOutroMethod = hasOutros; - }); - - generator.blocks.push(...blocks); - }, - - EachBlock: ( - generator: DomGenerator, - block: Block, - state: State, - node: Node, - inEachBlock: boolean, - elementStack: Node[], - componentStack: Node[], - stripWhitespace: boolean, - nextSibling: Node - ) => { - cannotUseInnerHTML(node); - node.var = block.getUniqueName(`each`); - node.iterations = block.getUniqueName(`${node.var}_blocks`); - - const { dependencies } = node.metadata; - block.addDependencies(dependencies); - - const indexNames = new Map(block.indexNames); - const indexName = - node.index || block.getUniqueName(`${node.context}_index`); - indexNames.set(node.context, indexName); - - const listNames = new Map(block.listNames); - const listName = block.getUniqueName( - (node.expression.type === 'MemberExpression' && !node.expression.computed) ? node.expression.property.name : - node.expression.type === 'Identifier' ? node.expression.name : - `each_value` - ); - listNames.set(node.context, listName); - - const context = block.getUniqueName(node.context); - const contexts = new Map(block.contexts); - contexts.set(node.context, context); - - const indexes = new Map(block.indexes); - if (node.index) indexes.set(node.index, node.context); - - const changeableIndexes = new Map(block.changeableIndexes); - if (node.index) changeableIndexes.set(node.index, node.key); - - if (node.destructuredContexts) { - for (let i = 0; i < node.destructuredContexts.length; i += 1) { - contexts.set(node.destructuredContexts[i], `${context}[${i}]`); - } - } - - node._block = block.child({ - comment: createDebuggingComment(node, generator), - name: generator.getUniqueName('create_each_block'), - context: node.context, - key: node.key, - - contexts, - indexes, - changeableIndexes, - - listName, - indexName, - - indexNames, - listNames, - params: block.params.concat(listName, context, indexName), - }); - - node._state = getChildState(state, { - inEachBlock: true, - }); - - generator.blocks.push(node._block); - preprocessChildren(generator, node._block, node._state, node, true, elementStack, componentStack, stripWhitespace, nextSibling); - block.addDependencies(node._block.dependencies); - node._block.hasUpdateMethod = node._block.dependencies.size > 0; - - if (node.else) { - node.else._block = block.child({ - comment: createDebuggingComment(node.else, generator), - name: generator.getUniqueName(`${node._block.name}_else`), - }); - - node.else._state = getChildState(state); - - generator.blocks.push(node.else._block); - preprocessChildren( - generator, - node.else._block, - node.else._state, - node.else, - inEachBlock, - elementStack, - componentStack, - stripWhitespace, - nextSibling - ); - node.else._block.hasUpdateMethod = node.else._block.dependencies.size > 0; - } - }, - - Element: ( - generator: DomGenerator, - block: Block, - state: State, - node: Node, - inEachBlock: boolean, - elementStack: Node[], - componentStack: Node[], - stripWhitespace: boolean, - nextSibling: Node - ) => { - if (node.name === 'slot' || node.name === 'option') { - cannotUseInnerHTML(node); - } - - node.attributes.forEach((attribute: Node) => { - if (attribute.type === 'Attribute' && attribute.value !== true) { - attribute.value.forEach((chunk: Node) => { - if (chunk.type !== 'Text') { - if (node.parent) cannotUseInnerHTML(node.parent); - - const dependencies = chunk.metadata.dependencies; - block.addDependencies(dependencies); - - // special case — - // - if (node.name === 'option' && !valueAttribute) { - node.attributes.push({ - type: 'Attribute', - name: 'value', - value: node.children - }); - } - - // special case — in a case like this... - // - // `? - const dependencies = binding.metadata.dependencies; - state.selectBindingDependencies = dependencies; - dependencies.forEach((prop: string) => { - generator.indirectDependencies.set(prop, new Set()); - }); - } else { - state.selectBindingDependencies = null; - } - } - - const isComponent = - generator.components.has(node.name) || node.name === ':Self' || node.name === ':Component'; - - if (isComponent) { - cannotUseInnerHTML(node); - - node.var = block.getUniqueName( - ( - node.name === ':Self' ? generator.name : - node.name === ':Component' ? 'switch_instance' : - node.name - ).toLowerCase() - ); - - node._state = getChildState(state, { - parentNode: `${node.var}._slotted.default` - }); - } else { - const slot = getStaticAttributeValue(node, 'slot'); - if (slot && isChildOfComponent(node, generator)) { - cannotUseInnerHTML(node); - node.slotted = true; - // TODO validate slots — no nesting, no dynamic names... - const component = componentStack[componentStack.length - 1]; - component._slots.add(slot); - } - - node.var = block.getUniqueName( - node.name.replace(/[^a-zA-Z0-9_$]/g, '_') - ); - - node._state = getChildState(state, { - parentNode: node.var, - parentNodes: block.getUniqueName(`${node.var}_nodes`), - parentNodeName: node.name, - namespace: node.name === 'svg' - ? 'http://www.w3.org/2000/svg' - : state.namespace, - allUsedContexts: [], - }); - - generator.stylesheet.apply(node, elementStack); - } - - if (node.children.length) { - if (isComponent) { - if (node.children) node._slots = new Set(['default']); - preprocessChildren(generator, block, node._state, node, inEachBlock, elementStack, componentStack.concat(node), stripWhitespace, nextSibling); - } else { - if (node.name === 'pre' || node.name === 'textarea') stripWhitespace = false; - preprocessChildren(generator, block, node._state, node, inEachBlock, elementStack.concat(node), componentStack, stripWhitespace, nextSibling); - } - } - }, -}; - -function preprocessChildren( - generator: DomGenerator, - block: Block, - state: State, - node: Node, - inEachBlock: boolean, - elementStack: Node[], - componentStack: Node[], - stripWhitespace: boolean, - nextSibling: Node -) { - // glue text nodes together - const cleaned: Node[] = []; - let lastChild: Node; - - let windowComponent; - - node.children.forEach((child: Node) => { - if (child.type === 'Comment') return; - - // special case — this is an easy way to remove whitespace surrounding - // <:Window/>. lil hacky but it works - if (child.type === 'Element' && child.name === ':Window') { - windowComponent = child; - return; - } - - if (child.type === 'Text' && lastChild && lastChild.type === 'Text') { - lastChild.data += child.data; - lastChild.end = child.end; - } else { - if (child.type === 'Text' && stripWhitespace && cleaned.length === 0) { - child.data = trimStart(child.data); - if (child.data) cleaned.push(child); - } else { - cleaned.push(child); - } - } - - lastChild = child; - }); - - lastChild = null; - - cleaned.forEach((child: Node, i: number) => { - child.parent = node; - child.canUseInnerHTML = !generator.hydratable; - - const preprocessor = preprocessors[child.type]; - if (preprocessor) preprocessor(generator, block, state, child, inEachBlock, elementStack, componentStack, stripWhitespace, cleaned[i + 1] || nextSibling); - - if (child.shouldSkip) return; - - if (lastChild) lastChild.next = child; - child.prev = lastChild; - - lastChild = child; - }); - - // We want to remove trailing whitespace inside an element/component/block, - // *unless* there is no whitespace between this node and its next sibling - if (stripWhitespace && lastChild && lastChild.type === 'Text') { - const shouldTrim = ( - nextSibling ? (nextSibling.type === 'Text' && /^\s/.test(nextSibling.data)) : !inEachBlock - ); - - if (shouldTrim) { - lastChild.data = trimEnd(lastChild.data); - if (!lastChild.data) { - cleaned.pop(); - lastChild = cleaned[cleaned.length - 1]; - lastChild.next = null; - } - } - } - - node.children = cleaned; - if (windowComponent) cleaned.unshift(windowComponent); -} - -export default function preprocess( - generator: DomGenerator, - namespace: string, - node: Node -) { - const block = new Block({ - generator, - name: '@create_main_fragment', - key: null, - - contexts: new Map(), - indexes: new Map(), - changeableIndexes: new Map(), - - params: ['state'], - indexNames: new Map(), - listNames: new Map(), - - dependencies: new Set(), - }); - - const state: State = { - namespace, - parentNode: null, - parentNodes: 'nodes' - }; - - generator.blocks.push(block); - preprocessChildren(generator, block, state, node, false, [], [], true, null); - block.hasUpdateMethod = true; - - return { block, state }; -} diff --git a/src/generators/dom/visit.ts b/src/generators/dom/visit.ts deleted file mode 100644 index c5e8f0bed4..0000000000 --- a/src/generators/dom/visit.ts +++ /dev/null @@ -1,17 +0,0 @@ -import visitors from './visitors/index'; -import { DomGenerator } from './index'; -import Block from './Block'; -import { Node } from '../../interfaces'; -import { State } from './interfaces'; - -export default function visit( - generator: DomGenerator, - block: Block, - state: State, - node: Node, - elementStack: Node[], - componentStack: Node[] -) { - const visitor = visitors[node.type]; - visitor(generator, block, state, node, elementStack, componentStack); -} diff --git a/src/generators/dom/visitors/AwaitBlock.ts b/src/generators/dom/visitors/AwaitBlock.ts deleted file mode 100644 index 6e014af289..0000000000 --- a/src/generators/dom/visitors/AwaitBlock.ts +++ /dev/null @@ -1,159 +0,0 @@ -import deindent from '../../../utils/deindent'; -import visit from '../visit'; -import { DomGenerator } from '../index'; -import Block from '../Block'; -import isDomNode from './shared/isDomNode'; -import { Node } from '../../../interfaces'; -import { State } from '../interfaces'; - -export default function visitAwaitBlock( - generator: DomGenerator, - block: Block, - state: State, - node: Node, - elementStack: Node[], - componentStack: Node[] -) { - const name = node.var; - - const needsAnchor = node.next ? !isDomNode(node.next, generator) : !state.parentNode || !isDomNode(node.parent, generator); - const anchor = needsAnchor - ? block.getUniqueName(`${name}_anchor`) - : (node.next && node.next.var) || 'null'; - - const params = block.params.join(', '); - - block.contextualise(node.expression); - const { snippet } = node.metadata; - - if (needsAnchor) { - block.addElement( - anchor, - `@createComment()`, - `@createComment()`, - state.parentNode - ); - } - - const promise = block.getUniqueName(`promise`); - const resolved = block.getUniqueName(`resolved`); - const await_block = block.getUniqueName(`await_block`); - const await_block_type = block.getUniqueName(`await_block_type`); - const token = block.getUniqueName(`token`); - const await_token = block.getUniqueName(`await_token`); - const handle_promise = block.getUniqueName(`handle_promise`); - const replace_await_block = block.getUniqueName(`replace_await_block`); - const old_block = block.getUniqueName(`old_block`); - const value = block.getUniqueName(`value`); - const error = block.getUniqueName(`error`); - const create_pending_block = node.pending._block.name; - const create_then_block = node.then._block.name; - const create_catch_block = node.catch._block.name; - - block.addVariable(await_block); - block.addVariable(await_block_type); - block.addVariable(await_token); - block.addVariable(promise); - block.addVariable(resolved); - - block.builders.init.addBlock(deindent` - function ${replace_await_block}(${token}, type, ${value}, ${params}) { - if (${token} !== ${await_token}) return; - - var ${old_block} = ${await_block}; - ${await_block} = (${await_block_type} = type)(${params}, ${resolved} = ${value}, #component); - - if (${old_block}) { - ${old_block}.u(); - ${old_block}.d(); - ${await_block}.c(); - ${await_block}.m(${state.parentNode || `${anchor}.parentNode`}, ${anchor}); - } - } - - function ${handle_promise}(${promise}, ${params}) { - var ${token} = ${await_token} = {}; - - if (@isPromise(${promise})) { - ${promise}.then(function(${value}) { - ${replace_await_block}(${token}, ${create_then_block}, ${value}, ${params}); - }, function (${error}) { - ${replace_await_block}(${token}, ${create_catch_block}, ${error}, ${params}); - }); - - // if we previously had a then/catch block, destroy it - if (${await_block_type} !== ${create_pending_block}) { - ${replace_await_block}(${token}, ${create_pending_block}, null, ${params}); - return true; - } - } else { - ${resolved} = ${promise}; - if (${await_block_type} !== ${create_then_block}) { - ${replace_await_block}(${token}, ${create_then_block}, ${resolved}, ${params}); - return true; - } - } - } - - ${handle_promise}(${promise} = ${snippet}, ${params}); - `); - - block.builders.create.addBlock(deindent` - ${await_block}.c(); - `); - - block.builders.claim.addBlock(deindent` - ${await_block}.l(${state.parentNodes}); - `); - - const targetNode = state.parentNode || '#target'; - const anchorNode = state.parentNode ? 'null' : 'anchor'; - - block.builders.mount.addBlock(deindent` - ${await_block}.m(${targetNode}, ${anchorNode}); - `); - - const conditions = []; - if (node.metadata.dependencies) { - conditions.push( - `(${node.metadata.dependencies.map(dep => `'${dep}' in changed`).join(' || ')})` - ); - } - - conditions.push( - `${promise} !== (${promise} = ${snippet})`, - `${handle_promise}(${promise}, ${params})` - ); - - if (node.pending._block.hasUpdateMethod) { - block.builders.update.addBlock(deindent` - if (${conditions.join(' && ')}) { - // nothing - } else { - ${await_block}.p(changed, ${params}, ${resolved}); - } - `); - } else { - block.builders.update.addBlock(deindent` - if (${conditions.join(' && ')}) { - ${await_block}.c(); - ${await_block}.m(${anchor}.parentNode, ${anchor}); - } - `); - } - - block.builders.unmount.addBlock(deindent` - ${await_block}.u(); - `); - - block.builders.destroy.addBlock(deindent` - ${await_token} = null; - ${await_block}.d(); - `); - - [node.pending, node.then, node.catch].forEach(status => { - status.children.forEach(child => { - visit(generator, status._block, status._state, child, elementStack, componentStack); - }); - }); -} \ No newline at end of file diff --git a/src/generators/dom/visitors/Component.ts b/src/generators/dom/visitors/Component.ts deleted file mode 100644 index f3487b5a5c..0000000000 --- a/src/generators/dom/visitors/Component.ts +++ /dev/null @@ -1,588 +0,0 @@ -import deindent from '../../../utils/deindent'; -import CodeBuilder from '../../../utils/CodeBuilder'; -import visit from '../visit'; -import { DomGenerator } from '../index'; -import Block from '../Block'; -import isDomNode from './shared/isDomNode'; -import getTailSnippet from '../../../utils/getTailSnippet'; -import getObject from '../../../utils/getObject'; -import getExpressionPrecedence from '../../../utils/getExpressionPrecedence'; -import getStaticAttributeValue from '../../../utils/getStaticAttributeValue'; -import { stringify } from '../../../utils/stringify'; -import stringifyProps from '../../../utils/stringifyProps'; -import { Node } from '../../../interfaces'; -import { State } from '../interfaces'; - -interface Attribute { - name: string; - value: any; - dynamic: boolean; - dependencies?: string[] -} - -interface Binding { - name: string; - value: Node; - contexts: Set; - snippet: string; - obj: string; - prop: string; - dependencies: string[]; -} - -export default function visitComponent( - generator: DomGenerator, - block: Block, - state: State, - node: Node, - elementStack: Node[], - componentStack: Node[] -) { - generator.hasComponents = true; - - const name = node.var; - - const componentInitProperties = [`_root: #component._root`]; - - if (node.children.length > 0) { - const slots = Array.from(node._slots).map(name => `${name}: @createFragment()`); - componentInitProperties.push(`slots: { ${slots.join(', ')} }`); - - node.children.forEach((child: Node) => { - visit(generator, block, node._state, child, elementStack, componentStack.concat(node)); - }); - } - - const allContexts = new Set(); - const statements: string[] = []; - const name_context = block.getUniqueName(`${name}_context`); - - let name_updating: string; - let name_initial_data: string; - let beforecreate: string = null; - - const attributes = node.attributes - .filter(a => a.type === 'Attribute') - .map(a => mungeAttribute(a, block)); - - const bindings = node.attributes - .filter(a => a.type === 'Binding') - .map(a => mungeBinding(a, block)); - - const eventHandlers = node.attributes - .filter((a: Node) => a.type === 'EventHandler') - .map(a => mungeEventHandler(generator, node, a, block, name_context, allContexts)); - - const ref = node.attributes.find((a: Node) => a.type === 'Ref'); - if (ref) generator.usesRefs = true; - - const updates: string[] = []; - - if (attributes.length || bindings.length) { - const initialProps = attributes - .map((attribute: Attribute) => `${attribute.name}: ${attribute.value}`); - - const initialPropString = stringifyProps(initialProps); - - attributes - .filter((attribute: Attribute) => attribute.dynamic) - .forEach((attribute: Attribute) => { - if (attribute.dependencies.length) { - updates.push(deindent` - if (${attribute.dependencies - .map(dependency => `changed.${dependency}`) - .join(' || ')}) ${name}_changes.${attribute.name} = ${attribute.value}; - `); - } - - else { - // TODO this is an odd situation to encounter – I *think* it should only happen with - // each block indices, in which case it may be possible to optimise this - updates.push(`${name}_changes.${attribute.name} = ${attribute.value};`); - } - }); - - if (bindings.length) { - generator.hasComplexBindings = true; - - name_updating = block.alias(`${name}_updating`); - name_initial_data = block.getUniqueName(`${name}_initial_data`); - - block.addVariable(name_updating, '{}'); - statements.push(`var ${name_initial_data} = ${initialPropString};`); - - const setParentFromChildOnChange = new CodeBuilder(); - const setParentFromChildOnInit = new CodeBuilder(); - - bindings.forEach((binding: Binding) => { - let setParentFromChild; - - binding.contexts.forEach(context => { - allContexts.add(context); - }); - - const { name: key } = getObject(binding.value); - - if (block.contexts.has(key)) { - const prop = binding.dependencies[0]; - const computed = isComputed(binding.value); - const tail = binding.value.type === 'MemberExpression' ? getTailSnippet(binding.value) : ''; - - setParentFromChild = deindent` - var list = ${name_context}.${block.listNames.get(key)}; - var index = ${name_context}.${block.indexNames.get(key)}; - list[index]${tail} = childState.${binding.name}; - - ${binding.dependencies - .map((prop: string) => `newState.${prop} = state.${prop};`) - .join('\n')} - `; - } - - else if (binding.value.type === 'MemberExpression') { - setParentFromChild = deindent` - ${binding.snippet} = childState.${binding.name}; - ${binding.dependencies.map((prop: string) => `newState.${prop} = state.${prop};`).join('\n')} - `; - } - - else { - setParentFromChild = `newState.${binding.value.name} = childState.${binding.name};`; - } - - statements.push(deindent` - if (${binding.prop} in ${binding.obj}) { - ${name_initial_data}.${binding.name} = ${binding.snippet}; - ${name_updating}.${binding.name} = true; - }` - ); - - setParentFromChildOnChange.addConditional( - `!${name_updating}.${binding.name} && changed.${binding.name}`, - setParentFromChild - ); - - setParentFromChildOnInit.addConditional( - `!${name_updating}.${binding.name}`, - setParentFromChild - ); - - // TODO could binding.dependencies.length ever be 0? - if (binding.dependencies.length) { - updates.push(deindent` - if (!${name_updating}.${binding.name} && ${binding.dependencies.map((dependency: string) => `changed.${dependency}`).join(' || ')}) { - ${name}_changes.${binding.name} = ${binding.snippet}; - ${name_updating}.${binding.name} = true; - } - `); - } - }); - - componentInitProperties.push(`data: ${name_initial_data}`); - - componentInitProperties.push(deindent` - _bind: function(changed, childState) { - var state = #component.get(), newState = {}; - ${setParentFromChildOnChange} - ${name_updating} = @assign({}, changed); - #component._set(newState); - ${name_updating} = {}; - } - `); - - beforecreate = deindent` - #component._root._beforecreate.push(function() { - var state = #component.get(), childState = ${name}.get(), newState = {}; - if (!childState) return; - ${setParentFromChildOnInit} - ${name_updating} = { ${bindings.map((binding: Binding) => `${binding.name}: true`).join(', ')} }; - #component._set(newState); - ${name_updating} = {}; - }); - `; - } else if (initialProps.length) { - componentInitProperties.push(`data: ${initialPropString}`); - } - } - - const isDynamicComponent = node.name === ':Component'; - - const switch_vars = isDynamicComponent && { - value: block.getUniqueName('switch_value'), - props: block.getUniqueName('switch_props') - }; - - const expression = ( - node.name === ':Self' ? generator.name : - isDynamicComponent ? switch_vars.value : - `%components-${node.name}` - ); - - if (isDynamicComponent) { - block.contextualise(node.expression); - const { dependencies, snippet } = node.metadata; - - const needsAnchor = node.next ? !isDomNode(node.next, generator) : !state.parentNode || !isDomNode(node.parent, generator); - const anchor = needsAnchor - ? block.getUniqueName(`${name}_anchor`) - : (node.next && node.next.var) || 'null'; - - if (needsAnchor) { - block.addElement( - anchor, - `@createComment()`, - `@createComment()`, - state.parentNode - ); - } - - const params = block.params.join(', '); - - block.builders.init.addBlock(deindent` - var ${switch_vars.value} = ${snippet}; - - function ${switch_vars.props}(${params}) { - return { - ${componentInitProperties.join(',\n')} - }; - } - - if (${switch_vars.value}) { - ${statements.length > 0 && statements.join('\n')} - var ${name} = new ${expression}(${switch_vars.props}(${params})); - - ${beforecreate} - } - - ${eventHandlers.map(handler => deindent` - function ${handler.var}(event) { - ${handler.body} - } - - if (${name}) ${name}.on("${handler.name}", ${handler.var}); - `)} - `); - - block.builders.create.addLine( - `if (${name}) ${name}._fragment.c();` - ); - - block.builders.claim.addLine( - `if (${name}) ${name}._fragment.l(${state.parentNodes});` - ); - - block.builders.mount.addLine( - `if (${name}) ${name}._mount(${state.parentNode || '#target'}, ${state.parentNode ? 'null' : 'anchor'});` - ); - - block.builders.update.addBlock(deindent` - if (${switch_vars.value} !== (${switch_vars.value} = ${snippet})) { - if (${name}) ${name}.destroy(); - - if (${switch_vars.value}) { - ${name} = new ${switch_vars.value}(${switch_vars.props}(${params})); - ${name}._fragment.c(); - - ${node.children.map(child => remount(generator, child, name))} - ${name}._mount(${anchor}.parentNode, ${anchor}); - - ${eventHandlers.map(handler => deindent` - ${name}.on("${handler.name}", ${handler.var}); - `)} - - ${ref && `#component.refs.${ref.name} = ${name};`} - } - - ${ref && deindent` - else if (#component.refs.${ref.name} === ${name}) { - #component.refs.${ref.name} = null; - }`} - } - `); - - if (updates.length) { - block.builders.update.addBlock(deindent` - else { - var ${name}_changes = {}; - ${updates.join('\n')} - ${name}._set(${name}_changes); - ${bindings.length && `${name_updating} = {};`} - } - `); - } - - if (!state.parentNode) block.builders.unmount.addLine(`if (${name}) ${name}._unmount();`); - - block.builders.destroy.addLine(`if (${name}) ${name}.destroy(false);`); - } else { - block.builders.init.addBlock(deindent` - ${statements.join('\n')} - var ${name} = new ${expression}({ - ${componentInitProperties.join(',\n')} - }); - - ${beforecreate} - - ${eventHandlers.map(handler => deindent` - ${name}.on("${handler.name}", function(event) { - ${handler.body} - }); - `)} - - ${ref && `#component.refs.${ref.name} = ${name};`} - `); - - block.builders.create.addLine(`${name}._fragment.c();`); - - block.builders.claim.addLine( - `${name}._fragment.l(${state.parentNodes});` - ); - - block.builders.mount.addLine( - `${name}._mount(${state.parentNode || '#target'}, ${state.parentNode ? 'null' : 'anchor'});` - ); - - if (updates.length) { - block.builders.update.addBlock(deindent` - var ${name}_changes = {}; - ${updates.join('\n')} - ${name}._set(${name}_changes); - ${bindings.length && `${name_updating} = {};`} - `); - } - - if (!state.parentNode) block.builders.unmount.addLine(`${name}._unmount();`); - - block.builders.destroy.addLine(deindent` - ${name}.destroy(false); - ${ref && `if (#component.refs.${ref.name} === ${name}) #component.refs.${ref.name} = null;`} - `); - } - - // maintain component context - if (allContexts.size) { - const contexts = Array.from(allContexts); - - const initialProps = contexts - .map(contextName => { - if (contextName === 'state') return `state: state`; - - const listName = block.listNames.get(contextName); - const indexName = block.indexNames.get(contextName); - - return `${listName}: ${listName},\n${indexName}: ${indexName}`; - }) - .join(',\n'); - - const updates = contexts - .map(contextName => { - if (contextName === 'state') return `${name_context}.state = state;`; - - const listName = block.listNames.get(contextName); - const indexName = block.indexNames.get(contextName); - - return `${name_context}.${listName} = ${listName};\n${name_context}.${indexName} = ${indexName};`; - }) - .join('\n'); - - block.builders.init.addBlock(deindent` - var ${name_context} = { - ${initialProps} - }; - `); - - block.builders.update.addBlock(updates); - } -} - -function mungeAttribute(attribute: Node, block: Block): Attribute { - if (attribute.value === true) { - // attributes without values, e.g.