/** * — vault browser for rNotes. * * Replaces the old TipTap editor with a read-only vault sync browser. * Obsidian / Logseq vaults are uploaded as ZIP files, parsed server-side, * and stored in Automerge + on-disk. This component is the browse UI. * * Left sidebar : vault list (fetches /api/vault/list?space=) * Center panel : file tree (fetches /api/vault/:id/notes?space=) * Right panel : MD preview (fetches /api/vault/:id/note/:path?space=) */ import type { VaultMeta, VaultNoteMeta } from '../schemas'; // ── Tiny markdown → HTML converter (no dependencies) ───────────────────────── function renderMarkdown(md: string): string { // Escape HTML in code blocks first, then restore placeholders const codeBlocks: string[] = []; let html = md.replace(/```[\s\S]*?```/g, (m) => { const lang = m.match(/^```(\w*)/)?.[1] ?? ''; const code = m.replace(/^```\w*\n?/, '').replace(/\n?```$/, ''); const escaped = code.replace(/&/g, '&').replace(//g, '>'); codeBlocks.push(`
${escaped}
`); return `\x00CB${codeBlocks.length - 1}\x00`; }); // Inline code const inlineCodes: string[] = []; html = html.replace(/`([^`\n]+)`/g, (_, c) => { const escaped = c.replace(/&/g, '&').replace(//g, '>'); inlineCodes.push(`${escaped}`); return `\x00IC${inlineCodes.length - 1}\x00`; }); // Wikilinks [[target]] or [[target|alias]] html = html.replace(/\[\[([^\]]+)\]\]/g, (_, inner) => { const [target, alias] = inner.split('|'); const label = alias ?? target; return `${label.trim()}`; }); // Images ![alt](url) html = html.replace(/!\[([^\]]*)\]\(([^)]+)\)/g, '$1'); // Links [text](url) html = html.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '$1'); // Process line-by-line const lines = html.split('\n'); const out: string[] = []; let inUl = false; let inOl = false; let inBlockquote = false; function closeList() { if (inUl) { out.push(''); inUl = false; } if (inOl) { out.push(''); inOl = false; } } function closeBlockquote() { if (inBlockquote) { out.push(''); inBlockquote = false; } } for (const raw of lines) { const line = raw; // Headings const hm = line.match(/^(#{1,6})\s+(.+)/); if (hm) { closeList(); closeBlockquote(); const level = hm[1].length; out.push(`${inlineFormat(hm[2])}`); continue; } // Blockquote if (line.startsWith('> ')) { closeList(); if (!inBlockquote) { out.push('
'); inBlockquote = true; } out.push(`

${inlineFormat(line.slice(2))}

`); continue; } else if (inBlockquote && line.trim() === '') { closeBlockquote(); } // HR if (/^[-*_]{3,}$/.test(line.trim())) { closeList(); closeBlockquote(); out.push('
'); continue; } // Unordered list const ulm = line.match(/^(\s*)[-*+]\s+(.+)/); if (ulm) { closeBlockquote(); if (!inUl) { out.push('