@@ -67,6 +67,7 @@ export function Excalidraw({
6767 const [ loading , setLoading ] = useState ( true ) ;
6868 const [ isClient , setIsClient ] = useState ( false ) ;
6969 const [ viewMode , setViewMode ] = useState < 'overview' | 'slide' > ( 'overview' ) ;
70+ const [ zoom , setZoom ] = useState ( 100 ) ;
7071
7172 // The raw viewBox of the entire exported SVG
7273 const [ rawViewBox , setRawViewBox ] = useState < number [ ] | null > ( null ) ;
@@ -92,7 +93,6 @@ export function Excalidraw({
9293 if ( ! res . ok ) throw new Error ( `Failed to load: ${ res . status } ` ) ;
9394
9495 const textContent = await res . text ( ) ;
95- let json ;
9696 // Match lines like "hash: $$formula$$" for LaTeX extraction
9797 const latexMap : Record < string , string > = { } ;
9898 const latexLines = textContent . match ( / ^ [ a - f 0 - 9 ] { 40 } : \$ \$ .* ?\$ \$ / gm) ;
@@ -107,32 +107,30 @@ export function Excalidraw({
107107 } ) ;
108108 }
109109
110- // Try parsing as standard JSON first
111- try {
112- json = JSON . parse ( textContent ) ;
113- } catch ( e ) {
114- // If not JSON, try parsing as Obsidian-Excalidraw Markdown
115- // Look for ```compressed-json ... ``` block
116- const match = textContent . match ( / ` ` ` c o m p r e s s e d - j s o n \s * ( [ \s \S ] * ?) ` ` ` / ) ;
117- if ( match ) {
118- // Remove all whitespace (newlines, spaces) as LZString expects a continuous string
119- const compressed = match [ 1 ] . replace ( / \s / g, '' ) ;
120- const decompressed = LZString . decompressFromBase64 ( compressed ) ;
121- if ( decompressed ) {
122- json = JSON . parse ( decompressed ) ;
123-
124- // Populate json.files with LaTeX renders if we found any
125- if ( Object . keys ( latexMap ) . length > 0 ) {
126- json . files = json . files || { } ;
127- for ( const [ id , formula ] of Object . entries ( latexMap ) ) {
128- // Find corresponding image element to get its intended size
129- const el = json . elements ?. find ( ( e : any ) => e . fileId === id && ! e . isDeleted ) ;
130- if ( ! el ) continue ;
131-
132- try {
133- const html = katex . renderToString ( formula , { displayMode : true , throwOnError : false } ) ;
134- // Create a self-contained SVG with inlined KaTeX CSS
135- const svgString = `
110+ let json ;
111+ // Only support Obsidian-Excalidraw Markdown
112+ const match = textContent . match ( / ` ` ` c o m p r e s s e d - j s o n \s * ( [ \s \S ] * ?) ` ` ` / ) ;
113+ if ( ! match ) return ;
114+
115+ // Remove all whitespace (newlines, spaces) as LZString expects a continuous string
116+ const compressed = match [ 1 ] . replace ( / \s / g, '' ) ;
117+ const decompressed = LZString . decompressFromBase64 ( compressed ) ;
118+ if ( ! decompressed ) return ;
119+
120+ json = JSON . parse ( decompressed ) ;
121+
122+ // Populate json.files with LaTeX renders if we found any
123+ if ( Object . keys ( latexMap ) . length > 0 ) {
124+ json . files = json . files || { } ;
125+ for ( const [ id , formula ] of Object . entries ( latexMap ) ) {
126+ // Find corresponding image element to get its intended size
127+ const el = json . elements ?. find ( ( e : any ) => e . fileId === id && ! e . isDeleted ) ;
128+ if ( ! el ) continue ;
129+
130+ try {
131+ const html = katex . renderToString ( formula , { displayMode : true , throwOnError : false } ) ;
132+ // Create a self-contained SVG with inlined KaTeX CSS
133+ const svgString = `
136134<svg xmlns="http://www.w3.org/2000/svg" width="${ el . width } " height="${ el . height } ">
137135 <foreignObject width="100%" height="100%">
138136 <div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: center; justify-content: center; width: 100%; height: 100%; overflow: hidden;">
@@ -145,23 +143,16 @@ export function Excalidraw({
145143 </div>
146144 </foreignObject>
147145</svg>` . trim ( ) ;
148- const dataURL = `data:image/svg+xml;base64,${ btoa ( unescape ( encodeURIComponent ( svgString ) ) ) } ` ;
149- json . files [ id ] = {
150- mimeType : "image/svg+xml" ,
151- id,
152- dataURL,
153- created : Date . now ( )
154- } ;
155- } catch ( err ) {
156- console . error ( "KaTeX rendering failed for ID:" , id , err ) ;
157- }
158- }
159- }
160- } else {
161- throw new Error ( "Failed to decompress Excalidraw data" ) ;
146+ const dataURL = `data:image/svg+xml;base64,${ btoa ( unescape ( encodeURIComponent ( svgString ) ) ) } ` ;
147+ json . files [ id ] = {
148+ mimeType : "image/svg+xml" ,
149+ id,
150+ dataURL,
151+ created : Date . now ( )
152+ } ;
153+ } catch ( err ) {
154+ console . error ( "KaTeX rendering failed for ID:" , id , err ) ;
162155 }
163- } else {
164- throw new Error ( "Invalid Excalidraw file format" ) ;
165156 }
166157 }
167158
@@ -191,8 +182,12 @@ export function Excalidraw({
191182 if ( svgRef . current ) {
192183 svgRef . current . setAttribute ( 'viewBox' , vb . join ( ' ' ) ) ;
193184 currentViewBoxRef . current = vb ;
185+ if ( rawViewBox ) {
186+ const z = Math . round ( ( rawViewBox [ 2 ] / vb [ 2 ] ) * 100 ) ;
187+ setZoom ( z ) ;
188+ }
194189 }
195- } , [ ] ) ;
190+ } , [ rawViewBox ] ) ;
196191
197192 // 2. Render SVG
198193 useEffect ( ( ) => {
@@ -383,6 +378,22 @@ export function Excalidraw({
383378 }
384379 } ;
385380
381+ useEffect ( ( ) => {
382+ const handleKeyDown = ( e : KeyboardEvent ) => {
383+ if ( viewMode !== 'slide' || frames . length === 0 ) return ;
384+ if ( e . key === 'ArrowRight' ) {
385+ e . preventDefault ( ) ;
386+ goToSlide ( ( currentSlide + 1 ) % frames . length ) ;
387+ }
388+ if ( e . key === 'ArrowLeft' ) {
389+ e . preventDefault ( ) ;
390+ goToSlide ( ( currentSlide - 1 + frames . length ) % frames . length ) ;
391+ }
392+ } ;
393+ window . addEventListener ( 'keydown' , handleKeyDown ) ;
394+ return ( ) => window . removeEventListener ( 'keydown' , handleKeyDown ) ;
395+ } , [ viewMode , currentSlide , frames . length ] ) ;
396+
386397 // Drag Handlers
387398 const handleMouseDown = ( e : React . MouseEvent ) => {
388399 e . preventDefault ( ) ;
@@ -520,6 +531,7 @@ export function Excalidraw({
520531 </ div >
521532
522533 < div className = "flex items-center gap-1 w-24 justify-end" >
534+ < span className = "text-[10px] font-black text-gray-400 mr-1" > { zoom } %</ span >
523535 < button
524536 onClick = { ( ) => {
525537 syncView ( ) ;
0 commit comments