新增共享 Preview Engine,统一 Web 连续预览、快速分页、Playwright PDF 与 Electron PDF;实现稳定前缀复用和修改位置后的增量分页,保留媒体块按文档顺序串行回填与单次重排。 完善跨端链接与桌面文档工作流:Web 受控处理锚点和 HTTP/HTTPS 外链;Desktop 支持本地路径、file URI、系统协议、多窗口、同文件单例、Markdown 当前或新窗口打开,以及聚焦时外部文件变化提示。 统一四套内置主题名称并默认使用 Typora Github;修复连续预览双滚动条、ECharts 尺寸、PDF 本地链接、围栏代码块 Typora DOM 与重复行内样式;桌面发行链强制完整重建内嵌 Web,避免安装包携带陈旧资源。 发布 Web/Compose 与 Windows NSIS/ZIP:镜像 yixiong/md-to-pdf:v0.5.0 已健康部署;NSIS SHA-256 为 60992D1FDCA513F46346C78478537EB4159D8C0E76B41ECF3CDC25BE77707D92,ZIP SHA-256 为 D74F82293FB67126E583546CBA894569EFC9A0B1B6343FAC648CCC95F1D188D8,本机安装版已升级至 v0.5.0。 验证:全项目 238 项测试通过,类型检查、生产构建和 git diff --check 通过;Web 快速/连续/精确预览、Compose、Desktop 多窗口、窗口状态、文件关联、链接与代码块均完成真实环境验收。
328 lines
9.9 KiB
Markdown
328 lines
9.9 KiB
Markdown
# @md-to-pdf/markdown-echarts
|
||
|
||
为 Markdown 增加安全、可验证、可复用的 ECharts YAML 围栏,
|
||
并在浏览器中渲染为 SVG。
|
||
|
||
该包不依赖 React、Paged.js 或特定 PDF 引擎。宿主可以将生成的
|
||
HTML 用于网页预览,也可以在图表完成渲染后交给 Chromium 输出 PDF。
|
||
本项目由 `@md-to-pdf/renderer` 接入围栏插件,再由
|
||
`@md-to-pdf/preview-engine` 统一完成连续预览、分页适配和 SVG 冻结。
|
||
|
||
## 目录结构
|
||
|
||
```text
|
||
src/
|
||
markdown-it-plugin.ts ECharts YAML 围栏与错误占位
|
||
parse-yaml.ts 受限 YAML 解析
|
||
protocol.ts 围栏协议与基础校验
|
||
security-policy.ts URL、函数、原型和规模安全策略
|
||
semantic-validator.ts 系列数据与坐标系语义校验
|
||
dom-contract.ts 宿主与浏览器运行时共用的 DOM 协议
|
||
browser.ts 浏览器入口
|
||
browser/ ECharts 按需加载、渲染和 SVG 冻结
|
||
index.ts Markdown 侧公共导出
|
||
styles/
|
||
echarts.css 图表、标题和错误占位样式
|
||
tests/ 解析、安全、语义、插件和真实 SVG 测试
|
||
```
|
||
|
||
## 基本语法
|
||
|
||
````markdown
|
||
```echarts
|
||
height: 80mm
|
||
caption: 年度收入
|
||
option:
|
||
xAxis:
|
||
type: category
|
||
data: [2023, 2024]
|
||
yAxis:
|
||
type: value
|
||
series:
|
||
- type: bar
|
||
data: [120, 180]
|
||
```
|
||
````
|
||
|
||
围栏内容必须使用 YAML,并包含一个 ECharts `option` 对象。YAML
|
||
会先转换为安全的纯数据对象,再交给 ECharts;不能使用函数、
|
||
YAML 锚点、别名或自定义标签。
|
||
|
||
## 围栏字段
|
||
|
||
| 字段 | 类型 | 默认值 | 说明 |
|
||
| --- | --- | --- | --- |
|
||
| `version` | `1` | `1` | 围栏协议版本 |
|
||
| `height` | CSS 长度 | `80mm` | 支持 `mm`、`cm`、`in`、`px` |
|
||
| `aspectRatio` | 正数 | 无 | 与 `height` 二选一;指定后不补默认高度 |
|
||
| `theme` | `document`、`light`、`dark` | `document` | ECharts 主题 |
|
||
| `caption` | 字符串 | 无 | 图表标题说明 |
|
||
| `option` | 对象 | 必填 | 经过安全和语义验证的 ECharts 配置 |
|
||
|
||
## 数据来源
|
||
|
||
每个系列必须从以下任一位置获得有效数据:
|
||
|
||
- 非空的 `series[].data`;
|
||
- 非空的 `dataset.source`;
|
||
- 引用具有有效上游数据源的内置 `filter` 或 `sort` transform。
|
||
|
||
只有表头的二维数组、所有列均为空的对象以及空数组不属于有效
|
||
数据。使用 `dataset` 时保留 ECharts 的默认维度映射,也可以通过
|
||
`series.encode` 显式声明映射。
|
||
|
||
```yaml
|
||
option:
|
||
dataset:
|
||
source:
|
||
- [year, sales]
|
||
- [2025, 120]
|
||
- [2026, 180]
|
||
series:
|
||
- type: line
|
||
encode:
|
||
x: year
|
||
y: sales
|
||
```
|
||
|
||
## 系列验证规则
|
||
|
||
当前支持以下系列:
|
||
|
||
| 系列 | 主要规则 |
|
||
| --- | --- |
|
||
| `line` | 需要有效数据;支持直角坐标系和极坐标系 |
|
||
| `bar` | 需要有效数据;支持直角坐标系和极坐标系 |
|
||
| `scatter` | 直接数据至少包含两个坐标值 |
|
||
| `pie` | 直接数据项的 `value` 必须是有限数值 |
|
||
| `radar` | 必须提供非空 `radar.indicator`;数据维数不得少于指标数 |
|
||
| `heatmap` | 直角坐标数据为 `[x, y, value]`;日历数据为 `[date, value]` |
|
||
| `boxplot` | 直接数据为 `[min, Q1, median, Q3, max]` |
|
||
| `candlestick` | 直接数据为 `[open, close, lowest, highest]` |
|
||
| `tree` | 必须提供非空层级 `data`;递归校验 `children` |
|
||
| `treemap` | 必须提供非空层级 `data`;叶节点必须具有有限数值 |
|
||
| `sunburst` | 必须提供非空层级 `data`;叶节点必须具有有限数值 |
|
||
| `graph` | 必须提供 `data` 或 `nodes`;校验边引用;缺少坐标时默认 `force` 布局 |
|
||
| `sankey` | 必须提供节点和关系边;边权重必须是非负有限数值 |
|
||
| `chord` | 使用 ECharts 6 和弦图;必须提供节点、关系边和非负权重 |
|
||
| `funnel` | 数据项必须是有限数值,或使用有效 `dataset` |
|
||
| `gauge` | 数据项必须是有限数值,或使用有效 `dataset` |
|
||
| `pictorialBar` | 需要有效数据和直角坐标系;允许安全的 `path://` 内嵌矢量符号 |
|
||
|
||
直角坐标系列缺少 `xAxis` 或 `yAxis` 时会补充空对象,让 ECharts
|
||
使用自身的轴默认值。极坐标、雷达和日历坐标系表达了明确语义,
|
||
因此不会猜测配置,缺少对应组件时会报告错误。
|
||
|
||
使用 `dataset` 时,验证器只验证数据源是否存在,不重复实现
|
||
ECharts 的维度推断和 `encode` 映射规则。
|
||
|
||
## 第二阶段系列示例
|
||
|
||
### 层级图
|
||
|
||
```yaml
|
||
height: 60mm
|
||
option:
|
||
series:
|
||
- type: treemap
|
||
nodeClick: false
|
||
breadcrumb:
|
||
show: false
|
||
label:
|
||
show: true
|
||
formatter: "{b}"
|
||
color: "#1f2937"
|
||
fontWeight: bold
|
||
backgroundColor: "rgba(255, 255, 255, 0.88)"
|
||
borderRadius: 3
|
||
padding: [3, 6]
|
||
itemStyle:
|
||
borderColor: "#ffffff"
|
||
borderWidth: 2
|
||
gapWidth: 2
|
||
levels:
|
||
- itemStyle:
|
||
borderWidth: 0
|
||
gapWidth: 2
|
||
- upperLabel:
|
||
show: true
|
||
height: 28
|
||
color: "#1f2937"
|
||
fontWeight: bold
|
||
backgroundColor: "rgba(255, 255, 255, 0.88)"
|
||
padding: [3, 6]
|
||
itemStyle:
|
||
borderColor: "#ffffff"
|
||
borderWidth: 2
|
||
gapWidth: 2
|
||
- label:
|
||
show: true
|
||
formatter: "{b}"
|
||
color: "#1f2937"
|
||
fontWeight: bold
|
||
backgroundColor: "rgba(255, 255, 255, 0.88)"
|
||
borderRadius: 3
|
||
padding: [3, 6]
|
||
itemStyle:
|
||
borderColor: "#ffffff"
|
||
borderWidth: 2
|
||
gapWidth: 2
|
||
data:
|
||
- name: 渲染
|
||
children:
|
||
- { name: Markdown, value: 32 }
|
||
- { name: ECharts, value: 24 }
|
||
- name: 导出
|
||
children:
|
||
- { name: PDF, value: 28 }
|
||
```
|
||
|
||
`tree`、`treemap` 和 `sunburst` 共用递归 `children` 结构。
|
||
`treemap` 与 `sunburst` 的叶节点需要数值;父节点可以省略数值,
|
||
由 ECharts 根据子节点汇总。
|
||
|
||
### 关系图
|
||
|
||
```yaml
|
||
height: 60mm
|
||
option:
|
||
series:
|
||
- type: sankey
|
||
data:
|
||
- { name: Markdown }
|
||
- { name: HTML }
|
||
- { name: PDF }
|
||
links:
|
||
- { source: Markdown, target: HTML, value: 100 }
|
||
- { source: HTML, target: PDF, value: 40 }
|
||
```
|
||
|
||
`graph` 的关系边可以省略;`sankey` 和 ECharts 6 `chord` 必须
|
||
具有非空 `links` 或 `edges`。`source`、`target` 必须引用节点
|
||
名称、ID 或数组索引。
|
||
|
||
### 业务图表
|
||
|
||
```yaml
|
||
height: 60mm
|
||
option:
|
||
series:
|
||
- type: gauge
|
||
min: 0
|
||
max: 100
|
||
progress:
|
||
show: true
|
||
detail:
|
||
formatter: "{value}%"
|
||
data:
|
||
- { name: 完成率, value: 78 }
|
||
```
|
||
|
||
`funnel` 和 `gauge` 使用有限数值数据;`pictorialBar` 复用柱图
|
||
坐标系和数据规则,并允许不加载外部资源的 `path://` SVG 路径。
|
||
|
||
## 错误行为
|
||
|
||
配置错误不会中断整篇 Markdown。对应围栏会变成局部错误占位:
|
||
|
||
```html
|
||
<div class="md-echarts-error" role="alert">
|
||
ECharts 图表配置无效:option.series.0.data 必须是非空数组……
|
||
</div>
|
||
```
|
||
|
||
错误消息包含配置路径,便于用户或 AI Agent 定位并修正 YAML。
|
||
宿主还可以通过 `onError` 收集结构化错误。
|
||
|
||
```ts
|
||
markdown.use(markdownItECharts, {
|
||
onError(error, context) {
|
||
console.warn(error.code, error.message, context.tokenIndex);
|
||
}
|
||
});
|
||
```
|
||
|
||
## Markdown-it 接入
|
||
|
||
```ts
|
||
import MarkdownIt from "markdown-it";
|
||
import {
|
||
markdownItECharts
|
||
} from "@md-to-pdf/markdown-echarts";
|
||
|
||
const markdown = new MarkdownIt().use(markdownItECharts);
|
||
const html = markdown.render(source);
|
||
```
|
||
|
||
如果宿主还会对 HTML 做安全过滤,需要保留包生成的 `figure`、
|
||
`figcaption`、隐藏配置节点及 `data-echarts-*` 属性。
|
||
|
||
## 浏览器渲染
|
||
|
||
```ts
|
||
import {
|
||
renderEChartsBlocks
|
||
} from "@md-to-pdf/markdown-echarts/browser";
|
||
import "@md-to-pdf/markdown-echarts/styles.css";
|
||
|
||
const outcomes = await renderEChartsBlocks(document, {
|
||
outputMode: "inline-svg",
|
||
concurrency: 2,
|
||
timeoutMs: 5000
|
||
});
|
||
```
|
||
|
||
输出模式:
|
||
|
||
- `interactive`:保留 ECharts 实例和响应式尺寸监听;
|
||
- `inline-svg`:冻结为内联 SVG,适合打印和 PDF;
|
||
- `svg-image`:冻结为 SVG Data URL 图片。
|
||
|
||
PDF 输出前应等待所有图表、字体和图片完成,再开始分页或调用
|
||
Chromium `page.pdf()`。
|
||
|
||
浏览器引擎会强制关闭全局及各系列动画,Markdown 中的动画参数不会改变
|
||
静态 SVG、快速预览、精确预览或 PDF,避免输出停留在动画中间帧。
|
||
ECharts 围栏使用专用 `figure.md-echarts` DOM,不会套用普通代码围栏的
|
||
`pre.md-fences` 样式。
|
||
|
||
## 安全边界
|
||
|
||
默认策略会拒绝:
|
||
|
||
- `custom` 系列及 `renderItem`;
|
||
- JavaScript、HTTP、文件和 Data URL 外部资源;
|
||
- HTML formatter;
|
||
- 原型污染属性;
|
||
- 未允许的 toolbox 功能;
|
||
- 非内置数据转换;
|
||
- 超过节点数、深度、字符串、数组、系列、层级节点或关系边数量
|
||
上限的配置。
|
||
|
||
这些限制适用于不受信任的 Markdown。宿主如需扩大能力,应在明确
|
||
风险后扩展验证器,而不是直接执行 YAML 中的 JavaScript。
|
||
|
||
## 扩展系列验证
|
||
|
||
各系列规则注册在 `echartsSeriesValidators`。新增图形时需要同步:
|
||
|
||
1. 将系列类型加入协议支持列表;
|
||
2. 在浏览器引擎中注册对应 ECharts Chart;
|
||
3. 注册该系列的语义验证器;
|
||
4. 增加有效、缺少数据、错误数据结构和真实 SVG 渲染测试。
|
||
|
||
## 开发与验证
|
||
|
||
```powershell
|
||
npm run dev -w @md-to-pdf/markdown-echarts
|
||
npm run test -w @md-to-pdf/markdown-echarts
|
||
npm run typecheck -w @md-to-pdf/markdown-echarts
|
||
npm run build -w @md-to-pdf/markdown-echarts
|
||
```
|
||
|
||
参考:
|
||
|
||
- [ECharts 配置参考](https://echarts.apache.org/en/option.html)
|
||
- [ECharts Dataset](https://echarts.apache.org/handbook/en/concepts/dataset/)
|
||
- [ECharts Data Transform](https://echarts.apache.org/handbook/en/concepts/data-transform/)
|