pull/1367/head
Rich Harris 7 years ago
parent 7825c1230a
commit 32774a821d

@ -6,13 +6,13 @@ export default class Action extends Node {
name: string; name: string;
expression: Expression; expression: Expression;
constructor(compiler, parent, info) { constructor(compiler, parent, scope, info) {
super(compiler, parent, info); super(compiler, parent, scope, info);
this.name = info.name; this.name = info.name;
this.expression = info.expression this.expression = info.expression
? new Expression(compiler, this, info.expression) ? new Expression(compiler, this, scope, info.expression)
: null; : null;
} }
} }

@ -28,8 +28,8 @@ export default class Attribute extends Node {
dependencies: Set<string>; dependencies: Set<string>;
expression: Node; expression: Node;
constructor(compiler, parent, info) { constructor(compiler, parent, scope, info) {
super(compiler, parent, info); super(compiler, parent, scope, info);
this.name = info.name; this.name = info.name;
this.isTrue = info.value === true; this.isTrue = info.value === true;
@ -41,7 +41,7 @@ export default class Attribute extends Node {
: info.value.map(node => { : info.value.map(node => {
if (node.type === 'Text') return node; if (node.type === 'Text') return node;
const expression = new Expression(compiler, this, node.expression); const expression = new Expression(compiler, this, scope, node.expression);
addToSet(this.dependencies, expression.dependencies); addToSet(this.dependencies, expression.dependencies);
return expression; return expression;

@ -20,11 +20,11 @@ export default class Binding extends Node {
obj: string; obj: string;
prop: string; prop: string;
constructor(compiler, parent, info) { constructor(compiler, parent, scope, info) {
super(compiler, parent, info); super(compiler, parent, scope, info);
this.name = info.name; this.name = info.name;
this.value = new Expression(compiler, this, info.value); this.value = new Expression(compiler, this, scope, info.value);
// const contextual = block.contexts.has(name); // const contextual = block.contexts.has(name);
const contextual = false; // TODO const contextual = false; // TODO

@ -24,8 +24,8 @@ export default class Component extends Node {
children: Node[]; children: Node[];
ref: string; ref: string;
constructor(compiler, parent, info) { constructor(compiler, parent, scope, info) {
super(compiler, parent, info); super(compiler, parent, scope, info);
compiler.hasComponents = true; compiler.hasComponents = true;
@ -39,15 +39,15 @@ export default class Component extends Node {
switch (node.type) { switch (node.type) {
case 'Attribute': case 'Attribute':
// TODO spread // TODO spread
this.attributes.push(new Attribute(compiler, this, node)); this.attributes.push(new Attribute(compiler, this, scope, node));
break; break;
case 'Binding': case 'Binding':
this.bindings.push(new Binding(compiler, this, node)); this.bindings.push(new Binding(compiler, this, scope, node));
break; break;
case 'EventHandler': case 'EventHandler':
this.handlers.push(new EventHandler(compiler, this, node)); this.handlers.push(new EventHandler(compiler, this, scope, node));
break; break;
case 'Ref': case 'Ref':
@ -63,7 +63,7 @@ export default class Component extends Node {
} }
}); });
this.children = mapChildren(compiler, this, info.children); this.children = mapChildren(compiler, this, scope, info.children);
} }
init( init(
@ -150,7 +150,7 @@ export default class Component extends Node {
? '{}' ? '{}'
: stringifyProps( : stringifyProps(
// this.attributes.map(attr => `${attr.name}: ${attr.value}`) // this.attributes.map(attr => `${attr.name}: ${attr.value}`)
this.attributes.map(attr => `${attr.name}: "TODO"`) this.attributes.map(attr => `${attr.name}: ${attr.getValue()}`)
); );
if (this.attributes.length || this.bindings.length) { if (this.attributes.length || this.bindings.length) {

@ -21,14 +21,19 @@ export default class EachBlock extends Node {
children: Node[]; children: Node[];
else?: ElseBlock; else?: ElseBlock;
constructor(compiler, parent, info) { constructor(compiler, parent, scope, info) {
super(compiler, parent, info); super(compiler, parent, scope, info);
this.expression = new Expression(compiler, this, info.expression); this.expression = new Expression(compiler, this, scope, info.expression);
this.context = info.context; this.context = info.context;
this.key = info.key; this.key = info.key;
this.children = mapChildren(compiler, this, info.children); this.scope = scope.child();
// TODO handle indexes and destructuring
this.scope.add(this.context, this.expression.dependencies);
this.children = mapChildren(compiler, this, this.scope, info.children);
} }
init( init(

@ -33,8 +33,8 @@ export default class Element extends Node {
ref: string; ref: string;
namespace: string; namespace: string;
constructor(compiler, parent, info: any) { constructor(compiler, parent, scope, info: any) {
super(compiler, parent, info); super(compiler, parent, scope, info);
this.name = info.name; this.name = info.name;
const parentElement = parent.findNearest(/^Element/); const parentElement = parent.findNearest(/^Element/);
@ -53,26 +53,26 @@ export default class Element extends Node {
info.attributes.forEach(node => { info.attributes.forEach(node => {
switch (node.type) { switch (node.type) {
case 'Action': case 'Action':
this.actions.push(new Action(compiler, this, node)); this.actions.push(new Action(compiler, this, scope, node));
break; break;
case 'Attribute': case 'Attribute':
// special case // special case
if (node.name === 'xmlns') this.namespace = node.value[0].data; if (node.name === 'xmlns') this.namespace = node.value[0].data;
this.attributes.push(new Attribute(compiler, this, node)); this.attributes.push(new Attribute(compiler, this, scope, node));
break; break;
case 'Binding': case 'Binding':
this.bindings.push(new Binding(compiler, this, node)); this.bindings.push(new Binding(compiler, this, scope, node));
break; break;
case 'EventHandler': case 'EventHandler':
this.handlers.push(new EventHandler(compiler, this, node)); this.handlers.push(new EventHandler(compiler, this, scope, node));
break; break;
case 'Transition': case 'Transition':
const transition = new Transition(compiler, this, node); const transition = new Transition(compiler, this, scope, node);
if (node.intro) this.intro = transition; if (node.intro) this.intro = transition;
if (node.outro) this.outro = transition; if (node.outro) this.outro = transition;
break; break;
@ -92,7 +92,7 @@ export default class Element extends Node {
// TODO break out attributes and directives here // TODO break out attributes and directives here
this.children = mapChildren(compiler, this, info.children); this.children = mapChildren(compiler, this, scope, info.children);
} }
init( init(

@ -6,8 +6,8 @@ export default class ElseBlock extends Node {
type: 'ElseBlock'; type: 'ElseBlock';
children: Node[]; children: Node[];
constructor(compiler, parent, info) { constructor(compiler, parent, scope, info) {
super(compiler, parent, info); super(compiler, parent, scope, info);
this.children = mapChildren(compiler, this, info.children); this.children = mapChildren(compiler, this, scope, info.children);
} }
} }

@ -13,8 +13,8 @@ export default class EventHandler extends Node {
args: Expression[]; args: Expression[];
snippet: string; snippet: string;
constructor(compiler, parent, info) { constructor(compiler, parent, scope, info) {
super(compiler, parent, info); super(compiler, parent, scope, info);
this.name = info.name; this.name = info.name;
this.dependencies = new Set(); this.dependencies = new Set();
@ -23,7 +23,7 @@ export default class EventHandler extends Node {
this.callee = flattenReference(info.expression.callee); this.callee = flattenReference(info.expression.callee);
this.insertionPoint = info.expression.start; this.insertionPoint = info.expression.start;
this.args = info.expression.arguments.map(param => { this.args = info.expression.arguments.map(param => {
const expression = new Expression(compiler, this, param); const expression = new Expression(compiler, this, scope, param);
addToSet(this.dependencies, expression.dependencies); addToSet(this.dependencies, expression.dependencies);
return expression; return expression;
}); });

@ -4,13 +4,39 @@ import Generator from '../Generator';
import mapChildren from './shared/mapChildren'; import mapChildren from './shared/mapChildren';
import Block from '../dom/Block'; import Block from '../dom/Block';
class TemplateScope {
names: Set<string>;
indexes: Set<string>;
dependenciesForName: Map<string, string>;
constructor(parent?: TemplateScope) {
this.names = new Set(parent ? parent.names : []);
this.indexes = new Set(parent ? parent.names : []);
this.dependenciesForName = new Map(parent ? parent.dependenciesForName : []);
}
add(name, dependencies) {
this.names.add(name);
this.dependenciesForName.set(name, dependencies);
}
child() {
return new TemplateScope(this);
}
}
export default class Fragment extends Node { export default class Fragment extends Node {
block: Block; block: Block;
children: Node[]; children: Node[];
scope: TemplateScope;
constructor(compiler: Generator, info: any) { constructor(compiler: Generator, info: any) {
super(compiler, null, info); const scope = new TemplateScope();
this.children = mapChildren(compiler, this, info.children); super(compiler, null, scope, info);
this.scope = scope;
this.children = mapChildren(compiler, this, scope, info.children);
} }
init() { init() {

@ -25,14 +25,14 @@ export default class IfBlock extends Node {
block: Block; block: Block;
constructor(compiler, parent, info) { constructor(compiler, parent, scope, info) {
super(compiler, parent, info); super(compiler, parent, scope, info);
this.expression = new Expression(compiler, this, info.expression); this.expression = new Expression(compiler, this, scope, info.expression);
this.children = mapChildren(compiler, this, info.children); this.children = mapChildren(compiler, this, scope, info.children);
this.else = info.else this.else = info.else
? new ElseBlock(compiler, this, info.else) ? new ElseBlock(compiler, this, scope, info.else)
: null; : null;
} }

@ -31,10 +31,10 @@ export default class Slot extends Element {
parentNode: string, parentNode: string,
parentNodes: string parentNodes: string
) { ) {
const { generator } = this; const { compiler } = this;
const slotName = this.getStaticAttributeValue('name') || 'default'; const slotName = this.getStaticAttributeValue('name') || 'default';
generator.slots.add(slotName); compiler.slots.add(slotName);
const content_name = block.getUniqueName(`slot_content_${slotName}`); const content_name = block.getUniqueName(`slot_content_${slotName}`);
const prop = !isValidIdentifier(slotName) ? `["${slotName}"]` : `.${slotName}`; const prop = !isValidIdentifier(slotName) ? `["${slotName}"]` : `.${slotName}`;

@ -33,8 +33,8 @@ export default class Text extends Node {
data: string; data: string;
shouldSkip: boolean; shouldSkip: boolean;
constructor(compiler, parent, info) { constructor(compiler, parent, scope, info) {
super(compiler, parent, info); super(compiler, parent, scope, info);
this.data = info.data; this.data = info.data;
} }

@ -6,13 +6,13 @@ export default class Transition extends Node {
name: string; name: string;
expression: Expression; expression: Expression;
constructor(compiler, parent, info) { constructor(compiler, parent, scope, info) {
super(compiler, parent, info); super(compiler, parent, scope, info);
this.name = info.name; this.name = info.name;
this.expression = info.expression this.expression = info.expression
? new Expression(compiler, this, info.expression) ? new Expression(compiler, this, scope, info.expression)
: null; : null;
} }
} }

@ -38,17 +38,17 @@ export default class Window extends Node {
handlers: EventHandler[]; handlers: EventHandler[];
bindings: Binding[]; bindings: Binding[];
constructor(compiler, parent, info) { constructor(compiler, parent, scope, info) {
super(compiler, parent, info); super(compiler, parent, scope, info);
this.handlers = []; this.handlers = [];
this.bindings = []; this.bindings = [];
info.attributes.forEach(node => { info.attributes.forEach(node => {
if (node.type === 'EventHandler') { if (node.type === 'EventHandler') {
this.handlers.push(new EventHandler(compiler, this, node)); this.handlers.push(new EventHandler(compiler, this, scope, node));
} else if (node.type === 'Binding') { } else if (node.type === 'Binding') {
this.bindings.push(new Binding(compiler, this, node)); this.bindings.push(new Binding(compiler, this, scope, node));
} }
}); });
} }

@ -14,7 +14,7 @@ export default class Expression {
contexts: Set<string>; contexts: Set<string>;
indexes: Set<string>; indexes: Set<string>;
constructor(compiler, parent, info) { constructor(compiler, parent, scope, info) {
this.compiler = compiler; this.compiler = compiler;
this.node = info; this.node = info;
@ -27,7 +27,7 @@ export default class Expression {
const { code, helpers } = compiler; const { code, helpers } = compiler;
let { map, scope } = createScopes(info); let { map, scope: currentScope } = createScopes(info);
const isEventHandler = parent.type === 'EventHandler'; const isEventHandler = parent.type === 'EventHandler';
walk(info, { walk(info, {
@ -36,22 +36,23 @@ export default class Expression {
code.addSourcemapLocation(node.end); code.addSourcemapLocation(node.end);
if (map.has(node)) { if (map.has(node)) {
scope = map.get(node); currentScope = map.get(node);
return; return;
} }
if (isReference(node, parent)) { if (isReference(node, parent)) {
const { name } = flattenReference(node); const { name } = flattenReference(node);
if (scope && scope.has(name) || helpers.has(name) || (name === 'event' && isEventHandler)) return; if (currentScope && currentScope.has(name) || helpers.has(name) || (name === 'event' && isEventHandler)) return;
code.prependRight(node.start, 'ctx.'); code.prependRight(node.start, 'ctx.');
if (contextDependencies.has(name)) { if (scope.names.has(name)) {
contextDependencies.get(name).forEach(dependency => { scope.dependenciesForName.get(name).forEach(dependency => {
dependencies.add(dependency); dependencies.add(dependency);
}); });
} else if (!indexes.has(name)) { } else if (!indexes.has(name)) {
dependencies.add(name); dependencies.add(name);
compiler.expectedProperties.add(name);
} }
this.skip(); this.skip();
@ -59,7 +60,7 @@ export default class Expression {
}, },
leave(node: Node, parent: Node) { leave(node: Node, parent: Node) {
if (map.has(node)) scope = scope.parent; if (map.has(node)) currentScope = currentScope.parent;
} }
}); });

@ -16,11 +16,11 @@ export default class Node {
canUseInnerHTML: boolean; canUseInnerHTML: boolean;
var: string; var: string;
constructor(compiler: Generator, parent, info: any) { constructor(compiler: Generator, parent, scope, info: any) {
this.start = info.start;
this.end = info.end;
this.compiler = compiler; this.compiler = compiler;
this.parent = parent; this.parent = parent;
this.start = info.start;
this.end = info.end;
this.type = info.type; this.type = info.type;
} }

@ -5,9 +5,9 @@ import Block from '../../dom/Block';
export default class Tag extends Node { export default class Tag extends Node {
expression: Expression; expression: Expression;
constructor(compiler, parent, info) { constructor(compiler, parent, scope, info) {
super(compiler, parent, info); super(compiler, parent, scope, info);
this.expression = new Expression(compiler, this, info.expression); this.expression = new Expression(compiler, this, scope, info.expression);
} }
renameThisMethod( renameThisMethod(
@ -19,8 +19,8 @@ export default class Tag extends Node {
const hasChangeableIndex = Array.from(indexes).some(index => block.changeableIndexes.get(index)); const hasChangeableIndex = Array.from(indexes).some(index => block.changeableIndexes.get(index));
const shouldCache = ( const shouldCache = (
this.expression.type !== 'Identifier' || this.expression.node.type !== 'Identifier' ||
block.contexts.has(this.expression.name) || block.contexts.has(this.expression.node.name) ||
hasChangeableIndex hasChangeableIndex
); );

@ -2,6 +2,7 @@ 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';
import Slot from '../Slot';
import Text from '../Text'; import Text from '../Text';
import MustacheTag from '../MustacheTag'; import MustacheTag from '../MustacheTag';
import Window from '../Window'; import Window from '../Window';
@ -13,6 +14,7 @@ function getConstructor(type): typeof Node {
case 'EachBlock': return EachBlock; case 'EachBlock': return EachBlock;
case 'Element': return Element; case 'Element': return Element;
case 'IfBlock': return IfBlock; case 'IfBlock': return IfBlock;
case 'Slot': return Slot;
case 'Text': return Text; case 'Text': return Text;
case 'MustacheTag': return MustacheTag; case 'MustacheTag': return MustacheTag;
case 'Window': return Window; case 'Window': return Window;
@ -20,11 +22,11 @@ function getConstructor(type): typeof Node {
} }
} }
export default function mapChildren(compiler, parent, children: any[]) { export default function mapChildren(compiler, parent, scope, children: any[]) {
let last = null; let last = null;
return children.map(child => { return children.map(child => {
const constructor = getConstructor(child.type); const constructor = getConstructor(child.type);
const node = new constructor(compiler, parent, child); const node = new constructor(compiler, parent, scope, child);
if (last) last.next = node; if (last) last.next = node;
node.prev = last; node.prev = last;

@ -50,21 +50,19 @@ export default function visitComponent(
}); });
function getAttributeValue(attribute) { function getAttributeValue(attribute) {
if (attribute.value === true) return `true`; if (attribute.isTrue) return `true`;
if (attribute.value.length === 0) return `''`; if (attribute.chunks.length === 0) return `''`;
if (attribute.value.length === 1) { if (attribute.chunks.length === 1) {
const chunk = attribute.value[0]; const chunk = attribute.chunks[0];
if (chunk.type === 'Text') { if (chunk.type === 'Text') {
return stringify(chunk.data); return stringify(chunk.data);
} }
block.contextualise(chunk.expression); return chunk.snippet;
const { snippet } = chunk.metadata;
return snippet;
} }
return '`' + attribute.value.map(stringifyAttribute).join('') + '`'; return '`' + attribute.chunks.map(stringifyAttribute).join('') + '`';
} }
const props = usesSpread const props = usesSpread
@ -72,8 +70,7 @@ export default function visitComponent(
attributes attributes
.map(attribute => { .map(attribute => {
if (attribute.type === 'Spread') { if (attribute.type === 'Spread') {
block.contextualise(attribute.expression); return attribute.expression.snippet;
return attribute.metadata.snippet;
} else { } else {
return `{ ${attribute.name}: ${getAttributeValue(attribute)} }`; return `{ ${attribute.name}: ${getAttributeValue(attribute)} }`;
} }

@ -8,8 +8,7 @@ export default function visitEachBlock(
block: Block, block: Block,
node: Node node: Node
) { ) {
block.contextualise(node.expression); const { snippet } = node.expression;
const { snippet } = node.metadata;
const open = `\${ ${node.else ? `${snippet}.length ? ` : ''}${snippet}.map(${node.index ? `(${node.context}, ${node.index})` : `(${node.context})`} => \``; const open = `\${ ${node.else ? `${snippet}.length ? ` : ''}${snippet}.map(${node.index ? `(${node.context}, ${node.index})` : `(${node.context})`} => \``;
generator.append(open); generator.append(open);

@ -145,12 +145,12 @@ function cjs(
helpers: { name: string, alias: string }[], helpers: { name: string, alias: string }[],
dependencies: Dependency[] dependencies: Dependency[]
) { ) {
const SHARED = '__shared'; const helperDeclarations = helpers && (
helpers.map(h => `${h.alias === h.name ? h.name : `${h.name}: ${h.alias}`}`).join(', ')
);
const helperBlock = helpers && ( const helperBlock = helpers && (
`var ${SHARED} = require(${JSON.stringify(sharedPath)});\n` + `var { ${helperDeclarations} } = require(${JSON.stringify(sharedPath)});\n`
helpers.map(helper => {
return `var ${helper.alias} = ${SHARED}.${helper.name};`;
}).join('\n')
); );
const requireBlock = dependencies.length > 0 && ( const requireBlock = dependencies.length > 0 && (

@ -113,7 +113,8 @@ export default function tag(parser: Parser) {
const type = metaTags.has(name) const type = metaTags.has(name)
? metaTags.get(name) ? metaTags.get(name)
: /[A-Z]/.test(name[0]) ? 'Component' : 'Element'; : /[A-Z]/.test(name[0]) ? 'Component'
: name === 'slot' ? 'Slot' : 'Element';
const element: Node = { const element: Node = {
start, start,

@ -1,6 +1,7 @@
import validateElement from './validateElement'; import validateElement from './validateElement';
import validateWindow from './validateWindow'; import validateWindow from './validateWindow';
import validateHead from './validateHead'; import validateHead from './validateHead';
import validateSlot from './validateSlot';
import a11y from './a11y'; import a11y from './a11y';
import fuzzymatch from '../utils/fuzzymatch' import fuzzymatch from '../utils/fuzzymatch'
import flattenReference from '../../utils/flattenReference'; import flattenReference from '../../utils/flattenReference';
@ -29,6 +30,10 @@ export default function validateHtml(validator: Validator, html: Node) {
validateHead(validator, node, refs, refCallees); validateHead(validator, node, refs, refCallees);
} }
else if (node.type === 'Slot') {
validateSlot(validator, node);
}
else if (node.type === 'Component' || node.name === 'svelte:self' || node.name === 'svelte:component') { else if (node.type === 'Component' || node.name === 'svelte:self' || node.name === 'svelte:component') {
validateElement( validateElement(
validator, validator,

@ -0,0 +1,56 @@
import * as namespaces from '../../utils/namespaces';
import validateEventHandler from './validateEventHandler';
import validate, { Validator } from '../index';
import { Node } from '../../interfaces';
export default function validateSlot(
validator: Validator,
node: Node
) {
node.attributes.forEach(attr => {
if (attr.type !== 'Attribute') {
validator.error(attr, {
code: `invalid-slot-directive`,
message: `<slot> cannot have directives`
});
}
if (attr.name !== 'name') {
validator.error(attr, {
code: `invalid-slot-attribute`,
message: `"name" is the only attribute permitted on <slot> elements`
});
}
if (attr.value.length !== 1 || attr.value[0].type !== 'Text') {
validator.error(attr, {
code: `dynamic-slot-name`,
message: `<slot> name cannot be dynamic`
});
}
const slotName = attr.value[0].data;
if (slotName === 'default') {
validator.error(attr, {
code: `invalid-slot-name`,
message: `default is a reserved word — it cannot be used as a slot name`
});
}
// 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);
});
// if (node.attributes.length === 0) && validator.slots.has('default')) {
// validator.error(node, {
// code: `duplicate-slot`,
// message: `duplicate default <slot> element`
// });
// }
}

@ -7,7 +7,7 @@ export default {
}, },
html: `<svg><rect x="0" y="0" width="100" height="100"></rect></svg>`, html: `<svg><rect x="0" y="0" width="100" height="100"></rect></svg>`,
test ( assert, component, target ) { test ( assert, component, target ) {
const svg = target.querySelector( 'svg' ); const svg = target.querySelector( 'svg' );
const rect = target.querySelector( 'rect' ); const rect = target.querySelector( 'rect' );

Loading…
Cancel
Save