From 2e56d10b398f0ec90a43fad1dec2277964cdb20d Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 5 Dec 2017 22:28:28 -0500 Subject: [PATCH 01/32] start moving logic into Node and its subclasses --- src/generators/Generator.ts | 19 ++- src/generators/dom/State.ts | 34 ++++ src/generators/dom/preprocess.ts | 86 ++++------ src/generators/dom/visitors/Component.ts | 8 +- .../dom/visitors/Element/Element.ts | 4 - src/generators/dom/visitors/index.ts | 2 + src/generators/nodes/AwaitBlock.ts | 68 ++++++++ src/generators/nodes/CatchBlock.ts | 6 + src/generators/nodes/Comment.ts | 5 + src/generators/nodes/Component.ts | 61 +++++++ src/generators/nodes/EachBlock.ts | 115 +++++++++++++ src/generators/nodes/Element.ts | 151 ++++++++++++++++++ src/generators/nodes/ElseBlock.ts | 5 + src/generators/nodes/Fragment.ts | 41 +++++ src/generators/nodes/IfBlock.ts | 93 +++++++++++ src/generators/nodes/MustacheTag.ts | 10 ++ src/generators/nodes/PendingBlock.ts | 6 + src/generators/nodes/RawMustacheTag.ts | 10 ++ src/generators/nodes/Slot.ts | 5 + src/generators/nodes/Text.ts | 31 ++++ src/generators/nodes/ThenBlock.ts | 6 + src/generators/nodes/index.ts | 36 +++++ src/generators/nodes/shared/Node.ts | 121 ++++++++++++++ .../server-side-rendering/preprocess.ts | 57 +++---- .../server-side-rendering/visitors/Element.ts | 5 - .../server-side-rendering/visitors/index.ts | 2 + src/utils/createDebuggingComment.ts | 21 +++ 27 files changed, 912 insertions(+), 96 deletions(-) create mode 100644 src/generators/dom/State.ts create mode 100644 src/generators/nodes/AwaitBlock.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/Fragment.ts create mode 100644 src/generators/nodes/IfBlock.ts 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/Slot.ts create mode 100644 src/generators/nodes/Text.ts create mode 100644 src/generators/nodes/ThenBlock.ts create mode 100644 src/generators/nodes/index.ts create mode 100644 src/generators/nodes/shared/Node.ts create mode 100644 src/utils/createDebuggingComment.ts diff --git a/src/generators/Generator.ts b/src/generators/Generator.ts index 65574ee30372..2c31f5f68ff2 100644 --- a/src/generators/Generator.ts +++ b/src/generators/Generator.ts @@ -17,6 +17,7 @@ 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 { @@ -638,6 +639,7 @@ export default class Generator { } walkTemplate() { + const generator = this; const { code, expectedProperties, @@ -703,7 +705,20 @@ 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.type in nodes) { + node.__proto__ = nodes[node.type].prototype; + } + if (node.type === 'EachBlock') { node.metadata = contextualise(node.expression, contextDependencies, indexes); @@ -764,7 +779,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); } }, diff --git a/src/generators/dom/State.ts b/src/generators/dom/State.ts new file mode 100644 index 000000000000..eb9a41a5d1d6 --- /dev/null +++ b/src/generators/dom/State.ts @@ -0,0 +1,34 @@ +import { assign } from '../../shared/index.js'; + +interface StateData { + namespace?: string; + parentNode?: string; + parentNodes?: string; + parentNodeName?: string; + inEachBlock?: boolean; + allUsedContexts?: string[]; + usesComponent?: boolean; + selectBindingDependencies?: string[]; +} + +export default class State { + namespace?: string; + parentNode?: string; + parentNodes?: string; + parentNodeName?: string; + inEachBlock?: boolean; + allUsedContexts?: string[]; + usesComponent?: boolean; + selectBindingDependencies?: string[]; + + constructor(data: StateData = {}) { + assign(this, data) + } + + child(data: StateData) { + return new State(assign({}, this, { + parentNode: null, + parentNodes: 'nodes' + }, data)); + } +} \ No newline at end of file diff --git a/src/generators/dom/preprocess.ts b/src/generators/dom/preprocess.ts index 89af90a36eac..fdddfb1acfe6 100644 --- a/src/generators/dom/preprocess.ts +++ b/src/generators/dom/preprocess.ts @@ -48,20 +48,6 @@ function cannotUseInnerHTML(node: Node) { } } -// 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, @@ -72,9 +58,7 @@ const preprocessors = { componentStack: Node[], stripWhitespace: boolean ) => { - cannotUseInnerHTML(node); - node.var = block.getUniqueName('text'); - block.addDependencies(node.metadata.dependencies); + node.init(block); }, RawMustacheTag: ( @@ -86,9 +70,7 @@ const preprocessors = { componentStack: Node[], stripWhitespace: boolean ) => { - cannotUseInnerHTML(node); - node.var = block.getUniqueName('raw'); - block.addDependencies(node.metadata.dependencies); + node.init(block); }, Text: ( @@ -100,12 +82,7 @@ const preprocessors = { componentStack: Node[], stripWhitespace: boolean ) => { - if (!/\S/.test(node.data) && (state.namespace || elementsWithoutText.has(state.parentNodeName))) { - node.shouldSkip = true; - return; - } - - node.var = block.getUniqueName(`text`); + node.init(block, state); }, AwaitBlock: ( @@ -537,7 +514,6 @@ function preprocessChildren( lastChild = null; cleaned.forEach((child: Node, i: number) => { - child.parent = node; child.canUseInnerHTML = !generator.hydratable; const preprocessor = preprocessors[child.type]; @@ -577,31 +553,35 @@ export default function preprocess( 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; + // 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; + + node.init( + namespace + ); - return { block, state }; + return node; } diff --git a/src/generators/dom/visitors/Component.ts b/src/generators/dom/visitors/Component.ts index f3487b5a5c8c..2271b499f949 100644 --- a/src/generators/dom/visitors/Component.ts +++ b/src/generators/dom/visitors/Component.ts @@ -562,11 +562,11 @@ function isComputed(node: Node) { function remount(generator: DomGenerator, node: Node, name: string) { // TODO make this a method of the nodes - if (node.type === 'Element') { - if (node.name === ':Self' || node.name === ':Component' || generator.components.has(node.name)) { - return `${node.var}._mount(${name}._slotted.default, null);`; - } + if (node.type === 'Component') { + return `${node.var}._mount(${name}._slotted.default, null);`; + } + if (node.type === 'Element') { const slot = node.attributes.find(attribute => attribute.name === 'slot'); if (slot) { return `@appendNode(${node.var}, ${name}._slotted.${getStaticAttributeValue(node, 'slot')});`; diff --git a/src/generators/dom/visitors/Element/Element.ts b/src/generators/dom/visitors/Element/Element.ts index 3121e24a013e..7e4367b0f555 100644 --- a/src/generators/dom/visitors/Element/Element.ts +++ b/src/generators/dom/visitors/Element/Element.ts @@ -43,10 +43,6 @@ export default function visitElement( } } - if (generator.components.has(node.name) || node.name === ':Self' || node.name === ':Component') { - return visitComponent(generator, block, state, node, elementStack, componentStack); - } - const childState = node._state; const name = childState.parentNode; diff --git a/src/generators/dom/visitors/index.ts b/src/generators/dom/visitors/index.ts index fb0da9e0bc9b..b2c5c85993a3 100644 --- a/src/generators/dom/visitors/index.ts +++ b/src/generators/dom/visitors/index.ts @@ -1,4 +1,5 @@ import AwaitBlock from './AwaitBlock'; +import Component from './Component'; import EachBlock from './EachBlock'; import Element from './Element/Element'; import IfBlock from './IfBlock'; @@ -9,6 +10,7 @@ import { Visitor } from '../interfaces'; const visitors: Record = { AwaitBlock, + Component, EachBlock, Element, IfBlock, diff --git a/src/generators/nodes/AwaitBlock.ts b/src/generators/nodes/AwaitBlock.ts new file mode 100644 index 000000000000..0b011a4017c0 --- /dev/null +++ b/src/generators/nodes/AwaitBlock.ts @@ -0,0 +1,68 @@ +import Node from './shared/Node'; +import { DomGenerator } from '../dom/index'; +import Block from '../dom/Block'; +import PendingBlock from './PendingBlock'; +import ThenBlock from './ThenBlock'; +import CatchBlock from './CatchBlock'; +import { State } from '../dom/interfaces'; +import createDebuggingComment from '../../utils/createDebuggingComment'; + +export default class AwaitBlock extends Node { + value: string; + error: string; + + pending: PendingBlock; + then: ThenBlock; + catch: CatchBlock; + + init( + block: Block, + state: State, + inEachBlock: boolean, + elementStack: Node[], + componentStack: Node[], + stripWhitespace: boolean, + nextSibling: Node + ) { + this.cannotUseInnerHTML(); + + this.var = block.getUniqueName('await_block'); + block.addDependencies(this.metadata.dependencies); + + let dynamic = false; + + [ + ['pending', null], + ['then', this.value], + ['catch', this.error] + ].forEach(([status, arg]) => { + const child = this[status]; + + const context = block.getUniqueName(arg || '_'); + const contexts = new Map(block.contexts); + contexts.set(arg, context); + + child._block = block.child({ + comment: createDebuggingComment(child, this.generator), + name: this.generator.getUniqueName(`create_${status}_block`), + params: block.params.concat(context), + context, + contexts + }); + + child._state = state.child(); + + child.initChildren(child._block, child._state, inEachBlock, elementStack, componentStack, stripWhitespace, nextSibling); + this.generator.blocks.push(child._block); + + if (child._block.dependencies.size > 0) { + dynamic = true; + block.addDependencies(child._block.dependencies); + } + }); + + this.pending._block.hasUpdateMethod = dynamic; + this.then._block.hasUpdateMethod = dynamic; + this.catch._block.hasUpdateMethod = dynamic; + } +} \ No newline at end of file diff --git a/src/generators/nodes/CatchBlock.ts b/src/generators/nodes/CatchBlock.ts new file mode 100644 index 000000000000..5bcaaeee96c2 --- /dev/null +++ b/src/generators/nodes/CatchBlock.ts @@ -0,0 +1,6 @@ +import Node from './shared/Node'; +import Block from '../dom/Block'; + +export default class CatchBlock extends Node { + _block: Block; +} \ No newline at end of file diff --git a/src/generators/nodes/Comment.ts b/src/generators/nodes/Comment.ts new file mode 100644 index 000000000000..9ac5f0bd8cb9 --- /dev/null +++ b/src/generators/nodes/Comment.ts @@ -0,0 +1,5 @@ +import Node from './shared/Node'; + +export default class Comment extends Node { + type: 'Comment' +} \ No newline at end of file diff --git a/src/generators/nodes/Component.ts b/src/generators/nodes/Component.ts new file mode 100644 index 000000000000..ef889fa63d45 --- /dev/null +++ b/src/generators/nodes/Component.ts @@ -0,0 +1,61 @@ +import Node from './shared/Node'; +import Block from '../dom/Block'; +import State from '../dom/State'; + +export default class Component extends Node { + type: 'Component'; // TODO fix this? + name: string; + attributes: Node[]; // TODO have more specific Attribute type + children: Node[]; + + init( + block: Block, + state: State, + inEachBlock: boolean, + elementStack: Node[], + componentStack: Node[], + stripWhitespace: boolean, + nextSibling: Node + ) { + this.cannotUseInnerHTML(); + + this.attributes.forEach((attribute: Node) => { + if (attribute.type === 'Attribute' && attribute.value !== true) { + attribute.value.forEach((chunk: Node) => { + if (chunk.type !== 'Text') { + const dependencies = chunk.metadata.dependencies; + block.addDependencies(dependencies); + } + }); + } else { + if (attribute.type === 'EventHandler' && attribute.expression) { + attribute.expression.arguments.forEach((arg: Node) => { + block.addDependencies(arg.metadata.dependencies); + }); + } else if (attribute.type === 'Binding') { + block.addDependencies(attribute.metadata.dependencies); + } + } + }); + + this.var = block.getUniqueName( + ( + this.name === ':Self' ? this.generator.name : + this.name === ':Component' ? 'switch_instance' : + this.name + ).toLowerCase() + ); + + this._state = state.child({ + parentNode: `${this.var}._slotted.default` + }); + + if (this.children.length) { + this._slots = new Set(['default']); + + this.children.forEach(child => { + child.init(block, state, inEachBlock, elementStack, componentStack.concat(this), stripWhitespace, nextSibling); + }); + } + } +} \ No newline at end of file diff --git a/src/generators/nodes/EachBlock.ts b/src/generators/nodes/EachBlock.ts new file mode 100644 index 000000000000..5d042f4ee682 --- /dev/null +++ b/src/generators/nodes/EachBlock.ts @@ -0,0 +1,115 @@ +import Node from './shared/Node'; +import ElseBlock from './ElseBlock'; +import { DomGenerator } from '../dom/index'; +import Block from '../dom/Block'; +import State from '../dom/State'; +import createDebuggingComment from '../../utils/createDebuggingComment'; + +export default class EachBlock extends Node { + _block: Block; + _state: State; + expression: Node; + + iterations: string; + index: string; + context: string; + key: string; + destructuredContexts: string[]; + + else?: ElseBlock; + + init( + block: Block, + state: State, + inEachBlock: boolean, + elementStack: Node[], + componentStack: Node[], + stripWhitespace: boolean, + nextSibling: Node + ) { + this.cannotUseInnerHTML(); + + this.var = block.getUniqueName(`each`); + this.iterations = block.getUniqueName(`${this.var}_blocks`); + + const { dependencies } = this.metadata; + block.addDependencies(dependencies); + + const indexNames = new Map(block.indexNames); + const indexName = + this.index || block.getUniqueName(`${this.context}_index`); + indexNames.set(this.context, indexName); + + const listNames = new Map(block.listNames); + const listName = block.getUniqueName( + (this.expression.type === 'MemberExpression' && !this.expression.computed) ? this.expression.property.name : + this.expression.type === 'Identifier' ? this.expression.name : + `each_value` + ); + listNames.set(this.context, listName); + + const context = block.getUniqueName(this.context); + const contexts = new Map(block.contexts); + contexts.set(this.context, context); + + const indexes = new Map(block.indexes); + if (this.index) indexes.set(this.index, this.context); + + const changeableIndexes = new Map(block.changeableIndexes); + if (this.index) changeableIndexes.set(this.index, this.key); + + if (this.destructuredContexts) { + for (let i = 0; i < this.destructuredContexts.length; i += 1) { + contexts.set(this.destructuredContexts[i], `${context}[${i}]`); + } + } + + this._block = block.child({ + comment: createDebuggingComment(this, this.generator), + name: this.generator.getUniqueName('create_each_block'), + context: this.context, + key: this.key, + + contexts, + indexes, + changeableIndexes, + + listName, + indexName, + + indexNames, + listNames, + params: block.params.concat(listName, context, indexName), + }); + + this._state = state.child({ + inEachBlock: true, + }); + + this.generator.blocks.push(this._block); + this.initChildren(this._block, this._state, true, elementStack, componentStack, stripWhitespace, nextSibling); + block.addDependencies(this._block.dependencies); + this._block.hasUpdateMethod = this._block.dependencies.size > 0; + + if (this.else) { + this.else._block = block.child({ + comment: '// TODO', // createDebuggingComment(this.else, generator), + name: this.generator.getUniqueName(`${this._block.name}_else`), + }); + + this.else._state = state.child(); + + this.generator.blocks.push(this.else._block); + this.else.initChildren( + this.else._block, + this.else._state, + inEachBlock, + elementStack, + componentStack, + stripWhitespace, + nextSibling + ); + this.else._block.hasUpdateMethod = this.else._block.dependencies.size > 0; + } + } +} \ No newline at end of file diff --git a/src/generators/nodes/Element.ts b/src/generators/nodes/Element.ts new file mode 100644 index 000000000000..66c9f84be9ee --- /dev/null +++ b/src/generators/nodes/Element.ts @@ -0,0 +1,151 @@ +import Node from './shared/Node'; +import Block from '../dom/Block'; +import State from '../dom/State'; + +export default class Element extends Node { + type: 'Element'; + name: string; + attributes: Node[]; // TODO have more specific Attribute type + children: Node[]; + + init( + block: Block, + state: State, + inEachBlock: boolean, + elementStack: Node[], + componentStack: Node[], + stripWhitespace: boolean, + nextSibling: Node + ) { + if (this.name === 'slot' || this.name === 'option') { + this.cannotUseInnerHTML(); + } + + this.attributes.forEach((attribute: Node) => { + if (attribute.type === 'Attribute' && attribute.value !== true) { + attribute.value.forEach((chunk: Node) => { + if (chunk.type !== 'Text') { + if (this.parent) this.parent.cannotUseInnerHTML(); + + const dependencies = chunk.metadata.dependencies; + block.addDependencies(dependencies); + + // special case — + // + if (this.name === 'option' && !valueAttribute) { + this.attributes.push({ + type: 'Attribute', + name: 'value', + value: this.children + }); + } + + // special case — in a case like this... + // + // `? + const dependencies = binding.metadata.dependencies; + state.selectBindingDependencies = dependencies; + dependencies.forEach((prop: string) => { + this.generator.indirectDependencies.set(prop, new Set()); + }); + } else { + state.selectBindingDependencies = null; + } + } + + const slot = this.getStaticAttributeValue('slot'); + if (slot && this.isChildOfComponent()) { + this.cannotUseInnerHTML(); + this.slotted = true; + // TODO validate slots — no nesting, no dynamic names... + const component = componentStack[componentStack.length - 1]; + component._slots.add(slot); + } + + this.var = block.getUniqueName( + this.name.replace(/[^a-zA-Z0-9_$]/g, '_') + ); + + this._state = state.child({ + parentNode: this.var, + parentNodes: block.getUniqueName(`${this.var}_nodes`), + parentNodeName: this.name, + namespace: this.name === 'svg' + ? 'http://www.w3.org/2000/svg' + : state.namespace, + allUsedContexts: [], + }); + + this.generator.stylesheet.apply(this, elementStack); + + if (this.children.length) { + if (this.name === 'pre' || this.name === 'textarea') stripWhitespace = false; + this.initChildren(block, this._state, inEachBlock, elementStack.concat(this), componentStack, stripWhitespace, nextSibling); + } + } + + getStaticAttributeValue(name: string) { + const attribute = this.attributes.find( + (attr: Node) => attr.name.toLowerCase() === name + ); + + if (!attribute) return null; + + if (attribute.value === true) return true; + if (attribute.value.length === 0) return ''; + + if (attribute.value.length === 1 && attribute.value[0].type === 'Text') { + return attribute.value[0].data; + } + + return null; + } +} \ No newline at end of file diff --git a/src/generators/nodes/ElseBlock.ts b/src/generators/nodes/ElseBlock.ts new file mode 100644 index 000000000000..71b811d23b8c --- /dev/null +++ b/src/generators/nodes/ElseBlock.ts @@ -0,0 +1,5 @@ +import Node from './shared/Node'; + +export default class ElseBlock extends Node { + +} \ No newline at end of file diff --git a/src/generators/nodes/Fragment.ts b/src/generators/nodes/Fragment.ts new file mode 100644 index 000000000000..97d7e4d4fbdd --- /dev/null +++ b/src/generators/nodes/Fragment.ts @@ -0,0 +1,41 @@ +import Node from './shared/Node'; +import { DomGenerator } from '../dom/index'; +import Block from '../dom/Block'; +import State from '../dom/State'; + +export default class Fragment extends Node { + block: Block; + state: State; + children: Node[]; + + init( + namespace: string + ) { + this.block = new Block({ + generator: this.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(), + }); + + this.state = new State({ + namespace, + parentNode: null, + parentNodes: 'nodes' + }); + + this.generator.blocks.push(this.block); + this.initChildren(this.block, this.state, false, [], [], true, null); + + this.block.hasUpdateMethod = true; + } +} \ No newline at end of file diff --git a/src/generators/nodes/IfBlock.ts b/src/generators/nodes/IfBlock.ts new file mode 100644 index 000000000000..cfae03a184b9 --- /dev/null +++ b/src/generators/nodes/IfBlock.ts @@ -0,0 +1,93 @@ +import Node from './shared/Node'; +import { DomGenerator } from '../dom/index'; +import Block from '../dom/Block'; +import { State } from '../dom/interfaces'; +import createDebuggingComment from '../../utils/createDebuggingComment'; + +function isElseIf(node: Node) { + return ( + node && node.children.length === 1 && node.children[0].type === 'IfBlock' + ); +} + +export default class IfBlock extends Node { + init( + block: Block, + state: State, + inEachBlock: boolean, + elementStack: Node[], + componentStack: Node[], + stripWhitespace: boolean, + nextSibling: Node + ) { + const { generator } = this; + + this.cannotUseInnerHTML(); + + 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 = state.child(); + + blocks.push(node._block); + node.initChildren(node._block, node._state, 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 = state.child(); + + blocks.push(node.else._block); + node.else.initChildren( + node.else._block, + node.else._state, + inEachBlock, + elementStack, + componentStack, + stripWhitespace, + nextSibling + ); + + if (node.else._block.dependencies.size > 0) { + dynamic = true; + block.addDependencies(node.else._block.dependencies); + } + } + } + + attachBlocks(this); + + blocks.forEach(block => { + block.hasUpdateMethod = dynamic; + block.hasIntroMethod = hasIntros; + block.hasOutroMethod = hasOutros; + }); + + generator.blocks.push(...blocks); + } +} \ No newline at end of file diff --git a/src/generators/nodes/MustacheTag.ts b/src/generators/nodes/MustacheTag.ts new file mode 100644 index 000000000000..59d7723be03e --- /dev/null +++ b/src/generators/nodes/MustacheTag.ts @@ -0,0 +1,10 @@ +import Node from './shared/Node'; +import Block from '../dom/Block'; + +export default class MustacheTag extends Node { + init(block: Block) { + this.cannotUseInnerHTML(); + this.var = block.getUniqueName('text'); + block.addDependencies(this.metadata.dependencies); + } +} \ No newline at end of file diff --git a/src/generators/nodes/PendingBlock.ts b/src/generators/nodes/PendingBlock.ts new file mode 100644 index 000000000000..d192f798d1be --- /dev/null +++ b/src/generators/nodes/PendingBlock.ts @@ -0,0 +1,6 @@ +import Node from './shared/Node'; +import Block from '../dom/Block'; + +export default class PendingBlock extends Node { + _block: Block; +} \ No newline at end of file diff --git a/src/generators/nodes/RawMustacheTag.ts b/src/generators/nodes/RawMustacheTag.ts new file mode 100644 index 000000000000..a77bc2de2d70 --- /dev/null +++ b/src/generators/nodes/RawMustacheTag.ts @@ -0,0 +1,10 @@ +import Node from './shared/Node'; +import Block from '../dom/Block'; + +export default class RawMustacheTag extends Node { + init(block: Block) { + this.cannotUseInnerHTML(); + this.var = block.getUniqueName('raw'); + block.addDependencies(this.metadata.dependencies); + } +} \ No newline at end of file diff --git a/src/generators/nodes/Slot.ts b/src/generators/nodes/Slot.ts new file mode 100644 index 000000000000..07c2ea107045 --- /dev/null +++ b/src/generators/nodes/Slot.ts @@ -0,0 +1,5 @@ +import Node from './shared/Node'; + +export default class Slot extends Node { + +} \ No newline at end of file diff --git a/src/generators/nodes/Text.ts b/src/generators/nodes/Text.ts new file mode 100644 index 000000000000..1ff2e2c0d8ad --- /dev/null +++ b/src/generators/nodes/Text.ts @@ -0,0 +1,31 @@ +import Node from './shared/Node'; +import Block from '../dom/Block'; +import { State } from '../dom/interfaces'; + +// 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', +]); + +export default class Text extends Node { + data: string; + shouldSkip: boolean; + + init(block: Block, state: State) { + if (!/\S/.test(this.data) && (state.namespace || elementsWithoutText.has(state.parentNodeName))) { + this.shouldSkip = true; + return; + } + + this.var = block.getUniqueName(`text`); + } +} \ No newline at end of file diff --git a/src/generators/nodes/ThenBlock.ts b/src/generators/nodes/ThenBlock.ts new file mode 100644 index 000000000000..a1805df454f2 --- /dev/null +++ b/src/generators/nodes/ThenBlock.ts @@ -0,0 +1,6 @@ +import Node from './shared/Node'; +import Block from '../dom/Block'; + +export default class ThenBlock extends Node { + _block: Block; +} \ No newline at end of file diff --git a/src/generators/nodes/index.ts b/src/generators/nodes/index.ts new file mode 100644 index 000000000000..787a1667e969 --- /dev/null +++ b/src/generators/nodes/index.ts @@ -0,0 +1,36 @@ +import Node from './shared/Node'; +import AwaitBlock from './AwaitBlock'; +import CatchBlock from './CatchBlock'; +import Comment from './Comment'; +import Component from './Component'; +import EachBlock from './EachBlock'; +import Element from './Element'; +import ElseBlock from './ElseBlock'; +import Fragment from './Fragment'; +import IfBlock from './IfBlock'; +import MustacheTag from './MustacheTag'; +import PendingBlock from './PendingBlock'; +import RawMustacheTag from './RawMustacheTag'; +import Slot from './Slot'; +import Text from './Text'; +import ThenBlock from './ThenBlock'; + +const nodes: Record = { + AwaitBlock, + CatchBlock, + Comment, + Component, + EachBlock, + Element, + ElseBlock, + Fragment, + IfBlock, + MustacheTag, + PendingBlock, + RawMustacheTag, + Slot, + Text, + ThenBlock +}; + +export default nodes; \ No newline at end of file diff --git a/src/generators/nodes/shared/Node.ts b/src/generators/nodes/shared/Node.ts new file mode 100644 index 000000000000..95d057ba4715 --- /dev/null +++ b/src/generators/nodes/shared/Node.ts @@ -0,0 +1,121 @@ +import { DomGenerator } from '../../dom/index'; +import Block from '../../dom/Block'; +import State from '../../dom/State'; +import { trimStart, trimEnd } from '../../../utils/trim'; + +export default class Node { + metadata: { + dependencies: string[]; + }; + + type: string; + parent: Node; + generator: DomGenerator; + + canUseInnerHTML: boolean; + var: string; + + cannotUseInnerHTML() { + if (this.canUseInnerHTML !== false) { + this.canUseInnerHTML = false; + if (this.parent) { + if (!this.parent.cannotUseInnerHTML) console.log(this.parent.type, this.type); + this.parent.cannotUseInnerHTML(); + } + } + } + + init( + block: Block, + state: State, + inEachBlock: boolean, + elementStack: Node[], + componentStack: Node[], + stripWhitespace: boolean, + nextSibling: Node + ) { + // implemented by subclasses + } + + initChildren( + block: Block, + state: State, + inEachBlock: boolean, + elementStack: Node[], + componentStack: Node[], + stripWhitespace: boolean, + nextSibling: Node + ) { + // glue text nodes together + const cleaned: Node[] = []; + let lastChild: Node; + + let windowComponent; + + this.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.canUseInnerHTML = !this.generator.hydratable; + + child.init(block, state, 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; + } + } + } + + this.children = cleaned; + if (windowComponent) cleaned.unshift(windowComponent); + } + + isChildOfComponent() { + return this.parent ? + this.parent.type === 'Component' || this.parent.isChildOfComponent() : + false; + } +} \ No newline at end of file diff --git a/src/generators/server-side-rendering/preprocess.ts b/src/generators/server-side-rendering/preprocess.ts index 7e9cb7365db8..8556cad3fdb8 100644 --- a/src/generators/server-side-rendering/preprocess.ts +++ b/src/generators/server-side-rendering/preprocess.ts @@ -57,37 +57,38 @@ const preprocessors = { node: Node, elementStack: Node[] ) => { - const isComponent = - generator.components.has(node.name) || node.name === ':Self'; - - if (!isComponent) { - generator.stylesheet.apply(node, elementStack); - - const slot = getStaticAttributeValue(node, 'slot'); - if (slot && isChildOfComponent(node, generator)) { - node.slotted = true; - } - - // Treat these the same way: - // - // - const valueAttribute = node.attributes.find((attribute: Node) => attribute.name === 'value'); - - if (node.name === 'option' && !valueAttribute) { - node.attributes.push({ - type: 'Attribute', - name: 'value', - value: node.children - }); - } + generator.stylesheet.apply(node, elementStack); + + const slot = getStaticAttributeValue(node, 'slot'); + if (slot && node.isChildOfComponent()) { + node.slotted = true; + } + + // Treat these the same way: + // + // + const valueAttribute = node.attributes.find((attribute: Node) => attribute.name === 'value'); + + if (node.name === 'option' && !valueAttribute) { + node.attributes.push({ + type: 'Attribute', + name: 'value', + value: node.children + }); } if (node.children.length) { - if (isComponent) { - preprocessChildren(generator, node, elementStack); - } else { - preprocessChildren(generator, node, elementStack.concat(node)); - } + preprocessChildren(generator, node, elementStack.concat(node)); + } + }, + + Component: ( + generator: SsrGenerator, + node: Node, + elementStack: Node[] + ) => { + if (node.children.length) { + preprocessChildren(generator, node, elementStack); } }, }; diff --git a/src/generators/server-side-rendering/visitors/Element.ts b/src/generators/server-side-rendering/visitors/Element.ts index 25da587a1f9f..c02fa6c8d303 100644 --- a/src/generators/server-side-rendering/visitors/Element.ts +++ b/src/generators/server-side-rendering/visitors/Element.ts @@ -40,11 +40,6 @@ export default function visitElement( return; } - if (generator.components.has(node.name) || node.name === ':Self' || node.name === ':Component') { - visitComponent(generator, block, node); - return; - } - let openingTag = `<${node.name}`; let textareaContents; // awkward special case diff --git a/src/generators/server-side-rendering/visitors/index.ts b/src/generators/server-side-rendering/visitors/index.ts index 468239a9496b..5a8031c0ea3e 100644 --- a/src/generators/server-side-rendering/visitors/index.ts +++ b/src/generators/server-side-rendering/visitors/index.ts @@ -1,5 +1,6 @@ import AwaitBlock from './AwaitBlock'; import Comment from './Comment'; +import Component from './Component'; import EachBlock from './EachBlock'; import Element from './Element'; import IfBlock from './IfBlock'; @@ -10,6 +11,7 @@ import Text from './Text'; export default { AwaitBlock, Comment, + Component, EachBlock, Element, IfBlock, diff --git a/src/utils/createDebuggingComment.ts b/src/utils/createDebuggingComment.ts new file mode 100644 index 000000000000..bb9dffd0f4c6 --- /dev/null +++ b/src/utils/createDebuggingComment.ts @@ -0,0 +1,21 @@ +import { DomGenerator } from '../generators/dom/index'; +import { Node } from '../interfaces'; + +export default 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, ' '); +} \ No newline at end of file From 4b31992c9b7ddf23a80c85f47b7ec3dd2e496b15 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 5 Dec 2017 22:32:17 -0500 Subject: [PATCH 02/32] remove unused code --- src/generators/dom/index.ts | 4 +- src/generators/dom/preprocess.ts | 587 ------------------------------- src/generators/nodes/Fragment.ts | 2 +- 3 files changed, 3 insertions(+), 590 deletions(-) delete mode 100644 src/generators/dom/preprocess.ts diff --git a/src/generators/dom/index.ts b/src/generators/dom/index.ts index c6d9e94df211..6a8624a60151 100644 --- a/src/generators/dom/index.ts +++ b/src/generators/dom/index.ts @@ -12,7 +12,6 @@ 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,7 +95,8 @@ export default function dom( namespace, } = generator; - const { block, state } = preprocess(generator, namespace, parsed.html); + parsed.html.init(); + const { block, state } = parsed.html; generator.stylesheet.warnOnUnusedSelectors(options.onwarn); diff --git a/src/generators/dom/preprocess.ts b/src/generators/dom/preprocess.ts deleted file mode 100644 index fdddfb1acfe6..000000000000 --- a/src/generators/dom/preprocess.ts +++ /dev/null @@ -1,587 +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; - } -} - -const preprocessors = { - MustacheTag: ( - generator: DomGenerator, - block: Block, - state: State, - node: Node, - elementStack: Node[], - componentStack: Node[], - stripWhitespace: boolean - ) => { - node.init(block); - }, - - RawMustacheTag: ( - generator: DomGenerator, - block: Block, - state: State, - node: Node, - elementStack: Node[], - componentStack: Node[], - stripWhitespace: boolean - ) => { - node.init(block); - }, - - Text: ( - generator: DomGenerator, - block: Block, - state: State, - node: Node, - elementStack: Node[], - componentStack: Node[], - stripWhitespace: boolean - ) => { - node.init(block, state); - }, - - 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.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; - - node.init( - namespace - ); - - return node; -} diff --git a/src/generators/nodes/Fragment.ts b/src/generators/nodes/Fragment.ts index 97d7e4d4fbdd..90f4faedbf39 100644 --- a/src/generators/nodes/Fragment.ts +++ b/src/generators/nodes/Fragment.ts @@ -28,7 +28,7 @@ export default class Fragment extends Node { }); this.state = new State({ - namespace, + namespace: this.generator.namespace, parentNode: null, parentNodes: 'nodes' }); From 39f1af26ae1e6d0cb5f4176fdea7b1e16164b18c Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 7 Dec 2017 10:17:16 -0500 Subject: [PATCH 03/32] start implementing build() methods --- mocha.opts | 1 + src/generators/dom/index.ts | 7 +- src/generators/nodes/Attribute.ts | 7 + src/generators/nodes/Element.ts | 353 +++++++++++++++++++++++++++- src/generators/nodes/Fragment.ts | 13 + src/generators/nodes/shared/Node.ts | 14 ++ 6 files changed, 387 insertions(+), 8 deletions(-) create mode 100644 src/generators/nodes/Attribute.ts diff --git a/mocha.opts b/mocha.opts index 427b029758d3..af6b17a845d6 100644 --- a/mocha.opts +++ b/mocha.opts @@ -1 +1,2 @@ +--bail test/test.js \ No newline at end of file diff --git a/src/generators/dom/index.ts b/src/generators/dom/index.ts index 6a8624a60151..a2e3bb281f15 100644 --- a/src/generators/dom/index.ts +++ b/src/generators/dom/index.ts @@ -100,9 +100,10 @@ export default function dom( generator.stylesheet.warnOnUnusedSelectors(options.onwarn); - parsed.html.children.forEach((node: Node) => { - visit(generator, block, state, node, [], []); - }); + // parsed.html.children.forEach((node: Node) => { + // visit(generator, block, state, node, [], []); + // }); + parsed.html.build(); const builder = new CodeBuilder(); const computationBuilder = new CodeBuilder(); diff --git a/src/generators/nodes/Attribute.ts b/src/generators/nodes/Attribute.ts new file mode 100644 index 000000000000..daaa9e6b7f2c --- /dev/null +++ b/src/generators/nodes/Attribute.ts @@ -0,0 +1,7 @@ +import Node from './shared/Node'; + +export default class Attribute extends Node { + name: string; + value: true | Node[] + expression: Node +} \ No newline at end of file diff --git a/src/generators/nodes/Element.ts b/src/generators/nodes/Element.ts index 66c9f84be9ee..f659dbd50f5b 100644 --- a/src/generators/nodes/Element.ts +++ b/src/generators/nodes/Element.ts @@ -1,11 +1,17 @@ import Node from './shared/Node'; import Block from '../dom/Block'; import State from '../dom/State'; +import Attribute from './Attribute'; +import * as namespaces from '../../utils/namespaces'; + +const meta: Record = { + ':Window': {}, // TODO this should be dealt with in walkTemplate +}; export default class Element extends Node { type: 'Element'; name: string; - attributes: Node[]; // TODO have more specific Attribute type + attributes: Attribute[]; // TODO have more specific Attribute type children: Node[]; init( @@ -21,7 +27,7 @@ export default class Element extends Node { this.cannotUseInnerHTML(); } - this.attributes.forEach((attribute: Node) => { + this.attributes.forEach(attribute => { if (attribute.type === 'Attribute' && attribute.value !== true) { attribute.value.forEach((chunk: Node) => { if (chunk.type !== 'Text') { @@ -70,11 +76,11 @@ export default class Element extends Node { // // if (this.name === 'option' && !valueAttribute) { - this.attributes.push({ + this.attributes.push(new Attribute({ type: 'Attribute', name: 'value', value: this.children - }); + })); } // special case — in a case like this... @@ -88,7 +94,7 @@ export default class Element extends Node { // so that if `foo.qux` changes, we know that we need to // mark `bar` and `baz` as dirty too if (this.name === 'select') { - const binding = this.attributes.find((node: Node) => node.type === 'Binding' && node.name === 'value'); + const binding = this.attributes.find(node => node.type === 'Binding' && node.name === 'value'); if (binding) { // TODO does this also apply to e.g. ``? const dependencies = binding.metadata.dependencies; @@ -132,6 +138,294 @@ export default class Element extends Node { } } + build( + block: Block, + state: State, + node: Node, + elementStack: Node[], + componentStack: Node[] + ) { + if (this.name in meta) { + return meta[this.name](generator, block, this); + } + + if (this.name === 'slot') { // TODO deal with in walkTemplate + if (this.generator.customElement) { + const slotName = getStaticAttributeValue(this, 'name') || 'default'; + this.generator.slots.add(slotName); + } else { + return visitSlot(this.generator, block, state, this, elementStack, componentStack); + } + } + + const childState = this._state; + const name = childState.parentNode; + + const slot = this.attributes.find((attribute: Node) => attribute.name === 'slot'); + const parentNode = this.slotted ? + `${componentStack[componentStack.length - 1].var}._slotted.${slot.value[0].data}` : // TODO this looks bonkers + state.parentNode; + + block.addVariable(name); + block.builders.create.addLine( + `${name} = ${getRenderStatement( + this.generator, + childState.namespace, + this.name + )};` + ); + + if (this.generator.hydratable) { + block.builders.claim.addBlock(deindent` + ${name} = ${getClaimStatement(generator, childState.namespace, state.parentNodes, node)}; + var ${childState.parentNodes} = @children(${name}); + `); + } + + if (parentNode) { + block.builders.mount.addLine( + `@appendNode(${name}, ${parentNode});` + ); + } else { + block.builders.mount.addLine(`@insertNode(${name}, #target, anchor);`); + + // TODO we eventually need to consider what happens to elements + // that belong to the same outgroup as an outroing element... + block.builders.unmount.addLine(`@detachNode(${name});`); + } + + // add CSS encapsulation attribute + if (this._needsCssAttribute && !generator.customElement) { + generator.needsEncapsulateHelper = true; + block.builders.hydrate.addLine( + `@encapsulateStyles(${name});` + ); + + if (this._cssRefAttribute) { + block.builders.hydrate.addLine( + `@setAttribute(${name}, "svelte-ref-${this._cssRefAttribute}", "");` + ) + } + } + + if (this.name === 'textarea') { + // this is an egregious hack, but it's the easiest way to get