diff --git a/packages/ui/package.json b/packages/ui/package.json index 09e79078..e5c9cb85 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -21,12 +21,12 @@ "@tailwindcss/cli": "^4.1.18", "@tailwindcss/typography": "^0.5.18", "@types/dompurify": "^3.2.0", + "@types/sanitize-html": "^2.16.1", "@zxcvbn-ts/core": "^3.0.4", "@zxcvbn-ts/language-common": "^3.0.4", "@zxcvbn-ts/language-en": "^3.0.2", "bits-ui": "^2.16.1", "clsx": "^2.1.1", - "isomorphic-dompurify": "2.25.0", "jsrepo": "^3.5.1", "layerchart": "2.0.0-next.27", "mode-watcher": "^1.1.0", @@ -69,6 +69,7 @@ "katex": "^0.16.25", "lowlight": "^3.3.0", "plyr": "^3.7.8", + "sanitize-html": "^2.17.2", "simple-icons": "^16.9.0", "svelte-dnd-action": "^0.9.68", "svelte-tiptap": "^2.2.0", diff --git a/packages/ui/src/custom/code/code-utils.svelte.ts b/packages/ui/src/custom/code/code-utils.svelte.ts index 08c61da1..5e9aa69c 100644 --- a/packages/ui/src/custom/code/code-utils.svelte.ts +++ b/packages/ui/src/custom/code/code-utils.svelte.ts @@ -2,8 +2,59 @@ import { Context } from 'runed'; import type { ReadableBoxedValues, WritableBoxedValues } from 'svelte-toolbelt'; import type { CodeRootProps } from './types'; import { highlighter } from './shiki'; -import DOMPurify from 'isomorphic-dompurify'; import type { HighlighterCore } from 'shiki'; +import sanitizeHtml from 'sanitize-html'; +import type { IOptions as SanitizeHtmlOptions } from 'sanitize-html'; + +/** Shiki HTML only; avoids isomorphic-dompurify/jsdom (CommonJS `require` breaks Vite ESM SSR). */ +const SHIKI_HIGHLIGHT_SANITIZE: SanitizeHtmlOptions = { + allowedTags: ['pre', 'code', 'span', 'div', 'br'], + allowedAttributes: { + pre: ['class', 'style', 'tabindex'], + code: ['class', 'style'], + span: ['class', 'style'], + div: ['class', 'style'], + br: ['class', 'style'] + }, + allowedStyles: { + '*': { + color: [ + /^#[0-9a-f]{3,8}$/i, + /^rgb\(/i, + /^rgba\(/i, + /^hsl\(/i, + /^hsla\(/i, + /^var\(/, + /^inherit$/i, + /^initial$/i, + /^currentColor$/i, + /^transparent$/i, + /^unset$/i, + /^color-mix\(/i + ], + 'background-color': [ + /^#[0-9a-f]{3,8}$/i, + /^rgb\(/i, + /^rgba\(/i, + /^hsl\(/i, + /^hsla\(/i, + /^var\(/, + /^inherit$/i, + /^initial$/i, + /^transparent$/i, + /^unset$/i, + /^color-mix\(/i + ], + 'font-style': [/^[\w-]+$/, /^var\(/], + 'font-weight': [/^\d+$/, /^[\w-]+$/, /^var\(/], + 'text-decoration': [/^[\w\s-]+$/, /^var\(/] + } + } +}; + +function sanitizeShikiHtml(html: string): string { + return sanitizeHtml(html, SHIKI_HIGHLIGHT_SANITIZE); +} type CodeOverflowStateProps = WritableBoxedValues<{ collapsed: boolean; @@ -74,7 +125,7 @@ class CodeRootState { return this.opts.code.current; } - highlighted = $derived(DOMPurify.sanitize(this.highlight(this.code) ?? '')); + highlighted = $derived(sanitizeShikiHtml(this.highlight(this.code) ?? '')); } function within(num: number, range: CodeRootProps['highlight']) { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f6c588ba..515ead28 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -660,7 +660,7 @@ importers: version: link:../../packages/tsconfig '@sveltejs/adapter-cloudflare': specifier: ^7.0.0 - version: 7.2.8(@sveltejs/kit@2.49.2(@sveltejs/vite-plugin-svelte@4.0.4(svelte@5.46.0)(vite@5.4.21(@types/node@24.10.4)(lightningcss@1.31.1)(sass@1.97.1)))(svelte@5.46.0)(vite@5.4.21(@types/node@24.10.4)(lightningcss@1.31.1)(sass@1.97.1)))(wrangler@4.21.2(@cloudflare/workers-types@4.20260313.1)) + version: 7.2.8(@sveltejs/kit@2.49.2(@sveltejs/vite-plugin-svelte@4.0.4(svelte@5.46.0)(vite@5.4.21(@types/node@24.10.4)(lightningcss@1.31.1)(sass@1.97.1)))(svelte@5.46.0)(vite@5.4.21(@types/node@24.10.4)(lightningcss@1.31.1)(sass@1.97.1)))(wrangler@4.21.2(@cloudflare/workers-types@4.20260313.1)(bufferutil@4.1.0)(utf-8-validate@6.0.6)) '@sveltejs/kit': specifier: ^2.5.27 version: 2.49.2(@sveltejs/vite-plugin-svelte@4.0.4(svelte@5.46.0)(vite@5.4.21(@types/node@24.10.4)(lightningcss@1.31.1)(sass@1.97.1)))(svelte@5.46.0)(vite@5.4.21(@types/node@24.10.4)(lightningcss@1.31.1)(sass@1.97.1)) @@ -1120,6 +1120,9 @@ importers: plyr: specifier: ^3.7.8 version: 3.8.4 + sanitize-html: + specifier: ^2.17.2 + version: 2.17.2 simple-icons: specifier: ^16.9.0 version: 16.9.0 @@ -1166,6 +1169,9 @@ importers: '@types/dompurify': specifier: ^3.2.0 version: 3.2.0 + '@types/sanitize-html': + specifier: ^2.16.1 + version: 2.16.1 '@zxcvbn-ts/core': specifier: ^3.0.4 version: 3.0.4 @@ -1181,9 +1187,6 @@ importers: clsx: specifier: ^2.1.1 version: 2.1.1 - isomorphic-dompurify: - specifier: 2.25.0 - version: 2.25.0(bufferutil@4.1.0)(utf-8-validate@6.0.6) jsrepo: specifier: ^3.5.1 version: 3.5.1(svelte@5.46.0)(vue@3.5.26(typescript@5.9.3)) @@ -6008,6 +6011,9 @@ packages: '@types/resolve@1.20.2': resolution: {integrity: sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==} + '@types/sanitize-html@2.16.1': + resolution: {integrity: sha512-n9wjs8bCOTyN/ynwD8s/nTcTreIHB1vf31vhLMGqUPNHaweKC4/fAl4Dj+hUlCTKYgm4P3k83fmiFfzkZ6sgMA==} + '@types/semver@7.7.1': resolution: {integrity: sha512-FmgJfu+MOcQ370SD0ev7EI8TlCAfKYU+B4m5T3yXc1CiRN94g/SZPtsCkk506aUDtlMnFZvasDwHHUcZUEaYuA==} @@ -7752,8 +7758,8 @@ packages: resolution: {integrity: sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==} engines: {node: '>=0.12'} - entities@7.0.0: - resolution: {integrity: sha512-FDWG5cmEYf2Z00IkYRhbFrwIwvdFKH07uV8dvNy0omp/Qb1xcyCWp2UDtcwJF4QZZvk0sLudP6/hAu42TaqVhQ==} + entities@7.0.1: + resolution: {integrity: sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==} engines: {node: '>=0.12'} error-ex@1.3.4: @@ -8567,8 +8573,8 @@ packages: htmlparser2-svelte@4.1.0: resolution: {integrity: sha512-+4f4RBFz7Rj2Hp0ZbFbXC+Kzbd6S9PgjiuFtdT76VMNgKogrEZy0pG2UrPycPbrZzVEIM5lAT3lAdkSTCHLPjg==} - htmlparser2@10.0.0: - resolution: {integrity: sha512-TwAZM+zE5Tq3lrEHvOlvwgj1XLWQCtaaibSN11Q+gGBAS7Y1uZSWwXXRe4iF6OXnaq1riyQAPFOBtYc77Mxq0g==} + htmlparser2@10.1.0: + resolution: {integrity: sha512-VTZkM9GWRAtEpveh7MSF6SjjrpNVNNVJfFup7xTY3UpFtm67foy9HDVXneLtFVt4pMz5kZtgNcvCniNFb1hlEQ==} http-errors@2.0.1: resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==} @@ -8795,6 +8801,10 @@ packages: resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==} engines: {node: '>=12'} + is-plain-object@5.0.0: + resolution: {integrity: sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==} + engines: {node: '>=0.10.0'} + is-potential-custom-element-name@1.0.1: resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==} @@ -8879,10 +8889,6 @@ packages: isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} - isomorphic-dompurify@2.25.0: - resolution: {integrity: sha512-bcpJzu9DOjN21qaCVpcoCwUX1ytpvA6EFqCK5RNtPg5+F0Jz9PX50jl6jbEicBNeO87eDDfC7XtPs4zjDClZJg==} - engines: {node: '>=18'} - istanbul-lib-coverage@3.2.2: resolution: {integrity: sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==} engines: {node: '>=8'} @@ -10145,6 +10151,9 @@ packages: resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} engines: {node: '>=8'} + parse-srcset@1.0.2: + resolution: {integrity: sha512-/2qh0lav6CmI15FzA3i/2Bzk2zCgQhGMkvhOhKNcBVQ1ldgpbfiNTVslmooUmWJcADi1f1kIeynbDRVzNlfR6Q==} + parse5-htmlparser2-tree-adapter@7.1.0: resolution: {integrity: sha512-ruw5xyKs6lrpo9x9rCZqZZnIUntICjQAd0Wsmp396Ul9lN/h+ifgVV1x1gZHi8euej6wTfpqX8j+BFQxF0NS/g==} @@ -11082,6 +11091,9 @@ packages: sander@0.5.1: resolution: {integrity: sha512-3lVqBir7WuKDHGrKRDn/1Ye3kwpXaDOMsiRP1wd6wpZW56gJhsbp5RqQpA6JG/P+pkXizygnr1dKR8vzWaVsfA==} + sanitize-html@2.17.2: + resolution: {integrity: sha512-EnffJUl46VE9uvZ0XeWzObHLurClLlT12gsOk1cHyP2Ol1P0BnBnsXmShlBmWVJM+dKieQI68R0tsPY5m/B+Jg==} + sass@1.97.1: resolution: {integrity: sha512-uf6HoO8fy6ClsrShvMgaKUn14f2EHQLQRtpsZZLeU/Mv0Q1K5P0+x2uvH6Cub39TVVbWNSrraUhDAoFph6vh0A==} engines: {node: '>=14.0.0'} @@ -14181,7 +14193,7 @@ snapshots: '@better-auth/utils': 0.3.0 '@better-fetch/fetch': 1.1.21 '@standard-schema/spec': 1.1.0 - better-call: 1.1.8(zod@4.2.1) + better-call: 1.1.8(zod@4.3.6) jose: 6.1.3 kysely: 0.28.9 nanostores: 1.1.0 @@ -17059,7 +17071,7 @@ snapshots: worktop: 0.8.0-next.18 wrangler: 4.21.2(@cloudflare/workers-types@4.20260313.1)(bufferutil@4.1.0)(utf-8-validate@6.0.6) - '@sveltejs/adapter-cloudflare@7.2.8(@sveltejs/kit@2.49.2(@sveltejs/vite-plugin-svelte@4.0.4(svelte@5.46.0)(vite@5.4.21(@types/node@24.10.4)(lightningcss@1.31.1)(sass@1.97.1)))(svelte@5.46.0)(vite@5.4.21(@types/node@24.10.4)(lightningcss@1.31.1)(sass@1.97.1)))(wrangler@4.21.2(@cloudflare/workers-types@4.20260313.1))': + '@sveltejs/adapter-cloudflare@7.2.8(@sveltejs/kit@2.49.2(@sveltejs/vite-plugin-svelte@4.0.4(svelte@5.46.0)(vite@5.4.21(@types/node@24.10.4)(lightningcss@1.31.1)(sass@1.97.1)))(svelte@5.46.0)(vite@5.4.21(@types/node@24.10.4)(lightningcss@1.31.1)(sass@1.97.1)))(wrangler@4.21.2(@cloudflare/workers-types@4.20260313.1)(bufferutil@4.1.0)(utf-8-validate@6.0.6))': dependencies: '@cloudflare/workers-types': 4.20260313.1 '@sveltejs/kit': 2.49.2(@sveltejs/vite-plugin-svelte@4.0.4(svelte@5.46.0)(vite@5.4.21(@types/node@24.10.4)(lightningcss@1.31.1)(sass@1.97.1)))(svelte@5.46.0)(vite@5.4.21(@types/node@24.10.4)(lightningcss@1.31.1)(sass@1.97.1)) @@ -18280,6 +18292,10 @@ snapshots: '@types/resolve@1.20.2': {} + '@types/sanitize-html@2.16.1': + dependencies: + htmlparser2: 10.1.0 + '@types/semver@7.7.1': {} '@types/stack-utils@2.0.3': {} @@ -18655,7 +18671,7 @@ snapshots: dependencies: '@babel/parser': 7.28.5 '@vue/shared': 3.5.26 - entities: 7.0.0 + entities: 7.0.1 estree-walker: 2.0.2 source-map-js: 1.2.1 @@ -19024,7 +19040,7 @@ snapshots: '@better-fetch/fetch': 1.1.21 '@noble/ciphers': 2.1.1 '@noble/hashes': 2.0.1 - better-call: 1.1.8(zod@4.2.1) + better-call: 1.1.8(zod@4.3.6) defu: 6.1.4 jose: 6.1.3 kysely: 0.28.9 @@ -19070,15 +19086,6 @@ snapshots: vitest: 2.1.9(@types/node@24.10.4)(jsdom@26.1.0(bufferutil@4.1.0)(utf-8-validate@6.0.6))(lightningcss@1.31.1)(sass@1.97.1) vue: 3.5.26(typescript@5.9.3) - better-call@1.1.8(zod@4.2.1): - dependencies: - '@better-auth/utils': 0.3.0 - '@better-fetch/fetch': 1.1.21 - rou3: 0.7.12 - set-cookie-parser: 2.7.2 - optionalDependencies: - zod: 4.2.1 - better-call@1.1.8(zod@4.3.6): dependencies: '@better-auth/utils': 0.3.0 @@ -19334,7 +19341,7 @@ snapshots: domhandler: 5.0.3 domutils: 3.2.2 encoding-sniffer: 0.2.1 - htmlparser2: 10.0.0 + htmlparser2: 10.1.0 parse5: 7.3.0 parse5-htmlparser2-tree-adapter: 7.1.0 parse5-parser-stream: 7.1.2 @@ -20164,7 +20171,7 @@ snapshots: entities@6.0.1: {} - entities@7.0.0: {} + entities@7.0.1: {} error-ex@1.3.4: dependencies: @@ -21280,12 +21287,12 @@ snapshots: domutils: 2.8.0 entities: 2.2.0 - htmlparser2@10.0.0: + htmlparser2@10.1.0: dependencies: domelementtype: 2.3.0 domhandler: 5.0.3 domutils: 3.2.2 - entities: 6.0.1 + entities: 7.0.1 http-errors@2.0.1: dependencies: @@ -21505,6 +21512,8 @@ snapshots: is-plain-obj@4.1.0: {} + is-plain-object@5.0.0: {} + is-potential-custom-element-name@1.0.1: {} is-promise@4.0.0: {} @@ -21580,16 +21589,6 @@ snapshots: isexe@2.0.0: {} - isomorphic-dompurify@2.25.0(bufferutil@4.1.0)(utf-8-validate@6.0.6): - dependencies: - dompurify: 3.3.1 - jsdom: 26.1.0(bufferutil@4.1.0)(utf-8-validate@6.0.6) - transitivePeerDependencies: - - bufferutil - - canvas - - supports-color - - utf-8-validate - istanbul-lib-coverage@3.2.2: {} istanbul-lib-instrument@5.2.1: @@ -22064,6 +22063,7 @@ snapshots: - bufferutil - supports-color - utf-8-validate + optional: true jsesc@3.1.0: {} @@ -23400,6 +23400,8 @@ snapshots: json-parse-even-better-errors: 2.3.1 lines-and-columns: 1.2.4 + parse-srcset@1.0.2: {} + parse5-htmlparser2-tree-adapter@7.1.0: dependencies: domhandler: 5.0.3 @@ -24355,6 +24357,15 @@ snapshots: mkdirp: 0.5.6 rimraf: 2.7.1 + sanitize-html@2.17.2: + dependencies: + deepmerge: 4.3.1 + escape-string-regexp: 4.0.0 + htmlparser2: 10.1.0 + is-plain-object: 5.0.0 + parse-srcset: 1.0.2 + postcss: 8.5.6 + sass@1.97.1: dependencies: chokidar: 4.0.3