Support multidocument files
This commit is contained in:
parent
c75752b1b7
commit
85b2d5cad7
2 changed files with 191 additions and 22 deletions
147
src/index.ts
147
src/index.ts
|
@ -1,5 +1,82 @@
|
||||||
import YAML from "yaml";
|
import YAML, { YAMLError } from "yaml";
|
||||||
const FRONTMATTER_REGEX = /^---[\s\S]*?---/gm;
|
|
||||||
|
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
|
* 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
|
* @param markdownKey The key to use for the markdown content
|
||||||
* @returns The parsed markdown content and frontmatter
|
* @returns The parsed markdown content and frontmatter
|
||||||
*/
|
*/
|
||||||
function parse(
|
export function parse(content: string, markdownKey = "content"): EMDYData {
|
||||||
content: string,
|
return parseDocument({ content, markdownKey });
|
||||||
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,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -34,7 +96,7 @@ function parse(
|
||||||
* @param markdownKey The key to use for the markdown content
|
* @param markdownKey The key to use for the markdown content
|
||||||
* @returns The parsed markdown content and frontmatter
|
* @returns The parsed markdown content and frontmatter
|
||||||
*/
|
*/
|
||||||
function stringify(
|
export function stringify(
|
||||||
data: Record<string, unknown>,
|
data: Record<string, unknown>,
|
||||||
markdownKey = "content",
|
markdownKey = "content",
|
||||||
): string {
|
): string {
|
||||||
|
@ -50,11 +112,54 @@ function stringify(
|
||||||
|
|
||||||
const stringifiedFrontmatter = YAML.stringify(frontmatter).trim();
|
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 = {
|
const EMDY = {
|
||||||
parse,
|
parse,
|
||||||
|
parseAll,
|
||||||
stringify,
|
stringify,
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { describe, expect, test } from "bun:test";
|
import { describe, expect, test } from "bun:test";
|
||||||
import EMDY from "../index";
|
import EMDY, { EMDYParseError } from "../index";
|
||||||
|
|
||||||
describe(EMDY.parse, () => {
|
describe(EMDY.parse, () => {
|
||||||
test("parses markdown content without frontmatter", () => {
|
test("parses markdown content without frontmatter", () => {
|
||||||
|
@ -31,6 +31,19 @@ And here is the content
|
||||||
content: "And here is the content",
|
content: "And here is the content",
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("throws an EMDYParseError for invalid frontmatter", () => {
|
||||||
|
const content = `---
|
||||||
|
key:
|
||||||
|
anothe
|
||||||
|
---
|
||||||
|
|
||||||
|
stuff`;
|
||||||
|
|
||||||
|
expect(() => {
|
||||||
|
EMDY.parse(content);
|
||||||
|
}).toThrow(EMDYParseError);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe(EMDY.stringify, () => {
|
describe(EMDY.stringify, () => {
|
||||||
|
@ -65,3 +78,54 @@ describe(EMDY.stringify, () => {
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe(EMDY.parseAll, () => {
|
||||||
|
test("parses multiple emdy documents", () => {
|
||||||
|
const content = `---
|
||||||
|
hello: world
|
||||||
|
goodbye: world
|
||||||
|
---
|
||||||
|
|
||||||
|
some markdown
|
||||||
|
===
|
||||||
|
another document
|
||||||
|
===
|
||||||
|
---
|
||||||
|
more frontmatter: yep
|
||||||
|
---
|
||||||
|
|
||||||
|
and how
|
||||||
|
`.trim();
|
||||||
|
|
||||||
|
const parsed = EMDY.parseAll(content);
|
||||||
|
expect(parsed).toEqual([
|
||||||
|
{
|
||||||
|
hello: "world",
|
||||||
|
goodbye: "world",
|
||||||
|
content: "some markdown",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
content: "another document",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"more frontmatter": "yep",
|
||||||
|
content: "and how",
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("parses a single emdy document", () => {
|
||||||
|
const content = `---
|
||||||
|
hello: world
|
||||||
|
---
|
||||||
|
example!
|
||||||
|
`;
|
||||||
|
|
||||||
|
expect(EMDY.parseAll(content)).toEqual([
|
||||||
|
{
|
||||||
|
hello: "world",
|
||||||
|
content: "example!",
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue