first eslint run

pull/2958/head
43081j 7 years ago
parent 47af18af9d
commit c3870fa2c8

@ -2,12 +2,12 @@
"root": true, "root": true,
"rules": { "rules": {
"indent": "off", "indent": "off",
"no-unused-vars": "off",
"semi": [2, "always"], "semi": [2, "always"],
"keyword-spacing": [2, { "before": true, "after": true }], "keyword-spacing": [2, { "before": true, "after": true }],
"space-before-blocks": [2, "always"], "space-before-blocks": [2, "always"],
"no-mixed-spaces-and-tabs": [2, "smart-tabs"], "no-mixed-spaces-and-tabs": [2, "smart-tabs"],
"no-cond-assign": 0, "no-cond-assign": 0,
"no-unused-vars": 2,
"object-shorthand": [2, "always"], "object-shorthand": [2, "always"],
"no-const-assign": 2, "no-const-assign": 2,
"no-class-assign": 2, "no-class-assign": 2,
@ -22,10 +22,17 @@
"arrow-spacing": 2, "arrow-spacing": 2,
"no-inner-declarations": 0, "no-inner-declarations": 0,
"@typescript-eslint/indent": [2, "tab", { "SwitchCase": 1 }], "@typescript-eslint/indent": [2, "tab", { "SwitchCase": 1 }],
"@typescript-eslint/explicit-function-return-type": ["error", { "@typescript-eslint/camelcase": "off",
"allowExpressions": true "@typescript-eslint/array-type": ["error", "array-simple"],
"@typescript-eslint/explicit-function-return-type": "off",
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/explicit-member-accessibility": "off",
"@typescript-eslint/no-unused-vars": ["error", {
"argsIgnorePattern": "^_"
}], }],
"@typescript-eslint/camelcase": "off" "@typescript-eslint/no-object-literal-type-assertion": ["error", {
"allowAsParameter": true
}]
}, },
"env": { "env": {
"es6": true, "es6": true,
@ -45,6 +52,20 @@
"sourceType": "module" "sourceType": "module"
}, },
"settings": { "settings": {
"import/core-modules": ["svelte"] "import/core-modules": [
} "svelte",
"svelte/internal",
"svelte/store",
"svelte/easing",
"estree"
]
},
"overrides": [
{
"files": ["*.js"],
"rules": {
"@typescript-eslint/no-var-requires": "off"
}
}
]
} }

@ -5,7 +5,7 @@ const now = (typeof process !== 'undefined' && process.hrtime)
} }
: () => self.performance.now(); : () => self.performance.now();
type Timing = { interface Timing {
label: string; label: string;
start: number; start: number;
end: number; end: number;

@ -24,13 +24,13 @@ import unwrap_parens from './utils/unwrap_parens';
import Slot from './nodes/Slot'; import Slot from './nodes/Slot';
import { Node as ESTreeNode } from 'estree'; import { Node as ESTreeNode } from 'estree';
type ComponentOptions = { interface ComponentOptions {
namespace?: string; namespace?: string;
tag?: string; tag?: string;
immutable?: boolean; immutable?: boolean;
accessors?: boolean; accessors?: boolean;
preserveWhitespace?: boolean; preserveWhitespace?: boolean;
}; }
// We need to tell estree-walker that it should always // We need to tell estree-walker that it should always
// look for an `else` block, otherwise it might get // look for an `else` block, otherwise it might get
@ -67,6 +67,120 @@ function remove_node(code: MagicString, start: number, end: number, body: Node,
return; return;
} }
function process_component_options(component: Component, nodes) {
const component_options: ComponentOptions = {
immutable: component.compile_options.immutable || false,
accessors: 'accessors' in component.compile_options
? component.compile_options.accessors
: !!component.compile_options.customElement,
preserveWhitespace: !!component.compile_options.preserveWhitespace
};
const node = nodes.find(node => node.name === 'svelte:options');
function get_value(attribute, code, message) {
const { value } = attribute;
const chunk = value[0];
if (!chunk) return true;
if (value.length > 1) {
component.error(attribute, { code, message });
}
if (chunk.type === 'Text') return chunk.data;
if (chunk.expression.type !== 'Literal') {
component.error(attribute, { code, message });
}
return chunk.expression.value;
}
if (node) {
node.attributes.forEach(attribute => {
if (attribute.type === 'Attribute') {
const { name } = attribute;
switch (name) {
case 'tag': {
const code = 'invalid-tag-attribute';
const message = `'tag' must be a string literal`;
const tag = get_value(attribute, code, message);
if (typeof tag !== 'string' && tag !== null) component.error(attribute, { code, message });
if (tag && !/^[a-zA-Z][a-zA-Z0-9]*-[a-zA-Z0-9-]+$/.test(tag)) {
component.error(attribute, {
code: `invalid-tag-property`,
message: `tag name must be two or more words joined by the '-' character`
});
}
component_options.tag = tag;
break;
}
case 'namespace': {
const code = 'invalid-namespace-attribute';
const message = `The 'namespace' attribute must be a string literal representing a valid namespace`;
const ns = get_value(attribute, code, message);
if (typeof ns !== 'string') component.error(attribute, { code, message });
if (valid_namespaces.indexOf(ns) === -1) {
const match = fuzzymatch(ns, valid_namespaces);
if (match) {
component.error(attribute, {
code: `invalid-namespace-property`,
message: `Invalid namespace '${ns}' (did you mean '${match}'?)`
});
} else {
component.error(attribute, {
code: `invalid-namespace-property`,
message: `Invalid namespace '${ns}'`
});
}
}
component_options.namespace = ns;
break;
}
case 'accessors':
case 'immutable':
case 'preserveWhitespace':
{
const code = `invalid-${name}-value`;
const message = `${name} attribute must be true or false`;
const value = get_value(attribute, code, message);
if (typeof value !== 'boolean') component.error(attribute, { code, message });
component_options[name] = value;
break;
}
default:
component.error(attribute, {
code: `invalid-options-attribute`,
message: `<svelte:options> unknown attribute`
});
}
}
else {
component.error(attribute, {
code: `invalid-options-attribute`,
message: `<svelte:options> can only have static 'tag', 'namespace', 'accessors', 'immutable' and 'preserveWhitespace' attributes`
});
}
});
}
return component_options;
}
export default class Component { export default class Component {
stats: Stats; stats: Stats;
warnings: Warning[]; warnings: Warning[];
@ -97,7 +211,7 @@ export default class Component {
node_for_declaration: Map<string, Node> = new Map(); node_for_declaration: Map<string, Node> = new Map();
partly_hoisted: string[] = []; partly_hoisted: string[] = [];
fully_hoisted: string[] = []; fully_hoisted: string[] = [];
reactive_declarations: Array<{ assignees: Set<string>, dependencies: Set<string>, node: Node, declaration: Node }> = []; reactive_declarations: Array<{ assignees: Set<string>; dependencies: Set<string>; node: Node; declaration: Node }> = [];
reactive_declaration_nodes: Set<Node> = new Set(); reactive_declaration_nodes: Set<Node> = new Set();
has_reactive_assignments = false; has_reactive_assignments = false;
injected_reactive_declaration_vars: Set<string> = new Set(); injected_reactive_declaration_vars: Set<string> = new Set();
@ -106,12 +220,12 @@ export default class Component {
indirect_dependencies: Map<string, Set<string>> = new Map(); indirect_dependencies: Map<string, Set<string>> = new Map();
file: string; file: string;
locate: (c: number) => { line: number, column: number }; locate: (c: number) => { line: number; column: number };
// TODO this does the same as component.locate! remove one or the other // TODO this does the same as component.locate! remove one or the other
locator: (search: number, startIndex?: number) => { locator: (search: number, startIndex?: number) => {
line: number, line: number;
column: number column: number;
}; };
stylesheet: Stylesheet; stylesheet: Stylesheet;
@ -140,6 +254,7 @@ export default class Component {
this.compile_options = compile_options; this.compile_options = compile_options;
this.file = compile_options.filename && ( this.file = compile_options.filename && (
// eslint-disable-next-line no-useless-escape
typeof process !== 'undefined' ? compile_options.filename.replace(process.cwd(), '').replace(/^[\/\\]/, '') : compile_options.filename typeof process !== 'undefined' ? compile_options.filename.replace(process.cwd(), '').replace(/^[\/\\]/, '') : compile_options.filename
); );
this.locate = getLocator(this.source); this.locate = getLocator(this.source);
@ -283,7 +398,7 @@ export default class Component {
this.source this.source
); );
const parts = module.split(']'); const parts = module.split('✂]');
const final_chunk = parts.pop(); const final_chunk = parts.pop();
const compiled = new Bundle({ separator: '' }); const compiled = new Bundle({ separator: '' });
@ -296,7 +411,7 @@ export default class Component {
const { filename } = compile_options; const { filename } = compile_options;
// special case the source file doesn't actually get used anywhere. we need // 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 // to add an empty file to populate map.sources and map.sourcesContent
if (!parts.length) { if (!parts.length) {
compiled.addSource({ compiled.addSource({
@ -305,7 +420,7 @@ export default class Component {
}); });
} }
const pattern = /\[(\d+)-(\d+)$/; const pattern = /\[✂(\d+)-(\d+)$/;
parts.forEach((str: string) => { parts.forEach((str: string) => {
const chunk = str.replace(pattern, ''); const chunk = str.replace(pattern, '');
@ -398,12 +513,12 @@ export default class Component {
error( error(
pos: { pos: {
start: number, start: number;
end: number end: number;
}, },
e : { e: {
code: string, code: string;
message: string message: string;
} }
) { ) {
error(e.message, { error(e.message, {
@ -418,12 +533,12 @@ export default class Component {
warn( warn(
pos: { pos: {
start: number, start: number;
end: number end: number;
}, },
warning: { warning: {
code: string, code: string;
message: string message: string;
} }
) { ) {
if (!this.locator) { if (!this.locator) {
@ -527,9 +642,9 @@ export default class Component {
let result = ''; let result = '';
script.content.body.forEach((node, i) => { script.content.body.forEach((node) => {
if (this.hoistable_nodes.has(node) || this.reactive_declaration_nodes.has(node)) { if (this.hoistable_nodes.has(node) || this.reactive_declaration_nodes.has(node)) {
if (a !== b) result += `[${a}-${b}]`; if (a !== b) result += `[✂${a}-${b}✂]`;
a = node.end; a = node.end;
} }
@ -541,7 +656,7 @@ export default class Component {
b = script.content.end; b = script.content.end;
while (/\s/.test(this.source[b - 1])) b -= 1; while (/\s/.test(this.source[b - 1])) b -= 1;
if (a < b) result += `[${a}-${b}]`; if (a < b) result += `[✂${a}-${b}✂]`;
return result || null; return result || null;
} }
@ -564,7 +679,7 @@ export default class Component {
this.add_sourcemap_locations(script.content); this.add_sourcemap_locations(script.content);
let { scope, globals } = create_scopes(script.content); const { scope, globals } = create_scopes(script.content);
this.module_scope = scope; this.module_scope = scope;
scope.declarations.forEach((node, name) => { scope.declarations.forEach((node, name) => {
@ -588,7 +703,7 @@ export default class Component {
this.error(node, { this.error(node, {
code: 'illegal-subscription', code: 'illegal-subscription',
message: `Cannot reference store value inside <script context="module">` message: `Cannot reference store value inside <script context="module">`
}) });
} else { } else {
this.add_var({ this.add_var({
name, name,
@ -624,7 +739,7 @@ export default class Component {
}); });
}); });
let { scope: instance_scope, map, globals } = create_scopes(script.content); const { scope: instance_scope, map, globals } = create_scopes(script.content);
this.instance_scope = instance_scope; this.instance_scope = instance_scope;
this.instance_scope_map = map; this.instance_scope_map = map;
@ -705,7 +820,7 @@ export default class Component {
let scope = instance_scope; let scope = instance_scope;
walk(this.ast.instance.content, { walk(this.ast.instance.content, {
enter(node, parent) { enter(node) {
if (map.has(node)) { if (map.has(node)) {
scope = map.get(node); scope = map.get(node);
} }
@ -738,7 +853,7 @@ export default class Component {
scope = scope.parent; scope = scope.parent;
} }
} }
}) });
} }
extract_reactive_store_references() { extract_reactive_store_references() {
@ -786,7 +901,7 @@ export default class Component {
} }
if (name[0] === '$' && name[1] !== '$') { if (name[0] === '$' && name[1] !== '$') {
return `${name.slice(1)}.set(${name})` return `${name.slice(1)}.set(${name})`;
} }
if (variable && !variable.referenced && !variable.is_reactive_dependency && !variable.export_name && !name.startsWith('$$')) { if (variable && !variable.referenced && !variable.is_reactive_dependency && !variable.export_name && !name.startsWith('$$')) {
@ -888,13 +1003,13 @@ export default class Component {
} }
if (variable.writable && variable.name !== variable.export_name) { if (variable.writable && variable.name !== variable.export_name) {
code.prependRight(declarator.id.start, `${variable.export_name}: `) code.prependRight(declarator.id.start, `${variable.export_name}: `);
} }
if (next) { if (next) {
const next_variable = component.var_lookup.get(next.id.name) const next_variable = component.var_lookup.get(next.id.name);
const new_declaration = !next_variable.export_name const new_declaration = !next_variable.export_name
|| (current_group.insert && next_variable.subscribable) || (current_group.insert && next_variable.subscribable);
if (new_declaration) { if (new_declaration) {
code.overwrite(declarator.end, next.start, ` ${node.kind} `); code.overwrite(declarator.end, next.start, ` ${node.kind} `);
@ -904,7 +1019,7 @@ export default class Component {
current_group = null; current_group = null;
if (variable.subscribable) { if (variable.subscribable) {
let insert = get_insert(variable); const insert = get_insert(variable);
if (next) { if (next) {
code.overwrite(declarator.end, next.start, `; ${insert}; ${node.kind} `); code.overwrite(declarator.end, next.start, `; ${insert}; ${node.kind} `);
@ -975,9 +1090,9 @@ export default class Component {
if (!d.init) return false; if (!d.init) return false;
if (d.init.type !== 'Literal') return false; if (d.init.type !== 'Literal') return false;
const v = this.var_lookup.get(d.id.name) const v = this.var_lookup.get(d.id.name);
if (v.reassigned) return false if (v.reassigned) return false;
if (v.export_name) return false if (v.export_name) return false;
if (this.var_lookup.get(d.id.name).reassigned) return false; if (this.var_lookup.get(d.id.name).reassigned) return false;
if (this.vars.find(variable => variable.name === d.id.name && variable.module)) return false; if (this.vars.find(variable => variable.name === d.id.name && variable.module)) return false;
@ -992,7 +1107,7 @@ export default class Component {
}); });
hoistable_nodes.add(node); hoistable_nodes.add(node);
this.fully_hoisted.push(`[${node.start}-${node.end}]`); this.fully_hoisted.push(`[✂${node.start}-${node.end}✂]`);
} }
} }
@ -1006,7 +1121,7 @@ export default class Component {
}); });
const checked = new Set(); const checked = new Set();
let walking = new Set(); const walking = new Set();
const is_hoistable = fn_declaration => { const is_hoistable = fn_declaration => {
if (fn_declaration.type === 'ExportNamedDeclaration') { if (fn_declaration.type === 'ExportNamedDeclaration') {
@ -1015,7 +1130,7 @@ export default class Component {
const instance_scope = this.instance_scope; const instance_scope = this.instance_scope;
let scope = this.instance_scope; let scope = this.instance_scope;
let map = this.instance_scope_map; const map = this.instance_scope_map;
let hoistable = true; let hoistable = true;
@ -1051,7 +1166,7 @@ export default class Component {
hoistable = false; hoistable = false;
} else if (!is_hoistable(other_declaration)) { } else if (!is_hoistable(other_declaration)) {
hoistable = false; hoistable = false;
} }
} }
else { else {
@ -1084,7 +1199,7 @@ export default class Component {
remove_indentation(this.code, node); remove_indentation(this.code, node);
this.fully_hoisted.push(`[${node.start}-${node.end}]`); this.fully_hoisted.push(`[✂${node.start}-${node.end}✂]`);
} }
} }
} }
@ -1103,7 +1218,7 @@ export default class Component {
const dependencies = new Set(); const dependencies = new Set();
let scope = this.instance_scope; let scope = this.instance_scope;
let map = this.instance_scope_map; const map = this.instance_scope_map;
walk(node.body, { walk(node.body, {
enter(node, parent) { enter(node, parent) {
@ -1236,115 +1351,3 @@ export default class Component {
}); });
} }
} }
function process_component_options(component: Component, nodes) {
const component_options: ComponentOptions = {
immutable: component.compile_options.immutable || false,
accessors: 'accessors' in component.compile_options
? component.compile_options.accessors
: !!component.compile_options.customElement,
preserveWhitespace: !!component.compile_options.preserveWhitespace
};
const node = nodes.find(node => node.name === 'svelte:options');
function get_value(attribute, code, message) {
const { value } = attribute;
const chunk = value[0];
if (!chunk) return true;
if (value.length > 1) {
component.error(attribute, { code, message });
}
if (chunk.type === 'Text') return chunk.data;
if (chunk.expression.type !== 'Literal') {
component.error(attribute, { code, message });
}
return chunk.expression.value;
}
if (node) {
node.attributes.forEach(attribute => {
if (attribute.type === 'Attribute') {
const { name } = attribute;
switch (name) {
case 'tag': {
const code = 'invalid-tag-attribute';
const message = `'tag' must be a string literal`;
const tag = get_value(attribute, code, message);
if (typeof tag !== 'string' && tag !== null) component.error(attribute, { code, message });
if (tag && !/^[a-zA-Z][a-zA-Z0-9]*-[a-zA-Z0-9-]+$/.test(tag)) {
component.error(attribute, {
code: `invalid-tag-property`,
message: `tag name must be two or more words joined by the '-' character`
});
}
component_options.tag = tag;
break;
}
case 'namespace': {
const code = 'invalid-namespace-attribute';
const message = `The 'namespace' attribute must be a string literal representing a valid namespace`;
const ns = get_value(attribute, code, message);
if (typeof ns !== 'string') component.error(attribute, { code, message });
if (valid_namespaces.indexOf(ns) === -1) {
const match = fuzzymatch(ns, valid_namespaces);
if (match) {
component.error(attribute, {
code: `invalid-namespace-property`,
message: `Invalid namespace '${ns}' (did you mean '${match}'?)`
});
} else {
component.error(attribute, {
code: `invalid-namespace-property`,
message: `Invalid namespace '${ns}'`
});
}
}
component_options.namespace = ns;
break;
}
case 'accessors':
case 'immutable':
case 'preserveWhitespace':
const code = `invalid-${name}-value`;
const message = `${name} attribute must be true or false`
const value = get_value(attribute, code, message);
if (typeof value !== 'boolean') component.error(attribute, { code, message });
component_options[name] = value;
break;
default:
component.error(attribute, {
code: `invalid-options-attribute`,
message: `<svelte:options> unknown attribute`
});
}
}
else {
component.error(attribute, {
code: `invalid-options-attribute`,
message: `<svelte:options> can only have static 'tag', 'namespace', 'accessors', 'immutable' and 'preserveWhitespace' attributes`
});
}
});
}
return component_options;
}

@ -3,33 +3,9 @@ import list from '../utils/list';
import { ModuleFormat, Node } from '../interfaces'; import { ModuleFormat, Node } from '../interfaces';
import { stringify_props } from './utils/stringify_props'; import { stringify_props } from './utils/stringify_props';
const wrappers = { esm, cjs }; interface Export {
type Export = {
name: string; name: string;
as: string; as: string;
};
export default function create_module(
code: string,
format: ModuleFormat,
name: string,
banner: string,
sveltePath = 'svelte',
helpers: { name: string, alias: string }[],
imports: Node[],
module_exports: Export[],
source: string
): string {
const internal_path = `${sveltePath}/internal`;
if (format === 'esm') {
return esm(code, name, banner, sveltePath, internal_path, helpers, imports, module_exports, source);
}
if (format === 'cjs') return cjs(code, name, banner, sveltePath, internal_path, helpers, imports, module_exports);
throw new Error(`options.format is invalid (must be ${list(Object.keys(wrappers))})`);
} }
function edit_source(source, sveltePath) { function edit_source(source, sveltePath) {
@ -44,7 +20,7 @@ function esm(
banner: string, banner: string,
sveltePath: string, sveltePath: string,
internal_path: string, internal_path: string,
helpers: { name: string, alias: string }[], helpers: Array<{ name: string; alias: string }>,
imports: Node[], imports: Node[],
module_exports: Export[], module_exports: Export[],
source: string source: string
@ -84,7 +60,7 @@ function cjs(
banner: string, banner: string,
sveltePath: string, sveltePath: string,
internal_path: string, internal_path: string,
helpers: { name: string, alias: string }[], helpers: Array<{ name: string; alias: string }>,
imports: Node[], imports: Node[],
module_exports: Export[] module_exports: Export[]
) { ) {
@ -115,7 +91,7 @@ function cjs(
const source = edit_source(node.source.value, sveltePath); const source = edit_source(node.source.value, sveltePath);
return `const ${lhs} = require("${source}");` return `const ${lhs} = require("${source}");`;
}); });
const exports = [`exports.default = ${name};`].concat( const exports = [`exports.default = ${name};`].concat(
@ -131,5 +107,29 @@ function cjs(
${code} ${code}
${exports}` ${exports}`;
} }
const wrappers = { esm, cjs };
export default function create_module(
code: string,
format: ModuleFormat,
name: string,
banner: string,
sveltePath = 'svelte',
helpers: Array<{ name: string; alias: string }>,
imports: Node[],
module_exports: Export[],
source: string
): string {
const internal_path = `${sveltePath}/internal`;
if (format === 'esm') {
return esm(code, name, banner, sveltePath, internal_path, helpers, imports, module_exports, source);
}
if (format === 'cjs') return cjs(code, name, banner, sveltePath, internal_path, helpers, imports, module_exports);
throw new Error(`options.format is invalid (must be ${list(Object.keys(wrappers))})`);
}

@ -4,121 +4,102 @@ import { gather_possible_values, UNKNOWN } from './gather_possible_values';
import { Node } from '../../interfaces'; import { Node } from '../../interfaces';
import Component from '../Component'; import Component from '../Component';
export default class Selector { class Block {
node: Node; global: boolean;
stylesheet: Stylesheet; combinator: Node;
blocks: Block[]; selectors: Node[]
local_blocks: Block[]; start: number;
used: boolean; end: number;
should_encapsulate: boolean;
constructor(node: Node, stylesheet: Stylesheet) { constructor(combinator: Node) {
this.node = node; this.combinator = combinator;
this.stylesheet = stylesheet; this.global = false;
this.selectors = [];
this.blocks = group_selectors(node); this.start = null;
this.end = null;
// take trailing :global(...) selectors out of consideration this.should_encapsulate = false;
let i = this.blocks.length; }
while (i > 0) {
if (!this.blocks[i - 1].global) break; add(selector: Node) {
i -= 1; if (this.selectors.length === 0) {
this.start = selector.start;
this.global = selector.type === 'PseudoClassSelector' && selector.name === 'global';
} }
this.local_blocks = this.blocks.slice(0, i); this.selectors.push(selector);
this.used = this.blocks[0].global; this.end = selector.end;
} }
}
apply(node: Node, stack: Node[]) { function group_selectors(selector: Node) {
const to_encapsulate: Node[] = []; let block: Block = new Block(null);
apply_selector(this.stylesheet, this.local_blocks.slice(), node, stack.slice(), to_encapsulate);
if (to_encapsulate.length > 0) { const blocks = [block];
to_encapsulate.filter((_, i) => i === 0 || i === to_encapsulate.length - 1).forEach(({ node, block }) => {
this.stylesheet.nodes_with_css_class.add(node);
block.should_encapsulate = true;
});
this.used = true; selector.children.forEach((child: Node) => {
if (child.type === 'WhiteSpace' || child.type === 'Combinator') {
block = new Block(child);
blocks.push(block);
} else {
block.add(child);
} }
} });
minify(code: MagicString) {
let c: number = null;
this.blocks.forEach((block, i) => {
if (i > 0) {
if (block.start - c > 1) {
code.overwrite(c, block.start, block.combinator.name || ' ');
}
}
c = block.end; return blocks;
}); }
}
transform(code: MagicString, attr: string) { const operators = {
function encapsulate_block(block: Block) { '=' : (value: string, flags: string) => new RegExp(`^${value}$`, flags),
let i = block.selectors.length; '~=': (value: string, flags: string) => new RegExp(`\\b${value}\\b`, flags),
while (i--) { '|=': (value: string, flags: string) => new RegExp(`^${value}(-.+)?$`, flags),
const selector = block.selectors[i]; '^=': (value: string, flags: string) => new RegExp(`^${value}`, flags),
if (selector.type === 'PseudoElementSelector' || selector.type === 'PseudoClassSelector') continue; '$=': (value: string, flags: string) => new RegExp(`${value}$`, flags),
'*=': (value: string, flags: string) => new RegExp(value, flags)
};
if (selector.type === 'TypeSelector' && selector.name === '*') { function attribute_matches(node: Node, name: string, expected_value: string, operator: string, case_insensitive: boolean) {
code.overwrite(selector.start, selector.end, attr); const spread = node.attributes.find(attr => attr.type === 'Spread');
} else { if (spread) return true;
code.appendLeft(selector.end, attr);
}
break; const attr = node.attributes.find((attr: Node) => attr.name === name);
} if (!attr) return false;
} if (attr.is_true) return operator === null;
if (attr.chunks.length > 1) return true;
if (!expected_value) return true;
this.blocks.forEach((block, i) => { const pattern = operators[operator](expected_value, case_insensitive ? 'i' : '');
if (block.global) { const value = attr.chunks[0];
const selector = block.selectors[0];
const first = selector.children[0];
const last = selector.children[selector.children.length - 1];
code.remove(selector.start, first.start).remove(last.end, selector.end);
}
if (block.should_encapsulate) encapsulate_block(block); if (!value) return false;
}); if (value.type === 'Text') return pattern.test(value.data);
}
validate(component: Component) { const possible_values = new Set();
this.blocks.forEach((block) => { gather_possible_values(value.node, possible_values);
let i = block.selectors.length; if (possible_values.has(UNKNOWN)) return true;
while (i-- > 1) {
const selector = block.selectors[i];
if (selector.type === 'PseudoClassSelector' && selector.name === 'global') {
component.error(selector, {
code: `css-invalid-global`,
message: `:global(...) must be the first element in a compound selector`
});
}
}
});
let start = 0; for (const x of Array.from(possible_values)) { // TypeScript for-of is slightly unlike JS
let end = this.blocks.length; if (pattern.test(x)) return true;
}
for (; start < end; start += 1) { return false;
if (!this.blocks[start].global) break; }
}
for (; end > start; end -= 1) { function class_matches(node, name: string) {
if (!this.blocks[end - 1].global) break; return node.classes.some((class_directive) => {
} return class_directive.name === name;
});
}
for (let i = start; i < end; i += 1) { function unquote(value: Node) {
if (this.blocks[i].global) { if (value.type === 'Identifier') return value.name;
component.error(this.blocks[i].selectors[0], { const str = value.value;
code: `css-invalid-global`, if (str[0] === str[str.length - 1] && str[0] === "'" || str[0] === '"') {
message: `:global(...) can be at the start or end of a selector sequence, but not in the middle` return str.slice(1, str.length - 1);
});
}
}
} }
return str;
} }
function apply_selector(stylesheet: Stylesheet, blocks: Block[], node: Node, stack: Node[], to_encapsulate: any[]): boolean { function apply_selector(stylesheet: Stylesheet, blocks: Block[], node: Node, stack: Node[], to_encapsulate: any[]): boolean {
@ -201,100 +182,119 @@ function apply_selector(stylesheet: Stylesheet, blocks: Block[], node: Node, sta
return true; return true;
} }
const operators = { export default class Selector {
'=' : (value: string, flags: string) => new RegExp(`^${value}$`, flags), node: Node;
'~=': (value: string, flags: string) => new RegExp(`\\b${value}\\b`, flags), stylesheet: Stylesheet;
'|=': (value: string, flags: string) => new RegExp(`^${value}(-.+)?$`, flags), blocks: Block[];
'^=': (value: string, flags: string) => new RegExp(`^${value}`, flags), local_blocks: Block[];
'$=': (value: string, flags: string) => new RegExp(`${value}$`, flags), used: boolean;
'*=': (value: string, flags: string) => new RegExp(value, flags)
};
function attribute_matches(node: Node, name: string, expected_value: string, operator: string, case_insensitive: boolean) {
const spread = node.attributes.find(attr => attr.type === 'Spread');
if (spread) return true;
const attr = node.attributes.find((attr: Node) => attr.name === name);
if (!attr) return false;
if (attr.is_true) return operator === null;
if (attr.chunks.length > 1) return true;
if (!expected_value) return true;
const pattern = operators[operator](expected_value, case_insensitive ? 'i' : ''); constructor(node: Node, stylesheet: Stylesheet) {
const value = attr.chunks[0]; this.node = node;
this.stylesheet = stylesheet;
if (!value) return false; this.blocks = group_selectors(node);
if (value.type === 'Text') return pattern.test(value.data);
const possible_values = new Set(); // take trailing :global(...) selectors out of consideration
gather_possible_values(value.node, possible_values); let i = this.blocks.length;
if (possible_values.has(UNKNOWN)) return true; while (i > 0) {
if (!this.blocks[i - 1].global) break;
i -= 1;
}
for (const x of Array.from(possible_values)) { // TypeScript for-of is slightly unlike JS this.local_blocks = this.blocks.slice(0, i);
if (pattern.test(x)) return true; this.used = this.blocks[0].global;
} }
return false; apply(node: Node, stack: Node[]) {
} const to_encapsulate: Node[] = [];
function class_matches(node, name: string) { apply_selector(this.stylesheet, this.local_blocks.slice(), node, stack.slice(), to_encapsulate);
return node.classes.some(function(class_directive) {
return class_directive.name === name;
});
}
function unquote(value: Node) { if (to_encapsulate.length > 0) {
if (value.type === 'Identifier') return value.name; to_encapsulate.filter((_, i) => i === 0 || i === to_encapsulate.length - 1).forEach(({ node, block }) => {
const str = value.value; this.stylesheet.nodes_with_css_class.add(node);
if (str[0] === str[str.length - 1] && str[0] === "'" || str[0] === '"') { block.should_encapsulate = true;
return str.slice(1, str.length - 1); });
this.used = true;
}
} }
return str;
}
class Block { minify(code: MagicString) {
global: boolean; let c: number = null;
combinator: Node; this.blocks.forEach((block, i) => {
selectors: Node[] if (i > 0) {
start: number; if (block.start - c > 1) {
end: number; code.overwrite(c, block.start, block.combinator.name || ' ');
should_encapsulate: boolean; }
}
constructor(combinator: Node) { c = block.end;
this.combinator = combinator; });
this.global = false; }
this.selectors = [];
this.start = null; transform(code: MagicString, attr: string) {
this.end = null; function encapsulate_block(block: Block) {
let i = block.selectors.length;
while (i--) {
const selector = block.selectors[i];
if (selector.type === 'PseudoElementSelector' || selector.type === 'PseudoClassSelector') continue;
this.should_encapsulate = false; if (selector.type === 'TypeSelector' && selector.name === '*') {
} code.overwrite(selector.start, selector.end, attr);
} else {
code.appendLeft(selector.end, attr);
}
add(selector: Node) { break;
if (this.selectors.length === 0) { }
this.start = selector.start;
this.global = selector.type === 'PseudoClassSelector' && selector.name === 'global';
} }
this.selectors.push(selector); this.blocks.forEach((block) => {
this.end = selector.end; if (block.global) {
const selector = block.selectors[0];
const first = selector.children[0];
const last = selector.children[selector.children.length - 1];
code.remove(selector.start, first.start).remove(last.end, selector.end);
}
if (block.should_encapsulate) encapsulate_block(block);
});
} }
}
function group_selectors(selector: Node) { validate(component: Component) {
let block: Block = new Block(null); this.blocks.forEach((block) => {
let i = block.selectors.length;
while (i-- > 1) {
const selector = block.selectors[i];
if (selector.type === 'PseudoClassSelector' && selector.name === 'global') {
component.error(selector, {
code: `css-invalid-global`,
message: `:global(...) must be the first element in a compound selector`
});
}
}
});
const blocks = [block]; let start = 0;
let end = this.blocks.length;
selector.children.forEach((child: Node, i: number) => { for (; start < end; start += 1) {
if (child.type === 'WhiteSpace' || child.type === 'Combinator') { if (!this.blocks[start].global) break;
block = new Block(child);
blocks.push(block);
} else {
block.add(child);
} }
});
return blocks; for (; end > start; end -= 1) {
if (!this.blocks[end - 1].global) break;
}
for (let i = start; i < end; i += 1) {
if (this.blocks[i].global) {
component.error(this.blocks[i].selectors[0], {
code: `css-invalid-global`,
message: `:global(...) can be at the start or end of a selector sequence, but not in the middle`
});
}
}
}
} }

@ -20,6 +20,44 @@ function hash(str: string): string {
return (hash >>> 0).toString(36); return (hash >>> 0).toString(36);
} }
class Declaration {
node: Node;
constructor(node: Node) {
this.node = node;
}
transform(code: MagicString, keyframes: Map<string, string>) {
const property = this.node.property && remove_css_prefix(this.node.property.toLowerCase());
if (property === 'animation' || property === 'animation-name') {
this.node.value.children.forEach((block: Node) => {
if (block.type === 'Identifier') {
const name = block.name;
if (keyframes.has(name)) {
code.overwrite(block.start, block.end, keyframes.get(name));
}
}
});
}
}
minify(code: MagicString) {
if (!this.node.property) return; // @apply, and possibly other weird cases?
const c = this.node.start + this.node.property.length;
const first = this.node.value.children
? this.node.value.children[0]
: this.node.value;
let start = first.start;
while (/\s/.test(code.original[start])) start += 1;
if (start - c > 1) {
code.overwrite(c, start, ':');
}
}
}
class Rule { class Rule {
selectors: Selector[]; selectors: Selector[];
declarations: Declaration[]; declarations: Declaration[];
@ -43,11 +81,11 @@ class Rule {
return this.selectors.some(s => s.used); return this.selectors.some(s => s.used);
} }
minify(code: MagicString, dev: boolean) { minify(code: MagicString, _dev: boolean) {
let c = this.node.start; let c = this.node.start;
let started = false; let started = false;
this.selectors.forEach((selector, i) => { this.selectors.forEach((selector) => {
if (selector.used) { if (selector.used) {
const separator = started ? ',' : ''; const separator = started ? ',' : '';
if ((selector.node.start - c) > separator.length) { if ((selector.node.start - c) > separator.length) {
@ -100,47 +138,9 @@ class Rule {
} }
} }
class Declaration {
node: Node;
constructor(node: Node) {
this.node = node;
}
transform(code: MagicString, keyframes: Map<string, string>) {
const property = this.node.property && remove_css_prefix(this.node.property.toLowerCase());
if (property === 'animation' || property === 'animation-name') {
this.node.value.children.forEach((block: Node) => {
if (block.type === 'Identifier') {
const name = block.name;
if (keyframes.has(name)) {
code.overwrite(block.start, block.end, keyframes.get(name));
}
}
});
}
}
minify(code: MagicString) {
if (!this.node.property) return; // @apply, and possibly other weird cases?
const c = this.node.start + this.node.property.length;
const first = this.node.value.children
? this.node.value.children[0]
: this.node.value;
let start = first.start;
while (/\s/.test(code.original[start])) start += 1;
if (start - c > 1) {
code.overwrite(c, start, ':');
}
}
}
class Atrule { class Atrule {
node: Node; node: Node;
children: (Atrule|Rule)[]; children: Array<Atrule|Rule>;
constructor(node: Node) { constructor(node: Node) {
this.node = node; this.node = node;
@ -163,7 +163,7 @@ class Atrule {
} }
} }
is_used(dev: boolean) { is_used(_dev: boolean) {
return true; // TODO return true; // TODO
} }
@ -253,7 +253,7 @@ export default class Stylesheet {
has_styles: boolean; has_styles: boolean;
id: string; id: string;
children: (Rule|Atrule)[] = []; children: Array<Rule|Atrule> = [];
keyframes: Map<string, string> = new Map(); keyframes: Map<string, string> = new Map();
nodes_with_css_class: Set<Node> = new Set(); nodes_with_css_class: Set<Node> = new Set();
@ -269,7 +269,7 @@ export default class Stylesheet {
this.has_styles = true; this.has_styles = true;
const stack: (Rule | Atrule)[] = []; const stack: Array<Rule | Atrule> = [];
let current_atrule: Atrule = null; let current_atrule: Atrule = null;
walk(ast.css, { walk(ast.css, {
@ -280,7 +280,7 @@ export default class Stylesheet {
const atrule = new Atrule(node); const atrule = new Atrule(node);
stack.push(atrule); stack.push(atrule);
// this is an awkward special case @apply (and // this is an awkward special case — @apply (and
// possibly other future constructs) // possibly other future constructs)
if (last && !(last instanceof Atrule)) return; if (last && !(last instanceof Atrule)) return;

@ -3,7 +3,7 @@ import Stats from '../Stats';
import parse from '../parse/index'; import parse from '../parse/index';
import render_dom from './render-dom/index'; import render_dom from './render-dom/index';
import render_ssr from './render-ssr/index'; import render_ssr from './render-ssr/index';
import { CompileOptions, Ast, Warning } from '../interfaces'; import { CompileOptions, Warning } from '../interfaces';
import Component from './Component'; import Component from './Component';
import fuzzymatch from '../utils/fuzzymatch'; import fuzzymatch from '../utils/fuzzymatch';
@ -57,6 +57,7 @@ function validate_options(options: CompileOptions, warnings: Warning[]) {
function get_name(filename: string) { function get_name(filename: string) {
if (!filename) return null; if (!filename) return null;
// eslint-disable-next-line no-useless-escape
const parts = filename.split(/[\/\\]/); const parts = filename.split(/[\/\\]/);
if (parts.length > 1 && /^index\.\w+/.test(parts[parts.length - 1])) { if (parts.length > 1 && /^index\.\w+/.test(parts[parts.length - 1])) {
@ -79,12 +80,10 @@ export default function compile(source: string, options: CompileOptions = {}) {
const stats = new Stats(); const stats = new Stats();
const warnings = []; const warnings = [];
let ast: Ast;
validate_options(options, warnings); validate_options(options, warnings);
stats.start('parse'); stats.start('parse');
ast = parse(source, options); const ast = parse(source, options);
stats.stop('parse'); stats.stop('parse');
stats.start('create component'); stats.start('create component');

@ -21,7 +21,7 @@ export default class Attribute extends Node {
is_synthetic: boolean; is_synthetic: boolean;
should_cache: boolean; should_cache: boolean;
expression?: Expression; expression?: Expression;
chunks: (Text | Expression)[]; chunks: Array<Text | Expression>;
dependencies: Set<string>; dependencies: Set<string>;
constructor(component, parent, scope, info) { constructor(component, parent, scope, info) {

@ -8,13 +8,13 @@ import { Node as INode } from '../../interfaces';
import { new_tail } from '../utils/tail'; import { new_tail } from '../utils/tail';
import Element from './Element'; import Element from './Element';
type Context = { interface Context {
key: INode, key: INode;
name?: string, name?: string;
tail: string tail: string;
}; }
function unpack_destructuring(contexts: Array<Context>, node: INode, tail: string) { function unpack_destructuring(contexts: Context[], node: INode, tail: string) {
if (!node) return; if (!node) return;
if (node.type === 'Identifier' || node.type === 'RestIdentifier') { if (node.type === 'Identifier' || node.type === 'RestIdentifier') {
@ -25,7 +25,7 @@ function unpack_destructuring(contexts: Array<Context>, node: INode, tail: strin
} else if (node.type === 'ArrayPattern') { } else if (node.type === 'ArrayPattern') {
node.elements.forEach((element, i) => { node.elements.forEach((element, i) => {
if (element && element.type === 'RestIdentifier') { if (element && element.type === 'RestIdentifier') {
unpack_destructuring(contexts, element, `${tail}.slice(${i})`) unpack_destructuring(contexts, element, `${tail}.slice(${i})`);
} else { } else {
unpack_destructuring(contexts, element, `${tail}[${i}]`); unpack_destructuring(contexts, element, `${tail}[${i}]`);
} }
@ -60,7 +60,7 @@ export default class EachBlock extends AbstractBlock {
context: string; context: string;
key: Expression; key: Expression;
scope: TemplateScope; scope: TemplateScope;
contexts: Array<Context>; contexts: Context[];
has_animation: boolean; has_animation: boolean;
has_binding = false; has_binding = false;

@ -54,7 +54,7 @@ const a11y_required_content = new Set([
'h4', 'h4',
'h5', 'h5',
'h6' 'h6'
]) ]);
const invisible_elements = new Set(['meta', 'html', 'script', 'style']); const invisible_elements = new Set(['meta', 'html', 'script', 'style']);
@ -89,6 +89,22 @@ function get_namespace(parent: Element, element: Element, explicit_namespace: st
return parent_element.namespace; return parent_element.namespace;
} }
function should_have_attribute(
node,
attributes: string[],
name = node.name
) {
const article = /^[aeiou]/.test(attributes[0]) ? 'an' : 'a';
const sequence = attributes.length > 1 ?
attributes.slice(0, -1).join(', ') + ` or ${attributes[attributes.length - 1]}` :
attributes[0];
node.component.warn(node, {
code: `a11y-missing-attribute`,
message: `A11y: <${name}> element should have ${article} ${sequence} attribute`
});
}
export default class Element extends Node { export default class Element extends Node {
type: 'Element'; type: 'Element';
name: string; name: string;
@ -134,7 +150,7 @@ export default class Element extends Node {
} }
if (this.name === 'option') { if (this.name === 'option') {
// Special case treat these the same way: // Special case — treat these the same way:
// <option>{foo}</option> // <option>{foo}</option>
// <option value={foo}>{foo}</option> // <option value={foo}>{foo}</option>
const value_attribute = info.attributes.find(attribute => attribute.name === 'value'); const value_attribute = info.attributes.find(attribute => attribute.name === 'value');
@ -180,10 +196,12 @@ export default class Element extends Node {
break; break;
case 'Transition': case 'Transition':
{
const transition = new Transition(component, this, scope, node); const transition = new Transition(component, this, scope, node);
if (node.intro) this.intro = transition; if (node.intro) this.intro = transition;
if (node.outro) this.outro = transition; if (node.outro) this.outro = transition;
break; break;
}
case 'Animation': case 'Animation':
this.animation = new Animation(component, this, scope, node); this.animation = new Animation(component, this, scope, node);
@ -529,7 +547,7 @@ export default class Element extends Node {
if (type !== 'checkbox') { if (type !== 'checkbox') {
let message = `'${name}' binding can only be used with <input type="checkbox">`; let message = `'${name}' binding can only be used with <input type="checkbox">`;
if (type === 'radio') message += ` for <input type="radio">, use 'group' binding`; if (type === 'radio') message += ` — for <input type="radio">, use 'group' binding`;
component.error(binding, { code: `invalid-binding`, message }); component.error(binding, { code: `invalid-binding`, message });
} }
} else if (name === 'group') { } else if (name === 'group') {
@ -687,7 +705,7 @@ export default class Element extends Node {
if (class_attribute.chunks.length === 1 && class_attribute.chunks[0].type === 'Text') { if (class_attribute.chunks.length === 1 && class_attribute.chunks[0].type === 'Text') {
(class_attribute.chunks[0] as Text).data += ` ${class_name}`; (class_attribute.chunks[0] as Text).data += ` ${class_name}`;
} else { } else {
(<Node[]>class_attribute.chunks).push( (class_attribute.chunks as Node[]).push(
new Text(this.component, this, this.scope, { new Text(this.component, this, this.scope, {
type: 'Text', type: 'Text',
data: ` ${class_name}` data: ` ${class_name}`
@ -705,19 +723,3 @@ export default class Element extends Node {
} }
} }
} }
function should_have_attribute(
node,
attributes: string[],
name = node.name
) {
const article = /^[aeiou]/.test(attributes[0]) ? 'an' : 'a';
const sequence = attributes.length > 1 ?
attributes.slice(0, -1).join(', ') + ` or ${attributes[attributes.length - 1]}` :
attributes[0];
node.component.warn(node, {
code: `a11y-missing-attribute`,
message: `A11y: <${name}> element should have ${article} ${sequence} attribute`
});
}

@ -36,6 +36,7 @@ export default class InlineComponent extends Node {
: null; : null;
info.attributes.forEach(node => { info.attributes.forEach(node => {
/* eslint-disable no-fallthrough */
switch (node.type) { switch (node.type) {
case 'Action': case 'Action':
component.error(node, { component.error(node, {
@ -82,6 +83,7 @@ export default class InlineComponent extends Node {
default: default:
throw new Error(`Not implemented: ${node.type}`); throw new Error(`Not implemented: ${node.type}`);
} }
/* eslint-enable no-fallthrough */
}); });
if (this.lets.length > 0) { if (this.lets.length > 0) {

@ -2,7 +2,7 @@ import map_children from './shared/map_children';
import AbstractBlock from './shared/AbstractBlock'; import AbstractBlock from './shared/AbstractBlock';
export default class PendingBlock extends AbstractBlock { export default class PendingBlock extends AbstractBlock {
type: 'PendingBlock'; type: 'PendingBlock';
constructor(component, parent, scope, info) { constructor(component, parent, scope, info) {
super(component, parent, scope, info); super(component, parent, scope, info);
this.children = map_children(component, parent, scope, info.children); this.children = map_children(component, parent, scope, info.children);

@ -2,6 +2,12 @@ import Node from './shared/Node';
import Expression from './shared/Expression'; import Expression from './shared/Expression';
import Component from '../Component'; import Component from '../Component';
function describe(transition: Transition) {
return transition.directive === 'transition'
? `a 'transition'`
: `an '${transition.directive}'`;
}
export default class Transition extends Node { export default class Transition extends Node {
type: 'Transition'; type: 'Transition';
name: string; name: string;
@ -38,9 +44,3 @@ export default class Transition extends Node {
: null; : null;
} }
} }
function describe(transition: Transition) {
return transition.directive === 'transition'
? `a 'transition'`
: `an '${transition.directive}'`;
}

@ -44,8 +44,8 @@ export default class Window extends Node {
if (!~valid_bindings.indexOf(node.name)) { if (!~valid_bindings.indexOf(node.name)) {
const match = ( const match = (
node.name === 'width' ? 'innerWidth' : node.name === 'width' ? 'innerWidth' :
node.name === 'height' ? 'innerHeight' : node.name === 'height' ? 'innerHeight' :
fuzzymatch(node.name, valid_bindings) fuzzymatch(node.name, valid_bindings)
); );
const message = `'${node.name}' is not a valid binding on <svelte:window>`; const message = `'${node.name}' is not a valid binding on <svelte:window>`;

@ -33,32 +33,32 @@ import Window from './Window';
// note: to write less types each of types in union below should have type defined as literal // note: to write less types each of types in union below should have type defined as literal
// https://www.typescriptlang.org/docs/handbook/advanced-types.html#discriminated-unions // https://www.typescriptlang.org/docs/handbook/advanced-types.html#discriminated-unions
export type INode = Action export type INode = Action
| Animation | Animation
| Attribute | Attribute
| AwaitBlock | AwaitBlock
| Binding | Binding
| Body | Body
| CatchBlock | CatchBlock
| Class | Class
| Comment | Comment
| DebugTag | DebugTag
| EachBlock | EachBlock
| Element | Element
| ElseBlock | ElseBlock
| EventHandler | EventHandler
| Fragment | Fragment
| Head | Head
| IfBlock | IfBlock
| InlineComponent | InlineComponent
| Let | Let
| MustacheTag | MustacheTag
| Options | Options
| PendingBlock | PendingBlock
| RawMustacheTag | RawMustacheTag
| Slot | Slot
| Tag | Tag
| Text | Text
| ThenBlock | ThenBlock
| Title | Title
| Transition | Transition
| Window; | Window;

@ -4,10 +4,10 @@ import is_reference from 'is-reference';
import flatten_reference from '../../utils/flatten_reference'; import flatten_reference from '../../utils/flatten_reference';
import { create_scopes, Scope, extract_names } from '../../utils/scope'; import { create_scopes, Scope, extract_names } from '../../utils/scope';
import { Node } from '../../../interfaces'; import { Node } from '../../../interfaces';
import { globals } from '../../../utils/names'; import { globals , sanitize } from '../../../utils/names';
import deindent from '../../utils/deindent'; import deindent from '../../utils/deindent';
import Wrapper from '../../render-dom/wrappers/shared/Wrapper'; import Wrapper from '../../render-dom/wrappers/shared/Wrapper';
import { sanitize } from '../../../utils/names';
import TemplateScope from './TemplateScope'; import TemplateScope from './TemplateScope';
import get_object from '../../utils/get_object'; import get_object from '../../utils/get_object';
import { nodes_match } from '../../../utils/nodes_match'; import { nodes_match } from '../../../utils/nodes_match';
@ -28,8 +28,8 @@ const binary_operators: Record<string, number> = {
'<=': 11, '<=': 11,
'>': 11, '>': 11,
'>=': 11, '>=': 11,
'in': 11, in: 11,
'instanceof': 11, instanceof: 11,
'==': 10, '==': 10,
'!=': 10, '!=': 10,
'===': 10, '===': 10,
@ -64,6 +64,33 @@ const precedence: Record<string, (node?: Node) => number> = {
type Owner = Wrapper | INode; type Owner = Wrapper | INode;
function get_function_name(node, parent) {
if (parent.type === 'EventHandler') {
return `${parent.name}_handler`;
}
if (parent.type === 'Action') {
return `${parent.name}_function`;
}
return 'func';
}
function is_contextual(component: Component, scope: TemplateScope, name: string) {
if (name === '$$props') return true;
// if it's a name below root scope, it's contextual
if (!scope.is_top_level(name)) return true;
const variable = component.var_lookup.get(name);
// hoistables, module declarations, and imports are non-contextual
if (!variable || variable.hoistable) return false;
// assume contextual
return true;
}
export default class Expression { export default class Expression {
type: 'Expression' = 'Expression'; type: 'Expression' = 'Expression';
component: Component; component: Component;
@ -347,7 +374,7 @@ export default class Expression {
throw new Error(`Well that's odd`); throw new Error(`Well that's odd`);
} }
// TOOD optimisation if this is an event handler, // TOOD optimisation — if this is an event handler,
// the return value doesn't matter // the return value doesn't matter
} }
@ -486,33 +513,6 @@ export default class Expression {
}); });
} }
return this.rendered = `[${this.node.start}-${this.node.end}]`; return this.rendered = `[✂${this.node.start}-${this.node.end}✂]`;
} }
} }
function get_function_name(node, parent) {
if (parent.type === 'EventHandler') {
return `${parent.name}_handler`;
}
if (parent.type === 'Action') {
return `${parent.name}_function`;
}
return 'func';
}
function is_contextual(component: Component, scope: TemplateScope, name: string) {
if (name === '$$props') return true;
// if it's a name below root scope, it's contextual
if (!scope.is_top_level(name)) return true;
const variable = component.var_lookup.get(name);
// hoistables, module declarations, and imports are non-contextual
if (!variable || variable.hoistable) return false;
// assume contextual
return true;
}

@ -16,8 +16,6 @@ import Title from '../Title';
import Window from '../Window'; import Window from '../Window';
import { Node } from '../../../interfaces'; import { Node } from '../../../interfaces';
export type Children = ReturnType<typeof map_children>;
function get_constructor(type) { function get_constructor(type) {
switch (type) { switch (type) {
case 'AwaitBlock': return AwaitBlock; case 'AwaitBlock': return AwaitBlock;
@ -53,3 +51,5 @@ export default function map_children(component, parent, scope, children: Node[])
return node; return node;
}); });
} }
export type Children = ReturnType<typeof map_children>;

@ -9,7 +9,7 @@ export interface BlockOptions {
renderer?: Renderer; renderer?: Renderer;
comment?: string; comment?: string;
key?: string; key?: string;
bindings?: Map<string, { object: string, property: string, snippet: string }>; bindings?: Map<string, { object: string; property: string; snippet: string }>;
dependencies?: Set<string>; dependencies?: Set<string>;
} }
@ -26,7 +26,7 @@ export default class Block {
dependencies: Set<string>; dependencies: Set<string>;
bindings: Map<string, { object: string, property: string, snippet: string }>; bindings: Map<string, { object: string; property: string; snippet: string }>;
builders: { builders: {
init: CodeBuilder; init: CodeBuilder;
@ -372,8 +372,8 @@ export default class Block {
${properties} ${properties}
}; };
`.replace(/(#+)(\w*)/g, (match: string, sigil: string, name: string) => { `.replace(/(#+)(\w*)/g, (match: string, sigil: string, name: string) => {
return sigil === '#' ? this.alias(name) : sigil.slice(1) + name; return sigil === '#' ? this.alias(name) : sigil.slice(1) + name;
}); });
} }
render_listeners(chunk: string = '') { render_listeners(chunk: string = '') {
@ -387,7 +387,7 @@ export default class Block {
this.builders.destroy.add_line( this.builders.destroy.add_line(
`#dispose${chunk}();` `#dispose${chunk}();`
) );
} else { } else {
this.builders.hydrate.add_block(deindent` this.builders.hydrate.add_block(deindent`
#dispose${chunk} = [ #dispose${chunk} = [

@ -8,7 +8,7 @@ export default class Renderer {
component: Component; // TODO Maybe Renderer shouldn't know about Component? component: Component; // TODO Maybe Renderer shouldn't know about Component?
options: CompileOptions; options: CompileOptions;
blocks: (Block | string)[] = []; blocks: Array<Block | string> = [];
readonly: Set<string> = new Set(); readonly: Set<string> = new Set();
meta_bindings: CodeBuilder = new CodeBuilder(); // initial values for e.g. window.innerWidth, if there's a <svelte:window> meta tag meta_bindings: CodeBuilder = new CodeBuilder(); // initial values for e.g. window.innerWidth, if there's a <svelte:window> meta tag
binding_groups: string[] = []; binding_groups: string[] = [];
@ -17,7 +17,7 @@ export default class Renderer {
fragment: FragmentWrapper; fragment: FragmentWrapper;
file_var: string; file_var: string;
locate: (c: number) => { line: number; column: number; }; locate: (c: number) => { line: number; column: number };
constructor(component: Component, options: CompileOptions) { constructor(component: Component, options: CompileOptions) {
this.component = component; this.component = component;

@ -79,8 +79,8 @@ export default function dom(
${$$props} => { ${$$props} => {
${uses_props && component.invalidate('$$props', `$$props = @assign(@assign({}, $$props), $$new_props)`)} ${uses_props && component.invalidate('$$props', `$$props = @assign(@assign({}, $$props), $$new_props)`)}
${writable_props.map(prop => ${writable_props.map(prop =>
`if ('${prop.export_name}' in $$props) ${component.invalidate(prop.name, `${prop.name} = $$props.${prop.export_name}`)};` `if ('${prop.export_name}' in $$props) ${component.invalidate(prop.name, `${prop.name} = $$props.${prop.export_name}`)};`
)} )}
${component.slots.size > 0 && ${component.slots.size > 0 &&
`if ('$$scope' in ${$$props}) ${component.invalidate('$$scope', `$$scope = ${$$props}.$$scope`)};`} `if ('$$scope' in ${$$props}) ${component.invalidate('$$scope', `$$scope = ${$$props}.$$scope`)};`}
} }
@ -152,12 +152,12 @@ export default function dom(
// instrument assignments // instrument assignments
if (component.ast.instance) { if (component.ast.instance) {
let scope = component.instance_scope; let scope = component.instance_scope;
let map = component.instance_scope_map; const map = component.instance_scope_map;
let pending_assignments = new Set(); let pending_assignments = new Set();
walk(component.ast.instance.content, { walk(component.ast.instance.content, {
enter: (node, parent) => { enter: (node) => {
if (map.has(node)) { if (map.has(node)) {
scope = map.get(node); scope = map.get(node);
} }
@ -369,7 +369,7 @@ export default function dom(
}) })
.map(n => `$$dirty.${n}`).join(' || '); .map(n => `$$dirty.${n}`).join(' || ');
let snippet = `[${d.node.body.start}-${d.node.end}]`; let snippet = `[✂${d.node.body.start}-${d.node.end}✂]`;
if (condition) snippet = `if (${condition}) { ${snippet} }`; if (condition) snippet = `if (${condition}) { ${snippet} }`;
if (condition || uses_props) { if (condition || uses_props) {
@ -390,7 +390,7 @@ export default function dom(
const store = component.var_lookup.get(name); const store = component.var_lookup.get(name);
if (store && store.reassigned) { if (store && store.reassigned) {
return `${$name}, $$unsubscribe_${name} = @noop, $$subscribe_${name} = () => { $$unsubscribe_${name}(); $$unsubscribe_${name} = ${name}.subscribe($$value => { ${$name} = $$value; $$invalidate('${$name}', ${$name}); }) }` return `${$name}, $$unsubscribe_${name} = @noop, $$subscribe_${name} = () => { $$unsubscribe_${name}(); $$unsubscribe_${name} = ${name}.subscribe($$value => { ${$name} = $$value; $$invalidate('${$name}', ${$name}); }) }`;
} }
return $name; return $name;

@ -6,7 +6,7 @@ import Body from '../../nodes/Body';
export default class BodyWrapper extends Wrapper { export default class BodyWrapper extends Wrapper {
node: Body; node: Body;
render(block: Block, parent_node: string, parent_nodes: string) { render(block: Block, _parent_node: string, _parent_nodes: string) {
this.node.handlers.forEach(handler => { this.node.handlers.forEach(handler => {
const snippet = handler.render(block); const snippet = handler.render(block);

@ -13,13 +13,13 @@ export default class DebugTagWrapper extends Wrapper {
block: Block, block: Block,
parent: Wrapper, parent: Wrapper,
node: DebugTag, node: DebugTag,
strip_whitespace: boolean, _strip_whitespace: boolean,
next_sibling: Wrapper _next_sibling: Wrapper
) { ) {
super(renderer, block, parent, node); super(renderer, block, parent, node);
} }
render(block: Block, parent_node: string, parent_nodes: string) { render(block: Block, _parent_node: string, _parent_nodes: string) {
const { renderer } = this; const { renderer } = this;
const { component } = renderer; const { component } = renderer;
@ -32,7 +32,7 @@ export default class DebugTagWrapper extends Wrapper {
code.overwrite(this.node.start + 1, this.node.start + 7, 'debugger', { code.overwrite(this.node.start + 1, this.node.start + 7, 'debugger', {
storeName: true storeName: true
}); });
const statement = `[${this.node.start + 1}-${this.node.start + 7}];`; const statement = `[✂${this.node.start + 1}-${this.node.start + 7}✂];`;
block.builders.create.add_line(statement); block.builders.create.add_line(statement);
block.builders.update.add_line(statement); block.builders.update.add_line(statement);
@ -41,7 +41,7 @@ export default class DebugTagWrapper extends Wrapper {
code.overwrite(this.node.start + 1, this.node.start + 7, 'log', { code.overwrite(this.node.start + 1, this.node.start + 7, 'log', {
storeName: true storeName: true
}); });
const log = `[${this.node.start + 1}-${this.node.start + 7}]`; const log = `[✂${this.node.start + 1}-${this.node.start + 7}✂]`;
const dependencies = new Set(); const dependencies = new Set();
this.node.expressions.forEach(expression => { this.node.expressions.forEach(expression => {

@ -312,8 +312,8 @@ export default class EachBlockWrapper extends Wrapper {
block.builders.init.add_block(deindent` block.builders.init.add_block(deindent`
const ${get_key} = ctx => ${ const ${get_key} = ctx => ${
// @ts-ignore todo: probably error // @ts-ignore todo: probably error
this.node.key.render()}; this.node.key.render()};
for (var #i = 0; #i < ${this.vars.each_block_value}.${length}; #i += 1) { 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 child_ctx = ${this.vars.get_each_context}(ctx, ${this.vars.each_block_value}, #i);
@ -425,7 +425,7 @@ export default class EachBlockWrapper extends Wrapper {
all_dependencies.add(dependency); all_dependencies.add(dependency);
}); });
const outro_block = this.block.has_outros && block.get_unique_name('outro_block') const outro_block = this.block.has_outros && block.get_unique_name('outro_block');
if (outro_block) { if (outro_block) {
block.builders.init.add_block(deindent` block.builders.init.add_block(deindent`
function ${outro_block}(i, detaching, local) { function ${outro_block}(i, detaching, local) {

@ -6,224 +6,6 @@ import { stringify } from '../../../utils/stringify';
import deindent from '../../../utils/deindent'; import deindent from '../../../utils/deindent';
import Expression from '../../../nodes/shared/Expression'; import Expression from '../../../nodes/shared/Expression';
export default class AttributeWrapper {
node: Attribute;
parent: ElementWrapper;
constructor(parent: ElementWrapper, block: Block, node: Attribute) {
this.node = node;
this.parent = parent;
if (node.dependencies.size > 0) {
parent.cannot_use_innerhtml();
block.add_dependencies(node.dependencies);
// special case — <option value={foo}> — see below
if (this.parent.node.name === 'option' && node.name === 'value') {
let select: ElementWrapper = this.parent;
while (select && (select.node.type !== 'Element' || select.node.name !== 'select'))
// @ts-ignore todo: doublecheck this, but looks to be correct
select = select.parent;
if (select && select.select_binding_dependencies) {
select.select_binding_dependencies.forEach(prop => {
this.node.dependencies.forEach((dependency: string) => {
this.parent.renderer.component.indirect_dependencies.get(prop).add(dependency);
});
});
}
}
}
}
render(block: Block) {
const element = this.parent;
const name = fix_attribute_casing(this.node.name);
let metadata = element.node.namespace ? null : attribute_lookup[name];
if (metadata && metadata.applies_to && !~metadata.applies_to.indexOf(element.node.name))
metadata = null;
const is_indirectly_bound_value =
name === 'value' &&
(element.node.name === 'option' || // TODO check it's actually bound
(element.node.name === 'input' &&
element.node.bindings.find(
(binding) =>
/checked|group/.test(binding.name)
)));
const property_name = is_indirectly_bound_value
? '__value'
: metadata && metadata.property_name;
// xlink is a special case... we could maybe extend this to generic
// namespaced attributes but I'm not sure that's applicable in
// HTML5?
const method = /-/.test(element.node.name)
? '@set_custom_element_data'
: name.slice(0, 6) === 'xlink:'
? '@xlink_attr'
: '@attr';
const is_legacy_input_type = element.renderer.component.compile_options.legacy && name === 'type' && this.parent.node.name === 'input';
const is_dataset = /^data-/.test(name) && !element.renderer.component.compile_options.legacy && !element.node.namespace;
const camel_case_name = is_dataset ? name.replace('data-', '').replace(/(-\w)/g, function (m) {
return m[1].toUpperCase();
}) : name;
if (this.node.is_dynamic) {
let value;
// 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.chunks.length === 1) {
// single {tag} — may be a non-string
value = (this.node.chunks[0] as Expression).render(block);
} else {
// '{foo} {bar}' — treat as string concatenation
value =
(this.node.chunks[0].type === 'Text' ? '' : `"" + `) +
this.node.chunks
.map((chunk) => {
if (chunk.type === 'Text') {
return stringify(chunk.data);
} else {
return chunk.get_precedence() <= 13
? `(${chunk.render()})`
: chunk.render();
}
})
.join(' + ');
}
const is_select_value_attribute =
name === 'value' && element.node.name === 'select';
const should_cache = (this.node.should_cache || is_select_value_attribute);
const last = should_cache && block.get_unique_name(
`${element.var}_${name.replace(/[^a-zA-Z_$]/g, '_')}_value`
);
if (should_cache) block.add_variable(last);
let updater;
const init = should_cache ? `${last} = ${value}` : value;
if (is_legacy_input_type) {
block.builders.hydrate.add_line(
`@set_input_type(${element.var}, ${init});`
);
updater = `@set_input_type(${element.var}, ${should_cache ? last : value});`;
} else if (is_select_value_attribute) {
// annoying special case
const is_multiple_select = element.node.get_static_attribute_value('multiple');
const i = block.get_unique_name('i');
const option = block.get_unique_name('option');
const if_statement = is_multiple_select
? deindent`
${option}.selected = ~${last}.indexOf(${option}.__value);`
: deindent`
if (${option}.__value === ${last}) {
${option}.selected = true;
break;
}`;
updater = deindent`
for (var ${i} = 0; ${i} < ${element.var}.options.length; ${i} += 1) {
var ${option} = ${element.var}.options[${i}];
${if_statement}
}
`;
block.builders.mount.add_block(deindent`
${last} = ${value};
${updater}
`);
} else if (property_name) {
block.builders.hydrate.add_line(
`${element.var}.${property_name} = ${init};`
);
updater = `${element.var}.${property_name} = ${should_cache ? last : value};`;
} else if (is_dataset) {
block.builders.hydrate.add_line(
`${element.var}.dataset.${camel_case_name} = ${init};`
);
updater = `${element.var}.dataset.${camel_case_name} = ${should_cache ? last : value};`;
} else {
block.builders.hydrate.add_line(
`${method}(${element.var}, "${name}", ${init});`
);
updater = `${method}(${element.var}, "${name}", ${should_cache ? last : value});`;
}
// only add an update if mutations are involved (or it's a select?)
const dependencies = this.node.get_dependencies();
if (dependencies.length > 0 || is_select_value_attribute) {
const changed_check = (
(block.has_outros ? `!#current || ` : '') +
dependencies.map(dependency => `changed.${dependency}`).join(' || ')
);
const update_cached_value = `${last} !== (${last} = ${value})`;
const condition = should_cache
? (dependencies.length ? `(${changed_check}) && ${update_cached_value}` : update_cached_value)
: changed_check;
block.builders.update.add_conditional(
condition,
updater
);
}
} else {
const value = this.node.get_value(block);
const statement = (
is_legacy_input_type
? `@set_input_type(${element.var}, ${value});`
: property_name
? `${element.var}.${property_name} = ${value};`
: is_dataset
? `${element.var}.dataset.${camel_case_name} = ${value === true ? '""' : value};`
: `${method}(${element.var}, "${name}", ${value === true ? '""' : value});`
);
block.builders.hydrate.add_line(statement);
// special case autofocus. has to be handled in a bit of a weird way
if (this.node.is_true && name === 'autofocus') {
block.autofocus = element.var;
}
}
if (is_indirectly_bound_value) {
const update_value = `${element.var}.value = ${element.var}.__value;`;
block.builders.hydrate.add_line(update_value);
if (this.node.is_dynamic) block.builders.update.add_line(update_value);
}
}
stringify() {
if (this.node.is_true) return '';
const value = this.node.chunks;
if (value.length === 0) return `=""`;
return `="${value.map(chunk => {
return chunk.type === 'Text'
? chunk.data.replace(/"/g, '\\"')
: `\${${chunk.render()}}`
})}"`;
}
}
// source: https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes // source: https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes
const attribute_lookup = { const attribute_lookup = {
accept: { applies_to: ['form', 'input'] }, accept: { applies_to: ['form', 'input'] },
@ -444,3 +226,221 @@ Object.keys(attribute_lookup).forEach(name => {
const metadata = attribute_lookup[name]; const metadata = attribute_lookup[name];
if (!metadata.property_name) metadata.property_name = name; if (!metadata.property_name) metadata.property_name = name;
}); });
export default class AttributeWrapper {
node: Attribute;
parent: ElementWrapper;
constructor(parent: ElementWrapper, block: Block, node: Attribute) {
this.node = node;
this.parent = parent;
if (node.dependencies.size > 0) {
parent.cannot_use_innerhtml();
block.add_dependencies(node.dependencies);
// special case — <option value={foo}> — see below
if (this.parent.node.name === 'option' && node.name === 'value') {
let select: ElementWrapper = this.parent;
while (select && (select.node.type !== 'Element' || select.node.name !== 'select'))
// @ts-ignore todo: doublecheck this, but looks to be correct
select = select.parent;
if (select && select.select_binding_dependencies) {
select.select_binding_dependencies.forEach(prop => {
this.node.dependencies.forEach((dependency: string) => {
this.parent.renderer.component.indirect_dependencies.get(prop).add(dependency);
});
});
}
}
}
}
render(block: Block) {
const element = this.parent;
const name = fix_attribute_casing(this.node.name);
let metadata = element.node.namespace ? null : attribute_lookup[name];
if (metadata && metadata.applies_to && !~metadata.applies_to.indexOf(element.node.name))
metadata = null;
const is_indirectly_bound_value =
name === 'value' &&
(element.node.name === 'option' || // TODO check it's actually bound
(element.node.name === 'input' &&
element.node.bindings.find(
(binding) =>
/checked|group/.test(binding.name)
)));
const property_name = is_indirectly_bound_value
? '__value'
: metadata && metadata.property_name;
// xlink is a special case... we could maybe extend this to generic
// namespaced attributes but I'm not sure that's applicable in
// HTML5?
const method = /-/.test(element.node.name)
? '@set_custom_element_data'
: name.slice(0, 6) === 'xlink:'
? '@xlink_attr'
: '@attr';
const is_legacy_input_type = element.renderer.component.compile_options.legacy && name === 'type' && this.parent.node.name === 'input';
const is_dataset = /^data-/.test(name) && !element.renderer.component.compile_options.legacy && !element.node.namespace;
const camel_case_name = is_dataset ? name.replace('data-', '').replace(/(-\w)/g, (m) => {
return m[1].toUpperCase();
}) : name;
if (this.node.is_dynamic) {
let value;
// 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.chunks.length === 1) {
// single {tag} — may be a non-string
value = (this.node.chunks[0] as Expression).render(block);
} else {
// '{foo} {bar}' — treat as string concatenation
value =
(this.node.chunks[0].type === 'Text' ? '' : `"" + `) +
this.node.chunks
.map((chunk) => {
if (chunk.type === 'Text') {
return stringify(chunk.data);
} else {
return chunk.get_precedence() <= 13
? `(${chunk.render()})`
: chunk.render();
}
})
.join(' + ');
}
const is_select_value_attribute =
name === 'value' && element.node.name === 'select';
const should_cache = (this.node.should_cache || is_select_value_attribute);
const last = should_cache && block.get_unique_name(
`${element.var}_${name.replace(/[^a-zA-Z_$]/g, '_')}_value`
);
if (should_cache) block.add_variable(last);
let updater;
const init = should_cache ? `${last} = ${value}` : value;
if (is_legacy_input_type) {
block.builders.hydrate.add_line(
`@set_input_type(${element.var}, ${init});`
);
updater = `@set_input_type(${element.var}, ${should_cache ? last : value});`;
} else if (is_select_value_attribute) {
// annoying special case
const is_multiple_select = element.node.get_static_attribute_value('multiple');
const i = block.get_unique_name('i');
const option = block.get_unique_name('option');
const if_statement = is_multiple_select
? deindent`
${option}.selected = ~${last}.indexOf(${option}.__value);`
: deindent`
if (${option}.__value === ${last}) {
${option}.selected = true;
break;
}`;
updater = deindent`
for (var ${i} = 0; ${i} < ${element.var}.options.length; ${i} += 1) {
var ${option} = ${element.var}.options[${i}];
${if_statement}
}
`;
block.builders.mount.add_block(deindent`
${last} = ${value};
${updater}
`);
} else if (property_name) {
block.builders.hydrate.add_line(
`${element.var}.${property_name} = ${init};`
);
updater = `${element.var}.${property_name} = ${should_cache ? last : value};`;
} else if (is_dataset) {
block.builders.hydrate.add_line(
`${element.var}.dataset.${camel_case_name} = ${init};`
);
updater = `${element.var}.dataset.${camel_case_name} = ${should_cache ? last : value};`;
} else {
block.builders.hydrate.add_line(
`${method}(${element.var}, "${name}", ${init});`
);
updater = `${method}(${element.var}, "${name}", ${should_cache ? last : value});`;
}
// only add an update if mutations are involved (or it's a select?)
const dependencies = this.node.get_dependencies();
if (dependencies.length > 0 || is_select_value_attribute) {
const changed_check = (
(block.has_outros ? `!#current || ` : '') +
dependencies.map(dependency => `changed.${dependency}`).join(' || ')
);
const update_cached_value = `${last} !== (${last} = ${value})`;
const condition = should_cache
? (dependencies.length ? `(${changed_check}) && ${update_cached_value}` : update_cached_value)
: changed_check;
block.builders.update.add_conditional(
condition,
updater
);
}
} else {
const value = this.node.get_value(block);
const statement = (
is_legacy_input_type
? `@set_input_type(${element.var}, ${value});`
: property_name
? `${element.var}.${property_name} = ${value};`
: is_dataset
? `${element.var}.dataset.${camel_case_name} = ${value === true ? '""' : value};`
: `${method}(${element.var}, "${name}", ${value === true ? '""' : value});`
);
block.builders.hydrate.add_line(statement);
// special case – autofocus. has to be handled in a bit of a weird way
if (this.node.is_true && name === 'autofocus') {
block.autofocus = element.var;
}
}
if (is_indirectly_bound_value) {
const update_value = `${element.var}.value = ${element.var}.__value;`;
block.builders.hydrate.add_line(update_value);
if (this.node.is_dynamic) block.builders.update.add_line(update_value);
}
}
stringify() {
if (this.node.is_true) return '';
const value = this.node.chunks;
if (value.length === 0) return `=""`;
return `="${value.map(chunk => {
return chunk.type === 'Text'
? chunk.data.replace(/"/g, '\\"')
: `\${${chunk.render()}}`;
})}"`;
}
}

@ -1,6 +1,5 @@
import Binding from '../../../nodes/Binding'; import Binding from '../../../nodes/Binding';
import ElementWrapper from '../Element'; import ElementWrapper from '../Element';
import { dimensions } from '../../../../utils/patterns';
import get_object from '../../../utils/get_object'; import get_object from '../../../utils/get_object';
import Block from '../../Block'; import Block from '../../Block';
import Node from '../../../nodes/shared/Node'; import Node from '../../../nodes/shared/Node';
@ -15,6 +14,152 @@ function get_tail(node: INode) {
return { start: node.end, end }; return { start: node.end, end };
} }
function get_dom_updater(
element: ElementWrapper,
binding: BindingWrapper
) {
const { node } = element;
if (binding.is_readonly_media_attribute()) {
return null;
}
if (binding.node.name === 'this') {
return null;
}
if (node.name === 'select') {
return node.get_static_attribute_value('multiple') === true ?
`@select_options(${element.var}, ${binding.snippet})` :
`@select_option(${element.var}, ${binding.snippet})`;
}
if (binding.node.name === 'group') {
const type = node.get_static_attribute_value('type');
const condition = type === 'checkbox'
? `~${binding.snippet}.indexOf(${element.var}.__value)`
: `${element.var}.__value === ${binding.snippet}`;
return `${element.var}.checked = ${condition};`;
}
return `${element.var}.${binding.node.name} = ${binding.snippet};`;
}
function get_binding_group(renderer: Renderer, value: Node) {
const { parts } = flatten_reference(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.binding_groups.indexOf(keypath);
if (index === -1) {
index = renderer.binding_groups.length;
renderer.binding_groups.push(keypath);
}
return index;
}
function mutate_store(store, value, tail) {
return tail
? `${store}.update($$value => ($$value${tail} = ${value}, $$value));`
: `${store}.set(${value});`;
}
function get_value_from_dom(
renderer: Renderer,
element: ElementWrapper,
binding: BindingWrapper
) {
const { node } = element;
const { name } = binding.node;
if (name === 'this') {
return `$$node`;
}
// <select bind:value='selected>
if (node.name === 'select') {
return node.get_static_attribute_value('multiple') === true ?
`@select_multiple_value(this)` :
`@select_value(this)`;
}
const type = node.get_static_attribute_value('type');
// <input type='checkbox' bind:group='foo'>
if (name === 'group') {
const binding_group = get_binding_group(renderer, binding.node.expression.node);
if (type === 'checkbox') {
return `@get_binding_group_value($$binding_groups[${binding_group}])`;
}
return `this.__value`;
}
// <input type='range|number' bind:value>
if (type === 'range' || type === 'number') {
return `@to_number(this.${name})`;
}
if ((name === 'buffered' || name === 'seekable' || name === 'played')) {
return `@time_ranges_to_array(this.${name})`;
}
// everything else
return `this.${name}`;
}
function get_event_handler(
binding: BindingWrapper,
renderer: Renderer,
block: Block,
name: string,
snippet: string
) {
const value = get_value_from_dom(renderer, binding.parent, binding);
const store = binding.object[0] === '$' ? binding.object.slice(1) : null;
let tail = '';
if (binding.node.expression.node.type === 'MemberExpression') {
const { start, end } = get_tail(binding.node.expression.node);
tail = renderer.component.source.slice(start, end);
}
if (binding.node.is_contextual) {
const { object, property, snippet } = block.bindings.get(name);
return {
uses_context: true,
mutation: store
? mutate_store(store, value, tail)
: `${snippet}${tail} = ${value};`,
contextual_dependencies: new Set([object, property])
};
}
const mutation = store
? mutate_store(store, value, tail)
: `${snippet} = ${value};`;
if (binding.node.expression.node.type === 'MemberExpression') {
return {
uses_context: binding.node.expression.uses_context,
mutation,
contextual_dependencies: binding.node.expression.contextual_dependencies,
snippet
};
}
return {
uses_context: false,
mutation,
contextual_dependencies: new Set()
};
}
export default class BindingWrapper { export default class BindingWrapper {
node: Binding; node: Binding;
parent: ElementWrapper; parent: ElementWrapper;
@ -23,8 +168,8 @@ export default class BindingWrapper {
handler: { handler: {
uses_context: boolean; uses_context: boolean;
mutation: string; mutation: string;
contextual_dependencies: Set<string>, contextual_dependencies: Set<string>;
snippet?: string snippet?: string;
}; };
snippet: string; snippet: string;
is_readonly: boolean; is_readonly: boolean;
@ -87,7 +232,7 @@ export default class BindingWrapper {
} }
is_readonly_media_attribute() { is_readonly_media_attribute() {
return this.node.is_readonly_media_attribute() return this.node.is_readonly_media_attribute();
} }
render(block: Block, lock: string) { render(block: Block, lock: string) {
@ -95,23 +240,23 @@ export default class BindingWrapper {
const { parent } = this; const { parent } = this;
let update_conditions: string[] = this.needs_lock ? [`!${lock}`] : []; const update_conditions: string[] = this.needs_lock ? [`!${lock}`] : [];
const dependency_array = [...this.node.expression.dependencies]; const dependency_array = [...this.node.expression.dependencies];
if (dependency_array.length === 1) { if (dependency_array.length === 1) {
update_conditions.push(`changed.${dependency_array[0]}`) update_conditions.push(`changed.${dependency_array[0]}`);
} else if (dependency_array.length > 1) { } else if (dependency_array.length > 1) {
update_conditions.push( update_conditions.push(
`(${dependency_array.map(prop => `changed.${prop}`).join(' || ')})` `(${dependency_array.map(prop => `changed.${prop}`).join(' || ')})`
) );
} }
if (parent.node.name === 'input') { if (parent.node.name === 'input') {
const type = parent.node.get_static_attribute_value('type'); const type = parent.node.get_static_attribute_value('type');
if (type === null || type === "" || type === "text") { if (type === null || type === "" || type === "text") {
update_conditions.push(`(${parent.var}.${this.node.name} !== ${this.snippet})`) update_conditions.push(`(${parent.var}.${this.node.name} !== ${this.snippet})`);
} }
} }
@ -121,6 +266,7 @@ export default class BindingWrapper {
// special cases // special cases
switch (this.node.name) { switch (this.node.name) {
case 'group': case 'group':
{
const binding_group = get_binding_group(parent.renderer, this.node.expression.node); const binding_group = get_binding_group(parent.renderer, this.node.expression.node);
block.builders.hydrate.add_line( block.builders.hydrate.add_line(
@ -131,6 +277,7 @@ export default class BindingWrapper {
`ctx.$$binding_groups[${binding_group}].splice(ctx.$$binding_groups[${binding_group}].indexOf(${parent.var}), 1);` `ctx.$$binding_groups[${binding_group}].splice(ctx.$$binding_groups[${binding_group}].indexOf(${parent.var}), 1);`
); );
break; break;
}
case 'currentTime': case 'currentTime':
case 'playbackRate': case 'playbackRate':
@ -139,6 +286,7 @@ export default class BindingWrapper {
break; break;
case 'paused': case 'paused':
{
// this is necessary to prevent audio restarting by itself // this is necessary to prevent audio restarting by itself
const last = block.get_unique_name(`${parent.var}_is_paused`); const last = block.get_unique_name(`${parent.var}_is_paused`);
block.add_variable(last, 'true'); block.add_variable(last, 'true');
@ -146,6 +294,7 @@ export default class BindingWrapper {
update_conditions.push(`${last} !== (${last} = ${this.snippet})`); update_conditions.push(`${last} !== (${last} = ${this.snippet})`);
update_dom = `${parent.var}[${last} ? "pause" : "play"]();`; update_dom = `${parent.var}[${last} ? "pause" : "play"]();`;
break; break;
}
case 'value': case 'value':
if (parent.node.get_static_attribute_value('type') === 'file') { if (parent.node.get_static_attribute_value('type') === 'file') {
@ -164,149 +313,3 @@ export default class BindingWrapper {
} }
} }
} }
function get_dom_updater(
element: ElementWrapper,
binding: BindingWrapper
) {
const { node } = element;
if (binding.is_readonly_media_attribute()) {
return null;
}
if (binding.node.name === 'this') {
return null;
}
if (node.name === 'select') {
return node.get_static_attribute_value('multiple') === true ?
`@select_options(${element.var}, ${binding.snippet})` :
`@select_option(${element.var}, ${binding.snippet})`;
}
if (binding.node.name === 'group') {
const type = node.get_static_attribute_value('type');
const condition = type === 'checkbox'
? `~${binding.snippet}.indexOf(${element.var}.__value)`
: `${element.var}.__value === ${binding.snippet}`;
return `${element.var}.checked = ${condition};`
}
return `${element.var}.${binding.node.name} = ${binding.snippet};`;
}
function get_binding_group(renderer: Renderer, value: Node) {
const { parts } = flatten_reference(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.binding_groups.indexOf(keypath);
if (index === -1) {
index = renderer.binding_groups.length;
renderer.binding_groups.push(keypath);
}
return index;
}
function mutate_store(store, value, tail) {
return tail
? `${store}.update($$value => ($$value${tail} = ${value}, $$value));`
: `${store}.set(${value});`;
}
function get_event_handler(
binding: BindingWrapper,
renderer: Renderer,
block: Block,
name: string,
snippet: string
) {
const value = get_value_from_dom(renderer, binding.parent, binding);
const store = binding.object[0] === '$' ? binding.object.slice(1) : null;
let tail = '';
if (binding.node.expression.node.type === 'MemberExpression') {
const { start, end } = get_tail(binding.node.expression.node);
tail = renderer.component.source.slice(start, end);
}
if (binding.node.is_contextual) {
const { object, property, snippet } = block.bindings.get(name);
return {
uses_context: true,
mutation: store
? mutate_store(store, value, tail)
: `${snippet}${tail} = ${value};`,
contextual_dependencies: new Set([object, property])
};
}
const mutation = store
? mutate_store(store, value, tail)
: `${snippet} = ${value};`;
if (binding.node.expression.node.type === 'MemberExpression') {
return {
uses_context: binding.node.expression.uses_context,
mutation,
contextual_dependencies: binding.node.expression.contextual_dependencies,
snippet
};
}
return {
uses_context: false,
mutation,
contextual_dependencies: new Set()
};
}
function get_value_from_dom(
renderer: Renderer,
element: ElementWrapper,
binding: BindingWrapper
) {
const { node } = element;
const { name } = binding.node;
if (name === 'this') {
return `$$node`;
}
// <select bind:value='selected>
if (node.name === 'select') {
return node.get_static_attribute_value('multiple') === true ?
`@select_multiple_value(this)` :
`@select_value(this)`;
}
const type = node.get_static_attribute_value('type');
// <input type='checkbox' bind:group='foo'>
if (name === 'group') {
const binding_group = get_binding_group(renderer, binding.node.expression.node);
if (type === 'checkbox') {
return `@get_binding_group_value($$binding_groups[${binding_group}])`;
}
return `this.__value`;
}
// <input type='range|number' bind:value>
if (type === 'range' || type === 'number') {
return `@to_number(this.${name})`;
}
if ((name === 'buffered' || name === 'seekable' || name === 'played')) {
return `@time_ranges_to_array(this.${name})`
}
// everything else
return `this.${name}`;
}

@ -9,101 +9,11 @@ import Text from '../../../nodes/Text';
export interface StyleProp { export interface StyleProp {
key: string; key: string;
value: (Text|Expression)[]; value: Array<Text|Expression>;
} }
export default class StyleAttributeWrapper extends AttributeWrapper { function get_style_value(chunks: Array<Text | Expression>) {
node: Attribute; const value: Array<Text|Expression> = [];
parent: ElementWrapper;
render(block: Block) {
const style_props = optimize_style(this.node.chunks);
if (!style_props) return super.render(block);
style_props.forEach((prop: StyleProp) => {
let value;
if (is_dynamic(prop.value)) {
const prop_dependencies = new Set();
value =
((prop.value.length === 1 || prop.value[0].type === 'Text') ? '' : `"" + `) +
prop.value
.map((chunk) => {
if (chunk.type === 'Text') {
return stringify(chunk.data);
} else {
const snippet = chunk.render();
add_to_set(prop_dependencies, chunk.dependencies);
return chunk.get_precedence() <= 13 ? `(${snippet})` : snippet;
}
})
.join(' + ');
if (prop_dependencies.size) {
const dependencies = Array.from(prop_dependencies);
const condition = (
(block.has_outros ? `!#current || ` : '') +
dependencies.map(dependency => `changed.${dependency}`).join(' || ')
);
block.builders.update.add_conditional(
condition,
`@set_style(${this.parent.var}, "${prop.key}", ${value});`
);
}
} else {
value = stringify((prop.value[0] as Text).data);
}
block.builders.hydrate.add_line(
`@set_style(${this.parent.var}, "${prop.key}", ${value});`
);
});
}
}
function optimize_style(value: (Text|Expression)[]) {
const props: StyleProp[] = [];
let chunks = value.slice();
while (chunks.length) {
const chunk = chunks[0];
if (chunk.type !== 'Text') return null;
const key_match = /^\s*([\w-]+):\s*/.exec(chunk.data);
if (!key_match) return null;
const key = key_match[1];
const offset = key_match.index + key_match[0].length;
const remaining_data = chunk.data.slice(offset);
if (remaining_data) {
chunks[0] = {
start: chunk.start + offset,
end: chunk.end,
type: 'Text',
data: remaining_data
} as Text;
} else {
chunks.shift();
}
const result = get_style_value(chunks);
props.push({ key, value: result.value });
chunks = result.chunks;
}
return props;
}
function get_style_value(chunks: (Text | Expression)[]) {
const value: (Text|Expression)[] = [];
let in_url = false; let in_url = false;
let quote_mark = null; let quote_mark = null;
@ -171,6 +81,97 @@ function get_style_value(chunks: (Text | Expression)[]) {
}; };
} }
function is_dynamic(value: (Text|Expression)[]) { function optimize_style(value: Array<Text|Expression>) {
const props: StyleProp[] = [];
let chunks = value.slice();
while (chunks.length) {
const chunk = chunks[0];
if (chunk.type !== 'Text') return null;
const key_match = /^\s*([\w-]+):\s*/.exec(chunk.data);
if (!key_match) return null;
const key = key_match[1];
const offset = key_match.index + key_match[0].length;
const remaining_data = chunk.data.slice(offset);
if (remaining_data) {
// eslint-disable-next-line @typescript-eslint/no-object-literal-type-assertion
chunks[0] = {
start: chunk.start + offset,
end: chunk.end,
type: 'Text',
data: remaining_data
} as Text;
} else {
chunks.shift();
}
const result = get_style_value(chunks);
props.push({ key, value: result.value });
chunks = result.chunks;
}
return props;
}
function is_dynamic(value: Array<Text|Expression>) {
return value.length > 1 || value[0].type !== 'Text'; return value.length > 1 || value[0].type !== 'Text';
} }
export default class StyleAttributeWrapper extends AttributeWrapper {
node: Attribute;
parent: ElementWrapper;
render(block: Block) {
const style_props = optimize_style(this.node.chunks);
if (!style_props) return super.render(block);
style_props.forEach((prop: StyleProp) => {
let value;
if (is_dynamic(prop.value)) {
const prop_dependencies = new Set();
value =
((prop.value.length === 1 || prop.value[0].type === 'Text') ? '' : `"" + `) +
prop.value
.map((chunk) => {
if (chunk.type === 'Text') {
return stringify(chunk.data);
} else {
const snippet = chunk.render();
add_to_set(prop_dependencies, chunk.dependencies);
return chunk.get_precedence() <= 13 ? `(${snippet})` : snippet;
}
})
.join(' + ');
if (prop_dependencies.size) {
const dependencies = Array.from(prop_dependencies);
const condition = (
(block.has_outros ? `!#current || ` : '') +
dependencies.map(dependency => `changed.${dependency}`).join(' || ')
);
block.builders.update.add_conditional(
condition,
`@set_style(${this.parent.var}, "${prop.key}", ${value});`
);
}
} else {
value = stringify((prop.value[0] as Text).data);
}
block.builders.hydrate.add_line(
`@set_style(${this.parent.var}, "${prop.key}", ${value});`
);
});
}
}

@ -2,7 +2,6 @@ import Renderer from '../../Renderer';
import Element from '../../../nodes/Element'; import Element from '../../../nodes/Element';
import Wrapper from '../shared/Wrapper'; import Wrapper from '../shared/Wrapper';
import Block from '../../Block'; import Block from '../../Block';
import Node from '../../../nodes/shared/Node';
import { is_void, quote_prop_if_necessary, quote_name_if_necessary, sanitize } from '../../../../utils/names'; import { is_void, quote_prop_if_necessary, quote_name_if_necessary, sanitize } from '../../../../utils/names';
import FragmentWrapper from '../Fragment'; import FragmentWrapper from '../Fragment';
import { stringify, escape_html, escape } from '../../../utils/stringify'; import { stringify, escape_html, escape } from '../../../utils/stringify';
@ -24,19 +23,19 @@ import { get_context_merger } from '../shared/get_context_merger';
const events = [ const events = [
{ {
event_names: ['input'], event_names: ['input'],
filter: (node: Element, name: string) => filter: (node: Element, _name: string) =>
node.name === 'textarea' || node.name === 'textarea' ||
node.name === 'input' && !/radio|checkbox|range/.test(node.get_static_attribute_value('type') as string) node.name === 'input' && !/radio|checkbox|range/.test(node.get_static_attribute_value('type') as string)
}, },
{ {
event_names: ['change'], event_names: ['change'],
filter: (node: Element, name: string) => filter: (node: Element, _name: string) =>
node.name === 'select' || node.name === 'select' ||
node.name === 'input' && /radio|checkbox/.test(node.get_static_attribute_value('type') as string) node.name === 'input' && /radio|checkbox/.test(node.get_static_attribute_value('type') as string)
}, },
{ {
event_names: ['change', 'input'], event_names: ['change', 'input'],
filter: (node: Element, name: string) => filter: (node: Element, _name: string) =>
node.name === 'input' && node.get_static_attribute_value('type') === 'range' node.name === 'input' && node.get_static_attribute_value('type') === 'range'
}, },
@ -93,7 +92,7 @@ const events = [
// details event // details event
{ {
event_names: ['toggle'], event_names: ['toggle'],
filter: (node: Element, name: string) => filter: (node: Element, _name: string) =>
node.name === 'details' node.name === 'details'
}, },
]; ];
@ -119,7 +118,7 @@ export default class ElementWrapper extends Wrapper {
next_sibling: Wrapper next_sibling: Wrapper
) { ) {
super(renderer, block, parent, node); super(renderer, block, parent, node);
this.var = node.name.replace(/[^a-zA-Z0-9_$]/g, '_') this.var = node.name.replace(/[^a-zA-Z0-9_$]/g, '_');
this.class_dependencies = []; this.class_dependencies = [];
@ -230,6 +229,36 @@ export default class ElementWrapper extends Wrapper {
} }
render(block: Block, parent_node: string, parent_nodes: string) { render(block: Block, parent_node: string, parent_nodes: string) {
function to_html(wrapper: ElementWrapper | TextWrapper) {
if (wrapper.node.type === 'Text') {
const parent = wrapper.node.parent as Element;
const raw = parent && (
parent.name === 'script' ||
parent.name === 'style'
);
return raw
? wrapper.node.data
: escape_html(wrapper.node.data)
.replace(/\\/g, '\\\\')
.replace(/`/g, '\\`')
.replace(/\$/g, '\\$');
}
if (wrapper.node.name === 'noscript') return '';
let open = `<${wrapper.node.name}`;
(wrapper as ElementWrapper).attributes.forEach((attr: AttributeWrapper) => {
open += ` ${fix_attribute_casing(attr.node.name)}${attr.stringify()}`;
});
if (is_void(wrapper.node.name)) return open + '>';
return `${open}>${(wrapper as ElementWrapper).fragment.nodes.map(to_html).join('')}</${wrapper.node.name}>`;
}
const { renderer } = this; const { renderer } = this;
if (this.node.name === 'noscript') return; if (this.node.name === 'noscript') return;
@ -239,7 +268,7 @@ export default class ElementWrapper extends Wrapper {
} }
const node = this.var; const node = this.var;
const nodes = parent_nodes && block.get_unique_name(`${this.var}_nodes`) // if we're in unclaimable territory, i.e. <head>, parent_nodes is null const nodes = parent_nodes && block.get_unique_name(`${this.var}_nodes`); // if we're in unclaimable territory, i.e. <head>, parent_nodes is null
block.add_variable(node); block.add_variable(node);
const render_statement = this.get_render_statement(); const render_statement = this.get_render_statement();
@ -328,36 +357,6 @@ export default class ElementWrapper extends Wrapper {
); );
} }
function to_html(wrapper: ElementWrapper | TextWrapper) {
if (wrapper.node.type === 'Text') {
const parent = wrapper.node.parent as Element;
const raw = parent && (
parent.name === 'script' ||
parent.name === 'style'
);
return raw
? wrapper.node.data
: escape_html(wrapper.node.data)
.replace(/\\/g, '\\\\')
.replace(/`/g, '\\`')
.replace(/\$/g, '\\$');
}
if (wrapper.node.name === 'noscript') return '';
let open = `<${wrapper.node.name}`;
(wrapper as ElementWrapper).attributes.forEach((attr: AttributeWrapper) => {
open += ` ${fix_attribute_casing(attr.node.name)}${attr.stringify()}`
});
if (is_void(wrapper.node.name)) return open + '>';
return `${open}>${(wrapper as ElementWrapper).fragment.nodes.map(to_html).join('')}</${wrapper.node.name}>`;
}
if (renderer.options.dev) { if (renderer.options.dev) {
const loc = renderer.locate(this.node.start); const loc = renderer.locate(this.node.start);
block.builders.hydrate.add_line( block.builders.hydrate.add_line(
@ -441,7 +440,7 @@ export default class ElementWrapper extends Wrapper {
binding.render(block, lock); binding.render(block, lock);
}); });
// media bindings awkward special case. The native timeupdate events // media bindings — awkward special case. The native timeupdate events
// fire too infrequently, so we need to take matters into our // fire too infrequently, so we need to take matters into our
// own hands // own hands
let animation_frame; let animation_frame;
@ -454,7 +453,7 @@ export default class ElementWrapper extends Wrapper {
let callee; let callee;
// TODO dry this out similar code for event handlers and component bindings // TODO dry this out — similar code for event handlers and component bindings
if (has_local_function) { if (has_local_function) {
// need to create a block-local function that calls an instance-level function // need to create a block-local function that calls an instance-level function
block.builders.init.add_block(deindent` block.builders.init.add_block(deindent`
@ -796,7 +795,8 @@ export default class ElementWrapper extends Wrapper {
add_classes(block: Block) { add_classes(block: Block) {
this.node.classes.forEach(class_directive => { this.node.classes.forEach(class_directive => {
const { expression, name } = class_directive; const { expression, name } = class_directive;
let snippet, dependencies; let snippet;
let dependencies;
if (expression) { if (expression) {
snippet = expression.render(block); snippet = expression.render(block);
dependencies = expression.dependencies; dependencies = expression.dependencies;

@ -14,7 +14,6 @@ import Text from './Text';
import Title from './Title'; import Title from './Title';
import Window from './Window'; import Window from './Window';
import { INode } from '../../nodes/interfaces'; import { INode } from '../../nodes/interfaces';
import TextWrapper from './Text';
import Renderer from '../Renderer'; import Renderer from '../Renderer';
import Block from '../Block'; import Block from '../Block';
import { trim_start, trim_end } from '../../../utils/trim'; import { trim_start, trim_end } from '../../../utils/trim';
@ -64,14 +63,14 @@ export default class FragmentWrapper {
const child = nodes[i]; const child = nodes[i];
if (!child.type) { if (!child.type) {
throw new Error(`missing type`) throw new Error(`missing type`);
} }
if (!(child.type in wrappers)) { if (!(child.type in wrappers)) {
throw new Error(`TODO implement ${child.type}`); throw new Error(`TODO implement ${child.type}`);
} }
// special case this is an easy way to remove whitespace surrounding // special case — this is an easy way to remove whitespace surrounding
// <svelte:window/>. lil hacky but it works // <svelte:window/>. lil hacky but it works
if (child.type === 'Window') { if (child.type === 'Window') {
window_wrapper = new Window(renderer, block, parent, child); window_wrapper = new Window(renderer, block, parent, child);
@ -102,7 +101,7 @@ export default class FragmentWrapper {
continue; continue;
} }
const wrapper = new TextWrapper(renderer, block, parent, child, data); const wrapper = new Text(renderer, block, parent, child, data);
if (wrapper.skip) continue; if (wrapper.skip) continue;
this.nodes.unshift(wrapper); this.nodes.unshift(wrapper);
@ -120,7 +119,7 @@ export default class FragmentWrapper {
} }
if (strip_whitespace) { if (strip_whitespace) {
const first = this.nodes[0] as TextWrapper; const first = this.nodes[0] as Text;
if (first && first.node.type === 'Text') { if (first && first.node.type === 'Text') {
first.data = trim_start(first.data); first.data = trim_start(first.data);

@ -29,7 +29,7 @@ export default class HeadWrapper extends Wrapper {
); );
} }
render(block: Block, parent_node: string, parent_nodes: string) { render(block: Block, _parent_node: string, _parent_nodes: string) {
this.fragment.render(block, 'document.head', 'nodes'); this.fragment.render(block, 'document.head', 'nodes');
} }
} }

@ -213,8 +213,8 @@ export default class IfBlockWrapper extends Wrapper {
block.builders.init.add_block(deindent` block.builders.init.add_block(deindent`
function ${select_block_type}(ctx) { function ${select_block_type}(ctx) {
${this.branches ${this.branches
.map(({ condition, block }) => `${condition ? `if (${condition}) ` : ''}return ${block.name};`) .map(({ condition, block }) => `${condition ? `if (${condition}) ` : ''}return ${block.name};`)
.join('\n')} .join('\n')}
} }
`); `);
@ -292,8 +292,8 @@ export default class IfBlockWrapper extends Wrapper {
function ${select_block_type}(ctx) { function ${select_block_type}(ctx) {
${this.branches ${this.branches
.map(({ condition }, i) => `${condition ? `if (${condition}) ` : ''}return ${i};`) .map(({ condition }, i) => `${condition ? `if (${condition}) ` : ''}return ${i};`)
.join('\n')} .join('\n')}
${!has_else && `return -1;`} ${!has_else && `return -1;`}
} }
`); `);

@ -17,7 +17,7 @@ import TemplateScope from '../../../nodes/shared/TemplateScope';
export default class InlineComponentWrapper extends Wrapper { export default class InlineComponentWrapper extends Wrapper {
var: string; var: string;
slots: Map<string, { block: Block, scope: TemplateScope, fn?: string }> = new Map(); slots: Map<string, { block: Block; scope: TemplateScope; fn?: string }> = new Map();
node: InlineComponent; node: InlineComponent;
fragment: FragmentWrapper; fragment: FragmentWrapper;
@ -62,8 +62,8 @@ export default class InlineComponentWrapper extends Wrapper {
this.var = ( this.var = (
this.node.name === 'svelte:self' ? renderer.component.name : this.node.name === 'svelte:self' ? renderer.component.name :
this.node.name === 'svelte:component' ? 'switch_instance' : this.node.name === 'svelte:component' ? 'switch_instance' :
sanitize(this.node.name) sanitize(this.node.name)
).toLowerCase(); ).toLowerCase();
if (this.node.children.length) { if (this.node.children.length) {
@ -234,12 +234,12 @@ export default class InlineComponentWrapper extends Wrapper {
if (attribute.dependencies.size > 0) { if (attribute.dependencies.size > 0) {
updates.push(deindent` updates.push(deindent`
if (${[...attribute.dependencies] if (${[...attribute.dependencies]
.map(dependency => `changed.${dependency}`) .map(dependency => `changed.${dependency}`)
.join(' || ')}) ${name_changes}${quote_prop_if_necessary(attribute.name)} = ${attribute.get_value(block)}; .join(' || ')}) ${name_changes}${quote_prop_if_necessary(attribute.name)} = ${attribute.get_value(block)};
`); `);
} }
}); });
} }
} }
if (non_let_dependencies.length > 0) { if (non_let_dependencies.length > 0) {
@ -262,10 +262,10 @@ export default class InlineComponentWrapper extends Wrapper {
let object; let object;
if (binding.is_contextual && binding.expression.node.type === 'Identifier') { if (binding.is_contextual && binding.expression.node.type === 'Identifier') {
// bind:x={y} we can't just do `y = x`, we need to // bind:x={y} — we can't just do `y = x`, we need to
// to `array[index] = x; // to `array[index] = x;
const { name } = binding.expression.node; const { name } = binding.expression.node;
const { object, property, snippet } = block.bindings.get(name); const { snippet } = block.bindings.get(name);
lhs = snippet; lhs = snippet;
// TODO we need to invalidate... something // TODO we need to invalidate... something
@ -316,7 +316,7 @@ export default class InlineComponentWrapper extends Wrapper {
let lhs = component.source.slice(binding.expression.node.start, binding.expression.node.end).trim(); let lhs = component.source.slice(binding.expression.node.start, binding.expression.node.end).trim();
if (binding.is_contextual && binding.expression.node.type === 'Identifier') { if (binding.is_contextual && binding.expression.node.type === 'Identifier') {
// bind:x={y} we can't just do `y = x`, we need to // bind:x={y} — we can't just do `y = x`, we need to
// to `array[index] = x; // to `array[index] = x;
const { name } = binding.expression.node; const { name } = binding.expression.node;
const { object, property, snippet } = block.bindings.get(name); const { object, property, snippet } = block.bindings.get(name);

@ -78,7 +78,7 @@ export default class SlotWrapper extends Wrapper {
}); });
if (attribute.dependencies.size > 0) { if (attribute.dependencies.size > 0) {
changes_props.push(`${attribute.name}: ${[...attribute.dependencies].join(' || ')}`) changes_props.push(`${attribute.name}: ${[...attribute.dependencies].join(' || ')}`);
} }
}); });
@ -101,7 +101,7 @@ export default class SlotWrapper extends Wrapper {
const ${slot} = @create_slot(${slot_definition}, ctx, ${get_slot_context}); const ${slot} = @create_slot(${slot_definition}, ctx, ${get_slot_context});
`); `);
let mount_before = block.builders.mount.toString(); const mount_before = block.builders.mount.toString();
block.builders.create.push_condition(`!${slot}`); block.builders.create.push_condition(`!${slot}`);
block.builders.claim.push_condition(`!${slot}`); block.builders.claim.push_condition(`!${slot}`);

@ -14,13 +14,13 @@ export default class TitleWrapper extends Wrapper {
block: Block, block: Block,
parent: Wrapper, parent: Wrapper,
node: Title, node: Title,
strip_whitespace: boolean, _strip_whitespace: boolean,
next_sibling: Wrapper _next_sibling: Wrapper
) { ) {
super(renderer, block, parent, node); super(renderer, block, parent, node);
} }
render(block: Block, parent_node: string, parent_nodes: string) { render(block: Block, _parent_node: string, _parent_nodes: string) {
const is_dynamic = !!this.node.children.find(node => node.type !== 'Text'); const is_dynamic = !!this.node.children.find(node => node.type !== 'Text');
if (is_dynamic) { if (is_dynamic) {
@ -28,16 +28,16 @@ export default class TitleWrapper extends Wrapper {
const all_dependencies = new Set(); const all_dependencies = new Set();
// TODO some of this code is repeated in Tag.ts would be good to // 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 // DRY it out if that's possible without introducing crazy indirection
if (this.node.children.length === 1) { if (this.node.children.length === 1) {
// single {tag} may be a non-string // single {tag} — may be a non-string
// @ts-ignore todo: check this // @ts-ignore todo: check this
const { expression } = this.node.children[0]; const { expression } = this.node.children[0];
value = expression.render(block); value = expression.render(block);
add_to_set(all_dependencies, expression.dependencies); add_to_set(all_dependencies, expression.dependencies);
} else { } else {
// '{foo} {bar}' treat as string concatenation // '{foo} {bar}' — treat as string concatenation
value = value =
(this.node.children[0].type === 'Text' ? '' : `"" + `) + (this.node.children[0].type === 'Text' ? '' : `"" + `) +
this.node.children this.node.children
@ -65,13 +65,12 @@ export default class TitleWrapper extends Wrapper {
if (this.node.should_cache) block.add_variable(last); if (this.node.should_cache) block.add_variable(last);
let updater;
const init = this.node.should_cache ? `${last} = ${value}` : value; const init = this.node.should_cache ? `${last} = ${value}` : value;
block.builders.init.add_line( block.builders.init.add_line(
`document.title = ${init};` `document.title = ${init};`
); );
updater = `document.title = ${this.node.should_cache ? last : value};`; const updater = `document.title = ${this.node.should_cache ? last : value};`;
if (all_dependencies.size) { if (all_dependencies.size) {
const dependencies = Array.from(all_dependencies); const dependencies = Array.from(all_dependencies);

@ -1,6 +1,5 @@
import Renderer from '../Renderer'; import Renderer from '../Renderer';
import Block from '../Block'; import Block from '../Block';
import Node from '../../nodes/shared/Node';
import Wrapper from './shared/Wrapper'; import Wrapper from './shared/Wrapper';
import deindent from '../../utils/deindent'; import deindent from '../../utils/deindent';
import add_event_handlers from './shared/add_event_handlers'; import add_event_handlers from './shared/add_event_handlers';
@ -38,7 +37,7 @@ export default class WindowWrapper extends Wrapper {
super(renderer, block, parent, node); super(renderer, block, parent, node);
} }
render(block: Block, parent_node: string, parent_nodes: string) { render(block: Block, _parent_node: string, _parent_nodes: string) {
const { renderer } = this; const { renderer } = this;
const { component } = renderer; const { component } = renderer;
@ -88,7 +87,7 @@ export default class WindowWrapper extends Wrapper {
bindings.scrollY && `"${bindings.scrollY}" in this._state` bindings.scrollY && `"${bindings.scrollY}" in this._state`
].filter(Boolean).join(' || '); ].filter(Boolean).join(' || ');
const x = bindings.scrollX && `this._state.${bindings.scrollX}`; const x = bindings.scrollX && `this._state.${bindings.scrollX}`;
const y = bindings.scrollY && `this._state.${bindings.scrollY}`; const y = bindings.scrollY && `this._state.${bindings.scrollY}`;
renderer.meta_bindings.add_block(deindent` renderer.meta_bindings.add_block(deindent`
@ -142,17 +141,17 @@ export default class WindowWrapper extends Wrapper {
if (bindings.scrollX || bindings.scrollY) { if (bindings.scrollX || bindings.scrollY) {
block.builders.update.add_block(deindent` block.builders.update.add_block(deindent`
if (${ if (${
[bindings.scrollX, bindings.scrollY].filter(Boolean).map( [bindings.scrollX, bindings.scrollY].filter(Boolean).map(
b => `changed.${b}` b => `changed.${b}`
).join(' || ') ).join(' || ')
} && !${scrolling}) { } && !${scrolling}) {
${scrolling} = true; ${scrolling} = true;
clearTimeout(${scrolling_timeout}); clearTimeout(${scrolling_timeout});
window.scrollTo(${ window.scrollTo(${
bindings.scrollX ? `ctx.${bindings.scrollX}` : `window.pageXOffset` bindings.scrollX ? `ctx.${bindings.scrollX}` : `window.pageXOffset`
}, ${ }, ${
bindings.scrollY ? `ctx.${bindings.scrollY}` : `window.pageYOffset` bindings.scrollY ? `ctx.${bindings.scrollY}` : `window.pageYOffset`
}); });
${scrolling_timeout} = setTimeout(${clear_scrolling}, 100); ${scrolling_timeout} = setTimeout(${clear_scrolling}, 100);
} }
`); `);

@ -43,7 +43,7 @@ export default class Wrapper {
} }
get_or_create_anchor(block: Block, parent_node: string, parent_nodes: string) { get_or_create_anchor(block: Block, parent_node: string, parent_nodes: string) {
// TODO use this in EachBlock and IfBlock tricky because // TODO use this in EachBlock and IfBlock — tricky because
// children need to be created first // children need to be created first
const needs_anchor = this.next ? !this.next.is_dom_node() : !parent_node || !this.parent.is_dom_node(); const needs_anchor = this.next ? !this.next.is_dom_node() : !parent_node || !this.parent.is_dom_node();
const anchor = needs_anchor const anchor = needs_anchor
@ -76,7 +76,7 @@ export default class Wrapper {
); );
} }
render(block: Block, parent_node: string, parent_nodes: string){ render(_block: Block, _parent_node: string, _parent_nodes: string) {
throw Error('Wrapper class is not renderable'); throw Error('Wrapper class is not renderable');
} }
} }

@ -10,7 +10,8 @@ export default function add_actions(
) { ) {
actions.forEach(action => { actions.forEach(action => {
const { expression } = action; const { expression } = action;
let snippet, dependencies; let snippet;
let dependencies;
if (expression) { if (expression) {
snippet = expression.render(block); snippet = expression.render(block);
@ -44,4 +45,4 @@ export default function add_actions(
`if (${name} && typeof ${name}.destroy === 'function') ${name}.destroy();` `if (${name} && typeof ${name}.destroy === 'function') ${name}.destroy();`
); );
}); });
} }

@ -16,7 +16,7 @@ import { INode } from '../nodes/interfaces';
type Handler = (node: any, renderer: Renderer, options: CompileOptions) => void; type Handler = (node: any, renderer: Renderer, options: CompileOptions) => void;
function noop(){} function noop() {}
const handlers: Record<string, Handler> = { const handlers: Record<string, Handler> = {
AwaitBlock, AwaitBlock,
@ -38,8 +38,8 @@ const handlers: Record<string, Handler> = {
}; };
export interface RenderOptions extends CompileOptions{ export interface RenderOptions extends CompileOptions{
locate: (c: number) => { line: number; column: number; }; locate: (c: number) => { line: number; column: number };
}; }
export default class Renderer { export default class Renderer {
has_bindings = false; has_bindings = false;

@ -9,7 +9,7 @@ export default function(node: EachBlock, renderer: Renderer, options: RenderOpti
const ctx = node.index const ctx = node.index
? `([✂${start}-${end}✂], ${node.index})` ? `([✂${start}-${end}✂], ${node.index})`
: `([✂${start}-${end}✂])` : `([✂${start}-${end}✂])`;
const open = `\${${node.else ? `${snippet}.length ? ` : ''}@each(${snippet}, ${ctx} => \``; const open = `\${${node.else ? `${snippet}.length ? ` : ''}@each(${snippet}, ${ctx} => \``;
renderer.append(open); renderer.append(open);

@ -1,7 +1,6 @@
import { is_void, quote_prop_if_necessary, quote_name_if_necessary } from '../../../utils/names'; import { is_void, quote_prop_if_necessary, quote_name_if_necessary } from '../../../utils/names';
import Attribute from '../../nodes/Attribute'; import Attribute from '../../nodes/Attribute';
import Class from '../../nodes/Class'; import Class from '../../nodes/Class';
import Node from '../../nodes/shared/Node';
import { snip } from '../../utils/snip'; import { snip } from '../../utils/snip';
import { stringify_attribute } from '../../utils/stringify_attribute'; import { stringify_attribute } from '../../utils/stringify_attribute';
import { get_slot_scope } from './shared/get_slot_scope'; import { get_slot_scope } from './shared/get_slot_scope';

@ -2,6 +2,6 @@ import { snip } from '../../utils/snip';
import Renderer, { RenderOptions } from '../Renderer'; import Renderer, { RenderOptions } from '../Renderer';
import RawMustacheTag from '../../nodes/RawMustacheTag'; import RawMustacheTag from '../../nodes/RawMustacheTag';
export default function(node: RawMustacheTag, renderer: Renderer, options: RenderOptions) { export default function(node: RawMustacheTag, renderer: Renderer, _options: RenderOptions) {
renderer.append('${' + snip(node.expression) + '}'); renderer.append('${' + snip(node.expression) + '}');
} }

@ -1,6 +1,6 @@
import { snip } from '../../utils/snip'; import { snip } from '../../utils/snip';
import Renderer, { RenderOptions } from '../Renderer'; import Renderer, { RenderOptions } from '../Renderer';
export default function(node, renderer: Renderer, options: RenderOptions) { export default function(node, renderer: Renderer, _options: RenderOptions) {
const snippet = snip(node.expression); const snippet = snip(node.expression);
renderer.append( renderer.append(

@ -3,7 +3,7 @@ import Renderer, { RenderOptions } from '../Renderer';
import Text from '../../nodes/Text'; import Text from '../../nodes/Text';
import Element from '../../nodes/Element'; import Element from '../../nodes/Element';
export default function(node: Text, renderer: Renderer, options: RenderOptions) { export default function(node: Text, renderer: Renderer, _options: RenderOptions) {
let text = node.data; let text = node.data;
if ( if (
!node.parent || !node.parent ||

@ -7,6 +7,28 @@ import { extract_names } from '../utils/scope';
import { INode } from '../nodes/interfaces'; import { INode } from '../nodes/interfaces';
import Text from '../nodes/Text'; import Text from '../nodes/Text';
function trim(nodes: INode[]) {
let start = 0;
for (; start < nodes.length; start += 1) {
const node = nodes[start] as Text;
if (node.type !== 'Text') break;
node.data = node.data.replace(/^\s+/, '');
if (node.data) break;
}
let end = nodes.length;
for (; end > start; end -= 1) {
const node = nodes[end - 1] as Text;
if (node.type !== 'Text') break;
node.data = node.data.replace(/\s+$/, '');
if (node.data) break;
}
return nodes.slice(start, end);
}
export default function ssr( export default function ssr(
component: Component, component: Component,
options: CompileOptions options: CompileOptions
@ -66,7 +88,7 @@ export default function ssr(
: []; : [];
const reactive_declarations = component.reactive_declarations.map(d => { const reactive_declarations = component.reactive_declarations.map(d => {
let snippet = `[${d.node.body.start}-${d.node.end}]`; let snippet = `[✂${d.node.body.start}-${d.node.end}✂]`;
if (d.declaration) { if (d.declaration) {
const declared = extract_names(d.declaration); const declared = extract_names(d.declaration);
@ -152,25 +174,3 @@ export default function ssr(
}); });
`).trim(); `).trim();
} }
function trim(nodes: INode[]) {
let start = 0;
for (; start < nodes.length; start += 1) {
const node = nodes[start] as Text;
if (node.type !== 'Text') break;
node.data = node.data.replace(/^\s+/, '');
if (node.data) break;
}
let end = nodes.length;
for (; end > start; end -= 1) {
const node = nodes[end - 1] as Text;
if (node.type !== 'Text') break;
node.data = node.data.replace(/\s+$/, '');
if (node.data) break;
}
return nodes.slice(start, end);
}

@ -17,6 +17,41 @@ interface BlockChunk extends Chunk {
parent: BlockChunk; parent: BlockChunk;
} }
function find_line(chunk: BlockChunk) {
for (const c of chunk.children) {
if (c.type === 'line' || find_line(c as BlockChunk)) return true;
}
return false;
}
function chunk_to_string(chunk: Chunk, level: number = 0, last_block?: boolean, first?: boolean): string {
if (chunk.type === 'line') {
return `${last_block || (!first && chunk.block) ? '\n' : ''}${chunk.line.replace(/^/gm, repeat('\t', level))}`;
} else if (chunk.type === 'condition') {
let t = false;
const lines = chunk.children.map((c, i) => {
const str = chunk_to_string(c, level + 1, t, i === 0);
t = c.type !== 'line' || c.block;
return str;
}).filter(l => !!l);
if (!lines.length) return '';
return `${last_block || (!first) ? '\n' : ''}${repeat('\t', level)}if (${chunk.condition}) {\n${lines.join('\n')}\n${repeat('\t', level)}}`;
} else if (chunk.type === 'root') {
let t = false;
const lines = chunk.children.map((c, i) => {
const str = chunk_to_string(c, 0, t, i === 0);
t = c.type !== 'line' || c.block;
return str;
}).filter(l => !!l);
if (!lines.length) return '';
return lines.join('\n');
}
}
export default class CodeBuilder { export default class CodeBuilder {
root: BlockChunk = { type: 'root', children: [], parent: null }; root: BlockChunk = { type: 'root', children: [], parent: null };
last: Chunk; last: Chunk;
@ -66,38 +101,3 @@ export default class CodeBuilder {
return chunk_to_string(this.root); return chunk_to_string(this.root);
} }
} }
function find_line(chunk: BlockChunk) {
for (const c of chunk.children) {
if (c.type === 'line' || find_line(c as BlockChunk)) return true;
}
return false;
}
function chunk_to_string(chunk: Chunk, level: number = 0, last_block?: boolean, first?: boolean): string {
if (chunk.type === 'line') {
return `${last_block || (!first && chunk.block) ? '\n' : ''}${chunk.line.replace(/^/gm, repeat('\t', level))}`;
} else if (chunk.type === 'condition') {
let t = false;
const lines = chunk.children.map((c, i) => {
const str = chunk_to_string(c, level + 1, t, i === 0);
t = c.type !== 'line' || c.block;
return str;
}).filter(l => !!l);
if (!lines.length) return '';
return `${last_block || (!first) ? '\n' : ''}${repeat('\t', level)}if (${chunk.condition}) {\n${lines.join('\n')}\n${repeat('\t', level)}}`;
} else if (chunk.type === 'root') {
let t = false;
const lines = chunk.children.map((c, i) => {
const str = chunk_to_string(c, 0, t, i === 0);
t = c.type !== 'line' || c.block;
return str;
}).filter(l => !!l);
if (!lines.length) return '';
return lines.join('\n');
}
}

@ -1,4 +1,4 @@
export default function add_to_set<T>(a: Set<T>, b: Set<T> | Array<T>) { export default function add_to_set<T>(a: Set<T>, b: Set<T> | T[]) {
// @ts-ignore // @ts-ignore
b.forEach(item => { b.forEach(item => {
a.add(item); a.add(item);

@ -1,5 +1,15 @@
const start = /\n(\t+)/; const start = /\n(\t+)/;
function get_current_indentation(str: string) {
let a = str.length;
while (a > 0 && str[a - 1] !== '\n') a -= 1;
let b = a;
while (b < str.length && /\s/.test(str[b])) b += 1;
return str.slice(a, b);
}
export default function deindent( export default function deindent(
strings: TemplateStringsArray, strings: TemplateStringsArray,
...values: any[] ...values: any[]
@ -41,13 +51,3 @@ export default function deindent(
return result.trim().replace(/\t+$/gm, '').replace(/{\n\n/gm, '{\n'); return result.trim().replace(/\t+$/gm, '').replace(/{\n\n/gm, '{\n');
} }
function get_current_indentation(str: string) {
let a = str.length;
while (a > 0 && str[a - 1] !== '\n') a -= 1;
let b = a;
while (b < str.length && /\s/.test(str[b])) b += 1;
return str.slice(a, b);
}

@ -3,6 +3,85 @@ import is_reference from 'is-reference';
import { Node } from '../../interfaces'; import { Node } from '../../interfaces';
import { Node as ESTreeNode } from 'estree'; import { Node as ESTreeNode } from 'estree';
const extractors = {
Identifier(nodes: Node[], param: Node) {
nodes.push(param);
},
ObjectPattern(nodes: Node[], param: Node) {
param.properties.forEach((prop: Node) => {
if (prop.type === 'RestElement') {
nodes.push(prop.argument);
} else {
extractors[prop.value.type](nodes, prop.value);
}
});
},
ArrayPattern(nodes: Node[], param: Node) {
param.elements.forEach((element: Node) => {
if (element) extractors[element.type](nodes, element);
});
},
RestElement(nodes: Node[], param: Node) {
extractors[param.argument.type](nodes, param.argument);
},
AssignmentPattern(nodes: Node[], param: Node) {
extractors[param.left.type](nodes, param.left);
}
};
export function extract_identifiers(param: Node) {
const nodes: Node[] = [];
extractors[param.type] && extractors[param.type](nodes, param);
return nodes;
}
export function extract_names(param: Node) {
return extract_identifiers(param).map(node => node.name);
}
export class Scope {
parent: Scope;
block: boolean;
declarations: Map<string, Node> = new Map();
initialised_declarations: Set<string> = new Set();
constructor(parent: Scope, block: boolean) {
this.parent = parent;
this.block = block;
}
add_declaration(node: Node) {
if (node.kind === 'var' && this.block && this.parent) {
this.parent.add_declaration(node);
} else if (node.type === 'VariableDeclaration') {
node.declarations.forEach((declarator: Node) => {
extract_names(declarator.id).forEach(name => {
this.declarations.set(name, node);
if (declarator.init) this.initialised_declarations.add(name);
});
});
} else {
this.declarations.set(node.id.name, node);
}
}
find_owner(name: string): Scope {
if (this.declarations.has(name)) return this;
return this.parent && this.parent.find_owner(name);
}
has(name: string): boolean {
return (
this.declarations.has(name) || (this.parent && this.parent.has(name))
);
}
}
export function create_scopes(expression: Node) { export function create_scopes(expression: Node) {
const map = new WeakMap(); const map = new WeakMap();
@ -59,82 +138,3 @@ export function create_scopes(expression: Node) {
return { map, scope, globals }; return { map, scope, globals };
} }
export class Scope {
parent: Scope;
block: boolean;
declarations: Map<string, Node> = new Map();
initialised_declarations: Set<string> = new Set();
constructor(parent: Scope, block: boolean) {
this.parent = parent;
this.block = block;
}
add_declaration(node: Node) {
if (node.kind === 'var' && this.block && this.parent) {
this.parent.add_declaration(node);
} else if (node.type === 'VariableDeclaration') {
node.declarations.forEach((declarator: Node) => {
extract_names(declarator.id).forEach(name => {
this.declarations.set(name, node);
if (declarator.init) this.initialised_declarations.add(name);
});
});
} else {
this.declarations.set(node.id.name, node);
}
}
find_owner(name: string): Scope {
if (this.declarations.has(name)) return this;
return this.parent && this.parent.find_owner(name);
}
has(name: string): boolean {
return (
this.declarations.has(name) || (this.parent && this.parent.has(name))
);
}
}
export function extract_names(param: Node) {
return extract_identifiers(param).map(node => node.name);
}
export function extract_identifiers(param: Node) {
const nodes: Node[] = [];
extractors[param.type] && extractors[param.type](nodes, param);
return nodes;
}
const extractors = {
Identifier(nodes: Node[], param: Node) {
nodes.push(param);
},
ObjectPattern(nodes: Node[], param: Node) {
param.properties.forEach((prop: Node) => {
if (prop.type === 'RestElement') {
nodes.push(prop.argument);
} else {
extractors[prop.value.type](nodes, prop.value);
}
});
},
ArrayPattern(nodes: Node[], param: Node) {
param.elements.forEach((element: Node) => {
if (element) extractors[element.type](nodes, element);
});
},
RestElement(nodes: Node[], param: Node) {
extractors[param.argument.type](nodes, param.argument);
},
AssignmentPattern(nodes: Node[], param: Node) {
extractors[param.left.type](nodes, param.left);
}
};

@ -1,13 +1,13 @@
export function stringify(data: string, options = {}) {
return JSON.stringify(escape(data, options));
}
export function escape(data: string, { only_escape_at_symbol = false } = {}) { export function escape(data: string, { only_escape_at_symbol = false } = {}) {
return data.replace(only_escape_at_symbol ? /@+/g : /(@+|#+)/g, (match: string) => { return data.replace(only_escape_at_symbol ? /@+/g : /(@+|#+)/g, (match: string) => {
return match + match[0]; return match + match[0];
}); });
} }
export function stringify(data: string, options = {}) {
return JSON.stringify(escape(data, options));
}
const escaped = { const escaped = {
'&': '&amp;', '&': '&amp;',
'<': '&lt;', '<': '&lt;',

@ -7,33 +7,33 @@ interface BaseNode {
} }
export interface Text extends BaseNode { export interface Text extends BaseNode {
type: 'Text', type: 'Text';
data: string; data: string;
} }
export interface MustacheTag extends BaseNode { export interface MustacheTag extends BaseNode {
type: 'MustacheTag', type: 'MustacheTag';
expression: Node; expression: Node;
} }
export type DirectiveType = 'Action' export type DirectiveType = 'Action'
| 'Animation' | 'Animation'
| 'Binding' | 'Binding'
| 'Class' | 'Class'
| 'EventHandler' | 'EventHandler'
| 'Let' | 'Let'
| 'Ref' | 'Ref'
| 'Transition'; | 'Transition';
interface BaseDirective extends BaseNode { interface BaseDirective extends BaseNode {
type: DirectiveType; type: DirectiveType;
expression: null|Node; expression: null|Node;
name: string; name: string;
modifiers: string[] modifiers: string[];
} }
export interface Transition extends BaseDirective{ export interface Transition extends BaseDirective{
type: 'Transition', type: 'Transition';
intro: boolean; intro: boolean;
outro: boolean; outro: boolean;
} }
@ -41,17 +41,17 @@ export interface Transition extends BaseDirective{
export type Directive = BaseDirective | Transition; export type Directive = BaseDirective | Transition;
export type Node = Text export type Node = Text
| MustacheTag | MustacheTag
| BaseNode | BaseNode
| Directive | Directive
| Transition; | Transition;
export interface Parser { export interface Parser {
readonly template: string; readonly template: string;
readonly filename?: string; readonly filename?: string;
index: number; index: number;
stack: Array<Node>; stack: Node[];
html: Node; html: Node;
css: Node; css: Node;
@ -68,7 +68,7 @@ export interface Ast {
export interface Warning { export interface Warning {
start?: { line: number; column: number; pos?: number }; start?: { line: number; column: number; pos?: number };
end?: { line: number; column: number; }; end?: { line: number; column: number };
pos?: number; pos?: number;
code: string; code: string;
message: string; message: string;
@ -114,7 +114,7 @@ export interface Visitor {
export interface AppendTarget { export interface AppendTarget {
slots: Record<string, string>; slots: Record<string, string>;
slot_stack: string[] slot_stack: string[];
} }
export interface Var { export interface Var {

@ -14,7 +14,7 @@ export class Parser {
readonly customElement: boolean; readonly customElement: boolean;
index = 0; index = 0;
stack: Array<Node> = []; stack: Node[] = [];
html: Node; html: Node;
css: Node[] = []; css: Node[] = [];
@ -89,7 +89,7 @@ export class Parser {
}, err.pos); }, err.pos);
} }
error({ code, message }: { code: string, message: string }, index = this.index) { error({ code, message }: { code: string; message: string }, index = this.index) {
error(message, { error(message, {
name: 'ParseError', name: 'ParseError',
code, code,

@ -1,13 +1,13 @@
import { Parser } from '../index'; import { Parser } from '../index';
type Identifier = { interface Identifier {
start: number; start: number;
end: number; end: number;
type: 'Identifier'; type: 'Identifier';
name: string; name: string;
}; }
type Property = { interface Property {
start: number; start: number;
end: number; end: number;
type: 'Property'; type: 'Property';
@ -15,9 +15,9 @@ type Property = {
shorthand: boolean; shorthand: boolean;
key: Identifier; key: Identifier;
value: Context; value: Context;
}; }
type Context = { interface Context {
start: number; start: number;
end: number; end: number;
type: 'Identifier' | 'ArrayPattern' | 'ObjectPattern' | 'RestIdentifier'; type: 'Identifier' | 'ArrayPattern' | 'ObjectPattern' | 'RestIdentifier';
@ -91,7 +91,7 @@ export default function read_context(parser: Parser) {
end: parser.index, end: parser.index,
type: 'Identifier', type: 'Identifier',
name name
} };
const property: Property = { const property: Property = {
start, start,
end: parser.index, end: parser.index,
@ -100,7 +100,7 @@ export default function read_context(parser: Parser) {
shorthand: true, shorthand: true,
key, key,
value: key value: key
} };
context.properties.push(property); context.properties.push(property);

@ -12,6 +12,7 @@ export default function read_expression(parser: Parser): Node {
const end = start + name.length; const end = start + name.length;
if (literals.has(name)) { if (literals.has(name)) {
// eslint-disable-next-line @typescript-eslint/no-object-literal-type-assertion
return { return {
type: 'Literal', type: 'Literal',
start, start,
@ -21,6 +22,7 @@ export default function read_expression(parser: Parser): Node {
} as SimpleLiteral; } as SimpleLiteral;
} }
// eslint-disable-next-line @typescript-eslint/no-object-literal-type-assertion
return { return {
type: 'Identifier', type: 'Identifier',
start, start,

@ -3,6 +3,16 @@ import { walk } from 'estree-walker';
import { Parser } from '../index'; import { Parser } from '../index';
import { Node } from '../../interfaces'; import { Node } from '../../interfaces';
function is_ref_selector(a: Node, b: Node) {
if (!b) return false;
return (
a.type === 'TypeSelector' &&
a.name === 'ref' &&
b.type === 'PseudoClassSelector'
);
}
export default function read_style(parser: Parser, start: number, attributes: Node[]) { export default function read_style(parser: Parser, start: number, attributes: Node[]) {
const content_start = parser.index; const content_start = parser.index;
const styles = parser.read_until(/<\/style>/); const styles = parser.read_until(/<\/style>/);
@ -69,13 +79,3 @@ export default function read_style(parser: Parser, start: number, attributes: No
}, },
}; };
} }
function is_ref_selector(a: Node, b: Node) {
if (!b) return false;
return (
a.type === 'TypeSelector' &&
a.name === 'ref' &&
b.type === 'PseudoClassSelector'
);
}

@ -295,7 +295,7 @@ export default function mustache(parser: Parser) {
} }
} }
let await_block_shorthand = type === 'AwaitBlock' && parser.eat('then'); const await_block_shorthand = type === 'AwaitBlock' && parser.eat('then');
if (await_block_shorthand) { if (await_block_shorthand) {
parser.require_whitespace(); parser.require_whitespace();
block.value = parser.read_identifier(); block.value = parser.read_identifier();

@ -8,6 +8,7 @@ import { Directive, DirectiveType, Node, Text } from '../../interfaces';
import fuzzymatch from '../../utils/fuzzymatch'; import fuzzymatch from '../../utils/fuzzymatch';
import list from '../../utils/list'; import list from '../../utils/list';
// eslint-disable-next-line no-useless-escape
const valid_tag_name = /^\!?[a-zA-Z]{1,}:?[a-zA-Z0-9\-]*/; const valid_tag_name = /^\!?[a-zA-Z]{1,}:?[a-zA-Z0-9\-]*/;
const meta_tags = new Map([ const meta_tags = new Map([
@ -36,7 +37,9 @@ const specials = new Map([
], ],
]); ]);
// eslint-disable-next-line no-useless-escape
const SELF = /^svelte:self(?=[\s\/>])/; const SELF = /^svelte:self(?=[\s\/>])/;
// eslint-disable-next-line no-useless-escape
const COMPONENT = /^svelte:component(?=[\s\/>])/; const COMPONENT = /^svelte:component(?=[\s\/>])/;
// based on http://developers.whatwg.org/syntax.html#syntax-tag-omission // based on http://developers.whatwg.org/syntax.html#syntax-tag-omission
@ -74,190 +77,6 @@ function parent_is_head(stack) {
return false; return false;
} }
export default function tag(parser: Parser) {
const start = parser.index++;
let parent = parser.current();
if (parser.eat('!--')) {
const data = parser.read_until(/-->/);
parser.eat('-->', true, 'comment was left open, expected -->');
parser.current().children.push({
start,
end: parser.index,
type: 'Comment',
data,
});
return;
}
const is_closing_tag = parser.eat('/');
const name = read_tag_name(parser);
if (meta_tags.has(name)) {
const slug = meta_tags.get(name).toLowerCase();
if (is_closing_tag) {
if (
(name === 'svelte:window' || name === 'svelte:body') &&
parser.current().children.length
) {
parser.error({
code: `invalid-${name.slice(7)}-content`,
message: `<${name}> cannot have children`
}, parser.current().children[0].start);
}
} else {
if (name in parser.meta_tags) {
parser.error({
code: `duplicate-${slug}`,
message: `A component can only have one <${name}> tag`
}, start);
}
if (parser.stack.length > 1) {
parser.error({
code: `invalid-${slug}-placement`,
message: `<${name}> tags cannot be inside elements or blocks`
}, start);
}
parser.meta_tags[name] = true;
}
}
const type = meta_tags.has(name)
? meta_tags.get(name)
: (/[A-Z]/.test(name[0]) || name === 'svelte:self' || name === 'svelte:component') ? 'InlineComponent'
: name === 'title' && parent_is_head(parser.stack) ? 'Title'
: name === 'slot' && !parser.customElement ? 'Slot' : 'Element';
const element: Node = {
start,
end: null, // filled in later
type,
name,
attributes: [],
children: [],
};
parser.allow_whitespace();
if (is_closing_tag) {
if (is_void(name)) {
parser.error({
code: `invalid-void-content`,
message: `<${name}> is a void element and cannot have children, or a closing tag`
}, start);
}
parser.eat('>', true);
// close any elements that don't have their own closing tags, e.g. <div><p></div>
while (parent.name !== name) {
if (parent.type !== 'Element')
parser.error({
code: `invalid-closing-tag`,
message: `</${name}> attempted to close an element that was not open`
}, start);
parent.end = start;
parser.stack.pop();
parent = parser.current();
}
parent.end = parser.index;
parser.stack.pop();
return;
} else if (disallowed_contents.has(parent.name)) {
// can this be a child of the parent element, or does it implicitly
// close it, like `<li>one<li>two`?
if (disallowed_contents.get(parent.name).has(name)) {
parent.end = start;
parser.stack.pop();
}
}
const unique_names = new Set();
let attribute;
while ((attribute = read_attribute(parser, unique_names))) {
element.attributes.push(attribute);
parser.allow_whitespace();
}
if (name === 'svelte:component') {
const index = element.attributes.findIndex(attr => attr.type === 'Attribute' && attr.name === 'this');
if (!~index) {
parser.error({
code: `missing-component-definition`,
message: `<svelte:component> must have a 'this' attribute`
}, start);
}
const definition = element.attributes.splice(index, 1)[0];
if (definition.value === true || definition.value.length !== 1 || definition.value[0].type === 'Text') {
parser.error({
code: `invalid-component-definition`,
message: `invalid component definition`
}, definition.start);
}
element.expression = definition.value[0].expression;
}
// special cases top-level <script> and <style>
if (specials.has(name) && parser.stack.length === 1) {
const special = specials.get(name);
parser.eat('>', true);
const content = special.read(parser, start, element.attributes);
if (content) parser[special.property].push(content);
return;
}
parser.current().children.push(element);
const self_closing = parser.eat('/') || is_void(name);
parser.eat('>', true);
if (self_closing) {
// don't push self-closing elements onto the stack
element.end = parser.index;
} else if (name === 'textarea') {
// special case
element.children = read_sequence(
parser,
() =>
parser.template.slice(parser.index, parser.index + 11) === '</textarea>'
);
parser.read(/<\/textarea>/);
element.end = parser.index;
} else if (name === 'script') {
// special case
const start = parser.index;
const data = parser.read_until(/<\/script>/);
const end = parser.index;
element.children.push({ start, end, type: 'Text', data });
parser.eat('</script>', true);
element.end = parser.index;
} else if (name === 'style') {
// special case
const start = parser.index;
const data = parser.read_until(/<\/style>/);
const end = parser.index;
element.children.push({ start, end, type: 'Text', data });
parser.eat('</style>', true);
} else {
parser.stack.push(element);
}
}
function read_tag_name(parser: Parser) { function read_tag_name(parser: Parser) {
const start = parser.index; const start = parser.index;
@ -313,6 +132,90 @@ function read_tag_name(parser: Parser) {
return name; return name;
} }
function get_directive_type(name: string): DirectiveType {
if (name === 'use') return 'Action';
if (name === 'animate') return 'Animation';
if (name === 'bind') return 'Binding';
if (name === 'class') return 'Class';
if (name === 'on') return 'EventHandler';
if (name === 'let') return 'Let';
if (name === 'ref') return 'Ref';
if (name === 'in' || name === 'out' || name === 'transition') return 'Transition';
}
function read_sequence(parser: Parser, done: () => boolean): Node[] {
let current_chunk: Text = {
start: parser.index,
end: null,
type: 'Text',
raw: '',
data: null
};
const chunks: Node[] = [];
function flush() {
if (current_chunk.raw) {
current_chunk.data = decode_character_references(current_chunk.raw);
current_chunk.end = parser.index;
chunks.push(current_chunk);
}
}
while (parser.index < parser.template.length) {
const index = parser.index;
if (done()) {
flush();
return chunks;
} else if (parser.eat('{')) {
flush();
parser.allow_whitespace();
const expression = read_expression(parser);
parser.allow_whitespace();
parser.eat('}', true);
chunks.push({
start: index,
end: parser.index,
type: 'MustacheTag',
expression,
});
current_chunk = {
start: parser.index,
end: null,
type: 'Text',
raw: '',
data: null
};
} else {
current_chunk.raw += parser.template[parser.index++];
}
}
parser.error({
code: `unexpected-eof`,
message: `Unexpected end of input`
});
}
function read_attribute_value(parser: Parser) {
const quote_mark = parser.eat(`'`) ? `'` : parser.eat(`"`) ? `"` : null;
const regex = (
quote_mark === `'` ? /'/ :
quote_mark === `"` ? /"/ :
/(\/>|[\s"'=<>`])/
);
const value = read_sequence(parser, () => !!parser.match_regex(regex));
if (quote_mark) parser.index += 1;
return value;
}
function read_attribute(parser: Parser, unique_names: Set<string>) { function read_attribute(parser: Parser, unique_names: Set<string>) {
const start = parser.index; const start = parser.index;
@ -358,7 +261,8 @@ function read_attribute(parser: Parser, unique_names: Set<string>) {
} }
} }
let name = parser.read_until(/[\s=\/>"']/); // eslint-disable-next-line no-useless-escape
const name = parser.read_until(/[\s=\/>"']/);
if (!name) return null; if (!name) return null;
let end = parser.index; let end = parser.index;
@ -396,12 +300,12 @@ function read_attribute(parser: Parser, unique_names: Set<string>) {
if (type === 'Ref') { if (type === 'Ref') {
parser.error({ parser.error({
code: `invalid-ref-directive`, code: `invalid-ref-directive`,
message: `The ref directive is no longer supported use \`bind:this={${directive_name}}\` instead` message: `The ref directive is no longer supported — use \`bind:this={${directive_name}}\` instead`
}, start); }, start);
} }
if (value[0]) { if (value[0]) {
if ((value as Array<any>).length > 1 || value[0].type === 'Text') { if ((value as any[]).length > 1 || value[0].type === 'Text') {
parser.error({ parser.error({
code: `invalid-directive-value`, code: `invalid-directive-value`,
message: `Directive value must be a JavaScript expression enclosed in curly braces` message: `Directive value must be a JavaScript expression enclosed in curly braces`
@ -445,86 +349,186 @@ function read_attribute(parser: Parser, unique_names: Set<string>) {
}; };
} }
function get_directive_type(name: string):DirectiveType { export default function tag(parser: Parser) {
if (name === 'use') return 'Action'; const start = parser.index++;
if (name === 'animate') return 'Animation';
if (name === 'bind') return 'Binding';
if (name === 'class') return 'Class';
if (name === 'on') return 'EventHandler';
if (name === 'let') return 'Let';
if (name === 'ref') return 'Ref';
if (name === 'in' || name === 'out' || name === 'transition') return 'Transition';
}
function read_attribute_value(parser: Parser) { let parent = parser.current();
const quote_mark = parser.eat(`'`) ? `'` : parser.eat(`"`) ? `"` : null;
const regex = ( if (parser.eat('!--')) {
quote_mark === `'` ? /'/ : const data = parser.read_until(/-->/);
quote_mark === `"` ? /"/ : parser.eat('-->', true, 'comment was left open, expected -->');
/(\/>|[\s"'=<>`])/
);
const value = read_sequence(parser, () => !!parser.match_regex(regex)); parser.current().children.push({
start,
end: parser.index,
type: 'Comment',
data,
});
if (quote_mark) parser.index += 1; return;
return value; }
}
function read_sequence(parser: Parser, done: () => boolean): Node[] { const is_closing_tag = parser.eat('/');
let current_chunk: Text = {
start: parser.index,
end: null,
type: 'Text',
raw: '',
data: null
};
function flush() { const name = read_tag_name(parser);
if (current_chunk.raw) {
current_chunk.data = decode_character_references(current_chunk.raw); if (meta_tags.has(name)) {
current_chunk.end = parser.index; const slug = meta_tags.get(name).toLowerCase();
chunks.push(current_chunk); if (is_closing_tag) {
if (
(name === 'svelte:window' || name === 'svelte:body') &&
parser.current().children.length
) {
parser.error({
code: `invalid-${name.slice(7)}-content`,
message: `<${name}> cannot have children`
}, parser.current().children[0].start);
}
} else {
if (name in parser.meta_tags) {
parser.error({
code: `duplicate-${slug}`,
message: `A component can only have one <${name}> tag`
}, start);
}
if (parser.stack.length > 1) {
parser.error({
code: `invalid-${slug}-placement`,
message: `<${name}> tags cannot be inside elements or blocks`
}, start);
}
parser.meta_tags[name] = true;
} }
} }
const chunks: Node[] = []; const type = meta_tags.has(name)
? meta_tags.get(name)
: (/[A-Z]/.test(name[0]) || name === 'svelte:self' || name === 'svelte:component') ? 'InlineComponent'
: name === 'title' && parent_is_head(parser.stack) ? 'Title'
: name === 'slot' && !parser.customElement ? 'Slot' : 'Element';
while (parser.index < parser.template.length) { const element: Node = {
const index = parser.index; start,
end: null, // filled in later
type,
name,
attributes: [],
children: [],
};
if (done()) { parser.allow_whitespace();
flush();
return chunks;
} else if (parser.eat('{')) {
flush();
parser.allow_whitespace(); if (is_closing_tag) {
const expression = read_expression(parser); if (is_void(name)) {
parser.allow_whitespace(); parser.error({
parser.eat('}', true); code: `invalid-void-content`,
message: `<${name}> is a void element and cannot have children, or a closing tag`
}, start);
}
chunks.push({ parser.eat('>', true);
start: index,
end: parser.index,
type: 'MustacheTag',
expression,
});
current_chunk = { // close any elements that don't have their own closing tags, e.g. <div><p></div>
start: parser.index, while (parent.name !== name) {
end: null, if (parent.type !== 'Element')
type: 'Text', parser.error({
raw: '', code: `invalid-closing-tag`,
data: null message: `</${name}> attempted to close an element that was not open`
}; }, start);
} else {
current_chunk.raw += parser.template[parser.index++]; parent.end = start;
parser.stack.pop();
parent = parser.current();
}
parent.end = parser.index;
parser.stack.pop();
return;
} else if (disallowed_contents.has(parent.name)) {
// can this be a child of the parent element, or does it implicitly
// close it, like `<li>one<li>two`?
if (disallowed_contents.get(parent.name).has(name)) {
parent.end = start;
parser.stack.pop();
} }
} }
parser.error({ const unique_names = new Set();
code: `unexpected-eof`,
message: `Unexpected end of input` let attribute;
}); while ((attribute = read_attribute(parser, unique_names))) {
element.attributes.push(attribute);
parser.allow_whitespace();
}
if (name === 'svelte:component') {
const index = element.attributes.findIndex(attr => attr.type === 'Attribute' && attr.name === 'this');
if (!~index) {
parser.error({
code: `missing-component-definition`,
message: `<svelte:component> must have a 'this' attribute`
}, start);
}
const definition = element.attributes.splice(index, 1)[0];
if (definition.value === true || definition.value.length !== 1 || definition.value[0].type === 'Text') {
parser.error({
code: `invalid-component-definition`,
message: `invalid component definition`
}, definition.start);
}
element.expression = definition.value[0].expression;
}
// special cases – top-level <script> and <style>
if (specials.has(name) && parser.stack.length === 1) {
const special = specials.get(name);
parser.eat('>', true);
const content = special.read(parser, start, element.attributes);
if (content) parser[special.property].push(content);
return;
}
parser.current().children.push(element);
const self_closing = parser.eat('/') || is_void(name);
parser.eat('>', true);
if (self_closing) {
// don't push self-closing elements onto the stack
element.end = parser.index;
} else if (name === 'textarea') {
// special case
element.children = read_sequence(
parser,
() =>
parser.template.slice(parser.index, parser.index + 11) === '</textarea>'
);
parser.read(/<\/textarea>/);
element.end = parser.index;
} else if (name === 'script') {
// special case
const start = parser.index;
const data = parser.read_until(/<\/script>/);
const end = parser.index;
element.children.push({ start, end, type: 'Text', data });
parser.eat('</script>', true);
element.end = parser.index;
} else if (name === 'style') {
// special case
const start = parser.index;
const data = parser.read_until(/<\/style>/);
const end = parser.index;
element.children.push({ start, end, type: 'Text', data });
parser.eat('</style>', true);
} else {
parser.stack.push(element);
}
} }

@ -14,7 +14,7 @@ export default function text(parser: Parser) {
data += parser.template[parser.index++]; data += parser.template[parser.index++];
} }
let node = { const node = {
start, start,
end: parser.index, end: parser.index,
type: 'Text', type: 'Text',

@ -1,5 +1,6 @@
import entities from './entities'; import entities from './entities';
const NUL = 0;
const windows_1252 = [ const windows_1252 = [
8364, 8364,
129, 129,
@ -40,29 +41,6 @@ const entity_pattern = new RegExp(
'g' 'g'
); );
export function decode_character_references(html: string) {
return html.replace(entity_pattern, (match, entity) => {
let code;
// Handle named entities
if (entity[0] !== '#') {
code = entities[entity];
} else if (entity[1] === 'x') {
code = parseInt(entity.substring(2), 16);
} else {
code = parseInt(entity.substring(1), 10);
}
if (!code) {
return match;
}
return String.fromCodePoint(validate_code(code));
});
}
const NUL = 0;
// some code points are verboten. If we were inserting HTML, the browser would replace the illegal // some code points are verboten. If we were inserting HTML, the browser would replace the illegal
// code points with alternatives in some cases - since we're bypassing that mechanism, we need // code points with alternatives in some cases - since we're bypassing that mechanism, we need
// to replace them ourselves // to replace them ourselves
@ -80,7 +58,7 @@ function validate_code(code: number) {
} }
// code points 128-159 are dealt with leniently by browsers, but they're incorrect. We need // code points 128-159 are dealt with leniently by browsers, but they're incorrect. We need
// to correct the mistake or we'll end up with missing signs and so on // to correct the mistake or we'll end up with missing € signs and so on
if (code <= 159) { if (code <= 159) {
return windows_1252[code - 128]; return windows_1252[code - 128];
} }
@ -112,3 +90,24 @@ function validate_code(code: number) {
return NUL; return NUL;
} }
export function decode_character_references(html: string) {
return html.replace(entity_pattern, (match, entity) => {
let code;
// Handle named entities
if (entity[0] !== '#') {
code = entities[entity];
} else if (entity[1] === 'x') {
code = parseInt(entity.substring(2), 16);
} else {
code = parseInt(entity.substring(1), 10);
}
if (!code) {
return match;
}
return String.fromCodePoint(validate_code(code));
});
}

@ -2,18 +2,18 @@ import { SourceMap } from 'magic-string';
export interface PreprocessorGroup { export interface PreprocessorGroup {
markup?: (options: { markup?: (options: {
content: string, content: string;
filename: string filename: string;
}) => { code: string, map?: SourceMap | string, dependencies?: string[] }; }) => { code: string; map?: SourceMap | string; dependencies?: string[] };
style?: Preprocessor; style?: Preprocessor;
script?: Preprocessor; script?: Preprocessor;
} }
export type Preprocessor = (options: { export type Preprocessor = (options: {
content: string, content: string;
attributes: Record<string, string | boolean>, attributes: Record<string, string | boolean>;
filename?: string filename?: string;
}) => { code: string, map?: SourceMap | string, dependencies?: string[] }; }) => { code: string; map?: SourceMap | string; dependencies?: string[] };
interface Processed { interface Processed {
code: string; code: string;
@ -43,16 +43,16 @@ interface Replacement {
} }
async function replace_async(str: string, re: RegExp, func: (...any) => Promise<string>) { async function replace_async(str: string, re: RegExp, func: (...any) => Promise<string>) {
const replacements: Promise<Replacement>[] = []; const replacements: Array<Promise<Replacement>> = [];
str.replace(re, (...args) => { str.replace(re, (...args) => {
replacements.push( replacements.push(
func(...args).then( func(...args).then(
res => res => // eslint-disable-next-line @typescript-eslint/no-object-literal-type-assertion
<Replacement>({ ({
offset: args[args.length - 2], offset: args[args.length - 2],
length: args[0].length, length: args[0].length,
replacement: res, replacement: res,
}) }) as Replacement
) )
); );
return ''; return '';

@ -3,8 +3,8 @@ import get_code_frame from './get_code_frame';
class CompileError extends Error { class CompileError extends Error {
code: string; code: string;
start: { line: number, column: number }; start: { line: number; column: number };
end: { line: number, column: number }; end: { line: number; column: number };
pos: number; pos: number;
filename: string; filename: string;
frame: string; frame: string;
@ -15,12 +15,12 @@ class CompileError extends Error {
} }
export default function error(message: string, props: { export default function error(message: string, props: {
name: string, name: string;
code: string, code: string;
source: string, source: string;
filename: string, filename: string;
start: number, start: number;
end?: number end?: number;
}) { }) {
const error = new CompileError(message); const error = new CompileError(message);
error.name = props.name; error.name = props.name;

@ -2,9 +2,9 @@
// Reproduced under MIT License https://github.com/acornjs/acorn/blob/master/LICENSE // Reproduced under MIT License https://github.com/acornjs/acorn/blob/master/LICENSE
export default function full_char_code_at(str: string, i: number): number { export default function full_char_code_at(str: string, i: number): number {
let code = str.charCodeAt(i) const code = str.charCodeAt(i);
if (code <= 0xd7ff || code >= 0xe000) return code; if (code <= 0xd7ff || code >= 0xe000) return code;
let next = str.charCodeAt(i + 1); const next = str.charCodeAt(i + 1);
return (code << 10) + next - 0x35fdc00; return (code << 10) + next - 0x35fdc00;
} }

@ -1,32 +1,9 @@
export default function fuzzymatch(name: string, names: string[]) {
const set = new FuzzySet(names);
const matches = set.get(name);
return matches && matches[0] && matches[0][0] > 0.7 ? matches[0][1] : null;
}
// adapted from https://github.com/Glench/fuzzyset.js/blob/master/lib/fuzzyset.js // adapted from https://github.com/Glench/fuzzyset.js/blob/master/lib/fuzzyset.js
// BSD Licensed // BSD Licensed
const GRAM_SIZE_LOWER = 2; const GRAM_SIZE_LOWER = 2;
const GRAM_SIZE_UPPER = 3; const GRAM_SIZE_UPPER = 3;
// return an edit distance from 0 to 1
function _distance(str1: string, str2: string) {
if (str1 === null && str2 === null)
throw 'Trying to compare two null values';
if (str1 === null || str2 === null) return 0;
str1 = String(str1);
str2 = String(str2);
const distance = levenshtein(str1, str2);
if (str1.length > str2.length) {
return 1 - distance / str1.length;
} else {
return 1 - distance / str2.length;
}
}
// helper functions // helper functions
function levenshtein(str1: string, str2: string) { function levenshtein(str1: string, str2: string) {
const current: number[] = []; const current: number[] = [];
@ -53,6 +30,22 @@ function levenshtein(str1: string, str2: string) {
return current.pop(); return current.pop();
} }
// return an edit distance from 0 to 1
function _distance(str1: string, str2: string) {
if (str1 === null && str2 === null)
throw 'Trying to compare two null values';
if (str1 === null || str2 === null) return 0;
str1 = String(str1);
str2 = String(str2);
const distance = levenshtein(str1, str2);
if (str1.length > str2.length) {
return 1 - distance / str1.length;
} else {
return 1 - distance / str2.length;
}
}
const non_word_regex = /[^\w, ]+/; const non_word_regex = /[^\w, ]+/;
function iterate_grams(value: string, gram_size = 2) { function iterate_grams(value: string, gram_size = 2) {
@ -144,7 +137,7 @@ class FuzzySet {
items[index] = [vector_normal, normalized_value]; items[index] = [vector_normal, normalized_value];
this.items[gram_size] = items; this.items[gram_size] = items;
this.exact_set[normalized_value] = value; this.exact_set[normalized_value] = value;
}; }
get(value: string) { get(value: string) {
const normalized_value = value.toLowerCase(); const normalized_value = value.toLowerCase();
@ -232,5 +225,13 @@ class FuzzySet {
} }
return new_results; return new_results;
}; }
} }
export default function fuzzymatch(name: string, names: string[]) {
const set = new FuzzySet(names);
const matches = set.get(name);
return matches && matches[0] && matches[0][0] > 0.7 ? matches[0][1] : null;
}

@ -3,20 +3,20 @@ import { is_function } from 'svelte/internal';
// todo: same as Transition, should it be shared? // todo: same as Transition, should it be shared?
export interface AnimationConfig { export interface AnimationConfig {
delay?: number, delay?: number;
duration?: number, duration?: number;
easing?: (t: number) => number, easing?: (t: number) => number;
css?: (t: number, u: number) => string, css?: (t: number, u: number) => string;
tick?: (t: number, u: number) => void tick?: (t: number, u: number) => void;
} }
interface FlipParams { interface FlipParams {
delay: number; delay: number;
duration: number | ((len: number) => number); duration: number | ((len: number) => number);
easing: (t: number) => number, easing: (t: number) => number;
} }
export function flip(node: Element, animation: { from: DOMRect, to: DOMRect }, params: FlipParams): AnimationConfig { export function flip(node: Element, animation: { from: DOMRect; to: DOMRect }, params: FlipParams): AnimationConfig {
const style = getComputedStyle(node); const style = getComputedStyle(node);
const transform = style.transform === 'none' ? '' : style.transform; const transform = style.transform === 'none' ? '' : style.transform;

@ -3,6 +3,7 @@ import { current_component, set_current_component } from './lifecycle';
import { blank_object, is_function, run, run_all, noop } from './utils'; import { blank_object, is_function, run, run_all, noop } from './utils';
import { children } from './dom'; import { children } from './dom';
// eslint-disable-next-line @typescript-eslint/class-name-casing
interface T$$ { interface T$$ {
dirty: null; dirty: null;
ctx: null|any; ctx: null|any;
@ -16,7 +17,7 @@ interface T$$ {
before_render: any[]; before_render: any[];
context: Map<any, any>; context: Map<any, any>;
on_mount: any[]; on_mount: any[];
on_destroy: any[] on_destroy: any[];
} }
export function bind(component, name, callback) { export function bind(component, name, callback) {
@ -115,8 +116,10 @@ export function init(component, options, instance, create_fragment, not_equal, p
if (options.target) { if (options.target) {
if (options.hydrate) { if (options.hydrate) {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
$$.fragment!.l(children(options.target)); $$.fragment!.l(children(options.target));
} else { } else {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
$$.fragment!.c(); $$.fragment!.c();
} }

@ -7,7 +7,7 @@ import { AnimationConfig } from '../animate';
//todo: documentation says it is DOMRect, but in IE it would be ClientRect //todo: documentation says it is DOMRect, but in IE it would be ClientRect
type PositionRect = DOMRect|ClientRect; type PositionRect = DOMRect|ClientRect;
type AnimationFn = (node: Element, { from, to }: { from: PositionRect, to: PositionRect }, params: any) => AnimationConfig; type AnimationFn = (node: Element, { from, to }: { from: PositionRect; to: PositionRect }, params: any) => AnimationConfig;
export function create_animation(node: Element & ElementCSSInlineStyle, from: PositionRect, fn: AnimationFn, params) { export function create_animation(node: Element & ElementCSSInlineStyle, from: PositionRect, fn: AnimationFn, params) {
if (!from) return noop; if (!from) return noop;

@ -1,8 +1,8 @@
export function append(target:Node, node:Node) { export function append(target: Node, node: Node) {
target.appendChild(node); target.appendChild(node);
} }
export function insert(target: Node, node: Node, anchor?:Node) { export function insert(target: Node, node: Node, anchor?: Node) {
target.insertBefore(node, anchor || null); target.insertBefore(node, anchor || null);
} }
@ -16,13 +16,13 @@ export function detach_between(before: Node, after: Node) {
} }
} }
export function detach_before(after:Node) { export function detach_before(after: Node) {
while (after.previousSibling) { while (after.previousSibling) {
after.parentNode.removeChild(after.previousSibling); after.parentNode.removeChild(after.previousSibling);
} }
} }
export function detach_after(before:Node) { export function detach_after(before: Node) {
while (before.nextSibling) { while (before.nextSibling) {
before.parentNode.removeChild(before.nextSibling); before.parentNode.removeChild(before.nextSibling);
} }
@ -38,8 +38,8 @@ export function element<K extends keyof HTMLElementTagNameMap>(name: K) {
return document.createElement<K>(name); return document.createElement<K>(name);
} }
export function object_without_properties<T,K extends keyof T>(obj:T, exclude: K[]) { export function object_without_properties<T, K extends keyof T>(obj: T, exclude: K[]) {
const target = {} as Pick<T, Exclude<keyof T, K>>; const target: Pick<T, Exclude<keyof T, K>> = {};
for (const k in obj) { for (const k in obj) {
if ( if (
Object.prototype.hasOwnProperty.call(obj, k) Object.prototype.hasOwnProperty.call(obj, k)
@ -53,11 +53,11 @@ export function object_without_properties<T,K extends keyof T>(obj:T, exclude: K
return target; return target;
} }
export function svg_element<K extends keyof SVGElementTagNameMap>(name:K):SVGElement { export function svg_element<K extends keyof SVGElementTagNameMap>(name: K): SVGElement {
return document.createElementNS<K>('http://www.w3.org/2000/svg', name); return document.createElementNS<K>('http://www.w3.org/2000/svg', name);
} }
export function text(data:string) { export function text(data: string) {
return document.createTextNode(data); return document.createTextNode(data);
} }
@ -95,7 +95,7 @@ export function attr(node: Element, attribute: string, value?: string) {
else node.setAttribute(attribute, value); else node.setAttribute(attribute, value);
} }
export function set_attributes(node: Element & ElementCSSInlineStyle, attributes: { [x: string]: string; }) { export function set_attributes(node: Element & ElementCSSInlineStyle, attributes: { [x: string]: string }) {
for (const key in attributes) { for (const key in attributes) {
if (key === 'style') { if (key === 'style') {
node.style.cssText = attributes[key]; node.style.cssText = attributes[key];

@ -23,7 +23,7 @@ export function clear_loops() {
running = false; running = false;
} }
export function loop(fn: (number)=>void): Task { export function loop(fn: (number) => void): Task {
let task; let task;
if (!running) { if (!running) {

@ -10,28 +10,19 @@ const binding_callbacks = [];
const render_callbacks = []; const render_callbacks = [];
const flush_callbacks = []; const flush_callbacks = [];
export function schedule_update() {
if (!update_scheduled) {
update_scheduled = true;
resolved_promise.then(flush);
}
}
export function tick() {
schedule_update();
return resolved_promise;
}
export function add_binding_callback(fn) {
binding_callbacks.push(fn);
}
export function add_render_callback(fn) { export function add_render_callback(fn) {
render_callbacks.push(fn); render_callbacks.push(fn);
} }
export function add_flush_callback(fn) { function update($$) {
flush_callbacks.push(fn); if ($$.fragment) {
$$.update($$.dirty);
run_all($$.before_render);
$$.fragment.p($$.dirty, $$.ctx);
$$.dirty = null;
$$.after_render.forEach(add_render_callback);
}
} }
export function flush() { export function flush() {
@ -69,13 +60,22 @@ export function flush() {
update_scheduled = false; update_scheduled = false;
} }
function update($$) { export function schedule_update() {
if ($$.fragment) { if (!update_scheduled) {
$$.update($$.dirty); update_scheduled = true;
run_all($$.before_render); resolved_promise.then(flush);
$$.fragment.p($$.dirty, $$.ctx);
$$.dirty = null;
$$.after_render.forEach(add_render_callback);
} }
} }
export function tick() {
schedule_update();
return resolved_promise;
}
export function add_binding_callback(fn) {
binding_callbacks.push(fn);
}
export function add_flush_callback(fn) {
flush_callbacks.push(fn);
}

@ -44,6 +44,15 @@ export function create_rule(node: Element & ElementCSSInlineStyle, a: number, b:
return name; return name;
} }
export function clear_rules() {
raf(() => {
if (active) return;
let i = stylesheet.cssRules.length;
while (i--) stylesheet.deleteRule(i);
current_rules = {};
});
}
export function delete_rule(node: Element & ElementCSSInlineStyle, name?: string) { export function delete_rule(node: Element & ElementCSSInlineStyle, name?: string) {
node.style.animation = (node.style.animation || '') node.style.animation = (node.style.animation || '')
.split(', ') .split(', ')
@ -55,12 +64,3 @@ export function delete_rule(node: Element & ElementCSSInlineStyle, name?: string
if (name && !--active) clear_rules(); if (name && !--active) clear_rules();
} }
export function clear_rules() {
raf(() => {
if (active) return;
let i = stylesheet.cssRules.length;
while (i--) stylesheet.deleteRule(i);
current_rules = {};
});
}

@ -71,14 +71,14 @@ export function create_in_transition(node: Element & ElementCSSInlineStyle, fn:
if (task) task.abort(); if (task) task.abort();
running = true; running = true;
add_render_callback(() => dispatch(node, true, 'start')); add_render_callback(() => dispatch(node, true, 'start'));
task = loop(now => { task = loop(now => {
if (running) { if (running) {
if (now >= end_time) { if (now >= end_time) {
tick(1, 0); tick(1, 0);
dispatch(node, true, 'end'); dispatch(node, true, 'end');
cleanup(); cleanup();
return running = false; return running = false;
@ -146,14 +146,14 @@ export function create_out_transition(node: Element & ElementCSSInlineStyle, fn:
const start_time = now() + delay; const start_time = now() + delay;
const end_time = start_time + duration; const end_time = start_time + duration;
add_render_callback(() => dispatch(node, false, 'start')); add_render_callback(() => dispatch(node, false, 'start'));
loop(now => { loop(now => {
if (running) { if (running) {
if (now >= end_time) { if (now >= end_time) {
tick(0, 1); tick(0, 1);
dispatch(node, false, 'end'); dispatch(node, false, 'end');
if (!--group.remaining) { if (!--group.remaining) {
// this will result in `end()` being called, // this will result in `end()` being called,

@ -2,7 +2,7 @@ export function noop() {}
export const identity = x => x; export const identity = x => x;
export function assign<T, S>(tar:T, src:S): T & S { export function assign<T, S>(tar: T, src: S): T & S {
// @ts-ignore // @ts-ignore
for (const k in src) tar[k] = src[k]; for (const k in src) tar[k] = src[k];
return tar as T & S; return tar as T & S;
@ -56,6 +56,12 @@ export function subscribe(component, store, callback) {
: unsub); : unsub);
} }
export function get_slot_context(definition, ctx, fn) {
return definition[1]
? assign({}, assign(ctx.$$scope.ctx, definition[1](fn ? fn(ctx) : {})))
: ctx.$$scope.ctx;
}
export function create_slot(definition, ctx, fn) { export function create_slot(definition, ctx, fn) {
if (definition) { if (definition) {
const slot_ctx = get_slot_context(definition, ctx, fn); const slot_ctx = get_slot_context(definition, ctx, fn);
@ -63,12 +69,6 @@ export function create_slot(definition, ctx, fn) {
} }
} }
export function get_slot_context(definition, ctx, fn) {
return definition[1]
? assign({}, assign(ctx.$$scope.ctx, definition[1](fn ? fn(ctx) : {})))
: ctx.$$scope.ctx;
}
export function get_slot_changes(definition, ctx, changed, fn) { export function get_slot_changes(definition, ctx, changed, fn) {
return definition[1] return definition[1]
? assign({}, assign(ctx.$$scope.changed || {}, definition[1](fn ? fn(changed) : {}))) ? assign({}, assign(ctx.$$scope.changed || {}, definition[1](fn ? fn(changed) : {})))

@ -6,10 +6,10 @@ interface TickContext<T> {
inv_mass: number; inv_mass: number;
dt: number; dt: number;
opts: Spring<T>; opts: Spring<T>;
settled: boolean settled: boolean;
} }
function tick_spring<T>(ctx: TickContext<T>, last_value: T, current_value: T, target_value: T):T { function tick_spring<T>(ctx: TickContext<T>, last_value: T, current_value: T, target_value: T): T {
if (typeof current_value === 'number' || is_date(current_value)) { if (typeof current_value === 'number' || is_date(current_value)) {
// @ts-ignore // @ts-ignore
const delta = target_value - current_value; const delta = target_value - current_value;
@ -45,9 +45,9 @@ function tick_spring<T>(ctx: TickContext<T>, last_value: T, current_value: T, ta
} }
interface SpringOpts { interface SpringOpts {
stiffness?: number, stiffness?: number;
damping?: number, damping?: number;
precision?: number, precision?: number;
} }
interface SpringUpdateOpts { interface SpringUpdateOpts {
@ -62,7 +62,7 @@ interface Spring<T> extends Readable<T>{
update: (fn: Updater<T>, opts?: SpringUpdateOpts) => Promise<void>; update: (fn: Updater<T>, opts?: SpringUpdateOpts) => Promise<void>;
precision: number; precision: number;
damping: number; damping: number;
stiffness: number stiffness: number;
} }
export function spring<T=any>(value: T, opts: SpringOpts = {}): Spring<T> { export function spring<T=any>(value: T, opts: SpringOpts = {}): Spring<T> {
@ -72,13 +72,14 @@ export function spring<T=any>(value: T, opts: SpringOpts = {}): Spring<T> {
let last_time: number; let last_time: number;
let task: Task; let task: Task;
let current_token: object; let current_token: object;
let last_value:T = value; let last_value: T = value;
let target_value:T = value; let target_value: T = value;
let inv_mass = 1; let inv_mass = 1;
let inv_mass_recovery_rate = 0; let inv_mass_recovery_rate = 0;
let cancel_task = false; let cancel_task = false;
/* eslint-disable @typescript-eslint/no-use-before-define */
function set(new_value: T, opts: SpringUpdateOpts={}): Promise<void> { function set(new_value: T, opts: SpringUpdateOpts={}): Promise<void> {
target_value = new_value; target_value = new_value;
const token = current_token = {}; const token = current_token = {};
@ -133,15 +134,16 @@ export function spring<T=any>(value: T, opts: SpringOpts = {}): Spring<T> {
}); });
}); });
} }
/* eslint-enable @typescript-eslint/no-use-before-define */
const spring = { const spring: Spring<T> = {
set, set,
update: (fn, opts:SpringUpdateOpts) => set(fn(target_value, value), opts), update: (fn, opts: SpringUpdateOpts) => set(fn(target_value, value), opts),
subscribe: store.subscribe, subscribe: store.subscribe,
stiffness, stiffness,
damping, damping,
precision precision
} as Spring<T>; };
return spring; return spring;
} }

@ -56,9 +56,9 @@ function get_interpolator(a, b) {
interface Options<T> { interface Options<T> {
delay?: number; delay?: number;
duration?: number | ((from: T, to: T) => number) duration?: number | ((from: T, to: T) => number);
easing?: (t: number) => number; easing?: (t: number) => number;
interpolate?: (a: T, b: T) => (t: number) => T interpolate?: (a: T, b: T) => (t: number) => T;
} }
type Updater<T> = (target_value: T, value: T) => T; type Updater<T> = (target_value: T, value: T) => T;
@ -69,7 +69,7 @@ interface Tweened<T> extends Readable<T> {
update(updater: Updater<T>, opts: Options<T>): Promise<void>; update(updater: Updater<T>, opts: Options<T>): Promise<void>;
} }
export function tweened<T>(value: T, defaults: Options<T> = {}):Tweened<T> { export function tweened<T>(value: T, defaults: Options<T> = {}): Tweened<T> {
const store = writable(value); const store = writable(value);
let task: Task; let task: Task;
@ -122,7 +122,7 @@ export function tweened<T>(value: T, defaults: Options<T> = {}):Tweened<T> {
return { return {
set, set,
update: (fn, opts:Options<T>) => set(fn(target_value, value), opts), update: (fn, opts: Options<T>) => set(fn(target_value, value), opts),
subscribe: store.subscribe subscribe: store.subscribe
}; };
} }

@ -43,17 +43,6 @@ export interface Writable<T> extends Readable<T> {
/** Pair of subscriber and invalidator. */ /** Pair of subscriber and invalidator. */
type SubscribeInvalidateTuple<T> = [Subscriber<T>, Invalidater<T>]; type SubscribeInvalidateTuple<T> = [Subscriber<T>, Invalidater<T>];
/**
* Creates a `Readable` store that allows reading by subscription.
* @param value initial value
* @param {StartStopNotifier}start start and stop notifications for subscriptions
*/
export function readable<T>(value: T, start: StartStopNotifier<T>): Readable<T> {
return {
subscribe: writable(value, start).subscribe,
};
}
/** /**
* Create a `Writable` store that allows both updating and reading by subscription. * Create a `Writable` store that allows both updating and reading by subscription.
* @param {*=}value initial value * @param {*=}value initial value
@ -100,6 +89,17 @@ export function writable<T>(value: T, start: StartStopNotifier<T> = noop): Writa
return { set, update, subscribe }; return { set, update, subscribe };
} }
/**
* Creates a `Readable` store that allows reading by subscription.
* @param value initial value
* @param {StartStopNotifier}start start and stop notifications for subscriptions
*/
export function readable<T>(value: T, start: StartStopNotifier<T>): Readable<T> {
return {
subscribe: writable(value, start).subscribe,
};
}
/** One or more `Readable`s. */ /** One or more `Readable`s. */
type Stores = Readable<any> | [Readable<any>, ...Array<Readable<any>>]; type Stores = Readable<any> | [Readable<any>, ...Array<Readable<any>>];

@ -2,11 +2,11 @@ import { cubicOut, cubicInOut } from 'svelte/easing';
import { assign, is_function } from 'svelte/internal'; import { assign, is_function } from 'svelte/internal';
export interface TransitionConfig { export interface TransitionConfig {
delay?: number, delay?: number;
duration?: number, duration?: number;
easing?: (t: number) => number, easing?: (t: number) => number;
css?: (t: number, u: number) => string, css?: (t: number, u: number) => string;
tick?: (t: number, u: number) => void tick?: (t: number, u: number) => void;
} }
interface FadeParams { interface FadeParams {
@ -30,7 +30,7 @@ export function fade(node: Element, {
interface FlyParams { interface FlyParams {
delay: number; delay: number;
duration: number; duration: number;
easing: (t: number)=>number, easing: (t: number) => number;
x: number; x: number;
y: number; y: number;
opacity: number; opacity: number;
@ -63,7 +63,7 @@ export function fly(node: Element, {
interface SlideParams { interface SlideParams {
delay: number; delay: number;
duration: number; duration: number;
easing: (t: number)=>number, easing: (t: number) => number;
} }
export function slide(node: Element, { export function slide(node: Element, {
@ -101,7 +101,7 @@ export function slide(node: Element, {
interface ScaleParams { interface ScaleParams {
delay: number; delay: number;
duration: number; duration: number;
easing: (t: number)=>number, easing: (t: number) => number;
start: number; start: number;
opacity: number; opacity: number;
} }
@ -135,7 +135,7 @@ interface DrawParams {
delay: number; delay: number;
speed: number; speed: number;
duration: number | ((len: number) => number); duration: number | ((len: number) => number);
easing: (t: number) => number, easing: (t: number) => number;
} }
export function draw(node: SVGElement & { getTotalLength(): number }, { export function draw(node: SVGElement & { getTotalLength(): number }, {
@ -167,18 +167,18 @@ export function draw(node: SVGElement & { getTotalLength(): number }, {
interface CrossfadeParams { interface CrossfadeParams {
delay: number; delay: number;
duration: number | ((len: number) => number); duration: number | ((len: number) => number);
easing: (t: number) => number, easing: (t: number) => number;
} }
type ClientRectMap = Map<any, { rect: ClientRect }>; type ClientRectMap = Map<any, { rect: ClientRect }>;
export function crossfade({ fallback, ...defaults }: CrossfadeParams & { export function crossfade({ fallback, ...defaults }: CrossfadeParams & {
fallback: (node: Element, params: CrossfadeParams, intro: boolean)=> TransitionConfig fallback: (node: Element, params: CrossfadeParams, intro: boolean) => TransitionConfig;
}) { }) {
const to_receive: ClientRectMap = new Map(); const to_receive: ClientRectMap = new Map();
const to_send: ClientRectMap = new Map(); const to_send: ClientRectMap = new Map();
function crossfade(from: ClientRect, node: Element, params: CrossfadeParams):TransitionConfig { function crossfade(from: ClientRect, node: Element, params: CrossfadeParams): TransitionConfig {
const { const {
delay = 0, delay = 0,
duration = d => Math.sqrt(d) * 30, duration = d => Math.sqrt(d) * 30,

@ -0,0 +1,6 @@
{
"rules": {
"@typescript-eslint/no-unused-vars": "off",
"no-console": "off"
}
}

@ -36,6 +36,14 @@ function create(code) {
return module.exports.default; return module.exports.default;
} }
function read(file) {
try {
return fs.readFileSync(file, 'utf-8');
} catch (err) {
return null;
}
}
describe('css', () => { describe('css', () => {
fs.readdirSync('test/css/samples').forEach(dir => { fs.readdirSync('test/css/samples').forEach(dir => {
if (dir[0] === '.') return; if (dir[0] === '.') return;
@ -128,11 +136,3 @@ describe('css', () => {
}); });
}); });
}); });
function read(file) {
try {
return fs.readFileSync(file, 'utf-8');
} catch (err) {
return null;
}
}

@ -187,6 +187,12 @@ export function showOutput(cwd, options = {}, compile = svelte.compile) {
}); });
} }
function getTrailingIndentation(str) {
let i = str.length;
while (str[i - 1] === ' ' || str[i - 1] === '\t') i -= 1;
return str.slice(i, str.length);
}
const start = /\n(\t+)/; const start = /\n(\t+)/;
export function deindent(strings, ...values) { export function deindent(strings, ...values) {
const indentation = start.exec(strings[0])[1]; const indentation = start.exec(strings[0])[1];
@ -222,12 +228,6 @@ export function deindent(strings, ...values) {
return result.trim().replace(/\t+$/gm, ''); return result.trim().replace(/\t+$/gm, '');
} }
function getTrailingIndentation(str) {
let i = str.length;
while (str[i - 1] === ' ' || str[i - 1] === '\t') i -= 1;
return str.slice(i, str.length);
}
export function spaces(i) { export function spaces(i) {
let result = ''; let result = '';
while (i--) result += ' '; while (i--) result += ' ';

@ -29,9 +29,9 @@ export default {
right: 100, right: 100,
top, top,
bottom: top + 20 bottom: top + 20
} };
}; };
}) });
component.things = [ component.things = [
{ id: 5, name: 'e' }, { id: 5, name: 'e' },

@ -29,9 +29,9 @@ export default {
right: 100, right: 100,
top, top,
bottom: top + 20 bottom: top + 20
} };
}; };
}) });
component.things = [ component.things = [
{ id: 5, name: 'e' }, { id: 5, name: 'e' },

@ -29,9 +29,9 @@ export default {
right: 100, right: 100,
top, top,
bottom: top + 20 bottom: top + 20
} };
}; };
}) });
component.things = [ component.things = [
{ id: 5, name: 'e' }, { id: 5, name: 'e' },

@ -29,7 +29,7 @@ export default {
right: 100, right: 100,
top, top,
bottom: top + 20 bottom: top + 20
} };
}; };
}); });

@ -1,6 +1,6 @@
let fulfil; let fulfil;
let thePromise = new Promise(f => { const thePromise = new Promise(f => {
fulfil = f; fulfil = f;
}); });

@ -1,6 +1,6 @@
let fulfil; let fulfil;
let thePromise = new Promise(f => { const thePromise = new Promise(f => {
fulfil = f; fulfil = f;
}); });

@ -1,6 +1,6 @@
let fulfil; let fulfil;
let thePromise = new Promise(f => { const thePromise = new Promise(f => {
fulfil = f; fulfil = f;
}); });

@ -1,6 +1,6 @@
export default { export default {
async test({ assert, component, target }) { async test({ assert, component, target }) {
let resolve, reject; let resolve; let reject;
let promise = new Promise(ok => resolve = ok); let promise = new Promise(ok => resolve = ok);
component.promise = promise; component.promise = promise;

@ -22,4 +22,4 @@ export default {
</div> </div>
`); `);
} }
} };

@ -2,4 +2,4 @@ export default {
html: ` html: `
<foo-bar>Hello</foo-bar> <foo-bar>Hello</foo-bar>
` `
} };

@ -1,3 +1,3 @@
export default { export default {
html: 'Compile plz' html: 'Compile plz'
} };

@ -1,7 +1,7 @@
export default { export default {
props: { props: {
greeting: 'Good day' greeting: 'Good day'
}, },
html: '<h1>Good day, world</h1>' html: '<h1>Good day, world</h1>'
} };

@ -10,4 +10,4 @@ export default {
const { foo } = component; const { foo } = component;
assert.equal(foo, undefined); assert.equal(foo, undefined);
} }
} };

@ -15,7 +15,7 @@ export default {
</div>`, </div>`,
test({ assert, component, target }) { test({ assert, component, target }) {
var nested = component.nested; const nested = component.nested;
assert.htmlEqual(target.innerHTML, ` assert.htmlEqual(target.innerHTML, `
<div> <div>
@ -24,6 +24,7 @@ export default {
</div> </div>
`); `);
// eslint-disable-next-line no-self-assign
nested.foo = nested.foo; nested.foo = nested.foo;
assert.htmlEqual(target.innerHTML, ` assert.htmlEqual(target.innerHTML, `
<div> <div>

@ -4,6 +4,7 @@ export default {
html: `<div><h3>Called 1 times.</h3></div>`, html: `<div><h3>Called 1 times.</h3></div>`,
test({ assert, component, target }) { test({ assert, component, target }) {
// eslint-disable-next-line no-self-assign
component.foo = component.foo; component.foo = component.foo;
assert.htmlEqual(target.innerHTML, `<div><h3>Called 1 times.</h3></div>`); assert.htmlEqual(target.innerHTML, `<div><h3>Called 1 times.</h3></div>`);
} }

@ -4,6 +4,7 @@ export default {
html: `<div><h3>Called 1 times.</h3></div>`, html: `<div><h3>Called 1 times.</h3></div>`,
test({ assert, component, target }) { test({ assert, component, target }) {
// eslint-disable-next-line no-self-assign
component.foo = component.foo; component.foo = component.foo;
assert.htmlEqual(target.innerHTML, `<div><h3>Called 2 times.</h3></div>`); assert.htmlEqual(target.innerHTML, `<div><h3>Called 2 times.</h3></div>`);
} }

@ -2,6 +2,7 @@ export default {
html: `<div><h3>Called 1 times.</h3></div>`, html: `<div><h3>Called 1 times.</h3></div>`,
test({ assert, component, target }) { test({ assert, component, target }) {
// eslint-disable-next-line no-self-assign
component.foo = component.foo; component.foo = component.foo;
assert.htmlEqual(target.innerHTML, `<div><h3>Called 1 times.</h3></div>`); assert.htmlEqual(target.innerHTML, `<div><h3>Called 1 times.</h3></div>`);
} }

@ -1,18 +1,18 @@
export default { export default {
html: ` html: `
<p>internal: 1</p> <p>internal: 1</p>
<button>click me</button> <button>click me</button>
`, `,
async test({ assert, target, window }) { async test({ assert, target, window }) {
const button = target.querySelector('button'); const button = target.querySelector('button');
const click = new window.MouseEvent('click'); const click = new window.MouseEvent('click');
await button.dispatchEvent(click); await button.dispatchEvent(click);
assert.htmlEqual(target.innerHTML, ` assert.htmlEqual(target.innerHTML, `
<p>internal: 1</p> <p>internal: 1</p>
<button>click me</button> <button>click me</button>
`); `);
} }
}; };

@ -1,9 +1,9 @@
export default { export default {
props: { props: {
a: 42 a: 42
}, },
html: ` html: `
42 42
` `
} };

@ -2,29 +2,29 @@ import { writable } from '../../../../store';
export default { export default {
props: { props: {
s1: writable(42), s1: writable(42),
s2: writable(43), s2: writable(43),
p1: 2, p1: 2,
p3: 3, p3: 3,
a1: writable(1), a1: writable(1),
a2: 4, a2: 4,
a6: writable(29), a6: writable(29),
for: 'loop', for: 'loop',
continue: '...', continue: '...',
}, },
html: ` html: `
$s1=42 $s1=42
$s2=43 $s2=43
p1=2 p1=2
p3=3 p3=3
$v1=1 $v1=1
v2=4 v2=4
vi1=4 vi1=4
$vs1=1 $vs1=1
vl0=hello vl0=hello
vl1=test vl1=test
$s3=29 $s3=29
loop... loop...
` `
} };

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

@ -1,7 +1,7 @@
let fulfil; let fulfil;
let reject; let reject;
let promise = new Promise((f, r) => { const promise = new Promise((f, r) => {
fulfil = f; fulfil = f;
reject = r; reject = r;
}); });
@ -14,7 +14,7 @@ export default {
intro: true, intro: true,
test({ assert, target, raf }) { test({ assert, target, raf }) {
let p = target.querySelector('p'); const p = target.querySelector('p');
assert.equal(p.className, 'pending'); assert.equal(p.className, 'pending');
assert.equal(p.foo, 0); assert.equal(p.foo, 0);
@ -26,7 +26,7 @@ export default {
return promise.then(() => { return promise.then(() => {
raf.tick(80); raf.tick(80);
let ps = document.querySelectorAll('p'); const ps = document.querySelectorAll('p');
assert.equal(ps[1].className, 'pending'); assert.equal(ps[1].className, 'pending');
assert.equal(ps[0].className, 'then'); assert.equal(ps[0].className, 'then');
assert.equal(ps[1].foo, 0.2); assert.equal(ps[1].foo, 0.2);

@ -1,7 +1,6 @@
import * as assert from "assert"; import * as assert from "assert";
import * as fs from "fs"; import * as fs from "fs";
import * as path from "path"; import * as path from "path";
import * as glob from 'tiny-glob/sync.js';
import { import {
showOutput, showOutput,

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save