feat: 增加 ECharts 渲染与预览交互增强
- 新增可复用的 markdown-echarts 工作区,支持安全的 YAML 围栏、默认值、语义校验、错误占位和 SVG 渲染 - 将 ECharts 接入统一 Markdown、快速预览、精确预览与 PDF 链路,并复用 Mermaid 的不可跨页和超高图单页适配策略 - 默认示例加入 ECharts,增加源文本折叠、顶部文档状态、页码输入跳转与滚动同步 - 快速预览改用外层容器滚动,使滚动条与精确预览统一贴在预览区域右侧,并兼容显示缩放 - 完善 Docker 工作区复制、可配置 Compose 镜像、测试覆盖和进度文档
This commit is contained in:
@@ -0,0 +1,161 @@
|
||||
// @vitest-environment happy-dom
|
||||
|
||||
import MarkdownIt from "markdown-it";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import {
|
||||
markdownItECharts
|
||||
} from "../src/index.js";
|
||||
import {
|
||||
disposeEChartsBlocks,
|
||||
renderEChartsBlocks,
|
||||
type EChartsEngine,
|
||||
type EChartsInstanceLike
|
||||
} from "../src/browser.js";
|
||||
|
||||
function createDocumentWithChart() {
|
||||
const markdown = new MarkdownIt().use(markdownItECharts);
|
||||
document.body.innerHTML = markdown.render(`
|
||||
\`\`\`echarts
|
||||
version: 1
|
||||
height: 80mm
|
||||
caption: 测试图表
|
||||
option:
|
||||
xAxis:
|
||||
type: category
|
||||
data: [A, B]
|
||||
yAxis:
|
||||
type: value
|
||||
series:
|
||||
- type: bar
|
||||
data: [1, 2]
|
||||
\`\`\`
|
||||
`);
|
||||
}
|
||||
|
||||
function fakeEngine(
|
||||
configureSvg?: (svg: SVGSVGElement) => void
|
||||
) {
|
||||
const dispose = vi.fn();
|
||||
const resize = vi.fn();
|
||||
const init = vi.fn(
|
||||
(container: HTMLElement): EChartsInstanceLike => {
|
||||
dispose.mockImplementation(() => container.replaceChildren());
|
||||
const handlers = new Map<string, () => void>();
|
||||
return {
|
||||
setOption() {
|
||||
const svg = document.createElementNS(
|
||||
"http://www.w3.org/2000/svg",
|
||||
"svg"
|
||||
);
|
||||
svg.setAttribute("viewBox", "0 0 400 300");
|
||||
const text = document.createElementNS(
|
||||
"http://www.w3.org/2000/svg",
|
||||
"text"
|
||||
);
|
||||
text.textContent = "测试图表";
|
||||
svg.append(text);
|
||||
configureSvg?.(svg);
|
||||
container.replaceChildren(svg);
|
||||
handlers.get("finished")?.();
|
||||
},
|
||||
on(eventName, handler) {
|
||||
handlers.set(eventName, handler);
|
||||
},
|
||||
off(eventName) {
|
||||
handlers.delete(eventName);
|
||||
},
|
||||
resize,
|
||||
dispose
|
||||
};
|
||||
}
|
||||
);
|
||||
return {
|
||||
engine: { init } satisfies EChartsEngine,
|
||||
init,
|
||||
dispose,
|
||||
resize
|
||||
};
|
||||
}
|
||||
|
||||
describe("浏览器 ECharts 渲染", () => {
|
||||
it("生成安全的内联 SVG 并释放实例", async () => {
|
||||
createDocumentWithChart();
|
||||
const { engine, init, dispose } = fakeEngine();
|
||||
|
||||
const outcomes = await renderEChartsBlocks(document, {
|
||||
engine,
|
||||
outputMode: "inline-svg"
|
||||
});
|
||||
|
||||
expect(outcomes).toHaveLength(1);
|
||||
expect(outcomes[0]?.success).toBe(true);
|
||||
expect(init).toHaveBeenCalledWith(
|
||||
expect.any(HTMLElement),
|
||||
undefined,
|
||||
expect.objectContaining({ renderer: "svg" })
|
||||
);
|
||||
expect(document.querySelector(".md-echarts-host svg")).not.toBeNull();
|
||||
expect(dispose).toHaveBeenCalledOnce();
|
||||
expect(
|
||||
document.querySelector(".md-echarts")?.getAttribute(
|
||||
"data-echarts-pending"
|
||||
)
|
||||
).toBeNull();
|
||||
});
|
||||
|
||||
it("生成 SVG Data URL 图片", async () => {
|
||||
createDocumentWithChart();
|
||||
const { engine } = fakeEngine();
|
||||
|
||||
const outcomes = await renderEChartsBlocks(document, {
|
||||
engine,
|
||||
outputMode: "svg-image"
|
||||
});
|
||||
|
||||
expect(outcomes[0]?.success).toBe(true);
|
||||
const image = document.querySelector<HTMLImageElement>(
|
||||
".md-echarts-svg-image"
|
||||
);
|
||||
expect(image?.src).toMatch(/^data:image\/svg\+xml/u);
|
||||
expect(image?.alt).toBe("测试图表");
|
||||
});
|
||||
|
||||
it("交互模式保留实例并支持统一销毁", async () => {
|
||||
createDocumentWithChart();
|
||||
const { engine, dispose } = fakeEngine();
|
||||
|
||||
const outcomes = await renderEChartsBlocks(document, {
|
||||
engine,
|
||||
outputMode: "interactive"
|
||||
});
|
||||
|
||||
expect(outcomes[0]?.instance).toBeDefined();
|
||||
expect(dispose).not.toHaveBeenCalled();
|
||||
disposeEChartsBlocks(document);
|
||||
expect(dispose).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it("拒绝渲染器产生的外部 SVG 资源", async () => {
|
||||
createDocumentWithChart();
|
||||
const onError = vi.fn();
|
||||
const { engine } = fakeEngine((svg) => {
|
||||
const image = document.createElementNS(
|
||||
"http://www.w3.org/2000/svg",
|
||||
"image"
|
||||
);
|
||||
image.setAttribute("href", "https://example.com/chart.png");
|
||||
svg.append(image);
|
||||
});
|
||||
|
||||
const outcomes = await renderEChartsBlocks(document, {
|
||||
engine,
|
||||
outputMode: "inline-svg",
|
||||
onError
|
||||
});
|
||||
|
||||
expect(outcomes[0]?.success).toBe(false);
|
||||
expect(onError).toHaveBeenCalledOnce();
|
||||
expect(document.querySelector(".md-echarts-error")).not.toBeNull();
|
||||
expect(document.body.textContent).toContain("外部 SVG 链接");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user