import YAML from "yaml"; export type ParsedFrontmatter = Record; function stripQuotes(value: string): string { if ( (value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'")) ) { return value.slice(1, -1); } return value; } function coerceFrontmatterValue(value: unknown): string | undefined { if (value === null || value === undefined) { return undefined; } if (typeof value === "string") { return value.trim(); } if (typeof value === "number" || typeof value === "boolean") { return String(value); } if (typeof value === "object") { try { return JSON.stringify(value); } catch { return undefined; } } return undefined; } function parseYamlFrontmatter(block: string): ParsedFrontmatter | null { try { const parsed = YAML.parse(block) as unknown; if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) { return null; } const result: ParsedFrontmatter = {}; for (const [rawKey, value] of Object.entries(parsed as Record)) { const key = rawKey.trim(); if (!key) { continue; } const coerced = coerceFrontmatterValue(value); if (coerced === undefined) { continue; } result[key] = coerced; } return result; } catch { return null; } } function extractMultiLineValue( lines: string[], startIndex: number, ): { value: string; linesConsumed: number } { const startLine = lines[startIndex]; const match = startLine.match(/^([\w-]+):\s*(.*)$/); if (!match) { return { value: "", linesConsumed: 1 }; } const inlineValue = match[2].trim(); if (inlineValue) { return { value: inlineValue, linesConsumed: 1 }; } const valueLines: string[] = []; let i = startIndex + 1; while (i < lines.length) { const line = lines[i]; if (line.length > 0 && !line.startsWith(" ") && !line.startsWith("\t")) { break; } valueLines.push(line); i++; } const combined = valueLines.join("\n").trim(); return { value: combined, linesConsumed: i - startIndex }; } function parseLineFrontmatter(block: string): ParsedFrontmatter { const frontmatter: ParsedFrontmatter = {}; const lines = block.split("\n"); let i = 0; while (i < lines.length) { const line = lines[i]; const match = line.match(/^([\w-]+):\s*(.*)$/); if (!match) { i++; continue; } const key = match[1]; const inlineValue = match[2].trim(); if (!key) { i++; continue; } if (!inlineValue && i + 1 < lines.length) { const nextLine = lines[i + 1]; if (nextLine.startsWith(" ") || nextLine.startsWith("\t")) { const { value, linesConsumed } = extractMultiLineValue(lines, i); if (value) { frontmatter[key] = value; } i += linesConsumed; continue; } } const value = stripQuotes(inlineValue); if (value) { frontmatter[key] = value; } i++; } return frontmatter; } export function parseFrontmatterBlock(content: string): ParsedFrontmatter { const normalized = content.replace(/\r\n/g, "\n").replace(/\r/g, "\n"); if (!normalized.startsWith("---")) { return {}; } const endIndex = normalized.indexOf("\n---", 3); if (endIndex === -1) { return {}; } const block = normalized.slice(4, endIndex); const lineParsed = parseLineFrontmatter(block); const yamlParsed = parseYamlFrontmatter(block); if (yamlParsed === null) { return lineParsed; } const merged: ParsedFrontmatter = { ...yamlParsed }; for (const [key, value] of Object.entries(lineParsed)) { if (value.startsWith("{") || value.startsWith("[")) { merged[key] = value; } } return merged; }