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
@@ -0,0 +1,140 @@
import type MarkdownIt from "markdown-it";
import type { Options as MarkdownItOptions } from "markdown-it";
import type Renderer from "markdown-it/lib/renderer.mjs";
import type Token from "markdown-it/lib/token.mjs";
import { echartsDomContract } from "./dom-contract.js";
import {
ECHARTS_FENCE_VERSION,
EChartsFenceError,
type NormalizedEChartsFenceSpec
} from "./protocol.js";
import {
parseEChartsFence,
type ParseEChartsFenceOptions
} from "./parse-yaml.js";
export interface MarkdownItEChartsErrorContext {
source: string;
tokenIndex: number;
environment: unknown;
}
export interface MarkdownItEChartsOptions {
parse?: ParseEChartsFenceOptions;
onError?: (
error: EChartsFenceError,
context: MarkdownItEChartsErrorContext
) => void;
}
type FenceRenderer = (
tokens: Token[],
index: number,
options: MarkdownItOptions,
environment: unknown,
renderer: Renderer
) => string;
function renderError(markdown: MarkdownIt, error: EChartsFenceError) {
const message = markdown.utils.escapeHtml(error.message);
return `<div class="${echartsDomContract.errorClass}" role="alert">ECharts 图表配置无效:${message}</div>\n`;
}
function renderDefinition(
markdown: MarkdownIt,
definition: NormalizedEChartsFenceSpec
) {
const serialized = markdown.utils.escapeHtml(
JSON.stringify(definition)
);
const caption =
definition.caption === undefined
? ""
: `<figcaption>${markdown.utils.escapeHtml(definition.caption)}</figcaption>`;
const label = markdown.utils.escapeHtml(
definition.caption ?? "ECharts 图表"
);
return [
`<figure class="${echartsDomContract.figureClass}"`,
` ${echartsDomContract.pendingAttribute}="true"`,
` ${echartsDomContract.protocolAttribute}="${ECHARTS_FENCE_VERSION}">`,
`<div class="${echartsDomContract.hostClass}" role="img" aria-label="${label}"></div>`,
`<pre class="${echartsDomContract.definitionClass}" hidden="hidden">${serialized}</pre>`,
caption,
"</figure>\n"
].join("");
}
function renderDefaultFence(
defaultFenceRenderer: FenceRenderer | undefined,
tokens: Token[],
index: number,
options: MarkdownItOptions,
environment: unknown,
renderer: Renderer
) {
if (defaultFenceRenderer) {
return defaultFenceRenderer(
tokens,
index,
options,
environment,
renderer
);
}
return renderer.renderToken(tokens, index, options);
}
export function markdownItECharts(
markdown: MarkdownIt,
pluginOptions: MarkdownItEChartsOptions = {}
) {
const defaultFenceRenderer = markdown.renderer.rules.fence;
markdown.renderer.rules.fence = (
tokens,
index,
options,
environment,
renderer
) => {
const token = tokens[index];
const language = token?.info
.trim()
.split(/\s+/u)[0]
?.toLowerCase();
if (!token || language !== "echarts") {
return renderDefaultFence(
defaultFenceRenderer,
tokens,
index,
options,
environment,
renderer
);
}
try {
return renderDefinition(
markdown,
parseEChartsFence(token.content, pluginOptions.parse)
);
} catch (error) {
const normalizedError =
error instanceof EChartsFenceError
? error
: new EChartsFenceError(
"INVALID_YAML",
error instanceof Error ? error.message : "未知错误",
{ cause: error }
);
pluginOptions.onError?.(normalizedError, {
source: token.content,
tokenIndex: index,
environment
});
return renderError(markdown, normalizedError);
}
};
}