166 lines
4.3 KiB
TypeScript
166 lines
4.3 KiB
TypeScript
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
|
|
* with the markdown content and any frontmatter as keys.
|
|
* @param content The raw markdown content
|
|
* @param markdownKey The key to use for the markdown content
|
|
* @returns The parsed markdown content and frontmatter
|
|
*/
|
|
export function parse(content: string, markdownKey = "content"): EMDYData {
|
|
return parseDocument({ content, markdownKey });
|
|
}
|
|
|
|
/**
|
|
* Serialize an object with markdown content and optional frontmatter into a
|
|
* Markdown document. Uses the `content` key for the markdown content by default.
|
|
* @param data The object to serialize
|
|
* @param markdownKey The key to use for the markdown content
|
|
* @returns The parsed markdown content and frontmatter
|
|
*/
|
|
export function stringify(
|
|
data: Record<string, unknown>,
|
|
markdownKey = "content",
|
|
): string {
|
|
const markdown = String(data[markdownKey] ?? "");
|
|
const frontmatter = { ...data };
|
|
delete frontmatter[markdownKey];
|
|
|
|
const remainingKeys = Object.keys(frontmatter).length;
|
|
|
|
if (remainingKeys === 0) {
|
|
return markdown;
|
|
}
|
|
|
|
const stringifiedFrontmatter = YAML.stringify(frontmatter).trim();
|
|
|
|
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;
|
|
|
|
export default EMDY;
|