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>
This commit is contained in:
Your NameandClaude Sonnet 5.5 committed 2026-10-15 11:57:09 +02:00
1 parent 0b5f8bfa8b
commit 313c41f867
16 files changed
+2706 -15

No files matched your search

+68
View File
@@ -0,0 +1,68 @@
'use strict';
// Minimal YAML front matter: flat `key: value`, inline `[a, b]` lists and `- item` lists.
// Unknown keys are kept and written back, so notes from Obsidian survive a round trip.
const FM = /^?---\r?\n([\s\S]*?)\r?\n---[ \t]*(?:\r?\n|$)/;
function unquote(v) {
v = v.trim();
if (v.length >= 2 && v.startsWith('"') && v.endsWith('"')) {
try { return JSON.parse(v); } catch { return v.slice(1, -1); }
}
if (v.length >= 2 && v.startsWith("'") && v.endsWith("'")) return v.slice(1, -1).replace(/''/g, "'");
return v;
}
function parseFrontMatter(raw) {
const m = raw.match(FM);
if (!m) return { meta: {}, body: raw };
const meta = {};
let last = null;
for (const line of m[1].split(/\r?\n/)) {
const kv = line.match(/^([A-Za-z0-9_-]+):\s*(.*)$/);
if (kv) {
last = kv[1];
const v = kv[2].trim();
if (v.startsWith('[') && v.endsWith(']')) {
meta[last] = v.slice(1, -1).split(',').map(unquote).filter(Boolean);
} else {
meta[last] = v === '' ? [] : unquote(v);
}
continue;
}
const item = line.match(/^\s*-\s+(.*)$/);
if (item && last) {
if (!Array.isArray(meta[last])) meta[last] = [];
meta[last].push(unquote(item[1]));
}
}
// An empty `key:` with no list items below it is just an empty string.
for (const k of Object.keys(meta)) if (Array.isArray(meta[k]) && !meta[k].length && k !== 'tags') meta[k] = '';
return { meta, body: raw.slice(m[0].length).replace(/^\r?\n/, '') };
}
function quote(s) {
s = String(s);
return /^[\p{L}\p{N}_ .\/@#+-]+$/u.test(s) && s.trim() === s && !/^[-#@]/.test(s) ? s : JSON.stringify(s);
}
const ORDER = ['icon', 'cover', 'tags', 'created'];
function serializeFrontMatter(meta = {}) {
const keys = Object.keys(meta).filter((k) => {
const v = meta[k];
return v !== undefined && v !== null && v !== '' && !(Array.isArray(v) && !v.length);
});
if (!keys.length) return '';
keys.sort((a, b) => {
const ia = ORDER.indexOf(a), ib = ORDER.indexOf(b);
return (ia < 0 ? 99 : ia) - (ib < 0 ? 99 : ib);
});
const lines = keys.map((k) => {
const v = meta[k];
return Array.isArray(v) ? `${k}: [${v.map(quote).join(', ')}]` : `${k}: ${quote(v)}`;
});
return `---\n${lines.join('\n')}\n---\n\n`;
}
module.exports = { parseFrontMatter, serializeFrontMatter };