Merge pull request #787 from sveltejs/gh-763

Implement <slot>
pull/792/head
Rich Harris 7 years ago committed by GitHub
commit 6fad3cbf9c

@ -43,6 +43,7 @@ export default class Generator {
hasJs: boolean;
computations: Computation[];
templateProperties: Record<string, Node>;
slots: Set<string>;
code: MagicString;
@ -76,6 +77,7 @@ export default class Generator {
this.events = new Set();
this.transitions = new Set();
this.importedComponents = new Map();
this.slots = new Set();
this.bindingGroups = [];
this.indirectDependencies = new Map();

@ -72,7 +72,7 @@ export default function dom(
generator.stylesheet.warnOnUnusedSelectors(options.onwarn);
parsed.html.children.forEach((node: Node) => {
visit(generator, block, state, node, []);
visit(generator, block, state, node, [], []);
});
const builder = new CodeBuilder();
@ -181,6 +181,7 @@ export default function dom(
this._root = options._root || this;
this._yield = options._yield;
this._bind = options._bind;
${generator.slots.size && `this._slotted = options.slots || {};`}
${generator.stylesheet.hasStyles &&
options.css !== false &&
@ -200,6 +201,8 @@ export default function dom(
`}
`}
${generator.slots.size && `this.slots = {};`}
this._fragment = @create_main_fragment( this._state, this );
if ( options.target ) {

@ -1,3 +1,7 @@
import { DomGenerator } from './index';
import Block from './Block';
import { Node } from '../../interfaces';
export interface State {
name?: string;
namespace: string;
@ -11,3 +15,12 @@ export interface State {
usesComponent?: boolean;
selectBindingDependencies?: string[];
}
export type Visitor = (
generator: DomGenerator,
block: Block,
state: State,
node: Node,
elementStack: Node[],
componentStack: Node[]
) => void;

@ -1,6 +1,7 @@
import Block from './Block';
import { trimStart, trimEnd } from '../../utils/trim';
import { assign } from '../../shared/index.js';
import getStaticAttributeValue from '../shared/getStaticAttributeValue';
import { DomGenerator } from './index';
import { Node } from '../../interfaces';
import { State } from './interfaces';
@ -41,6 +42,7 @@ const preprocessors = {
state: State,
node: Node,
elementStack: Node[],
componentStack: Node[],
stripWhitespace: boolean
) => {
const dependencies = block.findDependencies(node.expression);
@ -57,6 +59,7 @@ const preprocessors = {
state: State,
node: Node,
elementStack: Node[],
componentStack: Node[],
stripWhitespace: boolean
) => {
const dependencies = block.findDependencies(node.expression);
@ -74,6 +77,7 @@ const preprocessors = {
state: State,
node: Node,
elementStack: Node[],
componentStack: Node[],
stripWhitespace: boolean
) => {
node._state = getChildState(state);
@ -94,6 +98,7 @@ const preprocessors = {
node: Node,
inEachBlock: boolean,
elementStack: Node[],
componentStack: Node[],
stripWhitespace: boolean,
nextSibling: Node
) => {
@ -113,7 +118,7 @@ const preprocessors = {
node._state = getChildState(state);
blocks.push(node._block);
preprocessChildren(generator, node._block, node._state, node, inEachBlock, elementStack, stripWhitespace, nextSibling);
preprocessChildren(generator, node._block, node._state, node, inEachBlock, elementStack, componentStack, stripWhitespace, nextSibling);
if (node._block.dependencies.size > 0) {
dynamic = true;
@ -140,6 +145,7 @@ const preprocessors = {
node.else,
inEachBlock,
elementStack,
componentStack,
stripWhitespace,
nextSibling
);
@ -169,6 +175,7 @@ const preprocessors = {
node: Node,
inEachBlock: boolean,
elementStack: Node[],
componentStack: Node[],
stripWhitespace: boolean,
nextSibling: Node
) => {
@ -221,7 +228,7 @@ const preprocessors = {
});
generator.blocks.push(node._block);
preprocessChildren(generator, node._block, node._state, node, true, elementStack, stripWhitespace, nextSibling);
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;
@ -240,6 +247,7 @@ const preprocessors = {
node.else,
inEachBlock,
elementStack,
componentStack,
stripWhitespace,
nextSibling
);
@ -254,6 +262,7 @@ const preprocessors = {
node: Node,
inEachBlock: boolean,
elementStack: Node[],
componentStack: Node[],
stripWhitespace: boolean,
nextSibling: Node
) => {
@ -326,10 +335,23 @@ const preprocessors = {
generator.components.has(node.name) || node.name === ':Self';
if (isComponent) {
const name = block.getUniqueName(
(node.name === ':Self' ? generator.name : node.name).toLowerCase()
);
node._state = getChildState(state, {
name,
parentNode: `${name}._slotted.default`,
isYield: true
});
} else {
const slot = getStaticAttributeValue(node, 'slot');
if (slot) {
// TODO validate slots — no nesting, no dynamic names...
const component = componentStack[componentStack.length - 1];
component._slots.add(slot);
}
const name = block.getUniqueName(
node.name.replace(/[^a-zA-Z0-9_$]/g, '_')
);
@ -355,17 +377,12 @@ const preprocessors = {
(node.name === ':Self' ? generator.name : node.name).toLowerCase()
);
node._block = block.child({
name: generator.getUniqueName(`create_${name}_yield_fragment`),
});
if (node.children) node._slots = new Set(['default']); // TODO only include default if there are unslotted children
generator.blocks.push(node._block);
preprocessChildren(generator, node._block, node._state, node, inEachBlock, elementStack, stripWhitespace, nextSibling);
block.addDependencies(node._block.dependencies);
node._block.hasUpdateMethod = node._block.dependencies.size > 0;
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), stripWhitespace, nextSibling);
preprocessChildren(generator, block, node._state, node, inEachBlock, elementStack.concat(node), componentStack, stripWhitespace, nextSibling);
}
}
},
@ -378,6 +395,7 @@ function preprocessChildren(
node: Node,
inEachBlock: boolean,
elementStack: Node[],
componentStack: Node[],
stripWhitespace: boolean,
nextSibling: Node
) {
@ -407,7 +425,7 @@ function preprocessChildren(
cleaned.forEach((child: Node, i: number) => {
const preprocessor = preprocessors[child.type];
if (preprocessor) preprocessor(generator, block, state, child, inEachBlock, elementStack, stripWhitespace, cleaned[i + 1] || nextSibling);
if (preprocessor) preprocessor(generator, block, state, child, inEachBlock, elementStack, componentStack, stripWhitespace, cleaned[i + 1] || nextSibling);
if (lastChild) {
lastChild.next = child;
@ -471,7 +489,7 @@ export default function preprocess(
};
generator.blocks.push(block);
preprocessChildren(generator, block, state, node, false, [], true, null);
preprocessChildren(generator, block, state, node, false, [], [], true, null);
block.hasUpdateMethod = true;
return { block, state };

@ -9,8 +9,9 @@ export default function visit(
block: Block,
state: State,
node: Node,
elementStack: Node[]
elementStack: Node[],
componentStack: Node[]
) {
const visitor = visitors[node.type];
visitor(generator, block, state, node, elementStack);
visitor(generator, block, state, node, elementStack, componentStack);
}

@ -43,47 +43,22 @@ export default function visitComponent(
block: Block,
state: State,
node: Node,
elementStack: Node[]
elementStack: Node[],
componentStack: Node[]
) {
generator.hasComponents = true;
const name = block.getUniqueName(
(node.name === ':Self' ? generator.name : node.name).toLowerCase()
);
const name = node._state.name;
const componentInitProperties = [`_root: #component._root`];
// Component has children, put them in a separate {{yield}} block
if (node.children.length > 0) {
const params = block.params.join(', ');
const childBlock = node._block;
const slots = Array.from(node._slots).map(name => `${name}: @createFragment()`);
componentInitProperties.push(`slots: { ${slots.join(', ')} }`);
node.children.forEach((child: Node) => {
visit(generator, childBlock, node._state, child, elementStack);
visit(generator, block, node._state, child, elementStack, componentStack.concat(node));
});
const yield_fragment = block.getUniqueName(`${name}_yield_fragment`);
block.builders.init.addLine(
`var ${yield_fragment} = ${childBlock.name}( ${params}, #component );`
);
block.builders.create.addLine(`${yield_fragment}.create();`);
block.builders.claim.addLine(
`${yield_fragment}.claim( ${state.parentNodes} );`
);
if (childBlock.hasUpdateMethod) {
block.builders.update.addLine(
`${yield_fragment}.update( changed, ${params} );`
);
}
block.builders.destroy.addLine(`${yield_fragment}.destroy();`);
componentInitProperties.push(`_yield: ${yield_fragment}`);
}
const allContexts = new Set();

@ -10,7 +10,8 @@ export default function visitEachBlock(
block: Block,
state: State,
node: Node,
elementStack: Node[]
elementStack: Node[],
componentStack: Node[]
) {
const each_block = generator.getUniqueName(`each_block`);
const create_each_block = node._block.name;
@ -125,12 +126,12 @@ export default function visitEachBlock(
}
node.children.forEach((child: Node) => {
visit(generator, node._block, node._state, child, elementStack);
visit(generator, node._block, node._state, child, elementStack, componentStack);
});
if (node.else) {
node.else.children.forEach((child: Node) => {
visit(generator, node.else._block, node.else._state, child, elementStack);
visit(generator, node.else._block, node.else._state, child, elementStack, componentStack);
});
}
}

@ -1,7 +1,7 @@
import attributeLookup from './lookup';
import deindent from '../../../../utils/deindent';
import { stringify } from '../../../../utils/stringify';
import getStaticAttributeValue from './getStaticAttributeValue';
import getStaticAttributeValue from '../../../shared/getStaticAttributeValue';
import { DomGenerator } from '../../index';
import Block from '../../Block';
import { Node } from '../../../../interfaces';

@ -1,6 +1,6 @@
import deindent from '../../../../utils/deindent';
import flattenReference from '../../../../utils/flattenReference';
import getStaticAttributeValue from './getStaticAttributeValue';
import getStaticAttributeValue from '../../../shared/getStaticAttributeValue';
import { DomGenerator } from '../../index';
import Block from '../../Block';
import { Node } from '../../../../interfaces';

@ -1,5 +1,6 @@
import deindent from '../../../../utils/deindent';
import visit from '../../visit';
import visitSlot from '../Slot';
import visitComponent from '../Component';
import visitWindow from './meta/Window';
import visitAttribute from './Attribute';
@ -36,20 +37,30 @@ export default function visitElement(
block: Block,
state: State,
node: Node,
elementStack: Node[]
elementStack: Node[],
componentStack: Node[]
) {
if (node.name in meta) {
return meta[node.name](generator, block, node);
}
if (node.name === 'slot') {
return visitSlot(generator, block, state, node, elementStack);
}
if (generator.components.has(node.name) || node.name === ':Self') {
return visitComponent(generator, block, state, node, elementStack);
return visitComponent(generator, block, state, node, elementStack, componentStack);
}
const childState = node._state;
const name = childState.parentNode;
const isToplevel = !state.parentNode;
const slot = node.attributes.find((attribute: Node) => attribute.name === 'slot');
const parentNode = slot ?
`${componentStack[componentStack.length - 1]._state.name}._slotted.${slot.value[0].data}` : // TODO this looks bonkers
state.parentNode;
const isToplevel = !parentNode;
block.addVariable(name);
block.builders.create.addLine(
@ -62,19 +73,14 @@ export default function visitElement(
if (generator.hydratable) {
block.builders.claim.addBlock(deindent`
${name} = ${getClaimStatement(
generator,
childState.namespace,
state.parentNodes,
node
)};
${name} = ${getClaimStatement(generator, childState.namespace, state.parentNodes, node)};
var ${childState.parentNodes} = @children( ${name} );
`);
}
if (state.parentNode) {
if (parentNode) {
block.builders.mount.addLine(
`@appendNode( ${name}, ${state.parentNode} );`
`@appendNode( ${name}, ${parentNode} );`
);
} else {
block.builders.mount.addLine(`@insertNode( ${name}, #target, anchor );`);
@ -190,7 +196,7 @@ export default function visitElement(
}
node.children.forEach((child: Node) => {
visit(generator, block, childState, child, elementStack.concat(node));
visit(generator, block, childState, child, elementStack.concat(node), componentStack);
});
if (node.lateUpdate) {

@ -20,7 +20,8 @@ function getBranches(
block: Block,
state: State,
node: Node,
elementStack: Node[]
elementStack: Node[],
componentStack: Node[]
) {
const branches = [
{
@ -32,11 +33,11 @@ function getBranches(
},
];
visitChildren(generator, block, state, node, elementStack);
visitChildren(generator, block, state, node, elementStack, componentStack);
if (isElseIf(node.else)) {
branches.push(
...getBranches(generator, block, state, node.else.children[0], elementStack)
...getBranches(generator, block, state, node.else.children[0], elementStack, componentStack)
);
} else {
branches.push({
@ -48,7 +49,7 @@ function getBranches(
});
if (node.else) {
visitChildren(generator, block, state, node.else, elementStack);
visitChildren(generator, block, state, node.else, elementStack, componentStack);
}
}
@ -60,10 +61,11 @@ function visitChildren(
block: Block,
state: State,
node: Node,
elementStack: Node[]
elementStack: Node[],
componentStack: Node[]
) {
node.children.forEach((child: Node) => {
visit(generator, node._block, node._state, child, elementStack);
visit(generator, node._block, node._state, child, elementStack, componentStack);
});
}
@ -72,7 +74,8 @@ export default function visitIfBlock(
block: Block,
state: State,
node: Node,
elementStack: Node[]
elementStack: Node[],
componentStack: Node[]
) {
const name = generator.getUniqueName(`if_block`);
const anchor = node.needsAnchor
@ -80,7 +83,7 @@ export default function visitIfBlock(
: (node.next && node.next._state.name) || 'null';
const params = block.params.join(', ');
const branches = getBranches(generator, block, state, node, elementStack);
const branches = getBranches(generator, block, state, node, elementStack, componentStack);
const hasElse = isElseBranch(branches[branches.length - 1]);
const if_name = hasElse ? '' : `if ( ${name} ) `;

@ -0,0 +1,76 @@
import { DomGenerator } from '../index';
import deindent from '../../../utils/deindent';
import visit from '../visit';
import Block from '../Block';
import getStaticAttributeValue from '../../shared/getStaticAttributeValue';
import { Node } from '../../../interfaces';
import { State } from '../interfaces';
export default function visitSlot(
generator: DomGenerator,
block: Block,
state: State,
node: Node,
elementStack: Node[],
componentStack: Node[]
) {
const slotName = getStaticAttributeValue(node, 'name') || 'default';
generator.slots.add(slotName);
const content_name = block.getUniqueName(`slot_content_${slotName}`);
block.addVariable(content_name, `#component._slotted.${slotName}`);
// TODO use surrounds as anchors where possible, a la if/each blocks
const before = block.getUniqueName(`${content_name}_before`);
const after = block.getUniqueName(`${content_name}_after`);
block.addVariable(before);
block.addVariable(after);
block.builders.create.pushCondition(`!${content_name}`);
block.builders.hydrate.pushCondition(`!${content_name}`);
block.builders.mount.pushCondition(`!${content_name}`);
block.builders.unmount.pushCondition(`!${content_name}`);
block.builders.destroy.pushCondition(`!${content_name}`);
node.children.forEach((child: Node) => {
visit(generator, block, state, child, elementStack, componentStack);
});
block.builders.create.popCondition();
block.builders.hydrate.popCondition();
block.builders.mount.popCondition();
block.builders.unmount.popCondition();
block.builders.destroy.popCondition();
// TODO can we use an else here?
if (state.parentNode) {
block.builders.mount.addBlock(deindent`
if (${content_name}) {
@appendNode(${before} || (${before} = @createComment()), ${state.parentNode});
@appendNode(${content_name}, ${state.parentNode});
@appendNode(${after} || (${after} = @createComment()), ${state.parentNode});
}
`);
} else {
block.builders.mount.addBlock(deindent`
if (${content_name}) {
@insertNode(${before} || (${before} = @createComment()), #target, anchor);
@insertNode(${content_name}, #target, anchor);
@insertNode(${after} || (${after} = @createComment()), #target, anchor);
}
`);
}
// if the slot is unmounted, move nodes back into the document fragment,
// so that it can be reinserted later
// TODO so that this can work with public API, component._slotted should
// be all fragments, derived from options.slots. Not === options.slots
// TODO can we use an else here?
block.builders.unmount.addBlock(deindent`
if (${content_name}) {
@reinsertBetween(${before}, ${after}, ${content_name});
@detachNode(${before});
@detachNode(${after});
}
`);
}

@ -5,8 +5,9 @@ import MustacheTag from './MustacheTag';
import RawMustacheTag from './RawMustacheTag';
import Text from './Text';
import YieldTag from './YieldTag';
import { Visitor } from '../interfaces';
export default {
const visitors: Record<string, Visitor> = {
EachBlock,
Element,
IfBlock,
@ -15,3 +16,5 @@ export default {
Text,
YieldTag,
};
export default visitors;

@ -11,7 +11,9 @@ import { stringify } from '../../utils/stringify';
export class SsrGenerator extends Generator {
bindings: string[];
renderCode: string;
elementDepth: number;
elementDepth: number; // TODO is this necessary? appears to be unused
appendTargets: Record<string, string> | null;
appendTarget: string | null;
constructor(
parsed: Parsed,
@ -24,6 +26,7 @@ export class SsrGenerator extends Generator {
this.bindings = [];
this.renderCode = '';
this.elementDepth = 0;
this.appendTargets = null;
// in an SSR context, we don't need to include events, methods, oncreate or ondestroy
const { templateProperties, defaultExport } = this;
@ -59,10 +62,26 @@ export class SsrGenerator extends Generator {
}
append(code: string) {
if (this.appendTarget) {
this.appendTargets[this.appendTarget] += code;
} else {
this.renderCode += code;
}
}
removeAppendTarget() {
this.appendTarget = this.appendTargets = null;
}
setAppendTarget(name: string) {
if (!this.appendTargets[name]) {
this.appendTargets[name] = '';
}
this.appendTarget = name;
}
}
export default function ssr(
parsed: Parsed,
source: string,

@ -80,10 +80,8 @@ export default function visitComponent(
let open = `\${${expression}.render({${props}}`;
if (node.children.length) {
open += `, { yield: () => \``;
}
generator.append(open);
generator.appendTargets = {};
generator.setAppendTarget('default');
generator.elementDepth += 1;
@ -93,6 +91,14 @@ export default function visitComponent(
generator.elementDepth -= 1;
const close = node.children.length ? `\` })}` : ')}';
generator.append(close);
const slotted = Object.keys(generator.appendTargets)
.map(name => `${name}: () => \`${generator.appendTargets[name]}\``)
.join(', ');
open += `, { slotted: { ${slotted} } }`;
generator.setAppendTarget(null);
}
generator.append(open);
generator.append(')}');
}

@ -1,4 +1,5 @@
import visitComponent from './Component';
import visitSlot from './Slot';
import isVoidElementName from '../../../utils/isVoidElementName';
import visit from '../visit';
import visitWindow from './meta/Window';
@ -33,6 +34,11 @@ export default function visitElement(
return meta[node.name](generator, block, node);
}
if (node.name === 'slot') {
visitSlot(generator, block, node);
return;
}
if (generator.components.has(node.name) || node.name === ':Self') {
visitComponent(generator, block, node);
return;
@ -41,6 +47,11 @@ export default function visitElement(
let openingTag = `<${node.name}`;
let textareaContents; // awkward special case
const slot = node.attributes.find((attribute: Node) => attribute.name === 'slot');
if (slot) {
generator.setAppendTarget(slot.value[0].data);
}
node.attributes.forEach((attribute: Node) => {
if (attribute.type !== 'Attribute') return;

@ -0,0 +1,26 @@
import visit from '../visit';
import { SsrGenerator } from '../index';
import Block from '../Block';
import { Node } from '../../../interfaces';
export default function visitSlot(
generator: SsrGenerator,
block: Block,
node: Node
) {
// TODO named slots
const name = node.attributes.find((attribute: Node) => attribute.name);
const slotName = name && name.value[0].data || 'default';
generator.append(`\${options && options.slotted && options.slotted.${slotName} ? options.slotted.${slotName}() : '`);
generator.elementDepth += 1;
node.children.forEach((child: Node) => {
visit(generator, block, child);
});
generator.elementDepth -= 1;
generator.append(`'}`);
}

@ -1,5 +0,0 @@
import { SsrGenerator } from '../index';
export default function visitYieldTag(generator: SsrGenerator) {
generator.append(`\${options && options.yield ? options.yield() : ''}`);
}

@ -5,7 +5,6 @@ import IfBlock from './IfBlock';
import MustacheTag from './MustacheTag';
import RawMustacheTag from './RawMustacheTag';
import Text from './Text';
import YieldTag from './YieldTag';
export default {
Comment,
@ -14,6 +13,5 @@ export default {
IfBlock,
MustacheTag,
RawMustacheTag,
Text,
YieldTag,
Text
};

@ -1,5 +1,5 @@
import { parseExpressionAt } from 'acorn';
import spaces from '../../utils/spaces';
import repeat from '../../utils/repeat';
import { Parser } from '../index';
function readExpression(parser: Parser, start: number, quoteMark: string|null) {
@ -31,7 +31,7 @@ function readExpression(parser: Parser, start: number, quoteMark: string|null) {
}
}
const expression = parseExpressionAt(spaces(start) + str, start);
const expression = parseExpressionAt(repeat(' ', start) + str, start);
parser.index = expression.end;
parser.allowWhitespace();
@ -101,7 +101,7 @@ export function readBindingDirective(
b = parser.index;
}
const source = spaces(a) + parser.template.slice(a, b);
const source = repeat(' ', a) + parser.template.slice(a, b);
value = parseExpressionAt(source, a);
if (value.type !== 'Identifier' && value.type !== 'MemberExpression') {

@ -1,5 +1,5 @@
import { parse } from 'acorn';
import spaces from '../../utils/spaces';
import repeat from '../../utils/repeat';
import { Parser } from '../index';
import { Node } from '../../interfaces';
@ -12,7 +12,7 @@ export default function readScript(parser: Parser, start: number, attributes: No
if (scriptEnd === -1) parser.error(`<script> must have a closing tag`);
const source =
spaces(scriptStart) + parser.template.slice(scriptStart, scriptEnd);
repeat(' ', scriptStart) + parser.template.slice(scriptStart, scriptEnd);
parser.index = scriptEnd + scriptClosingTag.length;
let ast;

@ -186,13 +186,17 @@ export default function mustache(parser: Parser) {
parser.stack.push(block);
} else if (parser.eat('yield')) {
// {{yield}}
// TODO deprecate
parser.allowWhitespace();
parser.eat('}}', true);
parser.current().children.push({
start,
end: parser.index,
type: 'YieldTag',
type: 'Element',
name: 'slot',
attributes: [],
children: []
});
} else if (parser.eat('{')) {
// {{{raw}}} mustache

@ -143,6 +143,26 @@ export default function tag(parser: Parser) {
}
}
if (name === 'slot') {
let i = parser.stack.length;
while (i--) {
const item = parser.stack[i];
if (item.type === 'EachBlock') {
parser.error(
`<slot> cannot be a child of an each-block`,
start
);
}
if (item.type === 'Element' && item.name === 'slot') {
parser.error(
`<slot> elements cannot be nested`,
start
);
}
}
}
const attributes = [];
const uniqueNames = new Set();

@ -16,6 +16,12 @@ export function detachBetween(before, after) {
}
}
export function reinsertBetween(before, after, target) {
while (before.nextSibling && before.nextSibling !== after) {
target.appendChild(before.parentNode.removeChild(before.nextSibling));
}
}
// TODO this is out of date
export function destroyEach(iterations, detach, start) {
for (var i = start; i < iterations.length; i += 1) {
@ -23,6 +29,10 @@ export function destroyEach(iterations, detach, start) {
}
}
export function createFragment() {
return document.createDocumentFragment();
}
export function createElement(name) {
return document.createElement(name);
}

@ -1,13 +1,22 @@
import repeat from './repeat';
enum ChunkType {
Line,
Block
}
interface Condition {
condition: string;
used: boolean;
}
export default class CodeBuilder {
result: string;
first: ChunkType;
last: ChunkType;
lastCondition: string;
conditionStack: Condition[];
indent: string;
constructor(str = '') {
this.result = str;
@ -19,19 +28,23 @@ export default class CodeBuilder {
this.last = initial;
this.lastCondition = null;
this.conditionStack = [];
this.indent = '';
}
addConditional(condition: string, body: string) {
body = body.replace(/^/gm, '\t');
this.reifyConditions();
body = body.replace(/^/gm, `${this.indent}\t`);
if (condition === this.lastCondition) {
this.result += `\n${body}`;
} else {
if (this.lastCondition) {
this.result += `\n}`;
this.result += `\n${this.indent}}`;
}
this.result += `${this.last === ChunkType.Block ? '\n\n' : '\n'}if ( ${condition} ) {\n${body}`;
this.result += `${this.last === ChunkType.Block ? '\n\n' : '\n'}${this.indent}if ( ${condition} ) {\n${body}`;
this.lastCondition = condition;
}
@ -39,15 +52,17 @@ export default class CodeBuilder {
}
addLine(line: string) {
this.reifyConditions();
if (this.lastCondition) {
this.result += `\n}`;
this.result += `\n${this.indent}}`;
this.lastCondition = null;
}
if (this.last === ChunkType.Block) {
this.result += `\n\n${line}`;
this.result += `\n\n${this.indent}${line}`;
} else if (this.last === ChunkType.Line) {
this.result += `\n${line}`;
this.result += `\n${this.indent}${line}`;
} else {
this.result += line;
}
@ -57,10 +72,12 @@ export default class CodeBuilder {
}
addLineAtStart(line: string) {
this.reifyConditions();
if (this.first === ChunkType.Block) {
this.result = `${line}\n\n${this.result}`;
this.result = `${line}\n\n${this.indent}${this.result}`;
} else if (this.first === ChunkType.Line) {
this.result = `${line}\n${this.result}`;
this.result = `${line}\n${this.indent}${this.result}`;
} else {
this.result += line;
}
@ -70,13 +87,17 @@ export default class CodeBuilder {
}
addBlock(block: string) {
this.reifyConditions();
if (this.indent) block = block.replace(/^/gm, `${this.indent}`);
if (this.lastCondition) {
this.result += `\n}`;
this.result += `\n${this.indent}}`;
this.lastCondition = null;
}
if (this.result) {
this.result += `\n\n${block}`;
this.result += `\n\n${this.indent}${block}`;
} else {
this.result += block;
}
@ -86,8 +107,10 @@ export default class CodeBuilder {
}
addBlockAtStart(block: string) {
this.reifyConditions();
if (this.result) {
this.result = `${block}\n\n${this.result}`;
this.result = `${block}\n\n${this.indent}${this.result}`;
} else {
this.result += block;
}
@ -100,6 +123,40 @@ export default class CodeBuilder {
return this.result === '';
}
pushCondition(condition: string) {
this.conditionStack.push({ condition, used: false });
}
popCondition() {
const { used } = this.conditionStack.pop();
this.indent = repeat('\t', this.conditionStack.length);
if (used) this.addLine('}');
}
reifyConditions() {
for (let i = 0; i < this.conditionStack.length; i += 1) {
const condition = this.conditionStack[i];
if (!condition.used) {
const line = `if (${condition.condition}) {`;
if (this.last === ChunkType.Block) {
this.result += `\n\n${this.indent}${line}`;
} else if (this.last === ChunkType.Line) {
this.result += `\n${this.indent}${line}`;
} else {
this.result += line;
}
this.last = ChunkType.Line;
if (!this.first) this.first = ChunkType.Line;
this.indent = repeat('\t', this.conditionStack.length);
condition.used = true;
}
}
}
toString() {
return this.result.trim() + (this.lastCondition ? `\n}` : ``);
}

@ -1,4 +1,4 @@
import spaces from './spaces';
import repeat from './repeat';
function tabsToSpaces(str: string) {
return str.replace(/^\t+/, match => match.split('\t').join(' '));
@ -26,7 +26,7 @@ export default function getCodeFrame(
if (isErrorLine) {
const indicator =
spaces(digits + 2 + tabsToSpaces(str.slice(0, column)).length) + '^';
repeat(' ', digits + 2 + tabsToSpaces(str.slice(0, column)).length) + '^';
return `${lineNum}: ${tabsToSpaces(str)}\n${indicator}`;
}

@ -0,0 +1,5 @@
export default function repeat(str: string, i: number) {
let result = '';
while (i--) result += str;
return result;
}

@ -1,5 +0,0 @@
export default function spaces(i: number) {
let result = '';
while (i--) result += ' ';
return result;
}

@ -11,6 +11,35 @@ export default function validateElement(validator: Validator, node: Node, refs:
validator.warn(`${node.name} component is not defined`, node.start);
}
if (node.name === 'slot') {
const nameAttribute = node.attributes.find((attribute: Node) => attribute.name === 'name');
if (nameAttribute) {
if (nameAttribute.value.length !== 1 || nameAttribute.value[0].type !== 'Text') {
validator.error(`<slot> name cannot be dynamic`, nameAttribute.start);
}
const slotName = nameAttribute.value[0].data;
if (slotName === 'default') {
validator.error(`default is a reserved word — it cannot be used as a slot name`, nameAttribute.start);
}
// TODO should duplicate slots be disallowed? Feels like it's more likely to be a
// bug than anything. Perhaps it should be a warning
// if (validator.slots.has(slotName)) {
// validator.error(`duplicate '${slotName}' <slot> element`, nameAttribute.start);
// }
// validator.slots.add(slotName);
} else {
// if (validator.slots.has('default')) {
// validator.error(`duplicate default <slot> element`, node.start);
// }
// validator.slots.add('default');
}
}
let hasIntro: boolean;
let hasOutro: boolean;
let hasTransition: boolean;
@ -136,6 +165,13 @@ export default function validateElement(validator: Validator, node: Node, refs:
);
}
}
if (attribute.name === 'slot' && !isComponent && isDynamic(attribute)) {
validator.error(
`slot attribute cannot have a dynamic value`,
attribute.start
);
}
}
});
}
@ -150,7 +186,7 @@ function checkTypeAttribute(validator: Validator, node: Node) {
validator.error(`'type' attribute must be specified`, attribute.start);
}
if (attribute.value.length > 1 || attribute.value[0].type !== 'Text') {
if (isDynamic(attribute)) {
validator.error(
`'type' attribute cannot be dynamic if input uses two-way binding`,
attribute.start
@ -159,3 +195,7 @@ function checkTypeAttribute(validator: Validator, node: Node) {
return attribute.value[0].data;
}
function isDynamic(attribute: Node) {
return attribute.value.length > 1 || attribute.value[0].type !== 'Text';
}

@ -32,6 +32,7 @@ export class Validator {
methods: Map<string, Node>;
helpers: Map<string, Node>;
transitions: Map<string, Node>;
slots: Set<string>;
constructor(parsed: Parsed, source: string, options: CompileOptions) {
this.source = source;
@ -47,6 +48,7 @@ export class Validator {
this.methods = new Map();
this.helpers = new Map();
this.transitions = new Map();
this.slots = new Set();
}
error(message: string, pos: number) {

@ -75,13 +75,8 @@ function cleanChildren(node) {
// recurse
[...node.childNodes].forEach(child => {
if (child.nodeType === 8) {
// comment
node.removeChild(child);
return;
}
if (child.nodeType === 3) {
// text
if (
node.namespaceURI === 'http://www.w3.org/2000/svg' &&
node.tagName !== 'text' &&
@ -90,12 +85,11 @@ function cleanChildren(node) {
node.removeChild(child);
}
child.data = child.data.replace(/\s{2,}/, '\n');
child.data = child.data.replace(/\s{2,}/g, '\n');
// text
if (previous && previous.nodeType === 3) {
previous.data += child.data;
previous.data = previous.data.replace(/\s{2,}/, '\n');
previous.data = previous.data.replace(/\s{2,}/g, '\n');
node.removeChild(child);
child = previous;
@ -120,12 +114,17 @@ function cleanChildren(node) {
}
export function normalizeHtml(window, html) {
try {
const node = window.document.createElement('div');
node.innerHTML = html
.replace(/<!--.*?-->/g, '')
.replace(/>[\s\r\n]+</g, '><')
.trim();
cleanChildren(node, '');
return node.innerHTML.replace(/<\/?noscript\/?>/g, '');
} catch (err) {
throw new Error(`Failed to normalize HTML:\n${html}`);
}
}
export function setupHtmlEqual() {

@ -8,7 +8,10 @@
{
"start": 0,
"end": 9,
"type": "YieldTag"
"type": "Element",
"name": "slot",
"attributes": [],
"children": []
}
]
},

@ -129,7 +129,7 @@ describe("runtime", () => {
try {
SvelteComponent = require(`./samples/${dir}/main.html`);
} catch (err) {
showOutput(cwd, { shared }); // eslint-disable-line no-console
showOutput(cwd, { shared, hydratable: hydrate }); // eslint-disable-line no-console
throw err;
}
@ -196,12 +196,12 @@ describe("runtime", () => {
config.error(assert, err);
} else {
failed.add(dir);
showOutput(cwd, { shared }); // eslint-disable-line no-console
showOutput(cwd, { shared, hydratable: hydrate }); // eslint-disable-line no-console
throw err;
}
}
if (config.show) showOutput(cwd, { shared });
if (config.show) showOutput(cwd, { shared, hydratable: hydrate });
});
}

@ -1,5 +1,5 @@
{{#if !hidden}}
{{ yield }}
<slot></slot>
{{/if}}
<script>

@ -1,5 +1,5 @@
<li>
{{yield}}
<slot></slot>
</li>
<script>

@ -0,0 +1,3 @@
export default {
html: '<p>Hello</p>'
};

@ -0,0 +1,13 @@
<Nested ref:nested>
Hello
</Nested>
<script>
import Nested from './Nested.html';
export default {
components: {
Nested
}
};
</script>

@ -0,0 +1,5 @@
<div>
<slot><p class='default'>default fallback content</p></slot>
<slot name='bar'><p class='default'>bar fallback content</p></slot>
<slot name='foo'><p class='default'>foo fallback content</p></slot>
</div>

@ -0,0 +1,9 @@
export default {
html: `
<div>
<p>not fallback</p>
<p class='default'>bar fallback content</p>
<p class='default'>foo fallback content</p>
</div>
`
};

@ -0,0 +1,13 @@
<Nested ref:nested>
<p>not fallback</p>
</Nested>
<script>
import Nested from './Nested.html';
export default {
components: {
Nested
}
};
</script>

@ -0,0 +1,5 @@
<div>
<slot/>
<slot name='bar'/>
<slot name='foo'/>
</div>

@ -0,0 +1,9 @@
export default {
html: `
<div>
Hello
<p slot='bar'>bar</p>
<p slot='foo'>foo</p>
</div>
`
};

@ -0,0 +1,16 @@
<Nested ref:nested>
Hello
<p slot='foo'>foo</p>
<p slot='bar'>bar</p>
</Nested>
<script>
import Nested from './Nested.html';
export default {
components: {
Nested
}
};
</script>

@ -9,7 +9,7 @@
data () {
return {
show: false
}
};
}
};
</script>

@ -1,14 +1,17 @@
<div>
<Widget ref:widget>{{data}}</Widget>
</div>
<script>
import Widget from './Widget.html'
import Widget from './Widget.html';
export default {
components: { Widget },
data() {
return {
data: "Hello"
};
}
}
}
};
</script>

@ -1,10 +1,19 @@
export default {
html: '<p>Hello Alice</p><p>Hello Bob</p><p>Hello Charles</p>',
html: `
<p>Hello Alice</p>
<p>Hello Bob</p>
<p>Hello Charles</p>
`,
test ( assert, component, target ) {
component.set({
people: [ 'Alice', 'Charles', 'Bob' ]
});
assert.htmlEqual( target.innerHTML, `<p>Hello Alice</p><p>Hello Charles</p><p>Hello Bob</p>` );
assert.htmlEqual( target.innerHTML, `
<p>Hello Alice</p>
<p>Hello Charles</p>
<p>Hello Bob</p>
`);
}
};

@ -1,3 +1,6 @@
export default {
html: '<p>Hello</p>'
html: `
<p>Hello
</p>`
};

@ -1,4 +1,4 @@
import counter from './counter.js';
// import counter from './counter.js';
export default {
'skip-ssr': true,

@ -1 +1,3 @@
<div><p>Hello</p></div>
<div>
<p>Hello</p>
</div>

@ -8,12 +8,13 @@ describe("validate", () => {
// add .solo to a sample directory name to only run that test
const solo = /\.solo/.test(dir);
const skip = /\.skip/.test(dir);
if (solo && process.env.CI) {
throw new Error("Forgot to remove `solo: true` from test");
}
(solo ? it.only : it)(dir, () => {
(solo ? it.only : skip ? it.skip : it)(dir, () => {
const filename = `test/validator/samples/${dir}/input.html`;
const input = fs.readFileSync(filename, "utf-8").replace(/\s+$/, "");

@ -0,0 +1,8 @@
[{
"message": "duplicate default <slot> element",
"loc": {
"line": 2,
"column": 0
},
"pos": 14
}]

@ -0,0 +1,8 @@
[{
"message": "default is a reserved word — it cannot be used as a slot name",
"loc": {
"line": 1,
"column": 6
},
"pos": 6
}]

@ -0,0 +1,8 @@
[{
"message": "slot attribute cannot have a dynamic value",
"loc": {
"line": 2,
"column": 9
},
"pos": 18
}]

@ -0,0 +1,3 @@
<Nested>
<button slot='{{foo}}'>click me</button>
</Nested>

@ -0,0 +1,8 @@
[{
"message": "<slot> name cannot be dynamic",
"loc": {
"line": 1,
"column": 6
},
"pos": 6
}]

@ -0,0 +1,8 @@
[{
"message": "<slot> cannot be a child of an each-block",
"loc": {
"line": 2,
"column": 1
},
"pos": 27
}]

@ -0,0 +1,3 @@
{{#each things as thing}}
<slot name='foo'></slot>
{{/each}}

@ -0,0 +1,8 @@
[{
"message": "duplicate 'foo' <slot> element",
"loc": {
"line": 2,
"column": 6
},
"pos": 31
}]

@ -0,0 +1,2 @@
<slot name='foo'></slot>
<slot name='foo'></slot>

@ -0,0 +1,8 @@
[{
"message": "<slot> elements cannot be nested",
"loc": {
"line": 2,
"column": 1
},
"pos": 8
}]

@ -0,0 +1,3 @@
<slot>
<slot name='foo'></slot>
</slot>
Loading…
Cancel
Save