Something went wrong. Try again.
🌿 Collaborative wiki on ATProto
Something went wrong. Try again.
3.6 kB · 99 lines
TypeScript
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100// A leading YAML block, as Obsidian writes it. Only a document-opening `---` counts;// anywhere else `---` stays a horizontal rule.const FRONTMATTER_RE = /^---[ \t]*\r?\n((?:[\s\S]*?\r?\n)?)---[ \t]*(?:\r?\n|$)/;
interface FrontmatterMatch { fields: string; bodyStart: number;}
function matchFrontmatter(source: string): FrontmatterMatch | null { if (!source.startsWith("---")) return null; const match = FRONTMATTER_RE.exec(source); if (!match) return null; // Swallow the blank line writeFrontmatter leaves behind the block, so writing and // stripping are exact inverses. const blank = /^[ \t]*\r?\n/.exec(source.slice(match[0].length)); return { fields: match[1] ?? "", bodyStart: match[0].length + (blank?.[0].length ?? 0), };}
export function stripFrontmatter(source: string): string { const match = matchFrontmatter(source); return match ? source.slice(match.bodyStart) : source;}
// Reads one top-level "key: value". Deliberately not a YAML parser — enough for the// fields we write ourselves, and blind to anything nested or multi-line.export function readFrontmatterField( source: string, key: string,): string | null { const match = matchFrontmatter(source); if (!match) return null; for (const line of match.fields.split(/\r?\n/)) { if (!line.startsWith(`${key}:`)) continue; return unquote(line.slice(key.length + 1).trim()) || null; } return null;}
// A block holding nothing but our own keys is metadata we emitted, not content the// author wrote — importing it back would graft it onto every note body. A block with// anything else in it belongs to the author and stays untouched.export function stripOwnFrontmatter(source: string, ownKeys: string[]): string { const match = matchFrontmatter(source); if (!match) return source; const owned = new Set(ownKeys); const isOurs = match.fields .split(/\r?\n/) .filter((line) => line.trim() !== "") .every((line) => { const key = topLevelKey(line); return key !== null && owned.has(key); }); return isOurs ? source.slice(match.bodyStart) : source;}
// Prepends a block, replacing the top-level keys we own and keeping every other line// the author wrote — a re-export must not silently drop their properties.export function writeFrontmatter( source: string, fields: [string, string][],): string { const match = matchFrontmatter(source); const owned = new Set(fields.map(([key]) => key)); const kept = (match?.fields ?? "").split(/\r?\n/).filter((line) => { const key = topLevelKey(line); return line.trim() !== "" && (key === null || !owned.has(key)); }); const written = fields.map(([key, value]) => `${key}: ${quote(value)}`); const body = match ? source.slice(match.bodyStart) : source; return `---\n${[...written, ...kept].join("\n")}\n---\n\n${body}`;}
// Indented lines belong to whatever key is above them, so they are never ours to replace.function topLevelKey(line: string): string | null { if (/^\s/.test(line)) return null; const colon = line.indexOf(":"); return colon > 0 ? line.slice(0, colon) : null;}
// Always quoted: one rule covers every title that YAML would otherwise read as a// number, a boolean, or a broken mapping.function quote(value: string): string { const escaped = value .replace(/\\/g, "\\\\") .replace(/"/g, '\\"') .replace(/\r?\n/g, " "); return `"${escaped}"`;}
function unquote(value: string): string { const quoted = /^"([\s\S]*)"$/.exec(value) ?? /^'([\s\S]*)'$/.exec(value); if (!quoted) return value; return (quoted[1] ?? "").replace(/\\"/g, '"').replace(/\\\\/g, "\\");}