Markdown Frontmatter Parser
Split Markdown frontmatter from the body and parse flat YAML scalars, with BOM handling and clear error messages.
Code
export interface FrontmatterResult<T extends Record<string, unknown>> {
attributes: T;
body: string;
rawFrontmatter: string;
}
function parseScalar(raw: string): unknown {
if (raw === "" || raw === "~" || raw === "null") return null;
if (raw === "true" || raw === "false") return raw === "true";
if (/^-?\d+(?:\.\d+)?$/.test(raw)) return Number(raw);
if (raw.startsWith('"') && raw.endsWith('"')) {
return raw.slice(1, -1).replace(/\\n/g, "\n").replace(/\\"/g, '"').replace(/\\\\/g, "\\");
}
if (raw.startsWith("'") && raw.endsWith("'")) {
return raw.slice(1, -1).replace(/''/g, "'");
}
return raw;
}
export function parseFrontmatter<T extends Record<string, unknown> = Record<string, unknown>>(
markdown: string,
): FrontmatterResult<T> {
if (typeof markdown !== "string") throw new TypeError("markdown must be a string");
const normalized = markdown.replace(/^\uFEFF/, "").replace(/\r\n?/g, "\n");
if (!normalized.startsWith("---\n")) {
throw new SyntaxError("frontmatter must start with a --- line");
}
const closing = normalized.indexOf("\n---", 4);
if (closing === -1) throw new SyntaxError("missing closing --- delimiter");
const rawFrontmatter = normalized.slice(4, closing);
const body = normalized.slice(closing + 4).replace(/^\n/, "");
const attributes: Record<string, unknown> = {};
for (const line of rawFrontmatter.split("\n")) {
if (line.trim() === "" || line.trimStart().startsWith("#")) continue;
const match = /^([A-Za-z0-9_-]+):\s*(.*)$/.exec(line);
if (!match) throw new SyntaxError("unsupported frontmatter line: " + line);
attributes[match[1]!] = parseScalar(match[2]!.trim());
}
return { attributes: attributes as T, body, rawFrontmatter };
}Report a problem with this exampleOpens a pre-filled issue on GitHub.