diff --git a/packages/web-ui/CHANGELOG.md b/packages/web-ui/CHANGELOG.md index efe7feab5..c8d1b31a1 100644 --- a/packages/web-ui/CHANGELOG.md +++ b/packages/web-ui/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased] +### Fixed + +- Render SVG artifact previews through a blob-backed image instead of injecting untrusted SVG markup into the page DOM ([#3552](https://github.com/badlogic/pi-mono/issues/3552)) + ## [0.68.1] - 2026-04-22 ## [0.68.0] - 2026-04-20 diff --git a/packages/web-ui/src/tools/artifacts/SvgArtifact.ts b/packages/web-ui/src/tools/artifacts/SvgArtifact.ts index 6901192e9..fe9f398f6 100644 --- a/packages/web-ui/src/tools/artifacts/SvgArtifact.ts +++ b/packages/web-ui/src/tools/artifacts/SvgArtifact.ts @@ -13,11 +13,17 @@ export class SvgArtifact extends ArtifactElement { @property() override filename = ""; private _content = ""; + @state() private previewUrl = ""; + override get content(): string { return this._content; } override set content(value: string) { + if (this._content === value) { + return; + } this._content = value; + this.updatePreviewUrl(); this.requestUpdate(); } @@ -31,6 +37,21 @@ export class SvgArtifact extends ArtifactElement { this.viewMode = mode; } + private revokePreviewUrl() { + if (this.previewUrl) { + URL.revokeObjectURL(this.previewUrl); + this.previewUrl = ""; + } + } + + private updatePreviewUrl() { + this.revokePreviewUrl(); + if (!this._content) { + return; + } + this.previewUrl = URL.createObjectURL(new Blob([this._content], { type: "image/svg+xml" })); + } + public getHeaderButtons() { const toggle = new PreviewCodeToggle(); toggle.mode = this.viewMode; @@ -52,14 +73,34 @@ export class SvgArtifact extends ArtifactElement { `; } + override connectedCallback() { + super.connectedCallback(); + if (this._content && !this.previewUrl) { + this.updatePreviewUrl(); + } + } + + override disconnectedCallback() { + super.disconnectedCallback(); + this.revokePreviewUrl(); + } + override render() { return html`
${unsafeHTML(
hljs.highlight(this.content, { language: "xml", ignoreIllegals: true }).value,