diff --git a/.eslintrc.json b/.eslintrc.json index 649b8dc46b..bc3786f5c0 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -23,6 +23,7 @@ "no-inner-declarations": 0, "@typescript-eslint/indent": [2, "tab", { "SwitchCase": 1 }], "@typescript-eslint/camelcase": "off", + "@typescript-eslint/no-use-before-define": "off", "@typescript-eslint/array-type": ["error", "array-simple"], "@typescript-eslint/explicit-function-return-type": "off", "@typescript-eslint/no-explicit-any": "off", diff --git a/src/compiler/compile/Component.ts b/src/compiler/compile/Component.ts index c8ac4cf7a6..89c1e6b4f6 100644 --- a/src/compiler/compile/Component.ts +++ b/src/compiler/compile/Component.ts @@ -67,120 +67,6 @@ 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: ` unknown attribute` - }); - } - } - - else { - component.error(attribute, { - code: `invalid-options-attribute`, - message: ` can only have static 'tag', 'namespace', 'accessors', 'immutable' and 'preserveWhitespace' attributes` - }); - } - }); - } - - return component_options; -} - export default class Component { stats: Stats; 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: ` unknown attribute` + }); + } + } + + else { + component.error(attribute, { + code: `invalid-options-attribute`, + message: ` can only have static 'tag', 'namespace', 'accessors', 'immutable' and 'preserveWhitespace' attributes` + }); + } + }); + } + + return component_options; +} diff --git a/src/compiler/compile/create_module.ts b/src/compiler/compile/create_module.ts index 4b90a7aa6f..54b891d4ac 100644 --- a/src/compiler/compile/create_module.ts +++ b/src/compiler/compile/create_module.ts @@ -3,11 +3,35 @@ import list from '../utils/list'; import { ModuleFormat, Node } from '../interfaces'; import { stringify_props } from './utils/stringify_props'; +const wrappers = { esm, cjs }; + interface Export { name: 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) { return source === 'svelte' || source.startsWith('svelte/') ? source.replace('svelte', sveltePath) @@ -109,27 +133,3 @@ function cjs( ${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))})`); -} diff --git a/src/compiler/compile/css/Selector.ts b/src/compiler/compile/css/Selector.ts index 057ac482fd..bb4a29ebe3 100644 --- a/src/compiler/compile/css/Selector.ts +++ b/src/compiler/compile/css/Selector.ts @@ -4,102 +4,121 @@ import { gather_possible_values, UNKNOWN } from './gather_possible_values'; import { Node } from '../../interfaces'; import Component from '../Component'; -class Block { - global: boolean; - combinator: Node; - selectors: Node[] - start: number; - end: number; - should_encapsulate: boolean; - - constructor(combinator: Node) { - this.combinator = combinator; - this.global = false; - this.selectors = []; +export default class Selector { + node: Node; + stylesheet: Stylesheet; + blocks: Block[]; + local_blocks: Block[]; + used: boolean; - this.start = null; - this.end = null; + constructor(node: Node, stylesheet: Stylesheet) { + this.node = node; + this.stylesheet = stylesheet; - this.should_encapsulate = false; - } + this.blocks = group_selectors(node); - add(selector: Node) { - if (this.selectors.length === 0) { - this.start = selector.start; - this.global = selector.type === 'PseudoClassSelector' && selector.name === 'global'; + // 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.selectors.push(selector); - this.end = selector.end; + this.local_blocks = this.blocks.slice(0, i); + this.used = this.blocks[0].global; } -} -function group_selectors(selector: Node) { - let block: Block = new Block(null); + apply(node: Node, stack: Node[]) { + const to_encapsulate: Node[] = []; - const blocks = [block]; + apply_selector(this.stylesheet, this.local_blocks.slice(), node, stack.slice(), to_encapsulate); - selector.children.forEach((child: Node) => { - if (child.type === 'WhiteSpace' || child.type === 'Combinator') { - block = new Block(child); - blocks.push(block); - } else { - block.add(child); - } - }); + 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; + }); - return blocks; -} + this.used = 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) -}; + 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 || ' '); + } + } -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; + c = block.end; + }); + } - 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; + 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 pattern = operators[operator](expected_value, case_insensitive ? 'i' : ''); - const value = attr.chunks[0]; + if (selector.type === 'TypeSelector' && selector.name === '*') { + code.overwrite(selector.start, selector.end, attr); + } else { + code.appendLeft(selector.end, attr); + } - if (!value) return false; - if (value.type === 'Text') return pattern.test(value.data); + break; + } + } - const possible_values = new Set(); - gather_possible_values(value.node, possible_values); - if (possible_values.has(UNKNOWN)) return true; + 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); + } - for (const x of Array.from(possible_values)) { // TypeScript for-of is slightly unlike JS - if (pattern.test(x)) return true; + if (block.should_encapsulate) encapsulate_block(block); + }); } - return false; -} + 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` + }); + } + } + }); -function class_matches(node, name: string) { - return node.classes.some((class_directive) => { - return new RegExp(`\\b${name}\\b`).test(class_directive.name); - }); -} + let start = 0; + let end = this.blocks.length; -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); + for (; start < end; start += 1) { + if (!this.blocks[start].global) break; + } + + 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` + }); + } + } } - return str; } 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; } -export default class Selector { - node: Node; - stylesheet: Stylesheet; - blocks: Block[]; - local_blocks: Block[]; - used: boolean; +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) +}; - constructor(node: Node, stylesheet: Stylesheet) { - this.node = node; - this.stylesheet = stylesheet; +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; - this.blocks = group_selectors(node); + 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; - // take trailing :global(...) selectors out of consideration - let i = this.blocks.length; - while (i > 0) { - if (!this.blocks[i - 1].global) break; - i -= 1; - } + const pattern = operators[operator](expected_value, case_insensitive ? 'i' : ''); + const value = attr.chunks[0]; - this.local_blocks = this.blocks.slice(0, i); - this.used = this.blocks[0].global; - } + if (!value) return false; + if (value.type === 'Text') return pattern.test(value.data); - apply(node: Node, stack: Node[]) { - const to_encapsulate: Node[] = []; + const possible_values = new Set(); + gather_possible_values(value.node, possible_values); + if (possible_values.has(UNKNOWN)) return true; - apply_selector(this.stylesheet, this.local_blocks.slice(), node, stack.slice(), to_encapsulate); + for (const x of Array.from(possible_values)) { // TypeScript for-of is slightly unlike JS + if (pattern.test(x)) return true; + } - 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; - }); + return false; +} - this.used = true; - } +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; +} - 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 || ' '); - } - } +class Block { + global: boolean; + combinator: Node; + selectors: Node[] + start: number; + end: number; + should_encapsulate: boolean; - c = block.end; - }); - } + constructor(combinator: Node) { + this.combinator = combinator; + this.global = false; + this.selectors = []; - 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.start = null; + this.end = null; - if (selector.type === 'TypeSelector' && selector.name === '*') { - code.overwrite(selector.start, selector.end, attr); - } else { - code.appendLeft(selector.end, attr); - } + this.should_encapsulate = false; + } - break; - } + add(selector: Node) { + if (this.selectors.length === 0) { + this.start = selector.start; + this.global = selector.type === 'PseudoClassSelector' && selector.name === 'global'; } - 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); - }); + this.selectors.push(selector); + this.end = selector.end; } +} - 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` - }); - } - } - }); - - let start = 0; - let end = this.blocks.length; +function group_selectors(selector: Node) { + let block: Block = new Block(null); - for (; start < end; start += 1) { - if (!this.blocks[start].global) break; - } + const blocks = [block]; - for (; end > start; end -= 1) { - if (!this.blocks[end - 1].global) break; + selector.children.forEach((child: Node) => { + 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; } diff --git a/src/compiler/compile/css/Stylesheet.ts b/src/compiler/compile/css/Stylesheet.ts index 90c5153ea5..2aead635b2 100644 --- a/src/compiler/compile/css/Stylesheet.ts +++ b/src/compiler/compile/css/Stylesheet.ts @@ -20,44 +20,6 @@ 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) { - 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[]; @@ -138,6 +100,44 @@ class Rule { } } +class Declaration { + node: Node; + + constructor(node: Node) { + this.node = node; + } + + transform(code: MagicString, keyframes: Map) { + 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: Array; diff --git a/src/compiler/compile/nodes/Element.ts b/src/compiler/compile/nodes/Element.ts index b76ca4cc00..da5f67a55a 100644 --- a/src/compiler/compile/nodes/Element.ts +++ b/src/compiler/compile/nodes/Element.ts @@ -89,22 +89,6 @@ 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; @@ -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` + }); +} diff --git a/src/compiler/compile/nodes/Transition.ts b/src/compiler/compile/nodes/Transition.ts index 8335b18ab9..82eb578f0f 100644 --- a/src/compiler/compile/nodes/Transition.ts +++ b/src/compiler/compile/nodes/Transition.ts @@ -2,12 +2,6 @@ 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; @@ -44,3 +38,9 @@ export default class Transition extends Node { : null; } } + +function describe(transition: Transition) { + return transition.directive === 'transition' + ? `a 'transition'` + : `an '${transition.directive}'`; +} \ No newline at end of file diff --git a/src/compiler/compile/nodes/shared/Expression.ts b/src/compiler/compile/nodes/shared/Expression.ts index d29e5abe58..004ae716b9 100644 --- a/src/compiler/compile/nodes/shared/Expression.ts +++ b/src/compiler/compile/nodes/shared/Expression.ts @@ -64,33 +64,6 @@ const precedence: Record 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; @@ -516,3 +489,30 @@ export default class Expression { 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; +} diff --git a/src/compiler/compile/nodes/shared/map_children.ts b/src/compiler/compile/nodes/shared/map_children.ts index 9be4cbc47a..71d764a889 100644 --- a/src/compiler/compile/nodes/shared/map_children.ts +++ b/src/compiler/compile/nodes/shared/map_children.ts @@ -16,6 +16,8 @@ import Title from '../Title'; import Window from '../Window'; import { Node } from '../../../interfaces'; +export type Children = ReturnType; + function get_constructor(type) { switch (type) { case 'AwaitBlock': return AwaitBlock; @@ -51,5 +53,3 @@ export default function map_children(component, parent, scope, children: Node[]) return node; }); } - -export type Children = ReturnType; diff --git a/src/compiler/compile/render-dom/wrappers/Element/Attribute.ts b/src/compiler/compile/render-dom/wrappers/Element/Attribute.ts index a7dbc8d71b..669a3774e3 100644 --- a/src/compiler/compile/render-dom/wrappers/Element/Attribute.ts +++ b/src/compiler/compile/render-dom/wrappers/Element/Attribute.ts @@ -6,6 +6,224 @@ 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 —