fix: handle nested script tags (#10416)

fixes #9484
pull/10409/head
Simon H 2 years ago committed by GitHub
parent 90f8b63bee
commit 9aa0ed3eb6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -0,0 +1,5 @@
---
"svelte": patch
---
fix: handle nested script tags

@ -84,7 +84,10 @@ export function client_component(source, analysis, options) {
}, },
legacy_reactive_statements: new Map(), legacy_reactive_statements: new Map(),
metadata: { metadata: {
template_needs_import_node: false, context: {
template_needs_import_node: false,
template_contains_script_tag: false
},
namespace: options.namespace, namespace: options.namespace,
bound_contenteditable: false bound_contenteditable: false
}, },

@ -47,9 +47,22 @@ export interface ComponentClientTransformState extends ClientTransformState {
readonly template: string[]; readonly template: string[];
readonly metadata: { readonly metadata: {
namespace: Namespace; namespace: Namespace;
/** `true` if the HTML template needs to be instantiated with `importNode` */
template_needs_import_node: boolean;
bound_contenteditable: boolean; bound_contenteditable: boolean;
/**
* Stuff that is set within the children of one `create_block` that is relevant
* to said `create_block`. Shouldn't be destructured or otherwise spread unless
* inside `create_block` to keep the object reference intact (it's also nested
* within `metadata` for this reason).
*/
context: {
/** `true` if the HTML template needs to be instantiated with `importNode` */
template_needs_import_node: boolean;
/**
* `true` if HTML template contains a `<script>` tag. In this case we need to invoke a special
* template instantiation function (see `create_fragment_with_script_from_html` for more info)
*/
template_contains_script_tag: boolean;
};
}; };
readonly preserve_whitespace: boolean; readonly preserve_whitespace: boolean;

@ -1065,7 +1065,10 @@ function create_block(parent, name, nodes, context) {
after_update: [], after_update: [],
template: [], template: [],
metadata: { metadata: {
template_needs_import_node: false, context: {
template_needs_import_node: false,
template_contains_script_tag: false
},
namespace, namespace,
bound_contenteditable: context.state.metadata.bound_contenteditable bound_contenteditable: context.state.metadata.bound_contenteditable
} }
@ -1085,10 +1088,14 @@ function create_block(parent, name, nodes, context) {
node: id node: id
}); });
const callee = namespace === 'svg' ? '$.svg_template' : '$.template';
context.state.hoisted.push( context.state.hoisted.push(
b.var(template_name, b.call(callee, b.template([b.quasi(state.template.join(''), true)], []))) b.var(
template_name,
b.call(
get_template_function(namespace, state),
b.template([b.quasi(state.template.join(''), true)], [])
)
)
); );
body.push( body.push(
@ -1097,7 +1104,7 @@ function create_block(parent, name, nodes, context) {
b.call( b.call(
'$.open', '$.open',
b.id('$$anchor'), b.id('$$anchor'),
b.literal(!state.metadata.template_needs_import_node), b.literal(!state.metadata.context.template_needs_import_node),
template_name template_name
) )
), ),
@ -1138,12 +1145,14 @@ function create_block(parent, name, nodes, context) {
// special case — we can use `$.comment` instead of creating a unique template // special case — we can use `$.comment` instead of creating a unique template
body.push(b.var(id, b.call('$.comment', b.id('$$anchor')))); body.push(b.var(id, b.call('$.comment', b.id('$$anchor'))));
} else { } else {
const callee = namespace === 'svg' ? '$.svg_template' : '$.template';
state.hoisted.push( state.hoisted.push(
b.var( b.var(
template_name, template_name,
b.call(callee, b.template([b.quasi(state.template.join(''), true)], []), b.true) b.call(
get_template_function(namespace, state),
b.template([b.quasi(state.template.join(''), true)], []),
b.true
)
) )
); );
@ -1153,7 +1162,7 @@ function create_block(parent, name, nodes, context) {
b.call( b.call(
'$.open_frag', '$.open_frag',
b.id('$$anchor'), b.id('$$anchor'),
b.literal(!state.metadata.template_needs_import_node), b.literal(!state.metadata.context.template_needs_import_node),
template_name template_name
) )
) )
@ -1217,6 +1226,23 @@ function create_block(parent, name, nodes, context) {
return body; return body;
} }
/**
*
* @param {import('#compiler').Namespace} namespace
* @param {import('../types.js').ComponentClientTransformState} state
* @returns
*/
function get_template_function(namespace, state) {
const contains_script_tag = state.metadata.context.template_contains_script_tag;
return namespace === 'svg'
? contains_script_tag
? '$.svg_template_with_script'
: '$.svg_template'
: contains_script_tag
? '$.template_with_script'
: '$.template';
}
/** /**
* *
* @param {import('../types.js').ComponentClientTransformState} state * @param {import('../types.js').ComponentClientTransformState} state
@ -1847,6 +1873,9 @@ export const template_visitors = {
context.state.template.push('<!>'); context.state.template.push('<!>');
return; return;
} }
if (node.name === 'script') {
context.state.metadata.context.template_contains_script_tag = true;
}
const metadata = context.state.metadata; const metadata = context.state.metadata;
const child_metadata = { const child_metadata = {
@ -1885,7 +1914,7 @@ export const template_visitors = {
// custom element until the template is connected to the dom, which would // custom element until the template is connected to the dom, which would
// cause problems when setting properties on the custom element. // cause problems when setting properties on the custom element.
// Therefore we need to use importNode instead, which doesn't have this caveat. // Therefore we need to use importNode instead, which doesn't have this caveat.
metadata.template_needs_import_node = true; metadata.context.template_needs_import_node = true;
} }
for (const attribute of node.attributes) { for (const attribute of node.attributes) {

@ -9,6 +9,25 @@ export function create_fragment_from_html(html) {
return elem.content; return elem.content;
} }
/**
* Creating a document fragment from HTML that contains script tags will not execute
* the scripts. We need to replace the script tags with new ones so that they are executed.
* @param {string} html
*/
export function create_fragment_with_script_from_html(html) {
var content = create_fragment_from_html(html);
var scripts = content.querySelectorAll('script');
for (const script of scripts) {
var new_script = document.createElement('script');
for (var i = 0; i < script.attributes.length; i++) {
new_script.setAttribute(script.attributes[i].name, script.attributes[i].value);
}
new_script.textContent = script.textContent;
/** @type {Node} */ (script.parentNode).replaceChild(new_script, script);
}
return content;
}
/** /**
* @param {Array<import('./types.js').TemplateNode> | import('./types.js').TemplateNode} current * @param {Array<import('./types.js').TemplateNode> | import('./types.js').TemplateNode} current
* @param {null | Element} parent_element * @param {null | Element} parent_element
@ -63,14 +82,16 @@ export function remove(current) {
} }
/** /**
* Creates the content for a `@html` tag from its string value,
* inserts it before the target anchor and returns the new nodes.
* @template V * @template V
* @param {Element | Text | Comment} dom * @param {Element | Text | Comment} target
* @param {V} value * @param {V} value
* @param {boolean} svg * @param {boolean} svg
* @returns {Element | Comment | (Element | Comment | Text)[]} * @returns {Element | Comment | (Element | Comment | Text)[]}
*/ */
export function reconcile_html(dom, value, svg) { export function reconcile_html(target, value, svg) {
hydrate_block_anchor(dom); hydrate_block_anchor(target);
if (current_hydration_fragment !== null) { if (current_hydration_fragment !== null) {
return current_hydration_fragment; return current_hydration_fragment;
} }
@ -78,12 +99,11 @@ export function reconcile_html(dom, value, svg) {
// Even if html is the empty string we need to continue to insert something or // Even if html is the empty string we need to continue to insert something or
// else the element ordering gets out of sync, resulting in subsequent values // else the element ordering gets out of sync, resulting in subsequent values
// not getting inserted anymore. // not getting inserted anymore.
var target = dom;
var frag_nodes; var frag_nodes;
if (svg) { if (svg) {
html = `<svg>${html}</svg>`; html = `<svg>${html}</svg>`;
} }
var content = create_fragment_from_html(html); var content = create_fragment_with_script_from_html(html);
if (svg) { if (svg) {
content = /** @type {DocumentFragment} */ (/** @type {unknown} */ (content.firstChild)); content = /** @type {DocumentFragment} */ (/** @type {unknown} */ (content.firstChild));
} }

@ -23,14 +23,18 @@ import {
PassiveDelegatedEvents, PassiveDelegatedEvents,
DelegatedEvents, DelegatedEvents,
AttributeAliases, AttributeAliases,
namespace_svg, namespace_svg
namespace_html
} from '../../constants.js'; } from '../../constants.js';
import { create_fragment_from_html, insert, reconcile_html, remove } from './reconciler.js'; import {
create_fragment_from_html,
create_fragment_with_script_from_html,
insert,
reconcile_html,
remove
} from './reconciler.js';
import { import {
render_effect, render_effect,
destroy_signal, destroy_signal,
get,
is_signal, is_signal,
push_destroy_fn, push_destroy_fn,
execute_effect, execute_effect,
@ -78,17 +82,35 @@ export function empty() {
/** /**
* @param {string} html * @param {string} html
* @param {boolean} is_fragment * @param {boolean} return_fragment
* @returns {() => Node} * @returns {() => Node}
*/ */
/*#__NO_SIDE_EFFECTS__*/ /*#__NO_SIDE_EFFECTS__*/
export function template(html, is_fragment) { export function template(html, return_fragment) {
/** @type {undefined | Node} */ /** @type {undefined | Node} */
let cached_content; let cached_content;
return () => { return () => {
if (cached_content === undefined) { if (cached_content === undefined) {
const content = create_fragment_from_html(html); const content = create_fragment_from_html(html);
cached_content = is_fragment ? content : /** @type {Node} */ (child(content)); cached_content = return_fragment ? content : /** @type {Node} */ (child(content));
}
return cached_content;
};
}
/**
* @param {string} html
* @param {boolean} return_fragment
* @returns {() => Node}
*/
/*#__NO_SIDE_EFFECTS__*/
export function template_with_script(html, return_fragment) {
/** @type {undefined | Node} */
let cached_content;
return () => {
if (cached_content === undefined) {
const content = create_fragment_with_script_from_html(html);
cached_content = return_fragment ? content : /** @type {Node} */ (child(content));
} }
return cached_content; return cached_content;
}; };
@ -96,17 +118,35 @@ export function template(html, is_fragment) {
/** /**
* @param {string} svg * @param {string} svg
* @param {boolean} is_fragment * @param {boolean} return_fragment
* @returns {() => Node}
*/
/*#__NO_SIDE_EFFECTS__*/
export function svg_template(svg, return_fragment) {
/** @type {undefined | Node} */
let cached_content;
return () => {
if (cached_content === undefined) {
const content = /** @type {Node} */ (child(create_fragment_from_html(`<svg>${svg}</svg>`)));
cached_content = return_fragment ? content : /** @type {Node} */ (child(content));
}
return cached_content;
};
}
/**
* @param {string} svg
* @param {boolean} return_fragment
* @returns {() => Node} * @returns {() => Node}
*/ */
/*#__NO_SIDE_EFFECTS__*/ /*#__NO_SIDE_EFFECTS__*/
export function svg_template(svg, is_fragment) { export function svg_template_with_script(svg, return_fragment) {
/** @type {undefined | Node} */ /** @type {undefined | Node} */
let cached_content; let cached_content;
return () => { return () => {
if (cached_content === undefined) { if (cached_content === undefined) {
const content = /** @type {Node} */ (child(create_fragment_from_html(`<svg>${svg}</svg>`))); const content = /** @type {Node} */ (child(create_fragment_from_html(`<svg>${svg}</svg>`)));
cached_content = is_fragment ? content : /** @type {Node} */ (child(content)); cached_content = return_fragment ? content : /** @type {Node} */ (child(content));
} }
return cached_content; return cached_content;
}; };

@ -0,0 +1,28 @@
import { test } from '../../test';
/**
* @type {any[]}
*/
let log;
/**
* @type {typeof console.log}}
*/
let original_log;
export default test({
skip_if_ssr: 'permanent',
skip_if_hydrate: 'permanent', // log patching will be too late
before_test() {
log = [];
original_log = console.log;
console.log = (...v) => {
log.push(...v);
};
},
after_test() {
console.log = original_log;
},
async test({ assert }) {
assert.deepEqual(log, ['init']);
}
});

@ -0,0 +1,5 @@
<div>
<script>
console.log('init');
</script>
</div>
Loading…
Cancel
Save