Files
dotfiles/pi/.pi/agent/prompts/test/support/frontmatter.ts
T
2026-07-27 08:46:32 +02:00

124 lines
3.8 KiB
TypeScript

/**
* Minimal YAML frontmatter parser tailored to the prompt workflow files
* in this directory. Mirrors the subset of YAML that the existing `c-*.md`
* and `p-*.md` files actually use:
*
* key: value -> string
* key: "quoted value" -> string (quotes stripped)
* key: -> block scalar (everything more-indented is
* collected, de-indented, and returned as one
* string with newlines)
*
* Comments (`# ...`) and blank lines are skipped. Quoted strings have
* their quotes stripped, matching the convention used in the rest of the
* repo (no nested quoting or escapes are needed by the fixtures).
*/
export interface ParsedDoc {
frontmatter: Record<string, string>;
body: string;
}
function stripComment(line: string): string {
const idx = line.indexOf("#");
// Only treat `#` as a comment when it appears at start-of-line or
// after whitespace; otherwise the `#` is part of a value (it never
// is in this repo, but it keeps the parser robust).
if (idx === -1) return line;
const before = line.slice(0, idx);
if (/^\s*$/.test(before)) return before;
return line;
}
function deIndentBlock(block: string): string {
const lines = block.split("\n");
const indents = lines
.filter((l) => l.trim() !== "")
.map((l) => l.match(/^[ \t]*/)?.[0].length ?? 0);
const min = indents.length === 0 ? 0 : Math.min(...indents);
if (min === 0) return block.replace(/^\n+/, "");
return lines
.map((l) => (l.length >= min ? l.slice(min) : l))
.join("\n")
.replace(/^\n+/, "");
}
export function parseFrontmatter(content: string): ParsedDoc {
const normalized = content.replace(/\r\n/g, "\n");
if (!normalized.startsWith("---")) {
return { frontmatter: {}, body: normalized };
}
// The closing fence is `\n---` (optionally followed by whitespace/eol).
const endIndex = normalized.indexOf("\n---", 3);
if (endIndex === -1) {
return { frontmatter: {}, body: normalized };
}
const block = normalized.slice(3, endIndex);
const body = normalized.slice(endIndex + 4).replace(/^\s+/, "");
const frontmatter: Record<string, string> = {};
let currentKey: string | null = null;
let currentBlock: string[] | null = null;
let currentIndent = -1;
const flush = () => {
if (currentKey === null || currentBlock === null) return;
frontmatter[currentKey] = deIndentBlock(currentBlock.join("\n"));
currentKey = null;
currentBlock = null;
currentIndent = -1;
};
for (const rawLine of block.split("\n")) {
const line = stripComment(rawLine);
if (line.trim() === "") {
// Preserve blank lines inside an open block scalar, otherwise
// ignore them between keys.
if (currentBlock !== null) currentBlock.push("");
continue;
}
const indent = line.match(/^[ \t]*/)?.[0].length ?? 0;
if (currentKey !== null && currentBlock !== null && indent > currentIndent) {
currentBlock.push(line);
continue;
}
flush();
const match = line.match(/^([A-Za-z][\w-]*):\s*(.*)$/);
if (!match) continue;
const key = match[1];
const raw = match[2].trim();
if (raw === "" || raw === ">" || raw === "|-" || raw === ">-" || raw === "|") {
currentKey = key;
currentBlock = [];
currentIndent = indent;
} else {
const quoted =
(raw.startsWith('"') && raw.endsWith('"')) ||
(raw.startsWith("'") && raw.endsWith("'"));
frontmatter[key] = quoted ? raw.slice(1, -1) : raw;
}
}
flush();
return { frontmatter, body };
}
/**
* Split a `chain:` frontmatter value into individual step names.
* Mirrors the upstream convention of ` -> ` as the delimiter and trims
* whitespace from each step.
*/
export function parseChain(value: string | undefined): string[] {
if (!value) return [];
return value
.split(" -> ")
.map((s) => s.trim())
.filter((s) => s.length > 0);
}