- 新增可复用的 markdown-echarts 工作区,支持安全的 YAML 围栏、默认值、语义校验、错误占位和 SVG 渲染 - 将 ECharts 接入统一 Markdown、快速预览、精确预览与 PDF 链路,并复用 Mermaid 的不可跨页和超高图单页适配策略 - 默认示例加入 ECharts,增加源文本折叠、顶部文档状态、页码输入跳转与滚动同步 - 快速预览改用外层容器滚动,使滚动条与精确预览统一贴在预览区域右侧,并兼容显示缩放 - 完善 Docker 工作区复制、可配置 Compose 镜像、测试覆盖和进度文档
141 lines
3.6 KiB
TypeScript
141 lines
3.6 KiB
TypeScript
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);
|
|
}
|
|
};
|
|
}
|