feat: 增加 ECharts 渲染与预览交互增强
- 新增可复用的 markdown-echarts 工作区,支持安全的 YAML 围栏、默认值、语义校验、错误占位和 SVG 渲染 - 将 ECharts 接入统一 Markdown、快速预览、精确预览与 PDF 链路,并复用 Mermaid 的不可跨页和超高图单页适配策略 - 默认示例加入 ECharts,增加源文本折叠、顶部文档状态、页码输入跳转与滚动同步 - 快速预览改用外层容器滚动,使滚动条与精确预览统一贴在预览区域右侧,并兼容显示缩放 - 完善 Docker 工作区复制、可配置 Compose 镜像、测试覆盖和进度文档
This commit is contained in:
@@ -0,0 +1,57 @@
|
||||
// @vitest-environment happy-dom
|
||||
|
||||
import { defaultExportConfig } from "@md-to-pdf/core";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { fitOversizedEChartsToPage } from "../src/echarts-page-fit";
|
||||
|
||||
describe("ECharts 单页高度适配", () => {
|
||||
it("将超高 SVG 和宿主同步缩放到单页内容区", () => {
|
||||
const root = document.createElement("div");
|
||||
root.innerHTML = `
|
||||
<figure class="md-echarts">
|
||||
<div class="md-echarts-host" style="height: 2000px">
|
||||
<svg width="1000" height="2000"></svg>
|
||||
</div>
|
||||
</figure>
|
||||
`;
|
||||
|
||||
fitOversizedEChartsToPage(root, defaultExportConfig);
|
||||
|
||||
const figure = root.querySelector<HTMLElement>(".md-echarts");
|
||||
const host = root.querySelector<HTMLElement>(
|
||||
".md-echarts-host"
|
||||
);
|
||||
const svg = root.querySelector<SVGSVGElement>("svg");
|
||||
expect(figure?.dataset.pageHeightFitted).toBe("true");
|
||||
expect(Number.parseFloat(host?.style.height ?? "0")).toBeCloseTo(
|
||||
1000.5748,
|
||||
3
|
||||
);
|
||||
expect(host?.style.aspectRatio).toBe("");
|
||||
expect(svg?.getAttribute("viewBox")).toBe("0 0 1000 2000");
|
||||
expect(Number.parseFloat(svg?.style.height ?? "0")).toBeCloseTo(
|
||||
1000.5748,
|
||||
3
|
||||
);
|
||||
});
|
||||
|
||||
it("普通高度图表不改变作者设置的宿主高度", () => {
|
||||
const root = document.createElement("div");
|
||||
root.innerHTML = `
|
||||
<figure class="md-echarts">
|
||||
<div class="md-echarts-host" style="height: 300px">
|
||||
<svg viewBox="0 0 1000 400"></svg>
|
||||
</div>
|
||||
</figure>
|
||||
`;
|
||||
|
||||
fitOversizedEChartsToPage(root, defaultExportConfig);
|
||||
|
||||
const figure = root.querySelector<HTMLElement>(".md-echarts");
|
||||
const host = root.querySelector<HTMLElement>(
|
||||
".md-echarts-host"
|
||||
);
|
||||
expect(figure?.dataset.pageHeightFitted).toBeUndefined();
|
||||
expect(host?.style.height).toBe("300px");
|
||||
});
|
||||
});
|
||||
@@ -41,7 +41,8 @@ describe("分页预览协议", () => {
|
||||
expect(css).toContain("padding: 34px 0");
|
||||
expect(css).toContain('html[data-render-target="preview"]');
|
||||
expect(css).toContain("overflow-x: hidden");
|
||||
expect(css).toContain("overflow-y: scroll");
|
||||
expect(css).toContain("overflow-y: hidden");
|
||||
expect(css).not.toContain("scrollbar-color");
|
||||
expect(css).toContain('html[data-render-target="pdf"]');
|
||||
expect(css).toContain("height: auto !important");
|
||||
expect(css).toContain("overflow: visible !important");
|
||||
@@ -178,9 +179,21 @@ describe("分页预览协议", () => {
|
||||
type: "rendered",
|
||||
requestId: 3,
|
||||
pageCount: 2,
|
||||
contentHeight: 2400,
|
||||
echartsErrors: [],
|
||||
mermaidErrors: []
|
||||
})
|
||||
).toBe(true);
|
||||
expect(
|
||||
isPagedPreviewFrameMessage({
|
||||
scope: PAGED_PREVIEW_MESSAGE_SCOPE,
|
||||
type: "rendered",
|
||||
requestId: 3,
|
||||
pageCount: 2,
|
||||
echartsErrors: [],
|
||||
mermaidErrors: []
|
||||
})
|
||||
).toBe(false);
|
||||
expect(
|
||||
isPagedPreviewFrameMessage({
|
||||
scope: "other",
|
||||
|
||||
@@ -23,6 +23,7 @@ describe("PDF 导出客户端", () => {
|
||||
blob: new Blob(["%PDF-test"]),
|
||||
fileName: "测试.pdf",
|
||||
pageCount: 1,
|
||||
echartsErrorCount: 0,
|
||||
mermaidErrorCount: 0
|
||||
};
|
||||
const cache: CachedPdfExport = { key, result };
|
||||
@@ -65,6 +66,7 @@ describe("PDF 导出客户端", () => {
|
||||
blob: new Blob(["%PDF-cached"]),
|
||||
fileName: "测试.pdf",
|
||||
pageCount: 1,
|
||||
echartsErrorCount: 0,
|
||||
mermaidErrorCount: 0
|
||||
};
|
||||
const cache: CachedPdfExport = {
|
||||
@@ -95,6 +97,7 @@ describe("PDF 导出客户端", () => {
|
||||
blob: new Blob(["%PDF-new"]),
|
||||
fileName: "测试.pdf",
|
||||
pageCount: 2,
|
||||
echartsErrorCount: 0,
|
||||
mermaidErrorCount: 0
|
||||
};
|
||||
const requester = vi.fn(async () => result);
|
||||
@@ -128,6 +131,7 @@ describe("PDF 导出客户端", () => {
|
||||
"content-disposition":
|
||||
"attachment; filename=\"document.pdf\"",
|
||||
"x-pdf-page-count": "4",
|
||||
"x-echarts-error-count": "2",
|
||||
"x-mermaid-error-count": "1"
|
||||
}
|
||||
});
|
||||
@@ -149,6 +153,7 @@ describe("PDF 导出客户端", () => {
|
||||
);
|
||||
expect(result.fileName).toBe("document.pdf");
|
||||
expect(result.pageCount).toBe(4);
|
||||
expect(result.echartsErrorCount).toBe(2);
|
||||
expect(result.mermaidErrorCount).toBe(1);
|
||||
expect(await result.blob.text()).toBe("%PDF-test");
|
||||
});
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { getNearestPageNumber } from "../src/preview-page";
|
||||
import {
|
||||
getNearestPageNumber,
|
||||
getPreviewPageScrollTop,
|
||||
normalizePreviewPageNumber
|
||||
} from "../src/preview-page";
|
||||
|
||||
const pages = [
|
||||
{ top: 0, bottom: 100 },
|
||||
@@ -22,3 +26,35 @@ describe("getNearestPageNumber", () => {
|
||||
expect(getNearestPageNumber(Number.NaN, pages)).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("normalizePreviewPageNumber", () => {
|
||||
it("保留有效的整数页码", () => {
|
||||
expect(normalizePreviewPageNumber("12", 3, 65)).toBe(12);
|
||||
});
|
||||
|
||||
it("将越界页码限制在有效范围", () => {
|
||||
expect(normalizePreviewPageNumber("0", 3, 65)).toBe(1);
|
||||
expect(normalizePreviewPageNumber("99", 3, 65)).toBe(65);
|
||||
});
|
||||
|
||||
it("无效输入恢复当前页", () => {
|
||||
expect(normalizePreviewPageNumber("", 7, 65)).toBe(7);
|
||||
expect(normalizePreviewPageNumber("3.5", 7, 65)).toBe(7);
|
||||
expect(normalizePreviewPageNumber("abc", 7, 65)).toBe(7);
|
||||
});
|
||||
|
||||
it("没有可用页面时返回 0", () => {
|
||||
expect(normalizePreviewPageNumber("1", 0, 0)).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getPreviewPageScrollTop", () => {
|
||||
it("按目标页相对滚动容器的位置计算滚动量", () => {
|
||||
expect(getPreviewPageScrollTop(200, 900, 120)).toBe(980);
|
||||
});
|
||||
|
||||
it("避免返回负数或无效滚动量", () => {
|
||||
expect(getPreviewPageScrollTop(20, 10, 100)).toBe(0);
|
||||
expect(getPreviewPageScrollTop(Number.NaN, 10, 0)).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user