feat: 实现真实分页预览

This commit is contained in:
SkyJourney
2026-07-26 07:27:13 +08:00
parent 52cf816683
commit 1d93f31f85
15 changed files with 1614 additions and 269 deletions
+53
View File
@@ -0,0 +1,53 @@
import { defaultExportConfig } from "@md-to-pdf/core";
import { describe, expect, it } from "vitest";
import {
calculateMermaidPageFit,
getPageContentDimensions,
parseSvgViewBox
} from "../src/mermaid-page-fit";
describe("Mermaid 单页高度适配", () => {
it("解析 SVG viewBox 尺寸", () => {
expect(parseSvgViewBox("0 0 1200 2400")).toEqual({
width: 1200,
height: 2400
});
expect(parseSvgViewBox("0,0,800,600")).toEqual({
width: 800,
height: 600
});
expect(parseSvgViewBox("0 0 0 600")).toBeUndefined();
expect(parseSvgViewBox(null)).toBeUndefined();
});
it("使用纸张和页边距计算正文区域", () => {
const dimensions = getPageContentDimensions(defaultExportConfig);
expect(dimensions.width).toBeCloseTo(672.7559, 3);
expect(dimensions.height).toBeCloseTo(1001.5748, 3);
});
it("宽度适配后未超高时保持普通 Mermaid 布局", () => {
const fit = calculateMermaidPageFit(
{ width: 1600, height: 900 },
{ width: 672, height: 1000 }
);
expect(fit).toEqual({
width: 672,
height: 378,
scaled: false
});
});
it("宽度适配后超高时按页面高度等比例缩小", () => {
const fit = calculateMermaidPageFit(
{ width: 1000, height: 2000 },
{ width: 672, height: 1000 }
);
expect(fit.scaled).toBe(true);
expect(fit.height).toBe(999);
expect(fit.width).toBe(499.5);
});
});
+185
View File
@@ -0,0 +1,185 @@
import { defaultExportConfig } from "@md-to-pdf/core";
import { describe, expect, it } from "vitest";
import {
buildPagedMediaCss,
createPagedPreviewRenderRequest,
documentGeometryCss,
formatPageNumber,
isPagedPreviewFrameMessage,
isPagedPreviewRenderRequest,
PAGED_PREVIEW_MESSAGE_SCOPE,
type PagedPreviewPayload
} from "../src/paged-preview";
const payload: PagedPreviewPayload = {
articleHtml: '<article id="write"><h1>分页测试</h1></article>',
fileName: "分页测试.md",
metadata: {
title: "分页测试",
author: "",
subject: "",
keywords: [],
language: "zh-CN"
},
features: [],
themeCss: "#write { color: #333; }",
exportConfig: defaultExportConfig
};
describe("分页预览协议", () => {
it("生成真实纸张尺寸和页边距 CSS", () => {
const css = buildPagedMediaCss(defaultExportConfig, payload);
expect(css).toContain("size: 210mm 297mm");
expect(css).toContain("margin: 16mm 16mm");
expect(css).toContain("@bottom-center");
expect(css).not.toContain("counter-reset: page");
expect(css).toContain("#write thead");
expect(css).toContain("display: table-header-group");
expect(css).toContain("break-inside: avoid");
expect(css).toContain(".pagedjs_pages");
expect(css).toContain('html[data-render-target="pdf"]');
expect(css).toContain("break-after: page");
expect(css).not.toContain("@media print");
});
it("安全替换页眉变量并生成三栏页边距盒", () => {
const css = buildPagedMediaCss(
{
...defaultExportConfig,
header: {
...defaultExportConfig.header,
enabled: true,
showDivider: true,
left: {
enabled: true,
content: '${title} — ${filename} "测试"'
},
center: {
enabled: true,
content: "${author}"
}
}
},
{
...payload,
metadata: {
...payload.metadata,
author: "内网团队"
}
}
);
expect(css).toContain("@top-left");
expect(css).toContain("@top-center");
expect(css).toContain("@top-right");
expect(css).toContain(
'content: "分页测试 — 分页测试.md \\"测试\\""'
);
expect(css).toContain('content: "内网团队"');
expect(css).toContain("border-bottom: 0.2mm solid currentColor");
});
it("支持自定义页码模板和起始页码", () => {
const css = buildPagedMediaCss(
{
...defaultExportConfig,
footer: {
...defaultExportConfig.footer,
format: "custom",
template: "第 ${page} / ${pages} 页",
alignment: "right",
startFrom: 5
}
},
payload
);
expect(css).toContain("@bottom-right");
expect(
formatPageNumber(
{
...defaultExportConfig.footer,
format: "custom",
template: "第 ${page} / ${pages} 页",
startFrom: 5
},
2,
8
)
).toBe("第 7 / 8 页");
});
it("逐页递增内置页码格式但保持真实总页数", () => {
expect(
formatPageNumber(defaultExportConfig.footer, 0, 5)
).toBe("1 / 5");
expect(
formatPageNumber(defaultExportConfig.footer, 1, 5)
).toBe("2 / 5");
expect(
formatPageNumber(
{
...defaultExportConfig.footer,
format: "chinese-page-total",
startFrom: 5
},
3,
5
)
).toBe("第 8 页 / 共 5 页");
expect(
formatPageNumber(
{
...defaultExportConfig.footer,
format: "dash-page"
},
4,
5
)
).toBe("- 5 -");
});
it("在主题 CSS 之后强制分页画布保持透明", () => {
expect(documentGeometryCss).toContain(
"background: transparent !important"
);
});
it("生成并识别分页渲染请求", () => {
const request = createPagedPreviewRenderRequest(7, payload);
expect(request).toEqual({
scope: PAGED_PREVIEW_MESSAGE_SCOPE,
type: "render",
requestId: 7,
payload
});
expect(isPagedPreviewRenderRequest(request)).toBe(true);
expect(
isPagedPreviewRenderRequest({
...request,
requestId: "7"
})
).toBe(false);
});
it("只接受分页 iframe 协议消息", () => {
expect(
isPagedPreviewFrameMessage({
scope: PAGED_PREVIEW_MESSAGE_SCOPE,
type: "rendered",
requestId: 3,
pageCount: 2,
contentHeight: 2300,
mermaidErrors: []
})
).toBe(true);
expect(
isPagedPreviewFrameMessage({
scope: "other",
type: "ready"
})
).toBe(false);
});
});