import { describe, it, expect } from "vitest"; import { renderArticleMarkdown, stripHtmlToText, } from "../src/publisher/markdown-renderer.js"; import type { PublishMeta, CitationData } from "../src/publisher/html-renderer.js"; const META: PublishMeta = { title: "Test Article", subtitle: "A subtitle", description: "A short description for SEO", authors: [ { name: "Alice", affiliationIndices: [1], affiliationNames: ["MIT"] }, { name: "Bob", affiliationIndices: [2], affiliationNames: ["HF"] }, ], affiliations: [{ name: "MIT" }, { name: "HF" }], date: "2026-04-30", doi: "10.1234/abcd.efgh", }; const doc = (content: any[]) => ({ type: "doc", content }); describe("renderArticleMarkdown - header", () => { it("emits an llms.txt-style header with title, description, authors, date and DOI", () => { const md = renderArticleMarkdown(doc([{ type: "paragraph" }]), META); expect(md).toContain("# Test Article"); expect(md).toContain("> A short description for SEO"); expect(md).toContain("- **Authors**: Alice, Bob"); expect(md).toContain("- **Published**: 2026-04-30"); expect(md).toContain("- **DOI**: https://doi.org/10.1234/abcd.efgh"); expect(md).toContain("---"); }); it("falls back to subtitle when description is empty", () => { const md = renderArticleMarkdown( doc([{ type: "paragraph" }]), { ...META, description: "" }, ); expect(md).toContain("> A subtitle"); }); it("collapses multi-line titles", () => { const md = renderArticleMarkdown( doc([{ type: "paragraph" }]), { ...META, title: "Line one\\nLine two" }, ); expect(md).toContain("# Line one Line two"); expect(md).not.toContain("\\n"); }); }); describe("renderArticleMarkdown - block nodes", () => { it("renders headings with the correct markdown level", () => { const md = renderArticleMarkdown( doc([ { type: "heading", attrs: { level: 2 }, content: [{ type: "text", text: "Hello" }] }, { type: "heading", attrs: { level: 3 }, content: [{ type: "text", text: "Sub" }] }, ]), META, ); expect(md).toContain("## Hello"); expect(md).toContain("### Sub"); }); it("applies bold/italic/code/link marks", () => { const md = renderArticleMarkdown( doc([ { type: "paragraph", content: [ { type: "text", text: "bold", marks: [{ type: "bold" }] }, { type: "text", text: " " }, { type: "text", text: "italic", marks: [{ type: "italic" }] }, { type: "text", text: " " }, { type: "text", text: "code", marks: [{ type: "code" }] }, { type: "text", text: " " }, { type: "text", text: "link", marks: [{ type: "link", attrs: { href: "https://example.com" } }], }, ], }, ]), META, ); expect(md).toContain("**bold**"); expect(md).toContain("*italic*"); expect(md).toContain("`code`"); expect(md).toContain("[link](https://example.com)"); }); it("renders bullet and ordered lists", () => { const md = renderArticleMarkdown( doc([ { type: "bulletList", content: [ { type: "listItem", content: [{ type: "paragraph", content: [{ type: "text", text: "one" }] }] }, { type: "listItem", content: [{ type: "paragraph", content: [{ type: "text", text: "two" }] }] }, ], }, { type: "orderedList", content: [ { type: "listItem", content: [{ type: "paragraph", content: [{ type: "text", text: "first" }] }] }, { type: "listItem", content: [{ type: "paragraph", content: [{ type: "text", text: "second" }] }] }, ], }, ]), META, ); expect(md).toContain("- one"); expect(md).toContain("- two"); expect(md).toContain("1. first"); expect(md).toContain("2. second"); }); it("renders code blocks with language fence", () => { const md = renderArticleMarkdown( doc([ { type: "codeBlock", attrs: { language: "ts" }, content: [{ type: "text", text: "const x = 1;" }], }, ]), META, ); expect(md).toContain("```ts"); expect(md).toContain("const x = 1;"); expect(md).toContain("```"); }); it("renders inline and block math", () => { const md = renderArticleMarkdown( doc([ { type: "paragraph", content: [ { type: "text", text: "Energy: " }, { type: "inlineMath", attrs: { latex: "E = mc^2" } }, ], }, { type: "blockMath", attrs: { latex: "\\int_0^1 x dx" } }, ]), META, ); expect(md).toContain("$E = mc^2$"); expect(md).toContain("$$\n\\int_0^1 x dx\n$$"); }); it("renders tables with a header row separator", () => { const md = renderArticleMarkdown( doc([ { type: "table", content: [ { type: "tableRow", content: [ { type: "tableHeader", content: [{ type: "paragraph", content: [{ type: "text", text: "Col A" }] }] }, { type: "tableHeader", content: [{ type: "paragraph", content: [{ type: "text", text: "Col B" }] }] }, ], }, { type: "tableRow", content: [ { type: "tableCell", content: [{ type: "paragraph", content: [{ type: "text", text: "1" }] }] }, { type: "tableCell", content: [{ type: "paragraph", content: [{ type: "text", text: "2" }] }] }, ], }, ], }, ]), META, ); expect(md).toContain("| Col A | Col B |"); expect(md).toContain("| --- | --- |"); expect(md).toContain("| 1 | 2 |"); }); }); describe("renderArticleMarkdown - custom components", () => { it("collapses HtmlEmbed to a single inline placeholder with title and src", () => { const md = renderArticleMarkdown( doc([ { type: "htmlEmbed", attrs: { src: "d3-chart.html", title: "Citations over time", desc: "" }, }, ]), META, ); expect(md).toContain("*[Interactive visualization: Citations over time]*"); expect(md).not.toContain(" { const md = renderArticleMarkdown( doc([ { type: "note", content: [ { type: "paragraph", content: [{ type: "text", text: "Heads up." }] }, ], }, ]), META, ); expect(md).toContain("> Heads up."); }); it("renders Accordion with bold title and inner content", () => { const md = renderArticleMarkdown( doc([ { type: "accordion", attrs: { title: "More details" }, content: [ { type: "paragraph", content: [{ type: "text", text: "Inside." }] }, ], }, ]), META, ); expect(md).toContain("**More details**"); expect(md).toContain("Inside."); }); it("renders QuoteBlock with attribution", () => { const md = renderArticleMarkdown( doc([ { type: "quoteBlock", attrs: { author: "Ada Lovelace", source: "Notes" }, content: [ { type: "paragraph", content: [{ type: "text", text: "The future is open." }] }, ], }, ]), META, ); expect(md).toContain("> The future is open."); expect(md).toContain("> -- Ada Lovelace, Notes"); }); it("renders HfUser as a markdown link to huggingface.co/", () => { const md = renderArticleMarkdown( doc([ { type: "hfUser", attrs: { username: "tfrere", name: "Thibaud Frere" }, }, ]), META, ); expect(md).toContain("[Thibaud Frere](https://huggingface.co/tfrere)"); }); it("renders Mermaid as a fenced ```mermaid block", () => { const md = renderArticleMarkdown( doc([ { type: "mermaid", attrs: { code: "graph TD\n A --> B" }, }, ]), META, ); expect(md).toContain("```mermaid"); expect(md).toContain("graph TD"); expect(md).toContain("A --> B"); }); it("unwraps Wide / FullWidth / Stack containers", () => { const md = renderArticleMarkdown( doc([ { type: "wide", content: [ { type: "paragraph", content: [{ type: "text", text: "Wide content." }] }, ], }, ]), META, ); expect(md).toContain("Wide content."); expect(md).not.toContain("[wide]"); }); }); describe("renderArticleMarkdown - citations and footnotes", () => { it("renders citations as keys for APA and as numeric tags for IEEE", () => { const json = doc([ { type: "paragraph", content: [ { type: "text", text: "See " }, { type: "citation", attrs: { key: "smith2024", label: "Smith (2024)" } }, { type: "text", text: "." }, ], }, ]); const apa: CitationData = { entries: [{ id: "smith2024" }], orderedKeys: ["smith2024"], style: "apa", }; const ieee: CitationData = { entries: [{ id: "smith2024" }], orderedKeys: ["smith2024"], style: "ieee", }; expect(renderArticleMarkdown(json, META, apa)).toContain("Smith (2024)"); expect(renderArticleMarkdown(json, META, ieee)).toContain("[1]"); }); it("collects footnotes and emits a footnotes section", () => { const md = renderArticleMarkdown( doc([ { type: "paragraph", content: [ { type: "text", text: "Body" }, { type: "footnote", attrs: { content: "First note" } }, { type: "text", text: " more " }, { type: "footnote", attrs: { content: "Second note" } }, ], }, ]), META, ); expect(md).toContain("[^1]"); expect(md).toContain("[^2]"); expect(md).toContain("## Footnotes"); expect(md).toContain("[^1]: First note"); expect(md).toContain("[^2]: Second note"); }); it("appends a References section from the formatted bibliography", () => { const biblio = '
Smith, J. (2024). Test Paper. Journal.
'; const md = renderArticleMarkdown( doc([{ type: "paragraph", content: [{ type: "text", text: "Body" }] }]), META, undefined, biblio, ); expect(md).toContain("## References"); expect(md).toContain("Smith, J. (2024)."); expect(md).toContain("Test Paper"); expect(md).not.toContain(" { it("converts to a markdown link", () => { expect(stripHtmlToText('click')).toBe( "[click](https://example.com)", ); }); it("decodes common HTML entities", () => { expect(stripHtmlToText("Tom & Jerry <3")).toBe("Tom & Jerry <3"); }); it("collapses block tags into newlines and removes the rest", () => { const html = "

One.

Two.

"; expect(stripHtmlToText(html).trim()).toBe("One.\nTwo."); }); });