import DOMPurify from 'dompurify' import { defineComponent, ref, watchEffect, type Ref, h, Fragment } from 'vue' import type { CodeBlockProps } from './code-block' import { keepAlive } from '../../../__internal__/keep-alive' keepAlive(h, Fragment) const SVG_NAMESPACE = 'http://www.w3.org/2000/svg' /** * Creates a sanitizer that allows foreignObject only inside SVG context. * * Mermaid v11+ uses foreignObject for flowchart node labels, but foreignObject * is a known mXSS vector (CVE-2020-26870) outside SVG context. The strategy: * 1. ADD_TAGS allows foreignObject to survive DOMPurify's initial filtering * 2. The uponSanitizeElement hook removes it if NOT inside an SVG element * 3. HTML_INTEGRATION_POINTS lets its HTML children parse correctly */ function createSvgAwareSanitizer() { const purify = DOMPurify() const config = { ADD_TAGS: ['foreignObject'], ADD_ATTR: ['xmlns'], HTML_INTEGRATION_POINTS: { foreignobject: true }, } purify.addHook('uponSanitizeElement', (node, data) => { if (data.tagName === 'foreignobject') { const parent = node.parentElement if (!parent || parent.namespaceURI !== SVG_NAMESPACE) { node.parentNode?.removeChild(node) } } }) return (dirty: string | Node) => purify.sanitize(dirty, config) } // Lazily initialize the sanitizer so that importing this module under SSR // (where `DOMPurify()` returns a stub without `.addHook`) does not throw. let cachedSanitizer: ReturnType | undefined function sanitizeSvg(dirty: string | Node) { cachedSanitizer ??= createSvgAwareSanitizer() return cachedSanitizer(dirty) } type PreviewPanelProps = Pick< CodeBlockProps, 'text' | 'language' | 'config' > & { previewOnlyMode: Ref preview: Ref } export const PreviewPanel = defineComponent({ props: { text: { type: Object, required: true, }, language: { type: Object, required: true, }, config: { type: Object, required: true, }, previewOnlyMode: { type: Object, required: true, }, preview: { type: Object, required: true, }, }, setup(props) { const { previewOnlyMode, config, preview } = props const previewRef = ref() watchEffect(() => { const previewContainer = previewRef.value if (!previewContainer) return while (previewContainer.firstChild) { previewContainer.removeChild(previewContainer.firstChild) } const previewContent = preview.value if ( typeof previewContent === 'string' || previewContent instanceof Element ) { previewContainer.innerHTML = sanitizeSvg(previewContent) } }) return () => { if (!preview.value) return null return (
{!previewOnlyMode.value && ( <>
{config.previewLabel}
)}
) } }, })