start moving logic into Node and its subclasses

pull/992/head
Rich Harris 8 years ago
parent 1636f1733b
commit 2e56d10b39

@ -17,6 +17,7 @@ import DomBlock from './dom/Block';
import SsrBlock from './server-side-rendering/Block'; import SsrBlock from './server-side-rendering/Block';
import Stylesheet from '../css/Stylesheet'; import Stylesheet from '../css/Stylesheet';
import { test } from '../config'; import { test } from '../config';
import nodes from './nodes/index';
import { Node, GenerateOptions, Parsed, CompileOptions, CustomElementOptions } from '../interfaces'; import { Node, GenerateOptions, Parsed, CompileOptions, CustomElementOptions } from '../interfaces';
interface Computation { interface Computation {
@ -638,6 +639,7 @@ export default class Generator {
} }
walkTemplate() { walkTemplate() {
const generator = this;
const { const {
code, code,
expectedProperties, expectedProperties,
@ -703,7 +705,20 @@ export default class Generator {
const indexesStack: Set<string>[] = [indexes]; const indexesStack: Set<string>[] = [indexes];
walk(html, { 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') { if (node.type === 'EachBlock') {
node.metadata = contextualise(node.expression, contextDependencies, indexes); node.metadata = contextualise(node.expression, contextDependencies, indexes);
@ -764,7 +779,7 @@ export default class Generator {
this.skip(); this.skip();
} }
if (node.type === 'Element' && node.name === ':Component') { if (node.type === 'Component' && node.name === ':Component') {
node.metadata = contextualise(node.expression, contextDependencies, indexes); node.metadata = contextualise(node.expression, contextDependencies, indexes);
} }
}, },

@ -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));
}
}

@ -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 = { const preprocessors = {
MustacheTag: ( MustacheTag: (
generator: DomGenerator, generator: DomGenerator,
@ -72,9 +58,7 @@ const preprocessors = {
componentStack: Node[], componentStack: Node[],
stripWhitespace: boolean stripWhitespace: boolean
) => { ) => {
cannotUseInnerHTML(node); node.init(block);
node.var = block.getUniqueName('text');
block.addDependencies(node.metadata.dependencies);
}, },
RawMustacheTag: ( RawMustacheTag: (
@ -86,9 +70,7 @@ const preprocessors = {
componentStack: Node[], componentStack: Node[],
stripWhitespace: boolean stripWhitespace: boolean
) => { ) => {
cannotUseInnerHTML(node); node.init(block);
node.var = block.getUniqueName('raw');
block.addDependencies(node.metadata.dependencies);
}, },
Text: ( Text: (
@ -100,12 +82,7 @@ const preprocessors = {
componentStack: Node[], componentStack: Node[],
stripWhitespace: boolean stripWhitespace: boolean
) => { ) => {
if (!/\S/.test(node.data) && (state.namespace || elementsWithoutText.has(state.parentNodeName))) { node.init(block, state);
node.shouldSkip = true;
return;
}
node.var = block.getUniqueName(`text`);
}, },
AwaitBlock: ( AwaitBlock: (
@ -537,7 +514,6 @@ function preprocessChildren(
lastChild = null; lastChild = null;
cleaned.forEach((child: Node, i: number) => { cleaned.forEach((child: Node, i: number) => {
child.parent = node;
child.canUseInnerHTML = !generator.hydratable; child.canUseInnerHTML = !generator.hydratable;
const preprocessor = preprocessors[child.type]; const preprocessor = preprocessors[child.type];
@ -577,31 +553,35 @@ export default function preprocess(
namespace: string, namespace: string,
node: Node node: Node
) { ) {
const block = new Block({ // const block = new Block({
generator, // generator,
name: '@create_main_fragment', // name: '@create_main_fragment',
key: null, // key: null,
contexts: new Map(), // contexts: new Map(),
indexes: new Map(), // indexes: new Map(),
changeableIndexes: new Map(), // changeableIndexes: new Map(),
params: ['state'], // params: ['state'],
indexNames: new Map(), // indexNames: new Map(),
listNames: new Map(), // listNames: new Map(),
dependencies: new Set(), // dependencies: new Set(),
}); // });
const state: State = { // const state: State = {
namespace, // namespace,
parentNode: null, // parentNode: null,
parentNodes: 'nodes' // parentNodes: 'nodes'
}; // };
generator.blocks.push(block); // generator.blocks.push(block);
preprocessChildren(generator, block, state, node, false, [], [], true, null); // preprocessChildren(generator, block, state, node, false, [], [], true, null);
block.hasUpdateMethod = true; // block.hasUpdateMethod = true;
node.init(
namespace
);
return { block, state }; return node;
} }

@ -562,11 +562,11 @@ function isComputed(node: Node) {
function remount(generator: DomGenerator, node: Node, name: string) { function remount(generator: DomGenerator, node: Node, name: string) {
// TODO make this a method of the nodes // TODO make this a method of the nodes
if (node.type === 'Element') { if (node.type === 'Component') {
if (node.name === ':Self' || node.name === ':Component' || generator.components.has(node.name)) {
return `${node.var}._mount(${name}._slotted.default, null);`; return `${node.var}._mount(${name}._slotted.default, null);`;
} }
if (node.type === 'Element') {
const slot = node.attributes.find(attribute => attribute.name === 'slot'); const slot = node.attributes.find(attribute => attribute.name === 'slot');
if (slot) { if (slot) {
return `@appendNode(${node.var}, ${name}._slotted.${getStaticAttributeValue(node, 'slot')});`; return `@appendNode(${node.var}, ${name}._slotted.${getStaticAttributeValue(node, 'slot')});`;

@ -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 childState = node._state;
const name = childState.parentNode; const name = childState.parentNode;

@ -1,4 +1,5 @@
import AwaitBlock from './AwaitBlock'; import AwaitBlock from './AwaitBlock';
import Component from './Component';
import EachBlock from './EachBlock'; import EachBlock from './EachBlock';
import Element from './Element/Element'; import Element from './Element/Element';
import IfBlock from './IfBlock'; import IfBlock from './IfBlock';
@ -9,6 +10,7 @@ import { Visitor } from '../interfaces';
const visitors: Record<string, Visitor> = { const visitors: Record<string, Visitor> = {
AwaitBlock, AwaitBlock,
Component,
EachBlock, EachBlock,
Element, Element,
IfBlock, IfBlock,

@ -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;
}
}

@ -0,0 +1,6 @@
import Node from './shared/Node';
import Block from '../dom/Block';
export default class CatchBlock extends Node {
_block: Block;
}

@ -0,0 +1,5 @@
import Node from './shared/Node';
export default class Comment extends Node {
type: 'Comment'
}

@ -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);
});
}
}
}

@ -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;
}
}
}

@ -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 — <option value='{{foo}}'> — see below
if (
this.name === 'option' &&
attribute.name === 'value' &&
state.selectBindingDependencies
) {
state.selectBindingDependencies.forEach(prop => {
dependencies.forEach((dependency: string) => {
this.generator.indirectDependencies.get(prop).add(dependency);
});
});
}
}
});
} else {
if (this.parent) this.parent.cannotUseInnerHTML();
if (attribute.type === 'EventHandler' && attribute.expression) {
attribute.expression.arguments.forEach((arg: Node) => {
block.addDependencies(arg.metadata.dependencies);
});
} else if (attribute.type === 'Binding') {
block.addDependencies(attribute.metadata.dependencies);
} else if (attribute.type === 'Transition') {
if (attribute.intro)
this.generator.hasIntroTransitions = block.hasIntroMethod = true;
if (attribute.outro) {
this.generator.hasOutroTransitions = block.hasOutroMethod = true;
block.outros += 1;
}
}
}
});
const valueAttribute = this.attributes.find((attribute: Node) => attribute.name === 'value');
// Treat these the same way:
// <option>{{foo}}</option>
// <option value='{{foo}}'>{{foo}}</option>
if (this.name === 'option' && !valueAttribute) {
this.attributes.push({
type: 'Attribute',
name: 'value',
value: this.children
});
}
// special case — in a case like this...
//
// <select bind:value='foo'>
// <option value='{{bar}}'>bar</option>
// <option value='{{baz}}'>baz</option>
// </option>
//
// ...we need to know that `foo` depends on `bar` and `baz`,
// 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');
if (binding) {
// TODO does this also apply to e.g. `<input type='checkbox' bind:group='foo'>`?
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;
}
}

@ -0,0 +1,5 @@
import Node from './shared/Node';
export default class ElseBlock extends Node {
}

@ -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;
}
}

@ -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);
}
}

@ -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);
}
}

@ -0,0 +1,6 @@
import Node from './shared/Node';
import Block from '../dom/Block';
export default class PendingBlock extends Node {
_block: Block;
}

@ -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);
}
}

@ -0,0 +1,5 @@
import Node from './shared/Node';
export default class Slot extends Node {
}

@ -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`);
}
}

@ -0,0 +1,6 @@
import Node from './shared/Node';
import Block from '../dom/Block';
export default class ThenBlock extends Node {
_block: Block;
}

@ -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<string, any> = {
AwaitBlock,
CatchBlock,
Comment,
Component,
EachBlock,
Element,
ElseBlock,
Fragment,
IfBlock,
MustacheTag,
PendingBlock,
RawMustacheTag,
Slot,
Text,
ThenBlock
};
export default nodes;

@ -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;
}
}

@ -57,14 +57,10 @@ const preprocessors = {
node: Node, node: Node,
elementStack: Node[] elementStack: Node[]
) => { ) => {
const isComponent =
generator.components.has(node.name) || node.name === ':Self';
if (!isComponent) {
generator.stylesheet.apply(node, elementStack); generator.stylesheet.apply(node, elementStack);
const slot = getStaticAttributeValue(node, 'slot'); const slot = getStaticAttributeValue(node, 'slot');
if (slot && isChildOfComponent(node, generator)) { if (slot && node.isChildOfComponent()) {
node.slotted = true; node.slotted = true;
} }
@ -80,14 +76,19 @@ const preprocessors = {
value: node.children value: node.children
}); });
} }
}
if (node.children.length) { 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);
} }
}, },
}; };

@ -40,11 +40,6 @@ export default function visitElement(
return; return;
} }
if (generator.components.has(node.name) || node.name === ':Self' || node.name === ':Component') {
visitComponent(generator, block, node);
return;
}
let openingTag = `<${node.name}`; let openingTag = `<${node.name}`;
let textareaContents; // awkward special case let textareaContents; // awkward special case

@ -1,5 +1,6 @@
import AwaitBlock from './AwaitBlock'; import AwaitBlock from './AwaitBlock';
import Comment from './Comment'; import Comment from './Comment';
import Component from './Component';
import EachBlock from './EachBlock'; import EachBlock from './EachBlock';
import Element from './Element'; import Element from './Element';
import IfBlock from './IfBlock'; import IfBlock from './IfBlock';
@ -10,6 +11,7 @@ import Text from './Text';
export default { export default {
AwaitBlock, AwaitBlock,
Comment, Comment,
Component,
EachBlock, EachBlock,
Element, Element,
IfBlock, IfBlock,

@ -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, ' ');
}
Loading…
Cancel
Save