124 lines
3.8 KiB
TypeScript
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);
|
|
}
|