@@ -8,8 +8,8 @@ import DocViewer, {
88
99import { cn } from "../../utils" ;
1010import MarkdownDocRenderer from "./markdown-viewer" ;
11+ import SvgDocRenderer from "./svg-viewer" ;
1112import "@cyntler/react-doc-viewer/dist/index.css" ;
12- import "./file-viewer.css" ;
1313
1414interface FileViewerProps {
1515 /** URL of the file to display. Supports https:, http:, blob:, and data: URIs. */
@@ -20,6 +20,19 @@ interface FileViewerProps {
2020 config ?: IConfig ;
2121 /** Class applied to the outer wrapper. */
2222 className ?: string ;
23+ /**
24+ * Opt-in allowlist of `data:` URI MIME types that bypass the script-execution
25+ * gate. By default the viewer rejects `data:` URLs whose MIME can execute
26+ * code on top-frame navigation: `text/html`, `application/xhtml+xml`,
27+ * `application/xml`, `text/xml`, `image/svg+xml`.
28+ *
29+ * Pass entries here only when the URL source is trusted. `image/svg+xml`
30+ * is rendered through `<img>` (W3C secure static mode — scripts and
31+ * external references are disabled by the browser) so opting it in stays
32+ * safe; the other entries lose the defense-in-depth gate against the
33+ * fallback "open in new tab" link.
34+ */
35+ allowedDataMimes ?: readonly string [ ] ;
2336}
2437
2538const SAFE_URL_SCHEMES = new Set ( [ "http:" , "https:" , "blob:" , "data:" ] ) ;
@@ -44,8 +57,14 @@ const UNSAFE_DATA_MIMES = new Set([
4457 * Returns true if `raw` is a relative URL or uses an allowlisted scheme.
4558 * Blocks `javascript:`, `vbscript:`, `file:`, `about:`, and anything else
4659 * that could execute code or escape sandboxing when clicked.
60+ *
61+ * `allowedDataMimes` is a consumer-supplied override for the default
62+ * `UNSAFE_DATA_MIMES` blocklist — entries here pass even if blocklisted.
4763 */
48- const isSafeUrl = ( raw : string ) : boolean => {
64+ const isSafeUrl = (
65+ raw : string ,
66+ allowedDataMimes : ReadonlySet < string > ,
67+ ) : boolean => {
4968 if ( typeof raw !== "string" || raw . length === 0 ) return false ;
5069 let parsed : URL ;
5170 try {
@@ -71,53 +90,12 @@ const isSafeUrl = (raw: string): boolean => {
7190 } catch {
7291 return false ;
7392 }
93+ if ( allowedDataMimes . has ( mime ) ) return true ;
7494 if ( UNSAFE_DATA_MIMES . has ( mime ) ) return false ;
7595 }
7696 return true ;
7797} ;
7898
79- type PdfjsLib = {
80- version ?: string ;
81- GlobalWorkerOptions ?: { workerSrc ?: string } ;
82- } ;
83-
84- const getPdfjs = ( ) : PdfjsLib | undefined =>
85- typeof globalThis === "undefined"
86- ? undefined
87- : ( globalThis as unknown as { pdfjsLib ?: PdfjsLib } ) . pdfjsLib ;
88-
89- const buildDefaultPdfWorkerSrc = ( ) : string => {
90- // doc-viewer bundles its own pdfjs and assigns it to globalThis.pdfjsLib at
91- // module init; reading the version from there guarantees the worker we load
92- // matches the runtime pdfjs (mismatched versions throw "incompatible worker").
93- // Fallback constant is the version doc-viewer@1.17.x ships with.
94- const version = getPdfjs ( ) ?. version ?? "4.3.136" ;
95- return `https://cdn.jsdelivr.net/npm/pdfjs-dist@${ version } /build/pdf.worker.min.mjs` ;
96- } ;
97-
98- const setPdfWorkerSrc = ( src : string ) : void => {
99- const lib = getPdfjs ( ) ;
100- if ( lib ?. GlobalWorkerOptions && lib . GlobalWorkerOptions . workerSrc !== src ) {
101- lib . GlobalWorkerOptions . workerSrc = src ;
102- }
103- } ;
104-
105- // Module init: doc-viewer's module top-level code points workerSrc at
106- // unpkg.com. Override it immediately so no unpkg request ever fires (the
107- // worker is only fetched when a PDF is rendered, which is always after this).
108- setPdfWorkerSrc ( buildDefaultPdfWorkerSrc ( ) ) ;
109-
110- /**
111- * Override the PDF.js worker URL globally. Call once at app startup if you
112- * want to self-host the worker (e.g. under strict CSP) instead of using the
113- * default version-pinned jsDelivr URL. The worker is a process-wide singleton
114- * on `globalThis.pdfjsLib.GlobalWorkerOptions` — there is no per-instance
115- * configuration.
116- */
117- export const configurePdfWorker = ( src : string ) : void => {
118- setPdfWorkerSrc ( src ) ;
119- } ;
120-
12199const defaultConfig : IConfig = {
122100 header : {
123101 disableHeader : true ,
@@ -142,8 +120,8 @@ const docTheme: ITheme = {
142120const UnsupportedUrlMessage = ( { url } : { url : string } ) => (
143121 < div
144122 className = { cn (
145- "text-klerosUIComponentsSecondaryText" ,
146- "flex flex-col gap-2 p-6 text-sm " ,
123+ "text-klerosUIComponentsSecondaryText text-sm " ,
124+ "flex flex-col gap-2 p-6" ,
147125 ) }
148126 >
149127 < p > Unable to display this file.</ p >
@@ -160,8 +138,8 @@ const NoRendererFallback = ({
160138} ) => (
161139 < div
162140 className = { cn (
163- "text-klerosUIComponentsPrimaryText" ,
164- "flex flex-col items-start gap-3 p-6 text-sm " ,
141+ "text-klerosUIComponentsPrimaryText text-sm " ,
142+ "flex flex-col items-start gap-3 p-6" ,
165143 ) }
166144 >
167145 < p > This file type can't be previewed.</ p >
@@ -190,13 +168,18 @@ function FileViewer({
190168 fileName,
191169 config,
192170 className,
171+ allowedDataMimes,
193172} : Readonly < FileViewerProps > ) {
194- const safe = isSafeUrl ( url ) ;
173+ const allowedDataMimesSet = useMemo (
174+ ( ) => new Set ( ( allowedDataMimes ?? [ ] ) . map ( ( m ) => m . toLowerCase ( ) ) ) ,
175+ [ allowedDataMimes ] ,
176+ ) ;
177+ const safe = isSafeUrl ( url , allowedDataMimesSet ) ;
195178
196179 const docs = useMemo ( ( ) => [ { uri : url , fileName } ] , [ url , fileName ] ) ;
197180
198181 const pluginRenderers = useMemo (
199- ( ) => [ ...DocViewerRenderers , MarkdownDocRenderer ] ,
182+ ( ) => [ ...DocViewerRenderers , MarkdownDocRenderer , SvgDocRenderer ] ,
200183 [ ] ,
201184 ) ;
202185
@@ -239,6 +222,19 @@ function FileViewer({
239222 "[&_#image-renderer]:!h-auto" ,
240223 "[&_#image-renderer]:!flex-none" ,
241224 "[&_#image-renderer]:!px-6" ,
225+ // Transparency checkerboard. The gradient is inlined in the
226+ // arbitrary property (rather than a theme token) so the inner
227+ // `var(--klerosUIComponentsImageCheckerColor)` resolves at this
228+ // element's cascade — consumers can override that single variable
229+ // anywhere up the tree and the color propagates. A theme-token
230+ // wrapper would bake the inner var() at `:root` and the override
231+ // would silently no-op for descendants. Full arbitrary-property
232+ // syntax (not `bg-*` shortcut) so `tailwind-merge` recognizes this
233+ // as background-image and doesn't collide with the bg-color above.
234+ // eslint-disable-next-line max-len
235+ "[&_#image-renderer]:![background-image:linear-gradient(45deg,var(--klerosUIComponentsImageCheckerColor)_25%,transparent_25%),linear-gradient(-45deg,var(--klerosUIComponentsImageCheckerColor)_25%,transparent_25%),linear-gradient(45deg,transparent_75%,var(--klerosUIComponentsImageCheckerColor)_75%),linear-gradient(-45deg,transparent_75%,var(--klerosUIComponentsImageCheckerColor)_75%)]" ,
236+ "[&_#image-renderer]:![background-size:20px_20px]" ,
237+ "[&_#image-renderer]:![background-position:0_0,0_10px,10px_-10px,-10px_0px]" ,
242238 "[&_#image-img]:!max-w-full" ,
243239 "[&_#image-img]:!max-h-[80vh]" ,
244240 "[&_[class*='--loading']]:text-klerosUIComponentsSecondaryText" ,
0 commit comments