diff --git a/blocksuite/affine/blocks/root/package.json b/blocksuite/affine/blocks/root/package.json index 2ad087624c..53dabe76d8 100644 --- a/blocksuite/affine/blocks/root/package.json +++ b/blocksuite/affine/blocks/root/package.json @@ -43,7 +43,7 @@ "@blocksuite/store": "workspace:*", "@preact/signals-core": "^1.8.0", "@types/lodash-es": "^4.17.12", - "dompurify": "^3.3.0", + "dompurify": "^3.4.11", "html2canvas": "^1.4.1", "lit": "^3.2.0", "lodash-es": "^4.17.23", diff --git a/blocksuite/affine/shared/package.json b/blocksuite/affine/shared/package.json index d712d7d708..0c845fc069 100644 --- a/blocksuite/affine/shared/package.json +++ b/blocksuite/affine/shared/package.json @@ -23,7 +23,7 @@ "@types/lodash-es": "^4.17.12", "@types/mdast": "^4.0.4", "bytes": "^3.1.2", - "dompurify": "^3.3.0", + "dompurify": "^3.4.11", "fractional-indexing": "^3.2.0", "lit": "^3.2.0", "lodash-es": "^4.17.23", @@ -46,6 +46,7 @@ "remark-parse": "^11.0.0", "remark-stringify": "^11.0.0", "rxjs": "^7.8.2", + "tldts": "^7.0.19", "ts-pattern": "^5.1.0", "unified": "^11.0.5", "unist-util-visit": "^5.0.0", diff --git a/blocksuite/affine/shared/src/__tests__/utils/svg.unit.spec.ts b/blocksuite/affine/shared/src/__tests__/utils/svg.unit.spec.ts new file mode 100644 index 0000000000..f6ad9de0d3 --- /dev/null +++ b/blocksuite/affine/shared/src/__tests__/utils/svg.unit.spec.ts @@ -0,0 +1,185 @@ +/** + * @vitest-environment happy-dom + */ +import { describe, expect, test } from 'vitest'; + +import { sanitizeSvg } from '../../utils/svg.js'; + +type HappyDOMWindow = Window & { + happyDOM: { + setURL: (url: string) => void; + }; +}; + +function setLocation(url: string) { + (window as unknown as HappyDOMWindow).happyDOM.setURL(url); +} + +function svgDataUrl(svg: string) { + const bytes = new TextEncoder().encode(svg); + let binary = ''; + bytes.forEach(byte => { + binary += String.fromCharCode(byte); + }); + return `data:image/svg+xml;base64,${btoa(binary)}`; +} + +function decodeSvgDataUrl(dataUrl: string) { + const base64 = dataUrl.split(',')[1]; + return new TextDecoder().decode( + Uint8Array.from(atob(base64), char => char.charCodeAt(0)) + ); +} + +describe('sanitizeSvg', () => { + test('wraps DOMPurify svg fragments back into an svg root', () => { + const sanitized = sanitizeSvg( + '' + ); + + expect(sanitized).toContain(' { + const sanitized = sanitizeSvg(` + + + + `); + + expect(sanitized).toContain(' { + expect(sanitizeSvg('
')).toBe(''); + }); + + test('keeps internal glyph references and safe image data urls', () => { + const sanitized = sanitizeSvg(` + + + + + + + + `); + + expect(sanitized).toContain('href="#glyph-a"'); + expect(sanitized).toContain('xlink:href="#glyph-a"'); + expect(sanitized).toContain('xlink:href="https://typst.app/docs/tutorial"'); + expect(sanitized).toContain('data:image/png;base64,AAAA'); + }); + + test('removes external glyph references and unsafe css', () => { + const sanitized = sanitizeSvg(` + + + + + + + + + `); + + expect(sanitized).not.toContain('https://example.com'); + expect(sanitized).not.toContain('javascript:'); + expect(sanitized).not.toContain('@import'); + expect(sanitized).not.toContain('url('); + }); + + test('removes links sharing the current registrable domain', () => { + setLocation('https://sub.example.co.uk/workspace'); + + const sanitized = sanitizeSvg(` + + + + + + `); + + expect(sanitized).not.toContain('https://sub.example.co.uk/docs'); + expect(sanitized).not.toContain('https://other.example.co.uk/docs'); + expect(sanitized).toContain('https://example.com/docs'); + }); + + test('keeps private suffix sibling domains separate', () => { + setLocation('https://foo.github.io/workspace'); + + const sanitized = sanitizeSvg(` + + + + + `); + + expect(sanitized).not.toContain('https://foo.github.io/docs'); + expect(sanitized).toContain('https://bar.github.io/docs'); + }); + + test('handles local hostnames by exact hostname', () => { + setLocation('http://localhost:3000/workspace'); + + const sanitized = sanitizeSvg(` + + + + + + `); + + expect(sanitized).not.toContain('http://localhost:8080/docs'); + expect(sanitized).toContain('http://share.localhost/docs'); + expect(sanitized).toContain('http://127.0.0.1/docs'); + }); + + test('recursively sanitizes svg images', () => { + const nestedSvg = svgDataUrl( + '' + ); + const sanitized = sanitizeSvg(` + + + + `); + const sanitizedImageHref = sanitized.match(/href="([^"]+)"/)?.[1]; + + expect(sanitizedImageHref).toMatch(/^data:image\/svg\+xml;base64,/); + expect(decodeSvgDataUrl(sanitizedImageHref ?? '')).toContain(' { + const thirdLevelSvg = svgDataUrl( + '' + ); + const secondLevelSvg = svgDataUrl( + `` + ); + const firstLevelSvg = svgDataUrl( + `` + ); + const sanitized = sanitizeSvg(` + + + + `); + const firstLevelHref = sanitized.match(/href="([^"]+)"/)?.[1]; + const firstLevelSanitizedSvg = decodeSvgDataUrl(firstLevelHref ?? ''); + const secondLevelHref = firstLevelSanitizedSvg.match(/href="([^"]+)"/)?.[1]; + const secondLevelSanitizedSvg = decodeSvgDataUrl(secondLevelHref ?? ''); + + expect(firstLevelSanitizedSvg).toContain(' { const trimmedText = text.trim(); if (trimmedText.startsWith(';base64)?,(?[\s\S]*)$/i; +const SAFE_IMAGE_DATA_URL_PATTERN = + /^data:image\/(?:png|jpe?g|gif|webp|svg\+xml);base64,[a-z0-9+/=]+$/i; +const UNSAFE_CSS_PATTERN = + /(?:url\s*\(|@import|javascript\s*:|expression\s*\(|-moz-binding)/i; +const SVG_ROOT_PATTERN = + /^\s*(?:<\?xml[\s\S]*?\?>\s*)?(?:\s*)?]/i; + +const SVG_ROOT_ATTRIBUTES = [ + 'class', + 'data-height', + 'data-width', + 'height', + 'preserveAspectRatio', + 'viewBox', + 'width', + 'xmlns', + 'xmlns:h5', + 'xmlns:xlink', +]; + +function getAttribute(element: Element, attribute: string) { + return ( + element.getAttribute(attribute) ?? + element.getAttribute(attribute.toLowerCase()) + ); +} + +function getSvgSanitizeConfig(options?: SanitizeSvgOptions) { + return { + ...DEFAULT_SVG_SANITIZE_CONFIG, + ...options?.svg, + }; +} + +function getForeignObjectHtmlSanitizeConfig(options?: SanitizeSvgOptions) { + return { + ...DEFAULT_FOREIGN_OBJECT_HTML_SANITIZE_CONFIG, + ...options?.foreignObjectHtml, + }; +} + +function getOriginalSvgRoot(svg: string, parser: DOMParser) { + const root = parser.parseFromString(svg, 'image/svg+xml').documentElement; + if (root?.tagName.toLowerCase() === 'svg') { + return root; + } + if (!SVG_ROOT_PATTERN.test(svg)) { + return null; + } + return parser.parseFromString(svg, 'text/html').querySelector('svg'); +} + +function ensureSvgRoot( + originalRoot: Element | null, + sanitized: string, + parser: DOMParser +) { + if (SVG_ROOT_PATTERN.test(sanitized)) { + const sanitizedDoc = parser.parseFromString(sanitized, 'image/svg+xml'); + const sanitizedRoot = sanitizedDoc.documentElement; + return sanitizedRoot?.tagName.toLowerCase() === 'svg' + ? sanitizedRoot + : null; + } + + const svgDoc = parser.parseFromString('', 'image/svg+xml'); + const svgRoot = svgDoc.documentElement; + SVG_ROOT_ATTRIBUTES.forEach(attribute => { + const value = originalRoot ? getAttribute(originalRoot, attribute) : null; + if (value) { + svgRoot.setAttribute(attribute, value); + } + }); + svgRoot.innerHTML = sanitized; + return svgRoot; +} + +function sanitizeForeignObjects( + root: ParentNode, + options?: SanitizeSvgOptions +) { + root.querySelectorAll('foreignObject, foreignobject').forEach(element => { + element.innerHTML = DOMPurify.sanitize( + element.innerHTML, + getForeignObjectHtmlSanitizeConfig(options) + ); + }); +} + +function getSiteDomain(hostname: string) { + return ( + parse(hostname, { allowPrivateDomains: true }).domain ?? + hostname.toLowerCase() + ); +} + +function isSameSiteDomain(url: URL) { + if (typeof location === 'undefined') return false; + return getSiteDomain(url.hostname) === getSiteDomain(location.hostname); +} + +function isSafeLinkUrl(value: string) { + try { + const url = new URL(value); + return SAFE_LINK_PROTOCOLS.has(url.protocol) && !isSameSiteDomain(url); + } catch { + return false; + } +} + +function isSafeHref(element: Element, value: string) { + if (value.startsWith('#')) return true; + const tagName = element.tagName.toLowerCase(); + if (tagName === 'use') return false; + if (tagName === 'image') return SAFE_IMAGE_DATA_URL_PATTERN.test(value); + if (tagName === 'a') return isSafeLinkUrl(value); + return false; +} + +function decodeSvgDataUrl(value: string) { + const groups = value.match(SVG_DATA_URL_PATTERN)?.groups; + if (!groups) return null; + + try { + if (groups.base64) { + return new TextDecoder().decode( + Uint8Array.from(atob(groups.data), char => char.charCodeAt(0)) + ); + } + return decodeURIComponent(groups.data); + } catch { + return null; + } +} + +function encodeSvgDataUrl(svg: string) { + const binary = Array.from(new TextEncoder().encode(svg), byte => + String.fromCharCode(byte) + ).join(''); + return `data:image/svg+xml;base64,${btoa(binary)}`; +} + +function getHrefAttributes(element: Element) { + return Array.from(element.attributes).filter( + attribute => attribute.name === 'href' || attribute.name === 'xlink:href' + ); +} + +function tightenSvgTree( + root: ParentNode, + options: SanitizeSvgOptions | undefined, + depth: number +) { + root.querySelectorAll('*').forEach(element => { + getHrefAttributes(element).forEach(attribute => { + const href = attribute.value.trim(); + const nestedSvg = + element.tagName.toLowerCase() === 'image' + ? decodeSvgDataUrl(href) + : null; + + if (nestedSvg !== null) { + if (depth < MAX_NESTED_SVG_IMAGE_DEPTH) { + const sanitized = sanitizeSvgWithDepth(nestedSvg, options, depth + 1); + if (sanitized) { + element.setAttribute(attribute.name, encodeSvgDataUrl(sanitized)); + return; + } + } + element.remove(); + } else if (!isSafeHref(element, href)) { + element.removeAttribute(attribute.name); + } + }); + + const style = element.getAttribute('style'); + if (style && UNSAFE_CSS_PATTERN.test(style)) { + element.removeAttribute('style'); + } + + if ( + element.tagName.toLowerCase() === 'style' && + UNSAFE_CSS_PATTERN.test(element.textContent ?? '') + ) { + element.remove(); + } + }); +} + +export function sanitizeSvg(svg: string, options?: SanitizeSvgOptions): string { + return sanitizeSvgWithDepth(svg, options, 0); +} + +function sanitizeSvgWithDepth( + svg: string, + options: SanitizeSvgOptions | undefined, + depth: number +): string { + const svgConfig = getSvgSanitizeConfig(options); + + if ( + typeof DOMParser === 'undefined' || + typeof XMLSerializer === 'undefined' + ) { + const sanitized = DOMPurify.sanitize(svg, svgConfig); + + if (typeof sanitized !== 'string' || !SVG_ROOT_PATTERN.test(sanitized)) { + return ''; + } + return sanitized.trim(); + } + + const parser = new DOMParser(); + const originalRoot = getOriginalSvgRoot(svg, parser); + if (!originalRoot) return ''; + + const sanitized = DOMPurify.sanitize(svg, svgConfig); + if (typeof sanitized !== 'string') return ''; + const sanitizedRoot = ensureSvgRoot(originalRoot, sanitized, parser); + if (!sanitizedRoot) return ''; + sanitizeForeignObjects(sanitizedRoot, options); + tightenSvgTree(sanitizedRoot, options, depth); + return new XMLSerializer().serializeToString(sanitizedRoot).trim(); +} diff --git a/blocksuite/affine/widgets/linked-doc/package.json b/blocksuite/affine/widgets/linked-doc/package.json index dd01cbe969..f229c00dea 100644 --- a/blocksuite/affine/widgets/linked-doc/package.json +++ b/blocksuite/affine/widgets/linked-doc/package.json @@ -24,7 +24,7 @@ "@toeverything/theme": "^1.1.23", "@types/lodash-es": "^4.17.12", "fflate": "^0.8.2", - "js-yaml": "^4.1.1", + "js-yaml": "^4.2.0", "jszip": "^3.10.1", "lit": "^3.2.0", "lodash-es": "^4.17.23", diff --git a/blocksuite/framework/std/package.json b/blocksuite/framework/std/package.json index 83cac33e26..6bc1e71546 100644 --- a/blocksuite/framework/std/package.json +++ b/blocksuite/framework/std/package.json @@ -19,7 +19,7 @@ "@preact/signals-core": "^1.8.0", "@types/hast": "^3.0.4", "@types/lodash-es": "^4.17.12", - "dompurify": "^3.3.0", + "dompurify": "^3.4.11", "fractional-indexing": "^3.2.0", "lib0": "^0.2.114", "lit": "^3.2.0", diff --git a/blocksuite/integration-test/package.json b/blocksuite/integration-test/package.json index d0ddb9c804..a7444df8b3 100644 --- a/blocksuite/integration-test/package.json +++ b/blocksuite/integration-test/package.json @@ -37,7 +37,7 @@ "@vanilla-extract/vite-plugin": "^5.0.0", "@vitest/browser-playwright": "^4.1.8", "playwright": "=1.58.2", - "vite": "^7.2.7", + "vite": "^7.3.5", "vite-plugin-wasm": "^3.5.0", "vitest": "^4.1.8" }, diff --git a/blocksuite/playground/package.json b/blocksuite/playground/package.json index cfdb3855f1..3b0016e22b 100644 --- a/blocksuite/playground/package.json +++ b/blocksuite/playground/package.json @@ -34,7 +34,7 @@ "@types/micromatch": "^4.0.9", "@vanilla-extract/vite-plugin": "^5.0.0", "magic-string": "^0.30.21", - "vite": "^7.2.7", + "vite": "^7.3.5", "vite-plugin-istanbul": "^7.2.1", "vite-plugin-wasm": "^3.5.0", "vite-plugin-web-components-hmr": "^0.1.3" diff --git a/package.json b/package.json index 6d1f35e38d..fcfbc40486 100644 --- a/package.json +++ b/package.json @@ -90,7 +90,7 @@ "typescript": "^5.9.3", "typescript-eslint": "^8.55.0", "unplugin-swc": "^1.5.9", - "vite": "^7.2.7", + "vite": "^7.3.5", "vitest": "^4.1.8" }, "packageManager": "yarn@4.13.0", diff --git a/packages/backend/server/package.json b/packages/backend/server/package.json index 5e7cf1653f..e7b0b88f9b 100644 --- a/packages/backend/server/package.json +++ b/packages/backend/server/package.json @@ -92,7 +92,7 @@ "nanoid": "^5.1.6", "nest-winston": "^1.9.7", "nestjs-cls": "^6.0.0", - "nodemailer": "^8.0.4", + "nodemailer": "^8.0.11", "on-headers": "^1.1.0", "piscina": "^5.1.4", "prisma": "^6.6.0", @@ -102,7 +102,7 @@ "rxjs": "^7.8.2", "semver": "^7.7.4", "ses": "^1.15.0", - "socket.io": "^4.8.1", + "socket.io": "^4.8.3", "stripe": "^17.7.0", "tldts": "^7.0.19", "winston": "^3.17.0", diff --git a/packages/frontend/admin/package.json b/packages/frontend/admin/package.json index 788a85a30f..23ec5bfcdf 100644 --- a/packages/frontend/admin/package.json +++ b/packages/frontend/admin/package.json @@ -51,7 +51,7 @@ "react-dom": "^19.2.1", "react-hook-form": "^7.54.1", "react-resizable-panels": "^3.0.6", - "react-router-dom": "^7.12.0", + "react-router-dom": "^7.18.0", "recharts": "^2.15.4", "sonner": "^2.0.7", "swr": "^2.3.7", diff --git a/packages/frontend/apps/android/package.json b/packages/frontend/apps/android/package.json index 591261e568..42e2fb66e1 100644 --- a/packages/frontend/apps/android/package.json +++ b/packages/frontend/apps/android/package.json @@ -31,7 +31,7 @@ "next-themes": "^0.4.4", "react": "^19.2.1", "react-dom": "^19.2.1", - "react-router-dom": "^6.30.3" + "react-router-dom": "^6.30.4" }, "devDependencies": { "@capacitor/cli": "^7.6.5", diff --git a/packages/frontend/apps/electron-renderer/package.json b/packages/frontend/apps/electron-renderer/package.json index c86e0b5eb0..209eaab295 100644 --- a/packages/frontend/apps/electron-renderer/package.json +++ b/packages/frontend/apps/electron-renderer/package.json @@ -23,7 +23,7 @@ "next-themes": "^0.4.4", "react": "^19.2.1", "react-dom": "^19.2.1", - "react-router-dom": "^6.30.3", + "react-router-dom": "^6.30.4", "uuid": "^14.0.0" }, "devDependencies": { diff --git a/packages/frontend/apps/electron/package.json b/packages/frontend/apps/electron/package.json index beb7ffef0d..a4b90da12a 100644 --- a/packages/frontend/apps/electron/package.json +++ b/packages/frontend/apps/electron/package.json @@ -59,7 +59,7 @@ "electron-log": "^5.4.3", "electron-squirrel-startup": "1.0.1", "electron-window-state": "^5.0.3", - "esbuild": "^0.25.0", + "esbuild": "^0.25.12", "fs-extra": "^11.2.0", "glob": "^11.0.0", "lodash-es": "^4.17.23", diff --git a/packages/frontend/apps/ios/package.json b/packages/frontend/apps/ios/package.json index 7710d59d6a..06e8955398 100644 --- a/packages/frontend/apps/ios/package.json +++ b/packages/frontend/apps/ios/package.json @@ -36,7 +36,7 @@ "next-themes": "^0.4.4", "react": "^19.2.1", "react-dom": "^19.2.1", - "react-router-dom": "^6.30.3" + "react-router-dom": "^6.30.4" }, "devDependencies": { "@affine-tools/cli": "workspace:*", diff --git a/packages/frontend/apps/mobile/package.json b/packages/frontend/apps/mobile/package.json index aaf1ad781c..24bc2cf576 100644 --- a/packages/frontend/apps/mobile/package.json +++ b/packages/frontend/apps/mobile/package.json @@ -18,7 +18,7 @@ "@toeverything/infra": "workspace:*", "react": "^19.2.1", "react-dom": "^19.2.1", - "react-router-dom": "^6.30.3" + "react-router-dom": "^6.30.4" }, "devDependencies": { "@types/react": "^19.0.1", diff --git a/packages/frontend/apps/web/package.json b/packages/frontend/apps/web/package.json index 970f2e54cf..2cbc881d9a 100644 --- a/packages/frontend/apps/web/package.json +++ b/packages/frontend/apps/web/package.json @@ -18,7 +18,7 @@ "@toeverything/infra": "workspace:*", "react": "^19.2.1", "react-dom": "^19.2.1", - "react-router-dom": "^6.30.3" + "react-router-dom": "^6.30.4" }, "devDependencies": { "@types/react": "^19.0.1", diff --git a/packages/frontend/component/package.json b/packages/frontend/component/package.json index 4449aa3473..fa63540aff 100644 --- a/packages/frontend/component/package.json +++ b/packages/frontend/component/package.json @@ -62,7 +62,7 @@ "react": "^19.2.1", "react-dom": "19.2.1", "react-paginate": "^8.3.0", - "react-router-dom": "^6.30.3", + "react-router-dom": "^6.30.4", "react-transition-state": "^2.2.0", "sonner": "^2.0.7", "swr": "^2.3.7", @@ -81,7 +81,7 @@ "storybook": "^10.2.14", "typescript": "^5.9.3", "unplugin-swc": "^1.5.9", - "vite": "^7.2.7", + "vite": "^7.3.5", "vitest": "^4.1.8" }, "version": "0.26.3" diff --git a/packages/frontend/core/package.json b/packages/frontend/core/package.json index b6c8c73141..ff109e8b0a 100644 --- a/packages/frontend/core/package.json +++ b/packages/frontend/core/package.json @@ -56,7 +56,7 @@ "cmdk": "^1.0.4", "core-js": "^3.39.0", "dayjs": "^1.11.13", - "dompurify": "^3.3.0", + "dompurify": "^3.4.11", "eventemitter2": "^6.4.9", "file-type": "^21.0.0", "filesize": "^10.1.6", @@ -82,7 +82,7 @@ "react": "^19.2.1", "react-dom": "^19.2.1", "react-error-boundary": "^6.0.0", - "react-router-dom": "^6.30.3", + "react-router-dom": "^6.30.4", "react-transition-state": "^2.2.0", "react-virtuoso": "^4.12.3", "recharts": "^2.15.4", @@ -98,6 +98,7 @@ }, "devDependencies": { "@blocksuite/affine-ext-loader": "workspace:*", + "@playwright/test": "=1.58.2", "@testing-library/dom": "^10.4.0", "@testing-library/react": "^16.1.0", "@types/bytes": "^3.1.5", @@ -107,6 +108,7 @@ "@vanilla-extract/css": "^1.17.0", "fake-indexeddb": "^6.0.0", "happy-dom": "^20.3.0", + "vite": "^7.3.5", "vitest": "^4.1.8" } } diff --git a/packages/frontend/core/src/modules/code-block-preview-renderer/bridge.spec.ts b/packages/frontend/core/src/modules/code-block-preview-renderer/bridge.spec.ts index 4ab3aa4451..f3c4af34a8 100644 --- a/packages/frontend/core/src/modules/code-block-preview-renderer/bridge.spec.ts +++ b/packages/frontend/core/src/modules/code-block-preview-renderer/bridge.spec.ts @@ -41,12 +41,12 @@ describe('preview render bridge', () => { }); }); - test('uses worker renderers and only sanitizes mermaid output', async () => { + test('uses worker renderers and sanitizes preview svg output', async () => { mermaidRender.mockResolvedValue({ svg: 'mermaid', }); typstRender.mockResolvedValue({ - svg: '
typst
', + svg: 'typst', }); const mermaid = await renderMermaidSvg({ code: 'flowchart TD;A-->B' }); @@ -57,9 +57,9 @@ describe('preview render bridge', () => { expect(mermaid.svg).toContain('typst' - ); + expect(typst.svg).toContain(' { @@ -101,6 +101,37 @@ describe('preview render bridge', () => { expect(sanitized).not.toContain(' { + if (typeof DOMParser === 'undefined') { + return; + } + + domPurifySanitize.mockImplementation((value: unknown) => { + if (typeof value !== 'string') { + return ''; + } + return ''; + }); + + const sanitized = sanitizeSvg( + '' + ); + + expect(sanitized).toContain(' { + typstRender.mockResolvedValue({ + svg: '
invalid
', + }); + + await expect(renderTypstSvg({ code: '= Title' })).rejects.toThrow( + 'Preview renderer returned invalid SVG.' + ); + }); + test('throws when sanitized svg is empty', async () => { mermaidRender.mockResolvedValue({ svg: '
invalid
', diff --git a/packages/frontend/core/src/modules/code-block-preview-renderer/bridge.ts b/packages/frontend/core/src/modules/code-block-preview-renderer/bridge.ts index ec7e682eb2..2bd57b555d 100644 --- a/packages/frontend/core/src/modules/code-block-preview-renderer/bridge.ts +++ b/packages/frontend/core/src/modules/code-block-preview-renderer/bridge.ts @@ -10,56 +10,10 @@ import type { TypstRenderRequest, TypstRenderResult, } from '@affine/core/modules/typst/renderer'; -import type { Config } from 'dompurify'; -import DOMPurify from 'dompurify'; - -/** Mermaid SVG uses ``, `