mirror of https://github.com/sveltejs/svelte
commit
86fd8f3e16
@ -1,16 +1,19 @@
|
||||
.DS_Store
|
||||
.nyc_output
|
||||
node_modules
|
||||
/cli/
|
||||
/compiler/
|
||||
/ssr/
|
||||
/shared.js
|
||||
/scratch/
|
||||
/coverage/
|
||||
/coverage.lcov/
|
||||
/test/cli/samples/*/actual
|
||||
/test/sourcemaps/samples/*/output.js
|
||||
/test/sourcemaps/samples/*/output.js.map
|
||||
/test/sourcemaps/samples/*/output.css
|
||||
/test/sourcemaps/samples/*/output.css.map
|
||||
/src/compile/shared.ts
|
||||
/package-lock.json
|
||||
/store.umd.js
|
||||
/yarn-error.log
|
||||
_actual*.*
|
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,145 @@
|
||||
import * as path from 'path';
|
||||
import * as fs from 'fs';
|
||||
import * as svelte from 'svelte';
|
||||
import error from './error.js';
|
||||
|
||||
function mkdirp(dir) {
|
||||
const parent = path.dirname(dir);
|
||||
if (dir === parent) return;
|
||||
|
||||
mkdirp(parent);
|
||||
if (!fs.existsSync(dir)) fs.mkdirSync(dir);
|
||||
}
|
||||
|
||||
export function compile(input, opts) {
|
||||
if (opts._.length > 0) {
|
||||
error(`Can only compile a single file or directory`);
|
||||
}
|
||||
|
||||
const output = opts.output;
|
||||
|
||||
const stats = fs.statSync(input);
|
||||
const isDir = stats.isDirectory();
|
||||
|
||||
if (isDir) {
|
||||
if (!output) {
|
||||
error(`You must specify an --output (-o) option when compiling a directory of files`);
|
||||
}
|
||||
|
||||
if (opts.name || opts.amdId) {
|
||||
error(`Cannot specify --${opts.name ? 'name' : 'amdId'} when compiling a directory`);
|
||||
}
|
||||
}
|
||||
|
||||
const globals = {};
|
||||
if (opts.globals) {
|
||||
opts.globals.split(',').forEach(pair => {
|
||||
const [key, value] = pair.split(':');
|
||||
globals[key] = value;
|
||||
});
|
||||
}
|
||||
|
||||
const options = {
|
||||
name: opts.name,
|
||||
format: opts.format,
|
||||
sourceMap: opts.sourcemap,
|
||||
globals,
|
||||
amd: opts.amdId
|
||||
? {
|
||||
id: opts.amdId,
|
||||
}
|
||||
: undefined,
|
||||
css: opts.css !== false,
|
||||
dev: opts.dev,
|
||||
immutable: opts.immutable,
|
||||
generate: opts.generate || 'dom',
|
||||
customElement: opts.customElement,
|
||||
store: opts.store,
|
||||
shared: opts.shared
|
||||
};
|
||||
|
||||
if (isDir) {
|
||||
mkdirp(output);
|
||||
compileDirectory(input, output, options);
|
||||
} else {
|
||||
compileFile(input, output, options);
|
||||
}
|
||||
}
|
||||
|
||||
function compileDirectory(input, output, options) {
|
||||
fs.readdirSync(input).forEach(file => {
|
||||
const src = path.resolve(input, file);
|
||||
const dest = path.resolve(output, file);
|
||||
|
||||
if (path.extname(file) === '.html') {
|
||||
compileFile(
|
||||
src,
|
||||
dest.substring(0, dest.lastIndexOf('.html')) + '.js',
|
||||
options
|
||||
);
|
||||
} else {
|
||||
const stats = fs.statSync(src);
|
||||
if (stats.isDirectory()) {
|
||||
compileDirectory(src, dest, options);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
let SOURCEMAPPING_URL = 'sourceMa';
|
||||
SOURCEMAPPING_URL += 'ppingURL';
|
||||
|
||||
function compileFile(input, output, options) {
|
||||
console.error(`compiling ${path.relative(process.cwd(), input)}...`); // eslint-disable-line no-console
|
||||
|
||||
options = Object.assign({}, options);
|
||||
if (!options.name) options.name = getName(input);
|
||||
|
||||
options.filename = input;
|
||||
options.outputFilename = output;
|
||||
|
||||
const { sourceMap } = options;
|
||||
const inline = sourceMap === 'inline';
|
||||
|
||||
let source = fs.readFileSync(input, 'utf-8');
|
||||
if (source[0] === 0xfeff) source = source.slice(1);
|
||||
|
||||
let compiled;
|
||||
|
||||
try {
|
||||
compiled = svelte.compile(source, options);
|
||||
} catch (err) {
|
||||
error(err);
|
||||
}
|
||||
|
||||
const { js } = compiled;
|
||||
|
||||
if (sourceMap) {
|
||||
js.code += `\n//# ${SOURCEMAPPING_URL}=${inline || !output
|
||||
? js.map.toUrl()
|
||||
: `${path.basename(output)}.map`}\n`;
|
||||
}
|
||||
|
||||
if (output) {
|
||||
const outputDir = path.dirname(output);
|
||||
mkdirp(outputDir);
|
||||
fs.writeFileSync(output, js.code);
|
||||
console.error(`wrote ${path.relative(process.cwd(), output)}`); // eslint-disable-line no-console
|
||||
if (sourceMap && !inline) {
|
||||
fs.writeFileSync(`${output}.map`, js.map);
|
||||
console.error(`wrote ${path.relative(process.cwd(), `${output}.map`)}`); // eslint-disable-line no-console
|
||||
}
|
||||
} else {
|
||||
process.stdout.write(js.code);
|
||||
}
|
||||
}
|
||||
|
||||
function getName(input) {
|
||||
return path
|
||||
.basename(input)
|
||||
.replace(path.extname(input), '')
|
||||
.replace(/[^a-zA-Z_$0-9]+/g, '_')
|
||||
.replace(/^_/, '')
|
||||
.replace(/_$/, '')
|
||||
.replace(/^(\d)/, '_$1');
|
||||
}
|
@ -0,0 +1,17 @@
|
||||
import c from 'kleur';
|
||||
|
||||
function stderr(msg) {
|
||||
console.error(msg); // eslint-disable-line no-console
|
||||
}
|
||||
|
||||
export default function error(err) {
|
||||
stderr(c.red(err.message || err));
|
||||
|
||||
if (err.frame) {
|
||||
stderr(err.frame);
|
||||
} else if (err.stack) {
|
||||
stderr(c.gray(err.stack));
|
||||
}
|
||||
|
||||
process.exit(1);
|
||||
}
|
@ -0,0 +1,31 @@
|
||||
import sade from 'sade';
|
||||
import * as pkg from '../../package.json';
|
||||
|
||||
const prog = sade('svelte').version(pkg.version);
|
||||
|
||||
prog
|
||||
.command('compile <input>')
|
||||
|
||||
.option('-o, --output', 'Output (if absent, prints to stdout)')
|
||||
.option('-f, --format', 'Type of output (amd, cjs, es, iife, umd)')
|
||||
.option('-g, --globals', 'Comma-separate list of `module ID:Global` pairs')
|
||||
.option('-n, --name', 'Name for IIFE/UMD export (inferred from filename by default)')
|
||||
.option('-m, --sourcemap', 'Generate sourcemap (`-m inline` for inline map)')
|
||||
.option('-d, --dev', 'Add dev mode warnings and errors')
|
||||
.option('--amdId', 'ID for AMD module (default is anonymous)')
|
||||
.option('--generate', 'Change generate format between `dom` and `ssr`')
|
||||
.option('--no-css', `Don't include CSS (useful with SSR)`)
|
||||
.option('--immutable', 'Support immutable data structures')
|
||||
.option('--shared', 'Don\'t include shared helpers')
|
||||
|
||||
.example('compile App.html > App.js')
|
||||
.example('compile src -o dest')
|
||||
.example('compile -f umd MyComponent.html > MyComponent.js')
|
||||
|
||||
.action((input, opts) => {
|
||||
import('./compile').then(({ compile }) => {
|
||||
compile(input, opts);
|
||||
});
|
||||
})
|
||||
|
||||
.parse(process.argv);
|
@ -1,720 +0,0 @@
|
||||
import { parseExpressionAt } from 'acorn';
|
||||
import MagicString, { Bundle } from 'magic-string';
|
||||
import isReference from 'is-reference';
|
||||
import { walk, childKeys } from 'estree-walker';
|
||||
import { getLocator } from 'locate-character';
|
||||
import Stats from '../Stats';
|
||||
import deindent from '../utils/deindent';
|
||||
import CodeBuilder from '../utils/CodeBuilder';
|
||||
import flattenReference from '../utils/flattenReference';
|
||||
import reservedNames from '../utils/reservedNames';
|
||||
import namespaces from '../utils/namespaces';
|
||||
import { removeNode, removeObjectKey } from '../utils/removeNode';
|
||||
import nodeToString from '../utils/nodeToString';
|
||||
import wrapModule from './wrapModule';
|
||||
import annotateWithScopes, { Scope } from '../utils/annotateWithScopes';
|
||||
import getName from '../utils/getName';
|
||||
import Stylesheet from '../css/Stylesheet';
|
||||
import { test } from '../config';
|
||||
import Fragment from './nodes/Fragment';
|
||||
import shared from './shared';
|
||||
import { DomTarget } from './dom/index';
|
||||
import { SsrTarget } from './ssr/index';
|
||||
import { Node, GenerateOptions, ShorthandImport, Ast, CompileOptions, CustomElementOptions } from '../interfaces';
|
||||
|
||||
interface Computation {
|
||||
key: string;
|
||||
deps: string[]
|
||||
}
|
||||
|
||||
function detectIndentation(str: string) {
|
||||
const pattern = /^[\t\s]{1,4}/gm;
|
||||
let match;
|
||||
|
||||
while (match = pattern.exec(str)) {
|
||||
if (match[0][0] === '\t') return '\t';
|
||||
if (match[0].length === 2) return ' ';
|
||||
}
|
||||
|
||||
return ' ';
|
||||
}
|
||||
|
||||
function getIndentationLevel(str: string, b: number) {
|
||||
let a = b;
|
||||
while (a > 0 && str[a - 1] !== '\n') a -= 1;
|
||||
return /^\s*/.exec(str.slice(a, b))[0];
|
||||
}
|
||||
|
||||
function getIndentExclusionRanges(node: Node) {
|
||||
const ranges: Node[] = [];
|
||||
walk(node, {
|
||||
enter(node: Node) {
|
||||
if (node.type === 'TemplateElement') ranges.push(node);
|
||||
}
|
||||
});
|
||||
return ranges;
|
||||
}
|
||||
|
||||
function removeIndentation(
|
||||
code: MagicString,
|
||||
start: number,
|
||||
end: number,
|
||||
indentationLevel: string,
|
||||
ranges: Node[]
|
||||
) {
|
||||
const str = code.original.slice(start, end);
|
||||
const pattern = new RegExp(`^${indentationLevel}`, 'gm');
|
||||
let match;
|
||||
|
||||
while (match = pattern.exec(str)) {
|
||||
// TODO bail if we're inside an exclusion range
|
||||
code.remove(start + match.index, start + match.index + indentationLevel.length);
|
||||
}
|
||||
}
|
||||
|
||||
// We need to tell estree-walker that it should always
|
||||
// look for an `else` block, otherwise it might get
|
||||
// the wrong idea about the shape of each/if blocks
|
||||
childKeys.EachBlock = childKeys.IfBlock = ['children', 'else'];
|
||||
childKeys.Attribute = ['value'];
|
||||
|
||||
export default class Compiler {
|
||||
stats: Stats;
|
||||
|
||||
ast: Ast;
|
||||
source: string;
|
||||
name: string;
|
||||
options: CompileOptions;
|
||||
fragment: Fragment;
|
||||
target: DomTarget | SsrTarget;
|
||||
|
||||
customElement: CustomElementOptions;
|
||||
tag: string;
|
||||
props: string[];
|
||||
|
||||
defaultExport: Node[];
|
||||
imports: Node[];
|
||||
shorthandImports: ShorthandImport[];
|
||||
helpers: Set<string>;
|
||||
components: Set<string>;
|
||||
events: Set<string>;
|
||||
methods: Set<string>;
|
||||
transitions: Set<string>;
|
||||
actions: Set<string>;
|
||||
importedComponents: Map<string, string>;
|
||||
namespace: string;
|
||||
hasComponents: boolean;
|
||||
computations: Computation[];
|
||||
templateProperties: Record<string, Node>;
|
||||
slots: Set<string>;
|
||||
javascript: string;
|
||||
|
||||
code: MagicString;
|
||||
|
||||
bindingGroups: string[];
|
||||
indirectDependencies: Map<string, Set<string>>;
|
||||
expectedProperties: Set<string>;
|
||||
usesRefs: boolean;
|
||||
|
||||
locate: (c: number) => { line: number, column: number };
|
||||
|
||||
stylesheet: Stylesheet;
|
||||
|
||||
userVars: Set<string>;
|
||||
templateVars: Map<string, string>;
|
||||
aliases: Map<string, string>;
|
||||
usedNames: Set<string>;
|
||||
|
||||
constructor(
|
||||
ast: Ast,
|
||||
source: string,
|
||||
name: string,
|
||||
stylesheet: Stylesheet,
|
||||
options: CompileOptions,
|
||||
stats: Stats,
|
||||
dom: boolean,
|
||||
target: DomTarget | SsrTarget
|
||||
) {
|
||||
stats.start('compile');
|
||||
this.stats = stats;
|
||||
|
||||
this.ast = ast;
|
||||
this.source = source;
|
||||
this.options = options;
|
||||
this.target = target;
|
||||
|
||||
this.imports = [];
|
||||
this.shorthandImports = [];
|
||||
this.helpers = new Set();
|
||||
this.components = new Set();
|
||||
this.events = new Set();
|
||||
this.methods = new Set();
|
||||
this.transitions = new Set();
|
||||
this.actions = new Set();
|
||||
this.importedComponents = new Map();
|
||||
this.slots = new Set();
|
||||
|
||||
this.bindingGroups = [];
|
||||
this.indirectDependencies = new Map();
|
||||
|
||||
this.locate = getLocator(this.source);
|
||||
|
||||
// track which properties are needed, so we can provide useful info
|
||||
// in dev mode
|
||||
this.expectedProperties = new Set();
|
||||
|
||||
this.code = new MagicString(source);
|
||||
this.usesRefs = false;
|
||||
|
||||
// styles
|
||||
this.stylesheet = stylesheet;
|
||||
|
||||
// allow compiler to deconflict user's `import { get } from 'whatever'` and
|
||||
// Svelte's builtin `import { get, ... } from 'svelte/shared.ts'`;
|
||||
this.userVars = new Set();
|
||||
this.templateVars = new Map();
|
||||
this.aliases = new Map();
|
||||
this.usedNames = new Set();
|
||||
|
||||
this.computations = [];
|
||||
this.templateProperties = {};
|
||||
|
||||
this.walkJs(dom);
|
||||
this.name = this.alias(name);
|
||||
|
||||
if (options.customElement === true) {
|
||||
this.customElement = {
|
||||
tag: this.tag,
|
||||
props: this.props
|
||||
}
|
||||
} else {
|
||||
this.customElement = options.customElement;
|
||||
}
|
||||
|
||||
if (this.customElement && !this.customElement.tag) {
|
||||
throw new Error(`No tag name specified`); // TODO better error
|
||||
}
|
||||
|
||||
this.fragment = new Fragment(this, ast.html);
|
||||
// this.walkTemplate();
|
||||
if (!this.customElement) this.stylesheet.reify();
|
||||
|
||||
stylesheet.warnOnUnusedSelectors(options.onwarn);
|
||||
}
|
||||
|
||||
addSourcemapLocations(node: Node) {
|
||||
walk(node, {
|
||||
enter: (node: Node) => {
|
||||
this.code.addSourcemapLocation(node.start);
|
||||
this.code.addSourcemapLocation(node.end);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
alias(name: string) {
|
||||
if (!this.aliases.has(name)) {
|
||||
this.aliases.set(name, this.getUniqueName(name));
|
||||
}
|
||||
|
||||
return this.aliases.get(name);
|
||||
}
|
||||
|
||||
generate(result: string, options: CompileOptions, { banner = '', name, format }: GenerateOptions ) {
|
||||
const pattern = /\[✂(\d+)-(\d+)$/;
|
||||
|
||||
const helpers = new Set();
|
||||
|
||||
// TODO use same regex for both
|
||||
result = result.replace(options.generate === 'ssr' ? /(@+|#+|%+)(\w*(?:-\w*)?)/g : /(%+|@+)(\w*(?:-\w*)?)/g, (match: string, sigil: string, name: string) => {
|
||||
if (sigil === '@') {
|
||||
if (name in shared) {
|
||||
if (options.dev && `${name}Dev` in shared) name = `${name}Dev`;
|
||||
helpers.add(name);
|
||||
}
|
||||
|
||||
return this.alias(name);
|
||||
}
|
||||
|
||||
if (sigil === '%') {
|
||||
return this.templateVars.get(name);
|
||||
}
|
||||
|
||||
return sigil.slice(1) + name;
|
||||
});
|
||||
|
||||
let importedHelpers;
|
||||
|
||||
if (options.shared) {
|
||||
if (format !== 'es' && format !== 'cjs') {
|
||||
throw new Error(`Components with shared helpers must be compiled with \`format: 'es'\` or \`format: 'cjs'\``);
|
||||
}
|
||||
|
||||
importedHelpers = Array.from(helpers).sort().map(name => {
|
||||
const alias = this.alias(name);
|
||||
return { name, alias };
|
||||
});
|
||||
} else {
|
||||
let inlineHelpers = '';
|
||||
|
||||
const compiler = this;
|
||||
|
||||
importedHelpers = [];
|
||||
|
||||
helpers.forEach(name => {
|
||||
const str = shared[name];
|
||||
const code = new MagicString(str);
|
||||
const expression = parseExpressionAt(str, 0);
|
||||
|
||||
let { scope } = annotateWithScopes(expression);
|
||||
|
||||
walk(expression, {
|
||||
enter(node: Node, parent: Node) {
|
||||
if (node._scope) scope = node._scope;
|
||||
|
||||
if (
|
||||
node.type === 'Identifier' &&
|
||||
isReference(node, parent) &&
|
||||
!scope.has(node.name)
|
||||
) {
|
||||
if (node.name in shared) {
|
||||
// this helper function depends on another one
|
||||
const dependency = node.name;
|
||||
helpers.add(dependency);
|
||||
|
||||
const alias = compiler.alias(dependency);
|
||||
if (alias !== node.name) {
|
||||
code.overwrite(node.start, node.end, alias);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
leave(node: Node) {
|
||||
if (node._scope) scope = scope.parent;
|
||||
},
|
||||
});
|
||||
|
||||
if (name === 'transitionManager') {
|
||||
// special case
|
||||
const global = `_svelteTransitionManager`;
|
||||
|
||||
inlineHelpers += `\n\nvar ${this.alias('transitionManager')} = window.${global} || (window.${global} = ${code});\n\n`;
|
||||
} else if (name === 'escaped' || name === 'missingComponent') {
|
||||
// vars are an awkward special case... would be nice to avoid this
|
||||
const alias = this.alias(name);
|
||||
inlineHelpers += `\n\nconst ${alias} = ${code};`
|
||||
} else {
|
||||
const alias = this.alias(expression.id.name);
|
||||
if (alias !== expression.id.name) {
|
||||
code.overwrite(expression.id.start, expression.id.end, alias);
|
||||
}
|
||||
|
||||
inlineHelpers += `\n\n${code}`;
|
||||
}
|
||||
});
|
||||
|
||||
result += inlineHelpers;
|
||||
}
|
||||
|
||||
const sharedPath = options.shared === true
|
||||
? 'svelte/shared.js'
|
||||
: options.shared || '';
|
||||
|
||||
const module = wrapModule(result, format, name, options, banner, sharedPath, importedHelpers, this.imports, this.shorthandImports, this.source);
|
||||
|
||||
const parts = module.split('✂]');
|
||||
const finalChunk = parts.pop();
|
||||
|
||||
const compiled = new Bundle({ separator: '' });
|
||||
|
||||
function addString(str: string) {
|
||||
compiled.addSource({
|
||||
content: new MagicString(str),
|
||||
});
|
||||
}
|
||||
|
||||
const { filename } = options;
|
||||
|
||||
// special case — the source file doesn't actually get used anywhere. we need
|
||||
// to add an empty file to populate map.sources and map.sourcesContent
|
||||
if (!parts.length) {
|
||||
compiled.addSource({
|
||||
filename,
|
||||
content: new MagicString(this.source).remove(0, this.source.length),
|
||||
});
|
||||
}
|
||||
|
||||
parts.forEach((str: string) => {
|
||||
const chunk = str.replace(pattern, '');
|
||||
if (chunk) addString(chunk);
|
||||
|
||||
const match = pattern.exec(str);
|
||||
|
||||
const snippet = this.code.snip(+match[1], +match[2]);
|
||||
|
||||
compiled.addSource({
|
||||
filename,
|
||||
content: snippet,
|
||||
});
|
||||
});
|
||||
|
||||
addString(finalChunk);
|
||||
|
||||
const css = this.customElement ?
|
||||
{ code: null, map: null } :
|
||||
this.stylesheet.render(options.cssOutputFilename, true);
|
||||
|
||||
const js = {
|
||||
code: compiled.toString(),
|
||||
map: compiled.generateMap({
|
||||
includeContent: true,
|
||||
file: options.outputFilename,
|
||||
})
|
||||
};
|
||||
|
||||
this.stats.stop('compile');
|
||||
|
||||
return {
|
||||
ast: this.ast,
|
||||
js,
|
||||
css,
|
||||
stats: this.stats.render(this)
|
||||
};
|
||||
}
|
||||
|
||||
getUniqueName(name: string) {
|
||||
if (test) name = `${name}$`;
|
||||
let alias = name;
|
||||
for (
|
||||
let i = 1;
|
||||
reservedNames.has(alias) ||
|
||||
this.userVars.has(alias) ||
|
||||
this.usedNames.has(alias);
|
||||
alias = `${name}_${i++}`
|
||||
);
|
||||
this.usedNames.add(alias);
|
||||
return alias;
|
||||
}
|
||||
|
||||
getUniqueNameMaker() {
|
||||
const localUsedNames = new Set();
|
||||
|
||||
function add(name: string) {
|
||||
localUsedNames.add(name);
|
||||
}
|
||||
|
||||
reservedNames.forEach(add);
|
||||
this.userVars.forEach(add);
|
||||
|
||||
return (name: string) => {
|
||||
if (test) name = `${name}$`;
|
||||
let alias = name;
|
||||
for (
|
||||
let i = 1;
|
||||
this.usedNames.has(alias) ||
|
||||
localUsedNames.has(alias);
|
||||
alias = `${name}_${i++}`
|
||||
);
|
||||
localUsedNames.add(alias);
|
||||
return alias;
|
||||
};
|
||||
}
|
||||
|
||||
walkJs(dom: boolean) {
|
||||
const {
|
||||
code,
|
||||
source,
|
||||
computations,
|
||||
methods,
|
||||
templateProperties,
|
||||
imports
|
||||
} = this;
|
||||
|
||||
const { js } = this.ast;
|
||||
|
||||
const componentDefinition = new CodeBuilder();
|
||||
|
||||
if (js) {
|
||||
this.addSourcemapLocations(js.content);
|
||||
|
||||
const indentation = detectIndentation(source.slice(js.start, js.end));
|
||||
const indentationLevel = getIndentationLevel(source, js.content.body[0].start);
|
||||
const indentExclusionRanges = getIndentExclusionRanges(js.content);
|
||||
|
||||
const { scope, globals } = annotateWithScopes(js.content);
|
||||
|
||||
scope.declarations.forEach(name => {
|
||||
this.userVars.add(name);
|
||||
});
|
||||
|
||||
globals.forEach(name => {
|
||||
this.userVars.add(name);
|
||||
});
|
||||
|
||||
const body = js.content.body.slice(); // slice, because we're going to be mutating the original
|
||||
|
||||
// imports need to be hoisted out of the IIFE
|
||||
for (let i = 0; i < body.length; i += 1) {
|
||||
const node = body[i];
|
||||
if (node.type === 'ImportDeclaration') {
|
||||
removeNode(code, js.content, node);
|
||||
imports.push(node);
|
||||
|
||||
node.specifiers.forEach((specifier: Node) => {
|
||||
this.userVars.add(specifier.local.name);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const defaultExport = this.defaultExport = body.find(
|
||||
(node: Node) => node.type === 'ExportDefaultDeclaration'
|
||||
);
|
||||
|
||||
if (defaultExport) {
|
||||
defaultExport.declaration.properties.forEach((prop: Node) => {
|
||||
templateProperties[getName(prop.key)] = prop;
|
||||
});
|
||||
|
||||
['helpers', 'events', 'components', 'transitions', 'actions'].forEach(key => {
|
||||
if (templateProperties[key]) {
|
||||
templateProperties[key].value.properties.forEach((prop: Node) => {
|
||||
this[key].add(getName(prop.key));
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const addArrowFunctionExpression = (name: string, node: Node) => {
|
||||
const { body, params, async } = node;
|
||||
const fnKeyword = async ? 'async function' : 'function';
|
||||
|
||||
const paramString = params.length ?
|
||||
`[✂${params[0].start}-${params[params.length - 1].end}✂]` :
|
||||
``;
|
||||
|
||||
if (body.type === 'BlockStatement') {
|
||||
componentDefinition.addBlock(deindent`
|
||||
${fnKeyword} ${name}(${paramString}) [✂${body.start}-${body.end}✂]
|
||||
`);
|
||||
} else {
|
||||
componentDefinition.addBlock(deindent`
|
||||
${fnKeyword} ${name}(${paramString}) {
|
||||
return [✂${body.start}-${body.end}✂];
|
||||
}
|
||||
`);
|
||||
}
|
||||
};
|
||||
|
||||
const addFunctionExpression = (name: string, node: Node) => {
|
||||
const { async } = node;
|
||||
const fnKeyword = async ? 'async function' : 'function';
|
||||
|
||||
let c = node.start;
|
||||
while (this.source[c] !== '(') c += 1;
|
||||
componentDefinition.addBlock(deindent`
|
||||
${fnKeyword} ${name}[✂${c}-${node.end}✂];
|
||||
`);
|
||||
};
|
||||
|
||||
const addValue = (name: string, node: Node) => {
|
||||
componentDefinition.addBlock(deindent`
|
||||
var ${name} = [✂${node.start}-${node.end}✂];
|
||||
`);
|
||||
};
|
||||
|
||||
const addDeclaration = (key: string, node: Node, allowShorthandImport?: boolean, disambiguator?: string, conflicts?: Record<string, boolean>) => {
|
||||
const qualified = disambiguator ? `${disambiguator}-${key}` : key;
|
||||
|
||||
if (node.type === 'Identifier' && node.name === key) {
|
||||
this.templateVars.set(qualified, key);
|
||||
return;
|
||||
}
|
||||
|
||||
let deconflicted = key;
|
||||
if (conflicts) while (deconflicted in conflicts) deconflicted += '_'
|
||||
|
||||
let name = this.getUniqueName(deconflicted);
|
||||
this.templateVars.set(qualified, name);
|
||||
|
||||
if (allowShorthandImport && node.type === 'Literal' && typeof node.value === 'string') {
|
||||
this.shorthandImports.push({ name, source: node.value });
|
||||
return;
|
||||
}
|
||||
|
||||
// deindent
|
||||
const indentationLevel = getIndentationLevel(source, node.start);
|
||||
if (indentationLevel) {
|
||||
removeIndentation(code, node.start, node.end, indentationLevel, indentExclusionRanges);
|
||||
}
|
||||
|
||||
if (node.type === 'ArrowFunctionExpression') {
|
||||
addArrowFunctionExpression(name, node);
|
||||
} else if (node.type === 'FunctionExpression') {
|
||||
addFunctionExpression(name, node);
|
||||
} else {
|
||||
addValue(name, node);
|
||||
}
|
||||
};
|
||||
|
||||
if (templateProperties.components) {
|
||||
templateProperties.components.value.properties.forEach((property: Node) => {
|
||||
addDeclaration(getName(property.key), property.value, true, 'components');
|
||||
});
|
||||
}
|
||||
|
||||
if (templateProperties.computed) {
|
||||
const dependencies = new Map();
|
||||
|
||||
const fullStateComputations = [];
|
||||
|
||||
templateProperties.computed.value.properties.forEach((prop: Node) => {
|
||||
const key = getName(prop.key);
|
||||
const value = prop.value;
|
||||
|
||||
addDeclaration(key, value, false, 'computed', {
|
||||
state: true,
|
||||
changed: true
|
||||
});
|
||||
|
||||
const param = value.params[0];
|
||||
|
||||
if (param.type === 'ObjectPattern') {
|
||||
const deps = param.properties.map(prop => prop.key.name);
|
||||
|
||||
deps.forEach(dep => {
|
||||
this.expectedProperties.add(dep);
|
||||
});
|
||||
dependencies.set(key, deps);
|
||||
} else {
|
||||
fullStateComputations.push({ key, deps: null })
|
||||
}
|
||||
});
|
||||
|
||||
const visited = new Set();
|
||||
|
||||
const visit = (key: string) => {
|
||||
if (!dependencies.has(key)) return; // not a computation
|
||||
|
||||
if (visited.has(key)) return;
|
||||
visited.add(key);
|
||||
|
||||
const deps = dependencies.get(key);
|
||||
deps.forEach(visit);
|
||||
|
||||
computations.push({ key, deps });
|
||||
|
||||
const prop = templateProperties.computed.value.properties.find((prop: Node) => getName(prop.key) === key);
|
||||
};
|
||||
|
||||
templateProperties.computed.value.properties.forEach((prop: Node) =>
|
||||
visit(getName(prop.key))
|
||||
);
|
||||
|
||||
if (fullStateComputations.length > 0) {
|
||||
computations.push(...fullStateComputations);
|
||||
}
|
||||
}
|
||||
|
||||
if (templateProperties.data) {
|
||||
addDeclaration('data', templateProperties.data.value);
|
||||
}
|
||||
|
||||
if (templateProperties.events && dom) {
|
||||
templateProperties.events.value.properties.forEach((property: Node) => {
|
||||
addDeclaration(getName(property.key), property.value, false, 'events');
|
||||
});
|
||||
}
|
||||
|
||||
if (templateProperties.helpers) {
|
||||
templateProperties.helpers.value.properties.forEach((property: Node) => {
|
||||
addDeclaration(getName(property.key), property.value, false, 'helpers');
|
||||
});
|
||||
}
|
||||
|
||||
if (templateProperties.methods && dom) {
|
||||
addDeclaration('methods', templateProperties.methods.value);
|
||||
|
||||
templateProperties.methods.value.properties.forEach(prop => {
|
||||
this.methods.add(prop.key.name);
|
||||
});
|
||||
}
|
||||
|
||||
if (templateProperties.namespace) {
|
||||
const ns = nodeToString(templateProperties.namespace.value);
|
||||
this.namespace = namespaces[ns] || ns;
|
||||
}
|
||||
|
||||
if (templateProperties.oncreate && dom) {
|
||||
addDeclaration('oncreate', templateProperties.oncreate.value);
|
||||
}
|
||||
|
||||
if (templateProperties.ondestroy && dom) {
|
||||
addDeclaration('ondestroy', templateProperties.ondestroy.value);
|
||||
}
|
||||
|
||||
if (templateProperties.onstate && dom) {
|
||||
addDeclaration('onstate', templateProperties.onstate.value);
|
||||
}
|
||||
|
||||
if (templateProperties.onupdate && dom) {
|
||||
addDeclaration('onupdate', templateProperties.onupdate.value);
|
||||
}
|
||||
|
||||
if (templateProperties.preload) {
|
||||
addDeclaration('preload', templateProperties.preload.value);
|
||||
}
|
||||
|
||||
if (templateProperties.props) {
|
||||
this.props = templateProperties.props.value.elements.map((element: Node) => nodeToString(element));
|
||||
}
|
||||
|
||||
if (templateProperties.setup) {
|
||||
addDeclaration('setup', templateProperties.setup.value);
|
||||
}
|
||||
|
||||
if (templateProperties.store) {
|
||||
addDeclaration('store', templateProperties.store.value);
|
||||
}
|
||||
|
||||
if (templateProperties.tag) {
|
||||
this.tag = nodeToString(templateProperties.tag.value);
|
||||
}
|
||||
|
||||
if (templateProperties.transitions) {
|
||||
templateProperties.transitions.value.properties.forEach((property: Node) => {
|
||||
addDeclaration(getName(property.key), property.value, false, 'transitions');
|
||||
});
|
||||
}
|
||||
|
||||
if (templateProperties.actions) {
|
||||
templateProperties.actions.value.properties.forEach((property: Node) => {
|
||||
addDeclaration(getName(property.key), property.value, false, 'actions');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (indentationLevel) {
|
||||
if (defaultExport) {
|
||||
removeIndentation(code, js.content.start, defaultExport.start, indentationLevel, indentExclusionRanges);
|
||||
removeIndentation(code, defaultExport.end, js.content.end, indentationLevel, indentExclusionRanges);
|
||||
} else {
|
||||
removeIndentation(code, js.content.start, js.content.end, indentationLevel, indentExclusionRanges);
|
||||
}
|
||||
}
|
||||
|
||||
let a = js.content.start;
|
||||
while (/\s/.test(source[a])) a += 1;
|
||||
|
||||
let b = js.content.end;
|
||||
while (/\s/.test(source[b - 1])) b -= 1;
|
||||
|
||||
if (defaultExport) {
|
||||
this.javascript = '';
|
||||
if (a !== defaultExport.start) this.javascript += `[✂${a}-${defaultExport.start}✂]`;
|
||||
if (!componentDefinition.isEmpty()) this.javascript += componentDefinition;
|
||||
if (defaultExport.end !== b) this.javascript += `[✂${defaultExport.end}-${b}✂]`;
|
||||
} else {
|
||||
this.javascript = a === b ? null : `[✂${a}-${b}✂]`;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,937 @@
|
||||
import { parseExpressionAt } from 'acorn';
|
||||
import MagicString, { Bundle } from 'magic-string';
|
||||
import isReference from 'is-reference';
|
||||
import { walk, childKeys } from 'estree-walker';
|
||||
import { getLocator } from 'locate-character';
|
||||
import Stats from '../Stats';
|
||||
import deindent from '../utils/deindent';
|
||||
import reservedNames from '../utils/reservedNames';
|
||||
import namespaces from '../utils/namespaces';
|
||||
import { removeNode } from '../utils/removeNode';
|
||||
import nodeToString from '../utils/nodeToString';
|
||||
import wrapModule from './wrapModule';
|
||||
import annotateWithScopes from '../utils/annotateWithScopes';
|
||||
import getName from '../utils/getName';
|
||||
import Stylesheet from './css/Stylesheet';
|
||||
import { test } from '../config';
|
||||
import Fragment from './nodes/Fragment';
|
||||
import shared from './shared';
|
||||
import { Node, ShorthandImport, Ast, CompileOptions, CustomElementOptions } from '../interfaces';
|
||||
import error from '../utils/error';
|
||||
import getCodeFrame from '../utils/getCodeFrame';
|
||||
import checkForComputedKeys from './validate/js/utils/checkForComputedKeys';
|
||||
import checkForDupes from './validate/js/utils/checkForDupes';
|
||||
import propValidators from './validate/js/propValidators';
|
||||
import fuzzymatch from './validate/utils/fuzzymatch';
|
||||
import flattenReference from '../utils/flattenReference';
|
||||
|
||||
interface Computation {
|
||||
key: string;
|
||||
deps: string[];
|
||||
hasRestParam: boolean;
|
||||
}
|
||||
|
||||
interface Declaration {
|
||||
type: string;
|
||||
name: string;
|
||||
node: Node;
|
||||
block: string;
|
||||
}
|
||||
|
||||
function detectIndentation(str: string) {
|
||||
const pattern = /^[\t\s]{1,4}/gm;
|
||||
let match;
|
||||
|
||||
while (match = pattern.exec(str)) {
|
||||
if (match[0][0] === '\t') return '\t';
|
||||
if (match[0].length === 2) return ' ';
|
||||
}
|
||||
|
||||
return ' ';
|
||||
}
|
||||
|
||||
function getIndentationLevel(str: string, b: number) {
|
||||
let a = b;
|
||||
while (a > 0 && str[a - 1] !== '\n') a -= 1;
|
||||
return /^\s*/.exec(str.slice(a, b))[0];
|
||||
}
|
||||
|
||||
function getIndentExclusionRanges(node: Node) {
|
||||
// TODO can we fold this into a different pass?
|
||||
const ranges: Node[] = [];
|
||||
walk(node, {
|
||||
enter(node: Node) {
|
||||
if (node.type === 'TemplateElement') ranges.push(node);
|
||||
}
|
||||
});
|
||||
return ranges;
|
||||
}
|
||||
|
||||
function removeIndentation(
|
||||
code: MagicString,
|
||||
start: number,
|
||||
end: number,
|
||||
indentationLevel: string,
|
||||
ranges: Node[]
|
||||
) {
|
||||
const str = code.original.slice(start, end);
|
||||
const pattern = new RegExp(`^${indentationLevel}`, 'gm');
|
||||
let match;
|
||||
|
||||
while (match = pattern.exec(str)) {
|
||||
// TODO bail if we're inside an exclusion range
|
||||
code.remove(start + match.index, start + match.index + indentationLevel.length);
|
||||
}
|
||||
}
|
||||
|
||||
// We need to tell estree-walker that it should always
|
||||
// look for an `else` block, otherwise it might get
|
||||
// the wrong idea about the shape of each/if blocks
|
||||
childKeys.EachBlock = childKeys.IfBlock = ['children', 'else'];
|
||||
childKeys.Attribute = ['value'];
|
||||
|
||||
export default class Component {
|
||||
stats: Stats;
|
||||
|
||||
ast: Ast;
|
||||
source: string;
|
||||
name: string;
|
||||
options: CompileOptions;
|
||||
fragment: Fragment;
|
||||
|
||||
customElement: CustomElementOptions;
|
||||
tag: string;
|
||||
props: string[];
|
||||
|
||||
properties: Map<string, Node>;
|
||||
|
||||
defaultExport: Node;
|
||||
imports: Node[];
|
||||
shorthandImports: ShorthandImport[];
|
||||
helpers: Set<string>;
|
||||
components: Set<string>;
|
||||
events: Set<string>;
|
||||
methods: Set<string>;
|
||||
animations: Set<string>;
|
||||
transitions: Set<string>;
|
||||
actions: Set<string>;
|
||||
importedComponents: Map<string, string>;
|
||||
namespace: string;
|
||||
hasComponents: boolean;
|
||||
computations: Computation[];
|
||||
templateProperties: Record<string, Node>;
|
||||
javascript: [string, string];
|
||||
|
||||
used: {
|
||||
components: Set<string>;
|
||||
helpers: Set<string>;
|
||||
events: Set<string>;
|
||||
animations: Set<string>;
|
||||
transitions: Set<string>;
|
||||
actions: Set<string>;
|
||||
};
|
||||
|
||||
declarations: Declaration[];
|
||||
|
||||
refCallees: Node[];
|
||||
|
||||
code: MagicString;
|
||||
|
||||
indirectDependencies: Map<string, Set<string>>;
|
||||
expectedProperties: Set<string>;
|
||||
refs: Set<string>;
|
||||
|
||||
file: string;
|
||||
locate: (c: number) => { line: number, column: number };
|
||||
|
||||
stylesheet: Stylesheet;
|
||||
|
||||
userVars: Set<string>;
|
||||
templateVars: Map<string, string>;
|
||||
aliases: Map<string, string>;
|
||||
usedNames: Set<string>;
|
||||
|
||||
locator: (search: number, startIndex?: number) => {
|
||||
line: number,
|
||||
column: number
|
||||
};
|
||||
|
||||
constructor(
|
||||
ast: Ast,
|
||||
source: string,
|
||||
name: string,
|
||||
options: CompileOptions,
|
||||
stats: Stats
|
||||
) {
|
||||
this.stats = stats;
|
||||
|
||||
this.ast = ast;
|
||||
this.source = source;
|
||||
this.options = options;
|
||||
|
||||
this.imports = [];
|
||||
this.shorthandImports = [];
|
||||
this.helpers = new Set();
|
||||
this.components = new Set();
|
||||
this.events = new Set();
|
||||
this.methods = new Set();
|
||||
this.animations = new Set();
|
||||
this.transitions = new Set();
|
||||
this.actions = new Set();
|
||||
this.importedComponents = new Map();
|
||||
|
||||
this.used = {
|
||||
components: new Set(),
|
||||
helpers: new Set(),
|
||||
events: new Set(),
|
||||
animations: new Set(),
|
||||
transitions: new Set(),
|
||||
actions: new Set(),
|
||||
};
|
||||
|
||||
this.declarations = [];
|
||||
|
||||
this.refs = new Set();
|
||||
this.refCallees = [];
|
||||
|
||||
this.indirectDependencies = new Map();
|
||||
|
||||
this.file = options.filename && (
|
||||
typeof process !== 'undefined' ? options.filename.replace(process.cwd(), '').replace(/^[\/\\]/, '') : options.filename
|
||||
);
|
||||
this.locate = getLocator(this.source);
|
||||
|
||||
// track which properties are needed, so we can provide useful info
|
||||
// in dev mode
|
||||
this.expectedProperties = new Set();
|
||||
|
||||
this.code = new MagicString(source);
|
||||
|
||||
// styles
|
||||
this.stylesheet = new Stylesheet(source, ast, options.filename, options.dev);
|
||||
this.stylesheet.validate(this);
|
||||
|
||||
// allow compiler to deconflict user's `import { get } from 'whatever'` and
|
||||
// Svelte's builtin `import { get, ... } from 'svelte/shared.ts'`;
|
||||
this.userVars = new Set();
|
||||
this.templateVars = new Map();
|
||||
this.aliases = new Map();
|
||||
this.usedNames = new Set();
|
||||
|
||||
this.computations = [];
|
||||
this.templateProperties = {};
|
||||
this.properties = new Map();
|
||||
|
||||
this.walkJs();
|
||||
this.name = this.alias(name);
|
||||
|
||||
if (options.customElement === true) {
|
||||
this.customElement = {
|
||||
tag: this.tag,
|
||||
props: this.props
|
||||
}
|
||||
} else {
|
||||
this.customElement = options.customElement;
|
||||
}
|
||||
|
||||
if (this.customElement && !this.customElement.tag) {
|
||||
throw new Error(`No tag name specified`); // TODO better error
|
||||
}
|
||||
|
||||
this.fragment = new Fragment(this, ast.html);
|
||||
// this.walkTemplate();
|
||||
if (!this.customElement) this.stylesheet.reify();
|
||||
|
||||
this.stylesheet.warnOnUnusedSelectors(options.onwarn);
|
||||
|
||||
if (this.defaultExport) {
|
||||
const categories = {
|
||||
components: 'component',
|
||||
helpers: 'helper',
|
||||
events: 'event definition',
|
||||
transitions: 'transition',
|
||||
actions: 'actions',
|
||||
};
|
||||
|
||||
Object.keys(categories).forEach(category => {
|
||||
const definitions = this.defaultExport.declaration.properties.find(prop => prop.key.name === category);
|
||||
if (definitions) {
|
||||
definitions.value.properties.forEach(prop => {
|
||||
const { name } = prop.key;
|
||||
if (!this.used[category].has(name)) {
|
||||
this.warn(prop, {
|
||||
code: `unused-${category.slice(0, -1)}`,
|
||||
message: `The '${name}' ${categories[category]} is unused`
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
this.refCallees.forEach(callee => {
|
||||
const { parts } = flattenReference(callee);
|
||||
const ref = parts[1];
|
||||
|
||||
if (this.refs.has(ref)) {
|
||||
// TODO check method is valid, e.g. `audio.stop()` should be `audio.pause()`
|
||||
} else {
|
||||
const match = fuzzymatch(ref, Array.from(this.refs.keys()));
|
||||
|
||||
let message = `'refs.${ref}' does not exist`;
|
||||
if (match) message += ` (did you mean 'refs.${match}'?)`;
|
||||
|
||||
this.error(callee, {
|
||||
code: `missing-ref`,
|
||||
message
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
addSourcemapLocations(node: Node) {
|
||||
walk(node, {
|
||||
enter: (node: Node) => {
|
||||
this.code.addSourcemapLocation(node.start);
|
||||
this.code.addSourcemapLocation(node.end);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
alias(name: string) {
|
||||
if (!this.aliases.has(name)) {
|
||||
this.aliases.set(name, this.getUniqueName(name));
|
||||
}
|
||||
|
||||
return this.aliases.get(name);
|
||||
}
|
||||
|
||||
generate(result: string, options: CompileOptions, {
|
||||
banner = '',
|
||||
name,
|
||||
format
|
||||
}) {
|
||||
const pattern = /\[✂(\d+)-(\d+)$/;
|
||||
|
||||
const helpers = new Set();
|
||||
|
||||
// TODO use same regex for both
|
||||
result = result.replace(options.generate === 'ssr' ? /(@+|#+|%+)(\w*(?:-\w*)?)/g : /(%+|@+)(\w*(?:-\w*)?)/g, (match: string, sigil: string, name: string) => {
|
||||
if (sigil === '@') {
|
||||
if (name in shared) {
|
||||
if (options.dev && `${name}Dev` in shared) name = `${name}Dev`;
|
||||
helpers.add(name);
|
||||
}
|
||||
|
||||
return this.alias(name);
|
||||
}
|
||||
|
||||
if (sigil === '%') {
|
||||
return this.templateVars.get(name);
|
||||
}
|
||||
|
||||
return sigil.slice(1) + name;
|
||||
});
|
||||
|
||||
let importedHelpers;
|
||||
|
||||
if (options.shared) {
|
||||
if (format !== 'es' && format !== 'cjs') {
|
||||
throw new Error(`Components with shared helpers must be compiled with \`format: 'es'\` or \`format: 'cjs'\``);
|
||||
}
|
||||
|
||||
importedHelpers = Array.from(helpers).sort().map(name => {
|
||||
const alias = this.alias(name);
|
||||
return { name, alias };
|
||||
});
|
||||
} else {
|
||||
let inlineHelpers = '';
|
||||
|
||||
const component = this;
|
||||
|
||||
importedHelpers = [];
|
||||
|
||||
helpers.forEach(name => {
|
||||
const str = shared[name];
|
||||
const code = new MagicString(str);
|
||||
const expression = parseExpressionAt(str, 0);
|
||||
|
||||
let { scope } = annotateWithScopes(expression);
|
||||
|
||||
walk(expression, {
|
||||
enter(node: Node, parent: Node) {
|
||||
if (node._scope) scope = node._scope;
|
||||
|
||||
if (
|
||||
node.type === 'Identifier' &&
|
||||
isReference(node, parent) &&
|
||||
!scope.has(node.name)
|
||||
) {
|
||||
if (node.name in shared) {
|
||||
// this helper function depends on another one
|
||||
const dependency = node.name;
|
||||
helpers.add(dependency);
|
||||
|
||||
const alias = component.alias(dependency);
|
||||
if (alias !== node.name) {
|
||||
code.overwrite(node.start, node.end, alias);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
leave(node: Node) {
|
||||
if (node._scope) scope = scope.parent;
|
||||
},
|
||||
});
|
||||
|
||||
if (name === 'transitionManager' || name === 'outros') {
|
||||
// special case
|
||||
const global = name === 'outros'
|
||||
? `_svelteOutros`
|
||||
: `_svelteTransitionManager`;
|
||||
|
||||
inlineHelpers += `\n\nvar ${this.alias(name)} = window.${global} || (window.${global} = ${code});\n\n`;
|
||||
} else if (name === 'escaped' || name === 'missingComponent' || name === 'invalidAttributeNameCharacter') {
|
||||
// vars are an awkward special case... would be nice to avoid this
|
||||
const alias = this.alias(name);
|
||||
inlineHelpers += `\n\nconst ${alias} = ${code};`
|
||||
} else {
|
||||
const alias = this.alias(expression.id.name);
|
||||
if (alias !== expression.id.name) {
|
||||
code.overwrite(expression.id.start, expression.id.end, alias);
|
||||
}
|
||||
|
||||
inlineHelpers += `\n\n${code}`;
|
||||
}
|
||||
});
|
||||
|
||||
result += inlineHelpers;
|
||||
}
|
||||
|
||||
const sharedPath = options.shared === true
|
||||
? 'svelte/shared.js'
|
||||
: options.shared || '';
|
||||
|
||||
const module = wrapModule(result, format, name, options, banner, sharedPath, importedHelpers, this.imports, this.shorthandImports, this.source);
|
||||
|
||||
const parts = module.split('✂]');
|
||||
const finalChunk = parts.pop();
|
||||
|
||||
const compiled = new Bundle({ separator: '' });
|
||||
|
||||
function addString(str: string) {
|
||||
compiled.addSource({
|
||||
content: new MagicString(str),
|
||||
});
|
||||
}
|
||||
|
||||
const { filename } = options;
|
||||
|
||||
// special case — the source file doesn't actually get used anywhere. we need
|
||||
// to add an empty file to populate map.sources and map.sourcesContent
|
||||
if (!parts.length) {
|
||||
compiled.addSource({
|
||||
filename,
|
||||
content: new MagicString(this.source).remove(0, this.source.length),
|
||||
});
|
||||
}
|
||||
|
||||
parts.forEach((str: string) => {
|
||||
const chunk = str.replace(pattern, '');
|
||||
if (chunk) addString(chunk);
|
||||
|
||||
const match = pattern.exec(str);
|
||||
|
||||
const snippet = this.code.snip(+match[1], +match[2]);
|
||||
|
||||
compiled.addSource({
|
||||
filename,
|
||||
content: snippet,
|
||||
});
|
||||
});
|
||||
|
||||
addString(finalChunk);
|
||||
|
||||
const css = this.customElement ?
|
||||
{ code: null, map: null } :
|
||||
this.stylesheet.render(options.cssOutputFilename, true);
|
||||
|
||||
const js = {
|
||||
code: compiled.toString(),
|
||||
map: compiled.generateMap({
|
||||
includeContent: true,
|
||||
file: options.outputFilename,
|
||||
})
|
||||
};
|
||||
|
||||
return {
|
||||
ast: this.ast,
|
||||
js,
|
||||
css,
|
||||
stats: this.stats.render(this)
|
||||
};
|
||||
}
|
||||
|
||||
getUniqueName(name: string) {
|
||||
if (test) name = `${name}$`;
|
||||
let alias = name;
|
||||
for (
|
||||
let i = 1;
|
||||
reservedNames.has(alias) ||
|
||||
this.userVars.has(alias) ||
|
||||
this.usedNames.has(alias);
|
||||
alias = `${name}_${i++}`
|
||||
);
|
||||
this.usedNames.add(alias);
|
||||
return alias;
|
||||
}
|
||||
|
||||
getUniqueNameMaker() {
|
||||
const localUsedNames = new Set();
|
||||
|
||||
function add(name: string) {
|
||||
localUsedNames.add(name);
|
||||
}
|
||||
|
||||
reservedNames.forEach(add);
|
||||
this.userVars.forEach(add);
|
||||
|
||||
return (name: string) => {
|
||||
if (test) name = `${name}$`;
|
||||
let alias = name;
|
||||
for (
|
||||
let i = 1;
|
||||
this.usedNames.has(alias) ||
|
||||
localUsedNames.has(alias);
|
||||
alias = `${name}_${i++}`
|
||||
);
|
||||
localUsedNames.add(alias);
|
||||
return alias;
|
||||
};
|
||||
}
|
||||
|
||||
error(
|
||||
pos: {
|
||||
start: number,
|
||||
end: number
|
||||
},
|
||||
e : {
|
||||
code: string,
|
||||
message: string
|
||||
}
|
||||
) {
|
||||
error(e.message, {
|
||||
name: 'ValidationError',
|
||||
code: e.code,
|
||||
source: this.source,
|
||||
start: pos.start,
|
||||
end: pos.end,
|
||||
filename: this.options.filename
|
||||
});
|
||||
}
|
||||
|
||||
warn(
|
||||
pos: {
|
||||
start: number,
|
||||
end: number
|
||||
},
|
||||
warning: {
|
||||
code: string,
|
||||
message: string
|
||||
}
|
||||
) {
|
||||
if (!this.locator) {
|
||||
this.locator = getLocator(this.source, { offsetLine: 1 });
|
||||
}
|
||||
|
||||
const start = this.locator(pos.start);
|
||||
const end = this.locator(pos.end);
|
||||
|
||||
const frame = getCodeFrame(this.source, start.line - 1, start.column);
|
||||
|
||||
this.stats.warn({
|
||||
code: warning.code,
|
||||
message: warning.message,
|
||||
frame,
|
||||
start,
|
||||
end,
|
||||
pos: pos.start,
|
||||
filename: this.options.filename,
|
||||
toString: () => `${warning.message} (${start.line + 1}:${start.column})\n${frame}`,
|
||||
});
|
||||
}
|
||||
|
||||
processDefaultExport(node, indentExclusionRanges) {
|
||||
const { templateProperties, source, code } = this;
|
||||
|
||||
if (node.declaration.type !== 'ObjectExpression') {
|
||||
this.error(node.declaration, {
|
||||
code: `invalid-default-export`,
|
||||
message: `Default export must be an object literal`
|
||||
});
|
||||
}
|
||||
|
||||
checkForComputedKeys(this, node.declaration.properties);
|
||||
checkForDupes(this, node.declaration.properties);
|
||||
|
||||
const props = this.properties;
|
||||
|
||||
node.declaration.properties.forEach((prop: Node) => {
|
||||
props.set(getName(prop.key), prop);
|
||||
});
|
||||
|
||||
const validPropList = Object.keys(propValidators);
|
||||
|
||||
// ensure all exported props are valid
|
||||
node.declaration.properties.forEach((prop: Node) => {
|
||||
const name = getName(prop.key);
|
||||
const propValidator = propValidators[name];
|
||||
|
||||
if (propValidator) {
|
||||
propValidator(this, prop);
|
||||
} else {
|
||||
const match = fuzzymatch(name, validPropList);
|
||||
if (match) {
|
||||
this.error(prop, {
|
||||
code: `unexpected-property`,
|
||||
message: `Unexpected property '${name}' (did you mean '${match}'?)`
|
||||
});
|
||||
} else if (/FunctionExpression/.test(prop.value.type)) {
|
||||
this.error(prop, {
|
||||
code: `unexpected-property`,
|
||||
message: `Unexpected property '${name}' (did you mean to include it in 'methods'?)`
|
||||
});
|
||||
} else {
|
||||
this.error(prop, {
|
||||
code: `unexpected-property`,
|
||||
message: `Unexpected property '${name}'`
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (props.has('namespace')) {
|
||||
const ns = nodeToString(props.get('namespace').value);
|
||||
this.namespace = namespaces[ns] || ns;
|
||||
}
|
||||
|
||||
node.declaration.properties.forEach((prop: Node) => {
|
||||
templateProperties[getName(prop.key)] = prop;
|
||||
});
|
||||
|
||||
['helpers', 'events', 'components', 'transitions', 'actions', 'animations'].forEach(key => {
|
||||
if (templateProperties[key]) {
|
||||
templateProperties[key].value.properties.forEach((prop: Node) => {
|
||||
this[key].add(getName(prop.key));
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const addArrowFunctionExpression = (type: string, name: string, node: Node) => {
|
||||
const { body, params, async } = node;
|
||||
const fnKeyword = async ? 'async function' : 'function';
|
||||
|
||||
const paramString = params.length ?
|
||||
`[✂${params[0].start}-${params[params.length - 1].end}✂]` :
|
||||
``;
|
||||
|
||||
const block = body.type === 'BlockStatement'
|
||||
? deindent`
|
||||
${fnKeyword} ${name}(${paramString}) [✂${body.start}-${body.end}✂]
|
||||
`
|
||||
: deindent`
|
||||
${fnKeyword} ${name}(${paramString}) {
|
||||
return [✂${body.start}-${body.end}✂];
|
||||
}
|
||||
`;
|
||||
|
||||
this.declarations.push({ type, name, block, node });
|
||||
};
|
||||
|
||||
const addFunctionExpression = (type: string, name: string, node: Node) => {
|
||||
const { async } = node;
|
||||
const fnKeyword = async ? 'async function' : 'function';
|
||||
|
||||
let c = node.start;
|
||||
while (this.source[c] !== '(') c += 1;
|
||||
|
||||
const block = deindent`
|
||||
${fnKeyword} ${name}[✂${c}-${node.end}✂];
|
||||
`;
|
||||
|
||||
this.declarations.push({ type, name, block, node });
|
||||
};
|
||||
|
||||
const addValue = (type: string, name: string, node: Node) => {
|
||||
const block = deindent`
|
||||
var ${name} = [✂${node.start}-${node.end}✂];
|
||||
`;
|
||||
|
||||
this.declarations.push({ type, name, block, node });
|
||||
};
|
||||
|
||||
const addDeclaration = (
|
||||
type: string,
|
||||
key: string,
|
||||
node: Node,
|
||||
allowShorthandImport?: boolean,
|
||||
disambiguator?: string,
|
||||
conflicts?: Record<string, boolean>
|
||||
) => {
|
||||
const qualified = disambiguator ? `${disambiguator}-${key}` : key;
|
||||
|
||||
if (node.type === 'Identifier' && node.name === key) {
|
||||
this.templateVars.set(qualified, key);
|
||||
return;
|
||||
}
|
||||
|
||||
let deconflicted = key;
|
||||
if (conflicts) while (deconflicted in conflicts) deconflicted += '_'
|
||||
|
||||
let name = this.getUniqueName(deconflicted);
|
||||
this.templateVars.set(qualified, name);
|
||||
|
||||
if (allowShorthandImport && node.type === 'Literal' && typeof node.value === 'string') {
|
||||
this.shorthandImports.push({ name, source: node.value });
|
||||
return;
|
||||
}
|
||||
|
||||
// deindent
|
||||
const indentationLevel = getIndentationLevel(source, node.start);
|
||||
if (indentationLevel) {
|
||||
removeIndentation(code, node.start, node.end, indentationLevel, indentExclusionRanges);
|
||||
}
|
||||
|
||||
if (node.type === 'ArrowFunctionExpression') {
|
||||
addArrowFunctionExpression(type, name, node);
|
||||
} else if (node.type === 'FunctionExpression') {
|
||||
addFunctionExpression(type, name, node);
|
||||
} else {
|
||||
addValue(type, name, node);
|
||||
}
|
||||
};
|
||||
|
||||
if (templateProperties.components) {
|
||||
templateProperties.components.value.properties.forEach((property: Node) => {
|
||||
addDeclaration('components', getName(property.key), property.value, true, 'components');
|
||||
});
|
||||
}
|
||||
|
||||
if (templateProperties.computed) {
|
||||
const dependencies = new Map();
|
||||
|
||||
const fullStateComputations = [];
|
||||
|
||||
templateProperties.computed.value.properties.forEach((prop: Node) => {
|
||||
const key = getName(prop.key);
|
||||
const value = prop.value;
|
||||
|
||||
addDeclaration('computed', key, value, false, 'computed', {
|
||||
state: true,
|
||||
changed: true
|
||||
});
|
||||
|
||||
const param = value.params[0];
|
||||
|
||||
const hasRestParam = (
|
||||
param.properties &&
|
||||
param.properties.some(prop => prop.type === 'RestElement')
|
||||
);
|
||||
|
||||
if (param.type !== 'ObjectPattern' || hasRestParam) {
|
||||
fullStateComputations.push({ key, deps: null, hasRestParam });
|
||||
} else {
|
||||
const deps = param.properties.map(prop => prop.key.name);
|
||||
|
||||
deps.forEach(dep => {
|
||||
this.expectedProperties.add(dep);
|
||||
});
|
||||
dependencies.set(key, deps);
|
||||
}
|
||||
});
|
||||
|
||||
const visited = new Set();
|
||||
|
||||
const visit = (key: string) => {
|
||||
if (!dependencies.has(key)) return; // not a computation
|
||||
|
||||
if (visited.has(key)) return;
|
||||
visited.add(key);
|
||||
|
||||
const deps = dependencies.get(key);
|
||||
deps.forEach(visit);
|
||||
|
||||
this.computations.push({ key, deps, hasRestParam: false });
|
||||
|
||||
const prop = templateProperties.computed.value.properties.find((prop: Node) => getName(prop.key) === key);
|
||||
};
|
||||
|
||||
templateProperties.computed.value.properties.forEach((prop: Node) =>
|
||||
visit(getName(prop.key))
|
||||
);
|
||||
|
||||
if (fullStateComputations.length > 0) {
|
||||
this.computations.push(...fullStateComputations);
|
||||
}
|
||||
}
|
||||
|
||||
if (templateProperties.data) {
|
||||
addDeclaration('data', 'data', templateProperties.data.value);
|
||||
}
|
||||
|
||||
if (templateProperties.events) {
|
||||
templateProperties.events.value.properties.forEach((property: Node) => {
|
||||
addDeclaration('events', getName(property.key), property.value, false, 'events');
|
||||
});
|
||||
}
|
||||
|
||||
if (templateProperties.helpers) {
|
||||
templateProperties.helpers.value.properties.forEach((property: Node) => {
|
||||
addDeclaration('helpers', getName(property.key), property.value, false, 'helpers');
|
||||
});
|
||||
}
|
||||
|
||||
if (templateProperties.methods) {
|
||||
addDeclaration('methods', 'methods', templateProperties.methods.value);
|
||||
|
||||
templateProperties.methods.value.properties.forEach(property => {
|
||||
this.methods.add(getName(property.key));
|
||||
});
|
||||
}
|
||||
|
||||
if (templateProperties.namespace) {
|
||||
const ns = nodeToString(templateProperties.namespace.value);
|
||||
this.namespace = namespaces[ns] || ns;
|
||||
}
|
||||
|
||||
if (templateProperties.oncreate) {
|
||||
addDeclaration('oncreate', 'oncreate', templateProperties.oncreate.value);
|
||||
}
|
||||
|
||||
if (templateProperties.ondestroy) {
|
||||
addDeclaration('ondestroy', 'ondestroy', templateProperties.ondestroy.value);
|
||||
}
|
||||
|
||||
if (templateProperties.onstate) {
|
||||
addDeclaration('onstate', 'onstate', templateProperties.onstate.value);
|
||||
}
|
||||
|
||||
if (templateProperties.onupdate) {
|
||||
addDeclaration('onupdate', 'onupdate', templateProperties.onupdate.value);
|
||||
}
|
||||
|
||||
if (templateProperties.preload) {
|
||||
addDeclaration('preload', 'preload', templateProperties.preload.value);
|
||||
}
|
||||
|
||||
if (templateProperties.props) {
|
||||
this.props = templateProperties.props.value.elements.map((element: Node) => nodeToString(element));
|
||||
}
|
||||
|
||||
if (templateProperties.setup) {
|
||||
addDeclaration('setup', 'setup', templateProperties.setup.value);
|
||||
}
|
||||
|
||||
if (templateProperties.store) {
|
||||
addDeclaration('store', 'store', templateProperties.store.value);
|
||||
}
|
||||
|
||||
if (templateProperties.tag) {
|
||||
this.tag = nodeToString(templateProperties.tag.value);
|
||||
}
|
||||
|
||||
if (templateProperties.transitions) {
|
||||
templateProperties.transitions.value.properties.forEach((property: Node) => {
|
||||
addDeclaration('transitions', getName(property.key), property.value, false, 'transitions');
|
||||
});
|
||||
}
|
||||
|
||||
if (templateProperties.animations) {
|
||||
templateProperties.animations.value.properties.forEach((property: Node) => {
|
||||
addDeclaration('animations', getName(property.key), property.value, false, 'animations');
|
||||
});
|
||||
}
|
||||
|
||||
if (templateProperties.actions) {
|
||||
templateProperties.actions.value.properties.forEach((property: Node) => {
|
||||
addDeclaration('actions', getName(property.key), property.value, false, 'actions');
|
||||
});
|
||||
}
|
||||
|
||||
this.defaultExport = node;
|
||||
}
|
||||
|
||||
walkJs() {
|
||||
const { js } = this.ast;
|
||||
if (!js) return;
|
||||
|
||||
this.addSourcemapLocations(js.content);
|
||||
|
||||
const { code, source, imports } = this;
|
||||
|
||||
const indentationLevel = getIndentationLevel(source, js.content.body[0].start);
|
||||
const indentExclusionRanges = getIndentExclusionRanges(js.content);
|
||||
|
||||
const { scope, globals } = annotateWithScopes(js.content);
|
||||
|
||||
scope.declarations.forEach(name => {
|
||||
this.userVars.add(name);
|
||||
});
|
||||
|
||||
globals.forEach(name => {
|
||||
this.userVars.add(name);
|
||||
});
|
||||
|
||||
const body = js.content.body.slice(); // slice, because we're going to be mutating the original
|
||||
|
||||
body.forEach(node => {
|
||||
// check there are no named exports
|
||||
if (node.type === 'ExportNamedDeclaration') {
|
||||
this.error(node, {
|
||||
code: `named-export`,
|
||||
message: `A component can only have a default export`
|
||||
});
|
||||
}
|
||||
|
||||
if (node.type === 'ExportDefaultDeclaration') {
|
||||
this.processDefaultExport(node, indentExclusionRanges);
|
||||
}
|
||||
|
||||
// imports need to be hoisted out of the IIFE
|
||||
else if (node.type === 'ImportDeclaration') {
|
||||
removeNode(code, js.content, node);
|
||||
imports.push(node);
|
||||
|
||||
node.specifiers.forEach((specifier: Node) => {
|
||||
this.userVars.add(specifier.local.name);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
if (indentationLevel) {
|
||||
if (this.defaultExport) {
|
||||
removeIndentation(code, js.content.start, this.defaultExport.start, indentationLevel, indentExclusionRanges);
|
||||
removeIndentation(code, this.defaultExport.end, js.content.end, indentationLevel, indentExclusionRanges);
|
||||
} else {
|
||||
removeIndentation(code, js.content.start, js.content.end, indentationLevel, indentExclusionRanges);
|
||||
}
|
||||
}
|
||||
|
||||
let a = js.content.start;
|
||||
while (/\s/.test(source[a])) a += 1;
|
||||
|
||||
let b = js.content.end;
|
||||
while (/\s/.test(source[b - 1])) b -= 1;
|
||||
|
||||
this.javascript = this.defaultExport
|
||||
? [
|
||||
a !== this.defaultExport.start ? `[✂${a}-${this.defaultExport.start}✂]` : '',
|
||||
b !== this.defaultExport.end ?`[✂${this.defaultExport.end}-${b}✂]` : ''
|
||||
]
|
||||
: [
|
||||
a !== b ? `[✂${a}-${b}✂]` : '',
|
||||
''
|
||||
];
|
||||
}
|
||||
}
|
@ -0,0 +1,45 @@
|
||||
import Node from './shared/Node';
|
||||
import Expression from './shared/Expression';
|
||||
|
||||
export default class Animation extends Node {
|
||||
type: 'Animation';
|
||||
name: string;
|
||||
expression: Expression;
|
||||
|
||||
constructor(component, parent, scope, info) {
|
||||
super(component, parent, scope, info);
|
||||
|
||||
this.name = info.name;
|
||||
|
||||
component.used.animations.add(this.name);
|
||||
|
||||
if (parent.animation) {
|
||||
component.error(this, {
|
||||
code: `duplicate-animation`,
|
||||
message: `An element can only have one 'animate' directive`
|
||||
});
|
||||
}
|
||||
|
||||
if (!component.animations.has(this.name)) {
|
||||
component.error(this, {
|
||||
code: `missing-animation`,
|
||||
message: `Missing animation '${this.name}'`
|
||||
});
|
||||
}
|
||||
|
||||
const block = parent.parent;
|
||||
if (!block || block.type !== 'EachBlock' || !block.key) {
|
||||
// TODO can we relax the 'immediate child' rule?
|
||||
component.error(this, {
|
||||
code: `invalid-animation`,
|
||||
message: `An element that use the animate directive must be the immediate child of a keyed each block`
|
||||
});
|
||||
}
|
||||
|
||||
block.hasAnimation = true;
|
||||
|
||||
this.expression = info.expression
|
||||
? new Expression(component, this, scope, info.expression)
|
||||
: null;
|
||||
}
|
||||
}
|
@ -1,13 +1,15 @@
|
||||
import Node from './shared/Node';
|
||||
import Block from '../dom/Block';
|
||||
import Block from '../render-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);
|
||||
constructor(component, parent, scope, info) {
|
||||
super(component, parent, scope, info);
|
||||
this.children = mapChildren(component, parent, scope, info.children);
|
||||
|
||||
this.warnIfEmptyBlock();
|
||||
}
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
import Node from './shared/Node';
|
||||
import Expression from './shared/Expression';
|
||||
|
||||
export default class Class extends Node {
|
||||
type: 'Class';
|
||||
name: string;
|
||||
expression: Expression;
|
||||
|
||||
constructor(component, parent, scope, info) {
|
||||
super(component, parent, scope, info);
|
||||
|
||||
this.name = info.name;
|
||||
|
||||
this.expression = info.expression
|
||||
? new Expression(component, this, scope, info.expression)
|
||||
: null;
|
||||
}
|
||||
}
|
@ -0,0 +1,14 @@
|
||||
import Node from './shared/Node';
|
||||
import Expression from './shared/Expression';
|
||||
|
||||
export default class DebugTag extends Node {
|
||||
expressions: Expression[];
|
||||
|
||||
constructor(component, parent, scope, info) {
|
||||
super(component, parent, scope, info);
|
||||
|
||||
this.expressions = info.identifiers.map(node => {
|
||||
return new Expression(component, parent, scope, node);
|
||||
});
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
@ -1,13 +1,16 @@
|
||||
import Node from './shared/Node';
|
||||
import Block from '../dom/Block';
|
||||
import Block from '../render-dom/Block';
|
||||
import mapChildren from './shared/mapChildren';
|
||||
|
||||
export default class ElseBlock extends Node {
|
||||
type: 'ElseBlock';
|
||||
children: Node[];
|
||||
block: Block;
|
||||
|
||||
constructor(compiler, parent, scope, info) {
|
||||
super(compiler, parent, scope, info);
|
||||
this.children = mapChildren(compiler, this, scope, info.children);
|
||||
constructor(component, parent, scope, info) {
|
||||
super(component, parent, scope, info);
|
||||
this.children = mapChildren(component, this, scope, info.children);
|
||||
|
||||
this.warnIfEmptyBlock();
|
||||
}
|
||||
}
|
@ -1,48 +1,23 @@
|
||||
import deindent from '../../utils/deindent';
|
||||
import { stringify } from '../../utils/stringify';
|
||||
import Node from './shared/Node';
|
||||
import Block from '../dom/Block';
|
||||
import Attribute from './Attribute';
|
||||
import Block from '../render-dom/Block';
|
||||
import mapChildren from './shared/mapChildren';
|
||||
|
||||
export default class Head extends Node {
|
||||
type: 'Head';
|
||||
children: any[]; // TODO
|
||||
|
||||
constructor(compiler, parent, scope, info) {
|
||||
super(compiler, parent, scope, info);
|
||||
this.children = mapChildren(compiler, parent, scope, info.children.filter(child => {
|
||||
return (child.type !== 'Text' || /\S/.test(child.data));
|
||||
}));
|
||||
}
|
||||
constructor(component, parent, scope, info) {
|
||||
super(component, parent, scope, info);
|
||||
|
||||
init(
|
||||
block: Block,
|
||||
stripWhitespace: boolean,
|
||||
nextSibling: Node
|
||||
) {
|
||||
this.initChildren(block, true, null);
|
||||
}
|
||||
|
||||
build(
|
||||
block: Block,
|
||||
parentNode: string,
|
||||
parentNodes: string
|
||||
) {
|
||||
this.var = 'document.head';
|
||||
if (info.attributes.length) {
|
||||
component.error(info.attributes[0], {
|
||||
code: `invalid-attribute`,
|
||||
message: `<svelte:head> should not have any attributes or directives`
|
||||
});
|
||||
}
|
||||
|
||||
this.children.forEach((child: Node) => {
|
||||
child.build(block, 'document.head', null);
|
||||
});
|
||||
}
|
||||
|
||||
ssr() {
|
||||
this.compiler.target.append('${(__result.head += `');
|
||||
|
||||
this.children.forEach((child: Node) => {
|
||||
child.ssr();
|
||||
});
|
||||
|
||||
this.compiler.target.append('`, "")}');
|
||||
this.children = mapChildren(component, parent, scope, info.children.filter(child => {
|
||||
return (child.type !== 'Text' || /\S/.test(child.data));
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,90 @@
|
||||
import Node from './shared/Node';
|
||||
import Attribute from './Attribute';
|
||||
import mapChildren from './shared/mapChildren';
|
||||
import Binding from './Binding';
|
||||
import EventHandler from './EventHandler';
|
||||
import Expression from './shared/Expression';
|
||||
import Component from '../Component';
|
||||
import Ref from './Ref';
|
||||
|
||||
export default class InlineComponent extends Node {
|
||||
type: 'InlineComponent';
|
||||
name: string;
|
||||
expression: Expression;
|
||||
attributes: Attribute[];
|
||||
bindings: Binding[];
|
||||
handlers: EventHandler[];
|
||||
children: Node[];
|
||||
ref: Ref;
|
||||
|
||||
constructor(component: Component, parent, scope, info) {
|
||||
super(component, parent, scope, info);
|
||||
|
||||
component.hasComponents = true;
|
||||
|
||||
this.name = info.name;
|
||||
|
||||
if (this.name !== 'svelte:self' && this.name !== 'svelte:component') {
|
||||
if (!component.components.has(this.name)) {
|
||||
component.error(this, {
|
||||
code: `missing-component`,
|
||||
message: `${this.name} component is not defined`
|
||||
});
|
||||
}
|
||||
|
||||
component.used.components.add(this.name);
|
||||
}
|
||||
|
||||
this.expression = this.name === 'svelte:component'
|
||||
? new Expression(component, this, scope, info.expression)
|
||||
: null;
|
||||
|
||||
this.attributes = [];
|
||||
this.bindings = [];
|
||||
this.handlers = [];
|
||||
|
||||
info.attributes.forEach(node => {
|
||||
switch (node.type) {
|
||||
case 'Action':
|
||||
component.error(node, {
|
||||
code: `invalid-action`,
|
||||
message: `Actions can only be applied to DOM elements, not components`
|
||||
});
|
||||
|
||||
case 'Attribute':
|
||||
case 'Spread':
|
||||
this.attributes.push(new Attribute(component, this, scope, node));
|
||||
break;
|
||||
|
||||
case 'Binding':
|
||||
this.bindings.push(new Binding(component, this, scope, node));
|
||||
break;
|
||||
|
||||
case 'Class':
|
||||
component.error(node, {
|
||||
code: `invalid-class`,
|
||||
message: `Classes can only be applied to DOM elements, not components`
|
||||
});
|
||||
|
||||
case 'EventHandler':
|
||||
this.handlers.push(new EventHandler(component, this, scope, node));
|
||||
break;
|
||||
|
||||
case 'Ref':
|
||||
this.ref = new Ref(component, this, scope, node);
|
||||
break;
|
||||
|
||||
case 'Transition':
|
||||
component.error(node, {
|
||||
code: `invalid-transition`,
|
||||
message: `Transitions can only be applied to DOM elements, not components`
|
||||
});
|
||||
|
||||
default:
|
||||
throw new Error(`Not implemented: ${node.type}`);
|
||||
}
|
||||
});
|
||||
|
||||
this.children = mapChildren(component, this, scope, info.children);
|
||||
}
|
||||
}
|
@ -1,37 +1,3 @@
|
||||
import Node from './shared/Node';
|
||||
import Tag from './shared/Tag';
|
||||
import Block from '../dom/Block';
|
||||
|
||||
export default class MustacheTag extends Tag {
|
||||
build(
|
||||
block: Block,
|
||||
parentNode: string,
|
||||
parentNodes: string
|
||||
) {
|
||||
const { init } = this.renameThisMethod(
|
||||
block,
|
||||
value => `${this.var}.data = ${value};`
|
||||
);
|
||||
|
||||
block.addElement(
|
||||
this.var,
|
||||
`@createText(${init})`,
|
||||
parentNodes && `@claimText(${parentNodes}, ${init})`,
|
||||
parentNode
|
||||
);
|
||||
}
|
||||
|
||||
remount(name: string) {
|
||||
return `@appendNode(${this.var}, ${name}._slotted.default);`;
|
||||
}
|
||||
|
||||
ssr() {
|
||||
this.compiler.target.append(
|
||||
this.parent &&
|
||||
this.parent.type === 'Element' &&
|
||||
this.parent.name === 'style'
|
||||
? '${' + this.expression.snippet + '}'
|
||||
: '${@escape(' + this.expression.snippet + ')}'
|
||||
);
|
||||
}
|
||||
}
|
||||
export default class MustacheTag extends Tag {}
|
@ -1,13 +1,15 @@
|
||||
import Node from './shared/Node';
|
||||
import Block from '../dom/Block';
|
||||
import Block from '../render-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);
|
||||
constructor(component, parent, scope, info) {
|
||||
super(component, parent, scope, info);
|
||||
this.children = mapChildren(component, parent, scope, info.children);
|
||||
|
||||
this.warnIfEmptyBlock();
|
||||
}
|
||||
}
|
@ -1,94 +1,3 @@
|
||||
import deindent from '../../utils/deindent';
|
||||
import Node from './shared/Node';
|
||||
import Tag from './shared/Tag';
|
||||
import Block from '../dom/Block';
|
||||
|
||||
export default class RawMustacheTag extends Tag {
|
||||
build(
|
||||
block: Block,
|
||||
parentNode: string,
|
||||
parentNodes: string
|
||||
) {
|
||||
const name = this.var;
|
||||
|
||||
const needsAnchorBefore = this.prev ? this.prev.type !== 'Element' : !parentNode;
|
||||
const needsAnchorAfter = this.next ? this.next.type !== 'Element' : !parentNode;
|
||||
|
||||
const anchorBefore = needsAnchorBefore
|
||||
? block.getUniqueName(`${name}_before`)
|
||||
: (this.prev && this.prev.var) || 'null';
|
||||
|
||||
const anchorAfter = needsAnchorAfter
|
||||
? block.getUniqueName(`${name}_after`)
|
||||
: (this.next && this.next.var) || 'null';
|
||||
|
||||
let detach: string;
|
||||
let insert: (content: string) => string;
|
||||
let useInnerHTML = false;
|
||||
|
||||
if (anchorBefore === 'null' && anchorAfter === 'null') {
|
||||
useInnerHTML = true;
|
||||
detach = `${parentNode}.innerHTML = '';`;
|
||||
insert = content => `${parentNode}.innerHTML = ${content};`;
|
||||
} else if (anchorBefore === 'null') {
|
||||
detach = `@detachBefore(${anchorAfter});`;
|
||||
insert = content => `${anchorAfter}.insertAdjacentHTML("beforebegin", ${content});`;
|
||||
} else if (anchorAfter === 'null') {
|
||||
detach = `@detachAfter(${anchorBefore});`;
|
||||
insert = content => `${anchorBefore}.insertAdjacentHTML("afterend", ${content});`;
|
||||
} else {
|
||||
detach = `@detachBetween(${anchorBefore}, ${anchorAfter});`;
|
||||
insert = content => `${anchorBefore}.insertAdjacentHTML("afterend", ${content});`;
|
||||
}
|
||||
|
||||
const { init } = this.renameThisMethod(
|
||||
block,
|
||||
content => deindent`
|
||||
${!useInnerHTML && detach}
|
||||
${insert(content)}
|
||||
`
|
||||
);
|
||||
|
||||
// we would have used comments here, but the `insertAdjacentHTML` api only
|
||||
// exists for `Element`s.
|
||||
if (needsAnchorBefore) {
|
||||
block.addElement(
|
||||
anchorBefore,
|
||||
`@createElement('noscript')`,
|
||||
parentNodes && `@createElement('noscript')`,
|
||||
parentNode
|
||||
);
|
||||
}
|
||||
|
||||
function addAnchorAfter() {
|
||||
block.addElement(
|
||||
anchorAfter,
|
||||
`@createElement('noscript')`,
|
||||
parentNodes && `@createElement('noscript')`,
|
||||
parentNode
|
||||
);
|
||||
}
|
||||
|
||||
if (needsAnchorAfter && anchorBefore === 'null') {
|
||||
// anchorAfter needs to be in the DOM before we
|
||||
// insert the HTML...
|
||||
addAnchorAfter();
|
||||
}
|
||||
|
||||
block.builders.mount.addLine(insert(init));
|
||||
block.builders.detachRaw.addBlock(detach);
|
||||
|
||||
if (needsAnchorAfter && anchorBefore !== 'null') {
|
||||
// ...otherwise it should go afterwards
|
||||
addAnchorAfter();
|
||||
}
|
||||
}
|
||||
|
||||
remount(name: string) {
|
||||
return `@appendNode(${this.var}, ${name}._slotted.default);`;
|
||||
}
|
||||
|
||||
ssr() {
|
||||
this.compiler.target.append('${' + this.expression.snippet + '}');
|
||||
}
|
||||
}
|
||||
export default class RawMustacheTag extends Tag {}
|
@ -0,0 +1,31 @@
|
||||
import Node from './shared/Node';
|
||||
import isValidIdentifier from '../../utils/isValidIdentifier';
|
||||
|
||||
export default class Ref extends Node {
|
||||
type: 'Ref';
|
||||
name: string;
|
||||
|
||||
constructor(component, parent, scope, info) {
|
||||
super(component, parent, scope, info);
|
||||
|
||||
if (parent.ref) {
|
||||
component.error({
|
||||
code: 'duplicate-refs',
|
||||
message: `Duplicate refs`
|
||||
});
|
||||
}
|
||||
|
||||
if (!isValidIdentifier(info.name)) {
|
||||
const suggestion = info.name.replace(/[^_$a-z0-9]/ig, '_').replace(/^\d/, '_$&');
|
||||
|
||||
component.error(info, {
|
||||
code: `invalid-reference-name`,
|
||||
message: `Reference name '${info.name}' is invalid — must be a valid identifier such as ${suggestion}`
|
||||
});
|
||||
} else {
|
||||
component.refs.add(info.name);
|
||||
}
|
||||
|
||||
this.name = info.name;
|
||||
}
|
||||
}
|
@ -1,83 +1,12 @@
|
||||
import { escape, escapeHTML, escapeTemplate, stringify } from '../../utils/stringify';
|
||||
import Node from './shared/Node';
|
||||
import Block from '../dom/Block';
|
||||
|
||||
// Whitespace inside one of these elements will not result in
|
||||
// a whitespace node being created in any circumstances. (This
|
||||
// list is almost certainly very incomplete)
|
||||
const elementsWithoutText = new Set([
|
||||
'audio',
|
||||
'datalist',
|
||||
'dl',
|
||||
'ol',
|
||||
'optgroup',
|
||||
'select',
|
||||
'ul',
|
||||
'video',
|
||||
]);
|
||||
|
||||
function shouldSkip(node: Text) {
|
||||
if (/\S/.test(node.data)) return false;
|
||||
|
||||
const parentElement = node.findNearest(/(?:Element|Component|Head)/);
|
||||
if (!parentElement) return false;
|
||||
|
||||
if (parentElement.type === 'Head') return true;
|
||||
if (parentElement.type === 'Component') return parentElement.children.length === 1 && node === parentElement.children[0];
|
||||
|
||||
return parentElement.namespace || elementsWithoutText.has(parentElement.name);
|
||||
}
|
||||
|
||||
export default class Text extends Node {
|
||||
type: 'Text';
|
||||
data: string;
|
||||
shouldSkip: boolean;
|
||||
|
||||
constructor(compiler, parent, scope, info) {
|
||||
super(compiler, parent, scope, info);
|
||||
constructor(component, parent, scope, info) {
|
||||
super(component, parent, scope, info);
|
||||
this.data = info.data;
|
||||
}
|
||||
|
||||
init(block: Block) {
|
||||
const parentElement = this.findNearest(/(?:Element|Component)/);
|
||||
|
||||
if (shouldSkip(this)) {
|
||||
this.shouldSkip = true;
|
||||
return;
|
||||
}
|
||||
|
||||
this.var = block.getUniqueName(`text`);
|
||||
}
|
||||
|
||||
build(
|
||||
block: Block,
|
||||
parentNode: string,
|
||||
parentNodes: string
|
||||
) {
|
||||
if (this.shouldSkip) return;
|
||||
|
||||
block.addElement(
|
||||
this.var,
|
||||
`@createText(${stringify(this.data)})`,
|
||||
parentNodes && `@claimText(${parentNodes}, ${stringify(this.data)})`,
|
||||
parentNode
|
||||
);
|
||||
}
|
||||
|
||||
remount(name: string) {
|
||||
return `@appendNode(${this.var}, ${name}._slotted.default);`;
|
||||
}
|
||||
|
||||
ssr() {
|
||||
let text = this.data;
|
||||
if (
|
||||
!this.parent ||
|
||||
this.parent.type !== 'Element' ||
|
||||
(this.parent.name !== 'script' && this.parent.name !== 'style')
|
||||
) {
|
||||
// unless this Text node is inside a <script> or <style> element, escape &,<,>
|
||||
text = escapeHTML(text);
|
||||
}
|
||||
this.compiler.target.append(escape(escapeTemplate(text)));
|
||||
}
|
||||
}
|
@ -1,13 +1,15 @@
|
||||
import Node from './shared/Node';
|
||||
import Block from '../dom/Block';
|
||||
import Block from '../render-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);
|
||||
constructor(component, parent, scope, info) {
|
||||
super(component, parent, scope, info);
|
||||
this.children = mapChildren(component, parent, scope, info.children);
|
||||
|
||||
this.warnIfEmptyBlock();
|
||||
}
|
||||
}
|
@ -1,244 +1,74 @@
|
||||
import CodeBuilder from '../../utils/CodeBuilder';
|
||||
import deindent from '../../utils/deindent';
|
||||
import { stringify } from '../../utils/stringify';
|
||||
import flattenReference from '../../utils/flattenReference';
|
||||
import isVoidElementName from '../../utils/isVoidElementName';
|
||||
import validCalleeObjects from '../../utils/validCalleeObjects';
|
||||
import reservedNames from '../../utils/reservedNames';
|
||||
import Node from './shared/Node';
|
||||
import Block from '../dom/Block';
|
||||
import Binding from './Binding';
|
||||
import EventHandler from './EventHandler';
|
||||
import flattenReference from '../../utils/flattenReference';
|
||||
import fuzzymatch from '../validate/utils/fuzzymatch';
|
||||
import list from '../../utils/list';
|
||||
|
||||
const associatedEvents = {
|
||||
innerWidth: 'resize',
|
||||
innerHeight: 'resize',
|
||||
outerWidth: 'resize',
|
||||
outerHeight: 'resize',
|
||||
|
||||
scrollX: 'scroll',
|
||||
scrollY: 'scroll',
|
||||
};
|
||||
|
||||
const properties = {
|
||||
scrollX: 'pageXOffset',
|
||||
scrollY: 'pageYOffset'
|
||||
};
|
||||
|
||||
const readonly = new Set([
|
||||
const validBindings = [
|
||||
'innerWidth',
|
||||
'innerHeight',
|
||||
'outerWidth',
|
||||
'outerHeight',
|
||||
'online',
|
||||
]);
|
||||
'scrollX',
|
||||
'scrollY',
|
||||
'online'
|
||||
];
|
||||
|
||||
export default class Window extends Node {
|
||||
type: 'Window';
|
||||
handlers: EventHandler[];
|
||||
bindings: Binding[];
|
||||
|
||||
constructor(compiler, parent, scope, info) {
|
||||
super(compiler, parent, scope, info);
|
||||
constructor(component, parent, scope, info) {
|
||||
super(component, parent, scope, info);
|
||||
|
||||
this.handlers = [];
|
||||
this.bindings = [];
|
||||
|
||||
info.attributes.forEach(node => {
|
||||
if (node.type === 'EventHandler') {
|
||||
this.handlers.push(new EventHandler(compiler, this, scope, node));
|
||||
} else if (node.type === 'Binding') {
|
||||
this.bindings.push(new Binding(compiler, this, scope, node));
|
||||
this.handlers.push(new EventHandler(component, this, scope, node));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
build(
|
||||
block: Block,
|
||||
parentNode: string,
|
||||
parentNodes: string
|
||||
) {
|
||||
const { compiler } = this;
|
||||
|
||||
const events = {};
|
||||
const bindings: Record<string, string> = {};
|
||||
|
||||
this.handlers.forEach(handler => {
|
||||
// TODO verify that it's a valid callee (i.e. built-in or declared method)
|
||||
compiler.addSourcemapLocations(handler.expression);
|
||||
|
||||
const isCustomEvent = compiler.events.has(handler.name);
|
||||
|
||||
let usesState = handler.dependencies.size > 0;
|
||||
|
||||
handler.render(compiler, block, false); // TODO hoist?
|
||||
|
||||
const handlerName = block.getUniqueName(`onwindow${handler.name}`);
|
||||
const handlerBody = deindent`
|
||||
${usesState && `var ctx = #component.get();`}
|
||||
${handler.snippet};
|
||||
`;
|
||||
else if (node.type === 'Binding') {
|
||||
if (node.value.type !== 'Identifier') {
|
||||
const { parts } = flattenReference(node.value);
|
||||
|
||||
if (isCustomEvent) {
|
||||
// TODO dry this out
|
||||
block.addVariable(handlerName);
|
||||
|
||||
block.builders.hydrate.addBlock(deindent`
|
||||
${handlerName} = %events-${handler.name}.call(#component, window, function(event) {
|
||||
${handlerBody}
|
||||
component.error(node.value, {
|
||||
code: `invalid-binding`,
|
||||
message: `Bindings on <svelte:window> must be to top-level properties, e.g. '${parts[parts.length - 1]}' rather than '${parts.join('.')}'`
|
||||
});
|
||||
`);
|
||||
|
||||
block.builders.destroy.addLine(deindent`
|
||||
${handlerName}.destroy();
|
||||
`);
|
||||
} else {
|
||||
block.builders.init.addBlock(deindent`
|
||||
function ${handlerName}(event) {
|
||||
${handlerBody}
|
||||
}
|
||||
window.addEventListener("${handler.name}", ${handlerName});
|
||||
`);
|
||||
|
||||
block.builders.destroy.addBlock(deindent`
|
||||
window.removeEventListener("${handler.name}", ${handlerName});
|
||||
`);
|
||||
}
|
||||
});
|
||||
|
||||
this.bindings.forEach(binding => {
|
||||
// in dev mode, throw if read-only values are written to
|
||||
if (readonly.has(binding.name)) {
|
||||
compiler.target.readonly.add(binding.value.node.name);
|
||||
}
|
||||
|
||||
bindings[binding.name] = binding.value.node.name;
|
||||
|
||||
// bind:online is a special case, we need to listen for two separate events
|
||||
if (binding.name === 'online') return;
|
||||
|
||||
const associatedEvent = associatedEvents[binding.name];
|
||||
const property = properties[binding.name] || binding.name;
|
||||
|
||||
if (!events[associatedEvent]) events[associatedEvent] = [];
|
||||
events[associatedEvent].push({
|
||||
name: binding.value.node.name,
|
||||
value: property
|
||||
});
|
||||
});
|
||||
|
||||
const lock = block.getUniqueName(`window_updating`);
|
||||
const clear = block.getUniqueName(`clear_window_updating`);
|
||||
const timeout = block.getUniqueName(`window_updating_timeout`);
|
||||
|
||||
Object.keys(events).forEach(event => {
|
||||
const handlerName = block.getUniqueName(`onwindow${event}`);
|
||||
const props = events[event];
|
||||
|
||||
if (event === 'scroll') {
|
||||
// TODO other bidirectional bindings...
|
||||
block.addVariable(lock, 'false');
|
||||
block.addVariable(clear, `function() { ${lock} = false; }`);
|
||||
block.addVariable(timeout);
|
||||
|
||||
const condition = [
|
||||
bindings.scrollX && `"${bindings.scrollX}" in this._state`,
|
||||
bindings.scrollY && `"${bindings.scrollY}" in this._state`
|
||||
].filter(Boolean).join(' || ');
|
||||
|
||||
const x = bindings.scrollX && `this._state.${bindings.scrollX}`;
|
||||
const y = bindings.scrollY && `this._state.${bindings.scrollY}`;
|
||||
|
||||
compiler.target.metaBindings.addBlock(deindent`
|
||||
if (${condition}) {
|
||||
window.scrollTo(${x || 'window.pageXOffset'}, ${y || 'window.pageYOffset'});
|
||||
}
|
||||
|
||||
${x && `${x} = window.pageXOffset;`}
|
||||
|
||||
${y && `${y} = window.pageYOffset;`}
|
||||
`);
|
||||
} else {
|
||||
props.forEach(prop => {
|
||||
compiler.target.metaBindings.addLine(
|
||||
`this._state.${prop.name} = window.${prop.value};`
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
const handlerBody = deindent`
|
||||
${event === 'scroll' && deindent`
|
||||
if (${lock}) return;
|
||||
${lock} = true;
|
||||
`}
|
||||
${compiler.options.dev && `component._updatingReadonlyProperty = true;`}
|
||||
|
||||
#component.set({
|
||||
${props.map(prop => `${prop.name}: this.${prop.value}`)}
|
||||
});
|
||||
|
||||
${compiler.options.dev && `component._updatingReadonlyProperty = false;`}
|
||||
${event === 'scroll' && `${lock} = false;`}
|
||||
`;
|
||||
|
||||
block.builders.init.addBlock(deindent`
|
||||
function ${handlerName}(event) {
|
||||
${handlerBody}
|
||||
}
|
||||
window.addEventListener("${event}", ${handlerName});
|
||||
`);
|
||||
|
||||
block.builders.destroy.addBlock(deindent`
|
||||
window.removeEventListener("${event}", ${handlerName});
|
||||
`);
|
||||
});
|
||||
if (!~validBindings.indexOf(node.name)) {
|
||||
const match = node.name === 'width'
|
||||
? 'innerWidth'
|
||||
: node.name === 'height'
|
||||
? 'innerHeight'
|
||||
: fuzzymatch(node.name, validBindings);
|
||||
|
||||
const message = `'${node.name}' is not a valid binding on <svelte:window>`;
|
||||
|
||||
// special case... might need to abstract this out if we add more special cases
|
||||
if (bindings.scrollX || bindings.scrollY) {
|
||||
block.builders.init.addBlock(deindent`
|
||||
#component.on("state", ({ changed, current }) => {
|
||||
if (${
|
||||
[bindings.scrollX, bindings.scrollY].map(
|
||||
binding => binding && `changed["${binding}"]`
|
||||
).filter(Boolean).join(' || ')
|
||||
}) {
|
||||
${lock} = true;
|
||||
clearTimeout(${timeout});
|
||||
window.scrollTo(${
|
||||
bindings.scrollX ? `current["${bindings.scrollX}"]` : `window.pageXOffset`
|
||||
}, ${
|
||||
bindings.scrollY ? `current["${bindings.scrollY}"]` : `window.pageYOffset`
|
||||
if (match) {
|
||||
component.error(node, {
|
||||
code: `invalid-binding`,
|
||||
message: `${message} (did you mean '${match}'?)`
|
||||
});
|
||||
} else {
|
||||
component.error(node, {
|
||||
code: `invalid-binding`,
|
||||
message: `${message} — valid bindings are ${list(validBindings)}`
|
||||
});
|
||||
${timeout} = setTimeout(${clear}, 100);
|
||||
}
|
||||
});
|
||||
`);
|
||||
}
|
||||
|
||||
// another special case. (I'm starting to think these are all special cases.)
|
||||
if (bindings.online) {
|
||||
const handlerName = block.getUniqueName(`onlinestatuschanged`);
|
||||
block.builders.init.addBlock(deindent`
|
||||
function ${handlerName}(event) {
|
||||
#component.set({ ${bindings.online}: navigator.onLine });
|
||||
}
|
||||
window.addEventListener("online", ${handlerName});
|
||||
window.addEventListener("offline", ${handlerName});
|
||||
`);
|
||||
|
||||
// add initial value
|
||||
compiler.target.metaBindings.push(
|
||||
`this._state.${bindings.online} = navigator.onLine;`
|
||||
);
|
||||
|
||||
block.builders.destroy.addBlock(deindent`
|
||||
window.removeEventListener("online", ${handlerName});
|
||||
window.removeEventListener("offline", ${handlerName});
|
||||
`);
|
||||
}
|
||||
}
|
||||
this.bindings.push(new Binding(component, this, scope, node));
|
||||
}
|
||||
|
||||
ssr() {
|
||||
// noop
|
||||
else {
|
||||
// TODO there shouldn't be anything else here...
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -1,56 +1,17 @@
|
||||
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);
|
||||
constructor(component, parent, scope, info) {
|
||||
super(component, parent, scope, info);
|
||||
this.expression = new Expression(component, 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,75 @@
|
||||
import Block from './Block';
|
||||
import { CompileOptions } from '../../interfaces';
|
||||
import Component from '../Component';
|
||||
import FragmentWrapper from './wrappers/Fragment';
|
||||
|
||||
export default class Renderer {
|
||||
component: Component; // TODO Maybe Renderer shouldn't know about Component?
|
||||
options: CompileOptions;
|
||||
|
||||
blocks: (Block | string)[];
|
||||
readonly: Set<string>;
|
||||
slots: Set<string>;
|
||||
metaBindings: string[];
|
||||
bindingGroups: string[];
|
||||
|
||||
block: Block;
|
||||
fragment: FragmentWrapper;
|
||||
|
||||
usedNames: Set<string>;
|
||||
fileVar: string;
|
||||
|
||||
hasIntroTransitions: boolean;
|
||||
hasOutroTransitions: boolean;
|
||||
hasComplexBindings: boolean;
|
||||
|
||||
constructor(component: Component, options: CompileOptions) {
|
||||
this.component = component;
|
||||
this.options = options;
|
||||
this.locate = component.locate; // TODO messy
|
||||
|
||||
this.readonly = new Set();
|
||||
this.slots = new Set();
|
||||
|
||||
this.usedNames = new Set();
|
||||
this.fileVar = options.dev && this.component.getUniqueName('file');
|
||||
|
||||
// initial values for e.g. window.innerWidth, if there's a <svelte:window> meta tag
|
||||
this.metaBindings = [];
|
||||
|
||||
this.bindingGroups = [];
|
||||
|
||||
// main block
|
||||
this.block = new Block({
|
||||
renderer: this,
|
||||
name: '@create_main_fragment',
|
||||
key: null,
|
||||
|
||||
bindings: new Map(),
|
||||
|
||||
dependencies: new Set(),
|
||||
});
|
||||
|
||||
this.block.hasUpdateMethod = true;
|
||||
this.blocks = [];
|
||||
|
||||
this.fragment = new FragmentWrapper(
|
||||
this,
|
||||
this.block,
|
||||
component.fragment.children,
|
||||
null,
|
||||
true,
|
||||
null
|
||||
);
|
||||
|
||||
this.blocks.push(this.block);
|
||||
|
||||
this.blocks.forEach(block => {
|
||||
if (typeof block !== 'string') {
|
||||
block.assignVariableNames();
|
||||
}
|
||||
});
|
||||
|
||||
this.fragment.render(this.block, null, 'nodes');
|
||||
}
|
||||
}
|
@ -0,0 +1,230 @@
|
||||
import Wrapper from './shared/Wrapper';
|
||||
import Renderer from '../Renderer';
|
||||
import Block from '../Block';
|
||||
import AwaitBlock from '../../nodes/AwaitBlock';
|
||||
import createDebuggingComment from '../../../utils/createDebuggingComment';
|
||||
import deindent from '../../../utils/deindent';
|
||||
import FragmentWrapper from './Fragment';
|
||||
import PendingBlock from '../../nodes/PendingBlock';
|
||||
import ThenBlock from '../../nodes/ThenBlock';
|
||||
import CatchBlock from '../../nodes/CatchBlock';
|
||||
|
||||
class AwaitBlockBranch extends Wrapper {
|
||||
node: PendingBlock | ThenBlock | CatchBlock;
|
||||
block: Block;
|
||||
fragment: FragmentWrapper;
|
||||
isDynamic: boolean;
|
||||
|
||||
var = null;
|
||||
|
||||
constructor(
|
||||
status: string,
|
||||
renderer: Renderer,
|
||||
block: Block,
|
||||
parent: Wrapper,
|
||||
node: AwaitBlock,
|
||||
stripWhitespace: boolean,
|
||||
nextSibling: Wrapper
|
||||
) {
|
||||
super(renderer, block, parent, node);
|
||||
|
||||
this.block = block.child({
|
||||
comment: createDebuggingComment(node, this.renderer.component),
|
||||
name: this.renderer.component.getUniqueName(`create_${status}_block`)
|
||||
});
|
||||
|
||||
this.fragment = new FragmentWrapper(
|
||||
renderer,
|
||||
this.block,
|
||||
this.node.children,
|
||||
parent,
|
||||
stripWhitespace,
|
||||
nextSibling
|
||||
);
|
||||
|
||||
this.isDynamic = this.block.dependencies.size > 0;
|
||||
}
|
||||
}
|
||||
|
||||
export default class AwaitBlockWrapper extends Wrapper {
|
||||
node: AwaitBlock;
|
||||
|
||||
pending: AwaitBlockBranch;
|
||||
then: AwaitBlockBranch;
|
||||
catch: AwaitBlockBranch;
|
||||
|
||||
var = 'await_block';
|
||||
|
||||
constructor(
|
||||
renderer: Renderer,
|
||||
block: Block,
|
||||
parent: Wrapper,
|
||||
node: AwaitBlock,
|
||||
stripWhitespace: boolean,
|
||||
nextSibling: Wrapper
|
||||
) {
|
||||
super(renderer, block, parent, node);
|
||||
|
||||
this.cannotUseInnerHTML();
|
||||
|
||||
block.addDependencies(this.node.expression.dependencies);
|
||||
|
||||
let isDynamic = false;
|
||||
let hasIntros = false;
|
||||
let hasOutros = false;
|
||||
|
||||
['pending', 'then', 'catch'].forEach(status => {
|
||||
const child = this.node[status];
|
||||
|
||||
const branch = new AwaitBlockBranch(
|
||||
status,
|
||||
renderer,
|
||||
block,
|
||||
parent,
|
||||
child,
|
||||
stripWhitespace,
|
||||
nextSibling
|
||||
);
|
||||
|
||||
renderer.blocks.push(branch.block);
|
||||
|
||||
if (branch.isDynamic) {
|
||||
isDynamic = true;
|
||||
// TODO should blocks update their own parents?
|
||||
block.addDependencies(branch.block.dependencies);
|
||||
}
|
||||
|
||||
if (branch.block.hasIntros) hasIntros = true;
|
||||
if (branch.block.hasOutros) hasOutros = true;
|
||||
|
||||
this[status] = branch;
|
||||
});
|
||||
|
||||
this.pending.block.hasUpdateMethod = isDynamic;
|
||||
this.then.block.hasUpdateMethod = isDynamic;
|
||||
this.catch.block.hasUpdateMethod = isDynamic;
|
||||
|
||||
this.pending.block.hasIntroMethod = hasIntros;
|
||||
this.then.block.hasIntroMethod = hasIntros;
|
||||
this.catch.block.hasIntroMethod = hasIntros;
|
||||
|
||||
this.pending.block.hasOutroMethod = hasOutros;
|
||||
this.then.block.hasOutroMethod = hasOutros;
|
||||
this.catch.block.hasOutroMethod = hasOutros;
|
||||
|
||||
if (hasOutros && this.renderer.options.nestedTransitions) {
|
||||
block.addOutro();
|
||||
}
|
||||
}
|
||||
|
||||
render(
|
||||
block: Block,
|
||||
parentNode: string,
|
||||
parentNodes: string
|
||||
) {
|
||||
const anchor = this.getOrCreateAnchor(block, parentNode, parentNodes);
|
||||
const updateMountNode = this.getUpdateMountNode(anchor);
|
||||
|
||||
const { snippet } = this.node.expression;
|
||||
|
||||
const info = block.getUniqueName(`info`);
|
||||
const promise = block.getUniqueName(`promise`);
|
||||
|
||||
block.addVariable(promise);
|
||||
|
||||
block.maintainContext = true;
|
||||
|
||||
const infoProps = [
|
||||
block.alias('component') === 'component' ? 'component' : `component: #component`,
|
||||
'ctx',
|
||||
'current: null',
|
||||
this.pending.block.name && `pending: ${this.pending.block.name}`,
|
||||
this.then.block.name && `then: ${this.then.block.name}`,
|
||||
this.catch.block.name && `catch: ${this.catch.block.name}`,
|
||||
this.then.block.name && `value: '${this.node.value}'`,
|
||||
this.catch.block.name && `error: '${this.node.error}'`,
|
||||
this.pending.block.hasOutroMethod && `blocks: Array(3)`
|
||||
].filter(Boolean);
|
||||
|
||||
block.builders.init.addBlock(deindent`
|
||||
let ${info} = {
|
||||
${infoProps.join(',\n')}
|
||||
};
|
||||
`);
|
||||
|
||||
block.builders.init.addBlock(deindent`
|
||||
@handlePromise(${promise} = ${snippet}, ${info});
|
||||
`);
|
||||
|
||||
block.builders.create.addBlock(deindent`
|
||||
${info}.block.c();
|
||||
`);
|
||||
|
||||
if (parentNodes) {
|
||||
block.builders.claim.addBlock(deindent`
|
||||
${info}.block.l(${parentNodes});
|
||||
`);
|
||||
}
|
||||
|
||||
const initialMountNode = parentNode || '#target';
|
||||
const anchorNode = parentNode ? 'null' : 'anchor';
|
||||
|
||||
const hasTransitions = this.pending.block.hasIntroMethod || this.pending.block.hasOutroMethod;
|
||||
|
||||
block.builders.mount.addBlock(deindent`
|
||||
${info}.block.${hasTransitions ? 'i' : 'm'}(${initialMountNode}, ${info}.anchor = ${anchorNode});
|
||||
${info}.mount = () => ${updateMountNode};
|
||||
`);
|
||||
|
||||
const conditions = [];
|
||||
if (this.node.expression.dependencies.size > 0) {
|
||||
conditions.push(
|
||||
`(${[...this.node.expression.dependencies].map(dep => `'${dep}' in changed`).join(' || ')})`
|
||||
);
|
||||
}
|
||||
|
||||
conditions.push(
|
||||
`${promise} !== (${promise} = ${snippet})`,
|
||||
`@handlePromise(${promise}, ${info})`
|
||||
);
|
||||
|
||||
block.builders.update.addLine(
|
||||
`${info}.ctx = ctx;`
|
||||
);
|
||||
|
||||
if (this.pending.block.hasUpdateMethod) {
|
||||
block.builders.update.addBlock(deindent`
|
||||
if (${conditions.join(' && ')}) {
|
||||
// nothing
|
||||
} else {
|
||||
${info}.block.p(changed, @assign(@assign({}, ctx), ${info}.resolved));
|
||||
}
|
||||
`);
|
||||
} else {
|
||||
block.builders.update.addBlock(deindent`
|
||||
${conditions.join(' && ')}
|
||||
`);
|
||||
}
|
||||
|
||||
if (this.pending.block.hasOutroMethod && this.renderer.options.nestedTransitions) {
|
||||
const countdown = block.getUniqueName('countdown');
|
||||
block.builders.outro.addBlock(deindent`
|
||||
const ${countdown} = @callAfter(#outrocallback, 3);
|
||||
for (let #i = 0; #i < 3; #i += 1) {
|
||||
const block = ${info}.blocks[#i];
|
||||
if (block) block.o(${countdown});
|
||||
else ${countdown}();
|
||||
}
|
||||
`);
|
||||
}
|
||||
|
||||
block.builders.destroy.addBlock(deindent`
|
||||
${info}.block.d(${parentNode ? '' : 'detach'});
|
||||
${info} = null;
|
||||
`);
|
||||
|
||||
[this.pending, this.then, this.catch].forEach(branch => {
|
||||
branch.fragment.render(branch.block, null, 'nodes');
|
||||
});
|
||||
}
|
||||
}
|
@ -0,0 +1,72 @@
|
||||
import Renderer from '../Renderer';
|
||||
import Wrapper from './shared/Wrapper';
|
||||
import Block from '../Block';
|
||||
import DebugTag from '../../nodes/DebugTag';
|
||||
import addToSet from '../../../utils/addToSet';
|
||||
import deindent from '../../../utils/deindent';
|
||||
|
||||
export default class DebugTagWrapper extends Wrapper {
|
||||
node: DebugTag;
|
||||
|
||||
constructor(
|
||||
renderer: Renderer,
|
||||
block: Block,
|
||||
parent: Wrapper,
|
||||
node: DebugTag,
|
||||
stripWhitespace: boolean,
|
||||
nextSibling: Wrapper
|
||||
) {
|
||||
super(renderer, block, parent, node);
|
||||
}
|
||||
|
||||
render(block: Block, parentNode: string, parentNodes: string) {
|
||||
const { renderer } = this;
|
||||
const { component } = renderer;
|
||||
|
||||
if (!renderer.options.dev) return;
|
||||
|
||||
const { code } = component;
|
||||
|
||||
if (this.node.expressions.length === 0) {
|
||||
// Debug all
|
||||
code.overwrite(this.node.start + 1, this.node.start + 7, 'debugger', {
|
||||
storeName: true
|
||||
});
|
||||
const statement = `[✂${this.node.start + 1}-${this.node.start + 7}✂];`;
|
||||
|
||||
block.builders.create.addLine(statement);
|
||||
block.builders.update.addLine(statement);
|
||||
} else {
|
||||
const { code } = component;
|
||||
code.overwrite(this.node.start + 1, this.node.start + 7, 'log', {
|
||||
storeName: true
|
||||
});
|
||||
const log = `[✂${this.node.start + 1}-${this.node.start + 7}✂]`;
|
||||
|
||||
const dependencies = new Set();
|
||||
this.node.expressions.forEach(expression => {
|
||||
addToSet(dependencies, expression.dependencies);
|
||||
});
|
||||
|
||||
const condition = [...dependencies].map(d => `changed.${d}`).join(' || ');
|
||||
|
||||
const identifiers = this.node.expressions.map(e => e.node.name).join(', ');
|
||||
|
||||
block.builders.update.addBlock(deindent`
|
||||
if (${condition}) {
|
||||
const { ${identifiers} } = ctx;
|
||||
console.${log}({ ${identifiers} });
|
||||
debugger;
|
||||
}
|
||||
`);
|
||||
|
||||
block.builders.create.addBlock(deindent`
|
||||
{
|
||||
const { ${identifiers} } = ctx;
|
||||
console.${log}({ ${identifiers} });
|
||||
debugger;
|
||||
}
|
||||
`);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,503 @@
|
||||
import Renderer from '../Renderer';
|
||||
import Block from '../Block';
|
||||
import Node from '../../nodes/shared/Node';
|
||||
import Wrapper from './shared/Wrapper';
|
||||
import createDebuggingComment from '../../../utils/createDebuggingComment';
|
||||
import EachBlock from '../../nodes/EachBlock';
|
||||
import FragmentWrapper from './Fragment';
|
||||
import deindent from '../../../utils/deindent';
|
||||
import ElseBlock from '../../nodes/ElseBlock';
|
||||
|
||||
class ElseBlockWrapper extends Wrapper {
|
||||
node: ElseBlock;
|
||||
block: Block;
|
||||
fragment: FragmentWrapper;
|
||||
isDynamic: boolean;
|
||||
|
||||
var = null;
|
||||
|
||||
constructor(
|
||||
renderer: Renderer,
|
||||
block: Block,
|
||||
parent: Wrapper,
|
||||
node: ElseBlock,
|
||||
stripWhitespace: boolean,
|
||||
nextSibling: Wrapper
|
||||
) {
|
||||
super(renderer, block, parent, node);
|
||||
|
||||
this.block = block.child({
|
||||
comment: createDebuggingComment(node, this.renderer.component),
|
||||
name: this.renderer.component.getUniqueName(`create_else_block`)
|
||||
});
|
||||
|
||||
this.fragment = new FragmentWrapper(
|
||||
renderer,
|
||||
this.block,
|
||||
this.node.children,
|
||||
parent,
|
||||
stripWhitespace,
|
||||
nextSibling
|
||||
);
|
||||
|
||||
this.isDynamic = this.block.dependencies.size > 0;
|
||||
if (this.isDynamic) {
|
||||
// TODO this can't be right
|
||||
this.block.hasUpdateMethod = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default class EachBlockWrapper extends Wrapper {
|
||||
block: Block;
|
||||
node: EachBlock;
|
||||
fragment: FragmentWrapper;
|
||||
else?: ElseBlockWrapper;
|
||||
var: string;
|
||||
vars: {
|
||||
anchor: string;
|
||||
create_each_block: string;
|
||||
each_block_value: string;
|
||||
get_each_context: string;
|
||||
iterations: string;
|
||||
length: string;
|
||||
mountOrIntro: string;
|
||||
}
|
||||
|
||||
constructor(
|
||||
renderer: Renderer,
|
||||
block: Block,
|
||||
parent: Wrapper,
|
||||
node: EachBlock,
|
||||
stripWhitespace: boolean,
|
||||
nextSibling: Wrapper
|
||||
) {
|
||||
super(renderer, block, parent, node);
|
||||
this.cannotUseInnerHTML();
|
||||
|
||||
this.var = 'each';
|
||||
|
||||
const { dependencies } = node.expression;
|
||||
block.addDependencies(dependencies);
|
||||
|
||||
this.block = block.child({
|
||||
comment: createDebuggingComment(this.node, this.renderer.component),
|
||||
name: renderer.component.getUniqueName('create_each_block'),
|
||||
key: <string>node.key, // TODO...
|
||||
|
||||
bindings: new Map(block.bindings)
|
||||
});
|
||||
|
||||
// TODO this seems messy
|
||||
this.block.hasAnimation = this.node.hasAnimation;
|
||||
|
||||
this.indexName = this.node.index || renderer.component.getUniqueName(`${this.node.context}_index`);
|
||||
|
||||
node.contexts.forEach(prop => {
|
||||
// TODO this doesn't feel great
|
||||
this.block.bindings.set(prop.key.name, () => `ctx.${this.vars.each_block_value}[ctx.${this.indexName}]${prop.tail}`);
|
||||
});
|
||||
|
||||
if (this.node.index) {
|
||||
this.block.getUniqueName(this.node.index); // this prevents name collisions (#1254)
|
||||
}
|
||||
|
||||
renderer.blocks.push(this.block);
|
||||
|
||||
this.fragment = new FragmentWrapper(renderer, this.block, node.children, this, stripWhitespace, nextSibling);
|
||||
|
||||
if (this.node.else) {
|
||||
this.else = new ElseBlockWrapper(
|
||||
renderer,
|
||||
block,
|
||||
this,
|
||||
this.node.else,
|
||||
stripWhitespace,
|
||||
nextSibling
|
||||
);
|
||||
|
||||
renderer.blocks.push(this.else.block);
|
||||
|
||||
if (this.else.isDynamic) {
|
||||
this.block.addDependencies(this.else.block.dependencies);
|
||||
}
|
||||
}
|
||||
|
||||
block.addDependencies(this.block.dependencies);
|
||||
this.block.hasUpdateMethod = this.block.dependencies.size > 0; // TODO should this logic be in Block?
|
||||
|
||||
if (this.block.hasOutros || (this.else && this.else.block.hasOutros)) {
|
||||
block.addOutro();
|
||||
}
|
||||
}
|
||||
|
||||
render(block: Block, parentNode: string, parentNodes: string) {
|
||||
if (this.fragment.nodes.length === 0) return;
|
||||
|
||||
const { renderer } = this;
|
||||
const { component } = renderer;
|
||||
|
||||
// hack the sourcemap, so that if data is missing the bug
|
||||
// is easy to find
|
||||
let c = this.node.start + 2;
|
||||
while (component.source[c] !== 'e') c += 1;
|
||||
component.code.overwrite(c, c + 4, 'length');
|
||||
const length = `[✂${c}-${c+4}✂]`;
|
||||
|
||||
const needsAnchor = this.next
|
||||
? !this.next.isDomNode() :
|
||||
!parentNode || !this.parent.isDomNode();
|
||||
|
||||
this.vars = {
|
||||
anchor: needsAnchor
|
||||
? block.getUniqueName(`${this.var}_anchor`)
|
||||
: (this.next && this.next.var) || 'null',
|
||||
create_each_block: this.block.name,
|
||||
each_block_value: renderer.component.getUniqueName(`${this.var}_value`),
|
||||
get_each_context: renderer.component.getUniqueName(`get_${this.var}_context`),
|
||||
iterations: block.getUniqueName(`${this.var}_blocks`),
|
||||
length: `[✂${c}-${c+4}✂]`,
|
||||
mountOrIntro: (this.block.hasIntroMethod || this.block.hasOutroMethod)
|
||||
? 'i'
|
||||
: 'm'
|
||||
};
|
||||
|
||||
this.contextProps = this.node.contexts.map(prop => `child_ctx.${prop.key.name} = list[i]${prop.tail};`);
|
||||
|
||||
// TODO only add these if necessary
|
||||
this.contextProps.push(
|
||||
`child_ctx.${this.vars.each_block_value} = list;`,
|
||||
`child_ctx.${this.indexName} = i;`
|
||||
);
|
||||
|
||||
const { snippet } = this.node.expression;
|
||||
|
||||
block.builders.init.addLine(`var ${this.vars.each_block_value} = ${snippet};`);
|
||||
|
||||
renderer.blocks.push(deindent`
|
||||
function ${this.vars.get_each_context}(ctx, list, i) {
|
||||
const child_ctx = Object.create(ctx);
|
||||
${this.contextProps}
|
||||
return child_ctx;
|
||||
}
|
||||
`);
|
||||
|
||||
if (this.node.key) {
|
||||
this.renderKeyed(block, parentNode, parentNodes, snippet);
|
||||
} else {
|
||||
this.renderUnkeyed(block, parentNode, parentNodes, snippet);
|
||||
}
|
||||
|
||||
if (needsAnchor) {
|
||||
block.addElement(
|
||||
this.vars.anchor,
|
||||
`@createComment()`,
|
||||
parentNodes && `@createComment()`,
|
||||
parentNode
|
||||
);
|
||||
}
|
||||
|
||||
if (this.else) {
|
||||
const each_block_else = component.getUniqueName(`${this.var}_else`);
|
||||
const mountOrIntro = (this.else.block.hasIntroMethod || this.else.block.hasOutroMethod) ? 'i' : 'm';
|
||||
|
||||
block.builders.init.addLine(`var ${each_block_else} = null;`);
|
||||
|
||||
// TODO neaten this up... will end up with an empty line in the block
|
||||
block.builders.init.addBlock(deindent`
|
||||
if (!${this.vars.each_block_value}.${length}) {
|
||||
${each_block_else} = ${this.else.block.name}(#component, ctx);
|
||||
${each_block_else}.c();
|
||||
}
|
||||
`);
|
||||
|
||||
block.builders.mount.addBlock(deindent`
|
||||
if (${each_block_else}) {
|
||||
${each_block_else}.${mountOrIntro}(${parentNode || '#target'}, null);
|
||||
}
|
||||
`);
|
||||
|
||||
const initialMountNode = parentNode || `${this.vars.anchor}.parentNode`;
|
||||
|
||||
if (this.else.block.hasUpdateMethod) {
|
||||
block.builders.update.addBlock(deindent`
|
||||
if (!${this.vars.each_block_value}.${length} && ${each_block_else}) {
|
||||
${each_block_else}.p(changed, ctx);
|
||||
} else if (!${this.vars.each_block_value}.${length}) {
|
||||
${each_block_else} = ${this.else.block.name}(#component, ctx);
|
||||
${each_block_else}.c();
|
||||
${each_block_else}.${mountOrIntro}(${initialMountNode}, ${this.vars.anchor});
|
||||
} else if (${each_block_else}) {
|
||||
${each_block_else}.d(1);
|
||||
${each_block_else} = null;
|
||||
}
|
||||
`);
|
||||
} else {
|
||||
block.builders.update.addBlock(deindent`
|
||||
if (${this.vars.each_block_value}.${length}) {
|
||||
if (${each_block_else}) {
|
||||
${each_block_else}.d(1);
|
||||
${each_block_else} = null;
|
||||
}
|
||||
} else if (!${each_block_else}) {
|
||||
${each_block_else} = ${this.else.block.name}(#component, ctx);
|
||||
${each_block_else}.c();
|
||||
${each_block_else}.${mountOrIntro}(${initialMountNode}, ${this.vars.anchor});
|
||||
}
|
||||
`);
|
||||
}
|
||||
|
||||
block.builders.destroy.addBlock(deindent`
|
||||
if (${each_block_else}) ${each_block_else}.d(${parentNode ? '' : 'detach'});
|
||||
`);
|
||||
}
|
||||
|
||||
this.fragment.render(this.block, null, 'nodes');
|
||||
|
||||
if (this.else) {
|
||||
this.else.fragment.render(this.else.block, null, 'nodes');
|
||||
}
|
||||
}
|
||||
|
||||
renderKeyed(
|
||||
block: Block,
|
||||
parentNode: string,
|
||||
parentNodes: string,
|
||||
snippet: string
|
||||
) {
|
||||
const {
|
||||
create_each_block,
|
||||
length,
|
||||
anchor,
|
||||
mountOrIntro,
|
||||
} = this.vars;
|
||||
|
||||
const get_key = block.getUniqueName('get_key');
|
||||
const blocks = block.getUniqueName(`${this.var}_blocks`);
|
||||
const lookup = block.getUniqueName(`${this.var}_lookup`);
|
||||
|
||||
block.addVariable(blocks, '[]');
|
||||
block.addVariable(lookup, `@blankObject()`);
|
||||
|
||||
if (this.fragment.nodes[0].isDomNode()) {
|
||||
this.block.first = this.fragment.nodes[0].var;
|
||||
} else {
|
||||
this.block.first = this.block.getUniqueName('first');
|
||||
this.block.addElement(
|
||||
this.block.first,
|
||||
`@createComment()`,
|
||||
parentNodes && `@createComment()`,
|
||||
null
|
||||
);
|
||||
}
|
||||
|
||||
block.builders.init.addBlock(deindent`
|
||||
const ${get_key} = ctx => ${this.node.key.snippet};
|
||||
|
||||
for (var #i = 0; #i < ${this.vars.each_block_value}.${length}; #i += 1) {
|
||||
let child_ctx = ${this.vars.get_each_context}(ctx, ${this.vars.each_block_value}, #i);
|
||||
let key = ${get_key}(child_ctx);
|
||||
${blocks}[#i] = ${lookup}[key] = ${create_each_block}(#component, key, child_ctx);
|
||||
}
|
||||
`);
|
||||
|
||||
const initialMountNode = parentNode || '#target';
|
||||
const updateMountNode = this.getUpdateMountNode(anchor);
|
||||
const anchorNode = parentNode ? 'null' : 'anchor';
|
||||
|
||||
block.builders.create.addBlock(deindent`
|
||||
for (#i = 0; #i < ${blocks}.length; #i += 1) ${blocks}[#i].c();
|
||||
`);
|
||||
|
||||
if (parentNodes) {
|
||||
block.builders.claim.addBlock(deindent`
|
||||
for (#i = 0; #i < ${blocks}.length; #i += 1) ${blocks}[#i].l(${parentNodes});
|
||||
`);
|
||||
}
|
||||
|
||||
block.builders.mount.addBlock(deindent`
|
||||
for (#i = 0; #i < ${blocks}.length; #i += 1) ${blocks}[#i].${mountOrIntro}(${initialMountNode}, ${anchorNode});
|
||||
`);
|
||||
|
||||
const dynamic = this.block.hasUpdateMethod;
|
||||
|
||||
const rects = block.getUniqueName('rects');
|
||||
const destroy = this.node.hasAnimation
|
||||
? `@fixAndOutroAndDestroyBlock`
|
||||
: this.block.hasOutros
|
||||
? `@outroAndDestroyBlock`
|
||||
: `@destroyBlock`;
|
||||
|
||||
block.builders.update.addBlock(deindent`
|
||||
const ${this.vars.each_block_value} = ${snippet};
|
||||
|
||||
${this.block.hasOutros && `@groupOutros();`}
|
||||
${this.node.hasAnimation && `for (let #i = 0; #i < ${blocks}.length; #i += 1) ${blocks}[#i].r();`}
|
||||
${blocks} = @updateKeyedEach(${blocks}, #component, changed, ${get_key}, ${dynamic ? '1' : '0'}, ctx, ${this.vars.each_block_value}, ${lookup}, ${updateMountNode}, ${destroy}, ${create_each_block}, "${mountOrIntro}", ${anchor}, ${this.vars.get_each_context});
|
||||
${this.node.hasAnimation && `for (let #i = 0; #i < ${blocks}.length; #i += 1) ${blocks}[#i].a();`}
|
||||
`);
|
||||
|
||||
if (this.block.hasOutros && this.renderer.component.options.nestedTransitions) {
|
||||
const countdown = block.getUniqueName('countdown');
|
||||
block.builders.outro.addBlock(deindent`
|
||||
const ${countdown} = @callAfter(#outrocallback, ${blocks}.length);
|
||||
for (#i = 0; #i < ${blocks}.length; #i += 1) ${blocks}[#i].o(${countdown});
|
||||
`);
|
||||
}
|
||||
|
||||
block.builders.destroy.addBlock(deindent`
|
||||
for (#i = 0; #i < ${blocks}.length; #i += 1) ${blocks}[#i].d(${parentNode ? '' : 'detach'});
|
||||
`);
|
||||
}
|
||||
|
||||
renderUnkeyed(
|
||||
block: Block,
|
||||
parentNode: string,
|
||||
parentNodes: string,
|
||||
snippet: string
|
||||
) {
|
||||
const {
|
||||
create_each_block,
|
||||
length,
|
||||
iterations,
|
||||
anchor,
|
||||
mountOrIntro,
|
||||
} = this.vars;
|
||||
|
||||
block.builders.init.addBlock(deindent`
|
||||
var ${iterations} = [];
|
||||
|
||||
for (var #i = 0; #i < ${this.vars.each_block_value}.${length}; #i += 1) {
|
||||
${iterations}[#i] = ${create_each_block}(#component, ${this.vars.get_each_context}(ctx, ${this.vars.each_block_value}, #i));
|
||||
}
|
||||
`);
|
||||
|
||||
const initialMountNode = parentNode || '#target';
|
||||
const updateMountNode = this.getUpdateMountNode(anchor);
|
||||
const anchorNode = parentNode ? 'null' : 'anchor';
|
||||
|
||||
block.builders.create.addBlock(deindent`
|
||||
for (var #i = 0; #i < ${iterations}.length; #i += 1) {
|
||||
${iterations}[#i].c();
|
||||
}
|
||||
`);
|
||||
|
||||
if (parentNodes) {
|
||||
block.builders.claim.addBlock(deindent`
|
||||
for (var #i = 0; #i < ${iterations}.length; #i += 1) {
|
||||
${iterations}[#i].l(${parentNodes});
|
||||
}
|
||||
`);
|
||||
}
|
||||
|
||||
block.builders.mount.addBlock(deindent`
|
||||
for (var #i = 0; #i < ${iterations}.length; #i += 1) {
|
||||
${iterations}[#i].${mountOrIntro}(${initialMountNode}, ${anchorNode});
|
||||
}
|
||||
`);
|
||||
|
||||
const allDependencies = new Set(this.block.dependencies);
|
||||
const { dependencies } = this.node.expression;
|
||||
dependencies.forEach((dependency: string) => {
|
||||
allDependencies.add(dependency);
|
||||
});
|
||||
|
||||
const outroBlock = this.block.hasOutros && block.getUniqueName('outroBlock')
|
||||
if (outroBlock) {
|
||||
block.builders.init.addBlock(deindent`
|
||||
function ${outroBlock}(i, detach, fn) {
|
||||
if (${iterations}[i]) {
|
||||
${iterations}[i].o(() => {
|
||||
if (detach) {
|
||||
${iterations}[i].d(detach);
|
||||
${iterations}[i] = null;
|
||||
}
|
||||
if (fn) fn();
|
||||
});
|
||||
}
|
||||
}
|
||||
`);
|
||||
}
|
||||
|
||||
// TODO do this for keyed blocks as well
|
||||
const condition = Array.from(allDependencies)
|
||||
.map(dependency => `changed.${dependency}`)
|
||||
.join(' || ');
|
||||
|
||||
if (condition !== '') {
|
||||
const forLoopBody = this.block.hasUpdateMethod
|
||||
? (this.block.hasIntros || this.block.hasOutros)
|
||||
? deindent`
|
||||
if (${iterations}[#i]) {
|
||||
${iterations}[#i].p(changed, child_ctx);
|
||||
} else {
|
||||
${iterations}[#i] = ${create_each_block}(#component, child_ctx);
|
||||
${iterations}[#i].c();
|
||||
}
|
||||
${iterations}[#i].i(${updateMountNode}, ${anchor});
|
||||
`
|
||||
: deindent`
|
||||
if (${iterations}[#i]) {
|
||||
${iterations}[#i].p(changed, child_ctx);
|
||||
} else {
|
||||
${iterations}[#i] = ${create_each_block}(#component, child_ctx);
|
||||
${iterations}[#i].c();
|
||||
${iterations}[#i].m(${updateMountNode}, ${anchor});
|
||||
}
|
||||
`
|
||||
: deindent`
|
||||
${iterations}[#i] = ${create_each_block}(#component, child_ctx);
|
||||
${iterations}[#i].c();
|
||||
${iterations}[#i].${mountOrIntro}(${updateMountNode}, ${anchor});
|
||||
`;
|
||||
|
||||
const start = this.block.hasUpdateMethod ? '0' : `${iterations}.length`;
|
||||
|
||||
let destroy;
|
||||
|
||||
if (this.block.hasOutros) {
|
||||
destroy = deindent`
|
||||
@groupOutros();
|
||||
for (; #i < ${iterations}.length; #i += 1) ${outroBlock}(#i, 1);
|
||||
`;
|
||||
} else {
|
||||
destroy = deindent`
|
||||
for (${this.block.hasUpdateMethod ? `` : `#i = ${this.vars.each_block_value}.${length}`}; #i < ${iterations}.length; #i += 1) {
|
||||
${iterations}[#i].d(1);
|
||||
}
|
||||
${iterations}.length = ${this.vars.each_block_value}.${length};
|
||||
`;
|
||||
}
|
||||
|
||||
block.builders.update.addBlock(deindent`
|
||||
if (${condition}) {
|
||||
${this.vars.each_block_value} = ${snippet};
|
||||
|
||||
for (var #i = ${start}; #i < ${this.vars.each_block_value}.${length}; #i += 1) {
|
||||
const child_ctx = ${this.vars.get_each_context}(ctx, ${this.vars.each_block_value}, #i);
|
||||
|
||||
${forLoopBody}
|
||||
}
|
||||
|
||||
${destroy}
|
||||
}
|
||||
`);
|
||||
}
|
||||
|
||||
if (outroBlock && this.renderer.component.options.nestedTransitions) {
|
||||
const countdown = block.getUniqueName('countdown');
|
||||
block.builders.outro.addBlock(deindent`
|
||||
${iterations} = ${iterations}.filter(Boolean);
|
||||
const ${countdown} = @callAfter(#outrocallback, ${iterations}.length);
|
||||
for (let #i = 0; #i < ${iterations}.length; #i += 1) ${outroBlock}(#i, 0, ${countdown});`
|
||||
);
|
||||
}
|
||||
|
||||
block.builders.destroy.addBlock(`@destroyEach(${iterations}, detach);`);
|
||||
}
|
||||
|
||||
remount(name: string) {
|
||||
// TODO consider keyed blocks
|
||||
return `for (var #i = 0; #i < ${this.vars.iterations}.length; #i += 1) ${this.vars.iterations}[#i].m(${name}._slotted.default, null);`;
|
||||
}
|
||||
}
|
@ -0,0 +1,324 @@
|
||||
import Binding from '../../../nodes/Binding';
|
||||
import ElementWrapper from '.';
|
||||
import { dimensions } from '../../../../utils/patterns';
|
||||
import getObject from '../../../../utils/getObject';
|
||||
import Block from '../../Block';
|
||||
import Node from '../../../nodes/shared/Node';
|
||||
import Renderer from '../../Renderer';
|
||||
import flattenReference from '../../../../utils/flattenReference';
|
||||
import getTailSnippet from '../../../../utils/getTailSnippet';
|
||||
|
||||
// TODO this should live in a specific binding
|
||||
const readOnlyMediaAttributes = new Set([
|
||||
'duration',
|
||||
'buffered',
|
||||
'seekable',
|
||||
'played'
|
||||
]);
|
||||
|
||||
export default class BindingWrapper {
|
||||
node: Binding;
|
||||
parent: ElementWrapper;
|
||||
|
||||
object: string;
|
||||
handler: any; // TODO
|
||||
updateDom: string;
|
||||
initialUpdate: string;
|
||||
needsLock: boolean;
|
||||
updateCondition: string;
|
||||
|
||||
constructor(block: Block, node: Binding, parent: ElementWrapper) {
|
||||
this.node = node;
|
||||
this.parent = parent;
|
||||
|
||||
const { dependencies } = this.node.value;
|
||||
|
||||
block.addDependencies(dependencies);
|
||||
|
||||
// TODO does this also apply to e.g. `<input type='checkbox' bind:group='foo'>`?
|
||||
if (parent.node.name === 'select') {
|
||||
parent.selectBindingDependencies = dependencies;
|
||||
dependencies.forEach((prop: string) => {
|
||||
parent.renderer.component.indirectDependencies.set(prop, new Set());
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
isReadOnlyMediaAttribute() {
|
||||
return readOnlyMediaAttributes.has(this.node.name);
|
||||
}
|
||||
|
||||
munge(block: Block) {
|
||||
const { parent } = this;
|
||||
const { renderer } = parent;
|
||||
|
||||
const needsLock = (
|
||||
parent.node.name !== 'input' ||
|
||||
!/radio|checkbox|range|color/.test(parent.node.getStaticAttributeValue('type'))
|
||||
);
|
||||
|
||||
const isReadOnly = (
|
||||
(parent.node.isMediaNode() && readOnlyMediaAttributes.has(this.node.name)) ||
|
||||
dimensions.test(this.node.name)
|
||||
);
|
||||
|
||||
let updateConditions: string[] = [];
|
||||
|
||||
const { name } = getObject(this.node.value.node);
|
||||
const { snippet } = this.node.value;
|
||||
|
||||
// special case: if you have e.g. `<input type=checkbox bind:checked=selected.done>`
|
||||
// and `selected` is an object chosen with a <select>, then when `checked` changes,
|
||||
// we need to tell the component to update all the values `selected` might be
|
||||
// pointing to
|
||||
// TODO should this happen in preprocess?
|
||||
const dependencies = new Set(this.node.value.dependencies);
|
||||
|
||||
this.node.value.dependencies.forEach((prop: string) => {
|
||||
const indirectDependencies = renderer.component.indirectDependencies.get(prop);
|
||||
if (indirectDependencies) {
|
||||
indirectDependencies.forEach(indirectDependency => {
|
||||
dependencies.add(indirectDependency);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// view to model
|
||||
const valueFromDom = getValueFromDom(renderer, this.parent, this);
|
||||
const handler = getEventHandler(this, renderer, block, name, snippet, dependencies, valueFromDom);
|
||||
|
||||
// model to view
|
||||
let updateDom = getDomUpdater(parent, this, snippet);
|
||||
let initialUpdate = updateDom;
|
||||
|
||||
// special cases
|
||||
if (this.node.name === 'group') {
|
||||
const bindingGroup = getBindingGroup(renderer, this.node.value.node);
|
||||
|
||||
block.builders.hydrate.addLine(
|
||||
`#component._bindingGroups[${bindingGroup}].push(${parent.var});`
|
||||
);
|
||||
|
||||
block.builders.destroy.addLine(
|
||||
`#component._bindingGroups[${bindingGroup}].splice(#component._bindingGroups[${bindingGroup}].indexOf(${parent.var}), 1);`
|
||||
);
|
||||
}
|
||||
|
||||
if (this.node.name === 'currentTime' || this.node.name === 'volume') {
|
||||
updateConditions.push(`!isNaN(${snippet})`);
|
||||
|
||||
if (this.node.name === 'currentTime') initialUpdate = null;
|
||||
}
|
||||
|
||||
if (this.node.name === 'paused') {
|
||||
// this is necessary to prevent audio restarting by itself
|
||||
const last = block.getUniqueName(`${parent.var}_is_paused`);
|
||||
block.addVariable(last, 'true');
|
||||
|
||||
updateConditions.push(`${last} !== (${last} = ${snippet})`);
|
||||
updateDom = `${parent.var}[${last} ? "pause" : "play"]();`;
|
||||
initialUpdate = null;
|
||||
}
|
||||
|
||||
// bind:offsetWidth and bind:offsetHeight
|
||||
if (dimensions.test(this.node.name)) {
|
||||
initialUpdate = null;
|
||||
updateDom = null;
|
||||
}
|
||||
|
||||
const dependencyArray = [...this.node.value.dependencies]
|
||||
|
||||
if (dependencyArray.length === 1) {
|
||||
updateConditions.push(`changed.${dependencyArray[0]}`)
|
||||
} else if (dependencyArray.length > 1) {
|
||||
updateConditions.push(
|
||||
`(${dependencyArray.map(prop => `changed.${prop}`).join(' || ')})`
|
||||
)
|
||||
}
|
||||
|
||||
return {
|
||||
name: this.node.name,
|
||||
object: name,
|
||||
handler: handler,
|
||||
usesContext: handler.usesContext,
|
||||
updateDom: updateDom,
|
||||
initialUpdate: initialUpdate,
|
||||
needsLock: !isReadOnly && needsLock,
|
||||
updateCondition: updateConditions.length ? updateConditions.join(' && ') : undefined,
|
||||
isReadOnlyMediaAttribute: this.isReadOnlyMediaAttribute()
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function getDomUpdater(
|
||||
element: ElementWrapper,
|
||||
binding: BindingWrapper,
|
||||
snippet: string
|
||||
) {
|
||||
const { node } = element;
|
||||
|
||||
if (binding.isReadOnlyMediaAttribute()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (node.name === 'select') {
|
||||
return node.getStaticAttributeValue('multiple') === true ?
|
||||
`@selectOptions(${element.var}, ${snippet})` :
|
||||
`@selectOption(${element.var}, ${snippet})`;
|
||||
}
|
||||
|
||||
if (binding.node.name === 'group') {
|
||||
const type = node.getStaticAttributeValue('type');
|
||||
|
||||
const condition = type === 'checkbox'
|
||||
? `~${snippet}.indexOf(${element.var}.__value)`
|
||||
: `${element.var}.__value === ${snippet}`;
|
||||
|
||||
return `${element.var}.checked = ${condition};`
|
||||
}
|
||||
|
||||
return `${element.var}.${binding.node.name} = ${snippet};`;
|
||||
}
|
||||
|
||||
function getBindingGroup(renderer: Renderer, value: Node) {
|
||||
const { parts } = flattenReference(value); // TODO handle cases involving computed member expressions
|
||||
const keypath = parts.join('.');
|
||||
|
||||
// TODO handle contextual bindings — `keypath` should include unique ID of
|
||||
// each block that provides context
|
||||
let index = renderer.bindingGroups.indexOf(keypath);
|
||||
if (index === -1) {
|
||||
index = renderer.bindingGroups.length;
|
||||
renderer.bindingGroups.push(keypath);
|
||||
}
|
||||
|
||||
return index;
|
||||
}
|
||||
|
||||
function getEventHandler(
|
||||
binding: BindingWrapper,
|
||||
renderer: Renderer,
|
||||
block: Block,
|
||||
name: string,
|
||||
snippet: string,
|
||||
dependencies: Set<string>,
|
||||
value: string
|
||||
) {
|
||||
const storeDependencies = [...dependencies].filter(prop => prop[0] === '$').map(prop => prop.slice(1));
|
||||
let dependenciesArray = [...dependencies].filter(prop => prop[0] !== '$');
|
||||
|
||||
if (binding.node.isContextual) {
|
||||
const tail = binding.node.value.node.type === 'MemberExpression'
|
||||
? getTailSnippet(binding.node.value.node)
|
||||
: '';
|
||||
|
||||
const head = block.bindings.get(name);
|
||||
|
||||
return {
|
||||
usesContext: true,
|
||||
usesState: true,
|
||||
usesStore: storeDependencies.length > 0,
|
||||
mutation: `${head()}${tail} = ${value};`,
|
||||
props: dependenciesArray.map(prop => `${prop}: ctx.${prop}`),
|
||||
storeProps: storeDependencies.map(prop => `${prop}: $.${prop}`)
|
||||
};
|
||||
}
|
||||
|
||||
if (binding.node.value.node.type === 'MemberExpression') {
|
||||
// This is a little confusing, and should probably be tidied up
|
||||
// at some point. It addresses a tricky bug (#893), wherein
|
||||
// Svelte tries to `set()` a computed property, which throws an
|
||||
// error in dev mode. a) it's possible that we should be
|
||||
// replacing computations with *their* dependencies, and b)
|
||||
// we should probably populate `component.target.readonly` sooner so
|
||||
// that we don't have to do the `.some()` here
|
||||
dependenciesArray = dependenciesArray.filter(prop => !renderer.component.computations.some(computation => computation.key === prop));
|
||||
|
||||
return {
|
||||
usesContext: false,
|
||||
usesState: true,
|
||||
usesStore: storeDependencies.length > 0,
|
||||
mutation: `${snippet} = ${value}`,
|
||||
props: dependenciesArray.map((prop: string) => `${prop}: ctx.${prop}`),
|
||||
storeProps: storeDependencies.map(prop => `${prop}: $.${prop}`)
|
||||
};
|
||||
}
|
||||
|
||||
let props;
|
||||
let storeProps;
|
||||
|
||||
if (name[0] === '$') {
|
||||
props = [];
|
||||
storeProps = [`${name.slice(1)}: ${value}`];
|
||||
} else {
|
||||
props = [`${name}: ${value}`];
|
||||
storeProps = [];
|
||||
}
|
||||
|
||||
return {
|
||||
usesContext: false,
|
||||
usesState: false,
|
||||
usesStore: false,
|
||||
mutation: null,
|
||||
props,
|
||||
storeProps
|
||||
};
|
||||
}
|
||||
|
||||
function isComputed(node: Node) {
|
||||
while (node.type === 'MemberExpression') {
|
||||
if (node.computed) return true;
|
||||
node = node.object;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function getValueFromDom(
|
||||
renderer: Renderer,
|
||||
element: ElementWrapper,
|
||||
binding: BindingWrapper
|
||||
) {
|
||||
const { node } = element;
|
||||
const { name } = binding.node;
|
||||
|
||||
// <select bind:value='selected>
|
||||
if (node.name === 'select') {
|
||||
return node.getStaticAttributeValue('multiple') === true ?
|
||||
`@selectMultipleValue(${element.var})` :
|
||||
`@selectValue(${element.var})`;
|
||||
}
|
||||
|
||||
const type = node.getStaticAttributeValue('type');
|
||||
|
||||
// <input type='checkbox' bind:group='foo'>
|
||||
if (name === 'group') {
|
||||
const bindingGroup = getBindingGroup(renderer, binding.node.value.node);
|
||||
if (type === 'checkbox') {
|
||||
return `@getBindingGroupValue(#component._bindingGroups[${bindingGroup}])`;
|
||||
}
|
||||
|
||||
return `${element.var}.__value`;
|
||||
}
|
||||
|
||||
// <input type='range|number' bind:value>
|
||||
if (type === 'range' || type === 'number') {
|
||||
return `@toNumber(${element.var}.${name})`;
|
||||
}
|
||||
|
||||
if ((name === 'buffered' || name === 'seekable' || name === 'played')) {
|
||||
return `@timeRangesToArray(${element.var}.${name})`
|
||||
}
|
||||
|
||||
// everything else
|
||||
return `${element.var}.${name}`;
|
||||
}
|
||||
|
||||
function isComputed(node: Node) {
|
||||
while (node.type === 'MemberExpression') {
|
||||
if (node.computed) return true;
|
||||
node = node.object;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
@ -0,0 +1,178 @@
|
||||
import Attribute from '../../../nodes/Attribute';
|
||||
import Block from '../../Block';
|
||||
import AttributeWrapper from './Attribute';
|
||||
import Node from '../../../nodes/shared/Node';
|
||||
import ElementWrapper from '.';
|
||||
import { stringify } from '../../../../utils/stringify';
|
||||
|
||||
export interface StyleProp {
|
||||
key: string;
|
||||
value: Node[];
|
||||
}
|
||||
|
||||
export default class StyleAttributeWrapper extends AttributeWrapper {
|
||||
node: Attribute;
|
||||
parent: ElementWrapper;
|
||||
|
||||
render(block: Block) {
|
||||
const styleProps = optimizeStyle(this.node.chunks);
|
||||
if (!styleProps) return super.render(block);
|
||||
|
||||
styleProps.forEach((prop: StyleProp) => {
|
||||
let value;
|
||||
|
||||
if (isDynamic(prop.value)) {
|
||||
const propDependencies = new Set();
|
||||
let shouldCache;
|
||||
|
||||
value =
|
||||
((prop.value.length === 1 || prop.value[0].type === 'Text') ? '' : `"" + `) +
|
||||
prop.value
|
||||
.map((chunk: Node) => {
|
||||
if (chunk.type === 'Text') {
|
||||
return stringify(chunk.data);
|
||||
} else {
|
||||
const { dependencies, snippet } = chunk;
|
||||
|
||||
dependencies.forEach(d => {
|
||||
propDependencies.add(d);
|
||||
});
|
||||
|
||||
return chunk.getPrecedence() <= 13 ? `(${snippet})` : snippet;
|
||||
}
|
||||
})
|
||||
.join(' + ');
|
||||
|
||||
if (propDependencies.size) {
|
||||
const dependencies = Array.from(propDependencies);
|
||||
const condition = (
|
||||
(block.hasOutros ? `!#current || ` : '') +
|
||||
dependencies.map(dependency => `changed.${dependency}`).join(' || ')
|
||||
);
|
||||
|
||||
block.builders.update.addConditional(
|
||||
condition,
|
||||
`@setStyle(${this.parent.var}, "${prop.key}", ${value});`
|
||||
);
|
||||
}
|
||||
} else {
|
||||
value = stringify(prop.value[0].data);
|
||||
}
|
||||
|
||||
block.builders.hydrate.addLine(
|
||||
`@setStyle(${this.parent.var}, "${prop.key}", ${value});`
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function optimizeStyle(value: Node[]) {
|
||||
const props: { key: string, value: Node[] }[] = [];
|
||||
let chunks = value.slice();
|
||||
|
||||
while (chunks.length) {
|
||||
const chunk = chunks[0];
|
||||
|
||||
if (chunk.type !== 'Text') return null;
|
||||
|
||||
const keyMatch = /^\s*([\w-]+):\s*/.exec(chunk.data);
|
||||
if (!keyMatch) return null;
|
||||
|
||||
const key = keyMatch[1];
|
||||
|
||||
const offset = keyMatch.index + keyMatch[0].length;
|
||||
const remainingData = chunk.data.slice(offset);
|
||||
|
||||
if (remainingData) {
|
||||
chunks[0] = {
|
||||
start: chunk.start + offset,
|
||||
end: chunk.end,
|
||||
type: 'Text',
|
||||
data: remainingData
|
||||
};
|
||||
} else {
|
||||
chunks.shift();
|
||||
}
|
||||
|
||||
const result = getStyleValue(chunks);
|
||||
if (!result) return null;
|
||||
|
||||
props.push({ key, value: result.value });
|
||||
chunks = result.chunks;
|
||||
}
|
||||
|
||||
return props;
|
||||
}
|
||||
|
||||
function getStyleValue(chunks: Node[]) {
|
||||
const value: Node[] = [];
|
||||
|
||||
let inUrl = false;
|
||||
let quoteMark = null;
|
||||
let escaped = false;
|
||||
|
||||
while (chunks.length) {
|
||||
const chunk = chunks.shift();
|
||||
|
||||
if (chunk.type === 'Text') {
|
||||
let c = 0;
|
||||
while (c < chunk.data.length) {
|
||||
const char = chunk.data[c];
|
||||
|
||||
if (escaped) {
|
||||
escaped = false;
|
||||
} else if (char === '\\') {
|
||||
escaped = true;
|
||||
} else if (char === quoteMark) {
|
||||
quoteMark === null;
|
||||
} else if (char === '"' || char === "'") {
|
||||
quoteMark = char;
|
||||
} else if (char === ')' && inUrl) {
|
||||
inUrl = false;
|
||||
} else if (char === 'u' && chunk.data.slice(c, c + 4) === 'url(') {
|
||||
inUrl = true;
|
||||
} else if (char === ';' && !inUrl && !quoteMark) {
|
||||
break;
|
||||
}
|
||||
|
||||
c += 1;
|
||||
}
|
||||
|
||||
if (c > 0) {
|
||||
value.push({
|
||||
type: 'Text',
|
||||
start: chunk.start,
|
||||
end: chunk.start + c,
|
||||
data: chunk.data.slice(0, c)
|
||||
});
|
||||
}
|
||||
|
||||
while (/[;\s]/.test(chunk.data[c])) c += 1;
|
||||
const remainingData = chunk.data.slice(c);
|
||||
|
||||
if (remainingData) {
|
||||
chunks.unshift({
|
||||
start: chunk.start + c,
|
||||
end: chunk.end,
|
||||
type: 'Text',
|
||||
data: remainingData
|
||||
});
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
else {
|
||||
value.push(chunk);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
chunks,
|
||||
value
|
||||
};
|
||||
}
|
||||
|
||||
function isDynamic(value: Node[]) {
|
||||
return value.length > 1 || value[0].type !== 'Text';
|
||||
}
|
@ -0,0 +1,918 @@
|
||||
import Renderer from '../../Renderer';
|
||||
import Element from '../../../nodes/Element';
|
||||
import Wrapper from '../shared/Wrapper';
|
||||
import Block from '../../Block';
|
||||
import Node from '../../../nodes/shared/Node';
|
||||
import { quotePropIfNecessary, quoteNameIfNecessary } from '../../../../utils/quoteIfNecessary';
|
||||
import isVoidElementName from '../../../../utils/isVoidElementName';
|
||||
import FragmentWrapper from '../Fragment';
|
||||
import { stringify, escapeHTML, escape } from '../../../../utils/stringify';
|
||||
import TextWrapper from '../Text';
|
||||
import fixAttributeCasing from '../../../../utils/fixAttributeCasing';
|
||||
import deindent from '../../../../utils/deindent';
|
||||
import namespaces from '../../../../utils/namespaces';
|
||||
import AttributeWrapper from './Attribute';
|
||||
import StyleAttributeWrapper from './StyleAttribute';
|
||||
import { dimensions } from '../../../../utils/patterns';
|
||||
import Binding from './Binding';
|
||||
import InlineComponentWrapper from '../InlineComponent';
|
||||
|
||||
const events = [
|
||||
{
|
||||
eventNames: ['input'],
|
||||
filter: (node: Element, name: string) =>
|
||||
node.name === 'textarea' ||
|
||||
node.name === 'input' && !/radio|checkbox|range/.test(node.getStaticAttributeValue('type'))
|
||||
},
|
||||
{
|
||||
eventNames: ['change'],
|
||||
filter: (node: Element, name: string) =>
|
||||
node.name === 'select' ||
|
||||
node.name === 'input' && /radio|checkbox/.test(node.getStaticAttributeValue('type'))
|
||||
},
|
||||
{
|
||||
eventNames: ['change', 'input'],
|
||||
filter: (node: Element, name: string) =>
|
||||
node.name === 'input' && node.getStaticAttributeValue('type') === 'range'
|
||||
},
|
||||
|
||||
{
|
||||
eventNames: ['resize'],
|
||||
filter: (node: Element, name: string) =>
|
||||
dimensions.test(name)
|
||||
},
|
||||
|
||||
// media events
|
||||
{
|
||||
eventNames: ['timeupdate'],
|
||||
filter: (node: Element, name: string) =>
|
||||
node.isMediaNode() &&
|
||||
(name === 'currentTime' || name === 'played')
|
||||
},
|
||||
{
|
||||
eventNames: ['durationchange'],
|
||||
filter: (node: Element, name: string) =>
|
||||
node.isMediaNode() &&
|
||||
name === 'duration'
|
||||
},
|
||||
{
|
||||
eventNames: ['play', 'pause'],
|
||||
filter: (node: Element, name: string) =>
|
||||
node.isMediaNode() &&
|
||||
name === 'paused'
|
||||
},
|
||||
{
|
||||
eventNames: ['progress'],
|
||||
filter: (node: Element, name: string) =>
|
||||
node.isMediaNode() &&
|
||||
name === 'buffered'
|
||||
},
|
||||
{
|
||||
eventNames: ['loadedmetadata'],
|
||||
filter: (node: Element, name: string) =>
|
||||
node.isMediaNode() &&
|
||||
(name === 'buffered' || name === 'seekable')
|
||||
},
|
||||
{
|
||||
eventNames: ['volumechange'],
|
||||
filter: (node: Element, name: string) =>
|
||||
node.isMediaNode() &&
|
||||
name === 'volume'
|
||||
}
|
||||
];
|
||||
|
||||
export default class ElementWrapper extends Wrapper {
|
||||
node: Element;
|
||||
fragment: FragmentWrapper;
|
||||
attributes: AttributeWrapper[];
|
||||
bindings: Binding[];
|
||||
classDependencies: string[];
|
||||
initialUpdate: string;
|
||||
|
||||
slotOwner?: InlineComponentWrapper;
|
||||
selectBindingDependencies?: Set<string>;
|
||||
|
||||
var: string;
|
||||
|
||||
|
||||
constructor(
|
||||
renderer: Renderer,
|
||||
block: Block,
|
||||
parent: Wrapper,
|
||||
node: Element,
|
||||
stripWhitespace: boolean,
|
||||
nextSibling: Wrapper
|
||||
) {
|
||||
super(renderer, block, parent, node);
|
||||
this.var = node.name.replace(/[^a-zA-Z0-9_$]/g, '_')
|
||||
|
||||
this.classDependencies = [];
|
||||
|
||||
this.attributes = this.node.attributes.map(attribute => {
|
||||
if (attribute.name === 'slot') {
|
||||
// TODO make separate subclass for this?
|
||||
let owner = this.parent;
|
||||
while (owner) {
|
||||
if (owner.node.type === 'InlineComponent') {
|
||||
break;
|
||||
}
|
||||
|
||||
if (owner.node.type === 'Element' && /-/.test(owner.node.name)) {
|
||||
break;
|
||||
}
|
||||
|
||||
owner = owner.parent;
|
||||
}
|
||||
|
||||
if (owner && owner.node.type === 'InlineComponent') {
|
||||
this.slotOwner = <InlineComponentWrapper>owner;
|
||||
owner._slots.add(attribute.getStaticValue());
|
||||
}
|
||||
}
|
||||
if (attribute.name === 'style') {
|
||||
return new StyleAttributeWrapper(this, block, attribute);
|
||||
}
|
||||
return new AttributeWrapper(this, block, attribute);
|
||||
});
|
||||
|
||||
let has_bindings;
|
||||
const binding_lookup = {};
|
||||
this.node.bindings.forEach(binding => {
|
||||
binding_lookup[binding.name] = binding;
|
||||
has_bindings = true;
|
||||
});
|
||||
|
||||
const type = this.node.getStaticAttributeValue('type');
|
||||
|
||||
// ordinarily, there'll only be one... but we need to handle
|
||||
// the rare case where an element can have multiple bindings,
|
||||
// e.g. <audio bind:paused bind:currentTime>
|
||||
this.bindings = this.node.bindings.map(binding => new Binding(block, binding, this));
|
||||
|
||||
// TODO remove this, it's just useful during refactoring
|
||||
if (has_bindings && !this.bindings.length) {
|
||||
throw new Error(`no binding was created`);
|
||||
}
|
||||
|
||||
if (node.intro || node.outro) {
|
||||
if (node.intro) block.addIntro();
|
||||
if (node.outro) block.addOutro();
|
||||
}
|
||||
|
||||
if (node.animation) {
|
||||
block.addAnimation();
|
||||
}
|
||||
|
||||
// add directive and handler dependencies
|
||||
[node.animation, node.outro, ...node.actions, ...node.classes].forEach(directive => {
|
||||
if (directive && directive.expression) {
|
||||
block.addDependencies(directive.expression.dependencies);
|
||||
}
|
||||
});
|
||||
|
||||
node.handlers.forEach(handler => {
|
||||
block.addDependencies(handler.dependencies);
|
||||
});
|
||||
|
||||
if (this.parent) {
|
||||
if (node.actions.length > 0) this.parent.cannotUseInnerHTML();
|
||||
if (node.animation) this.parent.cannotUseInnerHTML();
|
||||
if (node.bindings.length > 0) this.parent.cannotUseInnerHTML();
|
||||
if (node.classes.length > 0) this.parent.cannotUseInnerHTML();
|
||||
if (node.intro || node.outro) this.parent.cannotUseInnerHTML();
|
||||
if (node.handlers.length > 0) this.parent.cannotUseInnerHTML();
|
||||
if (node.ref) this.parent.cannotUseInnerHTML();
|
||||
|
||||
if (this.node.name === 'option') this.parent.cannotUseInnerHTML();
|
||||
|
||||
if (renderer.options.dev) {
|
||||
this.parent.cannotUseInnerHTML(); // need to use addLoc
|
||||
}
|
||||
}
|
||||
|
||||
this.fragment = new FragmentWrapper(renderer, block, node.children, this, stripWhitespace, nextSibling);
|
||||
}
|
||||
|
||||
render(block: Block, parentNode: string, parentNodes: string) {
|
||||
const { renderer } = this;
|
||||
|
||||
if (this.node.name === 'slot') {
|
||||
const slotName = this.getStaticAttributeValue('name') || 'default';
|
||||
renderer.slots.add(slotName);
|
||||
}
|
||||
|
||||
if (this.node.name === 'noscript') return;
|
||||
|
||||
const node = this.var;
|
||||
const nodes = parentNodes && block.getUniqueName(`${this.var}_nodes`) // if we're in unclaimable territory, i.e. <head>, parentNodes is null
|
||||
|
||||
const slot = this.node.attributes.find((attribute: Node) => attribute.name === 'slot');
|
||||
const prop = slot && quotePropIfNecessary(slot.chunks[0].data);
|
||||
|
||||
let initialMountNode;
|
||||
|
||||
if (this.slotOwner) {
|
||||
initialMountNode = `${this.slotOwner.var}._slotted${prop}`;
|
||||
} else {
|
||||
initialMountNode = parentNode;
|
||||
}
|
||||
|
||||
block.addVariable(node);
|
||||
const renderStatement = this.getRenderStatement();
|
||||
block.builders.create.addLine(
|
||||
`${node} = ${renderStatement};`
|
||||
);
|
||||
|
||||
if (renderer.options.hydratable) {
|
||||
if (parentNodes) {
|
||||
block.builders.claim.addBlock(deindent`
|
||||
${node} = ${this.getClaimStatement(parentNodes)};
|
||||
var ${nodes} = @children(${this.node.name === 'template' ? `${node}.content` : node});
|
||||
`);
|
||||
} else {
|
||||
block.builders.claim.addLine(
|
||||
`${node} = ${renderStatement};`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (initialMountNode) {
|
||||
block.builders.mount.addLine(
|
||||
`@append(${initialMountNode}, ${node});`
|
||||
);
|
||||
|
||||
if (initialMountNode === 'document.head') {
|
||||
block.builders.destroy.addLine(`@detachNode(${node});`);
|
||||
}
|
||||
} else {
|
||||
block.builders.mount.addLine(`@insert(#target, ${node}, anchor);`);
|
||||
|
||||
// TODO we eventually need to consider what happens to elements
|
||||
// that belong to the same outgroup as an outroing element...
|
||||
block.builders.destroy.addConditional('detach', `@detachNode(${node});`);
|
||||
}
|
||||
|
||||
// insert static children with textContent or innerHTML
|
||||
if (!this.node.namespace && this.canUseInnerHTML && this.fragment.nodes.length > 0) {
|
||||
if (this.fragment.nodes.length === 1 && this.fragment.nodes[0].node.type === 'Text') {
|
||||
block.builders.create.addLine(
|
||||
`${node}.textContent = ${stringify(this.fragment.nodes[0].data)};`
|
||||
);
|
||||
} else {
|
||||
const innerHTML = escape(
|
||||
this.fragment.nodes
|
||||
.map(toHTML)
|
||||
.join('')
|
||||
);
|
||||
|
||||
block.builders.create.addLine(
|
||||
`${node}.innerHTML = \`${innerHTML}\`;`
|
||||
);
|
||||
}
|
||||
} else {
|
||||
this.fragment.nodes.forEach((child: Wrapper) => {
|
||||
child.render(
|
||||
block,
|
||||
this.node.name === 'template' ? `${node}.content` : node,
|
||||
nodes
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
let hasHoistedEventHandlerOrBinding = (
|
||||
//(this.hasAncestor('EachBlock') && this.bindings.length > 0) ||
|
||||
this.node.handlers.some(handler => handler.shouldHoist)
|
||||
);
|
||||
const eventHandlerOrBindingUsesComponent = (
|
||||
this.bindings.length > 0 ||
|
||||
this.node.handlers.some(handler => handler.usesComponent)
|
||||
);
|
||||
|
||||
const eventHandlerOrBindingUsesContext = (
|
||||
this.bindings.some(binding => binding.node.usesContext) ||
|
||||
this.node.handlers.some(handler => handler.usesContext)
|
||||
);
|
||||
|
||||
if (hasHoistedEventHandlerOrBinding) {
|
||||
const initialProps: string[] = [];
|
||||
const updates: string[] = [];
|
||||
|
||||
if (eventHandlerOrBindingUsesComponent) {
|
||||
const component = block.alias('component');
|
||||
initialProps.push(component === 'component' ? 'component' : `component: ${component}`);
|
||||
}
|
||||
|
||||
if (eventHandlerOrBindingUsesContext) {
|
||||
initialProps.push(`ctx`);
|
||||
block.builders.update.addLine(`${node}._svelte.ctx = ctx;`);
|
||||
block.maintainContext = true;
|
||||
}
|
||||
|
||||
if (initialProps.length) {
|
||||
block.builders.hydrate.addBlock(deindent`
|
||||
${node}._svelte = { ${initialProps.join(', ')} };
|
||||
`);
|
||||
}
|
||||
} else {
|
||||
if (eventHandlerOrBindingUsesContext) {
|
||||
block.maintainContext = true;
|
||||
}
|
||||
}
|
||||
|
||||
this.addBindings(block);
|
||||
this.addEventHandlers(block);
|
||||
if (this.node.ref) this.addRef(block);
|
||||
this.addAttributes(block);
|
||||
this.addTransitions(block);
|
||||
this.addAnimation(block);
|
||||
this.addActions(block);
|
||||
this.addClasses(block);
|
||||
|
||||
if (this.initialUpdate) {
|
||||
block.builders.mount.addBlock(this.initialUpdate);
|
||||
}
|
||||
|
||||
if (nodes) {
|
||||
block.builders.claim.addLine(
|
||||
`${nodes}.forEach(@detachNode);`
|
||||
);
|
||||
}
|
||||
|
||||
function toHTML(wrapper: ElementWrapper | TextWrapper) {
|
||||
if (wrapper.node.type === 'Text') {
|
||||
const { parent } = wrapper.node;
|
||||
|
||||
const raw = parent && (
|
||||
parent.name === 'script' ||
|
||||
parent.name === 'style'
|
||||
);
|
||||
|
||||
return raw
|
||||
? wrapper.node.data
|
||||
: escapeHTML(wrapper.node.data)
|
||||
.replace(/\\/g, '\\\\')
|
||||
.replace(/`/g, '\\`')
|
||||
.replace(/\$/g, '\\$');
|
||||
}
|
||||
|
||||
if (wrapper.node.name === 'noscript') return '';
|
||||
|
||||
let open = `<${wrapper.node.name}`;
|
||||
|
||||
(<ElementWrapper>wrapper).attributes.forEach((attr: AttributeWrapper) => {
|
||||
open += ` ${fixAttributeCasing(attr.node.name)}${attr.stringify()}`
|
||||
});
|
||||
|
||||
if (isVoidElementName(wrapper.node.name)) return open + '>';
|
||||
|
||||
return `${open}>${wrapper.fragment.nodes.map(toHTML).join('')}</${wrapper.node.name}>`;
|
||||
}
|
||||
|
||||
if (renderer.options.dev) {
|
||||
const loc = renderer.locate(this.node.start);
|
||||
block.builders.hydrate.addLine(
|
||||
`@addLoc(${this.var}, ${renderer.fileVar}, ${loc.line}, ${loc.column}, ${this.node.start});`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
getRenderStatement() {
|
||||
const { name, namespace } = this.node;
|
||||
|
||||
if (namespace === 'http://www.w3.org/2000/svg') {
|
||||
return `@createSvgElement("${name}")`;
|
||||
}
|
||||
|
||||
if (namespace) {
|
||||
return `document.createElementNS("${namespace}", "${name}")`;
|
||||
}
|
||||
|
||||
return `@createElement("${name}")`;
|
||||
}
|
||||
|
||||
getClaimStatement(nodes: string) {
|
||||
const attributes = this.node.attributes
|
||||
.filter((attr: Node) => attr.type === 'Attribute')
|
||||
.map((attr: Node) => `${quoteNameIfNecessary(attr.name)}: true`)
|
||||
.join(', ');
|
||||
|
||||
const name = this.node.namespace
|
||||
? this.node.name
|
||||
: this.node.name.toUpperCase();
|
||||
|
||||
return `@claimElement(${nodes}, "${name}", ${attributes
|
||||
? `{ ${attributes} }`
|
||||
: `{}`}, ${this.node.namespace === namespaces.svg ? true : false})`;
|
||||
}
|
||||
|
||||
addBindings(block: Block) {
|
||||
const { renderer } = this;
|
||||
|
||||
if (this.bindings.length === 0) return;
|
||||
|
||||
if (this.node.name === 'select' || this.isMediaNode()) {
|
||||
this.renderer.hasComplexBindings = true;
|
||||
}
|
||||
|
||||
const needsLock = this.node.name !== 'input' || !/radio|checkbox|range|color/.test(this.getStaticAttributeValue('type'));
|
||||
|
||||
// TODO munge in constructor
|
||||
const mungedBindings = this.bindings.map(binding => binding.munge(block));
|
||||
|
||||
const lock = mungedBindings.some(binding => binding.needsLock) ?
|
||||
block.getUniqueName(`${this.var}_updating`) :
|
||||
null;
|
||||
|
||||
if (lock) block.addVariable(lock, 'false');
|
||||
|
||||
const groups = events
|
||||
.map(event => {
|
||||
return {
|
||||
events: event.eventNames,
|
||||
bindings: mungedBindings.filter(binding => event.filter(this.node, binding.name))
|
||||
};
|
||||
})
|
||||
.filter(group => group.bindings.length);
|
||||
|
||||
groups.forEach(group => {
|
||||
const handler = block.getUniqueName(`${this.var}_${group.events.join('_')}_handler`);
|
||||
|
||||
const needsLock = group.bindings.some(binding => binding.needsLock);
|
||||
|
||||
group.bindings.forEach(binding => {
|
||||
if (!binding.updateDom) return;
|
||||
|
||||
const updateConditions = needsLock ? [`!${lock}`] : [];
|
||||
if (binding.updateCondition) updateConditions.push(binding.updateCondition);
|
||||
|
||||
block.builders.update.addLine(
|
||||
updateConditions.length ? `if (${updateConditions.join(' && ')}) ${binding.updateDom}` : binding.updateDom
|
||||
);
|
||||
});
|
||||
|
||||
const usesStore = group.bindings.some(binding => binding.handler.usesStore);
|
||||
const mutations = group.bindings.map(binding => binding.handler.mutation).filter(Boolean).join('\n');
|
||||
|
||||
const props = new Set();
|
||||
const storeProps = new Set();
|
||||
group.bindings.forEach(binding => {
|
||||
binding.handler.props.forEach(prop => {
|
||||
props.add(prop);
|
||||
});
|
||||
|
||||
binding.handler.storeProps.forEach(prop => {
|
||||
storeProps.add(prop);
|
||||
});
|
||||
}); // TODO use stringifyProps here, once indenting is fixed
|
||||
|
||||
// media bindings — awkward special case. The native timeupdate events
|
||||
// fire too infrequently, so we need to take matters into our
|
||||
// own hands
|
||||
let animation_frame;
|
||||
if (group.events[0] === 'timeupdate') {
|
||||
animation_frame = block.getUniqueName(`${this.var}_animationframe`);
|
||||
block.addVariable(animation_frame);
|
||||
}
|
||||
|
||||
block.builders.init.addBlock(deindent`
|
||||
function ${handler}() {
|
||||
${
|
||||
animation_frame && deindent`
|
||||
cancelAnimationFrame(${animation_frame});
|
||||
if (!${this.var}.paused) ${animation_frame} = requestAnimationFrame(${handler});`
|
||||
}
|
||||
${usesStore && `var $ = #component.store.get();`}
|
||||
${needsLock && `${lock} = true;`}
|
||||
${mutations.length > 0 && mutations}
|
||||
${props.size > 0 && `#component.set({ ${Array.from(props).join(', ')} });`}
|
||||
${storeProps.size > 0 && `#component.store.set({ ${Array.from(storeProps).join(', ')} });`}
|
||||
${needsLock && `${lock} = false;`}
|
||||
}
|
||||
`);
|
||||
|
||||
group.events.forEach(name => {
|
||||
if (name === 'resize') {
|
||||
// special case
|
||||
const resize_listener = block.getUniqueName(`${this.var}_resize_listener`);
|
||||
block.addVariable(resize_listener);
|
||||
|
||||
block.builders.mount.addLine(
|
||||
`${resize_listener} = @addResizeListener(${this.var}, ${handler});`
|
||||
);
|
||||
|
||||
block.builders.destroy.addLine(
|
||||
`${resize_listener}.cancel();`
|
||||
);
|
||||
} else {
|
||||
block.builders.hydrate.addLine(
|
||||
`@addListener(${this.var}, "${name}", ${handler});`
|
||||
);
|
||||
|
||||
block.builders.destroy.addLine(
|
||||
`@removeListener(${this.var}, "${name}", ${handler});`
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
const allInitialStateIsDefined = group.bindings
|
||||
.map(binding => `'${binding.object}' in ctx`)
|
||||
.join(' && ');
|
||||
|
||||
if (this.node.name === 'select' || group.bindings.find(binding => binding.name === 'indeterminate' || binding.isReadOnlyMediaAttribute)) {
|
||||
renderer.hasComplexBindings = true;
|
||||
|
||||
block.builders.hydrate.addLine(
|
||||
`if (!(${allInitialStateIsDefined})) #component.root._beforecreate.push(${handler});`
|
||||
);
|
||||
}
|
||||
|
||||
if (group.events[0] === 'resize') {
|
||||
renderer.hasComplexBindings = true;
|
||||
|
||||
block.builders.hydrate.addLine(
|
||||
`#component.root._beforecreate.push(${handler});`
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
this.initialUpdate = mungedBindings.map(binding => binding.initialUpdate).filter(Boolean).join('\n');
|
||||
}
|
||||
|
||||
addAttributes(block: Block) {
|
||||
if (this.node.attributes.find(attr => attr.type === 'Spread')) {
|
||||
this.addSpreadAttributes(block);
|
||||
return;
|
||||
}
|
||||
|
||||
this.attributes.forEach((attribute: Attribute) => {
|
||||
if (attribute.node.name === 'class' && attribute.node.isDynamic) {
|
||||
this.classDependencies.push(...attribute.node.dependencies);
|
||||
}
|
||||
attribute.render(block);
|
||||
});
|
||||
}
|
||||
|
||||
addSpreadAttributes(block: Block) {
|
||||
const levels = block.getUniqueName(`${this.var}_levels`);
|
||||
const data = block.getUniqueName(`${this.var}_data`);
|
||||
|
||||
const initialProps = [];
|
||||
const updates = [];
|
||||
|
||||
this.node.attributes
|
||||
.filter(attr => attr.type === 'Attribute' || attr.type === 'Spread')
|
||||
.forEach(attr => {
|
||||
const condition = attr.dependencies.size > 0
|
||||
? `(${[...attr.dependencies].map(d => `changed.${d}`).join(' || ')})`
|
||||
: null;
|
||||
|
||||
if (attr.isSpread) {
|
||||
const { snippet, dependencies } = attr.expression;
|
||||
|
||||
initialProps.push(snippet);
|
||||
|
||||
updates.push(condition ? `${condition} && ${snippet}` : snippet);
|
||||
} else {
|
||||
const snippet = `{ ${quoteNameIfNecessary(attr.name)}: ${attr.getValue()} }`;
|
||||
initialProps.push(snippet);
|
||||
|
||||
updates.push(condition ? `${condition} && ${snippet}` : snippet);
|
||||
}
|
||||
});
|
||||
|
||||
block.builders.init.addBlock(deindent`
|
||||
var ${levels} = [
|
||||
${initialProps.join(',\n')}
|
||||
];
|
||||
|
||||
var ${data} = {};
|
||||
for (var #i = 0; #i < ${levels}.length; #i += 1) {
|
||||
${data} = @assign(${data}, ${levels}[#i]);
|
||||
}
|
||||
`);
|
||||
|
||||
block.builders.hydrate.addLine(
|
||||
`@setAttributes(${this.var}, ${data});`
|
||||
);
|
||||
|
||||
block.builders.update.addBlock(deindent`
|
||||
@setAttributes(${this.var}, @getSpreadUpdate(${levels}, [
|
||||
${updates.join(',\n')}
|
||||
]));
|
||||
`);
|
||||
}
|
||||
|
||||
addEventHandlers(block: Block) {
|
||||
const { renderer } = this;
|
||||
const { component } = renderer;
|
||||
|
||||
this.node.handlers.forEach(handler => {
|
||||
const isCustomEvent = component.events.has(handler.name);
|
||||
|
||||
if (handler.callee) {
|
||||
// TODO move handler render method into a wrapper
|
||||
handler.render(this.renderer.component, block, this.var, handler.shouldHoist);
|
||||
}
|
||||
|
||||
const target = handler.shouldHoist ? 'this' : this.var;
|
||||
|
||||
// get a name for the event handler that is globally unique
|
||||
// if hoisted, locally unique otherwise
|
||||
const handlerName = (handler.shouldHoist ? component : block).getUniqueName(
|
||||
`${handler.name.replace(/[^a-zA-Z0-9_$]/g, '_')}_handler`
|
||||
);
|
||||
|
||||
const component_name = block.alias('component'); // can't use #component, might be hoisted
|
||||
|
||||
// create the handler body
|
||||
const handlerBody = deindent`
|
||||
${handler.shouldHoist && (
|
||||
handler.usesComponent || handler.usesContext
|
||||
? `const { ${[handler.usesComponent && 'component', handler.usesContext && 'ctx'].filter(Boolean).join(', ')} } = ${target}._svelte;`
|
||||
: null
|
||||
)}
|
||||
|
||||
${handler.snippet ?
|
||||
handler.snippet :
|
||||
`${component_name}.fire("${handler.name}", event);`}
|
||||
`;
|
||||
|
||||
if (isCustomEvent) {
|
||||
block.addVariable(handlerName);
|
||||
|
||||
block.builders.hydrate.addBlock(deindent`
|
||||
${handlerName} = %events-${handler.name}.call(${component_name}, ${this.var}, function(event) {
|
||||
${handlerBody}
|
||||
});
|
||||
`);
|
||||
|
||||
block.builders.destroy.addLine(deindent`
|
||||
${handlerName}.destroy();
|
||||
`);
|
||||
} else {
|
||||
const handlerFunction = deindent`
|
||||
function ${handlerName}(event) {
|
||||
${handlerBody}
|
||||
}
|
||||
`;
|
||||
|
||||
if (handler.shouldHoist) {
|
||||
renderer.blocks.push(handlerFunction);
|
||||
} else {
|
||||
block.builders.init.addBlock(handlerFunction);
|
||||
}
|
||||
|
||||
block.builders.hydrate.addLine(
|
||||
`@addListener(${this.var}, "${handler.name}", ${handlerName});`
|
||||
);
|
||||
|
||||
block.builders.destroy.addLine(
|
||||
`@removeListener(${this.var}, "${handler.name}", ${handlerName});`
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
addRef(block: Block) {
|
||||
const ref = `#component.refs.${this.node.ref.name}`;
|
||||
|
||||
block.builders.mount.addLine(
|
||||
`${ref} = ${this.var};`
|
||||
);
|
||||
|
||||
block.builders.destroy.addLine(
|
||||
`if (${ref} === ${this.var}) ${ref} = null;`
|
||||
);
|
||||
}
|
||||
|
||||
addTransitions(
|
||||
block: Block
|
||||
) {
|
||||
const { intro, outro } = this.node;
|
||||
|
||||
if (!intro && !outro) return;
|
||||
|
||||
if (intro === outro) {
|
||||
const name = block.getUniqueName(`${this.var}_transition`);
|
||||
const snippet = intro.expression
|
||||
? intro.expression.snippet
|
||||
: '{}';
|
||||
|
||||
block.addVariable(name);
|
||||
|
||||
const fn = `%transitions-${intro.name}`;
|
||||
|
||||
block.builders.intro.addConditional(`#component.root._intro`, deindent`
|
||||
if (${name}) ${name}.invalidate();
|
||||
|
||||
#component.root._aftercreate.push(() => {
|
||||
if (!${name}) ${name} = @wrapTransition(#component, ${this.var}, ${fn}, ${snippet}, true);
|
||||
${name}.run(1);
|
||||
});
|
||||
`);
|
||||
|
||||
block.builders.outro.addBlock(deindent`
|
||||
if (!${name}) ${name} = @wrapTransition(#component, ${this.var}, ${fn}, ${snippet}, false);
|
||||
${name}.run(0, () => {
|
||||
#outrocallback();
|
||||
${name} = null;
|
||||
});
|
||||
`);
|
||||
|
||||
block.builders.destroy.addConditional('detach', `if (${name}) ${name}.abort();`);
|
||||
} else {
|
||||
const introName = intro && block.getUniqueName(`${this.var}_intro`);
|
||||
const outroName = outro && block.getUniqueName(`${this.var}_outro`);
|
||||
|
||||
if (intro) {
|
||||
block.addVariable(introName);
|
||||
const snippet = intro.expression
|
||||
? intro.expression.snippet
|
||||
: '{}';
|
||||
|
||||
const fn = `%transitions-${intro.name}`; // TODO add built-in transitions?
|
||||
|
||||
if (outro) {
|
||||
block.builders.intro.addBlock(deindent`
|
||||
if (${introName}) ${introName}.abort(1);
|
||||
if (${outroName}) ${outroName}.abort(1);
|
||||
`);
|
||||
}
|
||||
|
||||
block.builders.intro.addConditional(`#component.root._intro`, deindent`
|
||||
#component.root._aftercreate.push(() => {
|
||||
${introName} = @wrapTransition(#component, ${this.var}, ${fn}, ${snippet}, true);
|
||||
${introName}.run(1);
|
||||
});
|
||||
`);
|
||||
}
|
||||
|
||||
if (outro) {
|
||||
block.addVariable(outroName);
|
||||
const snippet = outro.expression
|
||||
? outro.expression.snippet
|
||||
: '{}';
|
||||
|
||||
const fn = `%transitions-${outro.name}`;
|
||||
|
||||
block.builders.intro.addBlock(deindent`
|
||||
if (${outroName}) ${outroName}.abort(1);
|
||||
`);
|
||||
|
||||
// TODO hide elements that have outro'd (unless they belong to a still-outroing
|
||||
// group) prior to their removal from the DOM
|
||||
block.builders.outro.addBlock(deindent`
|
||||
${outroName} = @wrapTransition(#component, ${this.var}, ${fn}, ${snippet}, false);
|
||||
${outroName}.run(0, #outrocallback);
|
||||
`);
|
||||
|
||||
block.builders.destroy.addConditional('detach', `if (${outroName}) ${outroName}.abort();`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
addAnimation(block: Block) {
|
||||
if (!this.node.animation) return;
|
||||
|
||||
const rect = block.getUniqueName('rect');
|
||||
const animation = block.getUniqueName('animation');
|
||||
|
||||
block.addVariable(rect);
|
||||
block.addVariable(animation);
|
||||
|
||||
block.builders.measure.addBlock(deindent`
|
||||
${rect} = ${this.var}.getBoundingClientRect();
|
||||
`);
|
||||
|
||||
block.builders.fix.addBlock(deindent`
|
||||
@fixPosition(${this.var});
|
||||
if (${animation}) ${animation}.stop();
|
||||
`);
|
||||
|
||||
const params = this.node.animation.expression ? this.node.animation.expression.snippet : '{}';
|
||||
block.builders.animate.addBlock(deindent`
|
||||
if (${animation}) ${animation}.stop();
|
||||
${animation} = @wrapAnimation(${this.var}, ${rect}, %animations-${this.node.animation.name}, ${params});
|
||||
`);
|
||||
}
|
||||
|
||||
addActions(block: Block) {
|
||||
this.node.actions.forEach(action => {
|
||||
const { expression } = action;
|
||||
let snippet, dependencies;
|
||||
if (expression) {
|
||||
snippet = expression.snippet;
|
||||
dependencies = expression.dependencies;
|
||||
}
|
||||
|
||||
const name = block.getUniqueName(
|
||||
`${action.name.replace(/[^a-zA-Z0-9_$]/g, '_')}_action`
|
||||
);
|
||||
|
||||
block.addVariable(name);
|
||||
const fn = `%actions-${action.name}`;
|
||||
|
||||
block.builders.mount.addLine(
|
||||
`${name} = ${fn}.call(#component, ${this.var}${snippet ? `, ${snippet}` : ''}) || {};`
|
||||
);
|
||||
|
||||
if (dependencies && dependencies.size > 0) {
|
||||
let conditional = `typeof ${name}.update === 'function' && `;
|
||||
const deps = [...dependencies].map(dependency => `changed.${dependency}`).join(' || ');
|
||||
conditional += dependencies.size > 1 ? `(${deps})` : deps;
|
||||
|
||||
block.builders.update.addConditional(
|
||||
conditional,
|
||||
`${name}.update.call(#component, ${snippet});`
|
||||
);
|
||||
}
|
||||
|
||||
block.builders.destroy.addLine(
|
||||
`if (${name} && typeof ${name}.destroy === 'function') ${name}.destroy.call(#component);`
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
addClasses(block: Block) {
|
||||
this.node.classes.forEach(classDir => {
|
||||
const { expression, name } = classDir;
|
||||
let snippet, dependencies;
|
||||
if (expression) {
|
||||
snippet = expression.snippet;
|
||||
dependencies = expression.dependencies;
|
||||
} else {
|
||||
snippet = `ctx${quotePropIfNecessary(name)}`;
|
||||
dependencies = new Set([name]);
|
||||
}
|
||||
const updater = `@toggleClass(${this.var}, "${name}", ${snippet});`;
|
||||
|
||||
block.builders.hydrate.addLine(updater);
|
||||
|
||||
if ((dependencies && dependencies.size > 0) || this.classDependencies.length) {
|
||||
const allDeps = this.classDependencies.concat(...dependencies);
|
||||
const deps = allDeps.map(dependency => `changed${quotePropIfNecessary(dependency)}`).join(' || ');
|
||||
const condition = allDeps.length > 1 ? `(${deps})` : deps;
|
||||
|
||||
block.builders.update.addConditional(
|
||||
condition,
|
||||
updater
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
getStaticAttributeValue(name: string) {
|
||||
const attribute = this.node.attributes.find(
|
||||
(attr: Attribute) => attr.type === 'Attribute' && attr.name.toLowerCase() === name
|
||||
);
|
||||
|
||||
if (!attribute) return null;
|
||||
|
||||
if (attribute.isTrue) return true;
|
||||
if (attribute.chunks.length === 0) return '';
|
||||
|
||||
if (attribute.chunks.length === 1 && attribute.chunks[0].type === 'Text') {
|
||||
return attribute.chunks[0].data;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
isMediaNode() {
|
||||
return this.node.name === 'audio' || this.node.name === 'video';
|
||||
}
|
||||
|
||||
remount(name: string) {
|
||||
const slot = this.attributes.find(attribute => attribute.name === 'slot');
|
||||
if (slot) {
|
||||
const prop = quotePropIfNecessary(slot.chunks[0].data);
|
||||
return `@append(${name}._slotted${prop}, ${this.var});`;
|
||||
}
|
||||
|
||||
return `@append(${name}._slotted.default, ${this.var});`;
|
||||
}
|
||||
|
||||
addCssClass(className = this.component.stylesheet.id) {
|
||||
const classAttribute = this.attributes.find(a => a.name === 'class');
|
||||
if (classAttribute && !classAttribute.isTrue) {
|
||||
if (classAttribute.chunks.length === 1 && classAttribute.chunks[0].type === 'Text') {
|
||||
(<Text>classAttribute.chunks[0]).data += ` ${className}`;
|
||||
} else {
|
||||
(<Node[]>classAttribute.chunks).push(
|
||||
new Text(this.component, this, this.scope, {
|
||||
type: 'Text',
|
||||
data: ` ${className}`
|
||||
})
|
||||
);
|
||||
}
|
||||
} else {
|
||||
this.attributes.push(
|
||||
new Attribute(this.component, this, this.scope, {
|
||||
type: 'Attribute',
|
||||
name: 'class',
|
||||
value: [{ type: 'Text', data: className }]
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,140 @@
|
||||
import Wrapper from './shared/Wrapper';
|
||||
import AwaitBlock from './AwaitBlock';
|
||||
import DebugTag from './DebugTag';
|
||||
import EachBlock from './EachBlock';
|
||||
import Element from './Element';
|
||||
import Head from './Head';
|
||||
import IfBlock from './IfBlock';
|
||||
import InlineComponent from './InlineComponent';
|
||||
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 '../../nodes/shared/Node';
|
||||
import { trimStart, trimEnd } from '../../../utils/trim';
|
||||
import TextWrapper from './Text';
|
||||
import Renderer from '../Renderer';
|
||||
import Block from '../Block';
|
||||
|
||||
const wrappers = {
|
||||
AwaitBlock,
|
||||
Comment: null,
|
||||
DebugTag,
|
||||
EachBlock,
|
||||
Element,
|
||||
Head,
|
||||
IfBlock,
|
||||
InlineComponent,
|
||||
MustacheTag,
|
||||
RawMustacheTag,
|
||||
Slot,
|
||||
Text,
|
||||
Title,
|
||||
Window
|
||||
};
|
||||
|
||||
function link(next: Wrapper, prev: Wrapper) {
|
||||
prev.next = next;
|
||||
if (next) next.prev = prev;
|
||||
}
|
||||
|
||||
export default class FragmentWrapper {
|
||||
nodes: Wrapper[];
|
||||
|
||||
constructor(
|
||||
renderer: Renderer,
|
||||
block: Block,
|
||||
nodes: Node[],
|
||||
parent: Wrapper,
|
||||
stripWhitespace: boolean,
|
||||
nextSibling: Wrapper
|
||||
) {
|
||||
this.nodes = [];
|
||||
|
||||
let lastChild: Wrapper;
|
||||
let windowWrapper;
|
||||
|
||||
let i = nodes.length;
|
||||
while (i--) {
|
||||
const child = nodes[i];
|
||||
|
||||
if (!(child.type in wrappers)) {
|
||||
throw new Error(`TODO implement ${child.type}`);
|
||||
}
|
||||
|
||||
// special case — this is an easy way to remove whitespace surrounding
|
||||
// <svelte:window/>. lil hacky but it works
|
||||
if (child.type === 'Window') {
|
||||
windowWrapper = new Window(renderer, block, parent, child);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (child.type === 'Text') {
|
||||
let { data } = child;
|
||||
|
||||
// We want to remove trailing whitespace inside an element/component/block,
|
||||
// *unless* there is no whitespace between this node and its next sibling
|
||||
if (this.nodes.length === 0) {
|
||||
const shouldTrim = (
|
||||
nextSibling ? (nextSibling.node.type === 'Text' && /^\s/.test(nextSibling.data)) : !child.hasAncestor('EachBlock')
|
||||
);
|
||||
|
||||
if (shouldTrim) {
|
||||
data = trimEnd(data);
|
||||
if (!data) continue;
|
||||
}
|
||||
}
|
||||
|
||||
// glue text nodes (which could e.g. be separated by comments) together
|
||||
if (lastChild && lastChild.node.type === 'Text') {
|
||||
lastChild.data = data + lastChild.data;
|
||||
continue;
|
||||
}
|
||||
|
||||
const wrapper = new TextWrapper(renderer, block, parent, child, data);
|
||||
if (wrapper.skip) continue;
|
||||
|
||||
this.nodes.unshift(wrapper);
|
||||
|
||||
link(lastChild, lastChild = wrapper);
|
||||
} else {
|
||||
const Wrapper = wrappers[child.type];
|
||||
if (!Wrapper) continue;
|
||||
|
||||
const wrapper = new Wrapper(renderer, block, parent, child, stripWhitespace, lastChild || nextSibling);
|
||||
this.nodes.unshift(wrapper);
|
||||
|
||||
link(lastChild, lastChild = wrapper);
|
||||
}
|
||||
}
|
||||
|
||||
if (stripWhitespace) {
|
||||
const first = <TextWrapper>this.nodes[0];
|
||||
|
||||
if (first && first.node.type === 'Text') {
|
||||
first.data = trimStart(first.data);
|
||||
if (!first.data) {
|
||||
first.var = null;
|
||||
this.nodes.shift();
|
||||
|
||||
if (this.nodes[0]) {
|
||||
this.nodes[0].prev = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (windowWrapper) {
|
||||
this.nodes.unshift(windowWrapper);
|
||||
link(lastChild, windowWrapper);
|
||||
}
|
||||
}
|
||||
|
||||
render(block: Block, parentNode: string, parentNodes: string) {
|
||||
for (let i = 0; i < this.nodes.length; i += 1) {
|
||||
this.nodes[i].render(block, parentNode, parentNodes);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,33 @@
|
||||
import Wrapper from './shared/Wrapper';
|
||||
import Renderer from '../Renderer';
|
||||
import Block from '../Block';
|
||||
import Head from '../../nodes/Head';
|
||||
import FragmentWrapper from './Fragment';
|
||||
|
||||
export default class HeadWrapper extends Wrapper {
|
||||
fragment: FragmentWrapper;
|
||||
|
||||
constructor(
|
||||
renderer: Renderer,
|
||||
block: Block,
|
||||
parent: Wrapper,
|
||||
node: Head,
|
||||
stripWhitespace: boolean,
|
||||
nextSibling: Wrapper
|
||||
) {
|
||||
super(renderer, block, parent, node);
|
||||
|
||||
this.fragment = new FragmentWrapper(
|
||||
renderer,
|
||||
block,
|
||||
node.children,
|
||||
this,
|
||||
stripWhitespace,
|
||||
nextSibling
|
||||
);
|
||||
}
|
||||
|
||||
render(block: Block, parentNode: string, parentNodes: string) {
|
||||
this.fragment.render(block, 'document.head', null);
|
||||
}
|
||||
}
|
@ -0,0 +1,473 @@
|
||||
import Wrapper from './shared/Wrapper';
|
||||
import Renderer from '../Renderer';
|
||||
import Block from '../Block';
|
||||
import EachBlock from '../../nodes/EachBlock';
|
||||
import IfBlock from '../../nodes/IfBlock';
|
||||
import createDebuggingComment from '../../../utils/createDebuggingComment';
|
||||
import ElseBlock from '../../nodes/ElseBlock';
|
||||
import FragmentWrapper from './Fragment';
|
||||
import deindent from '../../../utils/deindent';
|
||||
|
||||
function isElseIf(node: ElseBlock) {
|
||||
return (
|
||||
node && node.children.length === 1 && node.children[0].type === 'IfBlock'
|
||||
);
|
||||
}
|
||||
|
||||
class IfBlockBranch extends Wrapper {
|
||||
block: Block;
|
||||
fragment: FragmentWrapper;
|
||||
condition: string;
|
||||
isDynamic: boolean;
|
||||
|
||||
var = null;
|
||||
|
||||
constructor(
|
||||
renderer: Renderer,
|
||||
block: Block,
|
||||
parent: IfBlockWrapper,
|
||||
node: IfBlock | ElseBlock,
|
||||
stripWhitespace: boolean,
|
||||
nextSibling: Wrapper
|
||||
) {
|
||||
super(renderer, block, parent, node);
|
||||
|
||||
this.condition = (<IfBlock>node).expression && (<IfBlock>node).expression.snippet;
|
||||
|
||||
this.block = block.child({
|
||||
comment: createDebuggingComment(node, parent.renderer.component),
|
||||
name: parent.renderer.component.getUniqueName(
|
||||
(<IfBlock>node).expression ? `create_if_block` : `create_else_block`
|
||||
)
|
||||
});
|
||||
|
||||
this.fragment = new FragmentWrapper(renderer, this.block, node.children, parent, stripWhitespace, nextSibling);
|
||||
|
||||
this.isDynamic = this.block.dependencies.size > 0;
|
||||
}
|
||||
}
|
||||
|
||||
export default class IfBlockWrapper extends Wrapper {
|
||||
node: IfBlock;
|
||||
branches: IfBlockBranch[];
|
||||
|
||||
var = 'if_block';
|
||||
|
||||
constructor(
|
||||
renderer: Renderer,
|
||||
block: Block,
|
||||
parent: Wrapper,
|
||||
node: EachBlock,
|
||||
stripWhitespace: boolean,
|
||||
nextSibling: Wrapper
|
||||
) {
|
||||
super(renderer, block, parent, node);
|
||||
|
||||
const { component } = renderer;
|
||||
|
||||
this.cannotUseInnerHTML();
|
||||
|
||||
this.branches = [];
|
||||
|
||||
const blocks: Block[] = [];
|
||||
let isDynamic = false;
|
||||
let hasIntros = false;
|
||||
let hasOutros = false;
|
||||
|
||||
const createBranches = (node: IfBlock) => {
|
||||
const branch = new IfBlockBranch(
|
||||
renderer,
|
||||
block,
|
||||
this,
|
||||
node,
|
||||
stripWhitespace,
|
||||
nextSibling
|
||||
);
|
||||
|
||||
this.branches.push(branch);
|
||||
|
||||
blocks.push(branch.block);
|
||||
block.addDependencies(node.expression.dependencies);
|
||||
|
||||
if (branch.block.dependencies.size > 0) {
|
||||
isDynamic = true;
|
||||
block.addDependencies(branch.block.dependencies);
|
||||
}
|
||||
|
||||
if (branch.block.hasIntros) hasIntros = true;
|
||||
if (branch.block.hasOutros) hasOutros = true;
|
||||
|
||||
if (isElseIf(node.else)) {
|
||||
createBranches(node.else.children[0]);
|
||||
} else if (node.else) {
|
||||
const branch = new IfBlockBranch(
|
||||
renderer,
|
||||
block,
|
||||
this,
|
||||
node.else,
|
||||
stripWhitespace,
|
||||
nextSibling
|
||||
);
|
||||
|
||||
this.branches.push(branch);
|
||||
|
||||
blocks.push(branch.block);
|
||||
|
||||
if (branch.block.dependencies.size > 0) {
|
||||
isDynamic = true;
|
||||
block.addDependencies(branch.block.dependencies);
|
||||
}
|
||||
|
||||
if (branch.block.hasIntros) hasIntros = true;
|
||||
if (branch.block.hasOutros) hasOutros = true;
|
||||
}
|
||||
};
|
||||
|
||||
createBranches(this.node);
|
||||
|
||||
if (component.options.nestedTransitions) {
|
||||
if (hasIntros) block.addIntro();
|
||||
if (hasOutros) block.addOutro();
|
||||
}
|
||||
|
||||
blocks.forEach(block => {
|
||||
block.hasUpdateMethod = isDynamic;
|
||||
block.hasIntroMethod = hasIntros;
|
||||
block.hasOutroMethod = hasOutros;
|
||||
});
|
||||
|
||||
renderer.blocks.push(...blocks);
|
||||
}
|
||||
|
||||
render(
|
||||
block: Block,
|
||||
parentNode: string,
|
||||
parentNodes: string
|
||||
) {
|
||||
const name = this.var;
|
||||
|
||||
const needsAnchor = this.next ? !this.next.isDomNode() : !parentNode || !this.parent.isDomNode();
|
||||
const anchor = needsAnchor
|
||||
? block.getUniqueName(`${name}_anchor`)
|
||||
: (this.next && this.next.var) || 'null';
|
||||
|
||||
const hasElse = !(this.branches[this.branches.length - 1].condition);
|
||||
const if_name = hasElse ? '' : `if (${name}) `;
|
||||
|
||||
const dynamic = this.branches[0].block.hasUpdateMethod; // can use [0] as proxy for all, since they necessarily have the same value
|
||||
const hasOutros = this.branches[0].block.hasOutroMethod;
|
||||
|
||||
const vars = { name, anchor, if_name, hasElse };
|
||||
|
||||
if (this.node.else) {
|
||||
if (hasOutros) {
|
||||
this.renderCompoundWithOutros(block, parentNode, parentNodes, dynamic, vars);
|
||||
|
||||
if (this.renderer.options.nestedTransitions) {
|
||||
block.builders.outro.addBlock(deindent`
|
||||
if (${name}) ${name}.o(#outrocallback);
|
||||
else #outrocallback();
|
||||
`);
|
||||
}
|
||||
} else {
|
||||
this.renderCompound(block, parentNode, parentNodes, dynamic, vars);
|
||||
}
|
||||
} else {
|
||||
this.renderSimple(block, parentNode, parentNodes, dynamic, vars);
|
||||
|
||||
if (hasOutros && this.renderer.options.nestedTransitions) {
|
||||
block.builders.outro.addBlock(deindent`
|
||||
if (${name}) ${name}.o(#outrocallback);
|
||||
else #outrocallback();
|
||||
`);
|
||||
}
|
||||
}
|
||||
|
||||
block.builders.create.addLine(`${if_name}${name}.c();`);
|
||||
|
||||
if (parentNodes) {
|
||||
block.builders.claim.addLine(
|
||||
`${if_name}${name}.l(${parentNodes});`
|
||||
);
|
||||
}
|
||||
|
||||
if (needsAnchor) {
|
||||
block.addElement(
|
||||
anchor,
|
||||
`@createComment()`,
|
||||
parentNodes && `@createComment()`,
|
||||
parentNode
|
||||
);
|
||||
}
|
||||
|
||||
this.branches.forEach(branch => {
|
||||
branch.fragment.render(branch.block, null, 'nodes');
|
||||
});
|
||||
}
|
||||
|
||||
renderCompound(
|
||||
block: Block,
|
||||
parentNode: string,
|
||||
parentNodes: string,
|
||||
dynamic,
|
||||
{ name, anchor, hasElse, if_name }
|
||||
) {
|
||||
const select_block_type = this.renderer.component.getUniqueName(`select_block_type`);
|
||||
const current_block_type = block.getUniqueName(`current_block_type`);
|
||||
const current_block_type_and = hasElse ? '' : `${current_block_type} && `;
|
||||
|
||||
block.builders.init.addBlock(deindent`
|
||||
function ${select_block_type}(ctx) {
|
||||
${this.branches
|
||||
.map(({ condition, block }) => `${condition ? `if (${condition}) ` : ''}return ${block.name};`)
|
||||
.join('\n')}
|
||||
}
|
||||
`);
|
||||
|
||||
block.builders.init.addBlock(deindent`
|
||||
var ${current_block_type} = ${select_block_type}(ctx);
|
||||
var ${name} = ${current_block_type_and}${current_block_type}(#component, ctx);
|
||||
`);
|
||||
|
||||
const mountOrIntro = this.branches[0].block.hasIntroMethod ? 'i' : 'm';
|
||||
|
||||
const initialMountNode = parentNode || '#target';
|
||||
const anchorNode = parentNode ? 'null' : 'anchor';
|
||||
block.builders.mount.addLine(
|
||||
`${if_name}${name}.${mountOrIntro}(${initialMountNode}, ${anchorNode});`
|
||||
);
|
||||
|
||||
const updateMountNode = this.getUpdateMountNode(anchor);
|
||||
|
||||
const changeBlock = deindent`
|
||||
${if_name}${name}.d(1);
|
||||
${name} = ${current_block_type_and}${current_block_type}(#component, ctx);
|
||||
${if_name}${name}.c();
|
||||
${if_name}${name}.${mountOrIntro}(${updateMountNode}, ${anchor});
|
||||
`;
|
||||
|
||||
if (dynamic) {
|
||||
block.builders.update.addBlock(deindent`
|
||||
if (${current_block_type} === (${current_block_type} = ${select_block_type}(ctx)) && ${name}) {
|
||||
${name}.p(changed, ctx);
|
||||
} else {
|
||||
${changeBlock}
|
||||
}
|
||||
`);
|
||||
} else {
|
||||
block.builders.update.addBlock(deindent`
|
||||
if (${current_block_type} !== (${current_block_type} = ${select_block_type}(ctx))) {
|
||||
${changeBlock}
|
||||
}
|
||||
`);
|
||||
}
|
||||
|
||||
block.builders.destroy.addLine(`${if_name}${name}.d(${parentNode ? '' : 'detach'});`);
|
||||
}
|
||||
|
||||
// if any of the siblings have outros, we need to keep references to the blocks
|
||||
// (TODO does this only apply to bidi transitions?)
|
||||
renderCompoundWithOutros(
|
||||
block: Block,
|
||||
parentNode: string,
|
||||
parentNodes: string,
|
||||
dynamic,
|
||||
{ name, anchor, hasElse }
|
||||
) {
|
||||
const select_block_type = this.renderer.component.getUniqueName(`select_block_type`);
|
||||
const current_block_type_index = block.getUniqueName(`current_block_type_index`);
|
||||
const previous_block_index = block.getUniqueName(`previous_block_index`);
|
||||
const if_block_creators = block.getUniqueName(`if_block_creators`);
|
||||
const if_blocks = block.getUniqueName(`if_blocks`);
|
||||
|
||||
const if_current_block_type_index = hasElse
|
||||
? ''
|
||||
: `if (~${current_block_type_index}) `;
|
||||
|
||||
block.addVariable(current_block_type_index);
|
||||
block.addVariable(name);
|
||||
|
||||
block.builders.init.addBlock(deindent`
|
||||
var ${if_block_creators} = [
|
||||
${this.branches.map(branch => branch.block.name).join(',\n')}
|
||||
];
|
||||
|
||||
var ${if_blocks} = [];
|
||||
|
||||
function ${select_block_type}(ctx) {
|
||||
${this.branches
|
||||
.map(({ condition }, i) => `${condition ? `if (${condition}) ` : ''}return ${i};`)
|
||||
.join('\n')}
|
||||
${!hasElse && `return -1;`}
|
||||
}
|
||||
`);
|
||||
|
||||
if (hasElse) {
|
||||
block.builders.init.addBlock(deindent`
|
||||
${current_block_type_index} = ${select_block_type}(ctx);
|
||||
${name} = ${if_blocks}[${current_block_type_index}] = ${if_block_creators}[${current_block_type_index}](#component, ctx);
|
||||
`);
|
||||
} else {
|
||||
block.builders.init.addBlock(deindent`
|
||||
if (~(${current_block_type_index} = ${select_block_type}(ctx))) {
|
||||
${name} = ${if_blocks}[${current_block_type_index}] = ${if_block_creators}[${current_block_type_index}](#component, ctx);
|
||||
}
|
||||
`);
|
||||
}
|
||||
|
||||
const mountOrIntro = this.branches[0].block.hasIntroMethod ? 'i' : 'm';
|
||||
const initialMountNode = parentNode || '#target';
|
||||
const anchorNode = parentNode ? 'null' : 'anchor';
|
||||
|
||||
block.builders.mount.addLine(
|
||||
`${if_current_block_type_index}${if_blocks}[${current_block_type_index}].${mountOrIntro}(${initialMountNode}, ${anchorNode});`
|
||||
);
|
||||
|
||||
const updateMountNode = this.getUpdateMountNode(anchor);
|
||||
|
||||
const destroyOldBlock = deindent`
|
||||
@groupOutros();
|
||||
${name}.o(function() {
|
||||
${if_blocks}[${previous_block_index}].d(1);
|
||||
${if_blocks}[${previous_block_index}] = null;
|
||||
});
|
||||
`;
|
||||
|
||||
const createNewBlock = deindent`
|
||||
${name} = ${if_blocks}[${current_block_type_index}];
|
||||
if (!${name}) {
|
||||
${name} = ${if_blocks}[${current_block_type_index}] = ${if_block_creators}[${current_block_type_index}](#component, ctx);
|
||||
${name}.c();
|
||||
}
|
||||
${name}.${mountOrIntro}(${updateMountNode}, ${anchor});
|
||||
`;
|
||||
|
||||
const changeBlock = hasElse
|
||||
? deindent`
|
||||
${destroyOldBlock}
|
||||
|
||||
${createNewBlock}
|
||||
`
|
||||
: deindent`
|
||||
if (${name}) {
|
||||
${destroyOldBlock}
|
||||
}
|
||||
|
||||
if (~${current_block_type_index}) {
|
||||
${createNewBlock}
|
||||
} else {
|
||||
${name} = null;
|
||||
}
|
||||
`;
|
||||
|
||||
if (dynamic) {
|
||||
block.builders.update.addBlock(deindent`
|
||||
var ${previous_block_index} = ${current_block_type_index};
|
||||
${current_block_type_index} = ${select_block_type}(ctx);
|
||||
if (${current_block_type_index} === ${previous_block_index}) {
|
||||
${if_current_block_type_index}${if_blocks}[${current_block_type_index}].p(changed, ctx);
|
||||
} else {
|
||||
${changeBlock}
|
||||
}
|
||||
`);
|
||||
} else {
|
||||
block.builders.update.addBlock(deindent`
|
||||
var ${previous_block_index} = ${current_block_type_index};
|
||||
${current_block_type_index} = ${select_block_type}(ctx);
|
||||
if (${current_block_type_index} !== ${previous_block_index}) {
|
||||
${changeBlock}
|
||||
}
|
||||
`);
|
||||
}
|
||||
|
||||
block.builders.destroy.addLine(deindent`
|
||||
${if_current_block_type_index}${if_blocks}[${current_block_type_index}].d(${parentNode ? '' : 'detach'});
|
||||
`);
|
||||
}
|
||||
|
||||
renderSimple(
|
||||
block: Block,
|
||||
parentNode: string,
|
||||
parentNodes: string,
|
||||
dynamic,
|
||||
{ name, anchor, if_name }
|
||||
) {
|
||||
const branch = this.branches[0];
|
||||
|
||||
block.builders.init.addBlock(deindent`
|
||||
var ${name} = (${branch.condition}) && ${branch.block.name}(#component, ctx);
|
||||
`);
|
||||
|
||||
const mountOrIntro = branch.block.hasIntroMethod ? 'i' : 'm';
|
||||
const initialMountNode = parentNode || '#target';
|
||||
const anchorNode = parentNode ? 'null' : 'anchor';
|
||||
|
||||
block.builders.mount.addLine(
|
||||
`if (${name}) ${name}.${mountOrIntro}(${initialMountNode}, ${anchorNode});`
|
||||
);
|
||||
|
||||
const updateMountNode = this.getUpdateMountNode(anchor);
|
||||
|
||||
const enter = dynamic
|
||||
? (branch.block.hasIntroMethod || branch.block.hasOutroMethod)
|
||||
? deindent`
|
||||
if (${name}) {
|
||||
${name}.p(changed, ctx);
|
||||
} else {
|
||||
${name} = ${branch.block.name}(#component, ctx);
|
||||
if (${name}) ${name}.c();
|
||||
}
|
||||
|
||||
${name}.i(${updateMountNode}, ${anchor});
|
||||
`
|
||||
: deindent`
|
||||
if (${name}) {
|
||||
${name}.p(changed, ctx);
|
||||
} else {
|
||||
${name} = ${branch.block.name}(#component, ctx);
|
||||
${name}.c();
|
||||
${name}.m(${updateMountNode}, ${anchor});
|
||||
}
|
||||
`
|
||||
: (branch.block.hasIntroMethod || branch.block.hasOutroMethod)
|
||||
? deindent`
|
||||
if (!${name}) {
|
||||
${name} = ${branch.block.name}(#component, ctx);
|
||||
${name}.c();
|
||||
}
|
||||
${name}.i(${updateMountNode}, ${anchor});
|
||||
`
|
||||
: deindent`
|
||||
if (!${name}) {
|
||||
${name} = ${branch.block.name}(#component, ctx);
|
||||
${name}.c();
|
||||
${name}.m(${updateMountNode}, ${anchor});
|
||||
}
|
||||
`;
|
||||
|
||||
// no `p()` here — we don't want to update outroing nodes,
|
||||
// as that will typically result in glitching
|
||||
const exit = branch.block.hasOutroMethod
|
||||
? deindent`
|
||||
@groupOutros();
|
||||
${name}.o(function() {
|
||||
${name}.d(1);
|
||||
${name} = null;
|
||||
});
|
||||
`
|
||||
: deindent`
|
||||
${name}.d(1);
|
||||
${name} = null;
|
||||
`;
|
||||
|
||||
block.builders.update.addBlock(deindent`
|
||||
if (${branch.condition}) {
|
||||
${enter}
|
||||
} else if (${name}) {
|
||||
${exit}
|
||||
}
|
||||
`);
|
||||
|
||||
block.builders.destroy.addLine(`${if_name}${name}.d(${parentNode ? '' : 'detach'});`);
|
||||
}
|
||||
}
|
@ -0,0 +1,478 @@
|
||||
import Wrapper from '../shared/Wrapper';
|
||||
import Renderer from '../../Renderer';
|
||||
import Block from '../../Block';
|
||||
import Node from '../../../nodes/shared/Node';
|
||||
import InlineComponent from '../../../nodes/InlineComponent';
|
||||
import FragmentWrapper from '../Fragment';
|
||||
import { quoteNameIfNecessary, quotePropIfNecessary } from '../../../../utils/quoteIfNecessary';
|
||||
import stringifyProps from '../../../../utils/stringifyProps';
|
||||
import addToSet from '../../../../utils/addToSet';
|
||||
import deindent from '../../../../utils/deindent';
|
||||
import Attribute from '../../../nodes/Attribute';
|
||||
import CodeBuilder from '../../../../utils/CodeBuilder';
|
||||
import getObject from '../../../../utils/getObject';
|
||||
import Binding from '../../../nodes/Binding';
|
||||
import getTailSnippet from '../../../../utils/getTailSnippet';
|
||||
|
||||
export default class InlineComponentWrapper extends Wrapper {
|
||||
var: string;
|
||||
_slots: Set<string>; // TODO lose the underscore
|
||||
node: InlineComponent;
|
||||
fragment: FragmentWrapper;
|
||||
|
||||
constructor(
|
||||
renderer: Renderer,
|
||||
block: Block,
|
||||
parent: Wrapper,
|
||||
node: InlineComponent,
|
||||
stripWhitespace: boolean,
|
||||
nextSibling: Wrapper
|
||||
) {
|
||||
super(renderer, block, parent, node);
|
||||
|
||||
this.cannotUseInnerHTML();
|
||||
|
||||
if (this.node.expression) {
|
||||
block.addDependencies(this.node.expression.dependencies);
|
||||
}
|
||||
|
||||
this.node.attributes.forEach(attr => {
|
||||
block.addDependencies(attr.dependencies);
|
||||
});
|
||||
|
||||
this.node.bindings.forEach(binding => {
|
||||
block.addDependencies(binding.value.dependencies);
|
||||
});
|
||||
|
||||
this.node.handlers.forEach(handler => {
|
||||
block.addDependencies(handler.dependencies);
|
||||
});
|
||||
|
||||
this.var = (
|
||||
this.node.name === 'svelte:self' ? renderer.component.name :
|
||||
this.node.name === 'svelte:component' ? 'switch_instance' :
|
||||
this.node.name
|
||||
).toLowerCase();
|
||||
|
||||
if (this.node.children.length) {
|
||||
this._slots = new Set(['default']);
|
||||
this.fragment = new FragmentWrapper(renderer, block, node.children, this, stripWhitespace, nextSibling);
|
||||
}
|
||||
|
||||
if (renderer.component.options.nestedTransitions) {
|
||||
block.addOutro();
|
||||
}
|
||||
}
|
||||
|
||||
render(
|
||||
block: Block,
|
||||
parentNode: string,
|
||||
parentNodes: string
|
||||
) {
|
||||
const { renderer } = this;
|
||||
const { component } = renderer;
|
||||
|
||||
const name = this.var;
|
||||
|
||||
const componentInitProperties = [
|
||||
`root: #component.root`,
|
||||
`store: #component.store`
|
||||
];
|
||||
|
||||
if (this.fragment) {
|
||||
const slots = Array.from(this._slots).map(name => `${quoteNameIfNecessary(name)}: @createFragment()`);
|
||||
componentInitProperties.push(`slots: { ${slots.join(', ')} }`);
|
||||
|
||||
this.fragment.nodes.forEach((child: Wrapper) => {
|
||||
child.render(block, `${this.var}._slotted.default`, 'nodes');
|
||||
});
|
||||
}
|
||||
|
||||
const statements: string[] = [];
|
||||
|
||||
const name_initial_data = block.getUniqueName(`${name}_initial_data`);
|
||||
const name_changes = block.getUniqueName(`${name}_changes`);
|
||||
let name_updating: string;
|
||||
let beforecreate: string = null;
|
||||
|
||||
const updates: string[] = [];
|
||||
|
||||
const usesSpread = !!this.node.attributes.find(a => a.isSpread);
|
||||
|
||||
const attributeObject = usesSpread
|
||||
? '{}'
|
||||
: stringifyProps(
|
||||
this.node.attributes.map(attr => `${quoteNameIfNecessary(attr.name)}: ${attr.getValue()}`)
|
||||
);
|
||||
|
||||
if (this.node.attributes.length || this.node.bindings.length) {
|
||||
componentInitProperties.push(`data: ${name_initial_data}`);
|
||||
}
|
||||
|
||||
if (!usesSpread && (this.node.attributes.filter(a => a.isDynamic).length || this.node.bindings.length)) {
|
||||
updates.push(`var ${name_changes} = {};`);
|
||||
}
|
||||
|
||||
if (this.node.attributes.length) {
|
||||
if (usesSpread) {
|
||||
const levels = block.getUniqueName(`${this.var}_spread_levels`);
|
||||
|
||||
const initialProps = [];
|
||||
const changes = [];
|
||||
|
||||
const allDependencies = new Set();
|
||||
|
||||
this.node.attributes.forEach(attr => {
|
||||
addToSet(allDependencies, attr.dependencies);
|
||||
});
|
||||
|
||||
this.node.attributes.forEach(attr => {
|
||||
const { name, dependencies } = attr;
|
||||
|
||||
const condition = dependencies.size > 0 && (dependencies.size !== allDependencies.size)
|
||||
? `(${[...dependencies].map(d => `changed.${d}`).join(' || ')})`
|
||||
: null;
|
||||
|
||||
if (attr.isSpread) {
|
||||
const value = attr.expression.snippet;
|
||||
initialProps.push(value);
|
||||
|
||||
changes.push(condition ? `${condition} && ${value}` : value);
|
||||
} else {
|
||||
const obj = `{ ${quoteNameIfNecessary(name)}: ${attr.getValue()} }`;
|
||||
initialProps.push(obj);
|
||||
|
||||
changes.push(condition ? `${condition} && ${obj}` : obj);
|
||||
}
|
||||
});
|
||||
|
||||
block.builders.init.addBlock(deindent`
|
||||
var ${levels} = [
|
||||
${initialProps.join(',\n')}
|
||||
];
|
||||
`);
|
||||
|
||||
statements.push(deindent`
|
||||
for (var #i = 0; #i < ${levels}.length; #i += 1) {
|
||||
${name_initial_data} = @assign(${name_initial_data}, ${levels}[#i]);
|
||||
}
|
||||
`);
|
||||
|
||||
const conditions = [...allDependencies].map(dep => `changed.${dep}`).join(' || ');
|
||||
|
||||
updates.push(deindent`
|
||||
var ${name_changes} = ${allDependencies.size === 1 ? `${conditions}` : `(${conditions})`} ? @getSpreadUpdate(${levels}, [
|
||||
${changes.join(',\n')}
|
||||
]) : {};
|
||||
`);
|
||||
} else {
|
||||
this.node.attributes
|
||||
.filter((attribute: Attribute) => attribute.isDynamic)
|
||||
.forEach((attribute: Attribute) => {
|
||||
if (attribute.dependencies.size > 0) {
|
||||
updates.push(deindent`
|
||||
if (${[...attribute.dependencies]
|
||||
.map(dependency => `changed.${dependency}`)
|
||||
.join(' || ')}) ${name_changes}${quotePropIfNecessary(attribute.name)} = ${attribute.getValue()};
|
||||
`);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (this.node.bindings.length) {
|
||||
renderer.hasComplexBindings = true;
|
||||
|
||||
name_updating = block.alias(`${name}_updating`);
|
||||
block.addVariable(name_updating, '{}');
|
||||
|
||||
let hasLocalBindings = false;
|
||||
let hasStoreBindings = false;
|
||||
|
||||
const builder = new CodeBuilder();
|
||||
|
||||
this.node.bindings.forEach((binding: Binding) => {
|
||||
let { name: key } = getObject(binding.value.node);
|
||||
|
||||
let setFromChild;
|
||||
|
||||
if (binding.isContextual) {
|
||||
const computed = isComputed(binding.value.node);
|
||||
const tail = binding.value.node.type === 'MemberExpression' ? getTailSnippet(binding.value.node) : '';
|
||||
|
||||
const head = block.bindings.get(key);
|
||||
|
||||
const lhs = binding.value.node.type === 'MemberExpression'
|
||||
? binding.value.snippet
|
||||
: `${head()}${tail} = childState${quotePropIfNecessary(binding.name)}`;
|
||||
|
||||
setFromChild = deindent`
|
||||
${lhs} = childState${quotePropIfNecessary(binding.name)};
|
||||
|
||||
${[...binding.value.dependencies]
|
||||
.map((name: string) => {
|
||||
const isStoreProp = name[0] === '$';
|
||||
const prop = isStoreProp ? name.slice(1) : name;
|
||||
const newState = isStoreProp ? 'newStoreState' : 'newState';
|
||||
|
||||
if (isStoreProp) hasStoreBindings = true;
|
||||
else hasLocalBindings = true;
|
||||
|
||||
return `${newState}${quotePropIfNecessary(prop)} = ctx${quotePropIfNecessary(name)};`;
|
||||
})}
|
||||
`;
|
||||
}
|
||||
|
||||
else {
|
||||
const isStoreProp = key[0] === '$';
|
||||
const prop = isStoreProp ? key.slice(1) : key;
|
||||
const newState = isStoreProp ? 'newStoreState' : 'newState';
|
||||
|
||||
if (isStoreProp) hasStoreBindings = true;
|
||||
else hasLocalBindings = true;
|
||||
|
||||
if (binding.value.node.type === 'MemberExpression') {
|
||||
setFromChild = deindent`
|
||||
${binding.value.snippet} = childState${quotePropIfNecessary(binding.name)};
|
||||
${newState}${quotePropIfNecessary(prop)} = ctx${quotePropIfNecessary(key)};
|
||||
`;
|
||||
}
|
||||
|
||||
else {
|
||||
setFromChild = `${newState}${quotePropIfNecessary(prop)} = childState${quotePropIfNecessary(binding.name)};`;
|
||||
}
|
||||
}
|
||||
|
||||
statements.push(deindent`
|
||||
if (${binding.value.snippet} !== void 0) {
|
||||
${name_initial_data}${quotePropIfNecessary(binding.name)} = ${binding.value.snippet};
|
||||
${name_updating}${quotePropIfNecessary(binding.name)} = true;
|
||||
}`
|
||||
);
|
||||
|
||||
builder.addConditional(
|
||||
`!${name_updating}${quotePropIfNecessary(binding.name)} && changed${quotePropIfNecessary(binding.name)}`,
|
||||
setFromChild
|
||||
);
|
||||
|
||||
updates.push(deindent`
|
||||
if (!${name_updating}${quotePropIfNecessary(binding.name)} && ${[...binding.value.dependencies].map((dependency: string) => `changed.${dependency}`).join(' || ')}) {
|
||||
${name_changes}${quotePropIfNecessary(binding.name)} = ${binding.value.snippet};
|
||||
${name_updating}${quotePropIfNecessary(binding.name)} = ${binding.value.snippet} !== void 0;
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
block.maintainContext = true; // TODO put this somewhere more logical
|
||||
|
||||
const initialisers = [
|
||||
hasLocalBindings && 'newState = {}',
|
||||
hasStoreBindings && 'newStoreState = {}',
|
||||
].filter(Boolean).join(', ');
|
||||
|
||||
// TODO use component.on('state', ...) instead of _bind
|
||||
componentInitProperties.push(deindent`
|
||||
_bind(changed, childState) {
|
||||
var ${initialisers};
|
||||
${builder}
|
||||
${hasStoreBindings && `#component.store.set(newStoreState);`}
|
||||
${hasLocalBindings && `#component._set(newState);`}
|
||||
${name_updating} = {};
|
||||
}
|
||||
`);
|
||||
|
||||
beforecreate = deindent`
|
||||
#component.root._beforecreate.push(() => {
|
||||
${name}._bind({ ${this.node.bindings.map(b => `${quoteNameIfNecessary(b.name)}: 1`).join(', ')} }, ${name}.get());
|
||||
});
|
||||
`;
|
||||
}
|
||||
|
||||
this.node.handlers.forEach(handler => {
|
||||
handler.var = block.getUniqueName(`${this.var}_${handler.name}`); // TODO this is hacky
|
||||
handler.render(component, block, this.var, false); // TODO hoist when possible
|
||||
if (handler.usesContext) block.maintainContext = true; // TODO is there a better place to put this?
|
||||
});
|
||||
|
||||
if (this.node.name === 'svelte:component') {
|
||||
const switch_value = block.getUniqueName('switch_value');
|
||||
const switch_props = block.getUniqueName('switch_props');
|
||||
|
||||
const { snippet } = this.node.expression;
|
||||
|
||||
block.builders.init.addBlock(deindent`
|
||||
var ${switch_value} = ${snippet};
|
||||
|
||||
function ${switch_props}(ctx) {
|
||||
${(this.node.attributes.length || this.node.bindings.length) && deindent`
|
||||
var ${name_initial_data} = ${attributeObject};`}
|
||||
${statements}
|
||||
return {
|
||||
${componentInitProperties.join(',\n')}
|
||||
};
|
||||
}
|
||||
|
||||
if (${switch_value}) {
|
||||
var ${name} = new ${switch_value}(${switch_props}(ctx));
|
||||
|
||||
${beforecreate}
|
||||
}
|
||||
|
||||
${this.node.handlers.map(handler => deindent`
|
||||
function ${handler.var}(event) {
|
||||
${handler.snippet || `#component.fire("${handler.name}", event);`}
|
||||
}
|
||||
|
||||
if (${name}) ${name}.on("${handler.name}", ${handler.var});
|
||||
`)}
|
||||
`);
|
||||
|
||||
block.builders.create.addLine(
|
||||
`if (${name}) ${name}._fragment.c();`
|
||||
);
|
||||
|
||||
if (parentNodes) {
|
||||
block.builders.claim.addLine(
|
||||
`if (${name}) ${name}._fragment.l(${parentNodes});`
|
||||
);
|
||||
}
|
||||
|
||||
block.builders.mount.addBlock(deindent`
|
||||
if (${name}) {
|
||||
${name}._mount(${parentNode || '#target'}, ${parentNode ? 'null' : 'anchor'});
|
||||
${this.node.ref && `#component.refs.${this.node.ref.name} = ${name};`}
|
||||
}
|
||||
`);
|
||||
|
||||
const anchor = this.getOrCreateAnchor(block, parentNode, parentNodes);
|
||||
const updateMountNode = this.getUpdateMountNode(anchor);
|
||||
|
||||
if (updates.length) {
|
||||
block.builders.update.addBlock(deindent`
|
||||
${updates}
|
||||
`);
|
||||
}
|
||||
|
||||
block.builders.update.addBlock(deindent`
|
||||
if (${switch_value} !== (${switch_value} = ${snippet})) {
|
||||
if (${name}) {
|
||||
${component.options.nestedTransitions
|
||||
? deindent`
|
||||
@groupOutros();
|
||||
const old_component = ${name};
|
||||
old_component._fragment.o(() => {
|
||||
old_component.destroy();
|
||||
});`
|
||||
: `${name}.destroy();`}
|
||||
}
|
||||
|
||||
if (${switch_value}) {
|
||||
${name} = new ${switch_value}(${switch_props}(ctx));
|
||||
|
||||
${this.node.bindings.length > 0 && deindent`
|
||||
#component.root._beforecreate.push(() => {
|
||||
const changed = {};
|
||||
${this.node.bindings.map(binding => deindent`
|
||||
if (${binding.value.snippet} === void 0) changed.${binding.name} = 1;`)}
|
||||
${name}._bind(changed, ${name}.get());
|
||||
});`}
|
||||
${name}._fragment.c();
|
||||
|
||||
${this.fragment && this.fragment.nodes.map(child => child.remount(name))}
|
||||
${name}._mount(${updateMountNode}, ${anchor});
|
||||
|
||||
${this.node.handlers.map(handler => deindent`
|
||||
${name}.on("${handler.name}", ${handler.var});
|
||||
`)}
|
||||
|
||||
${this.node.ref && `#component.refs.${this.node.ref.name} = ${name};`}
|
||||
} else {
|
||||
${name} = null;
|
||||
${this.node.ref && deindent`
|
||||
if (#component.refs.${this.node.ref.name} === ${name}) {
|
||||
#component.refs.${this.node.ref.name} = null;
|
||||
}`}
|
||||
}
|
||||
}
|
||||
`);
|
||||
|
||||
if (updates.length) {
|
||||
block.builders.update.addBlock(deindent`
|
||||
else if (${switch_value}) {
|
||||
${name}._set(${name_changes});
|
||||
${this.node.bindings.length && `${name_updating} = {};`}
|
||||
}
|
||||
`);
|
||||
}
|
||||
|
||||
block.builders.destroy.addLine(`if (${name}) ${name}.destroy(${parentNode ? '' : 'detach'});`);
|
||||
} else {
|
||||
const expression = this.node.name === 'svelte:self'
|
||||
? component.name
|
||||
: `%components-${this.node.name}`;
|
||||
|
||||
block.builders.init.addBlock(deindent`
|
||||
${(this.node.attributes.length || this.node.bindings.length) && deindent`
|
||||
var ${name_initial_data} = ${attributeObject};`}
|
||||
${statements}
|
||||
var ${name} = new ${expression}({
|
||||
${componentInitProperties.join(',\n')}
|
||||
});
|
||||
|
||||
${beforecreate}
|
||||
|
||||
${this.node.handlers.map(handler => deindent`
|
||||
${name}.on("${handler.name}", function(event) {
|
||||
${handler.snippet || `#component.fire("${handler.name}", event);`}
|
||||
});
|
||||
`)}
|
||||
|
||||
${this.node.ref && `#component.refs.${this.node.ref.name} = ${name};`}
|
||||
`);
|
||||
|
||||
block.builders.create.addLine(`${name}._fragment.c();`);
|
||||
|
||||
if (parentNodes) {
|
||||
block.builders.claim.addLine(
|
||||
`${name}._fragment.l(${parentNodes});`
|
||||
);
|
||||
}
|
||||
|
||||
block.builders.mount.addLine(
|
||||
`${name}._mount(${parentNode || '#target'}, ${parentNode ? 'null' : 'anchor'});`
|
||||
);
|
||||
|
||||
if (updates.length) {
|
||||
block.builders.update.addBlock(deindent`
|
||||
${updates}
|
||||
${name}._set(${name_changes});
|
||||
${this.node.bindings.length && `${name_updating} = {};`}
|
||||
`);
|
||||
}
|
||||
|
||||
block.builders.destroy.addLine(deindent`
|
||||
${name}.destroy(${parentNode ? '' : 'detach'});
|
||||
${this.node.ref && `if (#component.refs.${this.node.ref.name} === ${name}) #component.refs.${this.node.ref.name} = null;`}
|
||||
`);
|
||||
}
|
||||
|
||||
if (component.options.nestedTransitions) {
|
||||
block.builders.outro.addLine(
|
||||
`if (${name}) ${name}._fragment.o(#outrocallback);`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
remount(name: string) {
|
||||
return `${this.var}._mount(${name}._slotted.default, null);`;
|
||||
}
|
||||
}
|
||||
|
||||
function isComputed(node: Node) {
|
||||
while (node.type === 'MemberExpression') {
|
||||
if (node.computed) return true;
|
||||
node = node.object;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
@ -0,0 +1,27 @@
|
||||
import Renderer from '../Renderer';
|
||||
import Block from '../Block';
|
||||
import Node from '../../nodes/shared/Node';
|
||||
import Tag from './shared/Tag';
|
||||
|
||||
export default class MustacheTagWrapper extends Tag {
|
||||
var = 'text';
|
||||
|
||||
constructor(renderer: Renderer, block: Block, parent: Wrapper, node: Node) {
|
||||
super(renderer, block, parent, node);
|
||||
this.cannotUseInnerHTML();
|
||||
}
|
||||
|
||||
render(block: Block, parentNode: string, parentNodes: string) {
|
||||
const { init } = this.renameThisMethod(
|
||||
block,
|
||||
value => `@setData(${this.var}, ${value});`
|
||||
);
|
||||
|
||||
block.addElement(
|
||||
this.var,
|
||||
`@createText(${init})`,
|
||||
parentNodes && `@claimText(${parentNodes}, ${init})`,
|
||||
parentNode
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,103 @@
|
||||
import Renderer from '../Renderer';
|
||||
import Block from '../Block';
|
||||
import Node from '../../nodes/shared/Node';
|
||||
import Tag from './shared/Tag';
|
||||
import Wrapper from './shared/wrapper';
|
||||
import deindent from '../../../utils/deindent';
|
||||
|
||||
export default class RawMustacheTagWrapper extends Tag {
|
||||
var = 'raw';
|
||||
|
||||
constructor(
|
||||
renderer: Renderer,
|
||||
block: Block,
|
||||
parent: Wrapper,
|
||||
node: Node
|
||||
) {
|
||||
super(renderer, block, parent, node);
|
||||
this.cannotUseInnerHTML();
|
||||
}
|
||||
|
||||
render(block: Block, parentNode: string, parentNodes: string) {
|
||||
const name = this.var;
|
||||
|
||||
// TODO use isDomNode instead of type === 'Element'?
|
||||
const needsAnchorBefore = this.prev ? this.prev.node.type !== 'Element' : !parentNode;
|
||||
const needsAnchorAfter = this.next ? this.next.node.type !== 'Element' : !parentNode;
|
||||
|
||||
const anchorBefore = needsAnchorBefore
|
||||
? block.getUniqueName(`${name}_before`)
|
||||
: (this.prev && this.prev.var) || 'null';
|
||||
|
||||
const anchorAfter = needsAnchorAfter
|
||||
? block.getUniqueName(`${name}_after`)
|
||||
: (this.next && this.next.var) || 'null';
|
||||
|
||||
let detach: string;
|
||||
let insert: (content: string) => string;
|
||||
let useInnerHTML = false;
|
||||
|
||||
if (anchorBefore === 'null' && anchorAfter === 'null') {
|
||||
useInnerHTML = true;
|
||||
detach = `${parentNode}.innerHTML = '';`;
|
||||
insert = content => `${parentNode}.innerHTML = ${content};`;
|
||||
} else if (anchorBefore === 'null') {
|
||||
detach = `@detachBefore(${anchorAfter});`;
|
||||
insert = content => `${anchorAfter}.insertAdjacentHTML("beforebegin", ${content});`;
|
||||
} else if (anchorAfter === 'null') {
|
||||
detach = `@detachAfter(${anchorBefore});`;
|
||||
insert = content => `${anchorBefore}.insertAdjacentHTML("afterend", ${content});`;
|
||||
} else {
|
||||
detach = `@detachBetween(${anchorBefore}, ${anchorAfter});`;
|
||||
insert = content => `${anchorBefore}.insertAdjacentHTML("afterend", ${content});`;
|
||||
}
|
||||
|
||||
const { init } = this.renameThisMethod(
|
||||
block,
|
||||
content => deindent`
|
||||
${!useInnerHTML && detach}
|
||||
${insert(content)}
|
||||
`
|
||||
);
|
||||
|
||||
// we would have used comments here, but the `insertAdjacentHTML` api only
|
||||
// exists for `Element`s.
|
||||
if (needsAnchorBefore) {
|
||||
block.addElement(
|
||||
anchorBefore,
|
||||
`@createElement('noscript')`,
|
||||
parentNodes && `@createElement('noscript')`,
|
||||
parentNode,
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
function addAnchorAfter() {
|
||||
block.addElement(
|
||||
anchorAfter,
|
||||
`@createElement('noscript')`,
|
||||
parentNodes && `@createElement('noscript')`,
|
||||
parentNode
|
||||
);
|
||||
}
|
||||
|
||||
if (needsAnchorAfter && anchorBefore === 'null') {
|
||||
// anchorAfter needs to be in the DOM before we
|
||||
// insert the HTML...
|
||||
addAnchorAfter();
|
||||
}
|
||||
|
||||
block.builders.mount.addLine(insert(init));
|
||||
|
||||
if (!parentNode) {
|
||||
block.builders.destroy.addConditional('detach', needsAnchorBefore
|
||||
? `${detach}\n@detachNode(${anchorBefore});`
|
||||
: detach);
|
||||
}
|
||||
|
||||
if (needsAnchorAfter && anchorBefore !== 'null') {
|
||||
// ...otherwise it should go afterwards
|
||||
addAnchorAfter();
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,144 @@
|
||||
import Wrapper from './shared/Wrapper';
|
||||
import Renderer from '../Renderer';
|
||||
import Block from '../Block';
|
||||
import Slot from '../../nodes/Slot';
|
||||
import { quotePropIfNecessary } from '../../../utils/quoteIfNecessary';
|
||||
import FragmentWrapper from './Fragment';
|
||||
import deindent from '../../../utils/deindent';
|
||||
|
||||
function sanitize(name) {
|
||||
return name.replace(/[^a-zA-Z]+/g, '_').replace(/^_/, '').replace(/_$/, '');
|
||||
}
|
||||
|
||||
export default class SlotWrapper extends Wrapper {
|
||||
node: Slot;
|
||||
fragment: FragmentWrapper;
|
||||
|
||||
var = 'slot';
|
||||
|
||||
constructor(
|
||||
renderer: Renderer,
|
||||
block: Block,
|
||||
parent: Wrapper,
|
||||
node: Slot,
|
||||
stripWhitespace: boolean,
|
||||
nextSibling: Wrapper
|
||||
) {
|
||||
super(renderer, block, parent, node);
|
||||
this.cannotUseInnerHTML();
|
||||
|
||||
this.fragment = new FragmentWrapper(
|
||||
renderer,
|
||||
block,
|
||||
node.children,
|
||||
parent,
|
||||
stripWhitespace,
|
||||
nextSibling
|
||||
);
|
||||
}
|
||||
|
||||
render(
|
||||
block: Block,
|
||||
parentNode: string,
|
||||
parentNodes: string
|
||||
) {
|
||||
const { renderer } = this;
|
||||
|
||||
const slotName = this.node.getStaticAttributeValue('name') || 'default';
|
||||
renderer.slots.add(slotName);
|
||||
|
||||
const content_name = block.getUniqueName(`slot_content_${sanitize(slotName)}`);
|
||||
const prop = quotePropIfNecessary(slotName);
|
||||
block.addVariable(content_name, `#component._slotted${prop}`);
|
||||
|
||||
// TODO can we use isDomNode instead of type === 'Element'?
|
||||
const needsAnchorBefore = this.prev ? this.prev.node.type !== 'Element' : !parentNode;
|
||||
const needsAnchorAfter = this.next ? this.next.node.type !== 'Element' : !parentNode;
|
||||
|
||||
const anchorBefore = needsAnchorBefore
|
||||
? block.getUniqueName(`${content_name}_before`)
|
||||
: (this.prev && this.prev.var) || 'null';
|
||||
|
||||
const anchorAfter = needsAnchorAfter
|
||||
? block.getUniqueName(`${content_name}_after`)
|
||||
: (this.next && this.next.var) || 'null';
|
||||
|
||||
if (needsAnchorBefore) block.addVariable(anchorBefore);
|
||||
if (needsAnchorAfter) block.addVariable(anchorAfter);
|
||||
|
||||
let mountBefore = block.builders.mount.toString();
|
||||
let destroyBefore = block.builders.destroy.toString();
|
||||
|
||||
block.builders.create.pushCondition(`!${content_name}`);
|
||||
block.builders.hydrate.pushCondition(`!${content_name}`);
|
||||
block.builders.mount.pushCondition(`!${content_name}`);
|
||||
block.builders.update.pushCondition(`!${content_name}`);
|
||||
block.builders.destroy.pushCondition(`!${content_name}`);
|
||||
|
||||
this.fragment.render(block, parentNode, parentNodes);
|
||||
|
||||
block.builders.create.popCondition();
|
||||
block.builders.hydrate.popCondition();
|
||||
block.builders.mount.popCondition();
|
||||
block.builders.update.popCondition();
|
||||
block.builders.destroy.popCondition();
|
||||
|
||||
const mountLeadin = block.builders.mount.toString() !== mountBefore
|
||||
? `else`
|
||||
: `if (${content_name})`;
|
||||
|
||||
if (parentNode) {
|
||||
block.builders.mount.addBlock(deindent`
|
||||
${mountLeadin} {
|
||||
${needsAnchorBefore && `@append(${parentNode}, ${anchorBefore} || (${anchorBefore} = @createComment()));`}
|
||||
@append(${parentNode}, ${content_name});
|
||||
${needsAnchorAfter && `@append(${parentNode}, ${anchorAfter} || (${anchorAfter} = @createComment()));`}
|
||||
}
|
||||
`);
|
||||
} else {
|
||||
block.builders.mount.addBlock(deindent`
|
||||
${mountLeadin} {
|
||||
${needsAnchorBefore && `@insert(#target, ${anchorBefore} || (${anchorBefore} = @createComment()), anchor);`}
|
||||
@insert(#target, ${content_name}, anchor);
|
||||
${needsAnchorAfter && `@insert(#target, ${anchorAfter} || (${anchorAfter} = @createComment()), 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
|
||||
const unmountLeadin = block.builders.destroy.toString() !== destroyBefore
|
||||
? `else`
|
||||
: `if (${content_name})`;
|
||||
|
||||
if (anchorBefore === 'null' && anchorAfter === 'null') {
|
||||
block.builders.destroy.addBlock(deindent`
|
||||
${unmountLeadin} {
|
||||
@reinsertChildren(${parentNode}, ${content_name});
|
||||
}
|
||||
`);
|
||||
} else if (anchorBefore === 'null') {
|
||||
block.builders.destroy.addBlock(deindent`
|
||||
${unmountLeadin} {
|
||||
@reinsertBefore(${anchorAfter}, ${content_name});
|
||||
}
|
||||
`);
|
||||
} else if (anchorAfter === 'null') {
|
||||
block.builders.destroy.addBlock(deindent`
|
||||
${unmountLeadin} {
|
||||
@reinsertAfter(${anchorBefore}, ${content_name});
|
||||
}
|
||||
`);
|
||||
} else {
|
||||
block.builders.destroy.addBlock(deindent`
|
||||
${unmountLeadin} {
|
||||
@reinsertBetween(${anchorBefore}, ${anchorAfter}, ${content_name});
|
||||
@detachNode(${anchorBefore});
|
||||
@detachNode(${anchorAfter});
|
||||
}
|
||||
`);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,67 @@
|
||||
import Renderer from '../Renderer';
|
||||
import Block from '../Block';
|
||||
import Text from '../../nodes/Text';
|
||||
import Wrapper from './shared/Wrapper';
|
||||
import { CompileOptions } from '../../../interfaces';
|
||||
import { stringify } from '../../../utils/stringify';
|
||||
|
||||
// Whitespace inside one of these elements will not result in
|
||||
// a whitespace node being created in any circumstances. (This
|
||||
// list is almost certainly very incomplete)
|
||||
const elementsWithoutText = new Set([
|
||||
'audio',
|
||||
'datalist',
|
||||
'dl',
|
||||
'optgroup',
|
||||
'select',
|
||||
'video',
|
||||
]);
|
||||
|
||||
// TODO this should probably be in Fragment
|
||||
function shouldSkip(node: Text) {
|
||||
if (/\S/.test(node.data)) return false;
|
||||
|
||||
const parentElement = node.findNearest(/(?:Element|InlineComponent|Head)/);
|
||||
if (!parentElement) return false;
|
||||
|
||||
if (parentElement.type === 'Head') return true;
|
||||
if (parentElement.type === 'InlineComponent') return parentElement.children.length === 1 && node === parentElement.children[0];
|
||||
|
||||
return parentElement.namespace || elementsWithoutText.has(parentElement.name);
|
||||
}
|
||||
|
||||
export default class TextWrapper extends Wrapper {
|
||||
node: Text;
|
||||
data: string;
|
||||
skip: boolean;
|
||||
var: string;
|
||||
|
||||
constructor(
|
||||
renderer: Renderer,
|
||||
block: Block,
|
||||
parent: Wrapper,
|
||||
node: Text,
|
||||
data: string
|
||||
) {
|
||||
super(renderer, block, parent, node);
|
||||
|
||||
this.skip = shouldSkip(this.node);
|
||||
this.data = data;
|
||||
this.var = this.skip ? null : 'text';
|
||||
}
|
||||
|
||||
render(block: Block, parentNode: string, parentNodes: string) {
|
||||
if (this.skip) return;
|
||||
|
||||
block.addElement(
|
||||
this.var,
|
||||
`@createText(${stringify(this.data)})`,
|
||||
parentNodes && `@claimText(${parentNodes}, ${stringify(this.data)})`,
|
||||
parentNode
|
||||
);
|
||||
}
|
||||
|
||||
remount(name: string) {
|
||||
return `@append(${name}._slotted.default, ${this.var});`;
|
||||
}
|
||||
}
|
@ -0,0 +1,99 @@
|
||||
import Wrapper from './shared/Wrapper';
|
||||
import Renderer from '../Renderer';
|
||||
import Block from '../Block';
|
||||
import Title from '../../nodes/Title';
|
||||
import FragmentWrapper from './Fragment';
|
||||
import { stringify } from '../../../utils/stringify';
|
||||
|
||||
export default class TitleWrapper extends Wrapper {
|
||||
node: Title;
|
||||
|
||||
constructor(
|
||||
renderer: Renderer,
|
||||
block: Block,
|
||||
parent: Wrapper,
|
||||
node: Title,
|
||||
stripWhitespace: boolean,
|
||||
nextSibling: Wrapper
|
||||
) {
|
||||
super(renderer, block, parent, node);
|
||||
}
|
||||
|
||||
render(block: Block, parentNode: string, parentNodes: string) {
|
||||
const isDynamic = !!this.node.children.find(node => node.type !== 'Text');
|
||||
|
||||
if (isDynamic) {
|
||||
let value;
|
||||
|
||||
const allDependencies = new Set();
|
||||
|
||||
// TODO some of this code is repeated in Tag.ts — would be good to
|
||||
// DRY it out if that's possible without introducing crazy indirection
|
||||
if (this.node.children.length === 1) {
|
||||
// single {tag} — may be a non-string
|
||||
const { expression } = this.node.children[0];
|
||||
const { dependencies, snippet } = this.node.children[0].expression;
|
||||
|
||||
value = snippet;
|
||||
dependencies.forEach(d => {
|
||||
allDependencies.add(d);
|
||||
});
|
||||
} else {
|
||||
// '{foo} {bar}' — treat as string concatenation
|
||||
value =
|
||||
(this.node.children[0].type === 'Text' ? '' : `"" + `) +
|
||||
this.node.children
|
||||
.map((chunk: Node) => {
|
||||
if (chunk.type === 'Text') {
|
||||
return stringify(chunk.data);
|
||||
} else {
|
||||
const { dependencies, snippet } = chunk.expression;
|
||||
|
||||
dependencies.forEach(d => {
|
||||
allDependencies.add(d);
|
||||
});
|
||||
|
||||
return chunk.expression.getPrecedence() <= 13 ? `(${snippet})` : snippet;
|
||||
}
|
||||
})
|
||||
.join(' + ');
|
||||
}
|
||||
|
||||
const last = this.node.shouldCache && block.getUniqueName(
|
||||
`title_value`
|
||||
);
|
||||
|
||||
if (this.node.shouldCache) block.addVariable(last);
|
||||
|
||||
let updater;
|
||||
const init = this.node.shouldCache ? `${last} = ${value}` : value;
|
||||
|
||||
block.builders.init.addLine(
|
||||
`document.title = ${init};`
|
||||
);
|
||||
updater = `document.title = ${this.node.shouldCache ? last : value};`;
|
||||
|
||||
if (allDependencies.size) {
|
||||
const dependencies = Array.from(allDependencies);
|
||||
const changedCheck = (
|
||||
( block.hasOutros ? `!#current || ` : '' ) +
|
||||
dependencies.map(dependency => `changed.${dependency}`).join(' || ')
|
||||
);
|
||||
|
||||
const updateCachedValue = `${last} !== (${last} = ${value})`;
|
||||
|
||||
const condition = this.node.shouldCache ?
|
||||
( dependencies.length ? `(${changedCheck}) && ${updateCachedValue}` : updateCachedValue ) :
|
||||
changedCheck;
|
||||
|
||||
block.builders.update.addConditional(
|
||||
condition,
|
||||
updater
|
||||
);
|
||||
}
|
||||
} else {
|
||||
const value = stringify(this.node.children[0].data);
|
||||
block.builders.hydrate.addLine(`document.title = ${value};`);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,198 @@
|
||||
import Renderer from '../Renderer';
|
||||
import Block from '../Block';
|
||||
import Node from '../../nodes/shared/Node';
|
||||
import Wrapper from './shared/Wrapper';
|
||||
import deindent from '../../../utils/deindent';
|
||||
|
||||
const associatedEvents = {
|
||||
innerWidth: 'resize',
|
||||
innerHeight: 'resize',
|
||||
outerWidth: 'resize',
|
||||
outerHeight: 'resize',
|
||||
|
||||
scrollX: 'scroll',
|
||||
scrollY: 'scroll',
|
||||
};
|
||||
|
||||
const properties = {
|
||||
scrollX: 'pageXOffset',
|
||||
scrollY: 'pageYOffset'
|
||||
};
|
||||
|
||||
const readonly = new Set([
|
||||
'innerWidth',
|
||||
'innerHeight',
|
||||
'outerWidth',
|
||||
'outerHeight',
|
||||
'online',
|
||||
]);
|
||||
|
||||
export default class WindowWrapper extends Wrapper {
|
||||
constructor(renderer: Renderer, block: Block, parent: Wrapper, node: Node) {
|
||||
super(renderer, block, parent, node);
|
||||
}
|
||||
|
||||
render(block: Block, parentNode: string, parentNodes: string) {
|
||||
const { renderer } = this;
|
||||
const { component } = renderer;
|
||||
|
||||
const events = {};
|
||||
const bindings: Record<string, string> = {};
|
||||
|
||||
this.node.handlers.forEach(handler => {
|
||||
// TODO verify that it's a valid callee (i.e. built-in or declared method)
|
||||
component.addSourcemapLocations(handler.expression);
|
||||
|
||||
const isCustomEvent = component.events.has(handler.name);
|
||||
|
||||
let usesState = handler.dependencies.size > 0;
|
||||
|
||||
handler.render(component, block, 'window', false); // TODO hoist?
|
||||
|
||||
const handlerName = block.getUniqueName(`onwindow${handler.name}`);
|
||||
const handlerBody = deindent`
|
||||
${usesState && `var ctx = #component.get();`}
|
||||
${handler.snippet};
|
||||
`;
|
||||
|
||||
if (isCustomEvent) {
|
||||
// TODO dry this out
|
||||
block.addVariable(handlerName);
|
||||
|
||||
block.builders.hydrate.addBlock(deindent`
|
||||
${handlerName} = %events-${handler.name}.call(#component, window, function(event) {
|
||||
${handlerBody}
|
||||
});
|
||||
`);
|
||||
|
||||
block.builders.destroy.addLine(deindent`
|
||||
${handlerName}.destroy();
|
||||
`);
|
||||
} else {
|
||||
block.builders.init.addBlock(deindent`
|
||||
function ${handlerName}(event) {
|
||||
${handlerBody}
|
||||
}
|
||||
window.addEventListener("${handler.name}", ${handlerName});
|
||||
`);
|
||||
|
||||
block.builders.destroy.addBlock(deindent`
|
||||
window.removeEventListener("${handler.name}", ${handlerName});
|
||||
`);
|
||||
}
|
||||
});
|
||||
|
||||
this.node.bindings.forEach(binding => {
|
||||
// in dev mode, throw if read-only values are written to
|
||||
if (readonly.has(binding.name)) {
|
||||
renderer.readonly.add(binding.value.node.name);
|
||||
}
|
||||
|
||||
bindings[binding.name] = binding.value.node.name;
|
||||
|
||||
// bind:online is a special case, we need to listen for two separate events
|
||||
if (binding.name === 'online') return;
|
||||
|
||||
const associatedEvent = associatedEvents[binding.name];
|
||||
const property = properties[binding.name] || binding.name;
|
||||
|
||||
if (!events[associatedEvent]) events[associatedEvent] = [];
|
||||
events[associatedEvent].push(
|
||||
`${binding.value.node.name}: this.${property}`
|
||||
);
|
||||
|
||||
// add initial value
|
||||
renderer.metaBindings.push(
|
||||
`this._state.${binding.value.node.name} = window.${property};`
|
||||
);
|
||||
});
|
||||
|
||||
const lock = block.getUniqueName(`window_updating`);
|
||||
const clear = block.getUniqueName(`clear_window_updating`);
|
||||
const timeout = block.getUniqueName(`window_updating_timeout`);
|
||||
|
||||
Object.keys(events).forEach(event => {
|
||||
const handlerName = block.getUniqueName(`onwindow${event}`);
|
||||
const props = events[event].join(',\n');
|
||||
|
||||
if (event === 'scroll') {
|
||||
// TODO other bidirectional bindings...
|
||||
block.addVariable(lock, 'false');
|
||||
block.addVariable(clear, `function() { ${lock} = false; }`);
|
||||
block.addVariable(timeout);
|
||||
}
|
||||
|
||||
const handlerBody = deindent`
|
||||
${event === 'scroll' && deindent`
|
||||
if (${lock}) return;
|
||||
${lock} = true;
|
||||
`}
|
||||
${component.options.dev && `component._updatingReadonlyProperty = true;`}
|
||||
|
||||
#component.set({
|
||||
${props}
|
||||
});
|
||||
|
||||
${component.options.dev && `component._updatingReadonlyProperty = false;`}
|
||||
${event === 'scroll' && `${lock} = false;`}
|
||||
`;
|
||||
|
||||
block.builders.init.addBlock(deindent`
|
||||
function ${handlerName}(event) {
|
||||
${handlerBody}
|
||||
}
|
||||
window.addEventListener("${event}", ${handlerName});
|
||||
`);
|
||||
|
||||
block.builders.destroy.addBlock(deindent`
|
||||
window.removeEventListener("${event}", ${handlerName});
|
||||
`);
|
||||
});
|
||||
|
||||
// special case... might need to abstract this out if we add more special cases
|
||||
if (bindings.scrollX || bindings.scrollY) {
|
||||
block.builders.init.addBlock(deindent`
|
||||
#component.on("state", ({ changed, current }) => {
|
||||
if (${
|
||||
[bindings.scrollX, bindings.scrollY].map(
|
||||
binding => binding && `changed["${binding}"]`
|
||||
).filter(Boolean).join(' || ')
|
||||
} && !${lock}) {
|
||||
${lock} = true;
|
||||
clearTimeout(${timeout});
|
||||
window.scrollTo(${
|
||||
bindings.scrollX ? `current["${bindings.scrollX}"]` : `window.pageXOffset`
|
||||
}, ${
|
||||
bindings.scrollY ? `current["${bindings.scrollY}"]` : `window.pageYOffset`
|
||||
});
|
||||
${timeout} = setTimeout(${clear}, 100);
|
||||
}
|
||||
});
|
||||
`);
|
||||
}
|
||||
|
||||
// another special case. (I'm starting to think these are all special cases.)
|
||||
if (bindings.online) {
|
||||
const handlerName = block.getUniqueName(`onlinestatuschanged`);
|
||||
block.builders.init.addBlock(deindent`
|
||||
function ${handlerName}(event) {
|
||||
${component.options.dev && `component._updatingReadonlyProperty = true;`}
|
||||
#component.set({ ${bindings.online}: navigator.onLine });
|
||||
${component.options.dev && `component._updatingReadonlyProperty = false;`}
|
||||
}
|
||||
window.addEventListener("online", ${handlerName});
|
||||
window.addEventListener("offline", ${handlerName});
|
||||
`);
|
||||
|
||||
// add initial value
|
||||
renderer.metaBindings.push(
|
||||
`this._state.${bindings.online} = navigator.onLine;`
|
||||
);
|
||||
|
||||
block.builders.destroy.addBlock(deindent`
|
||||
window.removeEventListener("online", ${handlerName});
|
||||
window.removeEventListener("offline", ${handlerName});
|
||||
`);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,62 @@
|
||||
import Renderer from '../../Renderer';
|
||||
import Block from '../../Block';
|
||||
import Wrapper from './Wrapper';
|
||||
import EventHandler from '../../../nodes/EventHandler';
|
||||
import validCalleeObjects from '../../../../utils/validCalleeObjects';
|
||||
|
||||
export default class EventHandlerWrapper extends Wrapper {
|
||||
node: EventHandler;
|
||||
|
||||
constructor(
|
||||
renderer: Renderer,
|
||||
block: Block,
|
||||
parent: Wrapper,
|
||||
node: EventHandler,
|
||||
stripWhitespace: boolean,
|
||||
nextSibling: Wrapper
|
||||
) {
|
||||
super(renderer, block, parent, node);
|
||||
}
|
||||
|
||||
render(block: Block, parentNode: string, parentNodes: string) {
|
||||
const { renderer } = this;
|
||||
const { component } = renderer;
|
||||
|
||||
const hoisted = this.node.shouldHoist;
|
||||
|
||||
if (this.node.insertionPoint === null) return; // TODO handle shorthand events here?
|
||||
|
||||
if (!validCalleeObjects.has(this.node.callee.name)) {
|
||||
const component_name = 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.node.callee.name[0] === '$' && !component.methods.has(this.node.callee.name)) {
|
||||
component.code.overwrite(
|
||||
this.node.insertionPoint,
|
||||
this.node.insertionPoint + 1,
|
||||
`${component_name}.store.`
|
||||
);
|
||||
} else {
|
||||
component.code.prependRight(
|
||||
this.node.insertionPoint,
|
||||
`${component_name}.`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (this.node.isCustomEvent) {
|
||||
this.node.args.forEach(arg => {
|
||||
arg.overwriteThis(this.parent.var);
|
||||
});
|
||||
|
||||
if (this.node.callee && this.node.callee.name === 'this') {
|
||||
const node = this.node.callee.nodes[0];
|
||||
component.code.overwrite(node.start, node.end, this.parent.var, {
|
||||
storeName: true,
|
||||
contentOnly: true
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,67 @@
|
||||
import Wrapper from './Wrapper';
|
||||
import Renderer from '../../Renderer';
|
||||
import Block from '../../Block';
|
||||
import Node from '../../../nodes/shared/Node';
|
||||
import MustacheTag from '../../../nodes/MustacheTag';
|
||||
import RawMustacheTag from '../../../nodes/RawMustacheTag';
|
||||
|
||||
export default class Tag extends Wrapper {
|
||||
node: MustacheTag | RawMustacheTag;
|
||||
|
||||
constructor(renderer: Renderer, block: Block, parent: Wrapper, node: MustacheTag | RawMustacheTag) {
|
||||
super(renderer, block, parent, node);
|
||||
this.cannotUseInnerHTML();
|
||||
|
||||
block.addDependencies(node.expression.dependencies);
|
||||
}
|
||||
|
||||
render(block: Block, parentNode: string, parentNodes: string) {
|
||||
const { init } = this.renameThisMethod(
|
||||
block,
|
||||
value => `@setData(${this.var}, ${value});`
|
||||
);
|
||||
|
||||
block.addElement(
|
||||
this.var,
|
||||
`@createText(${init})`,
|
||||
parentNodes && `@claimText(${parentNodes}, ${init})`,
|
||||
parentNode
|
||||
);
|
||||
}
|
||||
|
||||
renameThisMethod(
|
||||
block: Block,
|
||||
update: ((value: string) => string)
|
||||
) {
|
||||
const { snippet, dependencies } = this.node.expression;
|
||||
|
||||
const value = this.node.shouldCache && block.getUniqueName(`${this.var}_value`);
|
||||
const content = this.node.shouldCache ? value : snippet;
|
||||
|
||||
if (this.node.shouldCache) block.addVariable(value, snippet);
|
||||
|
||||
if (dependencies.size) {
|
||||
const changedCheck = (
|
||||
(block.hasOutros ? `!#current || ` : '') +
|
||||
[...dependencies].map((dependency: string) => `changed.${dependency}`).join(' || ')
|
||||
);
|
||||
|
||||
const updateCachedValue = `${value} !== (${value} = ${snippet})`;
|
||||
|
||||
const condition = this.node.shouldCache ?
|
||||
(dependencies.size ? `(${changedCheck}) && ${updateCachedValue}` : updateCachedValue) :
|
||||
changedCheck;
|
||||
|
||||
block.builders.update.addConditional(
|
||||
condition,
|
||||
update(content)
|
||||
);
|
||||
}
|
||||
|
||||
return { init: content };
|
||||
}
|
||||
|
||||
remount(name: string) {
|
||||
return `@append(${name}._slotted.default, ${this.var});`;
|
||||
}
|
||||
}
|
@ -0,0 +1,92 @@
|
||||
import Renderer from '../../Renderer';
|
||||
import Node from '../../../nodes/shared/Node';
|
||||
import Block from '../../Block';
|
||||
|
||||
export default class Wrapper {
|
||||
renderer: Renderer;
|
||||
parent: Wrapper;
|
||||
node: Node;
|
||||
|
||||
prev: Wrapper | null;
|
||||
next: Wrapper | null;
|
||||
|
||||
var: string;
|
||||
canUseInnerHTML: boolean;
|
||||
|
||||
constructor(
|
||||
renderer: Renderer,
|
||||
block: Block,
|
||||
parent: Wrapper,
|
||||
node: Node
|
||||
) {
|
||||
this.node = node;
|
||||
|
||||
// make these non-enumerable so that they can be logged sensibly
|
||||
// (TODO in dev only?)
|
||||
Object.defineProperties(this, {
|
||||
renderer: {
|
||||
value: renderer
|
||||
},
|
||||
parent: {
|
||||
value: parent
|
||||
}
|
||||
});
|
||||
|
||||
this.canUseInnerHTML = !renderer.options.hydratable;
|
||||
|
||||
block.wrappers.push(this);
|
||||
}
|
||||
|
||||
cannotUseInnerHTML() {
|
||||
this.canUseInnerHTML = false;
|
||||
if (this.parent) this.parent.cannotUseInnerHTML();
|
||||
}
|
||||
|
||||
// TODO do we still need equivalent method on Node?
|
||||
findNearest(pattern) {
|
||||
if (pattern.test(this.node.type)) return this;
|
||||
return this.parent && this.parent.findNearest(pattern);
|
||||
}
|
||||
|
||||
getOrCreateAnchor(block: Block, parentNode: string, parentNodes: string) {
|
||||
// TODO use this in EachBlock and IfBlock — tricky because
|
||||
// children need to be created first
|
||||
const needsAnchor = this.next ? !this.next.isDomNode() : !parentNode || !this.parent.isDomNode();
|
||||
const anchor = needsAnchor
|
||||
? block.getUniqueName(`${this.var}_anchor`)
|
||||
: (this.next && this.next.var) || 'null';
|
||||
|
||||
if (needsAnchor) {
|
||||
block.addElement(
|
||||
anchor,
|
||||
`@createComment()`,
|
||||
parentNodes && `@createComment()`,
|
||||
parentNode
|
||||
);
|
||||
}
|
||||
|
||||
return anchor;
|
||||
}
|
||||
|
||||
getUpdateMountNode(anchor: string) {
|
||||
return (this.parent && this.parent.isDomNode())
|
||||
? this.parent.var
|
||||
: `${anchor}.parentNode`;
|
||||
}
|
||||
|
||||
isDomNode() {
|
||||
return (
|
||||
this.node.type === 'Element' ||
|
||||
this.node.type === 'Text' ||
|
||||
this.node.type === 'MustacheTag'
|
||||
);
|
||||
}
|
||||
|
||||
render(block: Block, parentNode: string, parentNodes: string) {
|
||||
throw new Error(`render method not implemented by subclass ${this.node.type}`);
|
||||
}
|
||||
|
||||
remount(name: string) {
|
||||
return `${this.var}.m(${name}._slotted.default, null);`;
|
||||
}
|
||||
}
|
@ -0,0 +1,71 @@
|
||||
import AwaitBlock from './handlers/AwaitBlock';
|
||||
import Comment from './handlers/Comment';
|
||||
import DebugTag from './handlers/DebugTag';
|
||||
import EachBlock from './handlers/EachBlock';
|
||||
import Element from './handlers/Element';
|
||||
import Head from './handlers/Head';
|
||||
import HtmlTag from './handlers/HtmlTag';
|
||||
import IfBlock from './handlers/IfBlock';
|
||||
import InlineComponent from './handlers/InlineComponent';
|
||||
import Slot from './handlers/Slot';
|
||||
import Tag from './handlers/Tag';
|
||||
import Text from './handlers/Text';
|
||||
import Title from './handlers/Title';
|
||||
import { CompileOptions } from '../../interfaces';
|
||||
|
||||
type Handler = (node: any, renderer: Renderer, options: CompileOptions) => void;
|
||||
|
||||
function noop(){}
|
||||
|
||||
const handlers: Record<string, Handler> = {
|
||||
AwaitBlock,
|
||||
Comment,
|
||||
DebugTag,
|
||||
EachBlock,
|
||||
Element,
|
||||
Head,
|
||||
IfBlock,
|
||||
InlineComponent,
|
||||
MustacheTag: Tag, // TODO MustacheTag is an anachronism
|
||||
RawMustacheTag: HtmlTag,
|
||||
Slot,
|
||||
Text,
|
||||
Title,
|
||||
Window: noop
|
||||
};
|
||||
|
||||
type AppendTarget = any; // TODO
|
||||
|
||||
export default class Renderer {
|
||||
bindings: string[];
|
||||
code: string;
|
||||
targets: AppendTarget[];
|
||||
|
||||
constructor() {
|
||||
this.bindings = [];
|
||||
this.code = '';
|
||||
this.targets = [];
|
||||
}
|
||||
|
||||
append(code: string) {
|
||||
if (this.targets.length) {
|
||||
const target = this.targets[this.targets.length - 1];
|
||||
const slotName = target.slotStack[target.slotStack.length - 1];
|
||||
target.slots[slotName] += code;
|
||||
} else {
|
||||
this.code += code;
|
||||
}
|
||||
}
|
||||
|
||||
render(nodes, options) {
|
||||
nodes.forEach(node => {
|
||||
const handler = handlers[node.type];
|
||||
|
||||
if (!handler) {
|
||||
throw new Error(`No handler for '${node.type}' nodes`);
|
||||
}
|
||||
|
||||
handler(node, this, options);
|
||||
});
|
||||
}
|
||||
}
|
@ -0,0 +1,16 @@
|
||||
import Renderer from '../Renderer';
|
||||
import { CompileOptions } from '../../../interfaces';
|
||||
|
||||
export default function(node, renderer: Renderer, options: CompileOptions) {
|
||||
const { snippet } = node.expression;
|
||||
|
||||
renderer.append('${(function(__value) { if(@isPromise(__value)) return `');
|
||||
|
||||
renderer.render(node.pending.children, options);
|
||||
|
||||
renderer.append('`; return function(ctx) { return `');
|
||||
|
||||
renderer.render(node.then.children, options);
|
||||
|
||||
renderer.append(`\`;}(Object.assign({}, ctx, { ${node.value}: __value }));}(${snippet})) }`);
|
||||
}
|
@ -0,0 +1,8 @@
|
||||
import Renderer from '../Renderer';
|
||||
import { CompileOptions } from '../../../interfaces';
|
||||
|
||||
export default function(node, renderer: Renderer, options: CompileOptions) {
|
||||
if (options.preserveComments) {
|
||||
renderer.append(`<!--${node.data}-->`);
|
||||
}
|
||||
}
|
@ -0,0 +1,19 @@
|
||||
import { stringify } from '../../../utils/stringify';
|
||||
|
||||
export default function(node, renderer, options) {
|
||||
if (!options.dev) return;
|
||||
|
||||
const filename = options.file || null;
|
||||
const { line, column } = options.locate(node.start + 1);
|
||||
|
||||
const obj = node.expressions.length === 0
|
||||
? `ctx`
|
||||
: `{ ${node.expressions
|
||||
.map(e => e.node.name)
|
||||
.map(name => `${name}: ctx.${name}`)
|
||||
.join(', ')} }`;
|
||||
|
||||
const str = '${@debug(' + `${filename && stringify(filename)}, ${line}, ${column}, ${obj})}`;
|
||||
|
||||
renderer.append(str);
|
||||
}
|
@ -0,0 +1,25 @@
|
||||
export default function(node, renderer, options) {
|
||||
const { snippet } = node.expression;
|
||||
|
||||
const props = node.contexts.map(prop => `${prop.key.name}: item${prop.tail}`);
|
||||
|
||||
const getContext = node.index
|
||||
? `(item, i) => Object.assign({}, ctx, { ${props.join(', ')}, ${node.index}: i })`
|
||||
: `item => Object.assign({}, ctx, { ${props.join(', ')} })`;
|
||||
|
||||
const open = `\${ ${node.else ? `${snippet}.length ? ` : ''}@each(${snippet}, ${getContext}, ctx => \``;
|
||||
renderer.append(open);
|
||||
|
||||
renderer.render(node.children, options);
|
||||
|
||||
const close = `\`)`;
|
||||
renderer.append(close);
|
||||
|
||||
if (node.else) {
|
||||
renderer.append(` : \``);
|
||||
renderer.render(node.else.children, options);
|
||||
renderer.append(`\``);
|
||||
}
|
||||
|
||||
renderer.append('}');
|
||||
}
|
@ -0,0 +1,142 @@
|
||||
import { quotePropIfNecessary, quoteNameIfNecessary } from '../../../utils/quoteIfNecessary';
|
||||
import isVoidElementName from '../../../utils/isVoidElementName';
|
||||
|
||||
// source: https://gist.github.com/ArjanSchouten/0b8574a6ad7f5065a5e7
|
||||
const boolean_attributes = 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'
|
||||
]);
|
||||
|
||||
export default function(node, renderer, options) {
|
||||
let openingTag = `<${node.name}`;
|
||||
let textareaContents; // awkward special case
|
||||
|
||||
const slot = node.getStaticAttributeValue('slot');
|
||||
if (slot && node.hasAncestor('InlineComponent')) {
|
||||
const slot = node.attributes.find((attribute: Node) => attribute.name === 'slot');
|
||||
const slotName = slot.chunks[0].data;
|
||||
const target = renderer.targets[renderer.targets.length - 1];
|
||||
target.slotStack.push(slotName);
|
||||
target.slots[slotName] = '';
|
||||
}
|
||||
|
||||
const classExpr = node.classes.map((classDir: Class) => {
|
||||
const { expression, name } = classDir;
|
||||
const snippet = expression ? expression.snippet : `ctx${quotePropIfNecessary(name)}`;
|
||||
return `${snippet} ? "${name}" : ""`;
|
||||
}).join(', ');
|
||||
|
||||
let addClassAttribute = classExpr ? true : false;
|
||||
|
||||
if (node.attributes.find(attr => attr.isSpread)) {
|
||||
// TODO dry this out
|
||||
const args = [];
|
||||
node.attributes.forEach(attribute => {
|
||||
if (attribute.isSpread) {
|
||||
args.push(attribute.expression.snippet);
|
||||
} else {
|
||||
if (attribute.name === 'value' && node.name === 'textarea') {
|
||||
textareaContents = attribute.stringifyForSsr();
|
||||
} else if (attribute.isTrue) {
|
||||
args.push(`{ ${quoteNameIfNecessary(attribute.name)}: true }`);
|
||||
} else if (
|
||||
boolean_attributes.has(attribute.name) &&
|
||||
attribute.chunks.length === 1 &&
|
||||
attribute.chunks[0].type !== 'Text'
|
||||
) {
|
||||
// a boolean attribute with one non-Text chunk
|
||||
args.push(`{ ${quoteNameIfNecessary(attribute.name)}: ${attribute.chunks[0].snippet} }`);
|
||||
} else {
|
||||
args.push(`{ ${quoteNameIfNecessary(attribute.name)}: \`${attribute.stringifyForSsr()}\` }`);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
openingTag += "${@spread([" + args.join(', ') + "])}";
|
||||
} else {
|
||||
node.attributes.forEach((attribute: Node) => {
|
||||
if (attribute.type !== 'Attribute') return;
|
||||
|
||||
if (attribute.name === 'value' && node.name === 'textarea') {
|
||||
textareaContents = attribute.stringifyForSsr();
|
||||
} else if (attribute.isTrue) {
|
||||
openingTag += ` ${attribute.name}`;
|
||||
} else if (
|
||||
boolean_attributes.has(attribute.name) &&
|
||||
attribute.chunks.length === 1 &&
|
||||
attribute.chunks[0].type !== 'Text'
|
||||
) {
|
||||
// a boolean attribute with one non-Text chunk
|
||||
openingTag += '${' + attribute.chunks[0].snippet + ' ? " ' + attribute.name + '" : "" }';
|
||||
} else if (attribute.name === 'class' && classExpr) {
|
||||
addClassAttribute = false;
|
||||
openingTag += ` class="\${[\`${attribute.stringifyForSsr()}\`, ${classExpr}].join(' ').trim() }"`;
|
||||
} else {
|
||||
openingTag += ` ${attribute.name}="${attribute.stringifyForSsr()}"`;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
node.bindings.forEach(binding => {
|
||||
const { name, value: { snippet } } = binding;
|
||||
|
||||
if (name === 'group') {
|
||||
// TODO server-render group bindings
|
||||
} else {
|
||||
openingTag += ' ${(v => v ? ("' + name + '" + (v === true ? "" : "=" + JSON.stringify(v))) : "")(' + snippet + ')}';
|
||||
}
|
||||
});
|
||||
|
||||
if (addClassAttribute) {
|
||||
openingTag += `\${((v) => v ? ' class="' + v + '"' : '')([${classExpr}].join(' ').trim())}`;
|
||||
}
|
||||
|
||||
openingTag += '>';
|
||||
|
||||
renderer.append(openingTag);
|
||||
|
||||
if (node.name === 'textarea' && textareaContents !== undefined) {
|
||||
renderer.append(textareaContents);
|
||||
} else {
|
||||
renderer.render(node.children, options);
|
||||
}
|
||||
|
||||
if (!isVoidElementName(node.name)) {
|
||||
renderer.append(`</${node.name}>`);
|
||||
}
|
||||
}
|
@ -0,0 +1,7 @@
|
||||
export default function(node, renderer, options) {
|
||||
renderer.append('${(__result.head += `');
|
||||
|
||||
renderer.render(node.children, options);
|
||||
|
||||
renderer.append('`, "")}');
|
||||
}
|
@ -0,0 +1,3 @@
|
||||
export default function(node, renderer, options) {
|
||||
renderer.append('${' + node.expression.snippet + '}');
|
||||
}
|
@ -0,0 +1,15 @@
|
||||
export default function(node, renderer, options) {
|
||||
const { snippet } = node.expression;
|
||||
|
||||
renderer.append('${ ' + snippet + ' ? `');
|
||||
|
||||
renderer.render(node.children, options);
|
||||
|
||||
renderer.append('` : `');
|
||||
|
||||
if (node.else) {
|
||||
renderer.render(node.else.children, options);
|
||||
}
|
||||
|
||||
renderer.append('` }');
|
||||
}
|
@ -0,0 +1,130 @@
|
||||
import { escape, escapeTemplate, stringify } from '../../../utils/stringify';
|
||||
import getObject from '../../../utils/getObject';
|
||||
import getTailSnippet from '../../../utils/getTailSnippet';
|
||||
import { quoteNameIfNecessary, quotePropIfNecessary } from '../../../utils/quoteIfNecessary';
|
||||
import deindent from '../../../utils/deindent';
|
||||
|
||||
type AppendTarget = any; // TODO
|
||||
|
||||
export default function(node, renderer, options) {
|
||||
function stringifyAttribute(chunk: Node) {
|
||||
if (chunk.type === 'Text') {
|
||||
return escapeTemplate(escape(chunk.data));
|
||||
}
|
||||
|
||||
return '${@escape( ' + chunk.snippet + ')}';
|
||||
}
|
||||
|
||||
const bindingProps = node.bindings.map(binding => {
|
||||
const { name } = getObject(binding.value.node);
|
||||
const tail = binding.value.node.type === 'MemberExpression'
|
||||
? getTailSnippet(binding.value.node)
|
||||
: '';
|
||||
|
||||
return `${quoteNameIfNecessary(binding.name)}: ctx${quotePropIfNecessary(name)}${tail}`;
|
||||
});
|
||||
|
||||
function getAttributeValue(attribute) {
|
||||
if (attribute.isTrue) return `true`;
|
||||
if (attribute.chunks.length === 0) return `''`;
|
||||
|
||||
if (attribute.chunks.length === 1) {
|
||||
const chunk = attribute.chunks[0];
|
||||
if (chunk.type === 'Text') {
|
||||
return stringify(chunk.data);
|
||||
}
|
||||
|
||||
return chunk.snippet;
|
||||
}
|
||||
|
||||
return '`' + attribute.chunks.map(stringifyAttribute).join('') + '`';
|
||||
}
|
||||
|
||||
const usesSpread = node.attributes.find(attr => attr.isSpread);
|
||||
|
||||
const props = usesSpread
|
||||
? `Object.assign(${
|
||||
node.attributes
|
||||
.map(attribute => {
|
||||
if (attribute.isSpread) {
|
||||
return attribute.expression.snippet;
|
||||
} else {
|
||||
return `{ ${quoteNameIfNecessary(attribute.name)}: ${getAttributeValue(attribute)} }`;
|
||||
}
|
||||
})
|
||||
.concat(bindingProps.map(p => `{ ${p} }`))
|
||||
.join(', ')
|
||||
})`
|
||||
: `{ ${node.attributes
|
||||
.map(attribute => `${quoteNameIfNecessary(attribute.name)}: ${getAttributeValue(attribute)}`)
|
||||
.concat(bindingProps)
|
||||
.join(', ')} }`;
|
||||
|
||||
const expression = (
|
||||
node.name === 'svelte:self'
|
||||
? node.component.name
|
||||
: node.name === 'svelte:component'
|
||||
? `((${node.expression.snippet}) || @missingComponent)`
|
||||
: `%components-${node.name}`
|
||||
);
|
||||
|
||||
node.bindings.forEach(binding => {
|
||||
const conditions = [];
|
||||
|
||||
let parent = node;
|
||||
while (parent = parent.parent) {
|
||||
if (parent.type === 'IfBlock') {
|
||||
// TODO handle contextual bindings...
|
||||
conditions.push(`(${parent.expression.snippet})`);
|
||||
}
|
||||
}
|
||||
|
||||
conditions.push(
|
||||
`!('${binding.name}' in ctx)`,
|
||||
`${expression}.data`
|
||||
);
|
||||
|
||||
const { name } = getObject(binding.value.node);
|
||||
|
||||
renderer.bindings.push(deindent`
|
||||
if (${conditions.reverse().join('&&')}) {
|
||||
tmp = ${expression}.data();
|
||||
if ('${name}' in tmp) {
|
||||
ctx${quotePropIfNecessary(binding.name)} = tmp.${name};
|
||||
settled = false;
|
||||
}
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
let open = `\${@validateSsrComponent(${expression}, '${node.name}')._render(__result, ${props}`;
|
||||
|
||||
const component_options = [];
|
||||
component_options.push(`store: options.store`);
|
||||
|
||||
if (node.children.length) {
|
||||
const target: AppendTarget = {
|
||||
slots: { default: '' },
|
||||
slotStack: ['default']
|
||||
};
|
||||
|
||||
renderer.targets.push(target);
|
||||
|
||||
renderer.render(node.children, options);
|
||||
|
||||
const slotted = Object.keys(target.slots)
|
||||
.map(name => `${quoteNameIfNecessary(name)}: () => \`${target.slots[name]}\``)
|
||||
.join(', ');
|
||||
|
||||
component_options.push(`slotted: { ${slotted} }`);
|
||||
|
||||
renderer.targets.pop();
|
||||
}
|
||||
|
||||
if (component_options.length) {
|
||||
open += `, { ${component_options.join(', ')} }`;
|
||||
}
|
||||
|
||||
renderer.append(open);
|
||||
renderer.append(')}');
|
||||
}
|
@ -0,0 +1,14 @@
|
||||
import { quotePropIfNecessary } from '../../../utils/quoteIfNecessary';
|
||||
|
||||
export default function(node, renderer, options) {
|
||||
const name = node.attributes.find(attribute => attribute.name === 'name');
|
||||
|
||||
const slotName = name && name.chunks[0].data || 'default';
|
||||
const prop = quotePropIfNecessary(slotName);
|
||||
|
||||
renderer.append(`\${options && options.slotted && options.slotted${prop} ? options.slotted${prop}() : \``);
|
||||
|
||||
renderer.render(node.children, options);
|
||||
|
||||
renderer.append(`\`}`);
|
||||
}
|
@ -0,0 +1,9 @@
|
||||
export default function(node, renderer, options) {
|
||||
renderer.append(
|
||||
node.parent &&
|
||||
node.parent.type === 'Element' &&
|
||||
node.parent.name === 'style'
|
||||
? '${' + node.expression.snippet + '}'
|
||||
: '${@escape(' + node.expression.snippet + ')}'
|
||||
);
|
||||
}
|
@ -0,0 +1,14 @@
|
||||
import { escapeHTML, escapeTemplate, escape } from '../../../utils/stringify';
|
||||
|
||||
export default function(node, renderer, options) {
|
||||
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);
|
||||
}
|
||||
renderer.append(escape(escapeTemplate(text)));
|
||||
}
|
@ -0,0 +1,7 @@
|
||||
export default function(node, renderer, options) {
|
||||
renderer.append(`<title>`);
|
||||
|
||||
renderer.render(node.children, options);
|
||||
|
||||
renderer.append(`</title>`);
|
||||
}
|
@ -0,0 +1,16 @@
|
||||
import checkForDupes from '../utils/checkForDupes';
|
||||
import checkForComputedKeys from '../utils/checkForComputedKeys';
|
||||
import { Node } from '../../../../interfaces';
|
||||
import Component from '../../../Component';
|
||||
|
||||
export default function actions(component: Component, prop: Node) {
|
||||
if (prop.value.type !== 'ObjectExpression') {
|
||||
component.error(prop, {
|
||||
code: `invalid-actions`,
|
||||
message: `The 'actions' property must be an object literal`
|
||||
});
|
||||
}
|
||||
|
||||
checkForDupes(component, prop.value.properties);
|
||||
checkForComputedKeys(component, prop.value.properties);
|
||||
}
|
@ -0,0 +1,21 @@
|
||||
import checkForDupes from '../utils/checkForDupes';
|
||||
import checkForComputedKeys from '../utils/checkForComputedKeys';
|
||||
import { Node } from '../../../../interfaces';
|
||||
import Component from '../../../Component';
|
||||
|
||||
export default function transitions(component: Component, prop: Node) {
|
||||
if (prop.value.type !== 'ObjectExpression') {
|
||||
component.error(prop, {
|
||||
code: `invalid-transitions-property`,
|
||||
message: `The 'transitions' property must be an object literal`
|
||||
});
|
||||
}
|
||||
|
||||
checkForDupes(component, prop.value.properties);
|
||||
checkForComputedKeys(component, prop.value.properties);
|
||||
|
||||
prop.value.properties.forEach(() => {
|
||||
// TODO probably some validation that can happen here...
|
||||
// checking for use of `this` etc?
|
||||
});
|
||||
}
|
@ -1,33 +1,33 @@
|
||||
import checkForDupes from '../utils/checkForDupes';
|
||||
import checkForComputedKeys from '../utils/checkForComputedKeys';
|
||||
import getName from '../../../utils/getName';
|
||||
import { Validator } from '../../index';
|
||||
import { Node } from '../../../interfaces';
|
||||
import getName from '../../../../utils/getName';
|
||||
import { Node } from '../../../../interfaces';
|
||||
import Component from '../../../Component';
|
||||
|
||||
export default function components(validator: Validator, prop: Node) {
|
||||
export default function components(component: Component, prop: Node) {
|
||||
if (prop.value.type !== 'ObjectExpression') {
|
||||
validator.error(prop, {
|
||||
component.error(prop, {
|
||||
code: `invalid-components-property`,
|
||||
message: `The 'components' property must be an object literal`
|
||||
});
|
||||
}
|
||||
|
||||
checkForDupes(validator, prop.value.properties);
|
||||
checkForComputedKeys(validator, prop.value.properties);
|
||||
checkForDupes(component, prop.value.properties);
|
||||
checkForComputedKeys(component, prop.value.properties);
|
||||
|
||||
prop.value.properties.forEach((component: Node) => {
|
||||
const name = getName(component.key);
|
||||
prop.value.properties.forEach((node: Node) => {
|
||||
const name = getName(node.key);
|
||||
|
||||
if (name === 'state') {
|
||||
// TODO is this still true?
|
||||
validator.error(component, {
|
||||
component.error(node, {
|
||||
code: `invalid-name`,
|
||||
message: `Component constructors cannot be called 'state' due to technical limitations`
|
||||
});
|
||||
}
|
||||
|
||||
if (!/^[A-Z]/.test(name)) {
|
||||
validator.error(component, {
|
||||
component.error(node, {
|
||||
code: `component-lowercase`,
|
||||
message: `Component names must be capitalised`
|
||||
});
|
@ -1,13 +1,13 @@
|
||||
import { Validator } from '../../index';
|
||||
import { Node } from '../../../interfaces';
|
||||
import { Node } from '../../../../interfaces';
|
||||
import Component from '../../../Component';
|
||||
|
||||
const disallowed = new Set(['Literal', 'ObjectExpression', 'ArrayExpression']);
|
||||
|
||||
export default function data(validator: Validator, prop: Node) {
|
||||
export default function data(component: Component, prop: Node) {
|
||||
while (prop.type === 'ParenthesizedExpression') prop = prop.expression;
|
||||
|
||||
if (disallowed.has(prop.value.type)) {
|
||||
validator.error(prop.value, {
|
||||
component.error(prop.value, {
|
||||
code: `invalid-data-property`,
|
||||
message: `'data' must be a function`
|
||||
});
|
@ -0,0 +1,16 @@
|
||||
import checkForDupes from '../utils/checkForDupes';
|
||||
import checkForComputedKeys from '../utils/checkForComputedKeys';
|
||||
import { Node } from '../../../../interfaces';
|
||||
import Component from '../../../Component';
|
||||
|
||||
export default function events(component: Component, prop: Node) {
|
||||
if (prop.value.type !== 'ObjectExpression') {
|
||||
component.error(prop, {
|
||||
code: `invalid-events-property`,
|
||||
message: `The 'events' property must be an object literal`
|
||||
});
|
||||
}
|
||||
|
||||
checkForDupes(component, prop.value.properties);
|
||||
checkForComputedKeys(component, prop.value.properties);
|
||||
}
|
@ -1,9 +1,9 @@
|
||||
import { Validator } from '../../index';
|
||||
import { Node } from '../../../interfaces';
|
||||
import { Node } from '../../../../interfaces';
|
||||
import Component from '../../../Component';
|
||||
|
||||
export default function immutable(validator: Validator, prop: Node) {
|
||||
export default function immutable(component: Component, prop: Node) {
|
||||
if (prop.value.type !== 'Literal' || typeof prop.value.value !== 'boolean') {
|
||||
validator.error(prop.value, {
|
||||
component.error(prop.value, {
|
||||
code: `invalid-immutable-property`,
|
||||
message: `'immutable' must be a boolean literal`
|
||||
});
|
@ -1,11 +1,11 @@
|
||||
import usesThisOrArguments from '../utils/usesThisOrArguments';
|
||||
import { Validator } from '../../index';
|
||||
import { Node } from '../../../interfaces';
|
||||
import { Node } from '../../../../interfaces';
|
||||
import Component from '../../../Component';
|
||||
|
||||
export default function oncreate(validator: Validator, prop: Node) {
|
||||
export default function oncreate(component: Component, prop: Node) {
|
||||
if (prop.value.type === 'ArrowFunctionExpression') {
|
||||
if (usesThisOrArguments(prop.value.body)) {
|
||||
validator.error(prop, {
|
||||
component.error(prop, {
|
||||
code: `invalid-oncreate-property`,
|
||||
message: `'oncreate' should be a function expression, not an arrow function expression`
|
||||
});
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue