feat(excalidraw): support .excalidraw.md (Obsidian plugin format) with lz-string decompression
CI / lint (push) Failing after 14s
CI / test (push) Has been skipped
CI / build (push) Has been skipped
CI / e2e (push) Has been skipped
CI / security (push) Successful in 13s
Desktop Build / build-windows (push) Has been cancelled
Desktop Build / build-linux (push) Has been cancelled
CI / lint (push) Failing after 14s
CI / test (push) Has been skipped
CI / build (push) Has been skipped
CI / e2e (push) Has been skipped
CI / security (push) Successful in 13s
Desktop Build / build-windows (push) Has been cancelled
Desktop Build / build-linux (push) Has been cancelled
This commit is contained in:
@@ -64,6 +64,7 @@ SUPPORTED_EXTENSIONS = {
|
|||||||
".php", ".sql", ".r", ".m", ".swift", ".kt",
|
".php", ".sql", ".r", ".m", ".swift", ".kt",
|
||||||
".dockerfile", ".makefile", ".cmake",
|
".dockerfile", ".makefile", ".cmake",
|
||||||
".excalidraw",
|
".excalidraw",
|
||||||
|
".excalidraw.md",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+23
-1
@@ -124,6 +124,7 @@ class FileContentResponse(BaseModel):
|
|||||||
is_json: bool | None = Field(default=None, description="True for JSON files")
|
is_json: bool | None = Field(default=None, description="True for JSON files")
|
||||||
is_excalidraw: bool | None = Field(default=None, description="True for Excalidraw diagram files")
|
is_excalidraw: bool | None = Field(default=None, description="True for Excalidraw diagram files")
|
||||||
excalidraw_data: dict[str, Any] | None = Field(default=None, description="Excalidraw diagram data (elements, appState, files)")
|
excalidraw_data: dict[str, Any] | None = Field(default=None, description="Excalidraw diagram data (elements, appState, files)")
|
||||||
|
excalidraw_data_compressed: str | None = Field(default=None, description="Compressed Excalidraw data for .excalidraw.md files")
|
||||||
pdf_metadata: dict[str, Any] | None = Field(default=None, description="PDF metadata")
|
pdf_metadata: dict[str, Any] | None = Field(default=None, description="PDF metadata")
|
||||||
pdf_toc: list[dict[str, Any]] | None = Field(default=None, description="PDF table of contents")
|
pdf_toc: list[dict[str, Any]] | None = Field(default=None, description="PDF table of contents")
|
||||||
image_mime: str | None = Field(default=None, description="MIME type for image files")
|
image_mime: str | None = Field(default=None, description="MIME type for image files")
|
||||||
@@ -2505,7 +2506,28 @@ async def api_file(vault_name: str, path: str = Query(..., description="Relative
|
|||||||
"is_markdown": False, "is_json": True,
|
"is_markdown": False, "is_json": True,
|
||||||
}
|
}
|
||||||
|
|
||||||
# === Excalidraw: diagram editor ===
|
# === Excalidraw .excalidraw.md (Obsidian plugin format) ===
|
||||||
|
if path.lower().endswith(".excalidraw.md"):
|
||||||
|
import re as re_mod
|
||||||
|
raw_lower = file_path.read_text(encoding="utf-8", errors="replace")
|
||||||
|
# Check for excalidraw-plugin in frontmatter or body
|
||||||
|
if "excalidraw-plugin:" in raw_lower:
|
||||||
|
# Extract compressed JSON block
|
||||||
|
match = re_mod.search(r'```compressed-json\n(.*?)\n```', raw_lower, re_mod.DOTALL)
|
||||||
|
if match:
|
||||||
|
compressed = match.group(1).strip()
|
||||||
|
return {
|
||||||
|
"vault": vault_name, "path": path,
|
||||||
|
"title": file_path.name.replace(".excalidraw.md", ""),
|
||||||
|
"tags": [], "frontmatter": {},
|
||||||
|
"html": "", "raw_length": len(raw_lower),
|
||||||
|
"extension": ".excalidraw.md",
|
||||||
|
"is_markdown": False,
|
||||||
|
"is_excalidraw": True,
|
||||||
|
"excalidraw_data_compressed": compressed,
|
||||||
|
}
|
||||||
|
# Fallback: treat as regular markdown
|
||||||
|
raw = raw_lower
|
||||||
if ext == ".excalidraw":
|
if ext == ".excalidraw":
|
||||||
import json as json_mod
|
import json as json_mod
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -107,6 +107,7 @@
|
|||||||
<script type="module">
|
<script type="module">
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import ReactDOM from "react-dom";
|
import ReactDOM from "react-dom";
|
||||||
|
import { decompressFromBase64 } from "https://esm.sh/[email protected]";
|
||||||
import * as ExcalidrawLib from "https://esm.sh/@excalidraw/[email protected]?alias=react:[email protected],react-dom:[email protected]";
|
import * as ExcalidrawLib from "https://esm.sh/@excalidraw/[email protected]?alias=react:[email protected],react-dom:[email protected]";
|
||||||
|
|
||||||
window.React = React;
|
window.React = React;
|
||||||
@@ -223,11 +224,25 @@
|
|||||||
switch (msg.type) {
|
switch (msg.type) {
|
||||||
case "init":
|
case "init":
|
||||||
if (msg.data) {
|
if (msg.data) {
|
||||||
const initialData = {
|
let elements, appState, files;
|
||||||
elements: msg.data.elements || [],
|
// Handle compressed .excalidraw.md format
|
||||||
appState: msg.data.appState || {},
|
if (msg.data.compressed) {
|
||||||
files: msg.data.files || {},
|
try {
|
||||||
};
|
const decompressed = decompressFromBase64(msg.data.compressed);
|
||||||
|
const parsed = JSON.parse(decompressed);
|
||||||
|
elements = parsed.elements || [];
|
||||||
|
appState = parsed.appState || {};
|
||||||
|
files = parsed.files || {};
|
||||||
|
} catch(e) {
|
||||||
|
console.error("Failed to decompress Excalidraw data:", e);
|
||||||
|
elements = []; appState = {}; files = {};
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
elements = msg.data.elements || [];
|
||||||
|
appState = msg.data.appState || {};
|
||||||
|
files = msg.data.files || {};
|
||||||
|
}
|
||||||
|
const initialData = { elements, appState, files };
|
||||||
currentTheme = msg.theme || "light";
|
currentTheme = msg.theme || "light";
|
||||||
setTheme(currentTheme);
|
setTheme(currentTheme);
|
||||||
|
|
||||||
|
|||||||
@@ -75,13 +75,14 @@ export function renderExcalidraw(container, data, vaultName, filePath, opts = {}
|
|||||||
case 'ready':
|
case 'ready':
|
||||||
// Iframe is mounted — send initial data
|
// Iframe is mounted — send initial data
|
||||||
editorState.ready = true;
|
editorState.ready = true;
|
||||||
|
// Support both pure .excalidraw and compressed .excalidraw.md
|
||||||
|
const initData = data.excalidraw_data || {};
|
||||||
|
if (data.excalidraw_data_compressed) {
|
||||||
|
initData.compressed = data.excalidraw_data_compressed;
|
||||||
|
}
|
||||||
iframe.contentWindow.postMessage({
|
iframe.contentWindow.postMessage({
|
||||||
type: 'init',
|
type: 'init',
|
||||||
data: data.excalidraw_data || {
|
data: initData,
|
||||||
elements: [],
|
|
||||||
appState: { viewBackgroundColor: '#ffffff' },
|
|
||||||
files: {},
|
|
||||||
},
|
|
||||||
theme: theme,
|
theme: theme,
|
||||||
}, '*');
|
}, '*');
|
||||||
break;
|
break;
|
||||||
|
|||||||
@@ -80,6 +80,7 @@ const EXT_ICONS = {
|
|||||||
".csv": "table",
|
".csv": "table",
|
||||||
".tsv": "table",
|
".tsv": "table",
|
||||||
".excalidraw": "pen-tool",
|
".excalidraw": "pen-tool",
|
||||||
|
".excalidraw.md": "pen-tool",
|
||||||
".sql": "database",
|
".sql": "database",
|
||||||
".db": "database",
|
".db": "database",
|
||||||
".sqlite": "database",
|
".sqlite": "database",
|
||||||
@@ -206,6 +207,10 @@ const EXT_ICONS = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
function getFileIcon(name) {
|
function getFileIcon(name) {
|
||||||
|
const lower = name.toLowerCase();
|
||||||
|
// Check for double extensions first (e.g., .excalidraw.md)
|
||||||
|
if (lower.endsWith(".excalidraw.md")) return EXT_ICONS[".excalidraw.md"] || "pen-tool";
|
||||||
|
if (lower.endsWith(".md.excalidraw")) return EXT_ICONS[".excalidraw"] || "pen-tool";
|
||||||
const ext = "." + name.split(".").pop().toLowerCase();
|
const ext = "." + name.split(".").pop().toLowerCase();
|
||||||
return EXT_ICONS[ext] || "file";
|
return EXT_ICONS[ext] || "file";
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user