first eslint run

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

@ -2,12 +2,12 @@
"root": true,
"rules": {
"indent": "off",
"no-unused-vars": "off",
"semi": [2, "always"],
"keyword-spacing": [2, { "before": true, "after": true }],
"space-before-blocks": [2, "always"],
"no-mixed-spaces-and-tabs": [2, "smart-tabs"],
"no-cond-assign": 0,
"no-unused-vars": 2,
"object-shorthand": [2, "always"],
"no-const-assign": 2,
"no-class-assign": 2,
@ -22,10 +22,17 @@
"arrow-spacing": 2,
"no-inner-declarations": 0,
"@typescript-eslint/indent": [2, "tab", { "SwitchCase": 1 }],
"@typescript-eslint/explicit-function-return-type": ["error", {
"allowExpressions": true
"@typescript-eslint/camelcase": "off",
"@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": {
"es6": true,
@ -45,6 +52,20 @@
"sourceType": "module"
},
"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();
type Timing = {
interface Timing {
label: string;
start: number;
end: number;

@ -24,13 +24,13 @@ import unwrap_parens from './utils/unwrap_parens';
import Slot from './nodes/Slot';
import { Node as ESTreeNode } from 'estree';
type ComponentOptions = {
interface ComponentOptions {
namespace?: string;
tag?: string;
immutable?: boolean;
accessors?: boolean;
preserveWhitespace?: boolean;
};
}
// We need to tell estree-walker that it should always
// 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;
}
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 {
stats: Stats;
warnings: Warning[];
@ -97,7 +211,7 @@ export default class Component {
node_for_declaration: Map<string, Node> = new Map();
partly_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();
has_reactive_assignments = false;
injected_reactive_declaration_vars: Set<string> = new Set();
@ -106,12 +220,12 @@ export default class Component {
indirect_dependencies: Map<string, Set<string>> = new Map();
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
locator: (search: number, startIndex?: number) => {
line: number,
column: number
line: number;
column: number;
};
stylesheet: Stylesheet;
@ -140,6 +254,7 @@ export default class Component {
this.compile_options = compile_options;
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
);
this.locate = getLocator(this.source);
@ -283,7 +398,7 @@ export default class Component {
this.source
);
const parts = module.split(']');
const parts = module.split('✂]');
const final_chunk = parts.pop();
const compiled = new Bundle({ separator: '' });
@ -296,7 +411,7 @@ export default class Component {
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
if (!parts.length) {
compiled.addSource({
@ -305,7 +420,7 @@ export default class Component {
});
}
const pattern = /\[(\d+)-(\d+)$/;
const pattern = /\[✂(\d+)-(\d+)$/;
parts.forEach((str: string) => {
const chunk = str.replace(pattern, '');
@ -398,12 +513,12 @@ export default class Component {
error(
pos: {
start: number,
end: number
start: number;
end: number;
},
e : {
code: string,
message: string
e: {
code: string;
message: string;
}
) {
error(e.message, {
@ -418,12 +533,12 @@ export default class Component {
warn(
pos: {
start: number,
end: number
start: number;
end: number;
},
warning: {
code: string,
message: string
code: string;
message: string;
}
) {
if (!this.locator) {
@ -527,9 +642,9 @@ export default class Component {
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 (a !== b) result += `[${a}-${b}]`;
if (a !== b) result += `[✂${a}-${b}✂]`;
a = node.end;
}
@ -541,7 +656,7 @@ export default class Component {
b = script.content.end;
while (/\s/.test(this.source[b - 1])) b -= 1;
if (a < b) result += `[${a}-${b}]`;
if (a < b) result += `[✂${a}-${b}✂]`;
return result || null;
}
@ -564,7 +679,7 @@ export default class Component {
this.add_sourcemap_locations(script.content);
let { scope, globals } = create_scopes(script.content);
const { scope, globals } = create_scopes(script.content);
this.module_scope = scope;
scope.declarations.forEach((node, name) => {
@ -588,7 +703,7 @@ export default class Component {
this.error(node, {
code: 'illegal-subscription',
message: `Cannot reference store value inside <script context="module">`
})
});
} else {
this.add_var({
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_map = map;
@ -705,7 +820,7 @@ export default class Component {
let scope = instance_scope;
walk(this.ast.instance.content, {
enter(node, parent) {
enter(node) {
if (map.has(node)) {
scope = map.get(node);
}
@ -738,7 +853,7 @@ export default class Component {
scope = scope.parent;
}
}
})
});
}
extract_reactive_store_references() {
@ -786,7 +901,7 @@ export default class Component {
}
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('$$')) {
@ -888,13 +1003,13 @@ export default class Component {
}
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) {
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
|| (current_group.insert && next_variable.subscribable)
|| (current_group.insert && next_variable.subscribable);
if (new_declaration) {
code.overwrite(declarator.end, next.start, ` ${node.kind} `);
@ -904,7 +1019,7 @@ export default class Component {
current_group = null;
if (variable.subscribable) {
let insert = get_insert(variable);
const insert = get_insert(variable);
if (next) {
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.type !== 'Literal') return false;
const v = this.var_lookup.get(d.id.name)
if (v.reassigned) return false
if (v.export_name) return false
const v = this.var_lookup.get(d.id.name);
if (v.reassigned) return false;
if (v.export_name) 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;
@ -992,7 +1107,7 @@ export default class Component {
});
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();
let walking = new Set();
const walking = new Set();
const is_hoistable = fn_declaration => {
if (fn_declaration.type === 'ExportNamedDeclaration') {
@ -1015,7 +1130,7 @@ export default class Component {
const instance_scope = this.instance_scope;
let scope = this.instance_scope;
let map = this.instance_scope_map;
const map = this.instance_scope_map;
let hoistable = true;
@ -1051,7 +1166,7 @@ export default class Component {
hoistable = false;
} else if (!is_hoistable(other_declaration)) {
hoistable = false;
}
}
}
else {
@ -1084,7 +1199,7 @@ export default class Component {
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();
let scope = this.instance_scope;
let map = this.instance_scope_map;
const map = this.instance_scope_map;
walk(node.body, {
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 { stringify_props } from './utils/stringify_props';
const wrappers = { esm, cjs };
type Export = {
interface Export {
name: 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) {
@ -44,7 +20,7 @@ function esm(
banner: string,
sveltePath: string,
internal_path: string,
helpers: { name: string, alias: string }[],
helpers: Array<{ name: string; alias: string }>,
imports: Node[],
module_exports: Export[],
source: string
@ -84,7 +60,7 @@ function cjs(
banner: string,
sveltePath: string,
internal_path: string,
helpers: { name: string, alias: string }[],
helpers: Array<{ name: string; alias: string }>,
imports: Node[],
module_exports: Export[]
) {
@ -115,7 +91,7 @@ function cjs(
const source = edit_source(node.source.value, sveltePath);
return `const ${lhs} = require("${source}");`
return `const ${lhs} = require("${source}");`;
});
const exports = [`exports.default = ${name};`].concat(
@ -131,5 +107,29 @@ function cjs(
${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 Component from '../Component';
export default class Selector {
node: Node;
stylesheet: Stylesheet;
blocks: Block[];
local_blocks: Block[];
used: boolean;
class Block {
global: boolean;
combinator: Node;
selectors: Node[]
start: number;
end: number;
should_encapsulate: boolean;
constructor(node: Node, stylesheet: Stylesheet) {
this.node = node;
this.stylesheet = stylesheet;
constructor(combinator: Node) {
this.combinator = combinator;
this.global = false;
this.selectors = [];
this.blocks = group_selectors(node);
this.start = null;
this.end = null;
// take trailing :global(...) selectors out of consideration
let i = this.blocks.length;
while (i > 0) {
if (!this.blocks[i - 1].global) break;
i -= 1;
this.should_encapsulate = false;
}
add(selector: Node) {
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.used = this.blocks[0].global;
this.selectors.push(selector);
this.end = selector.end;
}
}
apply(node: Node, stack: Node[]) {
const to_encapsulate: Node[] = [];
apply_selector(this.stylesheet, this.local_blocks.slice(), node, stack.slice(), to_encapsulate);
function group_selectors(selector: Node) {
let block: Block = new Block(null);
if (to_encapsulate.length > 0) {
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;
});
const blocks = [block];
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) {
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;
const operators = {
'=' : (value: string, flags: string) => new RegExp(`^${value}$`, flags),
'~=': (value: string, flags: string) => new RegExp(`\\b${value}\\b`, flags),
'|=': (value: string, flags: string) => new RegExp(`^${value}(-.+)?$`, flags),
'^=': (value: string, flags: string) => new RegExp(`^${value}`, flags),
'$=': (value: string, flags: string) => new RegExp(`${value}$`, flags),
'*=': (value: string, flags: string) => new RegExp(value, flags)
};
if (selector.type === 'TypeSelector' && selector.name === '*') {
code.overwrite(selector.start, selector.end, attr);
} else {
code.appendLeft(selector.end, attr);
}
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;
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) => {
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);
}
const pattern = operators[operator](expected_value, case_insensitive ? 'i' : '');
const value = attr.chunks[0];
if (block.should_encapsulate) encapsulate_block(block);
});
}
if (!value) return false;
if (value.type === 'Text') return pattern.test(value.data);
validate(component: Component) {
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 possible_values = new Set();
gather_possible_values(value.node, possible_values);
if (possible_values.has(UNKNOWN)) return true;
let start = 0;
let end = this.blocks.length;
for (const x of Array.from(possible_values)) { // TypeScript for-of is slightly unlike JS
if (pattern.test(x)) return true;
}
for (; start < end; start += 1) {
if (!this.blocks[start].global) break;
}
return false;
}
for (; end > start; end -= 1) {
if (!this.blocks[end - 1].global) break;
}
function class_matches(node, name: string) {
return node.classes.some((class_directive) => {
return class_directive.name === name;
});
}
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`
});
}
}
function unquote(value: Node) {
if (value.type === 'Identifier') return value.name;
const str = value.value;
if (str[0] === str[str.length - 1] && str[0] === "'" || str[0] === '"') {
return str.slice(1, str.length - 1);
}
return str;
}
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;
}
const operators = {
'=' : (value: string, flags: string) => new RegExp(`^${value}$`, flags),
'~=': (value: string, flags: string) => new RegExp(`\\b${value}\\b`, flags),
'|=': (value: string, flags: string) => new RegExp(`^${value}(-.+)?$`, flags),
'^=': (value: string, flags: string) => new RegExp(`^${value}`, flags),
'$=': (value: string, flags: string) => new RegExp(`${value}$`, flags),
'*=': (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;
export default class Selector {
node: Node;
stylesheet: Stylesheet;
blocks: Block[];
local_blocks: Block[];
used: boolean;
const pattern = operators[operator](expected_value, case_insensitive ? 'i' : '');
const value = attr.chunks[0];
constructor(node: Node, stylesheet: Stylesheet) {
this.node = node;
this.stylesheet = stylesheet;
if (!value) return false;
if (value.type === 'Text') return pattern.test(value.data);
this.blocks = group_selectors(node);
const possible_values = new Set();
gather_possible_values(value.node, possible_values);
if (possible_values.has(UNKNOWN)) return true;
// take trailing :global(...) selectors out of consideration
let i = this.blocks.length;
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
if (pattern.test(x)) return true;
this.local_blocks = this.blocks.slice(0, i);
this.used = this.blocks[0].global;
}
return false;
}
apply(node: Node, stack: Node[]) {
const to_encapsulate: Node[] = [];
function class_matches(node, name: string) {
return node.classes.some(function(class_directive) {
return class_directive.name === name;
});
}
apply_selector(this.stylesheet, this.local_blocks.slice(), node, stack.slice(), to_encapsulate);
function unquote(value: Node) {
if (value.type === 'Identifier') return value.name;
const str = value.value;
if (str[0] === str[str.length - 1] && str[0] === "'" || str[0] === '"') {
return str.slice(1, str.length - 1);
if (to_encapsulate.length > 0) {
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;
}
}
return str;
}
class Block {
global: boolean;
combinator: Node;
selectors: Node[]
start: number;
end: number;
should_encapsulate: boolean;
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 || ' ');
}
}
constructor(combinator: Node) {
this.combinator = combinator;
this.global = false;
this.selectors = [];
c = block.end;
});
}
this.start = null;
this.end = null;
transform(code: MagicString, attr: string) {
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) {
if (this.selectors.length === 0) {
this.start = selector.start;
this.global = selector.type === 'PseudoClassSelector' && selector.name === 'global';
break;
}
}
this.selectors.push(selector);
this.end = selector.end;
this.blocks.forEach((block) => {
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) {
let block: Block = new Block(null);
validate(component: Component) {
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) => {
if (child.type === 'WhiteSpace' || child.type === 'Combinator') {
block = new Block(child);
blocks.push(block);
} else {
block.add(child);
for (; start < end; start += 1) {
if (!this.blocks[start].global) break;
}
});
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);
}
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 {
selectors: Selector[];
declarations: Declaration[];
@ -43,11 +81,11 @@ class Rule {
return this.selectors.some(s => s.used);
}
minify(code: MagicString, dev: boolean) {
minify(code: MagicString, _dev: boolean) {
let c = this.node.start;
let started = false;
this.selectors.forEach((selector, i) => {
this.selectors.forEach((selector) => {
if (selector.used) {
const separator = started ? ',' : '';
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 {
node: Node;
children: (Atrule|Rule)[];
children: Array<Atrule|Rule>;
constructor(node: Node) {
this.node = node;
@ -163,7 +163,7 @@ class Atrule {
}
}
is_used(dev: boolean) {
is_used(_dev: boolean) {
return true; // TODO
}
@ -253,7 +253,7 @@ export default class Stylesheet {
has_styles: boolean;
id: string;
children: (Rule|Atrule)[] = [];
children: Array<Rule|Atrule> = [];
keyframes: Map<string, string> = new Map();
nodes_with_css_class: Set<Node> = new Set();
@ -269,7 +269,7 @@ export default class Stylesheet {
this.has_styles = true;
const stack: (Rule | Atrule)[] = [];
const stack: Array<Rule | Atrule> = [];
let current_atrule: Atrule = null;
walk(ast.css, {
@ -280,7 +280,7 @@ export default class Stylesheet {
const atrule = new Atrule(node);
stack.push(atrule);
// this is an awkward special case @apply (and
// this is an awkward special case — @apply (and
// possibly other future constructs)
if (last && !(last instanceof Atrule)) return;

@ -3,7 +3,7 @@ import Stats from '../Stats';
import parse from '../parse/index';
import render_dom from './render-dom/index';
import render_ssr from './render-ssr/index';
import { CompileOptions, Ast, Warning } from '../interfaces';
import { CompileOptions, Warning } from '../interfaces';
import Component from './Component';
import fuzzymatch from '../utils/fuzzymatch';
@ -57,6 +57,7 @@ function validate_options(options: CompileOptions, warnings: Warning[]) {
function get_name(filename: string) {
if (!filename) return null;
// eslint-disable-next-line no-useless-escape
const parts = filename.split(/[\/\\]/);
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 warnings = [];
let ast: Ast;
validate_options(options, warnings);
stats.start('parse');
ast = parse(source, options);
const ast = parse(source, options);
stats.stop('parse');
stats.start('create component');

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

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

@ -54,7 +54,7 @@ const a11y_required_content = new Set([
'h4',
'h5',
'h6'
])
]);
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;
}
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 {
type: 'Element';
name: string;
@ -134,7 +150,7 @@ export default class Element extends Node {
}
if (this.name === 'option') {
// Special case treat these the same way:
// Special case — treat these the same way:
// <option>{foo}</option>
// <option value={foo}>{foo}</option>
const value_attribute = info.attributes.find(attribute => attribute.name === 'value');
@ -180,10 +196,12 @@ export default class Element extends Node {
break;
case 'Transition':
{
const transition = new Transition(component, this, scope, node);
if (node.intro) this.intro = transition;
if (node.outro) this.outro = transition;
break;
}
case 'Animation':
this.animation = new Animation(component, this, scope, node);
@ -529,7 +547,7 @@ export default class Element extends Node {
if (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 });
}
} 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') {
(class_attribute.chunks[0] as Text).data += ` ${class_name}`;
} else {
(<Node[]>class_attribute.chunks).push(
(class_attribute.chunks as Node[]).push(
new Text(this.component, this, this.scope, {
type: 'Text',
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;
info.attributes.forEach(node => {
/* eslint-disable no-fallthrough */
switch (node.type) {
case 'Action':
component.error(node, {
@ -82,6 +83,7 @@ export default class InlineComponent extends Node {
default:
throw new Error(`Not implemented: ${node.type}`);
}
/* eslint-enable no-fallthrough */
});
if (this.lets.length > 0) {

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

@ -2,6 +2,12 @@ import Node from './shared/Node';
import Expression from './shared/Expression';
import Component from '../Component';
function describe(transition: Transition) {
return transition.directive === 'transition'
? `a 'transition'`
: `an '${transition.directive}'`;
}
export default class Transition extends Node {
type: 'Transition';
name: string;
@ -38,9 +44,3 @@ export default class Transition extends Node {
: 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)) {
const match = (
node.name === 'width' ? 'innerWidth' :
node.name === 'height' ? 'innerHeight' :
fuzzymatch(node.name, valid_bindings)
node.name === 'height' ? 'innerHeight' :
fuzzymatch(node.name, valid_bindings)
);
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
// https://www.typescriptlang.org/docs/handbook/advanced-types.html#discriminated-unions
export type INode = Action
| Animation
| Attribute
| AwaitBlock
| Binding
| Body
| CatchBlock
| Class
| Comment
| DebugTag
| EachBlock
| Element
| ElseBlock
| EventHandler
| Fragment
| Head
| IfBlock
| InlineComponent
| Let
| MustacheTag
| Options
| PendingBlock
| RawMustacheTag
| Slot
| Tag
| Text
| ThenBlock
| Title
| Transition
| Window;
| Animation
| Attribute
| AwaitBlock
| Binding
| Body
| CatchBlock
| Class
| Comment
| DebugTag
| EachBlock
| Element
| ElseBlock
| EventHandler
| Fragment
| Head
| IfBlock
| InlineComponent
| Let
| MustacheTag
| Options
| PendingBlock
| RawMustacheTag
| Slot
| Tag
| Text
| ThenBlock
| Title
| Transition
| Window;

@ -4,10 +4,10 @@ import is_reference from 'is-reference';
import flatten_reference from '../../utils/flatten_reference';
import { create_scopes, Scope, extract_names } from '../../utils/scope';
import { Node } from '../../../interfaces';
import { globals } from '../../../utils/names';
import { globals , sanitize } from '../../../utils/names';
import deindent from '../../utils/deindent';
import Wrapper from '../../render-dom/wrappers/shared/Wrapper';
import { sanitize } from '../../../utils/names';
import TemplateScope from './TemplateScope';
import get_object from '../../utils/get_object';
import { nodes_match } from '../../../utils/nodes_match';
@ -28,8 +28,8 @@ const binary_operators: Record<string, number> = {
'<=': 11,
'>': 11,
'>=': 11,
'in': 11,
'instanceof': 11,
in: 11,
instanceof: 11,
'==': 10,
'!=': 10,
'===': 10,
@ -64,6 +64,33 @@ const precedence: Record<string, (node?: Node) => number> = {
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 {
type: 'Expression' = 'Expression';
component: Component;
@ -347,7 +374,7 @@ export default class Expression {
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
}
@ -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 { Node } from '../../../interfaces';
export type Children = ReturnType<typeof map_children>;
function get_constructor(type) {
switch (type) {
case 'AwaitBlock': return AwaitBlock;
@ -53,3 +51,5 @@ export default function map_children(component, parent, scope, children: Node[])
return node;
});
}
export type Children = ReturnType<typeof map_children>;

@ -9,7 +9,7 @@ export interface BlockOptions {
renderer?: Renderer;
comment?: string;
key?: string;
bindings?: Map<string, { object: string, property: string, snippet: string }>;
bindings?: Map<string, { object: string; property: string; snippet: string }>;
dependencies?: Set<string>;
}
@ -26,7 +26,7 @@ export default class Block {
dependencies: Set<string>;
bindings: Map<string, { object: string, property: string, snippet: string }>;
bindings: Map<string, { object: string; property: string; snippet: string }>;
builders: {
init: CodeBuilder;
@ -372,8 +372,8 @@ export default class Block {
${properties}
};
`.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 = '') {
@ -387,7 +387,7 @@ export default class Block {
this.builders.destroy.add_line(
`#dispose${chunk}();`
)
);
} else {
this.builders.hydrate.add_block(deindent`
#dispose${chunk} = [

@ -8,7 +8,7 @@ export default class Renderer {
component: Component; // TODO Maybe Renderer shouldn't know about Component?
options: CompileOptions;
blocks: (Block | string)[] = [];
blocks: Array<Block | string> = [];
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
binding_groups: string[] = [];
@ -17,7 +17,7 @@ export default class Renderer {
fragment: FragmentWrapper;
file_var: string;
locate: (c: number) => { line: number; column: number; };
locate: (c: number) => { line: number; column: number };
constructor(component: Component, options: CompileOptions) {
this.component = component;

@ -79,8 +79,8 @@ export default function dom(
${$$props} => {
${uses_props && component.invalidate('$$props', `$$props = @assign(@assign({}, $$props), $$new_props)`)}
${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 &&
`if ('$$scope' in ${$$props}) ${component.invalidate('$$scope', `$$scope = ${$$props}.$$scope`)};`}
}
@ -152,12 +152,12 @@ export default function dom(
// instrument assignments
if (component.ast.instance) {
let scope = component.instance_scope;
let map = component.instance_scope_map;
const map = component.instance_scope_map;
let pending_assignments = new Set();
walk(component.ast.instance.content, {
enter: (node, parent) => {
enter: (node) => {
if (map.has(node)) {
scope = map.get(node);
}
@ -369,7 +369,7 @@ export default function dom(
})
.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 || uses_props) {
@ -390,7 +390,7 @@ export default function dom(
const store = component.var_lookup.get(name);
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;

@ -6,7 +6,7 @@ import Body from '../../nodes/Body';
export default class BodyWrapper extends Wrapper {
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 => {
const snippet = handler.render(block);

@ -13,13 +13,13 @@ export default class DebugTagWrapper extends Wrapper {
block: Block,
parent: Wrapper,
node: DebugTag,
strip_whitespace: boolean,
next_sibling: Wrapper
_strip_whitespace: boolean,
_next_sibling: Wrapper
) {
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 { component } = renderer;
@ -32,7 +32,7 @@ export default class DebugTagWrapper extends Wrapper {
code.overwrite(this.node.start + 1, this.node.start + 7, 'debugger', {
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.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', {
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();
this.node.expressions.forEach(expression => {

@ -312,8 +312,8 @@ export default class EachBlockWrapper extends Wrapper {
block.builders.init.add_block(deindent`
const ${get_key} = ctx => ${
// @ts-ignore todo: probably error
this.node.key.render()};
// @ts-ignore todo: probably error
this.node.key.render()};
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);
@ -425,7 +425,7 @@ export default class EachBlockWrapper extends Wrapper {
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) {
block.builders.init.add_block(deindent`
function ${outro_block}(i, detaching, local) {

@ -6,224 +6,6 @@ import { stringify } from '../../../utils/stringify';
import deindent from '../../../utils/deindent';
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
const attribute_lookup = {
accept: { applies_to: ['form', 'input'] },
@ -444,3 +226,221 @@ Object.keys(attribute_lookup).forEach(name => {
const metadata = attribute_lookup[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 ElementWrapper from '../Element';
import { dimensions } from '../../../../utils/patterns';
import get_object from '../../../utils/get_object';
import Block from '../../Block';
import Node from '../../../nodes/shared/Node';
@ -15,6 +14,152 @@ function get_tail(node: INode) {
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 {
node: Binding;
parent: ElementWrapper;
@ -23,8 +168,8 @@ export default class BindingWrapper {
handler: {
uses_context: boolean;
mutation: string;
contextual_dependencies: Set<string>,
snippet?: string
contextual_dependencies: Set<string>;
snippet?: string;
};
snippet: string;
is_readonly: boolean;
@ -87,7 +232,7 @@ export default class BindingWrapper {
}
is_readonly_media_attribute() {
return this.node.is_readonly_media_attribute()
return this.node.is_readonly_media_attribute();
}
render(block: Block, lock: string) {
@ -95,23 +240,23 @@ export default class BindingWrapper {
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];
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) {
update_conditions.push(
`(${dependency_array.map(prop => `changed.${prop}`).join(' || ')})`
)
);
}
if (parent.node.name === 'input') {
const type = parent.node.get_static_attribute_value('type');
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
switch (this.node.name) {
case 'group':
{
const binding_group = get_binding_group(parent.renderer, this.node.expression.node);
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);`
);
break;
}
case 'currentTime':
case 'playbackRate':
@ -139,6 +286,7 @@ export default class BindingWrapper {
break;
case 'paused':
{
// this is necessary to prevent audio restarting by itself
const last = block.get_unique_name(`${parent.var}_is_paused`);
block.add_variable(last, 'true');
@ -146,6 +294,7 @@ export default class BindingWrapper {
update_conditions.push(`${last} !== (${last} = ${this.snippet})`);
update_dom = `${parent.var}[${last} ? "pause" : "play"]();`;
break;
}
case 'value':
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 {
key: string;
value: (Text|Expression)[];
value: Array<Text|Expression>;
}
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});`
);
});
}
}
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)[] = [];
function get_style_value(chunks: Array<Text | Expression>) {
const value: Array<Text|Expression> = [];
let in_url = false;
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';
}
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 Wrapper from '../shared/Wrapper';
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 FragmentWrapper from '../Fragment';
import { stringify, escape_html, escape } from '../../../utils/stringify';
@ -24,19 +23,19 @@ import { get_context_merger } from '../shared/get_context_merger';
const events = [
{
event_names: ['input'],
filter: (node: Element, name: string) =>
filter: (node: Element, _name: string) =>
node.name === 'textarea' ||
node.name === 'input' && !/radio|checkbox|range/.test(node.get_static_attribute_value('type') as string)
},
{
event_names: ['change'],
filter: (node: Element, name: string) =>
filter: (node: Element, _name: string) =>
node.name === 'select' ||
node.name === 'input' && /radio|checkbox/.test(node.get_static_attribute_value('type') as string)
},
{
event_names: ['change', 'input'],
filter: (node: Element, name: string) =>
filter: (node: Element, _name: string) =>
node.name === 'input' && node.get_static_attribute_value('type') === 'range'
},
@ -93,7 +92,7 @@ const events = [
// details event
{
event_names: ['toggle'],
filter: (node: Element, name: string) =>
filter: (node: Element, _name: string) =>
node.name === 'details'
},
];
@ -119,7 +118,7 @@ export default class ElementWrapper extends Wrapper {
next_sibling: Wrapper
) {
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 = [];
@ -230,6 +229,36 @@ export default class ElementWrapper extends Wrapper {
}
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;
if (this.node.name === 'noscript') return;
@ -239,7 +268,7 @@ export default class ElementWrapper extends Wrapper {
}
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);
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) {
const loc = renderer.locate(this.node.start);
block.builders.hydrate.add_line(
@ -441,7 +440,7 @@ export default class ElementWrapper extends Wrapper {
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
// own hands
let animation_frame;
@ -454,7 +453,7 @@ export default class ElementWrapper extends Wrapper {
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) {
// need to create a block-local function that calls an instance-level function
block.builders.init.add_block(deindent`
@ -796,7 +795,8 @@ export default class ElementWrapper extends Wrapper {
add_classes(block: Block) {
this.node.classes.forEach(class_directive => {
const { expression, name } = class_directive;
let snippet, dependencies;
let snippet;
let dependencies;
if (expression) {
snippet = expression.render(block);
dependencies = expression.dependencies;

@ -14,7 +14,6 @@ import Text from './Text';
import Title from './Title';
import Window from './Window';
import { INode } from '../../nodes/interfaces';
import TextWrapper from './Text';
import Renderer from '../Renderer';
import Block from '../Block';
import { trim_start, trim_end } from '../../../utils/trim';
@ -64,14 +63,14 @@ export default class FragmentWrapper {
const child = nodes[i];
if (!child.type) {
throw new Error(`missing type`)
throw new Error(`missing type`);
}
if (!(child.type in wrappers)) {
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
if (child.type === 'Window') {
window_wrapper = new Window(renderer, block, parent, child);
@ -102,7 +101,7 @@ export default class FragmentWrapper {
continue;
}
const wrapper = new TextWrapper(renderer, block, parent, child, data);
const wrapper = new Text(renderer, block, parent, child, data);
if (wrapper.skip) continue;
this.nodes.unshift(wrapper);
@ -120,7 +119,7 @@ export default class FragmentWrapper {
}
if (strip_whitespace) {
const first = this.nodes[0] as TextWrapper;
const first = this.nodes[0] as Text;
if (first && first.node.type === 'Text') {
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');
}
}

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

@ -17,7 +17,7 @@ import TemplateScope from '../../../nodes/shared/TemplateScope';
export default class InlineComponentWrapper extends Wrapper {
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;
fragment: FragmentWrapper;
@ -62,8 +62,8 @@ export default class InlineComponentWrapper extends Wrapper {
this.var = (
this.node.name === 'svelte:self' ? renderer.component.name :
this.node.name === 'svelte:component' ? 'switch_instance' :
sanitize(this.node.name)
this.node.name === 'svelte:component' ? 'switch_instance' :
sanitize(this.node.name)
).toLowerCase();
if (this.node.children.length) {
@ -234,12 +234,12 @@ export default class InlineComponentWrapper extends Wrapper {
if (attribute.dependencies.size > 0) {
updates.push(deindent`
if (${[...attribute.dependencies]
.map(dependency => `changed.${dependency}`)
.join(' || ')}) ${name_changes}${quote_prop_if_necessary(attribute.name)} = ${attribute.get_value(block)};
.map(dependency => `changed.${dependency}`)
.join(' || ')}) ${name_changes}${quote_prop_if_necessary(attribute.name)} = ${attribute.get_value(block)};
`);
}
});
}
}
}
if (non_let_dependencies.length > 0) {
@ -262,10 +262,10 @@ export default class InlineComponentWrapper extends Wrapper {
let object;
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;
const { name } = binding.expression.node;
const { object, property, snippet } = block.bindings.get(name);
const { snippet } = block.bindings.get(name);
lhs = snippet;
// 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();
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;
const { name } = binding.expression.node;
const { object, property, snippet } = block.bindings.get(name);

@ -78,7 +78,7 @@ export default class SlotWrapper extends Wrapper {
});
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});
`);
let mount_before = block.builders.mount.toString();
const mount_before = block.builders.mount.toString();
block.builders.create.push_condition(`!${slot}`);
block.builders.claim.push_condition(`!${slot}`);

@ -14,13 +14,13 @@ export default class TitleWrapper extends Wrapper {
block: Block,
parent: Wrapper,
node: Title,
strip_whitespace: boolean,
next_sibling: Wrapper
_strip_whitespace: boolean,
_next_sibling: Wrapper
) {
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');
if (is_dynamic) {
@ -28,16 +28,16 @@ export default class TitleWrapper extends Wrapper {
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
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
const { expression } = this.node.children[0];
value = expression.render(block);
add_to_set(all_dependencies, expression.dependencies);
} else {
// '{foo} {bar}' treat as string concatenation
// '{foo} {bar}' — treat as string concatenation
value =
(this.node.children[0].type === 'Text' ? '' : `"" + `) +
this.node.children
@ -65,13 +65,12 @@ export default class TitleWrapper extends Wrapper {
if (this.node.should_cache) block.add_variable(last);
let updater;
const init = this.node.should_cache ? `${last} = ${value}` : value;
block.builders.init.add_line(
`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) {
const dependencies = Array.from(all_dependencies);

@ -1,6 +1,5 @@
import Renderer from '../Renderer';
import Block from '../Block';
import Node from '../../nodes/shared/Node';
import Wrapper from './shared/Wrapper';
import deindent from '../../utils/deindent';
import add_event_handlers from './shared/add_event_handlers';
@ -38,7 +37,7 @@ export default class WindowWrapper extends Wrapper {
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 { component } = renderer;
@ -88,7 +87,7 @@ export default class WindowWrapper extends Wrapper {
bindings.scrollY && `"${bindings.scrollY}" in this._state`
].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}`;
renderer.meta_bindings.add_block(deindent`
@ -142,17 +141,17 @@ export default class WindowWrapper extends Wrapper {
if (bindings.scrollX || bindings.scrollY) {
block.builders.update.add_block(deindent`
if (${
[bindings.scrollX, bindings.scrollY].filter(Boolean).map(
b => `changed.${b}`
).join(' || ')
} && !${scrolling}) {
[bindings.scrollX, bindings.scrollY].filter(Boolean).map(
b => `changed.${b}`
).join(' || ')
} && !${scrolling}) {
${scrolling} = true;
clearTimeout(${scrolling_timeout});
window.scrollTo(${
bindings.scrollX ? `ctx.${bindings.scrollX}` : `window.pageXOffset`
}, ${
bindings.scrollY ? `ctx.${bindings.scrollY}` : `window.pageYOffset`
});
bindings.scrollX ? `ctx.${bindings.scrollX}` : `window.pageXOffset`
}, ${
bindings.scrollY ? `ctx.${bindings.scrollY}` : `window.pageYOffset`
});
${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) {
// TODO use this in EachBlock and IfBlock tricky because
// TODO use this in EachBlock and IfBlock — tricky because
// children need to be created first
const needs_anchor = this.next ? !this.next.is_dom_node() : !parent_node || !this.parent.is_dom_node();
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');
}
}

@ -10,7 +10,8 @@ export default function add_actions(
) {
actions.forEach(action => {
const { expression } = action;
let snippet, dependencies;
let snippet;
let dependencies;
if (expression) {
snippet = expression.render(block);
@ -44,4 +45,4 @@ export default function add_actions(
`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;
function noop(){}
function noop() {}
const handlers: Record<string, Handler> = {
AwaitBlock,
@ -38,8 +38,8 @@ const handlers: Record<string, Handler> = {
};
export interface RenderOptions extends CompileOptions{
locate: (c: number) => { line: number; column: number; };
};
locate: (c: number) => { line: number; column: number };
}
export default class Renderer {
has_bindings = false;

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

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

@ -2,6 +2,6 @@ import { snip } from '../../utils/snip';
import Renderer, { RenderOptions } from '../Renderer';
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) + '}');
}

@ -1,6 +1,6 @@
import { snip } from '../../utils/snip';
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);
renderer.append(

@ -3,7 +3,7 @@ import Renderer, { RenderOptions } from '../Renderer';
import Text from '../../nodes/Text';
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;
if (
!node.parent ||

@ -7,6 +7,28 @@ import { extract_names } from '../utils/scope';
import { INode } from '../nodes/interfaces';
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(
component: Component,
options: CompileOptions
@ -66,7 +88,7 @@ export default function ssr(
: [];
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) {
const declared = extract_names(d.declaration);
@ -152,25 +174,3 @@ export default function ssr(
});
`).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;
}
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 {
root: BlockChunk = { type: 'root', children: [], parent: null };
last: Chunk;
@ -66,38 +101,3 @@ export default class CodeBuilder {
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
b.forEach(item => {
a.add(item);

@ -1,5 +1,15 @@
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(
strings: TemplateStringsArray,
...values: any[]
@ -41,13 +51,3 @@ export default function deindent(
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 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) {
const map = new WeakMap();
@ -59,82 +138,3 @@ export function create_scopes(expression: Node) {
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 } = {}) {
return data.replace(only_escape_at_symbol ? /@+/g : /(@+|#+)/g, (match: string) => {
return match + match[0];
});
}
export function stringify(data: string, options = {}) {
return JSON.stringify(escape(data, options));
}
const escaped = {
'&': '&amp;',
'<': '&lt;',

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

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

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

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

@ -3,6 +3,16 @@ import { walk } from 'estree-walker';
import { Parser } from '../index';
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[]) {
const content_start = parser.index;
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) {
parser.require_whitespace();
block.value = parser.read_identifier();

@ -8,6 +8,7 @@ import { Directive, DirectiveType, Node, Text } from '../../interfaces';
import fuzzymatch from '../../utils/fuzzymatch';
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 meta_tags = new Map([
@ -36,7 +37,9 @@ const specials = new Map([
],
]);
// eslint-disable-next-line no-useless-escape
const SELF = /^svelte:self(?=[\s\/>])/;
// eslint-disable-next-line no-useless-escape
const COMPONENT = /^svelte:component(?=[\s\/>])/;
// based on http://developers.whatwg.org/syntax.html#syntax-tag-omission
@ -74,190 +77,6 @@ function parent_is_head(stack) {
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) {
const start = parser.index;
@ -313,6 +132,90 @@ function read_tag_name(parser: Parser) {
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>) {
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;
let end = parser.index;
@ -396,12 +300,12 @@ function read_attribute(parser: Parser, unique_names: Set<string>) {
if (type === 'Ref') {
parser.error({
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);
}
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({
code: `invalid-directive-value`,
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 {
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';
}
export default function tag(parser: Parser) {
const start = parser.index++;
function read_attribute_value(parser: Parser) {
const quote_mark = parser.eat(`'`) ? `'` : parser.eat(`"`) ? `"` : null;
let parent = parser.current();
const regex = (
quote_mark === `'` ? /'/ :
quote_mark === `"` ? /"/ :
/(\/>|[\s"'=<>`])/
);
if (parser.eat('!--')) {
const data = parser.read_until(/-->/);
parser.eat('-->', true, 'comment was left open, expected -->');
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 value;
}
return;
}
function read_sequence(parser: Parser, done: () => boolean): Node[] {
let current_chunk: Text = {
start: parser.index,
end: null,
type: 'Text',
raw: '',
data: null
};
const is_closing_tag = parser.eat('/');
function flush() {
if (current_chunk.raw) {
current_chunk.data = decode_character_references(current_chunk.raw);
current_chunk.end = parser.index;
chunks.push(current_chunk);
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 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 index = parser.index;
const element: Node = {
start,
end: null, // filled in later
type,
name,
attributes: [],
children: [],
};
if (done()) {
flush();
return chunks;
} else if (parser.eat('{')) {
flush();
parser.allow_whitespace();
parser.allow_whitespace();
const expression = read_expression(parser);
parser.allow_whitespace();
parser.eat('}', true);
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);
}
chunks.push({
start: index,
end: parser.index,
type: 'MustacheTag',
expression,
});
parser.eat('>', true);
current_chunk = {
start: parser.index,
end: null,
type: 'Text',
raw: '',
data: null
};
} else {
current_chunk.raw += parser.template[parser.index++];
// 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();
}
}
parser.error({
code: `unexpected-eof`,
message: `Unexpected end of input`
});
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);
}
}

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

@ -1,5 +1,6 @@
import entities from './entities';
const NUL = 0;
const windows_1252 = [
8364,
129,
@ -40,29 +41,6 @@ const entity_pattern = new RegExp(
'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
// code points with alternatives in some cases - since we're bypassing that mechanism, we need
// 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
// 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) {
return windows_1252[code - 128];
}
@ -112,3 +90,24 @@ function validate_code(code: number) {
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 {
markup?: (options: {
content: string,
filename: string
}) => { code: string, map?: SourceMap | string, dependencies?: string[] };
content: string;
filename: string;
}) => { code: string; map?: SourceMap | string; dependencies?: string[] };
style?: Preprocessor;
script?: Preprocessor;
}
export type Preprocessor = (options: {
content: string,
attributes: Record<string, string | boolean>,
filename?: string
}) => { code: string, map?: SourceMap | string, dependencies?: string[] };
content: string;
attributes: Record<string, string | boolean>;
filename?: string;
}) => { code: string; map?: SourceMap | string; dependencies?: string[] };
interface Processed {
code: string;
@ -43,16 +43,16 @@ interface Replacement {
}
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) => {
replacements.push(
func(...args).then(
res =>
<Replacement>({
res => // eslint-disable-next-line @typescript-eslint/no-object-literal-type-assertion
({
offset: args[args.length - 2],
length: args[0].length,
replacement: res,
})
}) as Replacement
)
);
return '';

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

@ -2,9 +2,9 @@
// Reproduced under MIT License https://github.com/acornjs/acorn/blob/master/LICENSE
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;
let next = str.charCodeAt(i + 1);
const next = str.charCodeAt(i + 1);
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
// BSD Licensed
const GRAM_SIZE_LOWER = 2;
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
function levenshtein(str1: string, str2: string) {
const current: number[] = [];
@ -53,6 +30,22 @@ function levenshtein(str1: string, str2: string) {
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, ]+/;
function iterate_grams(value: string, gram_size = 2) {
@ -144,7 +137,7 @@ class FuzzySet {
items[index] = [vector_normal, normalized_value];
this.items[gram_size] = items;
this.exact_set[normalized_value] = value;
};
}
get(value: string) {
const normalized_value = value.toLowerCase();
@ -232,5 +225,13 @@ class FuzzySet {
}
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?
export interface AnimationConfig {
delay?: number,
duration?: number,
easing?: (t: number) => number,
css?: (t: number, u: number) => string,
tick?: (t: number, u: number) => void
delay?: number;
duration?: number;
easing?: (t: number) => number;
css?: (t: number, u: number) => string;
tick?: (t: number, u: number) => void;
}
interface FlipParams {
delay: 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 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 { children } from './dom';
// eslint-disable-next-line @typescript-eslint/class-name-casing
interface T$$ {
dirty: null;
ctx: null|any;
@ -16,7 +17,7 @@ interface T$$ {
before_render: any[];
context: Map<any, any>;
on_mount: any[];
on_destroy: any[]
on_destroy: any[];
}
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.hydrate) {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
$$.fragment!.l(children(options.target));
} else {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
$$.fragment!.c();
}

@ -7,7 +7,7 @@ import { AnimationConfig } from '../animate';
//todo: documentation says it is DOMRect, but in IE it would be 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) {
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);
}
export function insert(target: Node, node: Node, anchor?:Node) {
export function insert(target: Node, node: Node, anchor?: Node) {
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) {
after.parentNode.removeChild(after.previousSibling);
}
}
export function detach_after(before:Node) {
export function detach_after(before: Node) {
while (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);
}
export function object_without_properties<T,K extends keyof T>(obj:T, exclude: K[]) {
const target = {} as Pick<T, Exclude<keyof T, K>>;
export function object_without_properties<T, K extends keyof T>(obj: T, exclude: K[]) {
const target: Pick<T, Exclude<keyof T, K>> = {};
for (const k in obj) {
if (
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;
}
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);
}
export function text(data:string) {
export function text(data: string) {
return document.createTextNode(data);
}
@ -95,7 +95,7 @@ export function attr(node: Element, attribute: string, value?: string) {
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) {
if (key === 'style') {
node.style.cssText = attributes[key];

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

@ -10,28 +10,19 @@ const binding_callbacks = [];
const render_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) {
render_callbacks.push(fn);
}
export function add_flush_callback(fn) {
flush_callbacks.push(fn);
function update($$) {
if ($$.fragment) {
$$.update($$.dirty);
run_all($$.before_render);
$$.fragment.p($$.dirty, $$.ctx);
$$.dirty = null;
$$.after_render.forEach(add_render_callback);
}
}
export function flush() {
@ -69,13 +60,22 @@ export function flush() {
update_scheduled = false;
}
function update($$) {
if ($$.fragment) {
$$.update($$.dirty);
run_all($$.before_render);
$$.fragment.p($$.dirty, $$.ctx);
$$.dirty = null;
$$.after_render.forEach(add_render_callback);
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_flush_callback(fn) {
flush_callbacks.push(fn);
}

@ -44,6 +44,15 @@ export function create_rule(node: Element & ElementCSSInlineStyle, a: number, b:
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) {
node.style.animation = (node.style.animation || '')
.split(', ')
@ -55,12 +64,3 @@ export function delete_rule(node: Element & ElementCSSInlineStyle, name?: string
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();
running = true;
add_render_callback(() => dispatch(node, true, 'start'));
add_render_callback(() => dispatch(node, true, 'start'));
task = loop(now => {
if (running) {
if (now >= end_time) {
tick(1, 0);
dispatch(node, true, 'end');
dispatch(node, true, 'end');
cleanup();
return running = false;
@ -146,14 +146,14 @@ export function create_out_transition(node: Element & ElementCSSInlineStyle, fn:
const start_time = now() + delay;
const end_time = start_time + duration;
add_render_callback(() => dispatch(node, false, 'start'));
add_render_callback(() => dispatch(node, false, 'start'));
loop(now => {
if (running) {
if (now >= end_time) {
tick(0, 1);
dispatch(node, false, 'end');
dispatch(node, false, 'end');
if (!--group.remaining) {
// this will result in `end()` being called,

@ -2,7 +2,7 @@ export function noop() {}
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
for (const k in src) tar[k] = src[k];
return tar as T & S;
@ -56,6 +56,12 @@ export function subscribe(component, store, callback) {
: 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) {
if (definition) {
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) {
return definition[1]
? assign({}, assign(ctx.$$scope.changed || {}, definition[1](fn ? fn(changed) : {})))

@ -6,10 +6,10 @@ interface TickContext<T> {
inv_mass: number;
dt: number;
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)) {
// @ts-ignore
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 {
stiffness?: number,
damping?: number,
precision?: number,
stiffness?: number;
damping?: number;
precision?: number;
}
interface SpringUpdateOpts {
@ -62,7 +62,7 @@ interface Spring<T> extends Readable<T>{
update: (fn: Updater<T>, opts?: SpringUpdateOpts) => Promise<void>;
precision: number;
damping: number;
stiffness: number
stiffness: number;
}
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 task: Task;
let current_token: object;
let last_value:T = value;
let target_value:T = value;
let last_value: T = value;
let target_value: T = value;
let inv_mass = 1;
let inv_mass_recovery_rate = 0;
let cancel_task = false;
/* eslint-disable @typescript-eslint/no-use-before-define */
function set(new_value: T, opts: SpringUpdateOpts={}): Promise<void> {
target_value = new_value;
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,
update: (fn, opts:SpringUpdateOpts) => set(fn(target_value, value), opts),
update: (fn, opts: SpringUpdateOpts) => set(fn(target_value, value), opts),
subscribe: store.subscribe,
stiffness,
damping,
precision
} as Spring<T>;
};
return spring;
}

@ -56,9 +56,9 @@ function get_interpolator(a, b) {
interface Options<T> {
delay?: number;
duration?: number | ((from: T, to: T) => number)
duration?: number | ((from: T, to: T) => 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;
@ -69,7 +69,7 @@ interface Tweened<T> extends Readable<T> {
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);
let task: Task;
@ -122,7 +122,7 @@ export function tweened<T>(value: T, defaults: Options<T> = {}):Tweened<T> {
return {
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
};
}

@ -43,17 +43,6 @@ export interface Writable<T> extends Readable<T> {
/** Pair of subscriber and invalidator. */
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.
* @param {*=}value initial value
@ -100,6 +89,17 @@ export function writable<T>(value: T, start: StartStopNotifier<T> = noop): Writa
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. */
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';
export interface TransitionConfig {
delay?: number,
duration?: number,
easing?: (t: number) => number,
css?: (t: number, u: number) => string,
tick?: (t: number, u: number) => void
delay?: number;
duration?: number;
easing?: (t: number) => number;
css?: (t: number, u: number) => string;
tick?: (t: number, u: number) => void;
}
interface FadeParams {
@ -30,7 +30,7 @@ export function fade(node: Element, {
interface FlyParams {
delay: number;
duration: number;
easing: (t: number)=>number,
easing: (t: number) => number;
x: number;
y: number;
opacity: number;
@ -63,7 +63,7 @@ export function fly(node: Element, {
interface SlideParams {
delay: number;
duration: number;
easing: (t: number)=>number,
easing: (t: number) => number;
}
export function slide(node: Element, {
@ -101,7 +101,7 @@ export function slide(node: Element, {
interface ScaleParams {
delay: number;
duration: number;
easing: (t: number)=>number,
easing: (t: number) => number;
start: number;
opacity: number;
}
@ -135,7 +135,7 @@ interface DrawParams {
delay: number;
speed: number;
duration: number | ((len: number) => number);
easing: (t: number) => number,
easing: (t: number) => number;
}
export function draw(node: SVGElement & { getTotalLength(): number }, {
@ -167,18 +167,18 @@ export function draw(node: SVGElement & { getTotalLength(): number }, {
interface CrossfadeParams {
delay: number;
duration: number | ((len: number) => number);
easing: (t: number) => number,
easing: (t: number) => number;
}
type ClientRectMap = Map<any, { rect: ClientRect }>;
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_send: ClientRectMap = new Map();
function crossfade(from: ClientRect, node: Element, params: CrossfadeParams):TransitionConfig {
function crossfade(from: ClientRect, node: Element, params: CrossfadeParams): TransitionConfig {
const {
delay = 0,
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;
}
function read(file) {
try {
return fs.readFileSync(file, 'utf-8');
} catch (err) {
return null;
}
}
describe('css', () => {
fs.readdirSync('test/css/samples').forEach(dir => {
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+)/;
export function deindent(strings, ...values) {
const indentation = start.exec(strings[0])[1];
@ -222,12 +228,6 @@ export function deindent(strings, ...values) {
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) {
let result = '';
while (i--) result += ' ';

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Loading…
Cancel
Save