mirror of https://github.com/sveltejs/svelte
commit
2616ab1520
@ -1,22 +1,16 @@
|
||||
.DS_Store
|
||||
node_modules
|
||||
compiler
|
||||
ssr
|
||||
shared.js
|
||||
scratch
|
||||
!test/compiler
|
||||
!test/ssr
|
||||
.nyc_output
|
||||
coverage
|
||||
coverage.lcov
|
||||
test/sourcemaps/samples/*/output.js
|
||||
test/sourcemaps/samples/*/output.js.map
|
||||
_actual.*
|
||||
_actual-v2.*
|
||||
_actual-bundle.*
|
||||
src/generators/dom/shared.ts
|
||||
package-lock.json
|
||||
.idea/
|
||||
*.iml
|
||||
store.umd.js
|
||||
yarn-error.log
|
||||
node_modules
|
||||
/compiler/
|
||||
/ssr/
|
||||
/shared.js
|
||||
/scratch/
|
||||
/coverage/
|
||||
/coverage.lcov/
|
||||
/test/sourcemaps/samples/*/output.js
|
||||
/test/sourcemaps/samples/*/output.js.map
|
||||
/src/compile/shared.ts
|
||||
/package-lock.json
|
||||
/store.umd.js
|
||||
/yarn-error.log
|
||||
_actual*.*
|
@ -0,0 +1,18 @@
|
||||
import Node from './shared/Node';
|
||||
import Expression from './shared/Expression';
|
||||
|
||||
export default class Action extends Node {
|
||||
type: 'Action';
|
||||
name: string;
|
||||
expression: Expression;
|
||||
|
||||
constructor(compiler, parent, scope, info) {
|
||||
super(compiler, parent, scope, info);
|
||||
|
||||
this.name = info.name;
|
||||
|
||||
this.expression = info.expression
|
||||
? new Expression(compiler, this, scope, info.expression)
|
||||
: null;
|
||||
}
|
||||
}
|
@ -0,0 +1,13 @@
|
||||
import Node from './shared/Node';
|
||||
import Block from '../dom/Block';
|
||||
import mapChildren from './shared/mapChildren';
|
||||
|
||||
export default class CatchBlock extends Node {
|
||||
block: Block;
|
||||
children: Node[];
|
||||
|
||||
constructor(compiler, parent, scope, info) {
|
||||
super(compiler, parent, scope, info);
|
||||
this.children = mapChildren(compiler, parent, scope, info.children);
|
||||
}
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
import Node from './shared/Node';
|
||||
|
||||
export default class Comment extends Node {
|
||||
type: 'Comment';
|
||||
data: string;
|
||||
|
||||
constructor(compiler, parent, scope, info) {
|
||||
super(compiler, parent, scope, info);
|
||||
this.data = info.data;
|
||||
}
|
||||
|
||||
ssr() {
|
||||
// Allow option to preserve comments, otherwise ignore
|
||||
if (this.compiler.options.preserveComments) {
|
||||
this.compiler.target.append(`<!--${this.data}-->`);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,13 @@
|
||||
import Node from './shared/Node';
|
||||
import Block from '../dom/Block';
|
||||
import mapChildren from './shared/mapChildren';
|
||||
|
||||
export default class ElseBlock extends Node {
|
||||
type: 'ElseBlock';
|
||||
children: Node[];
|
||||
|
||||
constructor(compiler, parent, scope, info) {
|
||||
super(compiler, parent, scope, info);
|
||||
this.children = mapChildren(compiler, this, scope, info.children);
|
||||
}
|
||||
}
|
@ -0,0 +1,84 @@
|
||||
import Node from './shared/Node';
|
||||
import Expression from './shared/Expression';
|
||||
import addToSet from '../../utils/addToSet';
|
||||
import flattenReference from '../../utils/flattenReference';
|
||||
import validCalleeObjects from '../../utils/validCalleeObjects';
|
||||
|
||||
export default class EventHandler extends Node {
|
||||
name: string;
|
||||
dependencies: Set<string>;
|
||||
expression: Node;
|
||||
callee: any; // TODO
|
||||
|
||||
usesComponent: boolean;
|
||||
usesContext: boolean;
|
||||
isCustomEvent: boolean;
|
||||
shouldHoist: boolean;
|
||||
|
||||
insertionPoint: number;
|
||||
args: Expression[];
|
||||
snippet: string;
|
||||
|
||||
constructor(compiler, parent, scope, info) {
|
||||
super(compiler, parent, scope, info);
|
||||
|
||||
this.name = info.name;
|
||||
this.dependencies = new Set();
|
||||
|
||||
if (info.expression) {
|
||||
this.callee = flattenReference(info.expression.callee);
|
||||
this.insertionPoint = info.expression.start;
|
||||
|
||||
this.usesComponent = !validCalleeObjects.has(this.callee.name);
|
||||
this.usesContext = false;
|
||||
|
||||
this.args = info.expression.arguments.map(param => {
|
||||
const expression = new Expression(compiler, this, scope, param);
|
||||
addToSet(this.dependencies, expression.dependencies);
|
||||
if (expression.usesContext) this.usesContext = true;
|
||||
return expression;
|
||||
});
|
||||
|
||||
this.snippet = `[✂${info.expression.start}-${info.expression.end}✂];`;
|
||||
} else {
|
||||
this.callee = null;
|
||||
this.insertionPoint = null;
|
||||
|
||||
this.args = null;
|
||||
this.usesComponent = true;
|
||||
this.usesContext = false;
|
||||
|
||||
this.snippet = null; // TODO handle shorthand events here?
|
||||
}
|
||||
|
||||
this.isCustomEvent = compiler.events.has(this.name);
|
||||
this.shouldHoist = !this.isCustomEvent && parent.hasAncestor('EachBlock');
|
||||
}
|
||||
|
||||
render(compiler, block, hoisted) { // TODO hoist more event handlers
|
||||
if (this.insertionPoint === null) return; // TODO handle shorthand events here?
|
||||
|
||||
if (!validCalleeObjects.has(this.callee.name)) {
|
||||
const component = hoisted ? `component` : block.alias(`component`);
|
||||
|
||||
// allow event.stopPropagation(), this.select() etc
|
||||
// TODO verify that it's a valid callee (i.e. built-in or declared method)
|
||||
if (this.callee.name[0] === '$' && !compiler.methods.has(this.callee.name)) {
|
||||
compiler.code.overwrite(
|
||||
this.insertionPoint,
|
||||
this.insertionPoint + 1,
|
||||
`${component}.store.`
|
||||
);
|
||||
} else {
|
||||
compiler.code.prependRight(
|
||||
this.insertionPoint,
|
||||
`${component}.`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
this.args.forEach(arg => {
|
||||
arg.overwriteThis(this.parent.var);
|
||||
});
|
||||
}
|
||||
}
|
@ -1,28 +1,35 @@
|
||||
import Node from './shared/Node';
|
||||
import { DomGenerator } from '../dom/index';
|
||||
import Compiler from '../Compiler';
|
||||
import mapChildren from './shared/mapChildren';
|
||||
import Block from '../dom/Block';
|
||||
import TemplateScope from './shared/TemplateScope';
|
||||
|
||||
export default class Fragment extends Node {
|
||||
block: Block;
|
||||
children: Node[];
|
||||
scope: TemplateScope;
|
||||
|
||||
constructor(compiler: Compiler, info: any) {
|
||||
const scope = new TemplateScope();
|
||||
super(compiler, null, scope, info);
|
||||
|
||||
this.scope = scope;
|
||||
this.children = mapChildren(compiler, this, scope, info.children);
|
||||
}
|
||||
|
||||
init() {
|
||||
this.block = new Block({
|
||||
generator: this.generator,
|
||||
compiler: this.compiler,
|
||||
name: '@create_main_fragment',
|
||||
key: null,
|
||||
|
||||
contexts: new Map(),
|
||||
indexes: new Map(),
|
||||
changeableIndexes: new Map(),
|
||||
|
||||
indexNames: new Map(),
|
||||
listNames: new Map(),
|
||||
|
||||
dependencies: new Set(),
|
||||
});
|
||||
|
||||
this.generator.blocks.push(this.block);
|
||||
this.compiler.target.blocks.push(this.block);
|
||||
this.initChildren(this.block, true, null);
|
||||
|
||||
this.block.hasUpdateMethod = true;
|
@ -0,0 +1,13 @@
|
||||
import Node from './shared/Node';
|
||||
import Block from '../dom/Block';
|
||||
import mapChildren from './shared/mapChildren';
|
||||
|
||||
export default class PendingBlock extends Node {
|
||||
block: Block;
|
||||
children: Node[];
|
||||
|
||||
constructor(compiler, parent, scope, info) {
|
||||
super(compiler, parent, scope, info);
|
||||
this.children = mapChildren(compiler, parent, scope, info.children);
|
||||
}
|
||||
}
|
@ -0,0 +1,13 @@
|
||||
import Node from './shared/Node';
|
||||
import Block from '../dom/Block';
|
||||
import mapChildren from './shared/mapChildren';
|
||||
|
||||
export default class ThenBlock extends Node {
|
||||
block: Block;
|
||||
children: Node[];
|
||||
|
||||
constructor(compiler, parent, scope, info) {
|
||||
super(compiler, parent, scope, info);
|
||||
this.children = mapChildren(compiler, parent, scope, info.children);
|
||||
}
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
import Node from './shared/Node';
|
||||
import Expression from './shared/Expression';
|
||||
|
||||
export default class Transition extends Node {
|
||||
type: 'Transition';
|
||||
name: string;
|
||||
expression: Expression;
|
||||
|
||||
constructor(compiler, parent, scope, info) {
|
||||
super(compiler, parent, scope, info);
|
||||
|
||||
this.name = info.name;
|
||||
|
||||
this.expression = info.expression
|
||||
? new Expression(compiler, this, scope, info.expression)
|
||||
: null;
|
||||
}
|
||||
}
|
@ -0,0 +1,173 @@
|
||||
import Compiler from '../../Compiler';
|
||||
import { walk } from 'estree-walker';
|
||||
import isReference from 'is-reference';
|
||||
import flattenReference from '../../../utils/flattenReference';
|
||||
import { createScopes } from '../../../utils/annotateWithScopes';
|
||||
import { Node } from '../../../interfaces';
|
||||
|
||||
const binaryOperators: Record<string, number> = {
|
||||
'**': 15,
|
||||
'*': 14,
|
||||
'/': 14,
|
||||
'%': 14,
|
||||
'+': 13,
|
||||
'-': 13,
|
||||
'<<': 12,
|
||||
'>>': 12,
|
||||
'>>>': 12,
|
||||
'<': 11,
|
||||
'<=': 11,
|
||||
'>': 11,
|
||||
'>=': 11,
|
||||
'in': 11,
|
||||
'instanceof': 11,
|
||||
'==': 10,
|
||||
'!=': 10,
|
||||
'===': 10,
|
||||
'!==': 10,
|
||||
'&': 9,
|
||||
'^': 8,
|
||||
'|': 7
|
||||
};
|
||||
|
||||
const logicalOperators: Record<string, number> = {
|
||||
'&&': 6,
|
||||
'||': 5
|
||||
};
|
||||
|
||||
const precedence: Record<string, (node?: Node) => number> = {
|
||||
Literal: () => 21,
|
||||
Identifier: () => 21,
|
||||
ParenthesizedExpression: () => 20,
|
||||
MemberExpression: () => 19,
|
||||
NewExpression: () => 19, // can be 18 (if no args) but makes no practical difference
|
||||
CallExpression: () => 19,
|
||||
UpdateExpression: () => 17,
|
||||
UnaryExpression: () => 16,
|
||||
BinaryExpression: (node: Node) => binaryOperators[node.operator],
|
||||
LogicalExpression: (node: Node) => logicalOperators[node.operator],
|
||||
ConditionalExpression: () => 4,
|
||||
AssignmentExpression: () => 3,
|
||||
YieldExpression: () => 2,
|
||||
SpreadElement: () => 1,
|
||||
SequenceExpression: () => 0
|
||||
};
|
||||
|
||||
export default class Expression {
|
||||
compiler: Compiler;
|
||||
node: any;
|
||||
snippet: string;
|
||||
|
||||
usesContext: boolean;
|
||||
references: Set<string>;
|
||||
dependencies: Set<string>;
|
||||
|
||||
thisReferences: Array<{ start: number, end: number }>;
|
||||
|
||||
constructor(compiler, parent, scope, info) {
|
||||
// TODO revert to direct property access in prod?
|
||||
Object.defineProperties(this, {
|
||||
compiler: {
|
||||
value: compiler
|
||||
}
|
||||
});
|
||||
|
||||
this.node = info;
|
||||
this.thisReferences = [];
|
||||
|
||||
this.snippet = `[✂${info.start}-${info.end}✂]`;
|
||||
|
||||
this.usesContext = false;
|
||||
|
||||
const dependencies = new Set();
|
||||
|
||||
const { code, helpers } = compiler;
|
||||
|
||||
let { map, scope: currentScope } = createScopes(info);
|
||||
|
||||
const isEventHandler = parent.type === 'EventHandler';
|
||||
const expression = this;
|
||||
const isSynthetic = parent.isSynthetic;
|
||||
|
||||
walk(info, {
|
||||
enter(node: any, parent: any, key: string) {
|
||||
// don't manipulate shorthand props twice
|
||||
if (key === 'value' && parent.shorthand) return;
|
||||
|
||||
code.addSourcemapLocation(node.start);
|
||||
code.addSourcemapLocation(node.end);
|
||||
|
||||
if (map.has(node)) {
|
||||
currentScope = map.get(node);
|
||||
return;
|
||||
}
|
||||
|
||||
if (node.type === 'ThisExpression') {
|
||||
expression.thisReferences.push(node);
|
||||
}
|
||||
|
||||
if (isReference(node, parent)) {
|
||||
const { name } = flattenReference(node);
|
||||
|
||||
if (currentScope.has(name) || (name === 'event' && isEventHandler)) return;
|
||||
|
||||
if (compiler.helpers.has(name)) {
|
||||
let object = node;
|
||||
while (object.type === 'MemberExpression') object = object.object;
|
||||
|
||||
const alias = compiler.templateVars.get(`helpers-${name}`);
|
||||
if (alias !== name) code.overwrite(object.start, object.end, alias);
|
||||
return;
|
||||
}
|
||||
|
||||
expression.usesContext = true;
|
||||
|
||||
if (!isSynthetic) {
|
||||
// <option> value attribute could be synthetic — avoid double editing
|
||||
code.prependRight(node.start, key === 'key' && parent.shorthand
|
||||
? `${name}: ctx.`
|
||||
: 'ctx.');
|
||||
}
|
||||
|
||||
if (scope.names.has(name)) {
|
||||
scope.dependenciesForName.get(name).forEach(dependency => {
|
||||
dependencies.add(dependency);
|
||||
});
|
||||
} else {
|
||||
dependencies.add(name);
|
||||
compiler.expectedProperties.add(name);
|
||||
}
|
||||
|
||||
if (node.type === 'MemberExpression') {
|
||||
walk(node, {
|
||||
enter(node) {
|
||||
code.addSourcemapLocation(node.start);
|
||||
code.addSourcemapLocation(node.end);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
this.skip();
|
||||
}
|
||||
},
|
||||
|
||||
leave(node: Node, parent: Node) {
|
||||
if (map.has(node)) currentScope = currentScope.parent;
|
||||
}
|
||||
});
|
||||
|
||||
this.dependencies = dependencies;
|
||||
}
|
||||
|
||||
getPrecedence() {
|
||||
return this.node.type in precedence ? precedence[this.node.type](this.node) : 0;
|
||||
}
|
||||
|
||||
overwriteThis(name) {
|
||||
this.thisReferences.forEach(ref => {
|
||||
this.compiler.code.overwrite(ref.start, ref.end, name, {
|
||||
storeName: true
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
@ -0,0 +1,56 @@
|
||||
import Node from './Node';
|
||||
import Expression from './Expression';
|
||||
import Block from '../../dom/Block';
|
||||
|
||||
export default class Tag extends Node {
|
||||
expression: Expression;
|
||||
shouldCache: boolean;
|
||||
|
||||
constructor(compiler, parent, scope, info) {
|
||||
super(compiler, parent, scope, info);
|
||||
this.expression = new Expression(compiler, this, scope, info.expression);
|
||||
|
||||
this.shouldCache = (
|
||||
info.expression.type !== 'Identifier' ||
|
||||
(this.expression.dependencies.size && scope.names.has(info.expression.name))
|
||||
);
|
||||
}
|
||||
|
||||
init(block: Block) {
|
||||
this.cannotUseInnerHTML();
|
||||
this.var = block.getUniqueName(this.type === 'MustacheTag' ? 'text' : 'raw');
|
||||
block.addDependencies(this.expression.dependencies);
|
||||
}
|
||||
|
||||
renameThisMethod(
|
||||
block: Block,
|
||||
update: ((value: string) => string)
|
||||
) {
|
||||
const { snippet, dependencies } = this.expression;
|
||||
|
||||
const value = this.shouldCache && block.getUniqueName(`${this.var}_value`);
|
||||
const content = this.shouldCache ? value : snippet;
|
||||
|
||||
if (this.shouldCache) block.addVariable(value, snippet);
|
||||
|
||||
if (dependencies.size) {
|
||||
const changedCheck = (
|
||||
(block.hasOutroMethod ? `#outroing || ` : '') +
|
||||
[...dependencies].map((dependency: string) => `changed.${dependency}`).join(' || ')
|
||||
);
|
||||
|
||||
const updateCachedValue = `${value} !== (${value} = ${snippet})`;
|
||||
|
||||
const condition = this.shouldCache ?
|
||||
(dependencies.size ? `(${changedCheck}) && ${updateCachedValue}` : updateCachedValue) :
|
||||
changedCheck;
|
||||
|
||||
block.builders.update.addConditional(
|
||||
condition,
|
||||
update(content)
|
||||
);
|
||||
}
|
||||
|
||||
return { init: content };
|
||||
}
|
||||
}
|
@ -0,0 +1,19 @@
|
||||
export default class TemplateScope {
|
||||
names: Set<string>;
|
||||
dependenciesForName: Map<string, string>;
|
||||
|
||||
constructor(parent?: TemplateScope) {
|
||||
this.names = new Set(parent ? parent.names : []);
|
||||
this.dependenciesForName = new Map(parent ? parent.dependenciesForName : []);
|
||||
}
|
||||
|
||||
add(name, dependencies) {
|
||||
this.names.add(name);
|
||||
this.dependenciesForName.set(name, dependencies);
|
||||
return this;
|
||||
}
|
||||
|
||||
child() {
|
||||
return new TemplateScope(this);
|
||||
}
|
||||
}
|
@ -0,0 +1,47 @@
|
||||
import AwaitBlock from '../AwaitBlock';
|
||||
import Comment from '../Comment';
|
||||
import Component from '../Component';
|
||||
import EachBlock from '../EachBlock';
|
||||
import Element from '../Element';
|
||||
import Head from '../Head';
|
||||
import IfBlock from '../IfBlock';
|
||||
import MustacheTag from '../MustacheTag';
|
||||
import RawMustacheTag from '../RawMustacheTag';
|
||||
import Slot from '../Slot';
|
||||
import Text from '../Text';
|
||||
import Title from '../Title';
|
||||
import Window from '../Window';
|
||||
import Node from './Node';
|
||||
|
||||
function getConstructor(type): typeof Node {
|
||||
switch (type) {
|
||||
case 'AwaitBlock': return AwaitBlock;
|
||||
case 'Comment': return Comment;
|
||||
case 'Component': return Component;
|
||||
case 'EachBlock': return EachBlock;
|
||||
case 'Element': return Element;
|
||||
case 'Head': return Head;
|
||||
case 'IfBlock': return IfBlock;
|
||||
case 'MustacheTag': return MustacheTag;
|
||||
case 'RawMustacheTag': return RawMustacheTag;
|
||||
case 'Slot': return Slot;
|
||||
case 'Text': return Text;
|
||||
case 'Title': return Title;
|
||||
case 'Window': return Window;
|
||||
default: throw new Error(`Not implemented: ${type}`);
|
||||
}
|
||||
}
|
||||
|
||||
export default function mapChildren(compiler, parent, scope, children: any[]) {
|
||||
let last = null;
|
||||
return children.map(child => {
|
||||
const constructor = getConstructor(child.type);
|
||||
const node = new constructor(compiler, parent, scope, child);
|
||||
|
||||
if (last) last.next = node;
|
||||
node.prev = last;
|
||||
last = node;
|
||||
|
||||
return node;
|
||||
});
|
||||
}
|
@ -1,7 +0,0 @@
|
||||
import Node from './shared/Node';
|
||||
|
||||
export default class Action extends Node {
|
||||
name: string;
|
||||
value: Node[]
|
||||
expression: Node
|
||||
}
|
@ -1,7 +0,0 @@
|
||||
import Node from './shared/Node';
|
||||
import Block from '../dom/Block';
|
||||
|
||||
export default class CatchBlock extends Node {
|
||||
block: Block;
|
||||
children: Node[];
|
||||
}
|
@ -1,5 +0,0 @@
|
||||
import Node from './shared/Node';
|
||||
|
||||
export default class Comment extends Node {
|
||||
type: 'Comment'
|
||||
}
|
@ -1,8 +0,0 @@
|
||||
import Node from './shared/Node';
|
||||
import Block from '../dom/Block';
|
||||
|
||||
export default class ElseBlock extends Node {
|
||||
type: 'ElseBlock';
|
||||
children: Node[];
|
||||
block: Block;
|
||||
}
|
@ -1,7 +0,0 @@
|
||||
import Node from './shared/Node';
|
||||
|
||||
export default class EventHandler extends Node {
|
||||
name: string;
|
||||
value: Node[]
|
||||
expression: Node
|
||||
}
|
@ -1,7 +0,0 @@
|
||||
import Node from './shared/Node';
|
||||
import Block from '../dom/Block';
|
||||
|
||||
export default class PendingBlock extends Node {
|
||||
block: Block;
|
||||
children: Node[];
|
||||
}
|
@ -1,7 +0,0 @@
|
||||
import Node from './shared/Node';
|
||||
|
||||
export default class Ref extends Node {
|
||||
name: string;
|
||||
value: Node[]
|
||||
expression: Node
|
||||
}
|
@ -1,7 +0,0 @@
|
||||
import Node from './shared/Node';
|
||||
import Block from '../dom/Block';
|
||||
|
||||
export default class ThenBlock extends Node {
|
||||
block: Block;
|
||||
children: Node[];
|
||||
}
|
@ -1,7 +0,0 @@
|
||||
import Node from './shared/Node';
|
||||
|
||||
export default class Transition extends Node {
|
||||
name: string;
|
||||
value: Node[]
|
||||
expression: Node
|
||||
}
|
@ -1,54 +0,0 @@
|
||||
import Node from './shared/Node';
|
||||
import Attribute from './Attribute';
|
||||
import AwaitBlock from './AwaitBlock';
|
||||
import Action from './Action';
|
||||
import Binding from './Binding';
|
||||
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 EventHandler from './EventHandler';
|
||||
import Fragment from './Fragment';
|
||||
import Head from './Head';
|
||||
import IfBlock from './IfBlock';
|
||||
import MustacheTag from './MustacheTag';
|
||||
import PendingBlock from './PendingBlock';
|
||||
import RawMustacheTag from './RawMustacheTag';
|
||||
import Ref from './Ref';
|
||||
import Slot from './Slot';
|
||||
import Text from './Text';
|
||||
import ThenBlock from './ThenBlock';
|
||||
import Title from './Title';
|
||||
import Transition from './Transition';
|
||||
import Window from './Window';
|
||||
|
||||
const nodes: Record<string, any> = {
|
||||
Attribute,
|
||||
AwaitBlock,
|
||||
Action,
|
||||
Binding,
|
||||
CatchBlock,
|
||||
Comment,
|
||||
Component,
|
||||
EachBlock,
|
||||
Element,
|
||||
ElseBlock,
|
||||
EventHandler,
|
||||
Fragment,
|
||||
Head,
|
||||
IfBlock,
|
||||
MustacheTag,
|
||||
PendingBlock,
|
||||
RawMustacheTag,
|
||||
Ref,
|
||||
Slot,
|
||||
Text,
|
||||
ThenBlock,
|
||||
Title,
|
||||
Transition,
|
||||
Window
|
||||
};
|
||||
|
||||
export default nodes;
|
@ -1,45 +0,0 @@
|
||||
import Node from './Node';
|
||||
import Block from '../../dom/Block';
|
||||
|
||||
export default class Tag extends Node {
|
||||
renameThisMethod(
|
||||
block: Block,
|
||||
update: ((value: string) => string)
|
||||
) {
|
||||
const { indexes } = block.contextualise(this.expression);
|
||||
const { dependencies, snippet } = this.metadata;
|
||||
|
||||
const hasChangeableIndex = Array.from(indexes).some(index => block.changeableIndexes.get(index));
|
||||
|
||||
const shouldCache = (
|
||||
this.expression.type !== 'Identifier' ||
|
||||
block.contexts.has(this.expression.name) ||
|
||||
hasChangeableIndex
|
||||
);
|
||||
|
||||
const value = shouldCache && block.getUniqueName(`${this.var}_value`);
|
||||
const content = shouldCache ? value : snippet;
|
||||
|
||||
if (shouldCache) block.addVariable(value, snippet);
|
||||
|
||||
if (dependencies.length || hasChangeableIndex) {
|
||||
const changedCheck = (
|
||||
(block.hasOutroMethod ? `#outroing || ` : '') +
|
||||
dependencies.map((dependency: string) => `changed.${dependency}`).join(' || ')
|
||||
);
|
||||
|
||||
const updateCachedValue = `${value} !== (${value} = ${snippet})`;
|
||||
|
||||
const condition = shouldCache ?
|
||||
(dependencies.length ? `(${changedCheck}) && ${updateCachedValue}` : updateCachedValue) :
|
||||
changedCheck;
|
||||
|
||||
block.builders.update.addConditional(
|
||||
condition,
|
||||
update(content)
|
||||
);
|
||||
}
|
||||
|
||||
return { init: content };
|
||||
}
|
||||
}
|
@ -1,107 +0,0 @@
|
||||
import { stringify } from '../../../utils/stringify';
|
||||
import getExpressionPrecedence from '../../../utils/getExpressionPrecedence';
|
||||
import Node from './Node';
|
||||
import Attribute from '../Attribute';
|
||||
import Block from '../../dom/Block';
|
||||
|
||||
type MungedAttribute = {
|
||||
spread: boolean;
|
||||
name: string;
|
||||
value: string | true;
|
||||
dependencies: string[];
|
||||
dynamic: boolean;
|
||||
}
|
||||
|
||||
export default function mungeAttribute(attribute: Node, block: Block): MungedAttribute {
|
||||
if (attribute.type === 'Spread') {
|
||||
block.contextualise(attribute.expression); // TODO remove
|
||||
const { dependencies, snippet } = attribute.metadata;
|
||||
|
||||
return {
|
||||
spread: true,
|
||||
name: null,
|
||||
value: snippet,
|
||||
dynamic: dependencies.length > 0,
|
||||
dependencies
|
||||
};
|
||||
}
|
||||
|
||||
if (attribute.value === true) {
|
||||
// attributes without values, e.g. <textarea readonly>
|
||||
return {
|
||||
spread: false,
|
||||
name: attribute.name,
|
||||
value: true,
|
||||
dynamic: false,
|
||||
dependencies: []
|
||||
};
|
||||
}
|
||||
|
||||
if (attribute.value.length === 0) {
|
||||
return {
|
||||
spread: false,
|
||||
name: attribute.name,
|
||||
value: `''`,
|
||||
dynamic: false,
|
||||
dependencies: []
|
||||
};
|
||||
}
|
||||
|
||||
if (attribute.value.length === 1) {
|
||||
const value = attribute.value[0];
|
||||
|
||||
if (value.type === 'Text') {
|
||||
// static attributes
|
||||
return {
|
||||
spread: false,
|
||||
name: attribute.name,
|
||||
value: stringify(value.data),
|
||||
dynamic: false,
|
||||
dependencies: []
|
||||
};
|
||||
}
|
||||
|
||||
// simple dynamic attributes
|
||||
block.contextualise(value.expression); // TODO remove
|
||||
const { dependencies, snippet } = value.metadata;
|
||||
|
||||
// TODO only update attributes that have changed
|
||||
return {
|
||||
spread: false,
|
||||
name: attribute.name,
|
||||
value: snippet,
|
||||
dependencies,
|
||||
dynamic: true
|
||||
};
|
||||
}
|
||||
|
||||
// otherwise we're dealing with a complex dynamic attribute
|
||||
const allDependencies = new Set();
|
||||
|
||||
const value =
|
||||
(attribute.value[0].type === 'Text' ? '' : `"" + `) +
|
||||
attribute.value
|
||||
.map((chunk: Node) => {
|
||||
if (chunk.type === 'Text') {
|
||||
return stringify(chunk.data);
|
||||
} else {
|
||||
block.contextualise(chunk.expression); // TODO remove
|
||||
const { dependencies, snippet } = chunk.metadata;
|
||||
|
||||
dependencies.forEach((dependency: string) => {
|
||||
allDependencies.add(dependency);
|
||||
});
|
||||
|
||||
return getExpressionPrecedence(chunk.expression) <= 13 ? `(${snippet})` : snippet;
|
||||
}
|
||||
})
|
||||
.join(' + ');
|
||||
|
||||
return {
|
||||
spread: false,
|
||||
name: attribute.name,
|
||||
value,
|
||||
dependencies: Array.from(allDependencies),
|
||||
dynamic: true
|
||||
};
|
||||
}
|
@ -1,49 +0,0 @@
|
||||
import deindent from '../../utils/deindent';
|
||||
import flattenReference from '../../utils/flattenReference';
|
||||
import { SsrGenerator } from './index';
|
||||
import { Node } from '../../interfaces';
|
||||
import getObject from '../../utils/getObject';
|
||||
|
||||
interface BlockOptions {
|
||||
// TODO
|
||||
}
|
||||
|
||||
export default class Block {
|
||||
generator: SsrGenerator;
|
||||
conditions: string[];
|
||||
|
||||
contexts: Map<string, string>;
|
||||
indexes: Map<string, string>;
|
||||
contextDependencies: Map<string, string[]>;
|
||||
|
||||
constructor(options: BlockOptions) {
|
||||
Object.assign(this, options);
|
||||
}
|
||||
|
||||
addBinding(binding: Node, name: string) {
|
||||
const conditions = [`!('${binding.name}' in state)`].concat(
|
||||
// TODO handle contextual bindings...
|
||||
this.conditions.map(c => `(${c})`)
|
||||
);
|
||||
|
||||
const { name: prop } = getObject(binding.value);
|
||||
|
||||
this.generator.bindings.push(deindent`
|
||||
if (${conditions.join('&&')}) {
|
||||
tmp = ${name}.data();
|
||||
if ('${prop}' in tmp) {
|
||||
state.${binding.name} = tmp.${prop};
|
||||
settled = false;
|
||||
}
|
||||
}
|
||||
`);
|
||||
}
|
||||
|
||||
child(options: BlockOptions) {
|
||||
return new Block(Object.assign({}, this, options, { parent: this }));
|
||||
}
|
||||
|
||||
contextualise(expression: Node, context?: string, isEventHandler?: boolean) {
|
||||
return this.generator.contextualise(this.contexts, this.indexes, expression, context, isEventHandler);
|
||||
}
|
||||
}
|
@ -1,14 +0,0 @@
|
||||
import { SsrGenerator } from './index';
|
||||
import Block from './Block';
|
||||
import { Node } from '../../interfaces';
|
||||
|
||||
export type Visitor = (
|
||||
generator: SsrGenerator,
|
||||
block: Block,
|
||||
node: Node
|
||||
) => void;
|
||||
|
||||
export interface AppendTarget {
|
||||
slots: Record<string, string>;
|
||||
slotStack: string[]
|
||||
}
|
@ -1,13 +0,0 @@
|
||||
import visitors from './visitors/index';
|
||||
import { SsrGenerator } from './index';
|
||||
import Block from './Block';
|
||||
import { Node } from '../../interfaces';
|
||||
|
||||
export default function visit(
|
||||
generator: SsrGenerator,
|
||||
block: Block,
|
||||
node: Node
|
||||
) {
|
||||
const visitor = visitors[node.type];
|
||||
visitor(generator, block, node);
|
||||
}
|
@ -1,40 +0,0 @@
|
||||
import visit from '../visit';
|
||||
import { SsrGenerator } from '../index';
|
||||
import Block from '../Block';
|
||||
import { Node } from '../../../interfaces';
|
||||
|
||||
export default function visitAwaitBlock(
|
||||
generator: SsrGenerator,
|
||||
block: Block,
|
||||
node: Node
|
||||
) {
|
||||
block.contextualise(node.expression);
|
||||
const { dependencies, snippet } = node.metadata;
|
||||
|
||||
// TODO should this be the generator's job? It's duplicated between
|
||||
// here and the equivalent DOM compiler visitor
|
||||
const contexts = new Map(block.contexts);
|
||||
contexts.set(node.value, '__value');
|
||||
|
||||
const contextDependencies = new Map(block.contextDependencies);
|
||||
contextDependencies.set(node.value, dependencies);
|
||||
|
||||
const childBlock = block.child({
|
||||
contextDependencies,
|
||||
contexts
|
||||
});
|
||||
|
||||
generator.append('${(function(__value) { if(__isPromise(__value)) return `');
|
||||
|
||||
node.pending.children.forEach((child: Node) => {
|
||||
visit(generator, childBlock, child);
|
||||
});
|
||||
|
||||
generator.append('`; return `');
|
||||
|
||||
node.then.children.forEach((child: Node) => {
|
||||
visit(generator, childBlock, child);
|
||||
});
|
||||
|
||||
generator.append(`\`;}(${snippet})) }`);
|
||||
}
|
@ -1,14 +0,0 @@
|
||||
import { SsrGenerator } from '../index';
|
||||
import Block from '../Block';
|
||||
import { Node } from '../../../interfaces';
|
||||
|
||||
export default function visitComment(
|
||||
generator: SsrGenerator,
|
||||
block: Block,
|
||||
node: Node
|
||||
) {
|
||||
// Allow option to preserve comments, otherwise ignore
|
||||
if (generator && generator.options && generator.options.preserveComments) {
|
||||
generator.append(`<!--${node.data}-->`);
|
||||
}
|
||||
}
|
@ -1,135 +0,0 @@
|
||||
import flattenReference from '../../../utils/flattenReference';
|
||||
import visit from '../visit';
|
||||
import { SsrGenerator } from '../index';
|
||||
import Block from '../Block';
|
||||
import { AppendTarget } from '../interfaces';
|
||||
import { Node } from '../../../interfaces';
|
||||
import getObject from '../../../utils/getObject';
|
||||
import getTailSnippet from '../../../utils/getTailSnippet';
|
||||
import { escape, escapeTemplate, stringify } from '../../../utils/stringify';
|
||||
|
||||
export default function visitComponent(
|
||||
generator: SsrGenerator,
|
||||
block: Block,
|
||||
node: Node
|
||||
) {
|
||||
function stringifyAttribute(chunk: Node) {
|
||||
if (chunk.type === 'Text') {
|
||||
return escapeTemplate(escape(chunk.data));
|
||||
}
|
||||
if (chunk.type === 'MustacheTag') {
|
||||
block.contextualise(chunk.expression);
|
||||
const { snippet } = chunk.metadata;
|
||||
return '${__escape( ' + snippet + ')}';
|
||||
}
|
||||
}
|
||||
|
||||
const attributes: Node[] = [];
|
||||
const bindings: Node[] = [];
|
||||
|
||||
let usesSpread;
|
||||
|
||||
node.attributes.forEach((attribute: Node) => {
|
||||
if (attribute.type === 'Attribute' || attribute.type === 'Spread') {
|
||||
if (attribute.type === 'Spread') usesSpread = true;
|
||||
attributes.push(attribute);
|
||||
} else if (attribute.type === 'Binding') {
|
||||
bindings.push(attribute);
|
||||
}
|
||||
});
|
||||
|
||||
const bindingProps = bindings.map(binding => {
|
||||
const { name } = getObject(binding.value);
|
||||
const tail = binding.value.type === 'MemberExpression'
|
||||
? getTailSnippet(binding.value)
|
||||
: '';
|
||||
|
||||
const keypath = block.contexts.has(name)
|
||||
? `${name}${tail}`
|
||||
: `state.${name}${tail}`;
|
||||
return `${binding.name}: ${keypath}`;
|
||||
});
|
||||
|
||||
function getAttributeValue(attribute) {
|
||||
if (attribute.value === true) return `true`;
|
||||
if (attribute.value.length === 0) return `''`;
|
||||
|
||||
if (attribute.value.length === 1) {
|
||||
const chunk = attribute.value[0];
|
||||
if (chunk.type === 'Text') {
|
||||
return stringify(chunk.data);
|
||||
}
|
||||
|
||||
block.contextualise(chunk.expression);
|
||||
const { snippet } = chunk.metadata;
|
||||
return snippet;
|
||||
}
|
||||
|
||||
return '`' + attribute.value.map(stringifyAttribute).join('') + '`';
|
||||
}
|
||||
|
||||
const props = usesSpread
|
||||
? `Object.assign(${
|
||||
attributes
|
||||
.map(attribute => {
|
||||
if (attribute.type === 'Spread') {
|
||||
block.contextualise(attribute.expression);
|
||||
return attribute.metadata.snippet;
|
||||
} else {
|
||||
return `{ ${attribute.name}: ${getAttributeValue(attribute)} }`;
|
||||
}
|
||||
})
|
||||
.concat(bindingProps.map(p => `{ ${p} }`))
|
||||
.join(', ')
|
||||
})`
|
||||
: `{ ${attributes
|
||||
.map(attribute => `${attribute.name}: ${getAttributeValue(attribute)}`)
|
||||
.concat(bindingProps)
|
||||
.join(', ')} }`;
|
||||
|
||||
const isDynamicComponent = node.name === 'svelte:component';
|
||||
if (isDynamicComponent) block.contextualise(node.expression);
|
||||
|
||||
const expression = (
|
||||
node.name === 'svelte:self' ? generator.name :
|
||||
isDynamicComponent ? `((${node.metadata.snippet}) || __missingComponent)` :
|
||||
`%components-${node.name}`
|
||||
);
|
||||
|
||||
bindings.forEach(binding => {
|
||||
block.addBinding(binding, expression);
|
||||
});
|
||||
|
||||
let open = `\${${expression}._render(__result, ${props}`;
|
||||
|
||||
const options = [];
|
||||
options.push(`store: options.store`);
|
||||
|
||||
if (node.children.length) {
|
||||
const appendTarget: AppendTarget = {
|
||||
slots: { default: '' },
|
||||
slotStack: ['default']
|
||||
};
|
||||
|
||||
generator.appendTargets.push(appendTarget);
|
||||
|
||||
node.children.forEach((child: Node) => {
|
||||
visit(generator, block, child);
|
||||
});
|
||||
|
||||
const slotted = Object.keys(appendTarget.slots)
|
||||
.map(name => `${name}: () => \`${appendTarget.slots[name]}\``)
|
||||
.join(', ');
|
||||
|
||||
options.push(`slotted: { ${slotted} }`);
|
||||
|
||||
generator.appendTargets.pop();
|
||||
}
|
||||
|
||||
if (options.length) {
|
||||
open += `, { ${options.join(', ')} }`;
|
||||
}
|
||||
|
||||
generator.append(open);
|
||||
generator.append(')}');
|
||||
}
|
@ -1,57 +0,0 @@
|
||||
import visit from '../visit';
|
||||
import { SsrGenerator } from '../index';
|
||||
import Block from '../Block';
|
||||
import { Node } from '../../../interfaces';
|
||||
|
||||
export default function visitEachBlock(
|
||||
generator: SsrGenerator,
|
||||
block: Block,
|
||||
node: Node
|
||||
) {
|
||||
block.contextualise(node.expression);
|
||||
const { dependencies, snippet } = node.metadata;
|
||||
|
||||
const open = `\${ ${node.else ? `${snippet}.length ? ` : ''}${snippet}.map(${node.index ? `(${node.context}, ${node.index})` : `(${node.context})`} => \``;
|
||||
generator.append(open);
|
||||
|
||||
// TODO should this be the generator's job? It's duplicated between
|
||||
// here and the equivalent DOM compiler visitor
|
||||
const contexts = new Map(block.contexts);
|
||||
contexts.set(node.context, node.context);
|
||||
|
||||
const indexes = new Map(block.indexes);
|
||||
if (node.index) indexes.set(node.index, node.context);
|
||||
|
||||
const contextDependencies = new Map(block.contextDependencies);
|
||||
contextDependencies.set(node.context, dependencies);
|
||||
|
||||
if (node.destructuredContexts) {
|
||||
for (let i = 0; i < node.destructuredContexts.length; i += 1) {
|
||||
contexts.set(node.destructuredContexts[i], `${node.context}[${i}]`);
|
||||
contextDependencies.set(node.destructuredContexts[i], dependencies);
|
||||
}
|
||||
}
|
||||
|
||||
const childBlock = block.child({
|
||||
contexts,
|
||||
indexes,
|
||||
contextDependencies,
|
||||
});
|
||||
|
||||
node.children.forEach((child: Node) => {
|
||||
visit(generator, childBlock, child);
|
||||
});
|
||||
|
||||
const close = `\`).join("")`;
|
||||
generator.append(close);
|
||||
|
||||
if (node.else) {
|
||||
generator.append(` : \``);
|
||||
node.else.children.forEach((child: Node) => {
|
||||
visit(generator, block, child);
|
||||
});
|
||||
generator.append(`\``);
|
||||
}
|
||||
|
||||
generator.append('}');
|
||||
}
|
@ -1,106 +0,0 @@
|
||||
import visitComponent from './Component';
|
||||
import visitSlot from './Slot';
|
||||
import isVoidElementName from '../../../utils/isVoidElementName';
|
||||
import quoteIfNecessary from '../../../utils/quoteIfNecessary';
|
||||
import visit from '../visit';
|
||||
import { SsrGenerator } from '../index';
|
||||
import Element from '../../nodes/Element';
|
||||
import Block from '../Block';
|
||||
import { Node } from '../../../interfaces';
|
||||
import stringifyAttributeValue from './shared/stringifyAttributeValue';
|
||||
import { escape } from '../../../utils/stringify';
|
||||
|
||||
// source: https://gist.github.com/ArjanSchouten/0b8574a6ad7f5065a5e7
|
||||
const booleanAttributes = new Set('async autocomplete autofocus autoplay border challenge checked compact contenteditable controls default defer disabled formnovalidate frameborder hidden indeterminate ismap loop multiple muted nohref noresize noshade novalidate nowrap open readonly required reversed scoped scrolling seamless selected sortable spellcheck translate'.split(' '));
|
||||
|
||||
export default function visitElement(
|
||||
generator: SsrGenerator,
|
||||
block: Block,
|
||||
node: Element
|
||||
) {
|
||||
if (node.name === 'slot') {
|
||||
visitSlot(generator, block, node);
|
||||
return;
|
||||
}
|
||||
|
||||
let openingTag = `<${node.name}`;
|
||||
let textareaContents; // awkward special case
|
||||
|
||||
const slot = node.getStaticAttributeValue('slot');
|
||||
if (slot && node.hasAncestor('Component')) {
|
||||
const slot = node.attributes.find((attribute: Node) => attribute.name === 'slot');
|
||||
const slotName = slot.value[0].data;
|
||||
const appendTarget = generator.appendTargets[generator.appendTargets.length - 1];
|
||||
appendTarget.slotStack.push(slotName);
|
||||
appendTarget.slots[slotName] = '';
|
||||
}
|
||||
|
||||
if (node.attributes.find(attr => attr.type === 'Spread')) {
|
||||
// TODO dry this out
|
||||
const args = [];
|
||||
node.attributes.forEach((attribute: Node) => {
|
||||
if (attribute.type === 'Spread') {
|
||||
block.contextualise(attribute.expression);
|
||||
args.push(attribute.metadata.snippet);
|
||||
} else if (attribute.type === 'Attribute') {
|
||||
if (attribute.name === 'value' && node.name === 'textarea') {
|
||||
textareaContents = stringifyAttributeValue(block, attribute.value);
|
||||
} else if (attribute.value === true) {
|
||||
args.push(`{ ${quoteIfNecessary(attribute.name)}: true }`);
|
||||
} else if (
|
||||
booleanAttributes.has(attribute.name) &&
|
||||
attribute.value.length === 1 &&
|
||||
attribute.value[0].type !== 'Text'
|
||||
) {
|
||||
// a boolean attribute with one non-Text chunk
|
||||
block.contextualise(attribute.value[0].expression);
|
||||
args.push(`{ ${quoteIfNecessary(attribute.name)}: ${attribute.value[0].metadata.snippet} }`);
|
||||
} else {
|
||||
args.push(`{ ${quoteIfNecessary(attribute.name)}: \`${stringifyAttributeValue(block, attribute.value)}\` }`);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
openingTag += "${__spread([" + args.join(', ') + "])}";
|
||||
} else {
|
||||
node.attributes.forEach((attribute: Node) => {
|
||||
if (attribute.type !== 'Attribute') return;
|
||||
|
||||
if (attribute.name === 'value' && node.name === 'textarea') {
|
||||
textareaContents = stringifyAttributeValue(block, attribute.value);
|
||||
} else if (attribute.value === true) {
|
||||
openingTag += ` ${attribute.name}`;
|
||||
} else if (
|
||||
booleanAttributes.has(attribute.name) &&
|
||||
attribute.value.length === 1 &&
|
||||
attribute.value[0].type !== 'Text'
|
||||
) {
|
||||
// a boolean attribute with one non-Text chunk
|
||||
block.contextualise(attribute.value[0].expression);
|
||||
openingTag += '${' + attribute.value[0].metadata.snippet + ' ? " ' + attribute.name + '" : "" }';
|
||||
} else {
|
||||
openingTag += ` ${attribute.name}="${stringifyAttributeValue(block, attribute.value)}"`;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (node._cssRefAttribute) {
|
||||
openingTag += ` svelte-ref-${node._cssRefAttribute}`;
|
||||
}
|
||||
|
||||
openingTag += '>';
|
||||
|
||||
generator.append(openingTag);
|
||||
|
||||
if (node.name === 'textarea' && textareaContents !== undefined) {
|
||||
generator.append(textareaContents);
|
||||
} else {
|
||||
node.children.forEach((child: Node) => {
|
||||
visit(generator, block, child);
|
||||
});
|
||||
}
|
||||
|
||||
if (!isVoidElementName(node.name)) {
|
||||
generator.append(`</${node.name}>`);
|
||||
}
|
||||
}
|
@ -1,19 +0,0 @@
|
||||
import { SsrGenerator } from '../index';
|
||||
import Block from '../Block';
|
||||
import { Node } from '../../../interfaces';
|
||||
import stringifyAttributeValue from './shared/stringifyAttributeValue';
|
||||
import visit from '../visit';
|
||||
|
||||
export default function visitDocument(
|
||||
generator: SsrGenerator,
|
||||
block: Block,
|
||||
node: Node
|
||||
) {
|
||||
generator.append('${(__result.head += `');
|
||||
|
||||
node.children.forEach((child: Node) => {
|
||||
visit(generator, block, child);
|
||||
});
|
||||
|
||||
generator.append('`, "")}');
|
||||
}
|
@ -1,33 +0,0 @@
|
||||
import visit from '../visit';
|
||||
import { SsrGenerator } from '../index';
|
||||
import Block from '../Block';
|
||||
import { Node } from '../../../interfaces';
|
||||
|
||||
export default function visitIfBlock(
|
||||
generator: SsrGenerator,
|
||||
block: Block,
|
||||
node: Node
|
||||
) {
|
||||
block.contextualise(node.expression);
|
||||
const { snippet } = node.metadata;
|
||||
|
||||
generator.append('${ ' + snippet + ' ? `');
|
||||
|
||||
const childBlock = block.child({
|
||||
conditions: block.conditions.concat(snippet),
|
||||
});
|
||||
|
||||
node.children.forEach((child: Node) => {
|
||||
visit(generator, childBlock, child);
|
||||
});
|
||||
|
||||
generator.append('` : `');
|
||||
|
||||
if (node.else) {
|
||||
node.else.children.forEach((child: Node) => {
|
||||
visit(generator, childBlock, child);
|
||||
});
|
||||
}
|
||||
|
||||
generator.append('` }');
|
||||
}
|
@ -1,20 +0,0 @@
|
||||
import { SsrGenerator } from '../index';
|
||||
import Block from '../Block';
|
||||
import { Node } from '../../../interfaces';
|
||||
|
||||
export default function visitMustacheTag(
|
||||
generator: SsrGenerator,
|
||||
block: Block,
|
||||
node: Node
|
||||
) {
|
||||
block.contextualise(node.expression);
|
||||
const { snippet } = node.metadata;
|
||||
|
||||
generator.append(
|
||||
node.parent &&
|
||||
node.parent.type === 'Element' &&
|
||||
node.parent.name === 'style'
|
||||
? '${' + snippet + '}'
|
||||
: '${__escape(' + snippet + ')}'
|
||||
);
|
||||
}
|
@ -1,14 +0,0 @@
|
||||
import { SsrGenerator } from '../index';
|
||||
import Block from '../Block';
|
||||
import { Node } from '../../../interfaces';
|
||||
|
||||
export default function visitRawMustacheTag(
|
||||
generator: SsrGenerator,
|
||||
block: Block,
|
||||
node: Node
|
||||
) {
|
||||
block.contextualise(node.expression);
|
||||
const { snippet } = node.metadata;
|
||||
|
||||
generator.append('${' + snippet + '}');
|
||||
}
|
@ -1,21 +0,0 @@
|
||||
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
|
||||
) {
|
||||
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}() : \``);
|
||||
|
||||
node.children.forEach((child: Node) => {
|
||||
visit(generator, block, child);
|
||||
});
|
||||
|
||||
generator.append(`\`}`);
|
||||
}
|
@ -1,21 +0,0 @@
|
||||
import { SsrGenerator } from '../index';
|
||||
import Block from '../Block';
|
||||
import { escape, escapeHTML, escapeTemplate } from '../../../utils/stringify';
|
||||
import { Node } from '../../../interfaces';
|
||||
|
||||
export default function visitText(
|
||||
generator: SsrGenerator,
|
||||
block: Block,
|
||||
node: Node
|
||||
) {
|
||||
let text = node.data;
|
||||
if (
|
||||
!node.parent ||
|
||||
node.parent.type !== 'Element' ||
|
||||
(node.parent.name !== 'script' && node.parent.name !== 'style')
|
||||
) {
|
||||
// unless this Text node is inside a <script> or <style> element, escape &,<,>
|
||||
text = escapeHTML(text);
|
||||
}
|
||||
generator.append(escape(escapeTemplate(text)));
|
||||
}
|
@ -1,19 +0,0 @@
|
||||
import { SsrGenerator } from '../index';
|
||||
import Block from '../Block';
|
||||
import { escape } from '../../../utils/stringify';
|
||||
import visit from '../visit';
|
||||
import { Node } from '../../../interfaces';
|
||||
|
||||
export default function visitTitle(
|
||||
generator: SsrGenerator,
|
||||
block: Block,
|
||||
node: Node
|
||||
) {
|
||||
generator.append(`<title>`);
|
||||
|
||||
node.children.forEach((child: Node) => {
|
||||
visit(generator, block, child);
|
||||
});
|
||||
|
||||
generator.append(`</title>`);
|
||||
}
|
@ -1,3 +0,0 @@
|
||||
export default function visitWindow() {
|
||||
// noop
|
||||
}
|
@ -1,29 +0,0 @@
|
||||
import AwaitBlock from './AwaitBlock';
|
||||
import Comment from './Comment';
|
||||
import Component from './Component';
|
||||
import EachBlock from './EachBlock';
|
||||
import Element from './Element';
|
||||
import Head from './Head';
|
||||
import IfBlock from './IfBlock';
|
||||
import MustacheTag from './MustacheTag';
|
||||
import RawMustacheTag from './RawMustacheTag';
|
||||
import Slot from './Slot';
|
||||
import Text from './Text';
|
||||
import Title from './Title';
|
||||
import Window from './Window';
|
||||
|
||||
export default {
|
||||
AwaitBlock,
|
||||
Comment,
|
||||
Component,
|
||||
EachBlock,
|
||||
Element,
|
||||
Head,
|
||||
IfBlock,
|
||||
MustacheTag,
|
||||
RawMustacheTag,
|
||||
Slot,
|
||||
Text,
|
||||
Title,
|
||||
Window
|
||||
};
|
@ -1,17 +0,0 @@
|
||||
import Block from '../../Block';
|
||||
import { escape, escapeTemplate } from '../../../../utils/stringify';
|
||||
import { Node } from '../../../../interfaces';
|
||||
|
||||
export default function stringifyAttributeValue(block: Block, chunks: Node[]) {
|
||||
return chunks
|
||||
.map((chunk: Node) => {
|
||||
if (chunk.type === 'Text') {
|
||||
return escapeTemplate(escape(chunk.data).replace(/"/g, '"'));
|
||||
}
|
||||
|
||||
block.contextualise(chunk.expression);
|
||||
const { snippet } = chunk.metadata;
|
||||
return '${__escape(' + snippet + ')}';
|
||||
})
|
||||
.join('');
|
||||
}
|
@ -0,0 +1,37 @@
|
||||
export function spread(args) {
|
||||
const attributes = Object.assign({}, ...args);
|
||||
let str = '';
|
||||
|
||||
Object.keys(attributes).forEach(name => {
|
||||
const value = attributes[name];
|
||||
if (value === undefined) return;
|
||||
if (value === true) str += " " + name;
|
||||
str += " " + name + "=" + JSON.stringify(value);
|
||||
});
|
||||
|
||||
return str;
|
||||
}
|
||||
|
||||
export const escaped = {
|
||||
'"': '"',
|
||||
"'": ''',
|
||||
'&': '&',
|
||||
'<': '<',
|
||||
'>': '>'
|
||||
};
|
||||
|
||||
export function escape(html) {
|
||||
return String(html).replace(/["'&<>]/g, match => escaped[match]);
|
||||
}
|
||||
|
||||
export function each(items, assign, fn) {
|
||||
let str = '';
|
||||
for (let i = 0; i < items.length; i += 1) {
|
||||
str += fn(assign(items[i], i));
|
||||
}
|
||||
return str;
|
||||
}
|
||||
|
||||
export const missingComponent = {
|
||||
_render: () => ''
|
||||
};
|
@ -0,0 +1,5 @@
|
||||
export default function addToSet(a: Set<any>, b: Set<any>) {
|
||||
b.forEach(item => {
|
||||
a.add(item);
|
||||
});
|
||||
}
|
@ -1,18 +0,0 @@
|
||||
import { Node, Parsed } from '../interfaces';
|
||||
|
||||
export default function clone(node: Node|Parsed) {
|
||||
const cloned: any = {};
|
||||
|
||||
for (const key in node) {
|
||||
const value = node[key];
|
||||
if (Array.isArray(value)) {
|
||||
cloned[key] = value.map(clone);
|
||||
} else if (value && typeof value === 'object') {
|
||||
cloned[key] = clone(value);
|
||||
} else {
|
||||
cloned[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
return cloned;
|
||||
}
|
@ -1,53 +0,0 @@
|
||||
import { Node } from '../interfaces';
|
||||
|
||||
const binaryOperators: Record<string, number> = {
|
||||
'**': 15,
|
||||
'*': 14,
|
||||
'/': 14,
|
||||
'%': 14,
|
||||
'+': 13,
|
||||
'-': 13,
|
||||
'<<': 12,
|
||||
'>>': 12,
|
||||
'>>>': 12,
|
||||
'<': 11,
|
||||
'<=': 11,
|
||||
'>': 11,
|
||||
'>=': 11,
|
||||
'in': 11,
|
||||
'instanceof': 11,
|
||||
'==': 10,
|
||||
'!=': 10,
|
||||
'===': 10,
|
||||
'!==': 10,
|
||||
'&': 9,
|
||||
'^': 8,
|
||||
'|': 7
|
||||
};
|
||||
|
||||
const logicalOperators: Record<string, number> = {
|
||||
'&&': 6,
|
||||
'||': 5
|
||||
};
|
||||
|
||||
const precedence: Record<string, (expression?: Node) => number> = {
|
||||
Literal: () => 21,
|
||||
Identifier: () => 21,
|
||||
ParenthesizedExpression: () => 20,
|
||||
MemberExpression: () => 19,
|
||||
NewExpression: () => 19, // can be 18 (if no args) but makes no practical difference
|
||||
CallExpression: () => 19,
|
||||
UpdateExpression: () => 17,
|
||||
UnaryExpression: () => 16,
|
||||
BinaryExpression: (expression: Node) => binaryOperators[expression.operator],
|
||||
LogicalExpression: (expression: Node) => logicalOperators[expression.operator],
|
||||
ConditionalExpression: () => 4,
|
||||
AssignmentExpression: () => 3,
|
||||
YieldExpression: () => 2,
|
||||
SpreadElement: () => 1,
|
||||
SequenceExpression: () => 0
|
||||
};
|
||||
|
||||
export default function getExpressionPrecedence(expression: Node) {
|
||||
return expression.type in precedence ? precedence[expression.type](expression) : 0;
|
||||
}
|
@ -0,0 +1,44 @@
|
||||
import * as namespaces from '../../utils/namespaces';
|
||||
import validateEventHandler from './validateEventHandler';
|
||||
import validate, { Validator } from '../index';
|
||||
import { Node } from '../../interfaces';
|
||||
|
||||
export default function validateComponent(
|
||||
validator: Validator,
|
||||
node: Node,
|
||||
refs: Map<string, Node[]>,
|
||||
refCallees: Node[],
|
||||
stack: Node[],
|
||||
elementStack: Node[]
|
||||
) {
|
||||
if (node.name !== 'svelte:self' && node.name !== 'svelte:component' && !validator.components.has(node.name)) {
|
||||
validator.error(node, {
|
||||
code: `missing-component`,
|
||||
message: `${node.name} component is not defined`
|
||||
});
|
||||
}
|
||||
|
||||
validator.used.components.add(node.name);
|
||||
|
||||
node.attributes.forEach((attribute: Node) => {
|
||||
if (attribute.type === 'Ref') {
|
||||
if (!refs.has(attribute.name)) refs.set(attribute.name, []);
|
||||
refs.get(attribute.name).push(node);
|
||||
}
|
||||
|
||||
if (attribute.type === 'EventHandler') {
|
||||
validator.used.events.add(attribute.name);
|
||||
validateEventHandler(validator, attribute, refCallees);
|
||||
} else if (attribute.type === 'Transition') {
|
||||
validator.error(attribute, {
|
||||
code: `invalid-transition`,
|
||||
message: `Transitions can only be applied to DOM elements, not components`
|
||||
});
|
||||
} else if (attribute.type === 'Action') {
|
||||
validator.error(attribute, {
|
||||
code: `invalid-action`,
|
||||
message: `Actions can only be applied to DOM elements, not components`
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
@ -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`
|
||||
// });
|
||||
// }
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue