revert ordering changes

pull/2958/head
James Garbutt 6 years ago
parent fa1ee4be9c
commit d22422ca2f

@ -23,6 +23,7 @@
"no-inner-declarations": 0, "no-inner-declarations": 0,
"@typescript-eslint/indent": [2, "tab", { "SwitchCase": 1 }], "@typescript-eslint/indent": [2, "tab", { "SwitchCase": 1 }],
"@typescript-eslint/camelcase": "off", "@typescript-eslint/camelcase": "off",
"@typescript-eslint/no-use-before-define": "off",
"@typescript-eslint/array-type": ["error", "array-simple"], "@typescript-eslint/array-type": ["error", "array-simple"],
"@typescript-eslint/explicit-function-return-type": "off", "@typescript-eslint/explicit-function-return-type": "off",
"@typescript-eslint/no-explicit-any": "off", "@typescript-eslint/no-explicit-any": "off",

@ -67,120 +67,6 @@ function remove_node(code: MagicString, start: number, end: number, body: Node,
return; return;
} }
function process_component_options(component: Component, nodes) {
const component_options: ComponentOptions = {
immutable: component.compile_options.immutable || false,
accessors: 'accessors' in component.compile_options
? component.compile_options.accessors
: !!component.compile_options.customElement,
preserveWhitespace: !!component.compile_options.preserveWhitespace
};
const node = nodes.find(node => node.name === 'svelte:options');
function get_value(attribute, code, message) {
const { value } = attribute;
const chunk = value[0];
if (!chunk) return true;
if (value.length > 1) {
component.error(attribute, { code, message });
}
if (chunk.type === 'Text') return chunk.data;
if (chunk.expression.type !== 'Literal') {
component.error(attribute, { code, message });
}
return chunk.expression.value;
}
if (node) {
node.attributes.forEach(attribute => {
if (attribute.type === 'Attribute') {
const { name } = attribute;
switch (name) {
case 'tag': {
const code = 'invalid-tag-attribute';
const message = `'tag' must be a string literal`;
const tag = get_value(attribute, code, message);
if (typeof tag !== 'string' && tag !== null) component.error(attribute, { code, message });
if (tag && !/^[a-zA-Z][a-zA-Z0-9]*-[a-zA-Z0-9-]+$/.test(tag)) {
component.error(attribute, {
code: `invalid-tag-property`,
message: `tag name must be two or more words joined by the '-' character`
});
}
component_options.tag = tag;
break;
}
case 'namespace': {
const code = 'invalid-namespace-attribute';
const message = `The 'namespace' attribute must be a string literal representing a valid namespace`;
const ns = get_value(attribute, code, message);
if (typeof ns !== 'string') component.error(attribute, { code, message });
if (valid_namespaces.indexOf(ns) === -1) {
const match = fuzzymatch(ns, valid_namespaces);
if (match) {
component.error(attribute, {
code: `invalid-namespace-property`,
message: `Invalid namespace '${ns}' (did you mean '${match}'?)`
});
} else {
component.error(attribute, {
code: `invalid-namespace-property`,
message: `Invalid namespace '${ns}'`
});
}
}
component_options.namespace = ns;
break;
}
case 'accessors':
case 'immutable':
case 'preserveWhitespace':
{
const code = `invalid-${name}-value`;
const message = `${name} attribute must be true or false`;
const value = get_value(attribute, code, message);
if (typeof value !== 'boolean') component.error(attribute, { code, message });
component_options[name] = value;
break;
}
default:
component.error(attribute, {
code: `invalid-options-attribute`,
message: `<svelte:options> unknown attribute`
});
}
}
else {
component.error(attribute, {
code: `invalid-options-attribute`,
message: `<svelte:options> can only have static 'tag', 'namespace', 'accessors', 'immutable' and 'preserveWhitespace' attributes`
});
}
});
}
return component_options;
}
export default class Component { export default class Component {
stats: Stats; stats: Stats;
warnings: Warning[]; warnings: Warning[];
@ -1351,3 +1237,117 @@ 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,11 +3,35 @@ import list from '../utils/list';
import { ModuleFormat, Node } from '../interfaces'; import { ModuleFormat, Node } from '../interfaces';
import { stringify_props } from './utils/stringify_props'; import { stringify_props } from './utils/stringify_props';
const wrappers = { esm, cjs };
interface Export { interface Export {
name: string; name: string;
as: string; as: string;
} }
export default function create_module(
code: string,
format: ModuleFormat,
name: string,
banner: string,
sveltePath = 'svelte',
helpers: 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))})`);
}
function edit_source(source, sveltePath) { function edit_source(source, sveltePath) {
return source === 'svelte' || source.startsWith('svelte/') return source === 'svelte' || source.startsWith('svelte/')
? source.replace('svelte', sveltePath) ? source.replace('svelte', sveltePath)
@ -109,27 +133,3 @@ function cjs(
${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,102 +4,121 @@ import { gather_possible_values, UNKNOWN } from './gather_possible_values';
import { Node } from '../../interfaces'; import { Node } from '../../interfaces';
import Component from '../Component'; import Component from '../Component';
class Block { export default class Selector {
global: boolean; node: Node;
combinator: Node; stylesheet: Stylesheet;
selectors: Node[] blocks: Block[];
start: number; local_blocks: Block[];
end: number; used: boolean;
should_encapsulate: boolean;
constructor(combinator: Node) { constructor(node: Node, stylesheet: Stylesheet) {
this.combinator = combinator; this.node = node;
this.global = false; this.stylesheet = stylesheet;
this.selectors = [];
this.start = null; this.blocks = group_selectors(node);
this.end = null;
this.should_encapsulate = false; // take trailing :global(...) selectors out of consideration
let i = this.blocks.length;
while (i > 0) {
if (!this.blocks[i - 1].global) break;
i -= 1;
} }
add(selector: Node) { this.local_blocks = this.blocks.slice(0, i);
if (this.selectors.length === 0) { this.used = this.blocks[0].global;
this.start = selector.start;
this.global = selector.type === 'PseudoClassSelector' && selector.name === 'global';
} }
this.selectors.push(selector); apply(node: Node, stack: Node[]) {
this.end = selector.end; const to_encapsulate: Node[] = [];
}
}
function group_selectors(selector: Node) { apply_selector(this.stylesheet, this.local_blocks.slice(), node, stack.slice(), to_encapsulate);
let block: Block = new Block(null);
const blocks = [block]; 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;
});
selector.children.forEach((child: Node) => { this.used = true;
if (child.type === 'WhiteSpace' || child.type === 'Combinator') { }
block = new Block(child); }
blocks.push(block);
} else { minify(code: MagicString) {
block.add(child); 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 = { if (selector.type === 'TypeSelector' && selector.name === '*') {
'=' : (value: string, flags: string) => new RegExp(`^${value}$`, flags), code.overwrite(selector.start, selector.end, attr);
'~=': (value: string, flags: string) => new RegExp(`\\b${value}\\b`, flags), } else {
'|=': (value: string, flags: string) => new RegExp(`^${value}(-.+)?$`, flags), code.appendLeft(selector.end, attr);
'^=': (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) { break;
const spread = node.attributes.find(attr => attr.type === 'Spread'); }
if (spread) return true; }
const attr = node.attributes.find((attr: Node) => attr.name === name); this.blocks.forEach((block) => {
if (!attr) return false; if (block.global) {
if (attr.is_true) return operator === null; const selector = block.selectors[0];
if (attr.chunks.length > 1) return true; const first = selector.children[0];
if (!expected_value) return true; 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' : ''); if (block.should_encapsulate) encapsulate_block(block);
const value = attr.chunks[0]; });
}
if (!value) return false; validate(component: Component) {
if (value.type === 'Text') return pattern.test(value.data); 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(); let start = 0;
gather_possible_values(value.node, possible_values); let end = this.blocks.length;
if (possible_values.has(UNKNOWN)) return true;
for (const x of Array.from(possible_values)) { // TypeScript for-of is slightly unlike JS for (; start < end; start += 1) {
if (pattern.test(x)) return true; 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) { for (let i = start; i < end; i += 1) {
return node.classes.some((class_directive) => { if (this.blocks[i].global) {
return new RegExp(`\\b${name}\\b`).test(class_directive.name); 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 { function apply_selector(stylesheet: Stylesheet, blocks: Block[], node: Node, stack: Node[], to_encapsulate: any[]): boolean {
@ -182,119 +201,100 @@ function apply_selector(stylesheet: Stylesheet, blocks: Block[], node: Node, sta
return true; return true;
} }
export default class Selector { const operators = {
node: Node; '=' : (value: string, flags: string) => new RegExp(`^${value}$`, flags),
stylesheet: Stylesheet; '~=': (value: string, flags: string) => new RegExp(`\\b${value}\\b`, flags),
blocks: Block[]; '|=': (value: string, flags: string) => new RegExp(`^${value}(-.+)?$`, flags),
local_blocks: Block[]; '^=': (value: string, flags: string) => new RegExp(`^${value}`, flags),
used: boolean; '$=': (value: string, flags: string) => new RegExp(`${value}$`, flags),
'*=': (value: string, flags: string) => new RegExp(value, flags)
constructor(node: Node, stylesheet: Stylesheet) { };
this.node = node;
this.stylesheet = stylesheet;
this.blocks = group_selectors(node);
// take trailing :global(...) selectors out of consideration function attribute_matches(node: Node, name: string, expected_value: string, operator: string, case_insensitive: boolean) {
let i = this.blocks.length; const spread = node.attributes.find(attr => attr.type === 'Spread');
while (i > 0) { if (spread) return true;
if (!this.blocks[i - 1].global) break;
i -= 1;
}
this.local_blocks = this.blocks.slice(0, i); const attr = node.attributes.find((attr: Node) => attr.name === name);
this.used = this.blocks[0].global; if (!attr) return false;
} if (attr.is_true) return operator === null;
if (attr.chunks.length > 1) return true;
if (!expected_value) return true;
apply(node: Node, stack: Node[]) { const pattern = operators[operator](expected_value, case_insensitive ? 'i' : '');
const to_encapsulate: Node[] = []; const value = attr.chunks[0];
apply_selector(this.stylesheet, this.local_blocks.slice(), node, stack.slice(), to_encapsulate); if (!value) return false;
if (value.type === 'Text') return pattern.test(value.data);
if (to_encapsulate.length > 0) { const possible_values = new Set();
to_encapsulate.filter((_, i) => i === 0 || i === to_encapsulate.length - 1).forEach(({ node, block }) => { gather_possible_values(value.node, possible_values);
this.stylesheet.nodes_with_css_class.add(node); if (possible_values.has(UNKNOWN)) return true;
block.should_encapsulate = true;
});
this.used = true; for (const x of Array.from(possible_values)) { // TypeScript for-of is slightly unlike JS
} if (pattern.test(x)) return true;
} }
minify(code: MagicString) { return false;
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; function class_matches(node, name: string) {
return node.classes.some((class_directive) => {
return new RegExp(`\\b${name}\\b`).test(class_directive.name);
}); });
}
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;
}
transform(code: MagicString, attr: string) { class Block {
function encapsulate_block(block: Block) { global: boolean;
let i = block.selectors.length; combinator: Node;
while (i--) { selectors: Node[]
const selector = block.selectors[i]; start: number;
if (selector.type === 'PseudoElementSelector' || selector.type === 'PseudoClassSelector') continue; end: number;
should_encapsulate: boolean;
if (selector.type === 'TypeSelector' && selector.name === '*') { constructor(combinator: Node) {
code.overwrite(selector.start, selector.end, attr); this.combinator = combinator;
} else { this.global = false;
code.appendLeft(selector.end, attr); this.selectors = [];
}
break; this.start = null;
} this.end = null;
}
this.blocks.forEach((block) => { this.should_encapsulate = false;
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); add(selector: Node) {
}); if (this.selectors.length === 0) {
this.start = selector.start;
this.global = selector.type === 'PseudoClassSelector' && selector.name === 'global';
} }
validate(component: Component) { this.selectors.push(selector);
this.blocks.forEach((block) => { this.end = selector.end;
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`
});
}
} }
}); }
let start = 0; function group_selectors(selector: Node) {
let end = this.blocks.length; let block: Block = new Block(null);
for (; start < end; start += 1) { const blocks = [block];
if (!this.blocks[start].global) break;
}
for (; end > start; end -= 1) { selector.children.forEach((child: Node) => {
if (!this.blocks[end - 1].global) break; if (child.type === 'WhiteSpace' || child.type === 'Combinator') {
block = new Block(child);
blocks.push(block);
} else {
block.add(child);
} }
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`
}); });
}
} return blocks;
}
} }

@ -20,44 +20,6 @@ function hash(str: string): string {
return (hash >>> 0).toString(36); return (hash >>> 0).toString(36);
} }
class Declaration {
node: Node;
constructor(node: Node) {
this.node = node;
}
transform(code: MagicString, keyframes: Map<string, string>) {
const property = this.node.property && remove_css_prefix(this.node.property.toLowerCase());
if (property === 'animation' || property === 'animation-name') {
this.node.value.children.forEach((block: Node) => {
if (block.type === 'Identifier') {
const name = block.name;
if (keyframes.has(name)) {
code.overwrite(block.start, block.end, keyframes.get(name));
}
}
});
}
}
minify(code: MagicString) {
if (!this.node.property) return; // @apply, and possibly other weird cases?
const c = this.node.start + this.node.property.length;
const first = this.node.value.children
? this.node.value.children[0]
: this.node.value;
let start = first.start;
while (/\s/.test(code.original[start])) start += 1;
if (start - c > 1) {
code.overwrite(c, start, ':');
}
}
}
class Rule { class Rule {
selectors: Selector[]; selectors: Selector[];
declarations: Declaration[]; declarations: Declaration[];
@ -138,6 +100,44 @@ class Rule {
} }
} }
class Declaration {
node: Node;
constructor(node: Node) {
this.node = node;
}
transform(code: MagicString, keyframes: Map<string, string>) {
const property = this.node.property && remove_css_prefix(this.node.property.toLowerCase());
if (property === 'animation' || property === 'animation-name') {
this.node.value.children.forEach((block: Node) => {
if (block.type === 'Identifier') {
const name = block.name;
if (keyframes.has(name)) {
code.overwrite(block.start, block.end, keyframes.get(name));
}
}
});
}
}
minify(code: MagicString) {
if (!this.node.property) return; // @apply, and possibly other weird cases?
const c = this.node.start + this.node.property.length;
const first = this.node.value.children
? this.node.value.children[0]
: this.node.value;
let start = first.start;
while (/\s/.test(code.original[start])) start += 1;
if (start - c > 1) {
code.overwrite(c, start, ':');
}
}
}
class Atrule { class Atrule {
node: Node; node: Node;
children: Array<Atrule|Rule>; children: Array<Atrule|Rule>;

@ -89,22 +89,6 @@ function get_namespace(parent: Element, element: Element, explicit_namespace: st
return parent_element.namespace; return parent_element.namespace;
} }
function should_have_attribute(
node,
attributes: string[],
name = node.name
) {
const article = /^[aeiou]/.test(attributes[0]) ? 'an' : 'a';
const sequence = attributes.length > 1 ?
attributes.slice(0, -1).join(', ') + ` or ${attributes[attributes.length - 1]}` :
attributes[0];
node.component.warn(node, {
code: `a11y-missing-attribute`,
message: `A11y: <${name}> element should have ${article} ${sequence} attribute`
});
}
export default class Element extends Node { export default class Element extends Node {
type: 'Element'; type: 'Element';
name: string; name: string;
@ -723,3 +707,19 @@ 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`
});
}

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

@ -64,33 +64,6 @@ const precedence: Record<string, (node?: Node) => number> = {
type Owner = Wrapper | INode; type Owner = Wrapper | INode;
function get_function_name(_node, parent) {
if (parent.type === 'EventHandler') {
return `${parent.name}_handler`;
}
if (parent.type === 'Action') {
return `${parent.name}_function`;
}
return 'func';
}
function is_contextual(component: Component, scope: TemplateScope, name: string) {
if (name === '$$props') return true;
// if it's a name below root scope, it's contextual
if (!scope.is_top_level(name)) return true;
const variable = component.var_lookup.get(name);
// hoistables, module declarations, and imports are non-contextual
if (!variable || variable.hoistable) return false;
// assume contextual
return true;
}
export default class Expression { export default class Expression {
type: 'Expression' = 'Expression'; type: 'Expression' = 'Expression';
component: Component; component: Component;
@ -516,3 +489,30 @@ 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,6 +16,8 @@ import Title from '../Title';
import Window from '../Window'; import Window from '../Window';
import { Node } from '../../../interfaces'; import { Node } from '../../../interfaces';
export type Children = ReturnType<typeof map_children>;
function get_constructor(type) { function get_constructor(type) {
switch (type) { switch (type) {
case 'AwaitBlock': return AwaitBlock; case 'AwaitBlock': return AwaitBlock;
@ -51,5 +53,3 @@ export default function map_children(component, parent, scope, children: Node[])
return node; return node;
}); });
} }
export type Children = ReturnType<typeof map_children>;

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

@ -14,152 +14,6 @@ function get_tail(node: INode) {
return { start: node.end, end }; return { start: node.end, end };
} }
function get_dom_updater(
element: ElementWrapper,
binding: BindingWrapper
) {
const { node } = element;
if (binding.is_readonly_media_attribute()) {
return null;
}
if (binding.node.name === 'this') {
return null;
}
if (node.name === 'select') {
return node.get_static_attribute_value('multiple') === true ?
`@select_options(${element.var}, ${binding.snippet})` :
`@select_option(${element.var}, ${binding.snippet})`;
}
if (binding.node.name === 'group') {
const type = node.get_static_attribute_value('type');
const condition = type === 'checkbox'
? `~${binding.snippet}.indexOf(${element.var}.__value)`
: `${element.var}.__value === ${binding.snippet}`;
return `${element.var}.checked = ${condition};`;
}
return `${element.var}.${binding.node.name} = ${binding.snippet};`;
}
function get_binding_group(renderer: Renderer, value: Node) {
const { parts } = flatten_reference(value); // TODO handle cases involving computed member expressions
const keypath = parts.join('.');
// TODO handle contextual bindings — `keypath` should include unique ID of
// each block that provides context
let index = renderer.binding_groups.indexOf(keypath);
if (index === -1) {
index = renderer.binding_groups.length;
renderer.binding_groups.push(keypath);
}
return index;
}
function mutate_store(store, value, tail) {
return tail
? `${store}.update($$value => ($$value${tail} = ${value}, $$value));`
: `${store}.set(${value});`;
}
function get_value_from_dom(
renderer: Renderer,
element: ElementWrapper,
binding: BindingWrapper
) {
const { node } = element;
const { name } = binding.node;
if (name === 'this') {
return `$$node`;
}
// <select bind:value='selected>
if (node.name === 'select') {
return node.get_static_attribute_value('multiple') === true ?
`@select_multiple_value(this)` :
`@select_value(this)`;
}
const type = node.get_static_attribute_value('type');
// <input type='checkbox' bind:group='foo'>
if (name === 'group') {
const binding_group = get_binding_group(renderer, binding.node.expression.node);
if (type === 'checkbox') {
return `@get_binding_group_value($$binding_groups[${binding_group}])`;
}
return `this.__value`;
}
// <input type='range|number' bind:value>
if (type === 'range' || type === 'number') {
return `@to_number(this.${name})`;
}
if ((name === 'buffered' || name === 'seekable' || name === 'played')) {
return `@time_ranges_to_array(this.${name})`;
}
// everything else
return `this.${name}`;
}
function get_event_handler(
binding: BindingWrapper,
renderer: Renderer,
block: Block,
name: string,
snippet: string
) {
const value = get_value_from_dom(renderer, binding.parent, binding);
const store = binding.object[0] === '$' ? binding.object.slice(1) : null;
let tail = '';
if (binding.node.expression.node.type === 'MemberExpression') {
const { start, end } = get_tail(binding.node.expression.node);
tail = renderer.component.source.slice(start, end);
}
if (binding.node.is_contextual) {
const { object, property, snippet } = block.bindings.get(name);
return {
uses_context: true,
mutation: store
? mutate_store(store, value, tail)
: `${snippet}${tail} = ${value};`,
contextual_dependencies: new Set([object, property])
};
}
const mutation = store
? mutate_store(store, value, tail)
: `${snippet} = ${value};`;
if (binding.node.expression.node.type === 'MemberExpression') {
return {
uses_context: binding.node.expression.uses_context,
mutation,
contextual_dependencies: binding.node.expression.contextual_dependencies,
snippet
};
}
return {
uses_context: false,
mutation,
contextual_dependencies: new Set()
};
}
export default class BindingWrapper { export default class BindingWrapper {
node: Binding; node: Binding;
parent: ElementWrapper; parent: ElementWrapper;
@ -313,3 +167,149 @@ 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}`;
}

@ -12,6 +12,98 @@ export interface StyleProp {
value: Array<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: 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 @typescript-eslint/no-object-literal-type-assertion */
chunks[0] = {
start: chunk.start + offset,
end: chunk.end,
type: 'Text',
data: remaining_data
} as Text;
/* eslint-enable @typescript-eslint/no-object-literal-type-assertion */
} 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: Array<Text | Expression>) { function get_style_value(chunks: Array<Text | Expression>) {
const value: Array<Text|Expression> = []; const value: Array<Text|Expression> = [];
@ -81,97 +173,6 @@ function get_style_value(chunks: Array<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>) { function is_dynamic(value: Array<Text|Expression>) {
return value.length > 1 || value[0].type !== 'Text'; return value.length > 1 || value[0].type !== 'Text';
} }
export default class StyleAttributeWrapper extends AttributeWrapper {
node: Attribute;
parent: ElementWrapper;
render(block: Block) {
const style_props = optimize_style(this.node.chunks);
if (!style_props) return super.render(block);
style_props.forEach((prop: StyleProp) => {
let value;
if (is_dynamic(prop.value)) {
const prop_dependencies = new Set();
value =
((prop.value.length === 1 || prop.value[0].type === 'Text') ? '' : `"" + `) +
prop.value
.map((chunk) => {
if (chunk.type === 'Text') {
return stringify(chunk.data);
} else {
const snippet = chunk.render();
add_to_set(prop_dependencies, chunk.dependencies);
return chunk.get_precedence() <= 13 ? `(${snippet})` : snippet;
}
})
.join(' + ');
if (prop_dependencies.size) {
const dependencies = Array.from(prop_dependencies);
const condition = (
(block.has_outros ? `!#current || ` : '') +
dependencies.map(dependency => `changed.${dependency}`).join(' || ')
);
block.builders.update.add_conditional(
condition,
`@set_style(${this.parent.var}, "${prop.key}", ${value});`
);
}
} else {
value = stringify((prop.value[0] as Text).data);
}
block.builders.hydrate.add_line(
`@set_style(${this.parent.var}, "${prop.key}", ${value});`
);
});
}
}

@ -229,36 +229,6 @@ export default class ElementWrapper extends Wrapper {
} }
render(block: Block, parent_node: string, parent_nodes: string) { render(block: Block, parent_node: string, parent_nodes: string) {
function to_html(wrapper: ElementWrapper | TextWrapper) {
if (wrapper.node.type === 'Text') {
const parent = wrapper.node.parent as Element;
const raw = parent && (
parent.name === 'script' ||
parent.name === 'style'
);
return raw
? wrapper.node.data
: escape_html(wrapper.node.data)
.replace(/\\/g, '\\\\')
.replace(/`/g, '\\`')
.replace(/\$/g, '\\$');
}
if (wrapper.node.name === 'noscript') return '';
let open = `<${wrapper.node.name}`;
(wrapper as ElementWrapper).attributes.forEach((attr: AttributeWrapper) => {
open += ` ${fix_attribute_casing(attr.node.name)}${attr.stringify()}`;
});
if (is_void(wrapper.node.name)) return open + '>';
return `${open}>${(wrapper as ElementWrapper).fragment.nodes.map(to_html).join('')}</${wrapper.node.name}>`;
}
const { renderer } = this; const { renderer } = this;
if (this.node.name === 'noscript') return; if (this.node.name === 'noscript') return;
@ -357,6 +327,36 @@ export default class ElementWrapper extends Wrapper {
); );
} }
function to_html(wrapper: ElementWrapper | TextWrapper) {
if (wrapper.node.type === 'Text') {
const parent = wrapper.node.parent as Element;
const raw = parent && (
parent.name === 'script' ||
parent.name === 'style'
);
return raw
? wrapper.node.data
: escape_html(wrapper.node.data)
.replace(/\\/g, '\\\\')
.replace(/`/g, '\\`')
.replace(/\$/g, '\\$');
}
if (wrapper.node.name === 'noscript') return '';
let open = `<${wrapper.node.name}`;
(wrapper as ElementWrapper).attributes.forEach((attr: AttributeWrapper) => {
open += ` ${fix_attribute_casing(attr.node.name)}${attr.stringify()}`;
});
if (is_void(wrapper.node.name)) return open + '>';
return `${open}>${(wrapper as ElementWrapper).fragment.nodes.map(to_html).join('')}</${wrapper.node.name}>`;
}
if (renderer.options.dev) { if (renderer.options.dev) {
const loc = renderer.locate(this.node.start); const loc = renderer.locate(this.node.start);
block.builders.hydrate.add_line( block.builders.hydrate.add_line(
@ -440,7 +440,7 @@ export default class ElementWrapper extends Wrapper {
binding.render(block, lock); binding.render(block, lock);
}); });
// media bindings — awkward special case. The native timeupdate events // media bindings awkward special case. The native timeupdate events
// fire too infrequently, so we need to take matters into our // fire too infrequently, so we need to take matters into our
// own hands // own hands
let animation_frame; let animation_frame;
@ -453,7 +453,7 @@ export default class ElementWrapper extends Wrapper {
let callee; let callee;
// TODO dry this out — similar code for event handlers and component bindings // TODO dry this out similar code for event handlers and component bindings
if (has_local_function) { if (has_local_function) {
// need to create a block-local function that calls an instance-level function // need to create a block-local function that calls an instance-level function
block.builders.init.add_block(deindent` block.builders.init.add_block(deindent`

@ -429,7 +429,7 @@ export default class IfBlockWrapper extends Wrapper {
} }
`; `;
// no `p()` here — we don't want to update outroing nodes, // no `p()` here we don't want to update outroing nodes,
// as that will typically result in glitching // as that will typically result in glitching
const exit = branch.block.has_outro_method const exit = branch.block.has_outro_method
? deindent` ? deindent`

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

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

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

@ -3,44 +3,61 @@ import is_reference from 'is-reference';
import { Node } from '../../interfaces'; import { Node } from '../../interfaces';
import { Node as ESTreeNode } from 'estree'; import { Node as ESTreeNode } from 'estree';
const extractors = { export function create_scopes(expression: Node) {
Identifier(nodes: Node[], param: Node) { const map = new WeakMap();
nodes.push(param);
},
ObjectPattern(nodes: Node[], param: Node) { const globals: Map<string, Node> = new Map();
param.properties.forEach((prop: Node) => { let scope = new Scope(null, false);
if (prop.type === 'RestElement') {
nodes.push(prop.argument); walk(expression, {
enter(node, parent) {
if (node.type === 'ImportDeclaration') {
node.specifiers.forEach(specifier => {
scope.declarations.set(specifier.local.name, specifier);
});
} else if (/Function/.test(node.type)) {
if (node.type === 'FunctionDeclaration') {
scope.declarations.set(node.id.name, node);
scope = new Scope(scope, false);
map.set(node, scope);
} else { } else {
extractors[prop.value.type](nodes, prop.value); scope = new Scope(scope, false);
map.set(node, scope);
if (node.id) scope.declarations.set(node.id.name, node);
} }
});
},
ArrayPattern(nodes: Node[], param: Node) { node.params.forEach((param) => {
param.elements.forEach((element: Node) => { extract_names(param).forEach(name => {
if (element) extractors[element.type](nodes, element); scope.declarations.set(name, node);
}); });
});
} else if (/For(?:In|Of)?Statement/.test(node.type)) {
scope = new Scope(scope, true);
map.set(node, scope);
} else if (node.type === 'BlockStatement') {
scope = new Scope(scope, true);
map.set(node, scope);
} else if (/(Class|Variable)Declaration/.test(node.type)) {
scope.add_declaration(node);
} else if (node.type === 'Identifier' && is_reference(node as ESTreeNode, parent as ESTreeNode)) {
if (!scope.has(node.name) && !globals.has(node.name)) {
globals.set(node.name, node);
}
}
}, },
RestElement(nodes: Node[], param: Node) { leave(node: Node) {
extractors[param.argument.type](nodes, param.argument); if (map.has(node)) {
}, scope = scope.parent;
AssignmentPattern(nodes: Node[], param: Node) {
extractors[param.left.type](nodes, param.left);
} }
}; }
});
export function extract_identifiers(param: Node) { scope.declarations.forEach((_node, name) => {
const nodes: Node[] = []; globals.delete(name);
extractors[param.type] && extractors[param.type](nodes, param); });
return nodes;
}
export function extract_names(param: Node) { return { map, scope, globals };
return extract_identifiers(param).map(node => node.name);
} }
export class Scope { export class Scope {
@ -82,59 +99,42 @@ export class Scope {
} }
} }
export function create_scopes(expression: Node) { export function extract_names(param: Node) {
const map = new WeakMap(); return extract_identifiers(param).map(node => node.name);
}
const globals: Map<string, Node> = new Map(); export function extract_identifiers(param: Node) {
let scope = new Scope(null, false); const nodes: Node[] = [];
extractors[param.type] && extractors[param.type](nodes, param);
return nodes;
}
walk(expression, { const extractors = {
enter(node, parent) { Identifier(nodes: Node[], param: Node) {
if (node.type === 'ImportDeclaration') { nodes.push(param);
node.specifiers.forEach(specifier => { },
scope.declarations.set(specifier.local.name, specifier);
}); ObjectPattern(nodes: Node[], param: Node) {
} else if (/Function/.test(node.type)) { param.properties.forEach((prop: Node) => {
if (node.type === 'FunctionDeclaration') { if (prop.type === 'RestElement') {
scope.declarations.set(node.id.name, node); nodes.push(prop.argument);
scope = new Scope(scope, false);
map.set(node, scope);
} else { } else {
scope = new Scope(scope, false); extractors[prop.value.type](nodes, prop.value);
map.set(node, scope);
if (node.id) scope.declarations.set(node.id.name, node);
} }
node.params.forEach((param) => {
extract_names(param).forEach(name => {
scope.declarations.set(name, node);
});
}); });
} else if (/For(?:In|Of)?Statement/.test(node.type)) {
scope = new Scope(scope, true);
map.set(node, scope);
} else if (node.type === 'BlockStatement') {
scope = new Scope(scope, true);
map.set(node, scope);
} else if (/(Class|Variable)Declaration/.test(node.type)) {
scope.add_declaration(node);
} else if (node.type === 'Identifier' && is_reference(node as ESTreeNode, parent as ESTreeNode)) {
if (!scope.has(node.name) && !globals.has(node.name)) {
globals.set(node.name, node);
}
}
}, },
leave(node: Node) { ArrayPattern(nodes: Node[], param: Node) {
if (map.has(node)) { param.elements.forEach((element: Node) => {
scope = scope.parent; if (element) extractors[element.type](nodes, element);
}
},
}); });
},
scope.declarations.forEach((_node, name) => { RestElement(nodes: Node[], param: Node) {
globals.delete(name); extractors[param.argument.type](nodes, param.argument);
}); },
return { map, scope, globals }; AssignmentPattern(nodes: Node[], param: Node) {
} extractors[param.left.type](nodes, param.left);
}
};

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

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

@ -77,6 +77,190 @@ function parent_is_head(stack) {
return false; return false;
} }
export default function tag(parser: Parser) {
const start = parser.index++;
let parent = parser.current();
if (parser.eat('!--')) {
const data = parser.read_until(/-->/);
parser.eat('-->', true, 'comment was left open, expected -->');
parser.current().children.push({
start,
end: parser.index,
type: 'Comment',
data,
});
return;
}
const is_closing_tag = parser.eat('/');
const name = read_tag_name(parser);
if (meta_tags.has(name)) {
const slug = meta_tags.get(name).toLowerCase();
if (is_closing_tag) {
if (
(name === 'svelte:window' || name === 'svelte:body') &&
parser.current().children.length
) {
parser.error({
code: `invalid-${name.slice(7)}-content`,
message: `<${name}> cannot have children`
}, parser.current().children[0].start);
}
} else {
if (name in parser.meta_tags) {
parser.error({
code: `duplicate-${slug}`,
message: `A component can only have one <${name}> tag`
}, start);
}
if (parser.stack.length > 1) {
parser.error({
code: `invalid-${slug}-placement`,
message: `<${name}> tags cannot be inside elements or blocks`
}, start);
}
parser.meta_tags[name] = true;
}
}
const type = meta_tags.has(name)
? meta_tags.get(name)
: (/[A-Z]/.test(name[0]) || name === 'svelte:self' || name === 'svelte:component') ? 'InlineComponent'
: name === 'title' && parent_is_head(parser.stack) ? 'Title'
: name === 'slot' && !parser.customElement ? 'Slot' : 'Element';
const element: Node = {
start,
end: null, // filled in later
type,
name,
attributes: [],
children: [],
};
parser.allow_whitespace();
if (is_closing_tag) {
if (is_void(name)) {
parser.error({
code: `invalid-void-content`,
message: `<${name}> is a void element and cannot have children, or a closing tag`
}, start);
}
parser.eat('>', true);
// close any elements that don't have their own closing tags, e.g. <div><p></div>
while (parent.name !== name) {
if (parent.type !== 'Element')
parser.error({
code: `invalid-closing-tag`,
message: `</${name}> attempted to close an element that was not open`
}, start);
parent.end = start;
parser.stack.pop();
parent = parser.current();
}
parent.end = parser.index;
parser.stack.pop();
return;
} else if (disallowed_contents.has(parent.name)) {
// can this be a child of the parent element, or does it implicitly
// close it, like `<li>one<li>two`?
if (disallowed_contents.get(parent.name).has(name)) {
parent.end = start;
parser.stack.pop();
}
}
const unique_names = new Set();
let attribute;
while ((attribute = read_attribute(parser, unique_names))) {
element.attributes.push(attribute);
parser.allow_whitespace();
}
if (name === 'svelte:component') {
const index = element.attributes.findIndex(attr => attr.type === 'Attribute' && attr.name === 'this');
if (!~index) {
parser.error({
code: `missing-component-definition`,
message: `<svelte:component> must have a 'this' attribute`
}, start);
}
const definition = element.attributes.splice(index, 1)[0];
if (definition.value === true || definition.value.length !== 1 || definition.value[0].type === 'Text') {
parser.error({
code: `invalid-component-definition`,
message: `invalid component definition`
}, definition.start);
}
element.expression = definition.value[0].expression;
}
// special cases top-level <script> and <style>
if (specials.has(name) && parser.stack.length === 1) {
const special = specials.get(name);
parser.eat('>', true);
const content = special.read(parser, start, element.attributes);
if (content) parser[special.property].push(content);
return;
}
parser.current().children.push(element);
const self_closing = parser.eat('/') || is_void(name);
parser.eat('>', true);
if (self_closing) {
// don't push self-closing elements onto the stack
element.end = parser.index;
} else if (name === 'textarea') {
// special case
element.children = read_sequence(
parser,
() =>
parser.template.slice(parser.index, parser.index + 11) === '</textarea>'
);
parser.read(/<\/textarea>/);
element.end = parser.index;
} else if (name === 'script') {
// special case
const start = parser.index;
const data = parser.read_until(/<\/script>/);
const end = parser.index;
element.children.push({ start, end, type: 'Text', data });
parser.eat('</script>', true);
element.end = parser.index;
} else if (name === 'style') {
// special case
const start = parser.index;
const data = parser.read_until(/<\/style>/);
const end = parser.index;
element.children.push({ start, end, type: 'Text', data });
parser.eat('</style>', true);
} else {
parser.stack.push(element);
}
}
function read_tag_name(parser: Parser) { function read_tag_name(parser: Parser) {
const start = parser.index; const start = parser.index;
@ -132,90 +316,6 @@ function read_tag_name(parser: Parser) {
return name; return name;
} }
function get_directive_type(name: string): DirectiveType {
if (name === 'use') return 'Action';
if (name === 'animate') return 'Animation';
if (name === 'bind') return 'Binding';
if (name === 'class') return 'Class';
if (name === 'on') return 'EventHandler';
if (name === 'let') return 'Let';
if (name === 'ref') return 'Ref';
if (name === 'in' || name === 'out' || name === 'transition') return 'Transition';
}
function read_sequence(parser: Parser, done: () => boolean): Node[] {
let current_chunk: Text = {
start: parser.index,
end: null,
type: 'Text',
raw: '',
data: null
};
const chunks: Node[] = [];
function flush() {
if (current_chunk.raw) {
current_chunk.data = decode_character_references(current_chunk.raw);
current_chunk.end = parser.index;
chunks.push(current_chunk);
}
}
while (parser.index < parser.template.length) {
const index = parser.index;
if (done()) {
flush();
return chunks;
} else if (parser.eat('{')) {
flush();
parser.allow_whitespace();
const expression = read_expression(parser);
parser.allow_whitespace();
parser.eat('}', true);
chunks.push({
start: index,
end: parser.index,
type: 'MustacheTag',
expression,
});
current_chunk = {
start: parser.index,
end: null,
type: 'Text',
raw: '',
data: null
};
} else {
current_chunk.raw += parser.template[parser.index++];
}
}
parser.error({
code: `unexpected-eof`,
message: `Unexpected end of input`
});
}
function read_attribute_value(parser: Parser) {
const quote_mark = parser.eat(`'`) ? `'` : parser.eat(`"`) ? `"` : null;
const regex = (
quote_mark === `'` ? /'/ :
quote_mark === `"` ? /"/ :
/(\/>|[\s"'=<>`])/
);
const value = read_sequence(parser, () => !!parser.match_regex(regex));
if (quote_mark) parser.index += 1;
return value;
}
function read_attribute(parser: Parser, unique_names: Set<string>) { function read_attribute(parser: Parser, unique_names: Set<string>) {
const start = parser.index; const start = parser.index;
@ -349,186 +449,86 @@ function read_attribute(parser: Parser, unique_names: Set<string>) {
}; };
} }
export default function tag(parser: Parser) { function get_directive_type(name: string): DirectiveType {
const start = parser.index++; if (name === 'use') return 'Action';
if (name === 'animate') return 'Animation';
let parent = parser.current(); if (name === 'bind') return 'Binding';
if (name === 'class') return 'Class';
if (parser.eat('!--')) { if (name === 'on') return 'EventHandler';
const data = parser.read_until(/-->/); if (name === 'let') return 'Let';
parser.eat('-->', true, 'comment was left open, expected -->'); if (name === 'ref') return 'Ref';
if (name === 'in' || name === 'out' || name === 'transition') return 'Transition';
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)) { function read_attribute_value(parser: Parser) {
const slug = meta_tags.get(name).toLowerCase(); const quote_mark = parser.eat(`'`) ? `'` : parser.eat(`"`) ? `"` : null;
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) { const regex = (
parser.error({ quote_mark === `'` ? /'/ :
code: `invalid-${slug}-placement`, quote_mark === `"` ? /"/ :
message: `<${name}> tags cannot be inside elements or blocks` /(\/>|[\s"'=<>`])/
}, start); );
}
parser.meta_tags[name] = true; const value = read_sequence(parser, () => !!parser.match_regex(regex));
}
}
const type = meta_tags.has(name) if (quote_mark) parser.index += 1;
? meta_tags.get(name) return value;
: (/[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 = { function read_sequence(parser: Parser, done: () => boolean): Node[] {
start, let current_chunk: Text = {
end: null, // filled in later start: parser.index,
type, end: null,
name, type: 'Text',
attributes: [], raw: '',
children: [], data: null
}; };
parser.allow_whitespace(); function flush() {
if (current_chunk.raw) {
if (is_closing_tag) { current_chunk.data = decode_character_references(current_chunk.raw);
if (is_void(name)) { current_chunk.end = parser.index;
parser.error({ chunks.push(current_chunk);
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; const chunks: Node[] = [];
parser.stack.pop();
return; while (parser.index < parser.template.length) {
} else if (disallowed_contents.has(parent.name)) { const index = parser.index;
// 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(); if (done()) {
flush();
return chunks;
} else if (parser.eat('{')) {
flush();
let attribute;
while ((attribute = read_attribute(parser, unique_names))) {
element.attributes.push(attribute);
parser.allow_whitespace(); parser.allow_whitespace();
} const expression = read_expression(parser);
parser.allow_whitespace();
if (name === 'svelte:component') { parser.eat('}', true);
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]; chunks.push({
if (definition.value === true || definition.value.length !== 1 || definition.value[0].type === 'Text') { start: index,
parser.error({ end: parser.index,
code: `invalid-component-definition`, type: 'MustacheTag',
message: `invalid component definition` expression,
}, definition.start); });
}
element.expression = definition.value[0].expression; current_chunk = {
start: parser.index,
end: null,
type: 'Text',
raw: '',
data: null
};
} else {
current_chunk.raw += parser.template[parser.index++];
} }
// 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); parser.error({
code: `unexpected-eof`,
const self_closing = parser.eat('/') || is_void(name); message: `Unexpected end of input`
});
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);
}
} }

@ -1,6 +1,5 @@
import entities from './entities'; import entities from './entities';
const NUL = 0;
const windows_1252 = [ const windows_1252 = [
8364, 8364,
129, 129,
@ -41,6 +40,29 @@ const entity_pattern = new RegExp(
'g' 'g'
); );
export function decode_character_references(html: string) {
return html.replace(entity_pattern, (match, entity) => {
let code;
// Handle named entities
if (entity[0] !== '#') {
code = entities[entity];
} else if (entity[1] === 'x') {
code = parseInt(entity.substring(2), 16);
} else {
code = parseInt(entity.substring(1), 10);
}
if (!code) {
return match;
}
return String.fromCodePoint(validate_code(code));
});
}
const NUL = 0;
// some code points are verboten. If we were inserting HTML, the browser would replace the illegal // some code points are verboten. If we were inserting HTML, the browser would replace the illegal
// code points with alternatives in some cases - since we're bypassing that mechanism, we need // code points with alternatives in some cases - since we're bypassing that mechanism, we need
// to replace them ourselves // to replace them ourselves
@ -90,24 +112,3 @@ function validate_code(code: number) {
return NUL; return NUL;
} }
export function decode_character_references(html: string) {
return html.replace(entity_pattern, (match, entity) => {
let code;
// Handle named entities
if (entity[0] !== '#') {
code = entities[entity];
} else if (entity[1] === 'x') {
code = parseInt(entity.substring(2), 16);
} else {
code = parseInt(entity.substring(1), 10);
}
if (!code) {
return match;
}
return String.fromCodePoint(validate_code(code));
});
}

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

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

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

@ -56,12 +56,6 @@ export function subscribe(component, store, callback) {
: unsub); : unsub);
} }
export function get_slot_context(definition, ctx, fn) {
return definition[1]
? assign({}, assign(ctx.$$scope.ctx, definition[1](fn ? fn(ctx) : {})))
: ctx.$$scope.ctx;
}
export function create_slot(definition, ctx, fn) { export function create_slot(definition, ctx, fn) {
if (definition) { if (definition) {
const slot_ctx = get_slot_context(definition, ctx, fn); const slot_ctx = get_slot_context(definition, ctx, fn);
@ -69,6 +63,12 @@ export function create_slot(definition, ctx, fn) {
} }
} }
export function get_slot_context(definition, ctx, fn) {
return definition[1]
? assign({}, assign(ctx.$$scope.ctx, definition[1](fn ? fn(ctx) : {})))
: ctx.$$scope.ctx;
}
export function get_slot_changes(definition, ctx, changed, fn) { export function get_slot_changes(definition, ctx, changed, fn) {
return definition[1] return definition[1]
? assign({}, assign(ctx.$$scope.changed || {}, definition[1](fn ? fn(changed) : {}))) ? assign({}, assign(ctx.$$scope.changed || {}, definition[1](fn ? fn(changed) : {})))

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

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

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

Loading…
Cancel
Save