UI/svg block rendering (#24080)
* ui: add svg block visualizer based on allozaur's mermaid PR * ui: rationalise diagram block styling and pre transforms shared by mermaid and svg * ui: live render streaming svg blocks * ui: also render svg authored in xml code fences * ui: refactor svg block rendering, address review from allozaur - Move the svg size ceiling and DOMPurify config out of sanitize-svg.ts into /constants. - Rename the svg-diagram class to svg-block so the name no longer implies diagrams only. - Replace the svg, xml and svg tag magic strings in the markdown pipeline with shared constants. - Promote the data-svg-rendered marker and its sibling data attributes to constants. * ui: render svg blocks in a shadow root for animation and live zoom Mount each sanitized svg inside an open shadow root so author <style> and keyframe or smil animations run while staying scoped to the host element. Relax the sanitizer to forbid only foreignObject and script, which lets animation, href and external resource refs through for wider compatibility. Render the inline block and the zoom dialog from the same reactive source, so a streaming svg keeps drawing live inside the open zoom popup.
This commit is contained in:
@@ -0,0 +1,22 @@
|
||||
import DOMPurify from 'dompurify';
|
||||
import { SVG_MAX_BYTES, SVG_SANITIZE_CONFIG, SVG_TAG_PREFIX } from '$lib/constants';
|
||||
|
||||
/**
|
||||
* Sanitizes a raw svg string for safe inline rendering.
|
||||
* Returns the cleaned svg markup, or an empty string when the input is not a
|
||||
* usable svg, exceeds the size ceiling, or sanitizes to nothing. An empty
|
||||
* return tells the caller to keep the raw code block instead of rendering.
|
||||
*/
|
||||
export function sanitizeSvg(source: string): string {
|
||||
const trimmed = source.trim();
|
||||
|
||||
if (!trimmed || trimmed.length > SVG_MAX_BYTES) return '';
|
||||
|
||||
if (!trimmed.startsWith(SVG_TAG_PREFIX)) return '';
|
||||
|
||||
const clean = DOMPurify.sanitize(trimmed, SVG_SANITIZE_CONFIG) as unknown as string;
|
||||
|
||||
if (!clean || !clean.includes(SVG_TAG_PREFIX)) return '';
|
||||
|
||||
return clean;
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
/**
|
||||
* Mounts svg markup inside an open shadow root on the host element.
|
||||
* The shadow boundary scopes the svg <style> and its animations to the host,
|
||||
* so model authored css can not reach the surrounding page. The caller passes
|
||||
* markup that is already sanitized, this only isolates and sizes it.
|
||||
*/
|
||||
export function mountSvgShadow(host: HTMLElement, markup: string, style: string): void {
|
||||
const root = host.shadowRoot ?? host.attachShadow({ mode: 'open' });
|
||||
root.innerHTML = markup ? `<style>${style}</style>${markup}` : '';
|
||||
}
|
||||
Reference in New Issue
Block a user