All checks were successful
CI / test (push) Successful in 16s
- Preset fields (from settings) are now seeded as editable/deletable rows in the modal alongside custom ones, using one consistent row style. - The note's own merged frontmatter is shown as read-only (disabled) rows at the top of the list (only when 'Merge document frontmatter' is on, and only for keys not overridden by an editable row). - Drops the separate read-only 'resolved' text preview; the rows are the WYSIWYG result. Adds pure, unit-tested composeFrontmatter()/docFrontmatterToPairs() and removes the superseded resolveFrontmatter(). Editable rows are authoritative: on a key collision they override the merged document field. Presets are deep-copied so editing rows never mutates saved settings. Co-Authored-By: Claude
70 lines
2.7 KiB
TypeScript
70 lines
2.7 KiB
TypeScript
import yaml from "js-yaml";
|
||
|
||
export interface Pair {
|
||
key: string;
|
||
value: string;
|
||
}
|
||
|
||
export function parseNote(text: string): { frontmatter: Record<string, unknown>; body: string } {
|
||
const stripped = text.replace(/^/, "");
|
||
const m = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?/.exec(stripped);
|
||
if (!m) return { frontmatter: {}, body: stripped };
|
||
const loaded = yaml.load(m[1]);
|
||
const frontmatter = loaded && typeof loaded === "object" ? (loaded as Record<string, unknown>) : {};
|
||
return { frontmatter, body: stripped.slice(m[0].length) };
|
||
}
|
||
|
||
/**
|
||
* Convert a parsed note's frontmatter object into ordered, stringified pairs
|
||
* suitable for display (read-only) and composition.
|
||
*/
|
||
export function docFrontmatterToPairs(docFrontmatter: Record<string, unknown>): Pair[] {
|
||
return Object.entries(docFrontmatter).map(([key, v]) => ({ key, value: stringifyScalar(v) }));
|
||
}
|
||
|
||
/**
|
||
* Build the final frontmatter for the post.
|
||
*
|
||
* Editable rows (the preset fields seeded from settings plus any custom rows
|
||
* the user added) are authoritative: on a key collision they win over the
|
||
* document's own merged fields, which appear first (and are shown read-only in
|
||
* the modal). Blank-key rows are dropped; duplicate editable keys keep the last
|
||
* value at the first position.
|
||
*/
|
||
export function composeFrontmatter(docPairs: Pair[], editableRows: Pair[], mergeDoc: boolean): Pair[] {
|
||
const editable = editableRows.filter((r) => r.key.trim() !== "");
|
||
const editableKeys = new Set(editable.map((r) => r.key));
|
||
const out: Pair[] = [];
|
||
const idx = new Map<string, number>();
|
||
const put = (key: string, value: string) => {
|
||
const at = idx.get(key);
|
||
if (at !== undefined) out[at] = { key, value };
|
||
else { idx.set(key, out.length); out.push({ key, value }); }
|
||
};
|
||
if (mergeDoc) for (const p of docPairs) if (!editableKeys.has(p.key)) put(p.key, p.value);
|
||
for (const r of editable) put(r.key, r.value);
|
||
return out;
|
||
}
|
||
|
||
function stringifyScalar(v: unknown): string {
|
||
if (v == null) return "";
|
||
if (v instanceof Date) return v.toISOString().slice(0, 10);
|
||
if (typeof v === "object") return yaml.dump(v).trim();
|
||
return String(v);
|
||
}
|
||
|
||
export function serializeFrontmatter(pairs: Pair[]): string {
|
||
const lines = pairs.map((p) => `${p.key}: ${formatValue(p.value)}`);
|
||
return `---\n${lines.join("\n")}\n---\n`;
|
||
}
|
||
|
||
function formatValue(v: string): string {
|
||
if (v === "") return '""';
|
||
const yamlSpecial = /^(true|false|null|yes|no|on|off|y|n|~)$/i.test(v);
|
||
const isoDate = /^\d{4}-\d{2}-\d{2}$/.test(v);
|
||
const numericLike = !isoDate && /^[+-]?(\d|\.\d)/.test(v);
|
||
if (yamlSpecial || numericLike) return JSON.stringify(v);
|
||
const bareSafe = /^[A-Za-z0-9_./-][A-Za-z0-9_./ -]*$/.test(v) && !/^\s|\s$/.test(v);
|
||
return bareSafe ? v : JSON.stringify(v);
|
||
}
|