feat: support TrustedHTML in {@html} expressions (#17701)

Follow-up to #16271.

## Summary

- Allow `{@html}` blocks to accept `TrustedHTML` objects (from
TrustedTypes policies) without coercing them to strings
- This enables usage like `{@html myPolicy.createHTML(someHTML)}`
- Works in regular HTML, SVG, and MathML contexts

## Changes

- **`html.js`**: Instead of calling `create_fragment_from_html`, create
the wrapper element directly (`<template>`, `<svg>`, or `<math>`
depending on context) and assign the value to `innerHTML`. This
preserves `TrustedHTML` objects.
- **`reconciler.js`**: Removed the `trusted` parameter from
`create_fragment_from_html` since it's no longer used by `{@html}` and
all remaining callers want trusted HTML.
- **`template.js`** and **`snippet.js`**: Removed the second argument
from `create_fragment_from_html` calls.

## Notes

No tests added because JSDOM doesn't implement TrustedTypes.
pull/17742/head
Rich Harris 7 months ago committed by GitHub
parent 9f48e7620f
commit be24b0dca7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -0,0 +1,5 @@
---
'svelte': minor
---
feat: support TrustedHTML in `{@html}` expressions

@ -1,21 +1,27 @@
/** @import { Effect, TemplateNode } from '#client' */ /** @import { Effect, TemplateNode } from '#client' */
import { FILENAME, HYDRATION_ERROR } from '../../../../constants.js'; /** @import {} from 'trusted-types' */
import {
FILENAME,
HYDRATION_ERROR,
NAMESPACE_SVG,
NAMESPACE_MATHML
} from '../../../../constants.js';
import { remove_effect_dom, template_effect } from '../../reactivity/effects.js'; import { remove_effect_dom, template_effect } from '../../reactivity/effects.js';
import { hydrate_next, hydrate_node, hydrating, set_hydrate_node } from '../hydration.js'; import { hydrate_next, hydrate_node, hydrating, set_hydrate_node } from '../hydration.js';
import { create_fragment_from_html } from '../reconciler.js';
import { assign_nodes } from '../template.js'; import { assign_nodes } from '../template.js';
import * as w from '../../warnings.js'; import * as w from '../../warnings.js';
import { hash, sanitize_location } from '../../../../utils.js'; import { hash, sanitize_location } from '../../../../utils.js';
import { DEV } from 'esm-env'; import { DEV } from 'esm-env';
import { dev_current_component_function } from '../../context.js'; import { dev_current_component_function } from '../../context.js';
import { get_first_child, get_next_sibling } from '../operations.js'; import { create_element, get_first_child, get_next_sibling } from '../operations.js';
import { active_effect } from '../../runtime.js'; import { active_effect } from '../../runtime.js';
import { COMMENT_NODE } from '#client/constants'; import { COMMENT_NODE } from '#client/constants';
/** /**
* @param {Element} element * @param {Element} element
* @param {string | null} server_hash * @param {string | null} server_hash
* @param {string} value * @param {string | TrustedHTML} value
*/ */
function check_hash(element, server_hash, value) { function check_hash(element, server_hash, value) {
if (!server_hash || server_hash === hash(String(value ?? ''))) return; if (!server_hash || server_hash === hash(String(value ?? ''))) return;
@ -35,7 +41,7 @@ function check_hash(element, server_hash, value) {
/** /**
* @param {Element | Text | Comment} node * @param {Element | Text | Comment} node
* @param {() => string} get_value * @param {() => string | TrustedHTML} get_value
* @param {boolean} [svg] * @param {boolean} [svg]
* @param {boolean} [mathml] * @param {boolean} [mathml]
* @param {boolean} [skip_warning] * @param {boolean} [skip_warning]
@ -44,6 +50,7 @@ function check_hash(element, server_hash, value) {
export function html(node, get_value, svg = false, mathml = false, skip_warning = false) { export function html(node, get_value, svg = false, mathml = false, skip_warning = false) {
var anchor = node; var anchor = node;
/** @type {string | TrustedHTML} */
var value = ''; var value = '';
template_effect(() => { template_effect(() => {
@ -92,18 +99,18 @@ export function html(node, get_value, svg = false, mathml = false, skip_warning
return; return;
} }
var html = value + '';
if (svg) html = `<svg>${html}</svg>`;
else if (mathml) html = `<math>${html}</math>`;
// Don't use create_fragment_with_script_from_html here because that would mean script tags are executed. // Don't use create_fragment_with_script_from_html here because that would mean script tags are executed.
// @html is basically `.innerHTML = ...` and that doesn't execute scripts either due to security reasons. // @html is basically `.innerHTML = ...` and that doesn't execute scripts either due to security reasons.
/** @type {DocumentFragment | Element} */ // Use a <template>, <svg>, or <math> wrapper depending on context. If value is a TrustedHTML object,
var node = create_fragment_from_html(html); // it will be assigned directly to innerHTML without coercion — this allows {@html policy.createHTML(...)} to work.
var ns = svg ? NAMESPACE_SVG : mathml ? NAMESPACE_MATHML : undefined;
var wrapper = /** @type {HTMLTemplateElement | SVGElement | MathMLElement} */ (
create_element(svg ? 'svg' : mathml ? 'math' : 'template', ns)
);
wrapper.innerHTML = /** @type {any} */ (value);
if (svg || mathml) { /** @type {DocumentFragment | Element} */
node = /** @type {Element} */ (get_first_child(node)); var node = svg || mathml ? wrapper : /** @type {HTMLTemplateElement} */ (wrapper).content;
}
assign_nodes( assign_nodes(
/** @type {TemplateNode} */ (get_first_child(node)), /** @type {TemplateNode} */ (get_first_child(node)),

@ -83,7 +83,7 @@ export function createRawSnippet(fn) {
hydrate_next(); hydrate_next();
} else { } else {
var html = snippet.render().trim(); var html = snippet.render().trim();
var fragment = create_fragment_from_html(html, true); var fragment = create_fragment_from_html(html);
element = /** @type {Element} */ (get_first_child(fragment)); element = /** @type {Element} */ (get_first_child(fragment));
if (DEV && (get_next_sibling(element) !== null || element.nodeType !== ELEMENT_NODE)) { if (DEV && (get_next_sibling(element) !== null || element.nodeType !== ELEMENT_NODE)) {

@ -1,5 +1,3 @@
/** @import {} from 'trusted-types' */
import { create_element } from './operations.js'; import { create_element } from './operations.js';
const policy = const policy =
@ -19,11 +17,9 @@ function create_trusted_html(html) {
/** /**
* @param {string} html * @param {string} html
* @param {boolean} trusted
*/ */
export function create_fragment_from_html(html, trusted = false) { export function create_fragment_from_html(html) {
var elem = create_element('template'); var elem = create_element('template');
html = html.replaceAll('<!>', '<!---->'); // XHTML compliance elem.innerHTML = create_trusted_html(html.replaceAll('<!>', '<!---->')); // XHTML compliance
elem.innerHTML = trusted ? create_trusted_html(html) : html;
return elem.content; return elem.content;
} }

@ -70,7 +70,7 @@ export function from_html(content, flags) {
} }
if (node === undefined) { if (node === undefined) {
node = create_fragment_from_html(has_start ? content : '<!>' + content, true); node = create_fragment_from_html(has_start ? content : '<!>' + content);
if (!is_fragment) node = /** @type {TemplateNode} */ (get_first_child(node)); if (!is_fragment) node = /** @type {TemplateNode} */ (get_first_child(node));
} }
@ -118,7 +118,7 @@ function from_namespace(content, flags, ns = 'svg') {
} }
if (!node) { if (!node) {
var fragment = /** @type {DocumentFragment} */ (create_fragment_from_html(wrapped, true)); var fragment = /** @type {DocumentFragment} */ (create_fragment_from_html(wrapped));
var root = /** @type {Element} */ (get_first_child(fragment)); var root = /** @type {Element} */ (get_first_child(fragment));
if (is_fragment) { if (is_fragment) {

Loading…
Cancel
Save