/*
Shared YAML frontmatter parsing and serialization helpers for migrators.
Extracted from rfc-0481.ts for reuse by rfc-0483.ts and future migrators.
- Does not implement migrator logic — helpers only.
- Does not depend on external YAML libraries — minimal hand-rolled parser sufficient for frontmatter.
- RFC-0483: extracted YAML helpers from rfc-0481.ts into shared module.
*/
export function parseFrontmatter(raw: string): {
frontmatter: Record;
body: string;
} {
const match = raw.match(/^---\n([\s\S]*?)\n---\n?([\s\S]*)$/);
if (!match) {
return { frontmatter: {}, body: raw };
}
const yamlText = match[1];
const body = match[2];
const frontmatter = parseSimpleYaml(yamlText);
return { frontmatter, body };
}
export function getIndent(line: string): number {
let indent = 0;
while (line[indent] === " ") indent++;
return indent;
}
export function parseSimpleYaml(yamlText: string): Record {
const lines = yamlText.split("\n");
const { result } = parseYamlBlock(lines, 0, 0);
return result;
}
export function parseYamlBlock(
lines: string[],
startIdx: number,
minIndent: number,
): { result: Record; nextIdx: number } {
const result: Record = {};
let i = startIdx;
while (i < lines.length) {
const line = lines[i];
if (!line || line.trim() === "" || line.trim().startsWith("#")) {
i++;
continue;
}
const indent = getIndent(line);
if (indent < minIndent) break;
const trimmed = line.trim();
const colonIdx = trimmed.indexOf(":");
if (colonIdx === -1) {
i++;
continue;
}
const key = trimmed.slice(0, colonIdx).trim();
const valuePart = trimmed.slice(colonIdx + 1).trim();
if (valuePart === "") {
const childIndent = indent + 2;
const { result: nested, nextIdx } = parseYamlBlock(lines, i + 1, childIndent);
result[key] = nested;
i = nextIdx;
} else if (valuePart === ">-" || valuePart === ">") {
i++;
const foldedLines: string[] = [];
while (i < lines.length) {
const foldedLine = lines[i];
if (!foldedLine || getIndent(foldedLine) === 0) break;
foldedLines.push(foldedLine.trim());
i++;
}
result[key] = foldedLines.join(" ");
} else if (valuePart.startsWith("- ")) {
const items: string[] = [stripQuotes(valuePart.slice(2))];
i++;
while (i < lines.length) {
const itemLine = lines[i];
if (!itemLine || getIndent(itemLine) !== indent) break;
const itemTrimmed = itemLine.trim();
if (!itemTrimmed.startsWith("- ")) break;
items.push(stripQuotes(itemTrimmed.slice(2)));
i++;
}
result[key] = items;
} else {
result[key] = stripQuotes(valuePart);
i++;
}
}
return { result, nextIdx: i };
}
export function stripQuotes(value: string): string {
if (
(value.startsWith('"') && value.endsWith('"')) ||
(value.startsWith("'") && value.endsWith("'"))
) {
return value.slice(1, -1);
}
return value;
}
export function serializeFrontmatter(data: Record): string {
const lines: string[] = ["---"];
for (const [key, value] of Object.entries(data)) {
if (value === null || value === undefined) continue;
if (typeof value === "string") {
lines.push(`${key}: ${yamlValue(value)}`);
} else if (typeof value === "number") {
lines.push(`${key}: ${value}`);
} else if (typeof value === "object" && !Array.isArray(value)) {
lines.push(`${key}:`);
serializeObject(value as Record, lines, 2);
}
}
lines.push("---");
lines.push("");
return lines.join("\n");
}
export function serializeObject(
obj: Record,
lines: string[],
indent: number,
): void {
const prefix = " ".repeat(indent);
for (const [key, value] of Object.entries(obj)) {
if (value === null || value === undefined) continue;
if (typeof value === "string") {
lines.push(`${prefix}${key}: ${yamlValue(value)}`);
} else if (typeof value === "number") {
lines.push(`${prefix}${key}: ${value}`);
} else if (typeof value === "object" && !Array.isArray(value)) {
lines.push(`${prefix}${key}:`);
serializeObject(value as Record, lines, indent + 2);
}
}
}
export function needsQuoting(value: string): boolean {
if (value === "") return true;
return (
/[:#\[\]{}&*!|>'"%`,\-?\n]/.test(value) ||
value.startsWith(" ") ||
value.endsWith(" ") ||
value.startsWith("@")
);
}
export function yamlValue(value: string): string {
if (needsQuoting(value)) {
return `"${escapeYamlString(value)}"`;
}
return value;
}
export function escapeYamlString(value: string): string {
return value.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
}