Project Files
src / validation / parser.ts
import { load as parseYaml } from "js-yaml";
import type { CommandFile } from "./types.js";
const FRONTMATTER_DELIMITER = "---";
export function parseCommandFile(
filePath: string,
content: string,
): CommandFile {
const lines = content.split(/\r?\n/);
const lineCount = lines.length;
const filename =
filePath.split(/[\\/]/).pop()?.replace(/\.md$/i, "") || "unknown";
// Must start with '---'
const trimmedLeft = content.trimStart();
const leadingWhitespace = content.length - trimmedLeft.length;
const firstLineEnd = content.indexOf("\n");
const firstLine =
firstLineEnd === -1 ? content : content.slice(0, firstLineEnd);
if (leadingWhitespace > 0 || firstLine.trim() !== FRONTMATTER_DELIMITER) {
return {
path: filePath,
filename,
raw: content,
frontmatter: null,
body: content,
parseError:
'File must start with "---" on the first line (no leading whitespace)',
lineCount,
};
}
// Find the closing '---'
const afterFirstDelim = content.indexOf("\n", firstLineEnd + 1);
if (afterFirstDelim === -1) {
return {
path: filePath,
filename,
raw: content,
frontmatter: null,
body: "",
parseError: 'Unclosed frontmatter: expected closing "---"',
lineCount,
};
}
const closingDelimIndex = content.indexOf(
`\n${FRONTMATTER_DELIMITER}`,
afterFirstDelim,
);
if (closingDelimIndex === -1) {
return {
path: filePath,
filename,
raw: content,
frontmatter: null,
body: "",
parseError: 'Unclosed frontmatter: expected closing "---"',
lineCount,
};
}
// Extract frontmatter YAML (between the two --- delimiters)
const yamlStart = firstLineEnd + 1;
const yamlEnd = closingDelimIndex;
const yamlContent = content.slice(yamlStart, yamlEnd);
// Body is everything after the closing --- and its trailing newline
let bodyStart = closingDelimIndex + FRONTMATTER_DELIMITER.length + 1;
// Skip the newline after closing ---
if (bodyStart < content.length && content[bodyStart] === "\n") {
bodyStart++;
} else if (bodyStart < content.length && content[bodyStart] === "\r") {
bodyStart += 2;
}
const body = content.slice(bodyStart);
// Parse YAML
let frontmatter: Record<string, unknown> | null = null;
let parseError: string | undefined;
if (yamlContent.trim().length === 0) {
parseError = "Empty frontmatter block";
} else {
try {
const parsed = parseYaml(yamlContent);
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
frontmatter = parsed as Record<string, unknown>;
} else if (parsed === undefined || parsed === null) {
frontmatter = {};
} else {
parseError =
"Frontmatter must be a YAML mapping (key-value pairs), got: " +
typeof parsed;
}
} catch (err: unknown) {
const msg = err instanceof Error ? err.message : String(err);
parseError = `YAML parse error: ${msg}`;
}
}
return {
path: filePath,
filename,
raw: content,
frontmatter,
body,
parseError,
lineCount,
};
}