feat: 增加 ECharts 渲染与预览交互增强

- 新增可复用的 markdown-echarts 工作区,支持安全的 YAML 围栏、默认值、语义校验、错误占位和 SVG 渲染
- 将 ECharts 接入统一 Markdown、快速预览、精确预览与 PDF 链路,并复用 Mermaid 的不可跨页和超高图单页适配策略
- 默认示例加入 ECharts,增加源文本折叠、顶部文档状态、页码输入跳转与滚动同步
- 快速预览改用外层容器滚动,使滚动条与精确预览统一贴在预览区域右侧,并兼容显示缩放
- 完善 Docker 工作区复制、可配置 Compose 镜像、测试覆盖和进度文档
This commit is contained in:
SkyJourney
2026-07-27 16:05:14 +08:00
parent 1db21c80fc
commit f5a46f79f1
53 changed files with 3998 additions and 221 deletions
+1
View File
@@ -26,6 +26,7 @@
"@mdit/plugin-katex": "^1.0.1",
"@mdit/plugin-tasklist": "^1.0.1",
"@md-to-pdf/core": "0.1.0",
"@md-to-pdf/markdown-echarts": "0.1.0",
"gray-matter": "^4.0.3",
"highlight.js": "^11.11.1",
"markdown-it": "^14.3.0",
+23 -1
View File
@@ -7,6 +7,10 @@ import type {
RenderedMarkdownDocument,
ThemeFeature
} from "@md-to-pdf/core";
import {
markdownItECharts,
type MarkdownItEChartsErrorContext
} from "@md-to-pdf/markdown-echarts";
import matter from "gray-matter";
import hljs from "highlight.js";
import MarkdownIt from "markdown-it";
@@ -110,6 +114,17 @@ markdown.renderer.rules.fence = (
return renderFallbackFence(tokens, index, renderer);
};
markdown.use(markdownItECharts, {
onError(error, context: MarkdownItEChartsErrorContext) {
const environment = context.environment as
| { warnings?: string[] }
| undefined;
environment?.warnings?.push(
`ECharts 图表配置无效:${error.message}`
);
}
});
const safeHtmlOptions: sanitizeHtml.IOptions = {
allowedTags: [
...sanitizeHtml.defaults.allowedTags,
@@ -130,10 +145,16 @@ const safeHtmlOptions: sanitizeHtml.IOptions = {
"*": ["class", "id", "title", "aria-*", "role"],
a: ["href", "name", "target", "rel"],
div: ["class", "data-mermaid-pending"],
figure: [
"class",
"data-echarts-pending",
"data-echarts-protocol"
],
img: ["src", "alt", "title", "width", "height"],
input: ["type", "checked", "disabled", "class"],
li: ["class", "value"],
ol: ["class", "start"],
pre: ["class", "hidden"],
span: ["class", "style"],
td: ["colspan", "rowspan", "style"],
th: ["colspan", "rowspan", "scope", "style"]
@@ -262,7 +283,8 @@ function detectFeatures(html: string): ThemeFeature[] {
["task-list", /\bmd-task-list-item\b/u],
["footnote", /\bfootnote/u],
["katex", /\bkatex\b/u],
["mermaid", /\bmermaid\b/u]
["mermaid", /\bmermaid\b/u],
["echarts", /\bmd-echarts\b/u]
];
return featureChecks
@@ -139,6 +139,65 @@ flowchart LR
expect(result.bodyHtml).toContain("class B important");
});
it("将 ECharts YAML 围栏转换为安全占位并识别能力", () => {
const result = renderMarkdown(`
\`\`\`echarts
version: 1
height: 80mm
caption: 年度收入
option:
xAxis:
type: category
data: [2023, 2024]
yAxis:
type: value
series:
- type: bar
data: [120, 180]
\`\`\`
`);
expect(result.bodyHtml).toContain('class="md-echarts"');
expect(result.bodyHtml).toContain(
'data-echarts-pending="true"'
);
expect(result.bodyHtml).toContain(
'data-echarts-protocol="1"'
);
expect(result.bodyHtml).toContain(
'class="md-echarts-definition" hidden="hidden"'
);
expect(result.bodyHtml).toContain(
"<figcaption>年度收入</figcaption>"
);
expect(result.features).toContain("echarts");
expect(result.warnings).toEqual([]);
});
it("将无效 ECharts 配置转为局部错误和文档告警", () => {
const result = renderMarkdown(`
\`\`\`echarts
version: 1
height: 80mm
option:
series:
- type: custom
\`\`\`
`);
expect(result.bodyHtml).toContain(
'class="md-echarts-error"'
);
expect(result.bodyHtml).not.toContain(
"data-echarts-pending"
);
expect(result.features).toContain("echarts");
expect(result.warnings).toHaveLength(1);
expect(result.warnings[0]).toContain(
"ECharts 图表配置无效"
);
});
it("阻止原始 HTML 和危险链接形成可执行内容", () => {
const result = renderMarkdown(`
<script>alert("xss")</script>