import { describe, it, expect } from "vitest";
import { renderArticleHTML, type PublishMeta, type CitationData } from "../src/publisher/html-renderer.js";
import type { PublishCSS } from "../src/publisher/index.js";
const EMPTY_CSS: PublishCSS = {
variables: "",
reset: "",
base: "",
layout: "",
print: "",
editorTokens: "",
article: "",
components: "",
publisher: "",
};
const BASIC_META: PublishMeta = {
title: "Test Article",
description: "A test article",
authors: [{ name: "Alice", affiliationIndices: [1], affiliationNames: ["MIT"] }],
affiliations: [{ name: "MIT" }],
date: "2025-01-01",
};
function minimalDoc(content: any[]) {
return { type: "doc", content };
}
describe("renderArticleHTML", () => {
it("produces a complete HTML document", async () => {
const json = minimalDoc([{ type: "paragraph", content: [{ type: "text", text: "Hello world" }] }]);
const html = await renderArticleHTML(json, BASIC_META, EMPTY_CSS);
expect(html).toContain("");
expect(html).toContain("
Test Article");
expect(html).toContain("Hello world");
});
it("escapes HTML in meta fields", async () => {
const meta: PublishMeta = { ...BASIC_META, title: 'Title with "quotes" & ' };
const json = minimalDoc([{ type: "paragraph" }]);
const html = await renderArticleHTML(json, meta, EMPTY_CSS);
expect(html).toContain("&");
expect(html).toContain("<tags>");
expect(html).not.toContain('');
});
it("renders PDF download link when pdfUrl is set", async () => {
const meta: PublishMeta = { ...BASIC_META, pdfUrl: "/published/test/article.pdf" };
const json = minimalDoc([{ type: "paragraph" }]);
const html = await renderArticleHTML(json, meta, EMPTY_CSS);
expect(html).toContain("Download PDF");
expect(html).toContain("/published/test/article.pdf");
});
});
describe("postProcess - accordion", () => {
it("transforms accordion div into details/summary", async () => {
const json = minimalDoc([{
type: "accordion",
attrs: { title: "My Section", open: false },
content: [{ type: "paragraph", content: [{ type: "text", text: "Inner content" }] }],
}]);
const html = await renderArticleHTML(json, BASIC_META, EMPTY_CSS);
expect(html).toContain("");
expect(html).toContain("Inner content");
});
});
describe("postProcess - citations", () => {
it("replaces citation spans with anchor links", async () => {
const json = minimalDoc([{
type: "paragraph",
content: [
{ type: "text", text: "See " },
{ type: "citation", attrs: { key: "smith2024", label: "Smith (2024)" } },
],
}]);
const citationData: CitationData = {
entries: [{ id: "smith2024", title: "Test Paper" }],
orderedKeys: ["smith2024"],
style: "apa",
};
const html = await renderArticleHTML(json, BASIC_META, EMPTY_CSS, citationData);
expect(html).toContain('href="#ref-smith2024"');
expect(html).toContain("citation-inline");
});
it("uses numeric labels for IEEE style", async () => {
const json = minimalDoc([{
type: "paragraph",
content: [
{ type: "citation", attrs: { key: "doe2023", label: "Doe (2023)" } },
],
}]);
const citationData: CitationData = {
entries: [{ id: "doe2023" }],
orderedKeys: ["doe2023"],
style: "ieee",
};
const html = await renderArticleHTML(json, BASIC_META, EMPTY_CSS, citationData);
expect(html).toContain("[1]");
});
});
describe("postProcess - footnotes", () => {
it("collects footnotes and appends a footnotes section", async () => {
const json = minimalDoc([{
type: "paragraph",
content: [
{ type: "text", text: "Text" },
{ type: "footnote", attrs: { content: "This is a footnote" } },
],
}]);
const html = await renderArticleHTML(json, BASIC_META, EMPTY_CSS);
expect(html).toContain('class="footnote-ref"');
expect(html).toContain('id="fn-1"');
expect(html).toContain("This is a footnote");
expect(html).toContain('class="footnotes"');
});
it("numbers multiple footnotes sequentially", async () => {
const json = minimalDoc([{
type: "paragraph",
content: [
{ type: "footnote", attrs: { content: "First note" } },
{ type: "text", text: " and " },
{ type: "footnote", attrs: { content: "Second note" } },
],
}]);
const html = await renderArticleHTML(json, BASIC_META, EMPTY_CSS);
expect(html).toContain('id="fn-1"');
expect(html).toContain('id="fn-2"');
expect(html).toContain("First note");
expect(html).toContain("Second note");
});
});
describe("postProcess - mermaid", () => {
it("transforms mermaid div into pre.mermaid", async () => {
const json = minimalDoc([{
type: "mermaid",
attrs: { code: "graph TD\n A --> B" },
}]);
const html = await renderArticleHTML(json, BASIC_META, EMPTY_CSS);
expect(html).toContain('class="mermaid"');
expect(html).toContain("graph TD");
});
});
describe("postProcess - htmlEmbed", () => {
it("transforms htmlEmbed div into iframe", async () => {
const json = minimalDoc([{
type: "htmlEmbed",
attrs: { src: "d3-chart.html", title: "Chart", desc: "" },
}]);
const html = await renderArticleHTML(json, BASIC_META, EMPTY_CSS);
expect(html).toContain("html-embed-container");
expect(html).toContain('data-embed-src="d3-chart.html"');
expect(html).toContain("