diff --git a/src/lib/frontmatter.ts b/src/lib/frontmatter.ts index d39e53899..ad40b285a 100644 --- a/src/lib/frontmatter.ts +++ b/src/lib/frontmatter.ts @@ -12,7 +12,12 @@ export function parseFrontmatter(input: string): ParsedFrontmatter { if (!match) { return { data: {}, content: input }; } - const parsed = load(match[1]); + let parsed: unknown; + try { + parsed = load(match[1]); + } catch { + parsed = undefined; + } const data = parsed && typeof parsed === "object" && !Array.isArray(parsed) ? (parsed as Record) diff --git a/tests/unit/frontmatter.test.ts b/tests/unit/frontmatter.test.ts new file mode 100644 index 000000000..3c0cce095 --- /dev/null +++ b/tests/unit/frontmatter.test.ts @@ -0,0 +1,50 @@ +import { expect, test } from "vitest"; + +import { parseFrontmatter } from "@app/lib/frontmatter"; + +test.each([ + ["LF", "---\ntitle: Hello\nn: 1\n---\n# Body\n"], + ["CRLF", "---\r\ntitle: Hello\r\nn: 1\r\n---\r\n# Body\n"], +])("splits %s frontmatter from the content", (_, input) => { + expect(parseFrontmatter(input)).toEqual({ + data: { title: "Hello", n: 1 }, + content: "# Body\n", + }); +}); + +test("accepts a closing fence at the end of the input", () => { + expect(parseFrontmatter("---\na: 1\n---")).toEqual({ + data: { a: 1 }, + content: "", + }); +}); + +test("stops at the first closing fence", () => { + expect(parseFrontmatter("---\na: 1\n---\ntext\n---\nmore")).toEqual({ + data: { a: 1 }, + content: "text\n---\nmore", + }); +}); + +test.each([ + "# Title\n---\na: 1\n---\n", + " ---\na: 1\n---\n", + "---\na: 1\n", + "---\n---\n", +])("leaves %j without frontmatter as is", input => { + expect(parseFrontmatter(input)).toEqual({ data: {}, content: input }); +}); + +test.each([ + ["a scalar", "just text"], + ["a list", "- a\n- b"], + ["null", "~"], + ["nothing", ""], + ["a comment", "# draft"], + ["invalid YAML", "Note: see: below"], +])("drops a block holding %s but strips it", (_, yaml) => { + expect(parseFrontmatter(`---\n${yaml}\n---\nbody`)).toEqual({ + data: {}, + content: "body", + }); +});