diff --git a/.changeset/witty-lies-happen.md b/.changeset/witty-lies-happen.md new file mode 100644 index 0000000000..e176103226 --- /dev/null +++ b/.changeset/witty-lies-happen.md @@ -0,0 +1,5 @@ +--- +'svelte': minor +--- + +feat: Use `TrustedTypes` for HTML handling where supported diff --git a/packages/svelte/package.json b/packages/svelte/package.json index c47e1ddbcd..452187662c 100644 --- a/packages/svelte/package.json +++ b/packages/svelte/package.json @@ -170,6 +170,7 @@ "@jridgewell/sourcemap-codec": "^1.5.0", "@sveltejs/acorn-typescript": "^1.0.5", "@types/estree": "^1.0.5", + "@types/trusted-types": "^2.0.7", "acorn": "^8.12.1", "aria-query": "^5.3.1", "axobject-query": "^4.1.0", diff --git a/packages/svelte/src/internal/client/dom/blocks/snippet.js b/packages/svelte/src/internal/client/dom/blocks/snippet.js index 0c4948aca0..7da7e5072d 100644 --- a/packages/svelte/src/internal/client/dom/blocks/snippet.js +++ b/packages/svelte/src/internal/client/dom/blocks/snippet.js @@ -83,7 +83,7 @@ export function createRawSnippet(fn) { hydrate_next(); } else { var html = snippet.render().trim(); - var fragment = create_fragment_from_html(html); + var fragment = create_fragment_from_html(html, true); element = /** @type {Element} */ (get_first_child(fragment)); if (DEV && (get_next_sibling(element) !== null || element.nodeType !== ELEMENT_NODE)) { diff --git a/packages/svelte/src/internal/client/dom/reconciler.js b/packages/svelte/src/internal/client/dom/reconciler.js index 54f2e23c6e..1f4750ce8f 100644 --- a/packages/svelte/src/internal/client/dom/reconciler.js +++ b/packages/svelte/src/internal/client/dom/reconciler.js @@ -1,8 +1,29 @@ +/** @import {} from 'trusted-types' */ + import { create_element } from './operations.js'; +const policy = /* @__PURE__ */ globalThis?.window?.trustedTypes?.createPolicy( + 'svelte-trusted-html', + { + /** @param {string} html */ + createHTML: (html) => { + return html; + } + } +); + /** @param {string} html */ -export function create_fragment_from_html(html) { +function create_trusted_html(html) { + return /** @type {string} */ (policy?.createHTML(html) ?? html); +} + +/** + * @param {string} html + * @param {boolean} trusted + */ +export function create_fragment_from_html(html, trusted = false) { var elem = create_element('template'); - elem.innerHTML = html.replaceAll('', ''); // XHTML compliance + html = html.replaceAll('', ''); // XHTML compliance + elem.innerHTML = trusted ? create_trusted_html(html) : html; return elem.content; } diff --git a/packages/svelte/src/internal/client/dom/template.js b/packages/svelte/src/internal/client/dom/template.js index a5d0405d0a..7e12ff433f 100644 --- a/packages/svelte/src/internal/client/dom/template.js +++ b/packages/svelte/src/internal/client/dom/template.js @@ -70,7 +70,7 @@ export function from_html(content, flags) { } if (node === undefined) { - node = create_fragment_from_html(has_start ? content : '' + content); + node = create_fragment_from_html(has_start ? content : '' + content, true); 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)); + var fragment = /** @type {DocumentFragment} */ (create_fragment_from_html(wrapped, true)); var root = /** @type {Element} */ (get_first_child(fragment)); if (is_fragment) { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 48c2a52cc5..1d3427fae0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -80,6 +80,9 @@ importers: '@types/estree': specifier: ^1.0.5 version: 1.0.8 + '@types/trusted-types': + specifier: ^2.0.7 + version: 2.0.7 acorn: specifier: ^8.12.1 version: 8.15.0 @@ -933,6 +936,9 @@ packages: '@types/resolve@1.20.2': resolution: {integrity: sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==} + '@types/trusted-types@2.0.7': + resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==} + '@typescript-eslint/eslint-plugin@8.55.0': resolution: {integrity: sha512-1y/MVSz0NglV1ijHC8OT49mPJ4qhPYjiK08YUQVbIOyu+5k862LKUHFkpKHWu//zmr7hDR2rhwUm6gnCGNmGBQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -3189,6 +3195,8 @@ snapshots: '@types/resolve@1.20.2': {} + '@types/trusted-types@2.0.7': {} + '@typescript-eslint/eslint-plugin@8.55.0(@typescript-eslint/parser@8.55.0(eslint@10.0.0)(typescript@5.5.4))(eslint@10.0.0)(typescript@5.5.4)': dependencies: '@eslint-community/regexpp': 4.12.2