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 2 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 { 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 { 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 * as w from '../../warnings.js';
import { hash, sanitize_location } from '../../../../utils.js';
import { DEV } from 'esm-env';
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 { COMMENT_NODE } from '#client/constants';
/**
* @param {Element} element
* @param {string | null} server_hash
* @param {string} value
* @param {string | TrustedHTML} value
*/
function check_hash(element, server_hash, value) {
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 {() => string} get_value
* @param {() => string | TrustedHTML} get_value
* @param {boolean} [svg]
* @param {boolean} [mathml]
* @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) {
var anchor = node;
/** @type {string | TrustedHTML} */
var value = '';
template_effect(() => {
@ -92,18 +99,18 @@ export function html(node, get_value, svg = false, mathml = false, skip_warning
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.
// @html is basically `.innerHTML = ...` and that doesn't execute scripts either due to security reasons.
/** @type {DocumentFragment | Element} */
var node = create_fragment_from_html(html);
// Use a <template>, <svg>, or <math> wrapper depending on context. If value is a TrustedHTML object,
// 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) {
node = /** @type {Element} */ (get_first_child(node));
}
/** @type {DocumentFragment | Element} */
var node = svg || mathml ? wrapper : /** @type {HTMLTemplateElement} */ (wrapper).content;
assign_nodes(
/** @type {TemplateNode} */ (get_first_child(node)),

@ -83,7 +83,7 @@ export function createRawSnippet(fn) {
hydrate_next();
} else {
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));
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';
const policy =
@ -19,11 +17,9 @@ function create_trusted_html(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');
html = html.replaceAll('<!>', '<!---->'); // XHTML compliance
elem.innerHTML = trusted ? create_trusted_html(html) : html;
elem.innerHTML = create_trusted_html(html.replaceAll('<!>', '<!---->')); // XHTML compliance
return elem.content;
}

@ -70,7 +70,7 @@ export function from_html(content, flags) {
}
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));
}
@ -118,7 +118,7 @@ function from_namespace(content, flags, ns = 'svg') {
}
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));
if (is_fragment) {

Loading…
Cancel
Save