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>
69 lines
2.3 KiB
JavaScript
69 lines
2.3 KiB
JavaScript
'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 };
|