more robust handling of html tags - fixes #3285

pull/3329/head
Richard Harris 6 years ago
parent 8a5ad34afb
commit 016158b692

@ -2,7 +2,6 @@ import Renderer from '../Renderer';
import Block from '../Block'; import Block from '../Block';
import Tag from './shared/Tag'; import Tag from './shared/Tag';
import Wrapper from './shared/Wrapper'; import Wrapper from './shared/Wrapper';
import deindent from '../../utils/deindent';
import MustacheTag from '../../nodes/MustacheTag'; import MustacheTag from '../../nodes/MustacheTag';
import RawMustacheTag from '../../nodes/RawMustacheTag'; import RawMustacheTag from '../../nodes/RawMustacheTag';
@ -19,95 +18,47 @@ export default class RawMustacheTagWrapper extends Tag {
this.cannot_use_innerhtml(); this.cannot_use_innerhtml();
} }
render(block: Block, parent_node: string, parent_nodes: string) { render(block: Block, parent_node: string, _parent_nodes: string) {
const name = this.var;
const in_head = parent_node === '@_document.head'; 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 can_use_innerhtml = !in_head && parent_node && !this.prev && !this.next;
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 anchor_before = needs_anchor_before if (can_use_innerhtml) {
? block.get_unique_name(`${name}_before`) const insert = content => `${parent_node}.innerHTML = ${content};`;
: (this.prev && this.prev.var) || 'null';
const anchor_after = needs_anchor_after const { init } = this.rename_this_method(
? block.get_unique_name(`${name}_after`) block,
: (this.next && this.next.var) || 'null'; content => insert(content)
);
let detach: string;
let insert: (content: string) => string;
let use_innerhtml = false;
if (anchor_before === 'null' && anchor_after === 'null') { block.builders.mount.add_line(insert(init));
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});`;
} }
const { init } = this.rename_this_method( else {
block, const needs_anchor = in_head || (this.next && !this.next.is_dom_node());
content => deindent`
${!use_innerhtml && detach}
${insert(content)}
`
);
// we would have used comments here, but the `insertAdjacentHTML` api only const html_tag = block.get_unique_name('html_tag');
// exists for `Element`s. const html_anchor = needs_anchor && block.get_unique_name('html_anchor');
if (needs_anchor_before) {
block.add_element( block.add_variable(html_tag);
anchor_before,
`@element('noscript')`,
parent_nodes && `@element('noscript')`,
parent_node,
true
);
}
function add_anchor_after() { const { init } = this.rename_this_method(
block.add_element( block,
anchor_after, content => `${html_tag}.p(${content});`
`@element('noscript')`,
parent_nodes && `@element('noscript')`,
parent_node
); );
}
if (needs_anchor_after && anchor_before === 'null') { const anchor = in_head ? 'null' : needs_anchor ? html_anchor : this.next ? this.next.var : 'null';
// anchor_after needs to be in the DOM before we
// insert the HTML...
add_anchor_after();
}
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) { if (needs_anchor) {
block.builders.destroy.add_conditional('detaching', needs_anchor_before block.add_element(html_anchor, '@empty()', '@empty()', parent_node);
? `${detach}\n@detach(${anchor_before});` }
: detach);
}
if (needs_anchor_after && anchor_before !== 'null') { if (!parent_node || in_head) {
// ...otherwise it should go afterwards block.builders.destroy.add_conditional('detaching', `${html_tag}.d();`);
add_anchor_after(); }
} }
} }
} }

@ -34,7 +34,7 @@ export default class Tag extends Wrapper {
const update_cached_value = `${value} !== (${value} = ${snippet})`; 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}) && ${update_cached_value}`
: changed_check; : changed_check;

@ -257,3 +257,39 @@ export function custom_event<T=any>(type: string, detail?: T) {
e.initCustomEvent(type, false, false, detail); e.initCustomEvent(type, false, false, detail);
return e; 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);
}
}

@ -1,11 +1,11 @@
/* generated by Svelte vX.Y.Z */ /* generated by Svelte vX.Y.Z */
import { import {
HtmlTag,
SvelteComponent, SvelteComponent,
append, append,
attr, attr,
destroy_each, destroy_each,
detach, detach,
detach_after,
element, element,
init, init,
insert, insert,
@ -25,7 +25,7 @@ function get_each_context(ctx, list, i) {
// (8:0) {#each comments as comment, i} // (8:0) {#each comments as comment, i}
function create_each_block(ctx) { 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 { return {
c() { c() {
@ -39,8 +39,8 @@ function create_each_block(ctx) {
t4 = text(t4_value); t4 = text(t4_value);
t5 = text(" ago:"); t5 = text(" ago:");
t6 = space(); t6 = space();
raw_before = element('noscript');
attr(span, "class", "meta"); attr(span, "class", "meta");
html_tag = new HtmlTag(raw_value, null);
attr(div, "class", "comment"); attr(div, "class", "comment");
}, },
@ -55,8 +55,7 @@ function create_each_block(ctx) {
append(span, t4); append(span, t4);
append(span, t5); append(span, t5);
append(div, t6); append(div, t6);
append(div, raw_before); html_tag.m(div, anchor);
raw_before.insertAdjacentHTML("afterend", raw_value);
}, },
p(changed, ctx) { p(changed, ctx) {
@ -69,8 +68,7 @@ function create_each_block(ctx) {
} }
if ((changed.comments) && raw_value !== (raw_value = ctx.comment.html)) { if ((changed.comments) && raw_value !== (raw_value = ctx.comment.html)) {
detach_after(raw_before); html_tag.p(raw_value);
raw_before.insertAdjacentHTML("afterend", raw_value);
} }
}, },

@ -1,5 +1,3 @@
const ns = '<noscript></noscript>';
export default { export default {
skip_if_ssr: true, skip_if_ssr: true,
@ -7,13 +5,13 @@ export default {
raw: '<span><em>raw html!!!\\o/</span></em>' raw: '<span><em>raw html!!!\\o/</span></em>'
}, },
html: `before${ns}<span><em>raw html!!!\\o/</span></em>${ns}after`, html: `before<span><em>raw html!!!\\o/</span></em>after`,
test({ assert, component, target }) { test({ assert, component, target }) {
component.raw = ''; component.raw = '';
assert.equal(target.innerHTML, `before${ns}${ns}after`); assert.equal(target.innerHTML, `beforeafter`);
component.raw = 'how about <strong>unclosed elements?'; component.raw = 'how about <strong>unclosed elements?';
assert.equal(target.innerHTML, `before${ns}how about <strong>unclosed elements?</strong>${ns}after`); assert.equal(target.innerHTML, `beforehow about <strong>unclosed elements?</strong>after`);
component.$destroy(); component.$destroy();
assert.equal(target.innerHTML, ''); assert.equal(target.innerHTML, '');
} }

Loading…
Cancel
Save