From d731766f3488bab42f14ad264f7092b592b937e3 Mon Sep 17 00:00:00 2001 From: Richard Harris Date: Thu, 12 Sep 2019 21:53:33 -0400 Subject: [PATCH] attributes and innerHTML --- src/compiler/compile/Component.ts | 5 + src/compiler/compile/nodes/Attribute.ts | 8 +- .../render_dom/wrappers/Element/Attribute.ts | 51 ++++---- .../wrappers/Element/StyleAttribute.ts | 62 +++++----- .../render_dom/wrappers/Element/index.ts | 111 ++++++++++++------ .../render_dom/wrappers/shared/changed.ts | 7 ++ test/runtime/index.js | 1 + 7 files changed, 150 insertions(+), 95 deletions(-) create mode 100644 src/compiler/compile/render_dom/wrappers/shared/changed.ts diff --git a/src/compiler/compile/Component.ts b/src/compiler/compile/Component.ts index 7711fd2684..b6d5eb49ca 100644 --- a/src/compiler/compile/Component.ts +++ b/src/compiler/compile/Component.ts @@ -300,6 +300,11 @@ export default class Component { walk(program, { enter: (node) => { if (node.type === 'Identifier' && node.name[0] === '@') { + // TODO temp + if (!/@\w+$/.test(node.name)) { + throw new Error(`wut "${node.name}"`); + } + if (node.name[1] === '_') { const alias = this.global(node.name.slice(2)); node.name = alias.name; diff --git a/src/compiler/compile/nodes/Attribute.ts b/src/compiler/compile/nodes/Attribute.ts index 94dea766ad..92b6dde405 100644 --- a/src/compiler/compile/nodes/Attribute.ts +++ b/src/compiler/compile/nodes/Attribute.ts @@ -1,4 +1,4 @@ -import { stringify } from '../utils/stringify'; +import { stringify, string_literal } from '../utils/stringify'; import add_to_set from '../utils/add_to_set'; import Component from '../Component'; import Node from './shared/Node'; @@ -82,11 +82,9 @@ export default class Attribute extends Node { if (this.chunks.length === 0) return `""`; if (this.chunks.length === 1) { - return this.chunks[0].type === 'Text' - ? stringify((this.chunks[0] as Text).data) - // @ts-ignore todo: probably error - : this.chunks[0].render(block); + ? string_literal((this.chunks[0] as Text).data) + : (this.chunks[0] as Expression).manipulate(block); } return (this.chunks[0].type === 'Text' ? '' : `"" + `) + diff --git a/src/compiler/compile/render_dom/wrappers/Element/Attribute.ts b/src/compiler/compile/render_dom/wrappers/Element/Attribute.ts index 76da9f0de9..0af686e443 100644 --- a/src/compiler/compile/render_dom/wrappers/Element/Attribute.ts +++ b/src/compiler/compile/render_dom/wrappers/Element/Attribute.ts @@ -3,9 +3,10 @@ import Block from '../../Block'; import fix_attribute_casing from './fix_attribute_casing'; import ElementWrapper from './index'; import { stringify } from '../../../utils/stringify'; -import { b } from 'code-red'; +import { b, x } from 'code-red'; import Expression from '../../../nodes/shared/Expression'; import Text from '../../../nodes/Text'; +import { changed } from '../shared/changed'; export default class AttributeWrapper { node: Attribute; @@ -80,14 +81,14 @@ export default class AttributeWrapper { // single {tag} — may be a non-string value = (this.node.chunks[0] as Expression).manipulate(block); } else { - // '{foo} {bar}' — treat as string concatenation - const prefix = this.node.chunks[0].type === 'Text' ? '' : `"" + `; - - const text = this.node.name === 'class' + value = this.node.name === 'class' ? this.get_class_name_text() - : this.render_chunks().join(' + '); + : this.render_chunks().reduce((lhs, rhs) => x`${lhs} + ${rhs}`) - value = `${prefix}${text}`; + // '{foo} {bar}' — treat as string concatenation + if (this.node.chunks[0].type !== 'Text') { + value = x`"" + ${value}`; + } } const is_select_value_attribute = @@ -96,19 +97,19 @@ export default class AttributeWrapper { 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` + `${element.var.name}_${name.replace(/[^a-zA-Z_$]/g, '_')}_value` ); if (should_cache) block.add_variable(last); let updater; - const init = should_cache ? `${last} = ${value}` : value; + const init = should_cache ? x`${last} = ${value}` : value; if (is_legacy_input_type) { block.chunks.hydrate.push( b`@set_input_type(${element.var}, ${init});` ); - updater = `@set_input_type(${element.var}, ${should_cache ? last : value});`; + updater = b`@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'); @@ -141,27 +142,29 @@ export default class AttributeWrapper { b`${element.var}.${property_name} = ${init};` ); updater = block.renderer.options.dev - ? `@prop_dev(${element.var}, "${property_name}", ${should_cache ? last : value});` - : `${element.var}.${property_name} = ${should_cache ? last : value};`; + ? b`@prop_dev(${element.var}, "${property_name}", ${should_cache ? last : value});` + : b`${element.var}.${property_name} = ${should_cache ? last : value};`; } else { block.chunks.hydrate.push( b`${method}(${element.var}, "${name}", ${init});` ); - updater = `${method}(${element.var}, "${name}", ${should_cache ? last : value});`; + updater = b`${method}(${element.var}, "${name}", ${should_cache ? last : value});`; } - const changed_check = ( - (block.has_outros ? `!#current || ` : '') + - dependencies.map(dependency => `changed.${dependency}`).join(' || ') - ); + let condition = changed(dependencies); - const update_cached_value = `${last} !== (${last} = ${value})`; + if (should_cache) { + condition = x`${condition} && (${last} !== (${last} = ${value}))`; + } - const condition = should_cache - ? (dependencies.length ? `(${changed_check}) && ${update_cached_value}` : update_cached_value) - : changed_check; + if (block.has_outros) { + condition = x`!#current || ${condition}`; + } - block.chunks.update.push(b`if (${condition}) ${updater}`); + block.chunks.update.push(b` + if (${condition}) { + ${updater} + }`); } else { const value = this.node.get_value(block); @@ -195,10 +198,10 @@ export default class AttributeWrapper { if (scoped_css && rendered.length === 2) { // we have a situation like class={possiblyUndefined} - rendered[0] = `@null_to_empty(${rendered[0]})`; + rendered[0] = x`@null_to_empty(${rendered[0]})`; } - return rendered.join(' + '); + return rendered.reduce((lhs, rhs) => x`${lhs} + ${rhs}`); } render_chunks() { diff --git a/src/compiler/compile/render_dom/wrappers/Element/StyleAttribute.ts b/src/compiler/compile/render_dom/wrappers/Element/StyleAttribute.ts index 1bf8a341d4..4da35952dc 100644 --- a/src/compiler/compile/render_dom/wrappers/Element/StyleAttribute.ts +++ b/src/compiler/compile/render_dom/wrappers/Element/StyleAttribute.ts @@ -1,4 +1,4 @@ -import { b } from 'code-red'; +import { b, x } from 'code-red'; import Attribute from '../../../nodes/Attribute'; import Block from '../../Block'; import AttributeWrapper from './Attribute'; @@ -7,6 +7,7 @@ import { stringify } from '../../../utils/stringify'; import add_to_set from '../../../utils/add_to_set'; import Expression from '../../../nodes/shared/Expression'; import Text from '../../../nodes/Text'; +import { changed } from '../shared/changed'; export interface StyleProp { key: string; @@ -26,42 +27,45 @@ export default class StyleAttributeWrapper extends AttributeWrapper { 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.manipulate(); - - add_to_set(prop_dependencies, chunk.dynamic_dependencies()); - return snippet; - } - }) - .join(' + '); + const prop_dependencies: Set = new Set(); + + value = prop.value + .map(chunk => { + if (chunk.type === 'Text') { + return stringify(chunk.data); + } else { + const snippet = chunk.manipulate(block); + + add_to_set(prop_dependencies, chunk.dynamic_dependencies()); + return snippet; + } + }) + .reduce((lhs, rhs) => x`${lhs} + ${rhs}`) + + if (prop.value.length === 1 || prop.value[0].type === 'Text') { + value = x`"" + ${value}`; + } if (prop_dependencies.size) { - const dependencies = Array.from(prop_dependencies); - const condition = ( - (block.has_outros ? `!#current || ` : '') + - dependencies.map(dependency => `changed.${dependency}`).join(' || ') - ); - - block.chunks.update.push( - b`if (${condition}) { - @set_style(${this.parent.var}, "${prop.key}", ${value}${prop.important ? ', 1' : ''}); - }` - ); + let condition = changed(Array.from(prop_dependencies)); + + if (block.has_outros) { + condition = x`!#current || ${condition}`; + } + + const update = b` + if (${condition}) { + @set_style(${this.parent.var}, "${prop.key}", ${value}, ${prop.important ? 1 : null}); + }`; + + block.chunks.update.push(update); } } else { value = stringify((prop.value[0] as Text).data); } block.chunks.hydrate.push( - b`@set_style(${this.parent.var}, "${prop.key}", ${value}${prop.important ? ', 1' : ''});` + b`@set_style(${this.parent.var}, "${prop.key}", ${value}, ${prop.important ? 1 : ''});` ); }); } diff --git a/src/compiler/compile/render_dom/wrappers/Element/index.ts b/src/compiler/compile/render_dom/wrappers/Element/index.ts index f30fc912b6..48b749848f 100644 --- a/src/compiler/compile/render_dom/wrappers/Element/index.ts +++ b/src/compiler/compile/render_dom/wrappers/Element/index.ts @@ -4,7 +4,7 @@ import Wrapper from '../shared/Wrapper'; import Block from '../../Block'; import { is_void, quote_prop_if_necessary, quote_name_if_necessary, sanitize } from '../../../../utils/names'; import FragmentWrapper from '../Fragment'; -import { escape_html, escape, string_literal } from '../../../utils/stringify'; +import { escape_html, string_literal } from '../../../utils/stringify'; import TextWrapper from '../Text'; import fix_attribute_casing from './fix_attribute_casing'; import { b, x } from 'code-red'; @@ -294,14 +294,24 @@ export default class ElementWrapper extends Wrapper { b`${node}.textContent = ${string_literal(this.fragment.nodes[0].data)};` ); } else { - const inner_html = escape( - this.fragment.nodes - .map(to_html) - .join('') - ); + const state = { + quasi: { + type: 'TemplateElement', + value: { raw: '' } + } + }; + + const literal = { + type: 'TemplateLiteral', + expressions: [], + quasis: [] + }; + + to_html((this.fragment.nodes as unknown as (ElementWrapper | TextWrapper)[]), block, literal, state); + literal.quasis.push(state.quasi); block.chunks.create.push( - b`${node}.innerHTML = \`${inner_html}\`;` + b`${node}.innerHTML = ${literal};` ); } } else { @@ -338,36 +348,6 @@ export default class ElementWrapper extends Wrapper { ); } - function to_html(wrapper: ElementWrapper | TextWrapper) { - if (wrapper.node.type === 'Text') { - if ((wrapper as TextWrapper).use_space()) return ' '; - - 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('')}`; - } - if (renderer.options.dev) { const loc = renderer.locate(this.node.start); block.chunks.hydrate.push( @@ -836,3 +816,60 @@ export default class ElementWrapper extends Wrapper { }); } } + +function to_html(wrappers: (ElementWrapper | TextWrapper)[], block: Block, literal: any, state: any) { + wrappers.forEach(wrapper => { + if (wrapper.node.type === 'Text') { + if ((wrapper as TextWrapper).use_space()) state.quasi.value.raw += ' '; + + const parent = wrapper.node.parent as Element; + + const raw = parent && ( + parent.name === 'script' || + parent.name === 'style' + ); + + state.quasi.value.raw += (raw ? wrapper.node.data : escape_html(wrapper.node.data)) + .replace(/\\/g, '\\\\') + .replace(/`/g, '\\`') + .replace(/\$/g, '\\$'); + } + + else if (wrapper.node.name === 'noscript') { + // do nothing + } + + else { + // element + state.quasi.value.raw += `<${wrapper.node.name}`; + + (wrapper as ElementWrapper).attributes.forEach((attr: AttributeWrapper) => { + state.quasi.value.raw += ` ${fix_attribute_casing(attr.node.name)}="`; + + attr.node.chunks.forEach(chunk => { + if (chunk.type === 'Text') { + state.quasi.value.raw += chunk.data; + } else { + literal.quasis.push(state.quasi); + literal.expressions.push(chunk.manipulate(block)) + + state.quasi = { + type: 'TemplateElement', + value: { raw: '' } + }; + } + }); + + state.quasi.value.raw += `"`; + }); + + state.quasi.value.raw += '>'; + + if (!is_void(wrapper.node.name)) { + to_html((wrapper as ElementWrapper).fragment.nodes as (ElementWrapper | TextWrapper)[], block, literal, state); + + state.quasi.value.raw += ``; + } + } + }); +} \ No newline at end of file diff --git a/src/compiler/compile/render_dom/wrappers/shared/changed.ts b/src/compiler/compile/render_dom/wrappers/shared/changed.ts new file mode 100644 index 0000000000..b7dc12e53f --- /dev/null +++ b/src/compiler/compile/render_dom/wrappers/shared/changed.ts @@ -0,0 +1,7 @@ +import { x } from 'code-red'; + +export function changed(dependencies: string[]) { + return dependencies + .map(d => x`#changed.${d}`) + .reduce((lhs, rhs) => x`${lhs} || ${rhs}`); +} \ No newline at end of file diff --git a/test/runtime/index.js b/test/runtime/index.js index b08f7c88e0..c3583ddccf 100644 --- a/test/runtime/index.js +++ b/test/runtime/index.js @@ -65,6 +65,7 @@ describe.only("runtime", () => { unhandled_rejection = null; + config.preserveIdentifiers = true; // TODO remove later compile = (config.preserveIdentifiers ? svelte : svelte$).compile; const cwd = path.resolve(`test/runtime/samples/${dir}`);