From be0c2a424b68e5470c20294bf09cf16c50345d73 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C5=ABdolfs=20O=C5=A1i=C5=86=C5=A1?= Date: Wed, 30 Sep 2026 15:50:40 +0200 Subject: [PATCH] Don't throw on empty or invalid frontmatter Since js-yaml 5 an empty or comment-only block throws instead of returning undefined, as does invalid YAML. Markdown renders the content from the same parse, so one such block made the whole document fail to render. --- src/lib/frontmatter.ts | 7 ++++- tests/unit/frontmatter.test.ts | 50 ++++++++++++++++++++++++++++++++++ 2 files changed, 56 insertions(+), 1 deletion(-) create mode 100644 tests/unit/frontmatter.test.ts 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", + }); +});