260 lines
		
	
	
		
			6.7 KiB
		
	
	
	
		
			Markdown
		
	
	
	
	
	
			
		
		
	
	
			260 lines
		
	
	
		
			6.7 KiB
		
	
	
	
		
			Markdown
		
	
	
	
	
	
| # Excalidraw Implementation - Obsidian Format Support
 | |
| 
 | |
| ## Overview
 | |
| 
 | |
| ObsiViewer now fully supports Obsidian's Excalidraw plugin format, including:
 | |
| - ✅ Reading `.excalidraw.md` files with LZ-String compressed data
 | |
| - ✅ Writing files in Obsidian-compatible format
 | |
| - ✅ Preserving front matter and metadata
 | |
| - ✅ Round-trip compatibility (ObsiViewer ⇄ Obsidian)
 | |
| - ✅ Backward compatibility with legacy flat JSON format
 | |
| - ✅ Migration tool for converting old files
 | |
| 
 | |
| ## File Format
 | |
| 
 | |
| ### Obsidian Format (`.excalidraw.md`)
 | |
| 
 | |
| ```markdown
 | |
| ---
 | |
| excalidraw-plugin: parsed
 | |
| tags: [excalidraw]
 | |
| ---
 | |
| ==⚠  Switch to EXCALIDRAW VIEW in the MORE OPTIONS menu of this document. ⚠==
 | |
| 
 | |
| # Excalidraw Data
 | |
| 
 | |
| ## Text Elements
 | |
| %%
 | |
| ## Drawing
 | |
| ```compressed-json
 | |
| <LZ-STRING_BASE64_COMPRESSED_DATA>
 | |
| ```
 | |
| %%
 | |
| ```
 | |
| 
 | |
| The `compressed-json` block contains the Excalidraw scene data compressed using **LZ-String** (`compressToBase64`/`decompressFromBase64`).
 | |
| 
 | |
| ### Legacy Format (`.excalidraw`, `.json`)
 | |
| 
 | |
| Plain JSON format:
 | |
| ```json
 | |
| {
 | |
|   "elements": [...],
 | |
|   "appState": {...},
 | |
|   "files": {...}
 | |
| }
 | |
| ```
 | |
| 
 | |
| ## Architecture
 | |
| 
 | |
| ### Backend (`server/`)
 | |
| 
 | |
| #### `excalidraw-obsidian.mjs`
 | |
| Core utilities for parsing and serializing Excalidraw files:
 | |
| 
 | |
| - **`parseObsidianExcalidrawMd(md)`** - Parse Obsidian format, decompress LZ-String
 | |
| - **`parseFlatJson(text)`** - Parse legacy JSON format
 | |
| - **`toObsidianExcalidrawMd(data, frontMatter?)`** - Convert to Obsidian format
 | |
| - **`extractFrontMatter(md)`** - Extract YAML front matter
 | |
| - **`parseExcalidrawAny(text)`** - Auto-detect and parse any format
 | |
| - **`isValidExcalidrawScene(data)`** - Validate scene structure
 | |
| 
 | |
| #### API Routes (`server/index.mjs`)
 | |
| 
 | |
| **GET `/api/files?path=<file>`**
 | |
| - Accepts `path` as query parameter (properly URL-encoded)
 | |
| - Returns parsed Excalidraw scene as JSON
 | |
| - Supports `.excalidraw.md`, `.excalidraw`, `.json`
 | |
| - Sets `ETag` header for conflict detection
 | |
| 
 | |
| **PUT `/api/files?path=<file>`**
 | |
| - Accepts `path` as query parameter
 | |
| - Content-Type: `application/json` (Excalidraw scene)
 | |
| - Automatically converts to Obsidian format for `.excalidraw.md` files
 | |
| - Preserves existing front matter
 | |
| - Supports `If-Match` header for conflict detection
 | |
| - Atomic write with backup (`.bak`)
 | |
| 
 | |
| **PUT `/api/files/blob?path=<file>`**
 | |
| - For binary sidecars (PNG/SVG exports)
 | |
| - Accepts `path` as query parameter
 | |
| 
 | |
| ### Frontend (`src/app/features/drawings/`)
 | |
| 
 | |
| #### `excalidraw-io.service.ts`
 | |
| Frontend parsing and serialization service:
 | |
| 
 | |
| - **`parseObsidianMd(md)`** - Parse Obsidian format
 | |
| - **`parseFlatJson(text)`** - Parse legacy format
 | |
| - **`toObsidianMd(data, frontMatter?)`** - Convert to Obsidian format
 | |
| - **`parseAny(text)`** - Auto-detect format
 | |
| - **`isValidScene(data)`** - Validate scene
 | |
| 
 | |
| #### `drawings-file.service.ts`
 | |
| HTTP service for file operations:
 | |
| 
 | |
| - **`get(path)`** - Load Excalidraw file
 | |
| - **`put(path, scene)`** - Save with conflict detection
 | |
| - **`putForce(path, scene)`** - Force overwrite
 | |
| - **`putBinary(path, blob, mime)`** - Save binary sidecar
 | |
| 
 | |
| All methods use query parameters for proper URL encoding.
 | |
| 
 | |
| #### `drawings-editor.component.ts`
 | |
| Editor component with:
 | |
| 
 | |
| - Auto-save (debounced)
 | |
| - Conflict detection and resolution
 | |
| - Manual save (Ctrl+S)
 | |
| - Export to PNG/SVG
 | |
| - Theme synchronization
 | |
| 
 | |
| ## Usage
 | |
| 
 | |
| ### Opening Files
 | |
| 
 | |
| Files are automatically detected and parsed:
 | |
| 
 | |
| ```typescript
 | |
| // Backend automatically detects format
 | |
| GET /api/files?path=drawing.excalidraw.md
 | |
| // Returns: { elements: [...], appState: {...}, files: {...} }
 | |
| ```
 | |
| 
 | |
| ### Saving Files
 | |
| 
 | |
| The backend automatically converts to Obsidian format:
 | |
| 
 | |
| ```typescript
 | |
| // Frontend sends JSON
 | |
| PUT /api/files?path=drawing.excalidraw.md
 | |
| Content-Type: application/json
 | |
| { elements: [...], appState: {...}, files: {...} }
 | |
| 
 | |
| // Backend writes Obsidian format with LZ-String compression
 | |
| ```
 | |
| 
 | |
| ### Migration
 | |
| 
 | |
| Convert old flat JSON files to Obsidian format:
 | |
| 
 | |
| ```bash
 | |
| # Dry run (preview changes)
 | |
| npm run migrate:excalidraw:dry
 | |
| 
 | |
| # Apply migration
 | |
| npm run migrate:excalidraw
 | |
| 
 | |
| # Custom vault path
 | |
| node server/migrate-excalidraw.mjs --vault-path=/path/to/vault
 | |
| ```
 | |
| 
 | |
| The migration script:
 | |
| - Scans for `.excalidraw` and `.json` files
 | |
| - Validates Excalidraw structure
 | |
| - Converts to `.excalidraw.md` with Obsidian format
 | |
| - Creates `.bak` backups
 | |
| - Removes original files
 | |
| 
 | |
| ## Testing
 | |
| 
 | |
| ### Unit Tests
 | |
| 
 | |
| ```bash
 | |
| # Run backend utility tests
 | |
| npm run test:excalidraw
 | |
| ```
 | |
| 
 | |
| Tests cover:
 | |
| - ✅ Front matter extraction
 | |
| - ✅ Obsidian format parsing
 | |
| - ✅ LZ-String compression/decompression
 | |
| - ✅ Round-trip conversion
 | |
| - ✅ Legacy JSON parsing
 | |
| - ✅ Edge cases (empty scenes, special characters, large files)
 | |
| 
 | |
| ### E2E Tests
 | |
| 
 | |
| ```bash
 | |
| # Run Playwright tests
 | |
| npm run test:e2e -- excalidraw.spec.ts
 | |
| ```
 | |
| 
 | |
| Tests cover:
 | |
| - ✅ Editor loading
 | |
| - ✅ API endpoints with query params
 | |
| - ✅ Obsidian format parsing
 | |
| - ✅ File structure validation
 | |
| 
 | |
| ## Compatibility
 | |
| 
 | |
| ### Obsidian → ObsiViewer
 | |
| ✅ Open `.excalidraw.md` files created in Obsidian  
 | |
| ✅ Render drawings correctly  
 | |
| ✅ Preserve all metadata and front matter
 | |
| 
 | |
| ### ObsiViewer → Obsidian
 | |
| ✅ Save in Obsidian-compatible format  
 | |
| ✅ Files open correctly in Obsidian  
 | |
| ✅ No warnings or data loss  
 | |
| ✅ Front matter preserved
 | |
| 
 | |
| ### Legacy Support
 | |
| ✅ Open old flat JSON files  
 | |
| ✅ Automatically convert on save  
 | |
| ✅ Migration tool available
 | |
| 
 | |
| ## Key Changes from Previous Implementation
 | |
| 
 | |
| ### Backend
 | |
| - ❌ Removed `zlib` decompression (wrong algorithm)
 | |
| - ✅ Added **LZ-String** support (`lz-string` package)
 | |
| - ❌ Removed splat routes (`/api/files/*splat`)
 | |
| - ✅ Added query param routes (`/api/files?path=...`)
 | |
| - ✅ Proper URL encoding/decoding
 | |
| - ✅ Front matter preservation
 | |
| - ✅ Atomic writes with backups
 | |
| 
 | |
| ### Frontend
 | |
| - ✅ Created `ExcalidrawIoService` for parsing
 | |
| - ✅ Updated all HTTP calls to use query params
 | |
| - ✅ Proper `encodeURIComponent` usage
 | |
| - ✅ No more 400 errors on special characters
 | |
| 
 | |
| ## Troubleshooting
 | |
| 
 | |
| ### 400 Bad Request
 | |
| **Cause**: Path not properly encoded or missing query parameter  
 | |
| **Fix**: Ensure using `?path=` query param, not URL path
 | |
| 
 | |
| ### Invalid Excalidraw Format
 | |
| **Cause**: Corrupted compressed data or wrong compression algorithm  
 | |
| **Fix**: Check file was created with LZ-String, not zlib
 | |
| 
 | |
| ### Conflict Detected (409)
 | |
| **Cause**: File modified externally while editing  
 | |
| **Fix**: Use "Reload from disk" or "Overwrite" buttons in UI
 | |
| 
 | |
| ### File Opens in Obsidian but Not ObsiViewer
 | |
| **Cause**: Unsupported Excalidraw version or corrupted data  
 | |
| **Fix**: Check console for parsing errors, validate JSON structure
 | |
| 
 | |
| ## Dependencies
 | |
| 
 | |
| - **Backend**: `lz-string` (^1.5.0)
 | |
| - **Frontend**: `lz-string` (^1.5.0)
 | |
| 
 | |
| ## Future Enhancements
 | |
| 
 | |
| - [ ] Support for Excalidraw libraries
 | |
| - [ ] Embedded images optimization
 | |
| - [ ] Collaborative editing
 | |
| - [ ] Version history
 | |
| - [ ] Template system
 | |
| 
 | |
| ## References
 | |
| 
 | |
| - [Obsidian Excalidraw Plugin](https://github.com/zsviczian/obsidian-excalidraw-plugin)
 | |
| - [LZ-String](https://github.com/pieroxy/lz-string)
 | |
| - [Excalidraw](https://excalidraw.com/)
 |