From 1db971abd8738cd5ffe08ccd54674c694271da43 Mon Sep 17 00:00:00 2001 From: quality Date: Thu, 8 Oct 2026 02:49:27 -0400 Subject: [PATCH] test: cover mdx-to-markdown block, fence and attribute edge cases Adds scripts/mdx-to-markdown.edge-cases.test.ts exercising the converter paths that had no tests: unterminated and nested blocks, stray closing tags, fence-aware close detection (tilde fences, longer fences, fence lines with trailing text), dedent of blank/zero-indent bodies, attribute parsing fallbacks, YouTube/Step/AgentBlock rendering variants, CRLF and single-quoted frontmatter, and the siteBase/imagePrefix/rewriteImage options. mdx-to-markdown.ts: 93.1% stmts / 79.7% branches -> 100% / 99.2%. Closes #273 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Signed-off-by: quality --- scripts/mdx-to-markdown.edge-cases.test.ts | 244 +++++++++++++++++++++ 1 file changed, 244 insertions(+) create mode 100644 scripts/mdx-to-markdown.edge-cases.test.ts diff --git a/scripts/mdx-to-markdown.edge-cases.test.ts b/scripts/mdx-to-markdown.edge-cases.test.ts new file mode 100644 index 0000000..648d24a --- /dev/null +++ b/scripts/mdx-to-markdown.edge-cases.test.ts @@ -0,0 +1,244 @@ +import { describe, it, expect } from "vitest"; +import { + convertMdxToMarkdown, + parseAttrs, + splitFrontmatter, +} from "./mdx-to-markdown"; + +const SITE = "https://spektacular.dev"; + +const convert = (src: string) => convertMdxToMarkdown(src, { siteBase: SITE }); + +describe("splitFrontmatter — edge cases", () => { + it("accepts CRLF line endings and single-quoted values", () => { + const { data, body } = splitFrontmatter( + "---\r\ntitle: 'Quoted'\r\ndescription: plain\r\n---\r\nBody\r\n" + ); + expect(data).toEqual({ title: "Quoted", description: "plain" }); + expect(body).toBe("Body\r\n"); + }); + + it("ignores frontmatter lines that are not key: value pairs", () => { + const { data } = splitFrontmatter( + "---\ntitle: T\n - list item\n# comment\nbad key: x\n---\n" + ); + expect(data).toEqual({ title: "T" }); + }); + + it("does not treat a mismatched quote pair as quoted", () => { + const { data } = splitFrontmatter("---\ntitle: \"half'\n---\n"); + expect(data.title).toBe("\"half'"); + }); +}); + +describe("parseAttrs — edge cases", () => { + it("reads single-quoted values", () => { + expect(parseAttrs("heading='Clone it'")).toEqual({ heading: "Clone it" }); + }); + + it("parses false, negative and decimal numeric expressions", () => { + expect(parseAttrs("a={false} b={-3} c={2.5}")).toEqual({ + a: false, + b: -3, + c: 2.5, + }); + }); + + it("unwraps a quoted string literal inside an expression", () => { + expect(parseAttrs("for={\"codex\"} alt={'x'}")).toEqual({ + for: "codex", + alt: "x", + }); + }); + + it("keeps a non-literal expression as its trimmed source text", () => { + expect(parseAttrs("start={ offset + 1 }")).toEqual({ + start: "offset + 1", + }); + }); +}); + +describe(" rendering", () => { + it("appends t= with & when the url already has a query string", () => { + const out = convert( + '' + ); + expect(out.markdown).toBe( + "[Watch this section on YouTube (from 1:05)](https://youtu.be/x?feature=share&t=65)\n" + ); + }); + + it("omits the timestamp when start is missing", () => { + const out = convert(''); + expect(out.markdown).toBe( + "[Watch this section on YouTube](https://youtu.be/x)\n" + ); + }); + + it("treats a non-numeric start as absent", () => { + const out = convert(''); + expect(out.markdown).toContain("[Watch this section on YouTube]("); + }); +}); + +describe(" heading variants", () => { + it("renders heading only when number is absent", () => { + const out = convert('\nrun it\n'); + expect(out.markdown).toBe("### Install\n\nrun it\n"); + }); + + it("renders Step N when heading is absent", () => { + const out = convert("\nrun it\n"); + expect(out.markdown).toBe("### Step 2\n\nrun it\n"); + }); +}); + +describe(" label fallbacks", () => { + it("capitalises an unknown agent id", () => { + const out = convert('\nhi\n'); + expect(out.markdown).toBe("**Gemini**\n\nhi\n"); + }); + + it("falls back to Agent when no id is given", () => { + const out = convert("\nhi\n"); + expect(out.markdown).toBe("**Agent**\n\nhi\n"); + }); +}); + +describe("block parsing — structure and fences", () => { + it("records a note and keeps content for an unterminated block", () => { + const out = convert("\nleft open\n\nmore"); + expect(out.notes).toEqual([ + "unterminated at line 1; tag removed, content kept", + ]); + expect(out.markdown).toBe("left open\n\nmore\n"); + }); + + it("removes a stray closing tag and records a note", () => { + const out = convert("text\n\nafter"); + expect(out.notes).toEqual(["stray closing tag removed at line 2: "]); + expect(out.markdown).toBe("text\nafter\n"); + }); + + it("matches the outermost close when the same component is nested", () => { + const src = [ + "", + "outer", + "", + "inner", + "", + "tail", + "", + "after", + ].join("\n"); + const out = convert(src); + expect(out.markdown).toBe( + "### Step 1\n\nouter\n### Step 2\n\ninner\ntail\nafter\n" + ); + expect(out.notes).toEqual([]); + }); + + it("ignores a closing tag that appears inside a fence within the block", () => { + const src = [ + "", + "```html", + "", + "", + "```", + "body", + "", + ].join("\n"); + const out = convert(src); + expect(out.notes).toEqual([]); + expect(out.markdown).toBe( + "### Step 1\n\n```html\n\n\n```\nbody\n" + ); + }); + + it("does not close a backtick fence with a tilde fence or a shorter fence", () => { + const src = [ + "````", + "~~~", + "```", + "", + "````", + '', + ].join("\n"); + const out = convert(src); + // Everything until the 4-backtick close stays verbatim; the component + // after the fence is still converted. + expect(out.markdown).toContain("````\n~~~\n```\n\n````"); + expect(out.markdown).toContain("[Watch this section on YouTube]"); + expect(out.notes).toEqual([]); + }); + + it("treats tilde fences as code and does not parse components inside them", () => { + const out = convert("~~~\n\n~~~\n"); + expect(out.markdown).toBe("~~~\n\n~~~\n"); + expect(out.notes).toEqual([]); + }); + + it("does not treat a fence line with trailing info text as a close", () => { + const out = convert("```\n``` not-a-close\n\n```\n"); + expect(out.notes).toEqual([]); + expect(out.markdown).toBe("```\n``` not-a-close\n\n```\n"); + }); + + it("keeps a block whose body is only blank lines", () => { + const out = convert("\n\n \n"); + expect(out.markdown).toBe("### Step 3\n"); + }); + + it("leaves an unindented body untouched by dedent", () => { + const out = convert("\nno indent\n some indent\n"); + expect(out.markdown).toBe("### Step 1\n\nno indent\n some indent\n"); + }); +}); + +describe("convertMdxToMarkdown — options and frontmatter fallbacks", () => { + it("uses frontmatter description when summary is absent", () => { + const out = convert('---\ntitle: T\ndescription: "Desc"\n---\nBody'); + expect(out.description).toBe("Desc"); + expect(out.markdown).toBe("# T\n\n_Desc_\n\nBody\n"); + }); + + it("emits only the body when there is no title or description", () => { + const out = convert("Body only"); + expect(out.title).toBeUndefined(); + expect(out.description).toBeUndefined(); + expect(out.markdown).toBe("Body only\n"); + }); + + it("strips a trailing slash from siteBase before joining links", () => { + const out = convertMdxToMarkdown("[a](/install/)", { + siteBase: SITE + "/", + }); + expect(out.markdown).toBe(`[a](${SITE}/install/)\n`); + }); + + it("honours a custom imagePrefix and rewriteImage, preserving the title", () => { + const out = convertMdxToMarkdown( + '![alt](/assets/a.png "Title") ![other](/images/b.png)', + { + siteBase: SITE, + imagePrefix: "/assets/", + rewriteImage: s => `./img/${s.split("/").pop()}`, + } + ); + expect(out.images).toEqual(["/assets/a.png"]); + expect(out.markdown).toBe( + '![alt](./img/a.png "Title") ![other](/images/b.png)\n' + ); + }); + + it("collapses three or more blank lines to one", () => { + const out = convert("a\n\n\n\n\nb"); + expect(out.markdown).toBe("a\n\nb\n"); + }); + + it("protects inline code from brace escaping and tag removal", () => { + const out = convert("Use `{x}` and `` here"); + expect(out.markdown).toBe("Use `{x}` and `` here\n"); + expect(out.notes).toEqual([]); + }); +});