Files
Your NameandClaude Sonnet 5.5 313c41f867 feat(electron): main process, vault, search index, links, import, capture
Vault on plain .md files with FTS5 index, link rewriting, canvas files,
history/trash/backup, link previews, image fetch, tasks and unlinked
mentions, Notion/Obsidian importer, quick-capture window and shortcut.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-15 11:57:09 +02:00

92 lines
4.3 KiB
JavaScript

'use strict';
// Canvas files (".canvas") on disk, in the JSON Canvas format Obsidian uses (https://jsoncanvas.org):
// { "nodes": [{ id, type: text|file|link|group, x, y, width, height, text|file|url|label, color? }],
// "edges": [{ id, fromNode, fromSide?, toNode, toSide?, label?, color? }] }
// Written the way Obsidian writes it (one node per line, tabs), so files changed by both apps diff cleanly.
// The page-side twin is src/lib/canvas.js; keep parse/serialize in step.
const { rewriteLinks, renameHeadingRefs, resolveLink, matchesHeading, linkHeading } = require('./links.cjs');
const isCanvasId = (id) => /\.canvas$/i.test(String(id || ''));
// { nodes, edges, extra } from the file's text. Empty text is an empty canvas; anything unreadable throws.
function parseCanvas(text) {
const s = String(text || '').trim();
if (!s) return { nodes: [], edges: [], extra: {} };
const data = JSON.parse(s);
if (!data || typeof data !== 'object' || Array.isArray(data)) throw new Error('This is not a canvas file.');
const { nodes, edges, ...extra } = data;
return { nodes: Array.isArray(nodes) ? nodes : [], edges: Array.isArray(edges) ? edges : [], extra };
}
function serializeCanvas({ nodes = [], edges = [], extra = {} }) {
const list = (arr) => (arr.length ? '[\n' + arr.map((o) => '\t\t' + JSON.stringify(o)).join(',\n') + '\n\t]' : '[]');
const parts = [`\t"nodes":${list(nodes)}`, `\t"edges":${list(edges)}`];
for (const [k, v] of Object.entries(extra)) parts.push(`\t${JSON.stringify(k)}:${JSON.stringify(v)}`);
return '{\n' + parts.join(',\n') + '\n}';
}
const EMPTY_CANVAS = serializeCanvas({});
// A note's file node points at "Folder/Note.md"; its page id has no extension. Other files keep theirs.
const fileToId = (file) => String(file || '').replace(/\.md$/i, '');
const isNoteFile = (file) => /\.md$/i.test(String(file || ''));
const isCanvasFile = (file) => isCanvasId(file);
// The canvas as Markdown-ish text: what search indexes, and where backlinks and the graph find its links
// (a note or canvas on it counts as a [[link]]).
function canvasText(text) {
let c;
try { c = parseCanvas(text); } catch { return ''; }
const out = [];
for (const n of c.nodes) {
if (n.type === 'text' && n.text) out.push(String(n.text));
else if (n.type === 'group' && n.label) out.push(String(n.label));
else if (n.type === 'link' && n.url) out.push(String(n.url));
else if (n.type === 'file' && (isNoteFile(n.file) || isCanvasFile(n.file))) out.push(`[[${fileToId(n.file)}${n.subpath || ''}]]`);
}
for (const e of c.edges) if (e.label) out.push(String(e.label));
return out.join('\n\n');
}
// Pages moved or renamed (see main.cjs relink): point file nodes and [[links]] in text nodes at the new places.
// Returns the new file text, or null when nothing changed.
function rewriteCanvas(text, before, after, moved) {
let c;
try { c = parseCanvas(text); } catch { return null; }
let changed = false;
for (const n of c.nodes) {
if (n.type === 'file' && n.file) {
const id = fileToId(n.file);
const now = moved(id);
if (now && now !== id) { n.file = isNoteFile(n.file) ? now + '.md' : now; changed = true; }
} else if (n.type === 'text' && n.text) {
const t = rewriteLinks(n.text, before, after, moved);
if (t !== null) { n.text = t; changed = true; }
}
}
return changed ? serializeCanvas(c) : null;
}
// A heading of page `pageId` was renamed: notes shown at that heading ("subpath") and [[Page#Heading]] links in
// text nodes follow. Returns { body, count } or null.
function renameHeadingInCanvas(text, pages, pageId, from, to) {
let c;
try { c = parseCanvas(text); } catch { return null; }
const safe = linkHeading(to);
if (!safe) return null;
let count = 0;
for (const n of c.nodes) {
if (n.type === 'file' && isNoteFile(n.file) && n.subpath && resolveLink(fileToId(n.file), pages) === pageId && matchesHeading(String(n.subpath).replace(/^#/, ''), from)) {
n.subpath = '#' + safe;
count++;
} else if (n.type === 'text' && n.text) {
const r = renameHeadingRefs(n.text, pages, pageId, from, to);
if (r) { n.text = r.body; count += r.count; }
}
}
return count ? { body: serializeCanvas(c), count } : null;
}
module.exports = { isCanvasId, parseCanvas, serializeCanvas, EMPTY_CANVAS, canvasText, rewriteCanvas, renameHeadingInCanvas, fileToId };