Support multidocument files

This commit is contained in:
Endeavorance 2025-06-06 09:40:19 -04:00
parent c75752b1b7
commit 85b2d5cad7
2 changed files with 191 additions and 22 deletions

View file

@ -1,5 +1,82 @@
import YAML from "yaml";
const FRONTMATTER_REGEX = /^---[\s\S]*?---/gm;
import YAML, { YAMLError } from "yaml";
export type EMDYData = Record<string, unknown>;
type Mode = "yaml" | "markdown";
interface PendingDocument {
content: string;
markdownKey: string;
}
export class EMDYError extends Error {
name = "EMDYError";
}
export class EMDYParseError extends EMDYError {
name = "EMDYParseError";
original?: Error;
constructor(message: string, original?: Error) {
super(`EMDY Parse Error: ${message}`);
this.original = original;
}
}
/**
* Parse a single PendingDocument into EMDYData
* @param document The PendingDocument to parse
* @returns The parsed EMDYData
*/
function parseDocument(document: PendingDocument): EMDYData {
const yamlLines: string[] = [];
const markdownLines: string[] = [];
const contentLines = document.content.trim().split("\n");
let mode: Mode = "markdown";
for (const line of contentLines) {
// Handle boundary lines
if (line.trim() === "---") {
// Upon encountering a boundary in YAML mode, switch to markdown mode
if (mode === "yaml") {
mode = "markdown";
}
// When in markdown mode (default), only switch to YAML mode
// if no markdown has been collected yet. This ensures that
// YAML frontmatter is only parsed if it exists at the start of the document.
else if (mode === "markdown" && markdownLines.length === 0) {
mode = "yaml";
}
// Continue the loop to skip the boundary line
continue;
}
// Handle content lines
if (mode === "yaml") {
yamlLines.push(line);
} else {
markdownLines.push(line);
}
}
const yamlContent = yamlLines.join("\n").trim();
const markdownContent = markdownLines.join("\n").trim();
try {
return {
...YAML.parse(yamlContent || "{}"),
[document.markdownKey]: markdownContent,
};
} catch (error) {
if (error instanceof YAMLError) {
throw new EMDYParseError("Failed to parse YAML frontmatter", error);
}
throw new EMDYError(`An unexpected error occured: ${error}`);
}
}
/**
* Parse a markdown document with optional YAML frontmatter into an object
@ -8,23 +85,8 @@ const FRONTMATTER_REGEX = /^---[\s\S]*?---/gm;
* @param markdownKey The key to use for the markdown content
* @returns The parsed markdown content and frontmatter
*/
function parse(
content: string,
markdownKey = "content",
): Record<string, unknown> {
const markdown = content.trim().replace(FRONTMATTER_REGEX, "").trim();
let frontmatter: Record<string, unknown> = {};
if (FRONTMATTER_REGEX.test(content)) {
const frontmatterString = content.match(FRONTMATTER_REGEX)?.[0] ?? "";
const cleanFrontmatter = frontmatterString.replaceAll("---", "").trim();
frontmatter = YAML.parse(cleanFrontmatter);
}
return {
[markdownKey]: markdown,
...frontmatter,
};
export function parse(content: string, markdownKey = "content"): EMDYData {
return parseDocument({ content, markdownKey });
}
/**
@ -34,7 +96,7 @@ function parse(
* @param markdownKey The key to use for the markdown content
* @returns The parsed markdown content and frontmatter
*/
function stringify(
export function stringify(
data: Record<string, unknown>,
markdownKey = "content",
): string {
@ -50,11 +112,54 @@ function stringify(
const stringifiedFrontmatter = YAML.stringify(frontmatter).trim();
return `---\n${stringifiedFrontmatter}\n---\n${markdown}`;
return `
---
${stringifiedFrontmatter}
---
${markdown}
`.trim();
}
/**
* Parse a multi-document file into an array of EMDYData objects
* @param content The raw markdown content containing multiple documents
* @param markdownKey The key to use for the markdown content in each document
* @returns An array of parsed EMDYData objects
*/
export function parseAll(content: string, markdownKey = "content"): EMDYData[] {
const documents: PendingDocument[] = [];
const lines = content.split("\n");
let currentDoc: string[] = [];
for (const line of lines) {
if (line.trim().startsWith("===") && currentDoc.length > 0) {
documents.push({
content: currentDoc.join("\n").trim(),
markdownKey,
});
currentDoc = [];
continue;
}
currentDoc.push(line);
}
// Add any remaining content as the last document
if (currentDoc.length > 0) {
documents.push({
content: currentDoc.join("\n").trim(),
markdownKey,
});
}
// Split by document boundaries
return documents.map(parseDocument);
}
const EMDY = {
parse,
parseAll,
stringify,
} as const;