1- import React , { useEffect } from 'react' ;
1+ import React , { useEffect , useState } from 'react' ;
22import ReactMarkdown , { defaultUrlTransform } from 'react-markdown' ;
33import remarkGfm from 'remark-gfm' ;
44import remarkEmoji from 'remark-emoji' ;
@@ -17,6 +17,8 @@ interface PreviewComponentProps {
1717 isHorizontal : boolean ;
1818 initializeMermaid : ( ) => void ;
1919 plainTextPreview ?: boolean ;
20+ currentFilePath ?: string | null ;
21+ currentDirHandle ?: any ;
2022}
2123
2224const PreviewComponent : React . FC < PreviewComponentProps > = React . memo ( ( {
@@ -25,7 +27,9 @@ const PreviewComponent: React.FC<PreviewComponentProps> = React.memo(({
2527 isPreviewFull,
2628 isHorizontal,
2729 initializeMermaid,
28- plainTextPreview
30+ plainTextPreview,
31+ currentFilePath,
32+ currentDirHandle
2933} ) => {
3034 // Custom remark plugin to preserve blank lines between list items
3135 const preserveListBreaks = ( ) => {
@@ -153,7 +157,105 @@ const PreviewComponent: React.FC<PreviewComponentProps> = React.memo(({
153157 } }
154158 components = { {
155159 img ( props ) {
156- return < img { ...props } style = { { maxWidth : '100%' , ...( props . style as React . CSSProperties ) } } /> ;
160+ const AsyncImage = ( { src, alt, style, ...rest } : any ) => {
161+ const [ resolvedSrc , setResolvedSrc ] = useState ( src ) ;
162+
163+ useEffect ( ( ) => {
164+ let objectUrl = '' ;
165+
166+ const resolveImg = async ( ) => {
167+ if ( ! src || src . startsWith ( 'http' ) || src . startsWith ( 'data:' ) || src . startsWith ( 'blob:' ) || src . startsWith ( 'tauri://' ) || src . startsWith ( 'asset://' ) ) {
168+ return ;
169+ }
170+
171+ // Check if running in Tauri
172+ const isTauri = typeof window !== 'undefined' && ( ( window as any ) . __TAURI_INTERNALS__ || ( window as any ) . __TAURI__ ) ;
173+
174+ if ( isTauri && currentFilePath ) {
175+ try {
176+ // Use plugin-fs to read file and create object URL, bypassing assetScopes
177+ const { readFile } = await import ( '@tauri-apps/plugin-fs' ) ;
178+ // currentFilePath is usually absolute. e.g. /home/user/repo/docs/README.md
179+ const dirPath = currentFilePath . replace ( / \\ / g, '/' ) . split ( '/' ) . slice ( 0 , - 1 ) . join ( '/' ) ;
180+
181+ let cleanSrc = src ;
182+ let absolutePath = '' ;
183+
184+ if ( cleanSrc . startsWith ( './' ) ) {
185+ absolutePath = `${ dirPath } /${ cleanSrc . substring ( 2 ) } ` ;
186+ } else if ( cleanSrc . startsWith ( '../' ) ) {
187+ const parts = dirPath . split ( '/' ) ;
188+ const srcParts = cleanSrc . split ( '/' ) ;
189+ for ( let p of srcParts ) {
190+ if ( p === '..' ) parts . pop ( ) ;
191+ else if ( p !== '.' ) parts . push ( p ) ;
192+ }
193+ absolutePath = parts . join ( '/' ) ;
194+ } else {
195+ absolutePath = `${ dirPath } /${ cleanSrc } ` ;
196+ }
197+
198+ const fileData = await readFile ( absolutePath ) ;
199+ const blob = new Blob ( [ fileData ] ) ;
200+ objectUrl = URL . createObjectURL ( blob ) ;
201+ setResolvedSrc ( objectUrl ) ;
202+ } catch ( e ) {
203+ console . error ( 'Failed to load Tauri fs core for image resolution:' , e ) ;
204+ }
205+ return ;
206+ }
207+
208+ // Check Web with File System Access API
209+ if ( ! isTauri && currentDirHandle && currentFilePath ) {
210+ try {
211+ // Attempt to traverse from currentDirHandle directly, assuming src is relative to current file
212+ const getFileHandleFromPath = async ( dirHandle : any , path : string ) => {
213+ const parts = path . split ( '/' ) . filter ( p => p && p !== '.' ) ;
214+ let currentHandle = dirHandle ;
215+ for ( let i = 0 ; i < parts . length - 1 ; i ++ ) {
216+ if ( parts [ i ] === '..' ) throw new Error ( "Parent traversal not fully supported without root handle." ) ;
217+ currentHandle = await currentHandle . getDirectoryHandle ( parts [ i ] ) ;
218+ }
219+ const fileName = parts [ parts . length - 1 ] ;
220+ return await currentHandle . getFileHandle ( fileName ) ;
221+ } ;
222+
223+ // We need the relative path from the dirHandle
224+ // If currentFilePath is a relative path starting from dirHandle's root
225+ const dirPath = currentFilePath . split ( / [ / \\ ] / ) . slice ( 0 , - 1 ) . join ( '/' ) ;
226+ let fetchPath = dirPath ? `${ dirPath } /${ src } ` : src ;
227+
228+ // Normalize fetchPath relative to dirHandle
229+ const fetchParts = fetchPath . split ( '/' ) ;
230+ const normalizedParts = [ ] ;
231+ for ( const p of fetchParts ) {
232+ if ( p === '..' ) normalizedParts . pop ( ) ;
233+ else if ( p !== '.' && p ) normalizedParts . push ( p ) ;
234+ }
235+ fetchPath = normalizedParts . join ( '/' ) ;
236+
237+ const fileHandle = await getFileHandleFromPath ( currentDirHandle , fetchPath ) ;
238+ const file = await fileHandle . getFile ( ) ;
239+ objectUrl = URL . createObjectURL ( file ) ;
240+ setResolvedSrc ( objectUrl ) ;
241+
242+ } catch ( e ) {
243+ console . warn ( 'Failed to resolve web local image:' , e ) ;
244+ }
245+ }
246+ } ;
247+
248+ resolveImg ( ) ;
249+
250+ return ( ) => {
251+ if ( objectUrl ) URL . revokeObjectURL ( objectUrl ) ;
252+ } ;
253+ } , [ src ] ) ;
254+
255+ return < img src = { resolvedSrc } alt = { alt } style = { { maxWidth : '100%' , ...( style as React . CSSProperties ) } } { ...rest } /> ;
256+ } ;
257+
258+ return < AsyncImage { ...props } /> ;
157259 } ,
158260 li ( { children, className } ) {
159261 return < li className = { className } > { children } </ li > ;
0 commit comments