diff --git a/src/compiler/compile/Component.ts b/src/compiler/compile/Component.ts index 9fb41b6108c..b9d78ae7bd3 100644 --- a/src/compiler/compile/Component.ts +++ b/src/compiler/compile/Component.ts @@ -38,6 +38,7 @@ import compiler_warnings from './compiler_warnings'; import compiler_errors from './compiler_errors'; import { extract_ignores_above_position, extract_svelte_ignore_from_comments } from '../utils/extract_svelte_ignore'; import check_enable_sourcemap from './utils/check_enable_sourcemap'; +import Tag from './nodes/shared/Tag'; interface ComponentOptions { namespace?: string; @@ -110,6 +111,8 @@ export default class Component { slots: Map = new Map(); slot_outlets: Set = new Set(); + tags: Tag[] = []; + constructor( ast: Ast, source: string, @@ -761,6 +764,7 @@ export default class Component { this.hoist_instance_declarations(); this.extract_reactive_declarations(); + this.check_if_tags_content_dynamic(); } post_template_walk() { @@ -1479,6 +1483,12 @@ export default class Component { unsorted_reactive_declarations.forEach(add_declaration); } + check_if_tags_content_dynamic() { + this.tags.forEach(tag => { + tag.check_if_content_dynamic(); + }); + } + warn_if_undefined(name: string, node, template_scope: TemplateScope) { if (name[0] === '$') { if (name === '$' || name[1] === '$' && !is_reserved_keyword(name)) { diff --git a/src/compiler/compile/nodes/Attribute.ts b/src/compiler/compile/nodes/Attribute.ts index ebed483288a..9e14a3233eb 100644 --- a/src/compiler/compile/nodes/Attribute.ts +++ b/src/compiler/compile/nodes/Attribute.ts @@ -59,6 +59,11 @@ export default class Attribute extends Node { return expression; }); } + + if (this.dependencies.size > 0) { + parent.cannot_use_innerhtml(); + parent.not_static_content(); + } } get_dependencies() { diff --git a/src/compiler/compile/nodes/AwaitBlock.ts b/src/compiler/compile/nodes/AwaitBlock.ts index 4a669b6365b..b61beab478b 100644 --- a/src/compiler/compile/nodes/AwaitBlock.ts +++ b/src/compiler/compile/nodes/AwaitBlock.ts @@ -27,6 +27,8 @@ export default class AwaitBlock extends Node { constructor(component: Component, parent: Node, scope: TemplateScope, info: TemplateNode) { super(component, parent, scope, info); + this.cannot_use_innerhtml(); + this.not_static_content(); this.expression = new Expression(component, this, scope, info.expression); diff --git a/src/compiler/compile/nodes/EachBlock.ts b/src/compiler/compile/nodes/EachBlock.ts index 4659a53d73c..b78f7033765 100644 --- a/src/compiler/compile/nodes/EachBlock.ts +++ b/src/compiler/compile/nodes/EachBlock.ts @@ -33,6 +33,8 @@ export default class EachBlock extends AbstractBlock { constructor(component: Component, parent: Node, scope: TemplateScope, info: TemplateNode) { super(component, parent, scope, info); + this.cannot_use_innerhtml(); + this.not_static_content(); this.expression = new Expression(component, this, scope, info.expression); this.context = info.context.name || 'each'; // TODO this is used to facilitate binding; currently fails with destructuring diff --git a/src/compiler/compile/nodes/Element.ts b/src/compiler/compile/nodes/Element.ts index ee62d3e7e8b..d38c299d36d 100644 --- a/src/compiler/compile/nodes/Element.ts +++ b/src/compiler/compile/nodes/Element.ts @@ -15,6 +15,7 @@ import { is_name_contenteditable, get_contenteditable_attr, has_contenteditable_ import { regex_dimensions, regex_starts_with_newline, regex_non_whitespace_character, regex_box_size } from '../../utils/patterns'; import fuzzymatch from '../../utils/fuzzymatch'; import list from '../../utils/list'; +import hash from '../utils/hash'; import Let from './Let'; import TemplateScope from './shared/TemplateScope'; import { INode } from './interfaces'; @@ -503,6 +504,25 @@ export default class Element extends Node { this.optimise(); component.apply_stylesheet(this); + + if (this.parent) { + if (this.actions.length > 0 || + this.animation || + this.bindings.length > 0 || + this.classes.length > 0 || + this.intro || this.outro || + this.handlers.length > 0 || + this.styles.length > 0 || + this.name === 'option' || + this.is_dynamic_element || + this.tag_expr.dynamic_dependencies().length || + this.is_dynamic_element || + component.compile_options.dev + ) { + this.parent.cannot_use_innerhtml(); // need to use add_location + this.parent.not_static_content(); + } + } } validate() { @@ -1262,6 +1282,20 @@ export default class Element extends Node { } }); } + + get can_use_textcontent() { + return this.is_static_content && this.children.every(node => node.type === 'Text' || node.type === 'MustacheTag'); + } + + get can_optimise_to_html_string() { + const can_use_textcontent = this.can_use_textcontent; + const is_template_with_text_content = this.name === 'template' && can_use_textcontent; + return !is_template_with_text_content && !this.namespace && (this.can_use_innerhtml || can_use_textcontent) && this.children.length > 0; + } + + hash() { + return `svelte-${hash(this.component.source.slice(this.start, this.end))}`; + } } const regex_starts_with_vowel = /^[aeiou]/; diff --git a/src/compiler/compile/nodes/Head.ts b/src/compiler/compile/nodes/Head.ts index 1ebe0c053aa..83319092b3e 100644 --- a/src/compiler/compile/nodes/Head.ts +++ b/src/compiler/compile/nodes/Head.ts @@ -15,6 +15,8 @@ export default class Head extends Node { constructor(component: Component, parent: Node, scope: TemplateScope, info: TemplateNode) { super(component, parent, scope, info); + this.cannot_use_innerhtml(); + if (info.attributes.length) { component.error(info.attributes[0], compiler_errors.invalid_attribute_head); return; diff --git a/src/compiler/compile/nodes/IfBlock.ts b/src/compiler/compile/nodes/IfBlock.ts index 8351f1e0e27..4c337ac0cee 100644 --- a/src/compiler/compile/nodes/IfBlock.ts +++ b/src/compiler/compile/nodes/IfBlock.ts @@ -18,6 +18,8 @@ export default class IfBlock extends AbstractBlock { constructor(component: Component, parent: Node, scope: TemplateScope, info: TemplateNode) { super(component, parent, scope, info); this.scope = scope.child(); + this.cannot_use_innerhtml(); + this.not_static_content(); this.expression = new Expression(component, this, this.scope, info.expression); ([this.const_tags, this.children] = get_const_tags(info.children, component, this, this)); diff --git a/src/compiler/compile/nodes/InlineComponent.ts b/src/compiler/compile/nodes/InlineComponent.ts index e9ac86a55cf..5fc7c89ae44 100644 --- a/src/compiler/compile/nodes/InlineComponent.ts +++ b/src/compiler/compile/nodes/InlineComponent.ts @@ -28,6 +28,9 @@ export default class InlineComponent extends Node { constructor(component: Component, parent: Node, scope: TemplateScope, info: TemplateNode) { super(component, parent, scope, info); + this.cannot_use_innerhtml(); + this.not_static_content(); + if (info.name !== 'svelte:component' && info.name !== 'svelte:self') { const name = info.name.split('.')[0]; // accommodate namespaces component.warn_if_undefined(name, info, scope); diff --git a/src/compiler/compile/nodes/KeyBlock.ts b/src/compiler/compile/nodes/KeyBlock.ts index 5261e14e0a0..cd0ea56f8ea 100644 --- a/src/compiler/compile/nodes/KeyBlock.ts +++ b/src/compiler/compile/nodes/KeyBlock.ts @@ -13,6 +13,8 @@ export default class KeyBlock extends AbstractBlock { constructor(component: Component, parent: Node, scope: TemplateScope, info: TemplateNode) { super(component, parent, scope, info); + this.cannot_use_innerhtml(); + this.not_static_content(); this.expression = new Expression(component, this, scope, info.expression); diff --git a/src/compiler/compile/nodes/RawMustacheTag.ts b/src/compiler/compile/nodes/RawMustacheTag.ts index 816ef8d7c57..99aeba26238 100644 --- a/src/compiler/compile/nodes/RawMustacheTag.ts +++ b/src/compiler/compile/nodes/RawMustacheTag.ts @@ -2,4 +2,9 @@ import Tag from './shared/Tag'; export default class RawMustacheTag extends Tag { type: 'RawMustacheTag'; + constructor(component, parent, scope, info) { + super(component, parent, scope, info); + this.cannot_use_innerhtml(); + this.not_static_content(); + } } diff --git a/src/compiler/compile/nodes/Slot.ts b/src/compiler/compile/nodes/Slot.ts index c21a48f5d66..9a0b1ea815f 100644 --- a/src/compiler/compile/nodes/Slot.ts +++ b/src/compiler/compile/nodes/Slot.ts @@ -60,5 +60,8 @@ export default class Slot extends Element { } component.slots.set(this.slot_name, this); + + this.cannot_use_innerhtml(); + this.not_static_content(); } } diff --git a/src/compiler/compile/nodes/Text.ts b/src/compiler/compile/nodes/Text.ts index ac6cd9b8287..d750477c8a7 100644 --- a/src/compiler/compile/nodes/Text.ts +++ b/src/compiler/compile/nodes/Text.ts @@ -18,6 +18,7 @@ const elements_without_text = new Set([ ]); const regex_ends_with_svg = /svg$/; +const regex_non_whitespace_characters = /[\S\u00A0]/; export default class Text extends Node { type: 'Text'; @@ -63,4 +64,11 @@ export default class Text extends Node { return false; } + + use_space(): boolean { + if (this.component.compile_options.preserveWhitespace) return false; + if (regex_non_whitespace_characters.test(this.data)) return false; + + return !this.within_pre(); + } } diff --git a/src/compiler/compile/nodes/shared/Expression.ts b/src/compiler/compile/nodes/shared/Expression.ts index 65b10d04c82..900e78efc1f 100644 --- a/src/compiler/compile/nodes/shared/Expression.ts +++ b/src/compiler/compile/nodes/shared/Expression.ts @@ -197,6 +197,15 @@ export default class Expression { }); } + dynamic_contextual_dependencies() { + return Array.from(this.contextual_dependencies).filter(name => { + return Array.from(this.template_scope.dependencies_for_name.get(name)).some(variable_name => { + const variable = this.component.var_lookup.get(variable_name); + return is_dynamic(variable); + }); + }); + } + // TODO move this into a render-dom wrapper? manipulate(block?: Block, ctx?: string | void) { // TODO ideally we wouldn't end up calling this method diff --git a/src/compiler/compile/nodes/shared/Node.ts b/src/compiler/compile/nodes/shared/Node.ts index 3c39f6d9d34..9eeb110d201 100644 --- a/src/compiler/compile/nodes/shared/Node.ts +++ b/src/compiler/compile/nodes/shared/Node.ts @@ -15,6 +15,7 @@ export default class Node { next?: INode; can_use_innerhtml: boolean; + is_static_content: boolean; var: string; attributes: Attribute[]; @@ -33,6 +34,9 @@ export default class Node { value: parent } }); + + this.can_use_innerhtml = true; + this.is_static_content = true; } cannot_use_innerhtml() { @@ -42,6 +46,11 @@ export default class Node { } } + not_static_content() { + this.is_static_content = false; + if (this.parent) this.parent.not_static_content(); + } + find_nearest(selector: RegExp) { if (selector.test(this.type)) return this; if (this.parent) return this.parent.find_nearest(selector); diff --git a/src/compiler/compile/nodes/shared/Tag.ts b/src/compiler/compile/nodes/shared/Tag.ts index bc826458d9a..23d94ce01e0 100644 --- a/src/compiler/compile/nodes/shared/Tag.ts +++ b/src/compiler/compile/nodes/shared/Tag.ts @@ -8,6 +8,9 @@ export default class Tag extends Node { constructor(component, parent, scope, info) { super(component, parent, scope, info); + component.tags.push(this); + this.cannot_use_innerhtml(); + this.expression = new Expression(component, this, scope, info.expression); this.should_cache = ( @@ -15,4 +18,12 @@ export default class Tag extends Node { (this.expression.dependencies.size && scope.names.has(info.expression.name)) ); } + is_dependencies_static() { + return this.expression.dynamic_contextual_dependencies().length === 0 && this.expression.dynamic_dependencies().length === 0; + } + check_if_content_dynamic() { + if (!this.is_dependencies_static()) { + this.not_static_content(); + } + } } diff --git a/src/compiler/compile/render_dom/wrappers/AwaitBlock.ts b/src/compiler/compile/render_dom/wrappers/AwaitBlock.ts index 534595f4a9d..e6807621d27 100644 --- a/src/compiler/compile/render_dom/wrappers/AwaitBlock.ts +++ b/src/compiler/compile/render_dom/wrappers/AwaitBlock.ts @@ -143,9 +143,6 @@ export default class AwaitBlockWrapper extends Wrapper { ) { super(renderer, block, parent, node); - this.cannot_use_innerhtml(); - this.not_static_content(); - block.add_dependencies(this.node.expression.dependencies); let is_dynamic = false; diff --git a/src/compiler/compile/render_dom/wrappers/Comment.ts b/src/compiler/compile/render_dom/wrappers/Comment.ts index a8c63b4227b..9c4e2ffdfb6 100644 --- a/src/compiler/compile/render_dom/wrappers/Comment.ts +++ b/src/compiler/compile/render_dom/wrappers/Comment.ts @@ -38,4 +38,10 @@ export default class CommentWrapper extends Wrapper { parent_node ); } + + text() { + if (!this.renderer.options.preserveComments) return ''; + + return ``; + } } diff --git a/src/compiler/compile/render_dom/wrappers/EachBlock.ts b/src/compiler/compile/render_dom/wrappers/EachBlock.ts index 9c1af7ce44d..36f733caef9 100644 --- a/src/compiler/compile/render_dom/wrappers/EachBlock.ts +++ b/src/compiler/compile/render_dom/wrappers/EachBlock.ts @@ -80,8 +80,6 @@ export default class EachBlockWrapper extends Wrapper { next_sibling: Wrapper ) { super(renderer, block, parent, node); - this.cannot_use_innerhtml(); - this.not_static_content(); const { dependencies } = node.expression; block.add_dependencies(dependencies); diff --git a/src/compiler/compile/render_dom/wrappers/Element/Attribute.ts b/src/compiler/compile/render_dom/wrappers/Element/Attribute.ts index 61311fd83ce..4b77c808e4e 100644 --- a/src/compiler/compile/render_dom/wrappers/Element/Attribute.ts +++ b/src/compiler/compile/render_dom/wrappers/Element/Attribute.ts @@ -36,9 +36,6 @@ export class BaseAttributeWrapper { this.parent = parent; if (node.dependencies.size > 0) { - parent.cannot_use_innerhtml(); - parent.not_static_content(); - block.add_dependencies(node.dependencies); } } diff --git a/src/compiler/compile/render_dom/wrappers/Element/index.ts b/src/compiler/compile/render_dom/wrappers/Element/index.ts index cefebfcff35..9906c29e391 100644 --- a/src/compiler/compile/render_dom/wrappers/Element/index.ts +++ b/src/compiler/compile/render_dom/wrappers/Element/index.ts @@ -29,6 +29,7 @@ import is_dynamic from '../shared/is_dynamic'; import { is_name_contenteditable, has_contenteditable_attr } from '../../../utils/contenteditable'; import create_debugging_comment from '../shared/create_debugging_comment'; import { push_array } from '../../../../utils/push_array'; +import CommentWrapper from '../Comment'; interface BindingGroup { events: string[]; @@ -288,24 +289,6 @@ export default class ElementWrapper extends Wrapper { } }); - if (this.parent) { - if (node.actions.length > 0 || - node.animation || - node.bindings.length > 0 || - node.classes.length > 0 || - node.intro || node.outro || - node.handlers.length > 0 || - node.styles.length > 0 || - this.node.name === 'option' || - node.tag_expr.dynamic_dependencies().length || - node.is_dynamic_element || - renderer.options.dev - ) { - this.parent.cannot_use_innerhtml(); // need to use add_location - this.parent.not_static_content(); - } - } - this.fragment = new FragmentWrapper(renderer, block, node.children, this, strip_whitespace, next_sibling); this.element_data_name = block.get_unique_name(`${this.var.name}_data`); @@ -445,6 +428,7 @@ export default class ElementWrapper extends Wrapper { render_element(block: Block, parent_node: Identifier, parent_nodes: Identifier) { const { renderer } = this; + const hydratable = renderer.options.hydratable; if (this.node.name === 'noscript') return; @@ -458,13 +442,15 @@ export default class ElementWrapper extends Wrapper { b`${node} = ${render_statement};` ); - if (renderer.options.hydratable) { + const { can_use_textcontent, can_optimise_to_html_string } = this.node; + + if (hydratable) { if (parent_nodes) { block.chunks.claim.push(b` - ${node} = ${this.get_claim_statement(block, parent_nodes)}; + ${node} = ${this.get_claim_statement(block, parent_nodes, can_optimise_to_html_string)}; `); - if (!this.void && this.node.children.length > 0) { + if (!can_optimise_to_html_string && !this.void && this.node.children.length > 0) { block.chunks.claim.push(b` var ${nodes} = ${children}; `); @@ -502,15 +488,19 @@ export default class ElementWrapper extends Wrapper { // insert static children with textContent or innerHTML // skip textcontent for