// FrontmatterCodec — 文件头 frontmatter 解析/序列化(FR-M1/M2) // 仅 4 个业务字段:title? / tags? / star? / links?;空数组与默认值不落盘 import type { DocMeta } from './doc.js'; const FENCE = '---'; // 解析:返回 { meta, body };无 frontmatter 时 meta={} body=原文 export function splitFrontmatter(raw: string): { meta: DocMeta; body: string } { // 必须以 --- 开头(允许前导空白行后紧跟围栏) const m = raw.match(/^\s*---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/); if (!m) return { meta: {}, body: raw }; const yaml = m[1]; const body = m[2]; return { meta: parseYaml(yaml), body }; } // 序列化:把 meta + body 合并为完整文件文本;默认值与空数组不写入 export function joinFrontmatter(meta: DocMeta, body: string): string { const yaml = serializeYaml(meta); if (!yaml) return body; return `${FENCE}\n${yaml}${FENCE}\n${body}`; } // ---- 极简 YAML 子集解析(只支持本项目 4 字段的形态)---- function parseYaml(yaml: string): DocMeta { const meta: DocMeta = {}; const lines = yaml.split(/\r?\n/); for (const line of lines) { if (!line.trim() || line.trim().startsWith('#')) continue; const kv = line.match(/^([a-zA-Z]+):\s*(.*)$/); if (!kv) continue; const [, key, val] = kv; const v = val.trim(); if (key === 'title') meta.title = unquote(v); else if (key === 'star') meta.star = v === 'true'; else if (key === 'tags') meta.tags = parseList(v); else if (key === 'links') meta.links = parseList(v); } return meta; } function parseList(v: string): string[] { // 内联数组 [a, b] 或流式;本项目只用内联 const m = v.match(/^\[\s*(.*)\s*\]$/); if (!m) return []; const inner = m[1].trim(); if (!inner) return []; return inner.split(',').map(s => unquote(s.trim())).filter(Boolean); } function unquote(s: string): string { const m = s.match(/^['"](.*)['"]$/); return m ? m[1] : s; } // ---- 序列化:剔除默认值与空数组 ---- function serializeYaml(meta: DocMeta): string { const lines: string[] = []; if (meta.title !== undefined && meta.title !== '') lines.push(`title: ${quoteIfNeeded(meta.title)}`); if (meta.tags && meta.tags.length > 0) lines.push(`tags: [${meta.tags.map(quoteIfNeeded).join(', ')}]`); if (meta.star === true) lines.push('star: true'); if (meta.links && meta.links.length > 0) lines.push(`links: [${meta.links.map(quoteIfNeeded).join(', ')}]`); return lines.length ? lines.join('\n') + '\n' : ''; } function quoteIfNeeded(s: string): string { // 含特殊字符(冒号/井号/方括号/引号/前后空格)则加引号 if (/[:#\[\]"']|^\s|\s$/.test(s)) return JSON.stringify(s); return s; }