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/)
|