From fc9b261c1e754bdba69e6a3fb25437f35e4e14e6 Mon Sep 17 00:00:00 2001 From: Richard Harris Date: Fri, 2 Aug 2019 07:49:03 -0400 Subject: [PATCH 1/4] failing test for #3285 --- test/runtime/samples/each-block-keyed-html/_config.js | 10 ++++++++++ test/runtime/samples/each-block-keyed-html/main.svelte | 8 ++++++++ 2 files changed, 18 insertions(+) create mode 100644 test/runtime/samples/each-block-keyed-html/_config.js create mode 100644 test/runtime/samples/each-block-keyed-html/main.svelte diff --git a/test/runtime/samples/each-block-keyed-html/_config.js b/test/runtime/samples/each-block-keyed-html/_config.js new file mode 100644 index 0000000000..536e54543e --- /dev/null +++ b/test/runtime/samples/each-block-keyed-html/_config.js @@ -0,0 +1,10 @@ +export default { + html: ` + JohnJill + `, + + test({ assert, component, target }) { + component.names = component.names.reverse(); + assert.htmlEqual(target.innerHTML, `JillJohn`); + } +}; diff --git a/test/runtime/samples/each-block-keyed-html/main.svelte b/test/runtime/samples/each-block-keyed-html/main.svelte new file mode 100644 index 0000000000..97c5f8c898 --- /dev/null +++ b/test/runtime/samples/each-block-keyed-html/main.svelte @@ -0,0 +1,8 @@ + + +{#each names as name (name)} + {@html name} +{/each} + From c226ba3d6503950e538007a55b08cbb218db47c4 Mon Sep 17 00:00:00 2001 From: Richard Harris Date: Sat, 3 Aug 2019 17:01:21 -0400 Subject: [PATCH 2/4] more robust handling of html tags - fixes #3285 --- .../render_dom/wrappers/RawMustacheTag.ts | 103 +++++------------- .../compile/render_dom/wrappers/shared/Tag.ts | 2 +- src/runtime/internal/dom.ts | 36 ++++++ .../each-block-changed-check/expected.js | 12 +- test/runtime/samples/raw-mustaches/_config.js | 8 +- 5 files changed, 72 insertions(+), 89 deletions(-) diff --git a/src/compiler/compile/render_dom/wrappers/RawMustacheTag.ts b/src/compiler/compile/render_dom/wrappers/RawMustacheTag.ts index f85c48935e..8b99861a2f 100644 --- a/src/compiler/compile/render_dom/wrappers/RawMustacheTag.ts +++ b/src/compiler/compile/render_dom/wrappers/RawMustacheTag.ts @@ -2,7 +2,6 @@ import Renderer from '../Renderer'; import Block from '../Block'; import Tag from './shared/Tag'; import Wrapper from './shared/Wrapper'; -import deindent from '../../utils/deindent'; import MustacheTag from '../../nodes/MustacheTag'; import RawMustacheTag from '../../nodes/RawMustacheTag'; @@ -19,95 +18,47 @@ export default class RawMustacheTagWrapper extends Tag { this.cannot_use_innerhtml(); } - render(block: Block, parent_node: string, parent_nodes: string) { - const name = this.var; - + render(block: Block, parent_node: string, _parent_nodes: string) { const in_head = parent_node === '@_document.head'; - const needs_anchors = !parent_node || in_head; - - // if in head always needs anchors - if (in_head) { - this.prev = null; - this.next = null; - } - // TODO use is_dom_node instead of type === 'Element'? - const needs_anchor_before = this.prev ? this.prev.node.type !== 'Element' : needs_anchors; - const needs_anchor_after = this.next ? this.next.node.type !== 'Element' : needs_anchors; + const can_use_innerhtml = !in_head && parent_node && !this.prev && !this.next; - const anchor_before = needs_anchor_before - ? block.get_unique_name(`${name}_before`) - : (this.prev && this.prev.var) || 'null'; + if (can_use_innerhtml) { + const insert = content => `${parent_node}.innerHTML = ${content};`; - const anchor_after = needs_anchor_after - ? block.get_unique_name(`${name}_after`) - : (this.next && this.next.var) || 'null'; - - let detach: string; - let insert: (content: string) => string; - let use_innerhtml = false; + const { init } = this.rename_this_method( + block, + content => insert(content) + ); - if (anchor_before === 'null' && anchor_after === 'null') { - use_innerhtml = true; - detach = `${parent_node}.innerHTML = '';`; - insert = content => `${parent_node}.innerHTML = ${content};`; - } else if (anchor_before === 'null') { - detach = `@detach_before(${anchor_after});`; - insert = content => `${anchor_after}.insertAdjacentHTML("beforebegin", ${content});`; - } else if (anchor_after === 'null') { - detach = `@detach_after(${anchor_before});`; - insert = content => `${anchor_before}.insertAdjacentHTML("afterend", ${content});`; - } else { - detach = `@detach_between(${anchor_before}, ${anchor_after});`; - insert = content => `${anchor_before}.insertAdjacentHTML("afterend", ${content});`; + block.builders.mount.add_line(insert(init)); } - const { init } = this.rename_this_method( - block, - content => deindent` - ${!use_innerhtml && detach} - ${insert(content)} - ` - ); + else { + const needs_anchor = in_head || (this.next && !this.next.is_dom_node()); - // we would have used comments here, but the `insertAdjacentHTML` api only - // exists for `Element`s. - if (needs_anchor_before) { - block.add_element( - anchor_before, - `@element('noscript')`, - parent_nodes && `@element('noscript')`, - parent_node, - true - ); - } + const html_tag = block.get_unique_name('html_tag'); + const html_anchor = needs_anchor && block.get_unique_name('html_anchor'); + + block.add_variable(html_tag); - function add_anchor_after() { - block.add_element( - anchor_after, - `@element('noscript')`, - parent_nodes && `@element('noscript')`, - parent_node + const { init } = this.rename_this_method( + block, + content => `${html_tag}.p(${content});` ); - } - if (needs_anchor_after && anchor_before === 'null') { - // anchor_after needs to be in the DOM before we - // insert the HTML... - add_anchor_after(); - } + const anchor = in_head ? 'null' : needs_anchor ? html_anchor : this.next ? this.next.var : 'null'; - block.builders.mount.add_line(insert(init)); + block.builders.hydrate.add_line(`${html_tag} = new @HtmlTag(${init}, ${anchor});`); + block.builders.mount.add_line(`${html_tag}.m(${parent_node || '#target'}, anchor);`); - if (needs_anchors) { - block.builders.destroy.add_conditional('detaching', needs_anchor_before - ? `${detach}\n@detach(${anchor_before});` - : detach); - } + if (needs_anchor) { + block.add_element(html_anchor, '@empty()', '@empty()', parent_node); + } - if (needs_anchor_after && anchor_before !== 'null') { - // ...otherwise it should go afterwards - add_anchor_after(); + if (!parent_node || in_head) { + block.builders.destroy.add_conditional('detaching', `${html_tag}.d();`); + } } } } diff --git a/src/compiler/compile/render_dom/wrappers/shared/Tag.ts b/src/compiler/compile/render_dom/wrappers/shared/Tag.ts index 27e5f1f03f..89567cbc30 100644 --- a/src/compiler/compile/render_dom/wrappers/shared/Tag.ts +++ b/src/compiler/compile/render_dom/wrappers/shared/Tag.ts @@ -34,7 +34,7 @@ export default class Tag extends Wrapper { const update_cached_value = `${value} !== (${value} = ${snippet})`; - const condition =this.node.should_cache + const condition = this.node.should_cache ? `(${changed_check}) && ${update_cached_value}` : changed_check; diff --git a/src/runtime/internal/dom.ts b/src/runtime/internal/dom.ts index 452347dc29..a6c41a63c7 100644 --- a/src/runtime/internal/dom.ts +++ b/src/runtime/internal/dom.ts @@ -257,3 +257,39 @@ export function custom_event(type: string, detail?: T) { e.initCustomEvent(type, false, false, detail); return e; } + +export class HtmlTag { + e: HTMLElement; + n: ChildNode[]; + t: HTMLElement; + a: HTMLElement; + + constructor(html: string, anchor: HTMLElement = null) { + this.e = element('div'); + this.a = anchor; + this.u(html); + } + + m(target: HTMLElement, anchor: HTMLElement) { + for (let i = 0; i < this.n.length; i += 1) { + insert(target, this.n[i], anchor); + } + + this.t = target; + } + + u(html: string) { + this.e.innerHTML = html; + this.n = Array.from(this.e.childNodes); + } + + p(html: string) { + this.d(); + this.u(html); + this.m(this.t, this.a); + } + + d() { + this.n.forEach(detach); + } +} \ No newline at end of file diff --git a/test/js/samples/each-block-changed-check/expected.js b/test/js/samples/each-block-changed-check/expected.js index b4d4577df3..6c72bcfae6 100644 --- a/test/js/samples/each-block-changed-check/expected.js +++ b/test/js/samples/each-block-changed-check/expected.js @@ -1,11 +1,11 @@ /* generated by Svelte vX.Y.Z */ import { + HtmlTag, SvelteComponent, append, attr, destroy_each, detach, - detach_after, element, init, insert, @@ -25,7 +25,7 @@ function get_each_context(ctx, list, i) { // (8:0) {#each comments as comment, i} function create_each_block(ctx) { - var div, strong, t0, t1, span, t2_value = ctx.comment.author, t2, t3, t4_value = ctx.elapsed(ctx.comment.time, ctx.time), t4, t5, t6, raw_value = ctx.comment.html, raw_before; + var div, strong, t0, t1, span, t2_value = ctx.comment.author, t2, t3, t4_value = ctx.elapsed(ctx.comment.time, ctx.time), t4, t5, t6, html_tag, raw_value = ctx.comment.html; return { c() { @@ -39,8 +39,8 @@ function create_each_block(ctx) { t4 = text(t4_value); t5 = text(" ago:"); t6 = space(); - raw_before = element('noscript'); attr(span, "class", "meta"); + html_tag = new HtmlTag(raw_value, null); attr(div, "class", "comment"); }, @@ -55,8 +55,7 @@ function create_each_block(ctx) { append(span, t4); append(span, t5); append(div, t6); - append(div, raw_before); - raw_before.insertAdjacentHTML("afterend", raw_value); + html_tag.m(div, anchor); }, p(changed, ctx) { @@ -69,8 +68,7 @@ function create_each_block(ctx) { } if ((changed.comments) && raw_value !== (raw_value = ctx.comment.html)) { - detach_after(raw_before); - raw_before.insertAdjacentHTML("afterend", raw_value); + html_tag.p(raw_value); } }, diff --git a/test/runtime/samples/raw-mustaches/_config.js b/test/runtime/samples/raw-mustaches/_config.js index c81e2c5cd0..cc9999aa34 100644 --- a/test/runtime/samples/raw-mustaches/_config.js +++ b/test/runtime/samples/raw-mustaches/_config.js @@ -1,5 +1,3 @@ -const ns = ''; - export default { skip_if_ssr: true, @@ -7,13 +5,13 @@ export default { raw: 'raw html!!!\\o/' }, - html: `before${ns}raw html!!!\\o/${ns}after`, + html: `beforeraw html!!!\\o/after`, test({ assert, component, target }) { component.raw = ''; - assert.equal(target.innerHTML, `before${ns}${ns}after`); + assert.equal(target.innerHTML, `beforeafter`); component.raw = 'how about unclosed elements?'; - assert.equal(target.innerHTML, `before${ns}how about unclosed elements?${ns}after`); + assert.equal(target.innerHTML, `beforehow about unclosed elements?after`); component.$destroy(); assert.equal(target.innerHTML, ''); } From 6992553329b09c39326086d458950ebeb67b5cff Mon Sep 17 00:00:00 2001 From: Richard Harris Date: Sat, 3 Aug 2019 17:03:09 -0400 Subject: [PATCH 3/4] remove unused helpers --- src/runtime/internal/dom.ts | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/src/runtime/internal/dom.ts b/src/runtime/internal/dom.ts index a6c41a63c7..b5123ba48d 100644 --- a/src/runtime/internal/dom.ts +++ b/src/runtime/internal/dom.ts @@ -10,24 +10,6 @@ export function detach(node: Node) { node.parentNode.removeChild(node); } -export function detach_between(before: Node, after: Node) { - while (before.nextSibling && before.nextSibling !== after) { - before.parentNode.removeChild(before.nextSibling); - } -} - -export function detach_before(after: Node) { - while (after.previousSibling) { - after.parentNode.removeChild(after.previousSibling); - } -} - -export function detach_after(before: Node) { - while (before.nextSibling) { - before.parentNode.removeChild(before.nextSibling); - } -} - export function destroy_each(iterations, detaching) { for (let i = 0; i < iterations.length; i += 1) { if (iterations[i]) iterations[i].d(detaching); From f3a9d39a9117b74265466d2b6b365bea1243bd36 Mon Sep 17 00:00:00 2001 From: Richard Harris Date: Sat, 3 Aug 2019 17:12:20 -0400 Subject: [PATCH 4/4] add test for #3326 --- test/runtime/samples/raw-mustache-before-element/_config.js | 3 +++ test/runtime/samples/raw-mustache-before-element/main.svelte | 1 + 2 files changed, 4 insertions(+) create mode 100644 test/runtime/samples/raw-mustache-before-element/_config.js create mode 100644 test/runtime/samples/raw-mustache-before-element/main.svelte diff --git a/test/runtime/samples/raw-mustache-before-element/_config.js b/test/runtime/samples/raw-mustache-before-element/_config.js new file mode 100644 index 0000000000..61288cbb52 --- /dev/null +++ b/test/runtime/samples/raw-mustache-before-element/_config.js @@ -0,0 +1,3 @@ +export default { + html: `

xbaz

` +}; diff --git a/test/runtime/samples/raw-mustache-before-element/main.svelte b/test/runtime/samples/raw-mustache-before-element/main.svelte new file mode 100644 index 0000000000..69c1d0107d --- /dev/null +++ b/test/runtime/samples/raw-mustache-before-element/main.svelte @@ -0,0 +1 @@ +

{@html 'x'}baz

\ No newline at end of file